MugenMvvmToolkit — кроссплатформенный MVVM фреймворк

MugenMvvmToolkit Введение Паттерн MVVM хорошо известен, о нем написано много статей, наверное каждый NET-разработчик сталкивался или слышал об этом паттерне. Цель этой статьи — рассказать о собственной реализации этого паттерна.MugenMvvmToolkit — является кроссплатформенной реализацией паттерна MVVM и на текущий момент поддерживает следующие платформы: WinForms WPF Silverlight 5 Silverlight for WP7.1, WP8, WP8.1 Xamarin.Android Xamarin.iOS Xamarin.Forms WinRT XAML framework for Windows 8 Store apps Data binding (привязка данных) Знакомство с проектом хотелось бы начать с элемента, без которого MVVM не может существовать — это привязка данных (Data Binding). Именно механизм привязки данных позволяет четко разделить абстракции View и ViewModel между собой.Разработчики приложений для платформ WPF, Silverlight, Windows Store и Windows Phone хорошо знакомы со стандартной реализацией механизма Binding. Это мощная система, покрывающая все основные задачи. Однако она имеет ряд недостатков, которые и подтолкнули к созданию собственной реализации Binding. Ниже приведены наиболее существенные, на мой взгляд, недостатки: Если работать лишь на одной платформе, можно смириться с этими недостатками и применять различные «обходные решения». Но т.к. проект предназначен для многих платформ, некоторые из которых не имеют даже стандартных Binding, было принято решение создать свою собственную реализацию, обладающую одинаковыми возможностями на всех платформах.В результате получилась реализация Binding со следующими возможностями: Расширяемость. Парсер при создании Binding строит синтаксическое дерево, и это позволяет легко его расширять без манипуляций с текстом. Структура очень похожа на деревья выражений в C#. Поддержка синтаксиса C#. Binding поддерживает все основные операторы (?, ?:, +, -, *, /, %, , ==, !=, <, >, <=, >=, &&(and), ||(or), |, &, !, ~), приоритет операций учитывается в соответствии со стандартом языка C#. Поддерживаются лямбда-выражения, вывод обобщенных типов на основании значений, вызов методов, вызов методов расширения, Linq.Пример синтаксиса: TargetPath SourcePath, Mode=TwoWay, Validate=trueTargetPath — путь для Binding из контрола. SourcePath — путь для Binding из источника данных или выражение на языке C#, можно использовать несколько путей. Mode, Validate — дополнительные параметры для Binding, например Mode, Fallback, Delay и т.д. Ключевые слова:$self — возвращает текущий контрол на который установлен Binding, аналог {RelativeSource Self}. $root — возвращает текущий корневой элемент для контрола на который установлен Binding. $context — возвращает текущий DataContext для Binding, аналог {RelativeSource Self, Path=DataContext}. $args — возвращает текущий параметр EventArgs, может быть использовано только если TargetPath указывает на событие. Примеры Text Property, Mode=TwoWay, Validate=True Text Items.First (x => x == Name).Value + Values.Select (x => x.Value).First (x => x == Name), Fallback=«empty» Text $string.Format ('{0} {1}', Prop1, Prop2), Delay=100 Text $string.Join ($Environment.NewLine, $GetErrors ()), TargetDelay=1000 Text Property.MyCustomMethod () Text Prop1? Prop2 Text $CustomMethod (Prop1, Prop2, «string value») Text Prop1 == «test» ? Prop2: «value» Поддержка Binding на события контрола с доступом к параметру EventArgs, используя ключевое слово $args.Примеры TextChanged EventMethod ($args.UndoAction) TextChanged EventMethodMultiParams (Text, $args.UndoAction) Поддержка валидации. Валидация обеспечивается стандартным интерфейсом INotifyDataErrorInfo. На каждой платформе будет показано сообщение об ошибке.Примеры Text Property, Mode=TwoWay, ValidatesOnNotifyDataErrors=True Text Property, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True Text Property, Mode=TwoWay, Validate=True //эквивалентно ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True Расширенный Binding на команды. Если Binding устанавливается на команду, можно определить, как будет реагировать контрол на «доступность» команды, команда может делать не активным контрол (Enabled = false) в случае если нельзя выполнить команду.Примеры Click Command, ToggleEnabledState=false //не изменяет состояние контрола Click Command, ToggleEnabledState=true //изменяет состояние контрола Расширенная поддержка валидации. Встроенный метод $GetErrors () вернет ошибки валидации всей формы по всем свойствам или ошибки для конкретных свойств. Метод бывает полезным, когда есть необходимость показать пользователю ошибки на форме.Примеры Text $GetErrors (Property).FirstOrDefault () Text $string.Join ($Environment.NewLine, $GetErrors ()) //Суммирует все ошибки в одну строку использую новую строку, как разделитель. Относительный Binding. Binding можно установить на текущий контрол или на любой другой внутри дерева визуальных контролов (аналог свойства RelativeSource для XAML платформ).Вспомогательные методы:$Element (ElementName) — ищет элемент с именем ElementName. $Relative (Type), $Relative (Type, 1) — ищет среди родительских элементов контрол с типом Type и (при необходимости) с учетом уровня родительского элемента (второй параметр). $self — возвращает текущий элемент на который установлен Binding. Примеры Text $Relative (Window).Title Text $self.ActualWidth Text $Element (NamedSlider).Value Поддержка присоединяемых свойств, событий и методов. Позволяет легко расширить любой тип. Например, в WinForms у DataGridView нет свойтсва SelectedItem, но мы легко можем его добавить, используя присоединяемое свойство: Пример var member = AttachedBindingMember.CreateMember(«SelectedItem», (info, view) => { var row = view.CurrentRow; if (row == null) return null; return row.DataBoundItem; }, (info, view, item) => { view.ClearSelection (); if (item == null) return; for (int i = 0; i < view.Rows.Count; i++) { if (Equals(view.Rows[i].DataBoundItem, item)) { var row = view.Rows[i]; row.Selected = true; } } }, "CurrentCellChanged"); //CurrentCellChanged - событие в DataGridView, которое отвечает за изменение свойства. //Регистрация свойства BindingServiceProvider.MemberProvider.Register(member); Поддержка динамических ресурсов. Вы можете добавить любой объект в ресурсы, а затем обращаться к нему через биндинг. С помощью динамических ресурсов легко реализовать кроссплатформенную локализацию приложения.Пример //Регистрирует объект типа MyResourceObject с именем i18n BindingServiceProvider.ResourceResolver.AddObject("i18n", new BindingResourceObject(new MyResourceObject())); //Пример Binding для доступа к ресурсу Text $i18n.MyResourceString Поддержка Fluent-синтаксиса.Пример var textBox = new TextBox(); var set = new BindingSet(textBox); set.Bind (window => window.Text).To (vm => vm.Property).TwoWay (); set.Apply (); Кроссплатформенность. Все необходимые интерфейсы и классы собраны в portable class library. Любая платформа будет работать с одним и тем же кодом, с одинаковыми возможностями. Производительность. На платформах, где есть стандартная реализация Binding, MugenMvvmToolkit Binding работает быстрее, стандартной реализации, при этом предоставляя гораздо больше возможностей. Особенности реализации MVVM На данный момент существует огромное количество различных MVVM фреймворков, но большинство из них выглядят примерно одинаково: Один или два класса, которые реализуют интерфейс INotifyPropertyChanged. Класс, который реализует интерфейс ICommand. Класс Messenger, который позволяет обмениваться сообщениями между классами. Несколько вспомогательных методов, для синхронизации UI потоков. Наверное, такой фреймворк писал каждый, но такие реализации далеки от идеальных и не решают, главных проблем MVVM, таких как: Навигация между ViewModel вне зависимости от платформы. Создание ViewModel, через конструктор с зависимостями и параметрами. Динамическое связывание ViewModel и View. Управление состоянием ViewModel в зависимости от жизненного цикла View. Сохранение\восстановление состояния ViewModel в зависимости от платформы. Основные особенности MugenMvvmToolkit: Навигация Отдельно хотелось бы рассмотреть навигацию между ViewModel. Навигация в MVVM это одна из самых сложных тем, сюда входит показ диалоговых окон, добавление вкладок в TabControl, показ страниц для мобильных приложений и т.д. Сложной эта тема является, потому что на разных платформах одна и та же ViewModel, может быть диалоговым окном, Page (WinRT, WP, WPF, SL), Activity, Fragment (Android), ViewController (iOS) и т.д. При этом API для работы с ViewModel, должно выглядеть одинаково в независимости от платформы, т.к. для ViewModel нет разницы, как себя отображать.Для начала рассмотрим примеры, того как навигация работает на разных платформах.Пример того, как показать диалоговое окно на WPF //При создании мы можем передавать любые параметры в конструктор var mainWindow = new MainWindow (); //Здесь можно писать любой код ининциализаии и взаимодействия с окном. mainWindow.Init (args); if (! mainWindow.ShowDialog ().GetValueOrDefault ()) return; //Этот код продолжит выполнение после закрытия окна, и мы легко можем получить результат. Для WPF все очень просто, мы сами контролируем создание окна, его инициализацию и легко можем узнать, когда окно было закрыто.Пример навигации на новую Activity (Xamarin.Android) //Мы не можем сами создать Activity, мы лишь указываем тип, а система сама создает ее. var page2 = new Intent (this, typeof (Page2)); //Мы можем передавать только простые параметры page2.PutExtra («arg1», arg) StartActivity (page2); //Нужно перезагрузить метод, чтобы узнать, когда завершится запущенная Activity Пример навигации на новую Page (WinRT и Windows phone) //Все те же ограничения что и на Android. NavigationService.Navigate (typeof (Page2), arg); Теперь давайте рассмотрим, как навигация работает в существующих MVVM фреймворках, для примера возьмем достаточно известный проект MvvmCross: Пример навигации MvvmCross ShowViewModel(new DetailParameters () { Index = 2 }); DetailViewModel должна иметь метод Init, который принимает класс DetailParameters: public void Init (DetailParameters parameters) { // use the parameters here } При этом объект DetailParameters должен быть сериализуемым, поэтому никаких сложных объектов передавать нельзя. С таким подходом, также очень сложно получить результат из DetailViewModel после завершения навигации. Подход в MvvmCross, очень похож на стандартную навигацию для мобильных платформ. Вы указываете тип ViewModel, сериализуемый параметр и система отображает View и связывает ее с ViewModel. При этом узнать из одной ViewModel, когда была закрыта другая ViewModel достаточно сложно. Все эти ограничения связаны с тем, что на мобильных устройствах ваше приложение может быть полностью выгружено из памяти, а затем снова восстановлено, и тут возникает проблема с сохранением и восстановлением состояния. В основном эту проблему решают сохранением пути навигации и сериализацией параметров навигации, чтобы затем их можно было восстановить.В сравнении с WPF, такой подход выглядит неудобным, но MugenMvvmToolkit позволяет использовать навигацию похожую на WPF для всех платформ. Основной идеей является возможность сериализовать делегат (класс машины состояний async/await), который должен выполниться после закрытия ViewModel. Рассмотрим на примере, нужно из Vm1, показать Vm2 и обработать результат после закрытия Vm2, при этом не важно, на какой платформе и какое отображение будет у Vm2: Пример навигации MugenMvvmToolkit public class Vm2: ViewModelBase { public void InitFromVm1() { } public object GetResult () { return null; } } public class Vm1: ViewModelBase { public async void Open () { var vm2 = GetViewModel(); //Здесь вы можете передать любые параметры, вызвать любые методы и т.д vm2.InitFromVm1(); //Возвращает интерфейс типа IAsyncOperation, //который позволяет зарегестрировать делегат который будет вызван при закрытии Vm2 IAsyncOperation asyncOperation = vm2.ShowAsync (Vm2CloseCallback); //Еще один способ добавить делегат asyncOperation.ContinueWith (Vm2CloseCallback); //Или вы можете использовать ключевое слово await await asyncOperation; //Этот код будет выполнен после закрытия Vm2 //Получаем результат после закрытия var result = vm2.GetResult (); } private void Vm2CloseCallback (IOperationResult operationResult) { //Получаем результат после закрытия var result = ((Vm2)operationResult.Source).GetResult (); } private void Vm2CloseCallback (Vm2 vm2, IOperationResult operationResult) { //Получаем результат после закрытия var result = vm2.GetResult (); } } И этот код будет работать в независимости от платформы и способа отображения Vm2, и даже если ваше приложение будет выгружено из памяти, все зарегистрированные делегаты и машины состояний, также будут сохранены, а затем восстановлены. Если вы хотите использовать async/await на платформе WinRT или Windows Phone вам нужно будет установить плагин для Fody, это связано с ограничениями рефлексии для этих платформ.Одной из особенностей MugenMvvmToolkit является глубокая интеграция с каждой платформой, это позволяет использовать все плюсы платформы в рамках MVVM.WPF и SL Особенности MugenMvvmToolkit для WPF\SL: Поддержка навигации с использованием диалогов/окон для WPF. Если вы сопоставите Window с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно. Поддержка навигации с использованием класса ChildWindow для SL. Если вы сопоставите ChildWindow с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно. Поддержка страничной навигацию, для WPF — NavigationWindow, для SL — Frame. Поддержка валидации с использованием стандартного свойства System.Windows.Controls.Validation.Errors. Для того, чтобы использовать Binding, необходимо установить дополнительный пакет из nuget, после установки вам будет доступен класс DataBindingExtension и attached property View.Bind.Примеры использования Binding