- PVSM.RU - https://www.pvsm.ru -
[1]
Паттерн MVVM хорошо известен, о нем написано много статей, наверное, каждый NET-разработчик сталкивался или слышал об этом паттерне. Цель этой статьи – рассказать о собственной реализации этого паттерна.
MugenMvvmToolkit — является кроссплатформенной реализацией паттерна MVVM и на текущий момент поддерживает следующие платформы:
Знакомство с проектом хотелось бы начать с элемента, без которого MVVM не может существовать – это привязка данных (Data Binding). Именно механизм привязки данных позволяет четко разделить абстракции View и ViewModel между собой.
Разработчики приложений для платформ WPF, Silverlight, Windows Store и Windows Phone хорошо знакомы со стандартной реализацией механизма Binding. Это мощная система, покрывающая все основные задачи. Однако она имеет ряд недостатков, которые и подтолкнули к созданию собственной реализации Binding. Ниже приведены наиболее существенные, на мой взгляд, недостатки:
System.Windows.Expression:
//“This type supports the Windows Presentation Foundation (WPF) infrastructure and is not intended to be used directly from your code”.
{Binding HasErrors, Converter={StaticResource InverseBooleanConverter}}{Binding !HasErrors}
EventToCommand для этих целей.UpdateSourceTrigger=PropertyChanged). На платформе WinRT эта возможность вернулась, но исчезли свойства ValidatesOnExceptions и ValidatesOnNotifyDataErrors, отвечающие за валидацию, а также исчезло свойство StringFormat, отвечающее за форматирование результата.
Если работать лишь на одной платформе, можно смириться с этими недостатками и применять различные «обходные решения». Но т.к. проект предназначен для многих платформ, некоторые из которых не имеют даже стандартных Binding, было принято решение создать свою собственную реализацию, обладающую одинаковыми возможностями на всех платформах.
В результате получилась реализация Binding со следующими возможностями:
??, ?:, +, -, *, /, %, , ==, !=, <, >, <=, >=, &&(and), ||(or), |, &, !, ~), приоритет операций учитывается в соответствии со стандартом языка C#. Поддерживаются лямбда-выражения, вывод обобщенных типов на основании значений, вызов методов, вызов методов расширения, Linq.TargetPath SourcePath, Mode=TwoWay, Validate=true
TargetPath – путь для 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’
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
Enabled = false) в случае если нельзя выполнить команду.
Click Command, ToggleEnabledState=false //не изменяет состояние контрола
Click Command, ToggleEnabledState=true //изменяет состояние контрола
$GetErrors() вернет ошибки валидации всей формы по всем свойствам или ошибки для конкретных свойств. Метод бывает полезным, когда есть необходимость показать пользователю ошибки на форме.
Text $GetErrors(Property).FirstOrDefault()
Text $string.Join($Environment.NewLine, $GetErrors()) //Суммирует все ошибки в одну строку использую новую строку, как разделитель.
RelativeSource для XAML платформ). $Element(ElementName) – ищет элемент с именем ElementName.$Relative(Type), $Relative(Type, 1) – ищет среди родительских элементов контрол с типом Type и (при необходимости) с учетом уровня родительского элемента(второй параметр).$self – возвращает текущий элемент на который установлен Binding.Text $Relative(Window).Title
Text $self.ActualWidth
Text $Element(NamedSlider).Value
DataGridView нет свойтсва SelectedItem, но мы легко можем его добавить, используя присоединяемое свойство:
var member = AttachedBindingMember.CreateMember<DataGridView, object>("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
var textBox = new TextBox();
var set = new BindingSet<TextBox, MainViewModel>(textBox);
set.Bind(window => window.Text).To(vm => vm.Property).TwoWay();
set.Apply();
На данный момент существует огромное количество различных MVVM фреймворков, но большинство из них выглядят примерно одинаково:
INotifyPropertyChanged.ICommand.Messenger, который позволяет обмениваться сообщениями между классами.Наверное, такой фреймворк писал каждый, но такие реализации далеки от идеальных и не решают, главных проблем MVVM, таких как:
ViewModel вне зависимости от платформы.ViewModel, через конструктор с зависимостями и параметрами.ViewModel и View.ViewModel в зависимости от жизненного цикла View.ViewModel в зависимости от платформы.ViewModel для разных платформ.IIocContainer. На текущий момент существуют реализации для трех DI-контейнеров MugenInjection, Autofac, Ninject. Список можно расширить, добавив реализацию интерфейса IIocContainer для любого другого контейнера.ViewModel, для различных ситуаций.
ViewModelBase – базовый класс для всех ViewModel, содержит методы для создания других ViewModel, методы для обмена сообщениями, методы и свойства IsBusy, BusyMessage для управление состоянием асинхронных операций.CloseableViewModel – наследуется от ViewModelBase, реализует интерфейс ICloseableViewModel, который позволяет управлять процессом закрытия ViewModel.ValidatableViewModel – наследуется от CloseableViewModel, содержит методы и свойства для валидации, реализует интерфейс INotifyDataErrorInfo для уведомления Binding об ошибках.EditableViewModel<T> — наследуется от ValidatableViewModel, позволяет редактировать и валидировать модель данных, следит за состоянием объекта, позволяет отменять изменения.WorkspaceViewModel, WorkspaceViewModel<TView> — наследуется от CloseableViewModel, содержит свойства IsSelected и DisplayName — для удобного отображения ViewModel в интерфейсе. Реализует интерфейс IViewAwareViewModel<TView>, который позволяет обращаться к View, через интерфейс IView. Реализует интерфейс INavigableViewModel, который позволяет отслеживать процесс навигации для ViewModel, методы OnNavigatedFrom, OnNavigatingFrom, OnNavigatedTo.GridViewModel — наследуется от ViewModelBase, позволяет работать с коллекциями различных объектов.MultiViewModel – наследуется от CloseableViewModel, позволяет работать с коллекциями других ViewModel, хорошо подходит для привязки к TabControl.
ViewModelLocator для создания ViewModel. Все ViewModel создаются с использованием DI-контейнера, за создание ViewModel отвечает интерфейс IViewModelProvider.
public class ItemViewModel : ViewModelBase
{
public ItemViewModel(ISomeService service)
{
}
public void InitializeValue()
{
}
}
public class MainViewModel : ViewModelBase
{
public void CreateViewModelMethod()
{
//Создание ViewModel
var viewModel = GetViewModel<ItemViewModel>();
//Использование любого метода, свойства, события и т.д.
viewModel.InitializeValue();
}
}
View с ViewModel происходит динамически. За сопоставление отвечает интерфейс IViewMappingProvider, по умолчанию используется соглашение об именовании. Для ViewModel удаляются следующие окончания:"ViewModel", "Vm", View:"ActivityView", "FragmentView", "WindowView", "PageView", "FormView", "Form", "View", "V", "Activity", "Fragment", "Page", "Window" View соответствует ViewModel. MainViewModel, MainVm -> MainActivityView, MainFragmentView, MainWindowView и т.д.View для ViewModel, вы можете использовать ViewModelAttribute (в этом случае соглашение об именовании игнорируется):
[ViewModel (typeof(MainViewModel))]
public partial class MainWindow : Window
Также вы можете задать имя для View и затем использовать его при создании/отображении ViewModel:
[ViewModel (typeof(ItemViewModel), “ViewName”)]
public partial class ItemView : Window
//Создание ViewModel с явно заданным именем View
//В момент показа ViewModel система будет искать View с именем ViewName
var viewModel = GetViewModel<ItemViewModel>(parameters: NavigationConstants.ViewName.ToValue("ViewName"));
//Создание ViewModel
var viewModel = GetViewModel<ItemViewModel>();
//Явно указываем, что для показа ViewModel необходимо использовать View с именем ViewName
viewModel.ShowAsync(NavigationConstants.ViewName.ToValue("ViewName"));
ViewModel. Если ViewModel имеет состоянии, которое нужно сохранять, она должна реализовать интерфейс IHasState, который имеет два метода LoadState и SaveState. Система будет автоматически вызывать эти методы в зависимости от жизненного цикла приложения и текущей платформы.
private static readonly DataConstant<string> StringState = DataConstant.Create(() => StringState, true);
public void LoadState(IDataContext state)
{
//вы можете использовать строго типизированные ключи
state.AddOrUpdate(StringState, "Constant key");
//вы также можете использовать обычные строки для ключей
state.AddOrUpdate("Test", "String key");
}
public void SaveState(IDataContext state)
{
string data = state.GetData(StringState);
var s = state.GetData<string>("Test");
}
Отдельно хотелось бы рассмотреть навигацию между ViewModel. Навигация в MVVM это одна из самых сложных тем, сюда входит показ диалоговых окон, добавление вкладок в TabControl, показ страниц для мобильных приложений и т.д. Сложной эта тема является, потому что на разных платформах одна и та же ViewModel, может быть диалоговым окном, Page (WinRT, WP, WPF, SL), Activity, Fragment (Android), ViewController (iOS) и т.д. При этом API для работы с ViewModel, должно выглядеть одинаково в независимости от платформы, т.к. для ViewModel нет разницы, как себя отображать.
Для начала рассмотрим примеры, того как навигация работает на разных платформах.
//При создании мы можем передавать любые параметры в конструктор
var mainWindow = new MainWindow();
//Здесь можно писать любой код ининциализаии и взаимодействия с окном.
mainWindow.Init(args);
if (!mainWindow.ShowDialog().GetValueOrDefault())
return;
//Этот код продолжит выполнение после закрытия окна, и мы легко можем получить результат.
Для WPF все очень просто, мы сами контролируем создание окна, его инициализацию и легко можем узнать, когда окно было закрыто.
//Мы не можем сами создать Activity, мы лишь указываем тип, а система сама создает ее.
var page2 = new Intent (this, typeof(Page2));
//Мы можем передавать только простые параметры
page2.PutExtra ("arg1", arg)
StartActivity (page2);
//Нужно перезагрузить метод, чтобы узнать, когда завершится запущенная Activity
//Все те же ограничения что и на Android.
NavigationService.Navigate(typeof(Page2), arg);
Теперь давайте рассмотрим, как навигация работает в существующих MVVM фреймворках, для примера возьмем достаточно известный проект MvvmCross:
ShowViewModel<DetailViewModel>(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:
public class Vm2 : ViewModelBase
{
public void InitFromVm1()
{
}
public object GetResult()
{
return null;
}
}
public class Vm1 : ViewModelBase
{
public async void Open()
{
var vm2 = GetViewModel<Vm2>();
//Здесь вы можете передать любые параметры, вызвать любые методы и т.д
vm2.InitFromVm1();
//Возвращает интерфейс типа IAsyncOperation,
//который позволяет зарегестрировать делегат который будет вызван при закрытии Vm2
IAsyncOperation<bool> asyncOperation = vm2.ShowAsync(Vm2CloseCallback);
//Еще один способ добавить делегат
asyncOperation.ContinueWith(Vm2CloseCallback);
//Или вы можете использовать ключевое слово await
await asyncOperation;
//Этот код будет выполнен после закрытия Vm2
//Получаем результат после закрытия
var result = vm2.GetResult();
}
private void Vm2CloseCallback(IOperationResult<bool> operationResult)
{
//Получаем результат после закрытия
var result = ((Vm2)operationResult.Source).GetResult();
}
private void Vm2CloseCallback(Vm2 vm2, IOperationResult<bool> operationResult)
{
//Получаем результат после закрытия
var result = vm2.GetResult();
}
}
И этот код будет работать в независимости от платформы и способа отображения Vm2, и даже если ваше приложение будет выгружено из памяти, все зарегистрированные делегаты и машины состояний, также будут сохранены, а затем восстановлены. Если вы хотите использовать async/await на платформе WinRT или Windows Phone вам нужно будет установить плагин для Fody [2], это связано с ограничениями рефлексии для этих платформ.
Одной из особенностей MugenMvvmToolkit является глубокая интеграция с каждой платформой, это позволяет использовать все плюсы платформы в рамках MVVM.
Особенности MugenMvvmToolkit для WPFSL:
Window с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно.ChildWindow для SL. Если вы сопоставите ChildWindow с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно.NavigationWindow, для SL — Frame.
Для того, чтобы использовать Binding, необходимо установить дополнительный пакет из nuget [4], после установки вам будет доступен класс DataBindingExtension и attached property View.Bind.
<TextBlock Text="{DataBinding 'Text.ExtensionMethod(Text.Count())'}" />
<TextBlock Text="{DataBinding '$string.IsNullOrEmpty(Text) ? 'String is empty' : 'String is not empty''}"/>
<TextBlock View.Bind="Text Text.ExtensionMethod(Text.Count())"/>
<TextBlock View.Bind="Text $string.IsNullOrEmpty(Text) ? 'String is empty' : 'String is not empty'"/>
<Button Click="{DataBinding Path=Command, ToggleEnabledState=False}" />
<Button View.Bind="Click Command, ToggleEnabledState=False" />
<TextBox TextChanged="{DataBinding 'EventMethodMultiParams($self.Text, $args.UndoAction)'}" />
<TextBox View.Bind="TextChanged EventMethodMultiParams($self.Text, $args.UndoAction)" />
Особенности MugenMvvmToolkit для WinRTWinPhone:
Page. Если вы сопоставите Page с какой-либо ViewModel, то при вызове метода ShowAsync, будет показана новая страница.
Для того, чтобы использовать Binding, необходимо установить дополнительный пакет из nuget [4], после установки вам будет доступно attached property View.Bind. Для его использования необходимо добавить пространство имен:
xmlns:markupExtensions="clr-namespace:MugenMvvmToolkit.MarkupExtensions;assembly=MugenMvvmToolkit.WinPhone"
xmlns:markupExtensions="using:MugenMvvmToolkit.MarkupExtensions"
<TextBlock markupExtensions:View.Bind="Text Text.ExtensionMethod(Text.Count())"/>
<TextBlock markupExtensions:View.Bind="Text $string.IsNullOrEmpty(Text) ? 'String is empty' : 'String is not empty'"/>
<Button markupExtensions:View.Bind="Click Command, ToggleEnabledState=False" />
<TextBox markupExtensions:View.Bind="TextChanged EventMethodMultiParams($self.Text, $args.UndoAction)" />
Особенности MugenMvvmToolkit для WinForms:
DataTemplateSelector для Binding, аналог DataTemplateSelector для Xaml платформ.Form. Если вы сопоставите Form с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно.System.Windows.Forms.ErrorProvider.Для того, чтобы использовать Binding необходимо:
public class ViewBinder : Binder
{
public ViewBinder()
{
}
public ViewBinder(IContainer container)
: base(container)
{
}
}
ViewBinderBindings.<Bindings>
<addToolStripButton Click="AddNodeCommand" />
<removeToolStripButton Click="RemoveNodeCommand" />
<treeView ItemsSource="Nodes" CollectionViewManager="$treeNodeCollectionViewManager" ItemTemplate="$treeNodeTemplate" SelectedNode.DataContext="SelectedNode, Mode=OneWayToSource" />
<nameTextBox Text="SelectedNode.Name, Mode=TwoWay, Fallback='Nothing selected'" />
<validCheckBox Checked="SelectedNode.IsValid, Mode=TwoWay" />
</Bindings>
Особенности MugenMvvmToolkit для Xamarin.Android:
Activity, для всех стандартных Activity существует реализация с префиксом Mvvm, для работы вам необходимо наследоваться не от стандартных Activity, а с префиксом Mvvm. Если вы сопоставите Activity с какой-либо ViewModel, то при вызове метода ShowAsync, будет совершена навигация на новую Activity этого типа.Fragment, для всех стандартных Fragment существует реализация с префиксом Mvvm, для работы вам необходимо наследоваться не от стандартных Fragment, а с префиксом Mvvm. Если вы сопоставите MvvmDialogFragment с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно.Activity и Fragment. Состояния Activity и Fragment уже отслеживается, поэтому вам не нужно вручную вызывать методы для сохранения/восстановления ViewModel.back stack fragment навигацию.xmlns:pkg="http://schemas.android.com/apk/res-auto", затем вы можете добавить Binding к любому контролу используя атрибут Bind
<ListView android:layout_width="fill_parent"
android:layout_height="wrap_content"
pkg:ItemTemplate="@layout/_itemlistviewtemplate"
pkg:Bind="ItemsSource Items"/>
ActionBar, Toolbar, PopupMenu и OptionsMenu.DataTemplateSelector для Binding, аналог DataTemplateSelector для Xaml платформ.TextView.Error.<TextView android:layout_width="fill_parent"
android:layout_height="fill_parent"
pkg:Bind="Text $Format('Name: {0}, Id: {1}', Name, Id)" />
<Button android:layout_width="fill_parent"
android:layout_height="wrap_content"
pkg:Bind="Click Command" />
Особенности MugenMvvmToolkit для Xamarin.iOS:
UIViewController, для всех стандартных UIViewController существует реализация с префиксом Mvvm, для работы вам необходимо наследоваться не от стандартных UIViewController, а с префиксом Mvvm. Если вы сопоставите UIViewController с какой-либо ViewModel, то при вызове метода ShowAsync, будет совершена навигация на новый UIViewController этого типа.UIViewController.UIViewController. Состояния UIViewController уже отслеживается, поэтому вам не нужно вручную вызывать методы для сохранения/восстановления ViewModel.DataTemplateSelector для Binding, аналог DataTemplateSelector для Xaml платформ.MonoTouch.Dialog.Особенности MugenMvvmToolkit для Xamarin.Forms:
Page. Если вы сопоставите Page с какой-либо ViewModel, то при вызове метода ShowAsync, будет показана новая страница.Page.DataBindingExtension и attached property View.Bind, для работы с Binding. Для использования Binding необходимо в файле Xaml-разметки добавить следующее пространство имен xmlns:mugen="clr-namespace:MugenMvvmToolkit.MarkupExtensions;assembly=MugenMvvmToolkit.Xamarin.Forms"<Entry Text="{mugen:DataBinding Name, Mode=TwoWay, Validate=True}" />
<Button Command="{mugen:DataBinding $Relative(ListView).DataContext.ShowCommand}"
Text="{mugen:DataBinding Item1}" CommandParameter="{mugen:DataBinding Item2}" />
В статье кратко описаны основные особенности проекта. Цель статьи – показать основные особенности реализации фреймворка, который позволяет использовать, всю силу подхода MVVM на любых кроссплатформенных проектах, действительно упрощая разработку и сопровождение.
Для более глубокого понимания обязательно ознакомьтесь с примерами [5].
P.S. Спасибо моим коллегам по работе за поддержку, отличные идеи и помощь в тестировании.
Автор: VVS0205
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/79112
Ссылки в тексте:
[1] Image: https://github.com/MugenMvvmToolkit/MugenMvvmToolkit
[2] плагин для Fody: https://github.com/MugenMvvmToolkit/MugenMvvmToolkit.Fody
[3] System.Windows.Controls.Validation.Errors: http://msdn.microsoft.com/en-US/library/system.windows.controls.validation.errors(v=vs.110).aspx
[4] nuget: http://www.nuget.org/packages/MugenMvvmToolkit.Binding/
[5] примерами: https://github.com/MugenMvvmToolkit/MugenMvvmToolkit.Samples
[6] Nuget: http://www.nuget.org/packages?q=MugenMvvmToolkit
[7] Источник: http://habrahabr.ru/post/236745/
Нажмите здесь для печати.