Крис Маринос
Хотя F# совсем недавно появился в семействе продуктов Visual Studio, он уже помог многим .NET-разработчикам раскрыть всю мощь функционального программирования. Язык F# завоевал прочную репутацию в таких областях, как, например, параллельное и асинхронное программирование, обработка данных и финансовое моделирование. Однако это не означает, что F# является узкоспециализированным языком; он отлично подходит и для решения рутинных задач.
В этой статье вы узнаете, как с помощью F# создавать приложения Model-View-ViewModel (MVVM) с использованием как Silverlight, так и Windows Presentation Foundation (WPF). Кроме того, вы увидите, как те же концепции, которые делают F# великолепным языком для упрощения сложнейших алгоритмов, можно применять и для сокращения стереотипного кода в моделях представлений (view models). Я также расскажу о том, как использовать широко известные асинхронные рабочие процессы F# в GUI. Наконец, мы рассмотрим два распространенных подхода к структуризации MVVM-приложений в F#, их сильные и слабые стороны. К концу этой статьи вы поймете, что F# - не только инструмент для решения специализированных задач; его можно использовать в любых приложениях, чтобы сделать их код более простым в чтении, написании и сопровождении.
Сокращение стереотипного кода
Возможно, вы подумали, что F# - странный язык для работы с GUI-приложениями. В конце концов, функциональные языки ослабляют побочные эффекты, а инфраструктуры наподобие Silverlight и WPF перегружены такими эффектами. Может быть, вы считаете, что модели представлений не содержат достаточно сложной логики, чтобы заметить преимущества переключения на функциональный стиль. Или полагаете, что функциональное программирование требует смены парадигмы, что затруднит работу с проектировочными шаблонами вроде MVVM. Истина в том, что использовать F# для конструирования ViewModel и Model столь же легко, как и в C#. Кроме того, на примере моделей представлений можно очень наглядно убедиться, насколько F# сокращает стереотипный код. Вероятно, вас удивит, насколько эффективно F# улучшает "отношение сигнал/шум" в вашем коде, если вы привыкли к программированию на C#.
На рис. 1 приведен код на F# для простой модели видеоклипа с полями, в которые записываются название, жанр и необязательный рейтинг. Если вы не знакомы с типами option в F#, то можете считать их более мощными и выразительными аналогами C#-типов, допускающих значения null (nullables). Тип option позволяет естественным образом выразить тот факт, что в модели видеоклипа рейтинг может как присутствовать, так и отсутствовать, однако это способно вызвать проблемы при связывании с данными. Например, попытка получить значение типа option, установленного в None (эквивалент null для nullable-типов), приведет к исключению. Кроме того, вы наверняка захотите скрывать элемент управления, связанный с Rating, если тот установлен в None. Увы, если WPF или Silverlight обнаруживают исключение при попытке получить значение Rating, связывание может не сработать. Это простой пример того, где вам потребуется модель представления для добавления дополнительной логики вокруг рейтингов.
Рис. 1. Простая модель видеоклипа
type Movie = {
Name: string
Genre: string
Rating: int option
}
На рис. 2 показана модель представления с примером такой дополнительной логики отображения. Если рейтинг есть, его значение передается представлению; в ином случае рейтингу присваивается значение по умолчанию, равное 0. Эта простая логика предотвращает исключение, если Rating установлен в None. Модель представления также содержит второе свойство для обработки связывания с визуальными элементами. Оно просто возвращает true, если Rating равен Some, и false, если он - None. Логика в этой модели представления проста, но не она - суть примера. Вместо нее посмотрите на то, как четко F# выражает определение модели представления. В C# модели представлений зачастую переполнены стереотипным кодом, который сильно мешает пониманию логики представления, - счастлив разве что компилятор.
Рис. 2. Модель представления для видеоролика с логикой отображения рейтингов
type MovieViewModel(movie:Movie) =
member this.Name = movie.Name
member this.Genre = movie.Genre
member this.Rating =
match movie.Rating with
/ Some x -> x
/ None -> 0
member this.HasRating = movie.Rating.IsSome
На рис. 3 показана та же модель представления, написанная на C#. Увеличение стереотипного кода просто драматическое. На C# нужно написать примерно в 4 раза больше строк кода, чем на F#. Большая часть этого увеличения строк кода связана с фигурными скобками, но даже значимые элементы вроде аннотаций типов, выражений return и модификаторов уровня доступа мешают восприятию логики, которую должна инкапсулировать модель представления. F# уменьшает этот шум и переводит в фокус код самой логики. Иногда к функциональному программированию предъявляют претензии за чрезмерную краткость и сложность в чтении кода, но в этом примере совершенно очевидно, что F# не жертвует ясностью во имя краткости.
Рис. 3. Та же модель представления, написанная на C#
class MovieViewModelCSharp
{
Movie movie;
public MovieViewModelCSharp(Movie movie)
{
this.movie = movie;
}
public string Name
{
get { return movie.Name; }
}
public string Genre
{
get { return movie.Genre; }
}
public int Rating
{
get
{
if(OptionModule.IsSome(movie.Rating))
{
return movie.Rating.Value;
}
else
{
return 0;
}
}
}
public bool HasRating
{
get
{
return OptionModule.IsSome(movie.Rating);
}
}
}
Предыдущий пример иллюстрирует одно преимущество F# в написании моделей представлений, но F# также позволяет устранить другую распространенную проблему с этими моделями в MVVM-приложениях. Допустим, ваша предметная область сменилась и вам нужно обновить свою модель; Genre теперь является списком тегов, а не одной строкой. В коде модели достаточно заменить строку:
Genre: string
на эту:
Genre: string list
Однако, поскольку представление взаимодействовало с этим свойством через модель представления, возвращаемый тип в модели тоже надо изменить. В C# это требует внесения изменений вручную, а в F# осуществляется автоматически благодаря логическому распознаванию типов (type inference). Это может показаться удивительным, если вы не привыкли к логическому распознаванию типов в F#, но данное поведение - как раз то, что нужно. Genre не требует никакой логики отображения, поэтому модель представления просто передает это поле представлению безо всякой модификации. Иначе говоря, вам не нужно заботиться о возвращаемом типе свойства в модели представления (ViewModel), если только оно соответствует возвращаемому типу свойства в модели (Model). Как раз это и заявляет код на F#. Учтите, что F# по-прежнему является статически типизируемым языком, поэтому любое неправильное применение поля в ViewModel или Model (с сопутствующим XAML) даст ошибку при компиляции.
Использование существующих наработок
Модели представлений на рис. 1 и 2 поддерживали только одностороннее связывание, так как они отвечали лишь за добавление логики отображения в модель. Эти простые модели представлений полезны для демонстрации способности F# сокращать стереотипный (или, как я называю его, церемониальный) код, но обычно модели представлений должны поддерживать двухстороннее связывание реализацией INotifyPropertyChanged для изменяемых свойств. MVVM-приложения на C# обычно содержат базовый класс модели представления, что упрощает реализацию INotifyPropertyChanged и выполнение других обязанностей. Возможно, вы сейчас волнуетесь насчет того, что вам придется заново реализовать это поведение в F#, но F# позволяет повторно использовать существующие базовые классы моделей представлений на C# без их переписывания.
На рис. 4 показано, как в F# используется класс ViewModelBase. Это базовый класс, написанный на C# Брайеном Генисио (Brian Genisio) (houseofbilz.com), который я предпочитаю применять во всех своих моделях представлений на C# и F#. На рис. 4 этот базовый класс предоставляет функции base.Get и base.Set, используемые для реализации INotifyPropertyChange. ViewModelBase также поддерживает генерацию команд на основе соглашения с применением средств динамического программирования в C#. Оба средства без проблем работают с модель представления в F#, так как F# разработан с расчетом на простоту взаимодействия с другими .NET-языками. Подробнее о том, как использовать ViewModelBase, см. на странице viewmodelsupport.codeplex.com.
Рис. 4. Наследование от C#-класса ViewModelBase
type MainWindowViewModel() = inherit ViewModelBase()
member this.Movies
with get() =
base.Get<ObservableCollection<MovieViewModel>>("Movies")
and set(value) = base.Set("Movies", value)
Переходим к асинхронности
Поддержка асинхронных и отменяемых операций - еще одно общее требование для ViewModel и Model. Выполнение этого требования традиционными способами может существенно усложнить ваше приложение, но F# предлагает мощные средства асинхронного программирования, которые упрощают эту задачу. На рис. 5 показан синхронный вызов веб-сервиса для получения данных по видеороликам. Ответ от сервера разбирается в список моделей видеороликов. Модели в этом списке затем проецируются на модели представлений и добавляются в ObservableCollection. Этот набор связан с элементом управления в представлении для отображения результатов пользователю.
Рис. 5. Простой веб-запрос для обработки видеороликов
member this.GetMovies() =
this.Movies <- new ObservableCollection<MovieViewModel>()
let response = webClient.DownloadString(movieDataUri)
let movies = parseMovies response
movies
/> Seq.map (fun m -> new MovieViewModel(m))
/> Seq.iter this.Movies.Add
Преобразование этого кода для асинхронного выполнения потребовало бы полного пересмотра потока управления при использовании традиционных асинхронных библиотек. Вам пришлось бы разбить код на отдельные методы обратного вызова для каждого асинхронного вызова. Это усложнило бы код, затруднило бы его понимание и сильно увеличило бы издержки на сопровождение. В модели F# этих проблем нет. В F# асинхронное поведение можно реализовать внесением небольших изменений, не влияющих на структуру кода, как показано на рис. 6.
Рис. 6. Асинхронный веб-запрос для обработки видеороликов
member this.GetMovies() =
this.Movies <- new ObservableCollection<MovieViewModel>()
let task = async {
let! response = webClient.AsyncDownloadString(movieDataUri)
let movies = parseMovies response
movies
/> Seq.map (fun m -> new MovieViewModel(m))
/> Seq.iter this.Movies.Add
}
Async.StartImmediate(task)
В примере на рис. 6 показан тот же код, выполняемый асинхронно. Этот код, вызывающий веб-сервис и обновляющий список результатов, заключен в блок async. Ключевое слово let! внутри этого блока сообщает F# выполнять выражение асинхронно. В этом примере let! указывает веб-клиенту асинхронно выдать веб-запрос. F# предоставляет метод AsyncDownloadString как расширение WebClient, что упрощает весь процесс. Последнее изменение - вызов Async.StartImmediate, которая запускает блок async в текущем потоке. Выполнение в GUI-потоке позволяет избежать запутанных исключений, которые возникают, если вы пытаетесь обновлять GUI из фонового потока, а асинхронное поведение гарантирует, что GUI не зависнет на время веб-запроса.
Изменения для асинхронного выполнения не потребовали много дополнительного кода и, что не менее важно, модификации структуры кода. Когда вы впервые пишете код, вам незачем беспокоиться о такой его структуризации, которая в будущем позволила бы выполнять его асинхронно. При создании прототипа вы свободно можете писать синхронный код, а затем легко преобразовать его в асинхронных, если появится такая необходимость. Эта гибкость экономит многие часы в разработке, да и ваши клиенты будут куда счастливее, когда вы сможете так быстро реагировать на изменениях в их потребностях.
Этот стиль асинхронного программирования должен быть знаком вам, если следили за последними разработками в области C#. Все дело в том, что обновления async, вводимые в C#, в основном опираются на модель F#. Async в C# доступно через CTP-версию (bit.ly/qqygW9), но асинхронные средства в F# есть уже сегодня. Асинхронные рабочие процессы были доступны в F# с момента первого выпуска этого языка, так что они являются отличным примером тому, почему изучение F# сделает вас более квалифицированным разработчиком на C#.
Хотя модели C# и F# похожи, некоторые различия весьма заметны, и поддержка отмены - главная из них. Код на рис. 7 добавляет в функцию GetMovies поддержку отмены. И вновь требуются минимальные изменения. Чтобы рабочий процесс поддерживал отмену, нужно создать CancellationTokenSource и передать его маркер отмены в функцию Async.StartImmediate. На рис. 7 показан кое-какой дополнительный код настройки в верхней части функции GetMovies, чтобы отменять любую незавершенную операцию и избежать обновления наблюдаемого набора (observable collection) более чем раз. Новый CancellationTokenSource также выдается при каждом вызове функции, гарантируя, что при каждом запуске работий процесс получает уникальный CancellationToken.
Рис. 7. Отмена в F#
let mutable cancellationSource = new CancellationTokenSource()
member this.GetMovies() =
this.Movies <- new ObservableCollection<MovieViewModel>()
cancellationSource.Cancel()
cancellationSource <- new CancellationTokenSource()
let task = async {
let! response = webClient.AsyncDownloadString(movieDataUri)
let movies = parseMovies response
movies
/> Seq.map (fun m -> new MovieViewModel(m))
/> Seq.iter this.Movies.Add
}
Async.StartImmediate(task, cancellationSource.Token)
В модели C# для поддержки отмены вы должны вручную передать CancellationToken по цепочке вызовов. Это интрузивное изменение, которое потенциально может потребовать введения дополнительного аргумента в сигнатуры многих функций. Вам также может понадобиться вручную проверять CancellationToken, чтобы выяснить, запрошена ли отмена. В модели F# работы куда меньше. Всякий раз, когда асинхронный рабочий процесс встречает let!, он проверяет CancellationToken, полученный при вызове StartImmediate. Если маркер действителен, рабочий процесс выполняется как обычно. Если маркер недействителен, операция не будет выполняться, равно как и оставшаяся часть рабочего процесса. Неявная обработка отмены - хорошая штука, но вы не ограничены этим. Если вам нужно вручную опрашивать CancellationToken для какого-либо рабочего процесса, вы можете обращаться к нему через свойство Async.CancellationToken:
let! token = Async.CancellationToken
Структуризация MVVM-приложений в F#
Теперь, когда вы увидели несколько практических способов, с помощью которых F# позволяет улучшить ваши ViewModel и Model, я поясню, как интегрировать F#-код в приложения Silverlight и WPF. В C# есть много способов структуризации MVVM-приложений; то же самое относится и к F#. В этой статье мы рассмотрим два из этих способов: использование только F# и метод полиглота, при котором для представлений применяется C#, а для ViewModel и Model - F#. Я предпочитаю второй способ по нескольким причинам. Во-первых, это подход, рекомендованный группой разработчиков языка F#. Во-вторых, инструментальная поддержка WPF и Silverlight в C# гораздо обширнее, чем в F#. Наконец, этот подход позволяет включать F#-код в существующие приложения и предоставляет способ с малым риском опробовать применение F# в MVVM-приложениях.
Подход "только F#" интересен, потому что дает возможность писать приложение на одном языке, но накладывает ряд ограничений. Позже я покажу, как преодолеть некоторые из них, но считаю, что овчинка не стоит выделки в любых приложениях, кроме очень небольших. Подход полиглота не дает вам использовать F# в коде представления, но тщательно продуманные MVVM-приложения должны содержать в представлениях минимум логики. Более того, C# - отличный язык для написания (при необходимости) логики представления, так как по своей природе она императивна и обременена множеством побочных эффектов.
Подход полиглота
При таком подходе создать MVVM-приложение очень легко. Сначала создайте новый проект WPF на C#, используя шаблон проекта WPF Application. Этот проект отвечает за любые представления и отделенный код (codebehind), который нужен вам в приложении. Затем добавьте в решение новый проект библиотеки на F#, который будет содержать модели представлений, модели и любой код, не относящийся к представлениям. Наконец, добавьте ссылку на проект F#-библиотеки из проекта C# WPF. Вот и все, что нужно для подготовки к работе с использованием подхода полиглота.
F# рассчитан на взаимодействие с любым .NET-языком, а это означает, что можете подключить модели представлений на F# к представлениям на C# любым методом, традиционным для моделей представлений на C#. Я покажу пример, где для простоты используется отделенный код. Сначала создайте простую модель представления, переименовав Module1.fs в проекте на F# в MainWindowViewModel.fs. Заполните модель представления кодом с рис. 8. Подключите модель представления на F# к представлению на C# с помощью кода, приведенного на рис. 9. Если бы вы не знали, что модель представления написана на F#, то не отличили бы ее от модели представления на C#.
Рис. 8. Пример модели представления на F#
namespace Core.ViewModels
type MainWindowViewModel() =
member this.Text = "hello world!"
Рис. 9. Подключение модели представления на F# к представлению на C#
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
this.DataContext = new MainWindowViewModel();
}
Добавьте текстовое поле к MainWindow.xaml и установите Binding в Text. И вновь все ведет себя так, будто приложение написано исключительно на C#. Проверьте привязку, запустив приложение: вы должны увидеть стандартное приветствие "hello world!".
Подход "только F#"
Я уже говорил, что предпочитаю подход полиглота за его простоту и гибкость. Но расскажу и о другом варианте - подходе с использованием только F#. Visual Studio 2010 поставляется с шаблоном для создания библиотек Silverlight на F#, но не включает никаких шаблонов для создания приложений WPF или Silverlight на F#. К счастью, в сети есть несколько отличных шаблонов для этого. Советую начать с шаблонов, созданных Дэниелом Молом (Daniel Mohl) (bloggemdano.blogspot.com), так как они включают приложения-примеры, позволяющие понять структуру приложений на основе этих шаблонов. Чисто в учебных целях я покажу, как с нуля создать WPF-приложение на F#, но рекомендую на практике использовать один из упомянутых шаблонов.
Создайте новый проект приложения на F# с именем FSharpOnly. Дождитесь, пока закончится создание проекта, откройте свойства проекта и измените тип вывода на Windows Application. Теперь добавьте ссылки на PresentationCore, PresentationFramework, PresentationUI, System.Xaml и WindowsBase. Добавьте в проект файлы App.xaml и MainWindow.xaml и задайте Build Action в каждом из этих файлов как Resource. Заметьте, что по умолчанию в F# нет элемента шаблона для генерации XAML-файлов, но вы можете использовать универсальный шаблон текстового документа с расширением .xaml. Заполните XAML-файлы кодом с рис. 10 и 11 соответственно. App.xaml и MainWindow.xaml выполняют те же функции, что и в стандартном WPF-приложении на C#.
Рис. 10. Пример файла App.xaml
<Application
xmlns="http://schemas.microsoft.com/winfx/2006/
xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="assembly=FSharpOnly"
StartupUri="MainWindow.xaml">
</Application>
Рис. 11. Пример файла MainWindow.xaml
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="assembly=FSharpOnly"
Title="Sample F# WPF Application Written Only in F#"
Height="100"
Width="100" >
<Grid>
<TextBlock>Hello World!</TextBlock>
</Grid>
</Window>
Теперь добавьте вProgram.fs код с рис. 12. Этот код отвечает за загрузку файла App.xaml и запуск приложения.
Рис. 12. Пример Program.fs
open System
open System.Windows
open System.Windows.Controls
open System.Windows.Markup
[<STAThread>]
[<EntryPoint>]
let main(_) =
let application = Application.LoadComponent(new Uri(
"App.xaml", UriKind.Relative)) :?> Application
application.Run()
Запустив приложение, вы увидите приветствие "Hello World!". С этого момента вы можете подключать свои ViewModel к Model любым способом, который вам нравится, - так же, как и при подходе полиглота.
Одна из проблем, с которой вы можете столкнуться, используя подход "только F#", - статические ресурсы. App.xaml обычно определяется как ApplicationDefinition в проектах WPF на C#, но при подходе "только F#" он определен как Resource. Это приводит к тому, что в период выполнение разрешение ресурсов, определенных в App.xaml, заканчивается неудачей, если вы используете их из других XAML-файлов. Обойти эту проблему несложно: смените Build Action для файла App.xaml на ApplicationDefinition и перезагрузите дизайнер. Это заставит дизайнер распознать ресурсы в App.xaml и должным образом загрузить ваши представления. Перед компиляцией приложения не забудьте вернуть App.xaml в Resource, а иначе вы получите ошибку при компиляции.
Отделенный код при подходе "только F#" тоже работает по-другому. F# не поддерживает частичные классы, поэтому XAML-файлы нельзя сопоставить с файлом отделенного кода .fs по аналогии с тем, как это делается в C#. Хорошая практика - по возможности избегать отделенного кода в MVVM-приложениях, но иногда отделенный код является самым эффективным способом решения какой-либо задачи. Обойти недостатки поддержки традиционного отделенного кода в F# можно несколькими способами. Самый прямолинейный - просто конструировать все представление на F#. Хотя этот подход несложен, он может оказаться весьма громоздким, поскольку вы потеряете декларативность XAML. Другой подход - при конструировании приложения подключаться к своим визуальным элементам. Пример этого подхода показан на рис.
Рис. 13. Main.fs, модифицированный для подключения к UI-элементам
let initialize (mainWindow:Window) =
let button = mainWindow.FindName("SampleButton") :?> Button
let text = mainWindow.FindName("SampleText") :?> TextBlock
button.Click
/> Event.add (fun _ -> text.Text <- "I've been clicked!")
[<STAThread>]
[<EntryPoint>]
let main(_) =
let application = Application.LoadComponent(new Uri(
"App.xaml", UriKind.Relative)) :?> Application
// Здесь подключаемся к UI-элементам
application.Activated
/> Event.add (fun _ -> initialize application.MainWindow)
application.Run()
Отсутствие частичных классов в F# также затрудняет работу с пользовательскими элементами управления (user controls). Создать их в XAML легко, но ссылаться на них из других XAML-файлов нельзя, потому что в сборке нет определения частичного класса. Чтобы обойти эту проблему, можно создать класс XamlLoader, как показано на рис. 14.
Рис. 14. Класс XamlLoader для создания пользовательских элементов управления в F#
type XamlLoader() =
inherit UserControl()
static let OnXamlPathChanged(d:DependencyObject)
(e:DependencyPropertyChangedEventArgs) =
let x = e.NewValue :?> string
let control = d :?> XamlLoader
let stream = Application.GetResourceStream(new Uri(
x, UriKind.Relative)).Stream
let children = XamlReader.Load(stream)
control.AddChild(children)
static let XamlPathProperty =
DependencyProperty.Register("XamlPath", typeof<string>,
typeof<XamlLoader>, new PropertyMetadata(
new PropertyChangedCallback(OnXamlPathChanged)))
member this.XamlPath
with get() =
this.GetValue(XamlPathProperty) :?> string
and set(x:string) =
this.SetValue(XamlPathProperty, x)
member this.AddChild child =
base.AddChild(child)
Этот класс позволяет задать путь к XAML-файлу, используя свойство зависимости (dependency property). Когда вы задаете это свойство, загрузчик разбирает XAML из файла и добавляет элементы управления, определенные в файле, как свои дочерние элементы. В XAML это используется так:
<local:XamlLoader XamlPath="UserControl.xaml" />
Решение на основе XamlLoader позволяет создавать пользовательские элементы управления, не прибегая к подходу полиглота, но это еще одно препятствие, из-за которого создавать представления лучше на C#.
Заключительные соображения
Посмотрев F# в действии, вы поняли, что это язык для написания практических приложений. Он уменьшает объем стереотипного кода, упрощая чтение и сопровождение моделей и моделей представлений. Вы увидели, как использовать такие средства, как асинхронное программирование, для быстрого и гибкого решения сложных задач. Наконец, я дал обзор двух основных способов структуризации MVVM-приложений в F#, а также их плюсов и минусов. Теперь вы готовы раскрыть всю мощь функционального программирования в своих приложениях Silverlight и WPF.
Когда вы будете в следующий раз писать приложение Silverlight или WPF, попытайтесь сделать это на F#. Попробуйте написать отдельные части существующего приложения на F#, используя подход полиглота. Вы быстро заметите, как резко уменьшается объем кода, который вам приходится писать и сопровождать. Засучите рукава и опробуйте подход "только F#"; вы наверняка узнаете нечто новое о WPF, Silverlight или F#. Независимо от того, как вы решите действовать дальше, однажды почувствовав удовольствие от программирования на F#, вы уже будете смотреть на C# по-другому.
Ссылки по теме