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. Ниже приведены наиболее существенные, на мой взгляд, недостатки:
    • Отсутствие расширяемости. Пожалуй, самый главный недостаток, который порождает все остальные. Возможно, в Microsoft очень торопились с реализацией Binding, т.к. все инфраструктурные классы имеют модификатор доступа internal, а если класс публичный, то все его виртуальные методы помечены модификатором internal. Положение дел хорошо иллюстрирует комментарии над публичным классом System.Windows.Expression:
      //“This type supports the Windows Presentation Foundation (WPF) infrastructure and is not intended to be used directly from your code”.
    • Избыточность синтаксиса. Например, у вас объявлено свойство типа bool и вы хотите в Binding использовать его отрицание. Для это необходимо написать класс конвертора для инверсии значения, зарегистрировать его в ресурсах, и лишь затем он будет доступен в коде. Binding в этом случае выглядит примерно так:
      {Binding HasErrors, Converter={StaticResource InverseBooleanConverter}}
      Было бы вполне логичном и естественным использовать привычный оператор «!»:
      {Binding !HasErrors}
    • Отсутствие поддержки Binding на события контрола. Наверное, многие использовали какой-нибудь вспомогательный класс EventToCommand для этих целей.
    • Зависимость от платформы. Возможности Binding сильно зависят от конкретной платформы. Например, на Windows Phone нет возможности обновлять Binding на изменение свойства (UpdateSourceTrigger=PropertyChanged). На платформе WinRT эта возможность вернулась, но исчезли свойства ValidatesOnExceptions и ValidatesOnNotifyDataErrors, отвечающие за валидацию, а также исчезло свойство StringFormat, отвечающее за форматирование результата.

    Если работать лишь на одной платформе, можно смириться с этими недостатками и применять различные «обходные решения». Но т.к. проект предназначен для многих платформ, некоторые из которых не имеют даже стандартных Binding, было принято решение создать свою собственную реализацию, обладающую одинаковыми возможностями на всех платформах.
    В результате получилась реализация Binding со следующими возможностями:
    • Расширяемость. Парсер при создании Binding строит синтаксическое дерево, и это позволяет легко его расширять без манипуляций с текстом. Структура очень похожа на деревья выражений в C#.
    • Поддержка синтаксиса C#. 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’
      


    • Поддержка 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<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


    • Поддержка Fluent-синтаксиса.
      Пример
      var textBox = new TextBox();
      var set = new BindingSet<TextBox, MainViewModel>(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:


    • Кроссплатформенность. На данный момент, поддерживаются все основные платформы, на которых можно использовать язык C#. Все необходимые интерфейсы и классы собраны в portable class library. Любая платформа будет работать с одним и тем же кодом, с одинаковыми возможностями.
    • Единый код ViewModel для разных платформ.
    • Интеграция с DI-контейнерами. MugenMvvmToolkit не привязан к конкретному DI-контейнеру, для взаимодействия используется интерфейс 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.

    • MugenMvvmToolkit не использует ViewModelLocator для создания ViewModel. Все ViewModel создаются с использованием DI-контейнера, за создание ViewModel отвечает интерфейс IViewModelProvider.
      Пример создания и взаимодействия ViewModel
       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 нет разницы, как себя отображать.
    Для начала рассмотрим примеры, того как навигация работает на разных платформах.

    Пример того, как показать диалоговое окно на 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<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:
    Пример навигации 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>();
            //Здесь вы можете передать любые параметры, вызвать любые методы и т.д
            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, это связано с ограничениями рефлексии для этих платформ.
    Одной из особенностей 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
    <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)" />
    



    WinRT и Windows phone


    Особенности MugenMvvmToolkit для WinRT\WinPhone:
    • Поддержка страничной навигации с использованием класса Page. Если вы сопоставите Page с какой-либо ViewModel, то при вызове метода ShowAsync, будет показана новая страница.
    • Поддержка валидации с использованием стандартного свойства System.Windows.Controls.Validation.Errors.

    Для того, чтобы использовать Binding, необходимо установить дополнительный пакет из nuget, после установки вам будет доступно attached property View.Bind. Для его использования необходимо добавить пространство имен:
    xmlns:markupExtensions="clr-namespace:MugenMvvmToolkit.MarkupExtensions;assembly=MugenMvvmToolkit.WinPhone"
    xmlns:markupExtensions="using:MugenMvvmToolkit.MarkupExtensions"
    

    Примеры использования Binding
    <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)" />
    



    WinForms


    Особенности MugenMvvmToolkit для WinForms:
    • MugenMvvmToolkit предоставляет удобный xml-редактор для Binding.
    • Поддержка DataTemplateSelector для Binding, аналог DataTemplateSelector для Xaml платформ.
    • Поддержка навигации с использованием класса Form. Если вы сопоставите Form с какой-либо ViewModel, то при вызове метода ShowAsync, будет показано диалоговое окно.
    • Поддержка валидация с использованием стандартного класса System.Windows.Forms.ErrorProvider.

    Для того, чтобы использовать Binding необходимо:
    1. Создать класс, который наследуется от класса Binder:
      public class ViewBinder : Binder
      {
          public ViewBinder()
          {
          }
       
          public ViewBinder(IContainer container)
              : base(container)
          {
          }
      }
      

    2. Скомпилировать проект, открыть дизайнер с нужной формой, перейти во вкладку Toolbox, там должен появиться класс ViewBinder
    3. Добавить его на форму, после этого можно добавлять Binding используя свойство Bindings.

    Примеры использования Binding
    <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>
    



    Xamarin.Android


    Особенности 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 навигацию.
    • Поддержка Binding с использованием layout (xml-разметки). Для использования Binding на Android необходимо в файле разметки добавить следующее пространство имен 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"/>
      


    • Поддержка Binding для ActionBar, Toolbar, PopupMenu и OptionsMenu.
    • Поддержка DataTemplateSelector для Binding, аналог DataTemplateSelector для Xaml платформ.
    • Валидация с использованием стандартного свойства TextView.Error.

    Примеры использования Binding
    <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" />



    Xamarin.iOS


    Особенности MugenMvvmToolkit для Xamarin.iOS:
    • Поддержка работы с UIViewController, для всех стандартных UIViewController существует реализация с префиксом Mvvm, для работы вам необходимо наследоваться не от стандартных UIViewController, а с префиксом Mvvm. Если вы сопоставите UIViewController с какой-либо ViewModel, то при вызове метода ShowAsync, будет совершена навигация на новый UIViewController этого типа.
    • Поддержка модальной навигации для UIViewController.
    • Управление состоянием для UIViewController. Состояния UIViewController уже отслеживается, поэтому вам не нужно вручную вызывать методы для сохранения/восстановления ViewModel.
    • Поддержка DataTemplateSelector для Binding, аналог DataTemplateSelector для Xaml платформ.
    • Поддержка библиотеки MonoTouch.Dialog.


    Xamarin.Forms


    Особенности MugenMvvmToolkit для 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
    <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 на любых кроссплатформенных проектах, действительно упрощая разработку и сопровождение.
    Для более глубокого понимания обязательно ознакомьтесь с примерами.

    Ссылки:




    P.S. Спасибо моим коллегам по работе за поддержку, отличные идеи и помощь в тестировании.
    Share post

    Similar posts

    Comments 16

      +1
      Расширенный синтаксис выглядит интересно, но опасно — наверняка студия в текущем варианте не может статически проверить его корректность. Было бы хорошо написать плагин для решарпера, который реализует Intellisense и проверки.
        0
        Вы правы, студия не сможет его проверить.
        Плагин уже есть, но он для версии 2.1 и не поддерживает Binding, в новой версии плагина, я постараюсь добавить поддержку синтаксиса.
        –3
        Похоже вы сделали Yet Another MVVMToolkit.
        Чем не устраивает MVVM Light или другие существующие?- из статьи не понятно.
        В чем плюсы? Вы описали «особенности», лично для меня особенности- это как правило костыли и не очевидные моменты, а плюсы то где?
          +1
          Не устраивает тем, что не хватает проработки для каждой платформы. Да, там есть базовый функционал, но взять к примеры Binding, нет ни одного фреймворков, который поддерживает Relative Binding, для всех плафтформ. Или, например работа с Fragment для Android, там есть проблема с сохранением состояния ViewModel, MvvmCross ее до сих пор не решил, вот тема на stackoverflow.
          Например, в MVVM Light для показа окна я должен использовать Messenger, вот пример со stackoverflow:
          Пример
          public void ShowView2CommandExecute()
          {
               Messenger.Default.Send(new NotificationMessage("ShowView2"));
          }
          
          public partial class View1 : UserControl
          {
              public View1()
              {
                  InitializeComponent();
                  Messenger.Default.Register<NotificationMessage>(this, NotificationMessageReceived);
              }
          
              private void NotificationMessageReceived(NotificationMessage msg)
              {
                  if (msg.Notification == "ShowView2")
                  {
                      var view2 = new view2();
                      view2.Show();
                  }
              }
          }
          

          Для меня такой способ навигации выглядит некрасиво потому что, я должен на каждой платформе в каждом классе View писать такой код, при этом чтобы узнать, когда окно закроется я опять должен писать код во View, чтобы уведомить ViewModel. Кроме, того я не могу сам создать ViewModel с нужными параметрами потому, что окно само создает ViewModel во время показа с помощью ViewModelLocator.
          В случае MugenMvvmToolkit вы сами создаете ViewModel, и для отображения не нужно писать никакой дополнительный код в классе View.
            0
            Так может лучше было бы свои идеи продвинуть пулл-реквестами в MvvmCross, у которого уже большое комьюнити, доки, плагины.
              0
              Слишком разные подходы, фундаментально проекты сильно различаются, поэтому было бы сложно реализовать все возможности на базе MvvmCross без полного переписывания.
          0
          >> собраны в portable class library

          А под каким профилем?
            0
            Есть несколько проектов: Profile259, Profile104, Net 4.0.
            0
            del
              0
              Выглядит очень круто, правда. Описанные проблемы очень актуальны, постоянно с ними сталкиваешься.
              Пара вопросов:
              1. Насколько фреймворк готов к продакшену? Есть ли выпущенные приложения?
              2. С утечками памяти проблем нет? Насколько я вижу, все View будут создаваться фреймворком, «ручной» доступ к ним будет затруднён. Есть ли уверенность, что View будут вовремя разрушены?
              3. Убедите меня, что фреймворк будет развиваться и через условный год будет в актуальном состоянии :) Кто-то кроме вас участвует в разработке?
                0
                1. Насколько фреймворк готов к продакшену? Есть ли выпущенные приложения?

                Проект полностью готов к продакшену. В нашей фирме мы активно используем фреймворк для основных проектов. Проекты написаны на WPF, Silverlight и WinForms — это enterprise приложения, они имеет достаточно сложную бизнес логику и состоят из нескольких десятков форм.
                Также, сейчас я пишу проект для мобильных платформ, проект находится в стадии разработки.
                2. С утечками памяти проблем нет? Насколько я вижу, все View будут создаваться фреймворком, «ручной» доступ к ним будет затруднён. Есть ли уверенность, что View будут вовремя разрушены?

                Утечек памяти нет, я много времени провел с профайлером, чтобы этого не допустить. Кроме того, архитектура проекта позволяет самим контролировать процесс создания и освобождения View, за это отвечает интерфейс IViewManager.
                В примерах есть проект Binding, там есть счетчик освободившихся ресурсов, чтобы показать, что никаких утечек памяти нет.
                3. Убедите меня, что фреймворк будет развиваться и через условный год будет в актуальном состоянии :) Кто-то кроме вас участвует в разработке?

                Я пишу этот проект с 2013 года, и у меня нет планов завершать проект. Проект пишу один, т.к. проект нигде особо не «рекламировался» и комьюнити еще не сложилось. Я всегда буду рад помощи, т.к проект open-source, вы всегда можете предложить свою идею или помощь. Если у вас возникнет проблема, вы всегда можете написать мне, и я буду рад помочь в решении вашей проблемы.

                В ближайших планах, написать основную часть документации по проекту.
                0
                Я с месяца два наткнулся на эту разработку, посоветовали в комментарии к статье. Возможно даже вы сами. У меня такой вопрос, я в ней ковырялся, дошел до того, как реализован байндинг для Windows Forms, я так понимаю, сделано это на ограниченном наборе компонентов, поставляемых стандартной поставкой с VS, путем привязки к их событиям с одной стороны и привязки к событиям INPC с другой стороны, я прав? Что будет, если я захочу использовать компоненты DevExpress или иные? Только допиливать самому?
                  0
                  Binding не зависит от компонентов. Для работы Binding в одну сторону (OneWay) из источника в компонент, можно использовать любое свойство компонента. Для работы Binding в две стороны (TwoWay) нужно, чтобы компонент умел уведомлять об изменении свойства, например если свойство называется SelectedIndex, то должно быть событие SelectedIndexChanged.
                  Что будет, если я захочу использовать компоненты DevExpress или иные? Только допиливать самому?

                  Если нужного вам свойства или события нет, вы всегда можете использовать присоединяемые свойства, события и методы для того, чтобы расширить тип.
                  Пример из статьи
                  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);


                  В данном пример, мы сами реализуем свойство SelectedItem для DataGridView. Это очень мощный подход, который позволяет вам добавить желаемое поведение в любой компонент.
                  0
                  Есть MVVM фреймворк, ориентированный на кроссплатформ: ReactiveUI. Любителям Rx — must have
                    +1
                    Очень любопытно.

                    1) У вас есть что-то готовое для списков?
                    MVVMCross для меня особенно хорош благодаря собственным MvxListView/MvxBaseTableViewSource которые абстрагируют довольно запутанный процесс создания списков и приближают его к понятному любому .net программисту DataTemplate.

                    2) Я не увидел поддержку ValueConverters, она не предусмотрена?

                    Вообще, если вы заинтересованы в продвижении своего фреймворка, я рекомендую попробовать пойти по пути Стюарта и записать серию видео (можно даже с теми же примерами). Хотя это конечно адский труд
                      0
                      1) У вас есть что-то готовое для списков?

                      Поддержка для списков есть на всех платформах. В MVVMCross для этого используются свои контролы, MugenMvvmToolkit использует присоединяемые свойства для стандартных контролов.
                      Свойтсво ItemsSource, ItemTemplate и ItemTempateSelector для Android поддерживают следующие контролы: ViewGroup(ListView, Spinner, LinearLayout), TabHost, RecyclerView, ViewPager, ActionBar, IMenu.
                      Пример Binding для ItemsSource
                        <ListView
                          android:layout_width="fill_parent"
                          android:layout_height="wrap_content"
                          pkg:ItemTemplate="@layout/_productlisttemplate"
                          pkg:Bind="ItemsSource GridViewModel.ItemsSource; SelectedItem GridViewModel.SelectedItem; ScrollToSelectedItem true" />
                      


                      Кроме, того есть поддержка DataTemplateSelector (аналог DataTemplateSelector для Xaml).
                      На Android за это отвечают интерфейсы IDataTemplateSelector и IResourceDataTemplateSelector, вы можете использовать более сложную логику для выбора шаблона:
                      Пример
                      public class ListItemTemplateSelector : ResourceDataTemplateSelectorBase<ListItemModel>
                      {
                      	public override int TemplateTypeCount
                      	{
                      		get { return 2; }
                      	}
                      	
                      	protected override int SelectTemplate(ListItemModel item, object container)
                      	{
                      		if (item.IsValid)
                      		   return Resource.Layout._ListItemTemplate;
                      
                      		return Resource.Layout._ListItemTemplateInvalid;
                      	}
                      }
                      
                      //Регистрация селектора:
                      BindingServiceProvider
                                      .ResourceResolver
                                      .AddObject("listItemTemplateSelector", new BindingResourceObject(new ListItemTemplateSelector()));
                      

                      <ListView android:layout_width="fill_parent"
                                android:layout_height="wrap_content"
                                pkg:Bind="ItemsSource ItemsSource; ItemTemplateSelector $listItemTemplateSelector;" />
                      



                      Свойтсво ItemsSource, ItemTemplate для iOS поддерживают следующие контролы: UIView, UITabBarController, UISplitViewController, UICollectionView, UITableView.
                      iOS также поддерживает DataTemplateSelector за это отвечают интерфейсы IDataTemplateSelector, ICollectionCellTemplateSelector и ITableCellTemplateSelector.
                      Ссылки на примеры, где используется списки для Android и iOS.
                      2) Я не увидел поддержку ValueConverters, она не предусмотрена?

                      Поддержка есть, за это отвечает интерфейс IBindingValueConverter. Все конвертеры автоматически регистрируются в ресурсах при старте приложения.
                      Пример Binding с использованием IBindingValueConverter
                      //MyColorConverter - класс который реализует интерфейс IBindingValueConverter
                      Color SourceColor, Converter=MyColorConverter
                      //Эквивалентный синтаксис
                      Color $MyColorConverter(SourceColor)
                      



                      Спасибо за совет про видео, для начала начну писать базовую документацию, а дальше может и для видео время найдется :)

                    Only users with full accounts can post comments. Log in, please.