MugenMvvmToolkit - cross-platform MVVM framework

    MugenMvvmToolkit


    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 EventToCommandfor 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 properties ValidatesOnExceptionsand those ValidatesOnNotifyDataErrorsresponsible for validation disappeared, and the property StringFormatresponsible 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 example Mode, 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 current DataContextfor Binding, analog {RelativeSource Self, Path=DataContext}.
      • $args- returns the current parameter EventArgs, can be used only if it TargetPathindicates an event.

      Examples
      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 support for control events with access to the parameter EventArgsusing the keyword $args.
      Examples
      TextChanged 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.
      Examples
      Text 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.
      Examples
      Click 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.
      Examples
      Text $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 RelativeSourcefor XAML platforms).
      Helper Methods:
      • $Element(ElementName) - Searches for an element named ElementName.
      • $Relative(Type), $Relative(Type, 1)- searches among parental controls with type Typeand (if necessary) taking into account the level of the parental element (second parameter).
      • $self - returns the current element on which Binding is set.

      Examples
      Text $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 DataGridViewnot have properties SelectedItem, but we can easily add it using the attached property:
      Example
      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);


    • 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 Messengerthat 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 ViewModelregardless of platform.
    • Creation ViewModel, through a constructor with dependencies and parameters.
    • Dynamic linking ViewModeland View.
    • ViewModelLifecycle management View.
    • Saving \ restoration of state ViewModeldepending 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 ViewModelfor 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-containers MugenInjection, Autofac, Ninject. The list can be expanded by adding an interface implementation IIocContainerfor any other container.
    • The presence of several base classes ViewModelfor various situations.
      • ViewModelBase- a base class for everyone ViewModel, contains methods for creating others ViewModel, methods for messaging, methods and properties IsBusy, BusyMessagefor managing the status of asynchronous operations.
      • CloseableViewModel- inherited from ViewModelBase, implements an interface ICloseableViewModelthat allows you to control the closing process ViewModel.
      • ValidatableViewModel- inherited from CloseableViewModel, contains methods and properties for validation, implements an interface INotifyDataErrorInfofor notifying Binding about errors.
      • EditableViewModel- inherited from ValidatableViewModel, allows you to edit and validate the data model, monitors the state of the object, allows you to undo the changes.
      • WorkspaceViewModel, WorkspaceViewModel- inherited from CloseableViewModel, contains properties IsSelectedand DisplayName- for convenient display ViewModelin 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 , , .IViewAwareViewModelViewIViewINavigableViewModelViewModelOnNavigatedFromOnNavigatingFromOnNavigatedTo
      • GridViewModel- inherited from ViewModelBase, allows you to work with collections of various objects.
      • MultiViewModel- inherited from CloseableViewModel, allows you to work with collections of others ViewModel, well suited for binding to TabControl.

    • MugenMvvmToolkit does not use ViewModelLocatorto create ViewModel. All ViewModelare created using the DI container, the ViewModelinterface is responsible for the creation IViewModelProvider.
      An example of creating and interacting ViewModel
       public class ItemViewModel : ViewModelBase
       {    
      	 public ItemViewModel(ISomeService service)
      	 {        
      	 }
      	 public void InitializeValue()
      	 {
      	 }
       }
       public class MainViewModel : ViewModelBase
       {
      	 public void CreateViewModelMethod()
      	 {
      		 //Создание ViewModel
      		 var viewModel = GetViewModel();
      		 //Использование любого метода, свойства, события и т.д.
      		 viewModel.InitializeValue();
      	 }
       }


    • Comparison Viewwith ViewModeloccurs dynamically. The interface is responsible for the mapping IViewMappingProvider; the default naming convention is used. To ViewModeldelete the following end:
      "ViewModel", "Vm",
      and for View:
      "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 that Viewcorresponds ViewModel.
      Matching example:
      MainViewModel, MainVm -> MainActivityView, MainFragmentView, MainWindowView и т.д.
      If you want to explicitly set Viewfor ViewModel, you can use ViewModelAttribute(in this case, the naming convention is ignored):
      [ViewModel (typeof(MainViewModel))]
      public partial class MainWindow : Window

      You can also specify a name for Viewand then use it when creating / displaying ViewModel:
      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 it ViewModelhas a state that needs to be saved, it must implement an interface IHasStatethat has two methods LoadStateand SaveState. The system will automatically call these methods depending on the life cycle of the application and the current platform.
      Example
      private 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 ViewModelmay 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 ViewModelshould look the same regardless of the platform, because for ViewModelthere 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 });

    DetailViewModelmust have a method Initthat takes a class DetailParameters:
    public void Init(DetailParameters parameters)
    {
        // use the parameters here
    }
    


    In this case, the object DetailParametersmust be serializable, so no complex objects can be transferred. With this approach, it is also very difficult to get the result from DetailViewModelafter 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 Viewand associates it with ViewModel. At the same time, learn from one ViewModelwhen the other was closedViewModelquite 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 Vm2and 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 Windowto any ViewModel, then when you call the method ShowAsync, a dialog box will be displayed.
    • Class navigation support ChildWindowfor SL. If you map ChildWindowto any ViewModel, then when you call the method ShowAsync, 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 DataBindingExtensionand 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 map Pageto any ViewModel, then when you call the method ShowAsync, 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 DataTemplateSelectorfor Binding, an analogue DataTemplateSelectorfor Xaml platforms.
    • Support navigation using the class Form. If you map Formto any ViewModel, then when you call the method ShowAsync, a dialog box will be displayed.
    • Validation support using the standard class System.Windows.Forms.ErrorProvider.

    In order to use Binding you must:
    1. Create a class that inherits from the Binder class:
      public class ViewBinder : Binder
      {
          public ViewBinder()
          {
          }
          public ViewBinder(IContainer container)
              : base(container)
          {
          }
      }
      

    2. Compile the project, open the designer with the desired form, go to the Toolbox tab, the class should appear there ViewBinder
    3. 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 Activityand Fragment. State Activityand Fragmentis already being tracked, so you do not need to manually invoke methods to save / restore ViewModel.
    • The ability to use back stack fragmentnavigation.
    • 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, PopupMenuand OptionsMenu.
    • Support DataTemplateSelectorfor Binding, an analogue DataTemplateSelectorfor 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 standard UIViewControllerones there is an implementation with the Mvvm prefix, for work you need to inherit not from the standard ones UIViewController, but with the Mvvm prefix. If you map UIViewControllerto any ViewModel, then when you call the method ShowAsync, navigation will be made to a new one of UIViewControllerthis type.
    • Support modal navigation for UIViewController.
    • State management for UIViewController. Statuses are UIViewControlleralready monitored, so you do not need to manually call methods to save / restore ViewModel.
    • Support DataTemplateSelectorfor Binding, an analogue DataTemplateSelectorfor Xaml platforms.
    • Library support MonoTouch.Dialog.


    Xamarin.Forms


    Features of MugenMvvmToolkit for Xamarin.Forms:
    • Поддержка страничной навигации с использованием класса Page. Если вы сопоставите Page с какой-либо ViewModel, то при вызове метода ShowAsync, будет показана новая страница.
    • Поддержка модальной навигации с использованием класса Page.
    • Поддержка Binding с использованием Xaml-разметки. MugenMvvmToolkit предоставляет класс DataBindingExtension и attached property View.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.

    Also popular now: