MugenMvvmToolkit - cross-platform MVVM framework
Introduction
The MVVM pattern is well known, many articles have been written about it, probably every NET developer has encountered or heard about this pattern. The purpose of this article is to talk about our own implementation of this pattern.
MugenMvvmToolkit is a cross-platform implementation of the MVVM pattern and currently supports the following platforms:
- 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
I would like to start acquaintance with the project with an element without which MVVM cannot exist - this is Data Binding. It is the data binding mechanism that allows you to clearly separate the abstractions of View and ViewModel among themselves.
Application developers for the WPF, Silverlight, Windows Store, and Windows Phone platforms are familiar with the standard implementation of the Binding mechanism. This is a powerful system that covers all the main tasks. However, it has a number of drawbacks that prompted the creation of its own implementation of Binding. Below are the most significant, in my opinion, disadvantages:
- Lack of extensibility. Perhaps the most important drawback that causes everyone else. Perhaps Microsoft was in a hurry to implement Binding, as all infrastructure classes have an internal access modifier, and if the class is public, then all of its virtual methods are marked with an internal modifier. The situation illustrates well the comments on the public class.
System.Windows.Expression:
//“This type supports the Windows Presentation Foundation (WPF) infrastructure and is not intended to be used directly from your code”.
- Redundant syntax. For example, you have declared a property of type bool and you want to use its negation in Binding. To do this, you need to write a converter class to invert the value, register it in resources, and only then it will be available in the code. Binding in this case looks something like this:
{Binding HasErrors, Converter={StaticResource InverseBooleanConverter}}
It would be quite logical and natural to use the familiar “!” Operator:{Binding !HasErrors}
- Lack of Binding support for control events. Probably, many used some kind of helper class
EventToCommand
for these purposes. - Platform dependent. Binding capabilities are highly platform specific. For example, on Windows Phone there is no way to update Binding to change the property (
UpdateSourceTrigger=PropertyChanged
). On the WinRT platform, this feature returned, but the propertiesValidatesOnExceptions
and thoseValidatesOnNotifyDataErrors
responsible for validation disappeared, and the propertyStringFormat
responsible for formatting the result also disappeared .
If you work on only one platform, you can come to terms with these shortcomings and apply various "workarounds." But since the project is intended for many platforms, some of which do not even have standard Binding, it was decided to create their own implementation, which has the same capabilities on all platforms.
The result is an implementation of Binding with the following features:
- Extensibility. When creating a Binding, the parser builds a syntax tree, and this allows you to easily expand it without manipulating the text. The structure is very similar to expression trees in C #.
- C # syntax support. Binding supports all major operators (
??, ?:, +, -, *, /, %, , ==, !=, <, >, <=, >=, &&(and), ||(or), |, &, !, ~
), the priority of operations is taken into account in accordance with the C # language standard. Supported lambda expressions, the derivation of generic types based on values, method calls, extension method calls, Linq.
Syntax Example:TargetPath SourcePath, Mode=TwoWay, Validate=true
TargetPath
- the way for Binding from control.SourcePath
- the path for Binding from a data source or an expression in C #, you can use several paths.Mode, Validate
- additional parameters for Binding, for exampleMode, Fallback, Delay
, etc.
Keywords:$self
- returns the current control to which Binding is installed, an analog{RelativeSource Self}
.$root
- returns the current root element for the control on which Binding is installed.$context
- returns the currentDataContext
for Binding, analog{RelativeSource Self, Path=DataContext}
.$args
- returns the current parameterEventArgs
, can be used only if itTargetPath
indicates an event.
ExamplesText 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 support for control events with access to the parameter
EventArgs
using the keyword$args
.ExamplesTextChanged EventMethod($args.UndoAction) TextChanged EventMethodMultiParams(Text, $args.UndoAction)
- Validation Support. Validation is provided by a standard interface
INotifyDataErrorInfo
. An error message will be displayed on each platform.ExamplesText Property, Mode=TwoWay, ValidatesOnNotifyDataErrors=True Text Property, Mode=TwoWay, ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True Text Property, Mode=TwoWay, Validate=True //эквивалентно ValidatesOnNotifyDataErrors=True, ValidatesOnExceptions=True
- Advanced Binding on teams. If Binding is installed on a command, you can determine how the control will respond to the “availability” of the command; the command may make control (
Enabled = false
) inactive if the command cannot be executed.ExamplesClick Command, ToggleEnabledState=false //не изменяет состояние контрола Click Command, ToggleEnabledState=true //изменяет состояние контрола
- Expanded validation support. The built-in method
$GetErrors()
will return validation errors of the entire form for all properties or errors for specific properties. The method is useful when there is a need to show the user errors on the form.ExamplesText $GetErrors(Property).FirstOrDefault() Text $string.Join($Environment.NewLine, $GetErrors()) //Суммирует все ошибки в одну строку использую новую строку, как разделитель.
- Relative binding. Binding can be installed on the current control or on any other inside the visual control tree (analogue of the property
RelativeSource
for XAML platforms).
Helper Methods:$Element(ElementName)
- Searches for an element named ElementName.$Relative(Type), $Relative(Type, 1)
- searches among parental controls with typeType
and (if necessary) taking into account the level of the parental element (second parameter).$self
- returns the current element on which Binding is set.
ExamplesText $Relative(Window).Title Text $self.ActualWidth Text $Element(NamedSlider).Value
- Support for attachable properties, events, and methods. Allows you to easily expand any type. For example, WinForms does
DataGridView
not have propertiesSelectedItem
, but we can easily add it using the attached property:Examplevar 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); - Support for dynamic resources. You can add any object to resources, and then access it through binding. With the help of dynamic resources, it is easy to implement cross-platform localization of the application.Example
//Регистрирует объект типа MyResourceObject с именем i18n BindingServiceProvider.ResourceResolver.AddObject("i18n", new BindingResourceObject(new MyResourceObject())); //Пример Binding для доступа к ресурсу Text $i18n.MyResourceString
- Fluent syntax support.Example
var textBox = new TextBox(); var set = new BindingSet
(textBox); set.Bind(window => window.Text).To(vm => vm.Property).TwoWay(); set.Apply(); - Cross-platform. All necessary interfaces and classes are collected in a portable class library. Any platform will work with the same code, with the same capabilities.
- Performance. On platforms where there is a standard Binding implementation, MugenMvvmToolkit Binding is faster than the standard implementation, while providing much more features.
MVVM Implementation Features
At the moment, there are a huge number of different MVVM frameworks, but most of them look about the same:
- One or two classes that implement the interface
INotifyPropertyChanged
. - A class that implements the interface
ICommand
. - A class
Messenger
that allows messaging between classes. - Several helper methods for synchronizing UI threads.
Everyone probably wrote such a framework, but such implementations are far from ideal and do not solve the main problems of MVVM, such as:
- Navigation between
ViewModel
regardless of platform. - Creation
ViewModel
, through a constructor with dependencies and parameters. - Dynamic linking
ViewModel
andView
. ViewModel
Lifecycle managementView
.- Saving \ restoration of state
ViewModel
depending on the platform.
Key Features of MugenMvvmToolkit:
- Cross-platform. At the moment, all major platforms on which C # can be used are supported. All necessary interfaces and classes are collected in a portable class library. Any platform will work with the same code, with the same capabilities.
- Unified code
ViewModel
for different platforms. - Integration with DI containers. MugenMvvmToolkit is not tied to a specific DI container; an interface is used for interaction
IIocContainer
. At the moment, there are implementations for three DI-containersMugenInjection
,Autofac
,Ninject
. The list can be expanded by adding an interface implementationIIocContainer
for any other container. - The presence of several base classes
ViewModel
for various situations.ViewModelBase
- a base class for everyoneViewModel
, contains methods for creating othersViewModel
, methods for messaging, methods and propertiesIsBusy
,BusyMessage
for managing the status of asynchronous operations.CloseableViewModel
- inherited fromViewModelBase
, implements an interfaceICloseableViewModel
that allows you to control the closing processViewModel
.ValidatableViewModel
- inherited fromCloseableViewModel
, contains methods and properties for validation, implements an interfaceINotifyDataErrorInfo
for notifying Binding about errors.EditableViewModel
- inherited fromValidatableViewModel
, allows you to edit and validate the data model, monitors the state of the object, allows you to undo the changes.WorkspaceViewModel, WorkspaceViewModel
- inherited fromCloseableViewModel
, contains propertiesIsSelected
andDisplayName
- for convenient displayViewModel
in the interface. Implements an interface that allows access to , through the interface . Implements an interface that allows you to track the navigation process for the methods , , .IViewAwareViewModel
View
IView
INavigableViewModel
ViewModel
OnNavigatedFrom
OnNavigatingFrom
OnNavigatedTo
GridViewModel
- inherited fromViewModelBase
, allows you to work with collections of various objects.MultiViewModel
- inherited fromCloseableViewModel
, allows you to work with collections of othersViewModel
, well suited for binding toTabControl
.
- MugenMvvmToolkit does not use
ViewModelLocator
to createViewModel
. AllViewModel
are created using the DI container, theViewModel
interface is responsible for the creationIViewModelProvider
.An example of creating and interacting ViewModelpublic class ItemViewModel : ViewModelBase { public ItemViewModel(ISomeService service) { } public void InitializeValue() { } } public class MainViewModel : ViewModelBase { public void CreateViewModelMethod() { //Создание ViewModel var viewModel = GetViewModel
(); //Использование любого метода, свойства, события и т.д. viewModel.InitializeValue(); } } - Comparison
View
withViewModel
occurs dynamically. The interface is responsible for the mappingIViewMappingProvider
; the default naming convention is used. ToViewModel
delete the following end:"ViewModel", "Vm"
,
and forView
:"ActivityView", "FragmentView", "WindowView", "PageView", "FormView", "Form", "View", "V", "Activity", "Fragment", "Page", "Window"
(you can expand the lists) and if it is then the names are the same, it is believed thatView
correspondsViewModel
.
Matching example:MainViewModel, MainVm -> MainActivityView, MainFragmentView, MainWindowView и т.д.
If you want to explicitly setView
forViewModel
, you can useViewModelAttribute
(in this case, the naming convention is ignored):[ViewModel (typeof(MainViewModel))] public partial class MainWindow : Window
You can also specify a name forView
and then use it when creating / displayingViewModel
:Example[ViewModel (typeof(ItemViewModel), “ViewName”)] public partial class ItemView : Window //Создание ViewModel с явно заданным именем View //В момент показа ViewModel система будет искать View с именем ViewName var viewModel = GetViewModel
(parameters: NavigationConstants.ViewName.ToValue("ViewName")); //Создание ViewModel var viewModel = GetViewModel (); //Явно указываем, что для показа ViewModel необходимо использовать View с именем ViewName viewModel.ShowAsync(NavigationConstants.ViewName.ToValue("ViewName")); - Powerful validation system, support for asynchronous validation, easy integration with existing validation frameworks.
- Support save / restore state
ViewModel
. If itViewModel
has a state that needs to be saved, it must implement an interfaceIHasState
that has two methodsLoadState
andSaveState
. The system will automatically call these methods depending on the life cycle of the application and the current platform.Exampleprivate static readonly DataConstant
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 ("Test"); }
Navigation
Separately, I would like to consider navigation between
ViewModel
. Navigating in MVVM is one of the most difficult topics, including showing dialog boxes, adding tabs to TabControl
, showing pages for mobile applications, etc. Complex this issue is, because on different platforms, the same ViewModel
may be a dialog box, Page
(the WinRT, WP, a WPF, SL), Activity
, Fragment
(All Android), ViewController
(iOS), etc. At the same time, the API for working with ViewModel
should look the same regardless of the platform, because for ViewModel
there is no difference how to display yourself. To get started, consider examples of how navigation works on different platforms.
An example of how to display a dialog box in WPF
//При создании мы можем передавать любые параметры в конструктор
var mainWindow = new MainWindow();
//Здесь можно писать любой код ининциализаии и взаимодействия с окном.
mainWindow.Init(args);
if (!mainWindow.ShowDialog().GetValueOrDefault())
return;
//Этот код продолжит выполнение после закрытия окна, и мы легко можем получить результат.
For WPF, everything is very simple, we ourselves control the creation of the window, its initialization and can easily find out when the window was closed.
An example of navigation to a new Activity (Xamarin.Android)
//Мы не можем сами создать Activity, мы лишь указываем тип, а система сама создает ее.
var page2 = new Intent (this, typeof(Page2));
//Мы можем передавать только простые параметры
page2.PutExtra ("arg1", arg)
StartActivity (page2);
//Нужно перезагрузить метод, чтобы узнать, когда завершится запущенная Activity
Example navigation on a new Page (WinRT and Windows phone)
//Все те же ограничения что и на Android.
NavigationService.Navigate(typeof(Page2), arg);
Now let's look at how navigation works in existing MVVM frameworks, for example, take a fairly well-known project
MvvmCross
:MvvmCross Navigation Example
ShowViewModel(new DetailParameters() { Index = 2 });
DetailViewModel
must have a method Init
that takes a class DetailParameters
:public void Init(DetailParameters parameters)
{
// use the parameters here
}
In this case, the object
DetailParameters
must be serializable, so no complex objects can be transferred. With this approach, it is also very difficult to get the result from DetailViewModel
after the navigation is complete. The approach in MvvmCross is very similar to the standard navigation for mobile platforms. You specify the type ViewModel
, serializable parameter, and the system displays View
and associates it with ViewModel
. At the same time, learn from one ViewModel
when the other was closedViewModel
quite difficult. All these limitations are related to the fact that on mobile devices your application can be completely unloaded from memory and then restored again, and here there is a problem with saving and restoring the state. Basically, this problem is solved by preserving the navigation path and serializing the navigation parameters so that they can then be restored. Compared to WPF, this approach seems inconvenient, but MugenMvvmToolkit allows you to use navigation similar to WPF for all platforms. The basic idea is to be able to serialize a delegate (async / await state machine class), which should execute after closing
ViewModel
. Consider an example, you need to from Vm1
, show Vm2
and process the result after closingVm2
, it doesn’t matter which platform and which display will be on Vm2
:MugenMvvmToolkit Navigation Example
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();
}
}
And this code will work regardless of the platform and display method
Vm2
, and even if your application is unloaded from memory, all registered delegates and state machines will also be saved and then restored. If you want to use async / await on the WinRT or Windows Phone platform, you will need to install the plugin for Fody , this is due to the reflection limitations for these platforms. One of the features of MugenMvvmToolkit is its deep integration with each platform, this allows you to use all the advantages of the platform within MVVM.
WPF and SL
Features of MugenMvvmToolkit for WPF \ SL:
- Dialog / window navigation support for WPF. If you map
Window
to anyViewModel
, then when you call the methodShowAsync
, a dialog box will be displayed. - Class navigation support
ChildWindow
for SL. If you mapChildWindow
to anyViewModel
, then when you call the methodShowAsync
, a dialog box will be displayed. - Support page navigation, for WPF -
NavigationWindow
, for SL -Frame
. - Validation support using the standard System.Windows.Controls.Validation.Errors property .
In order to use Binding, you need to install an additional package from nuget , after installation you will have access to the class
DataBindingExtension
and attached property View.Bind
.Binding Examples
WinRT and Windows phone
Features of MugenMvvmToolkit for WinRT \ WinPhone:
- Support for page navigation using the class
Page
. If you mapPage
to anyViewModel
, then when you call the methodShowAsync
, a new page will be displayed. - Validation support using the standard System.Windows.Controls.Validation.Errors property .
In order to use Binding, you need to install an additional package from nuget , after installation you will have access to the attached property
View.Bind
. To use it, you must add a namespace:xmlns:markupExtensions="clr-namespace:MugenMvvmToolkit.MarkupExtensions;assembly=MugenMvvmToolkit.WinPhone"
xmlns:markupExtensions="using:MugenMvvmToolkit.MarkupExtensions"
Binding Examples
Winforms
Features of MugenMvvmToolkit for WinForms:
- MugenMvvmToolkit provides a convenient xml editor for Binding.
- Support
DataTemplateSelector
for Binding, an analogueDataTemplateSelector
for Xaml platforms. - Support navigation using the class
Form
. If you mapForm
to anyViewModel
, then when you call the methodShowAsync
, a dialog box will be displayed. - Validation support using the standard class
System.Windows.Forms.ErrorProvider
.
In order to use Binding you must:
- Create a class that inherits from the Binder class:
public class ViewBinder : Binder { public ViewBinder() { } public ViewBinder(IContainer container) : base(container) { } }
- Compile the project, open the designer with the desired form, go to the Toolbox tab, the class should appear there
ViewBinder
- Add it to the form, after that you can add Binding using the property
Bindings
.
Binding Examples
Xamarin.Android
Features of MugenMvvmToolkit for Xamarin.Android:
- Поддержка работы с
Activity
, для всех стандартныхActivity
существует реализация с префиксом Mvvm, для работы вам необходимо наследоваться не от стандартныхActivity
, а с префиксом Mvvm. Если вы сопоставитеActivity
с какой-либоViewModel
, то при вызове методаShowAsync
, будет совершена навигация на новуюActivity
этого типа. - Поддержка работы с
Fragment
, для всех стандартныхFragment
существует реализация с префиксом Mvvm, для работы вам необходимо наследоваться не от стандартныхFragment
, а с префиксом Mvvm. Если вы сопоставитеMvvmDialogFragment
с какой-либоViewModel
, то при вызове методаShowAsync
, будет показано диалоговое окно. - State management for
Activity
andFragment
. StateActivity
andFragment
is already being tracked, so you do not need to manually invoke methods to save / restoreViewModel
. - The ability to use
back stack fragment
navigation. - Binding support using layout (xml markup). To use Binding on Android, you need to add the following namespace in the markup file
xmlns:pkg="http://schemas.android.com/apk/res-auto"
, then you can add Binding to any control using the attributeBind
Example - Binding support for
ActionBar
,Toolbar
,PopupMenu
andOptionsMenu
. - Support
DataTemplateSelector
for Binding, an analogueDataTemplateSelector
for Xaml platforms. - Validation using a standard property
TextView.Error
.
Binding Examples
Xamarin.iOS
Features of MugenMvvmToolkit for Xamarin.iOS:
- Support for working with
UIViewController
, for all standardUIViewController
ones there is an implementation with the Mvvm prefix, for work you need to inherit not from the standard onesUIViewController
, but with the Mvvm prefix. If you mapUIViewController
to anyViewModel
, then when you call the methodShowAsync
, navigation will be made to a new one ofUIViewController
this type. - Support modal navigation for
UIViewController
. - State management for
UIViewController
. Statuses areUIViewController
already monitored, so you do not need to manually call methods to save / restoreViewModel
. - Support
DataTemplateSelector
for Binding, an analogueDataTemplateSelector
for Xaml platforms. - Library support
MonoTouch.Dialog
.
Xamarin.Forms
Features of MugenMvvmToolkit for Xamarin.Forms:
- Поддержка страничной навигации с использованием класса
Page
. Если вы сопоставитеPage
с какой-либоViewModel
, то при вызове методаShowAsync
, будет показана новая страница. - Поддержка модальной навигации с использованием класса
Page
. - Поддержка Binding с использованием Xaml-разметки. MugenMvvmToolkit предоставляет класс
DataBindingExtension
и attached propertyView.Bind
, для работы с Binding. Для использования Binding необходимо в файле Xaml-разметки добавить следующее пространство именxmlns:mugen="clr-namespace:MugenMvvmToolkit.MarkupExtensions;assembly=MugenMvvmToolkit.Xamarin.Forms"
Примеры использования Binding
Заключение
The article briefly describes the main features of the project. The purpose of the article is to show the main features of the implementation of the framework, which allows you to use the full power of the MVVM approach on any cross-platform projects, really simplifying the development and maintenance.
For a deeper understanding, be sure to check out the examples .
References:
PS Thanks to my colleagues for their support, great ideas and help in testing.