В настоящей статье задействован мой опыт доведения некоторого числа студентов до полного и окончательного понимания паттерна MVVM и реализации его в WPF. Паттерн описывается на примерах возрастающей сложности. Сначала теоретическая часть, которая может использоваться безотносительно конкретного языка, затем практическая часть, в которой показано несколько вариантов реализации коммуникации между слоями с использованием WPF и, немножко, Prism.

Зачем вообще нужно использовать паттерн MVVM? Это ведь лишний код! Написать тоже самое можно гораздо понятнее и прямолинейнее.

Отвечаю: в маленьких проектах прямолинейный подход срабатывает. Но стоит ему стать чуть больше — и логика программы размазывается в интерфейсе так, что потом весь проект превращается в монолитный клубок, который проще переписать заново, чем пытаться распутать. Для наглядности можно посмотреть на две картинки:


Изображение 1: код без MVVM.


Изображение 2: код с MVVM.

В первом случае программист, со словами «Мне нужно просто соединить вот этот порт и этот, зачем мне все эти хомутики и лейблики?», просто соединяет патчкордом пару слотов. Во втором случае использует некоторый шаблонный подход.

Рассмотрение паттерна на примере №1: Сложение двух чисел с выводом результата


Методика:

Методика написания программы используя подход «ModelFirst».

  • 1. Разработать модель программы.
  • 2. Нарисовать интерфейс программы.
  • 3. Соединить интерфейс и модель прослойкой VM.

Итак, наше задание — написать программу сложения двух чисел с выводом результата. Выполняем первый пункт: пишем модель. Модель — это место в программе, которое может требовать некоторое творческое усилие или задействие своих творческих способностей.

Однако творчество — это довольно затратный по ресурсам мыслительный процесс, и следует избегать излишнее обращение к нему. Во-первых — чтобы не перенапрягаться. Во-вторых — чтобы ваш собрат по программированию (да и вы сами, через пару недель) заглянув к вам в код, не был бы принужден к, излишне увлекательному порой, следованию за творческим полетом мысли. Все места, где вам вдруг удалось применить творчество, — необходимо снабдить подробным комментарием. Кстати, существуют многочисленные паттерны программирования, которые помогают вам это самое творчество — не применять.

Итак, моделью в нашей задаче будет сложение чисел с возвратом результата. Модель, в принципе, может не хранить никакого состояния. Т.е. она может вполне быть реализована статическим методом статического класса. Примерно так:

 static class MathFuncs {
    public static int GetSumOf(int a, int b) => a + b;
  }

Следующий шаг — (см. методику «ModelFirst») — создать View или, проще — нарисовать интерфейс. Это тоже часть, которая может содержать творчество. Но, опять же, не стоить с ним перебарщивать. Пользователь не должен быть шокирован неожиданностями интерфейса. Интерфейс должен быть интуитивен. Наша View будет содержать три текстовых поля, которые можно снабдить лейблами: число номер один, число номер два, сумма.

Заключительный шаг — соединение View и модели через VM. VM — это такое место, которое вообще не должно содержать творческого элемента. Т.е. эта часть паттерна железно обуславливается View и не должна содержать в себе НИКАКОЙ «бизнес логики». Что значит обусловленность от View? Это значит, что если у нас во View есть три текстовых поля, или три места, которые должны вводить/выводить данные — следовательно в VM (своего рода подложке) должны быть минимум три свойства, которые эти данные принимают/предоставляют.

Следовательно два свойства принимают из View число номер один и два, а третье свойство — вызывает нашу модель для выполнения бизнес-логики нашей программы. VM ни в коем случае не выполняет сложение чисел самостоятельно, оно для этого действия только вызывает модель! В этом и состоит функция VM — соединять View (которое тоже ничем иным, кроме как приема ввода от пользователя и предоставления ему вывода не занимается) и Модель, в которой происходит все вычисление. Если нарисовать картинку нашей задачки, то получиться нечто такое:


Изображение 3: Схема Примера №1

Зеленое — это View, три зеленые точки в которой — это наши три текстовые поля. Синее — это VM, к которой эти три зеленых точки железно прибиты (прибиндены), ну а красное облачко — это модель, которая занимается вычислением.

Реализация Примера №1 в WPF


Конкретно в WPF реализована «аппаратная поддержка» паттерна MVVM. View реализуется в XAML. Т.е. зеленый слой (View) будет написана на XAML. Зеленые точки — это будут текстовые поля. А зеленые линии, соединяющиеся с синими — будут реализованы через механизм Binding. Зеленая пунктирная линия — связь всей View и VM осуществляется, когда мы создаем объект VM и присваиванием его свойству DataContext View.

Рисуем View:

<Window ....
xmlns:local "clr-namespace: MyNamespace">  <!-- Это пространство имен с нашей VM -->
<Window.DataContext>
  <local:MainVM/>  <!-- Создаем новый VM и соединяем его со View -->
</Window.DataContext>
<StackPanel>
  <!--Binding, собственно, соединяет текстовое поле со свойством в VM -->
  <!--UpdateSourceTrigger, в данном случае, выполняет передачу значение в VM в момент ввода -->
  <TextBox Width="30" Text="{Binding Number1, UpdateSourceTrigger=PropertyChanged}">
  <TextBox Width="30" Text="{Binding Number2, UpdateSourceTrigger=PropertyChanged}">
  <!--Mode=OneWay необходим для призязки свойства только для чтения -->
  <TextBox Width="30" Text="{Binding Number3, Mode=OneWay}" IsReadOnly="True">
</StackPanel>

Теперь выполняем последний пункт методики — реализуем VM. Чтобы наша VM «автоматически» обновляла View, требуется реализовать интерфейс INotifyPropertyChange. Именно посредством него View получает уведомления, что во VM что-то изменилось и требуется обновить данные.

Делается это следующим образом:

public class MainVM : INotifyPropertyChange
{
  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged(string propertyName) {
      PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

Теперь снабдим VM тремя необходимыми свойствами. (Требования для установления связи VM и View такое, что это должны быть открытые свойства)

private int _number1;
public int Number1 { get {return _number1;}
  set { _number1 = value;
    OnPropertyChanged("Number3"); // уведомление View о том, что изменилась сумма
  }
}

private int _number2;
public int Number2 { get {return _number2;}
  set { _number1 = value; OnPropertyChanged("Number3"); } }

Последнее свойство — это линия пунктирная синяя линия связи VM и модели:

//свойство только для чтения, оно считывается View каждый раз, когда обновляется Number1 или Number2
public int Number3 { get; } => MathFuncs.GetSumOf(Number1, Number2);

Мы реализовали полноценное приложение с применением паттерна MVVM.

Рассмотрение паттерна на примере №2:

Теперь усложним наше задание. В программе будет текстовое поле для ввода числа. Будет ListBox с коллекцией значений. Кнопка «Добавить», по нажатию на которую число в текстовом поле будет добавлено в коллекцию значений. Кнопка удалить, по нажатию на которую выделенное в ListBox'е число будет удалено из коллекции. И текстовое поле с суммой всех значений в коллекции.


Изображение 4: Интерфейс для Примера №2

Согласно методике — необходимо сначала разработать модель. Теперь модель не может быть stateless и должна хранить состояние. Значит в модели будет коллекция элементов. Это раз. Затем — операция добавление некоторого числа в коллекцию — это обязанность модели. VM не может залезать во внутренность модели и самостоятельно добавлять в коллекцию модели число, она обязана просить сделать это саму модель. В противном случае это будет нарушение принципа инкапсуляции. Это как если бы водитель не заливал, как положено, топливо в бензобак и т.д. — а лез бы под капот и впрыскивал топливо непосредственно в цилиндр. То есть будет метод «добавить число в коллекцию». Это два. И третье: модель будет предоставлять сумму значений коллекции и точно также уведомлять об ее изменении через интерфейс INotifyPropertyChanged. Не будем разводить споры о чистоте модели, а будем просто использовать уведомления.

Давайте сразу реализуем модель:

Коллекция элементов должна уведомлять подписчиков о своем изменении. И она должна быть только для чтения, чтобы никто, кроме модели, не могли ее как-либо изменить. Ограничение доступа — это выполнение принципа инкапсуляции, оно должно соблюдаться неукоснительно, чтобы: а) самому случайно не создать ситуацию трудноуловимого дебага, б) вселить уверенность, что поле не изменяется извне — опять же, в целях облегчения отладки.

Кроме того, так так мы далее все равно подключим Prism для DelegateCommand, то давайте сразу использовать BindableBase вместо самостоятельной реализации INotifyPropertyChange. Для этого надо подключить через NuGet библиотек Prism.Wpf (на момент написания 6.3.0). Соответственно OnPropertyChanged() измениться на RaisePropertyChanged().

public class MyMathModel : BindableBase
{
  private readonly ObservableCollection<int> _myValues = new ObservableCollection<int>();
  public readonly ReadOnlyObservableCollection<int> MyPublicValues;
  public MyMathModel() {
    MyPublicValues = new ReadOnlyObservableCollection<int>(_myValues);
  }
  //добавление в коллекцию числа и уведомление об изменении суммы
  public void AddValue(int value) {
    _myValues.Add(value);
    RaisePropertyChanged("Sum");
  }
  //проверка на валидность, удаление из коллекции и уведомление об изменении суммы
  public void RemoveValue(int index) {
      //проверка на валидность удаления из коллекции - обязанность модели
    if (index >= 0 && index < _myValues.Count) _myValues.RemoveAt(index);
    RaisePropertyChanged("Sum");
  }
  public int Sum => MyPublicValues.Sum(); //сумма
}

Согласно методике — рисуем View. Перед этим несколько необходимых пояснений. Для того, чтобы создать связь кнопки и VM, необходимо использовать DelegateCommand. Использование для этого событий и кода формы, для чистого MVVM — непозволительно. Используемые события необходимо обрамлять в команды. Но в случае с кнопкой такого обрамления не требуется, т.к. существует специальное ее свойство Command.

Кроме того, число, которое мы будем добавлять, используя DelegateCommand, мы будем не биндить к VM, а будем передавать в качестве параметра этого DelegateCommand, чтобы не загромождать VM и избежать рассинхронизации непосредственного вызова команды и использования параметра. Обратите внимание на получившуюся схему и, особенно, на место, обведенное красной линией.


Изображение 5: Схема для Примера №2

Здесь привязка на View происходит не вида View <=> ViewModel, а вида View <=> View. Для того, чтобы этого добиться используется второй вид биндинга, где указывается имя элемента и его свойства, к которому осуществляться привязка — "{Binding ElementName=TheNumber, Path=Text}".

<Window ...>
    <Window.DataContext>
        <local:MainVM/>   <!-- Устанавливаем DataContext -->
    </Window.DataContext>
    <DockPanel>
        <!-- Число для добавления в коллекцию -->
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
            <TextBox x:Name="TheNumber" Width="50" Margin="5"/>
            <Button Content="Add" Margin="5" Command="{Binding AddCommand}"
                    CommandParameter="{Binding ElementName=TheNumber, Path=Text}"/>
        </StackPanel>
        <!-- Сумма -->
        <TextBox DockPanel.Dock="Bottom" Text="{Binding Sum, Mode=OneWay}" Margin="5"/>
        <!-- Кнопка удаления из коллекции -->
        <Button DockPanel.Dock="Right" VerticalAlignment="Top" Content="Remove"
                Width="130" Margin="5"
                Command="{Binding RemoveCommand}"
                CommandParameter="{Binding ElementName=TheListBox, Path=SelectedIndex}"/>
        <!-- Коллекция -->
        <ListBox  x:Name="TheListBox" ItemsSource="{Binding MyValues}"/>
    </DockPanel>
</Window>

Теперь реализуем ViewModel:

public class MainVM : BindableBase
{
  readonly MyMathModel _model = new MyMathModel();
  public MainVM()
  {
    //таким нехитрым способом мы пробрасываем изменившиеся свойства модели во View
    _model.PropertyChanged += (s, e) => { RaisePropertyChanged(e.PropertyName); };
    AddCommand = new DelegateCommand<string>(str => {
      //проверка на валидность ввода - обязанность VM
      int ival;
      if (int.TryParse(str, out ival)) _model.AddValue(ival);
    });
    RemoveCommand = new DelegateCommand<int?>(i => {
        if(i.HasValue) _model.RemoveValue(i.Value);
    });
  }
  public DelegateCommand<string> AddCommand { get; }
  public DelegateCommand<int?> RemoveCommand { get; }
  public int Sum => _model.Sum;
  public ReadOnlyObservableCollection<int> MyValues => _model.MyPublicValues;
}

Внимание — важно! Касательно проброса уведомлений из модели. Уведомлять об изменении суммы самостоятельно VM не может, т.к. она не должна знать, что именно измениться в модели, после вызова ее методов и измениться ли вообще. Модель для VM должна быть черным ящиком. Т.е. она должна передавать ввод и действия пользователя в модель и если в модели что-то изменилось (о чем должна ее уведомлять сама модель), то только тогда уведомлять далее View.

Мы реализовали второе полноценное приложение с применением паттерна MVVM, познакомились с ObservableCollection, DelegateCommand, привязкой вида View <=> View и пробросом уведомлений во View.

Комментарии (50)


  1. vistauser1990
    22.09.2017 22:35

    Давно читаю хабр, но зарегаться не решался. Пишу первый коммент так что сильно не бейте. Большое спасибо автору за статью. Однако основным из преимуществ использования архитектурного подхода (любого, не только MVVM) является возможность модульного тестирования. Например конструктор VM во втором примере — штука как минимум сложно тестируемая, если вообще тестируемая. Мне кажется стоило бы немого разгрузить этот конструктор. А также просмотреть остальные примеры на возможность тестирования. Если есть какой-то способ тестировать такие штуки, с удовольствием о нём узнаю. Ещё спасибо за интересный материал.


    1. oggr Автор
      22.09.2017 22:46

      Совершенно верно: разделение ответственности в большой степени способствует повышению тестируемости. Т.е. когда мы выдираем модель из behind code обработчиков событий и собираем в одном месте — тестировать ее значительно легче. Вот, собственно, модель обычно и покрывают юнит-тестами. Например: в первом примере необходимо убедиться, а действительно ли статическая функция складывает два числа корректно…
      Касательно покрытия тестами VM — этим обычно… ну, во всяком случае я, — не занимаются. Текст VM совершенно плоский. И вызовы делегатов из VM — тоже вполне планарны. А вот то, что они вызывают в модели — покрывать тестами действительно стоит.


      1. vistauser1990
        22.09.2017 23:16

        По поводу модели — согласен. Готов согласится с тем, что тестировать делегаты не всегда нужно, но есть одна вещь которая требует дополнительных комментариев. Проверка валидности ввода должна быть покрыта тестами. В вашем случае это вроде как не так очевидно, но если мы сменим числа, например, на email то покрывать — обязательно. Или такая логика уже выводится в уровень модели и считается бизнес логикой?

        ЗЫ: Я бы все таки покрыл тестами вызов правильных методов в модели из VM.


        1. oggr Автор
          23.09.2017 07:23

          Проверку валидности ввода email можно организовать еще на этапе вьюшки, — есть data annotations, ErrorInfo и т.п. — такое проверять не надо. Если проверка комплексная, то выносим ее в модель, в какую-нибудь IsEntityValid, и там проверяем.
          Просто есть идеальное видение, с полным покрытием кода, и есть реальный мир, где нередко программисты вообще не пишут юнит-тесты. Растеряв силы или заскучав на проверке VM, они могут и не добраться до модели, а так — пусть хоть модель покроют.


          1. belousovsw
            23.09.2017 13:59

            А что будет если Вы пишете веб приложение и юзер отправит в Ваш контроллер данные не через валидирующую форму а напрямую в контроллер, через url get/post запрос. Тогда это может сломать Вашу модель или не дай бог базу данных.


            1. oggr Автор
              23.09.2017 14:04

              Ну, во-первых в вебе валидировать надо (и) на серверной стороне, конечно. Во-вторых в комментах про тестирование, а у вас мысль уже куда-то в сторону пошла


  1. xmetropol
    23.09.2017 14:11

    В моём понимании любой шаблон проектирования предназначен для решения определённой категории задач. Часто, читая статью про какой-либо шаблон проектирования, я вижу пример реализации этого шаблона для решения какой-то элементарнейшей задачи. При этом я вижу кучу непонятных абстракций и усложнений кода, реализация которых, зачастую, сложнее решения этой самой элементарнейшей задачи. Мне говорят, как надо делать, как нельзя делать, но не объясняют зачем вообще все это делать. Вот статья — классический пример. Аргументация такая: посмотрите на серверную стойку с лапшой из проводов, и посмотрите на другую стойку, где красота и порядок. Так вот пример с серверной стойкой мне, очевидно, понятен. И у меня не возникает вопросов, зачем использовать хомутики и жгутики. Ну а далее что? Давайте напишем даже не калькулятор, а просто сумматор 2-х чисел и будем MVVM-ить. Из статьи я понял как нужно соединять порты, но не понял для чего мне MVVM. Так что, если Вы беретесь объяснять за шаблоны проектирования, приведите пример, на котором применение этого самого шаблона будет аргументировано и будет видна его польза. Ну да ладно, всё-таки это часть 1, возможно, дальше будет больше и понятней.

    Теперь вопрос по существу (возможно на вопрос «зачем» из 1-й части моего комментария Вы ответите здесь). Предположим есть UserControl, он состоит из 2-х частей View (Xaml) и Codebehind (cs). Во View значит я верстаю саму форму, кладу TextBox. В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам. Т.е. Codebehind UserControl'а это Ваш ViewModel. Так для чего же мне уже имея View и Codebehind создавать ещё один файл и выносить туда какой-либо код?


    1. oggr Автор
      23.09.2017 14:24

      Два вопроса вижу:
      1. Для чего MVVM? Отвечаю: для того, чтобы у вас была самодостаточная модель, сгруппированная в одном месте. Чтобы была отделена от инфраструктурного кода. Если, условно, инфраструктурный код — это 70%(и больше) от всего кода, а бизнес-логика — 30% — то очень хорошо эту модель иметь в одном месте, а не размазанную по обработчикам событий кучи контролов. Тогда модель поддается тестированию, изменению функционала, она компактнее, понятнее и т.д. Всегда можно взять модель и написать совершенно другой инфраструктурный код, для другой платформы, другой библиотеки и т.д.
      2. Почему нельзя использовать кодбехайнд в качестве VM?
      Отвечаю: никто не запрещает вам использовать кодбехайнд в качестве DataContext самого себя, но только в случае, если у вас на один View одна VM. Но часто бывает, что одна VM используется для многих View. Это удобно делать, чтобы не городить огромную вьюшку, а разбить ее на ряд вьюшек поменьше — их тогда и перекомпоновать будет проще. В таком случае нелогично использовать один контрол в качестве DataContext других контролов.


      1. xmetropol
        23.09.2017 15:07

        1. В любом приложении есть модель и её представление. Шаблонов проектирования, в которых модель связывается с представлением много: MVC, MVP, MVVM. То, что модель должна быть отделена от инфраструктурного кода — это как раз понятно. Вопрос больше про то, для чего использовать сам шаблон MVVM. Что его использование даёт по сравнению с тем, что я напишу контроллер, через который вид взаимодействует с моделью. Или я буду просто в Codebehind взаимодействовать с моделью. Вопрос не о M, а о VM из этого шаблона. Я на самом деле знаю ответ на этот вопрос. Просто, читая статью о шаблоне проектирования, хотел бы видеть в ней объяснение. Возможно новички прочитают и не поймут для чего всё это нужно.


        2. Самому UserControl'у необязательно устанавливать DataContext'ом ссылку на себя, для того, чтобы использовать привязки внутри. Можно, например, задать имя контролу и в привязках ссылаться на контрол по этому имени:
          <UserControl x:Name="CalcView">   
          <TextBox Text="{Binding Path=Value1, ElementName=CalcView}" />
          </UserControl>

          Использовать один контрол в качестве DataContext другого нелогично, я с Вами согласен. Вот Вы пишете, что часто бывает одна большая VM которая используется для многих View, но городить один большой View не хотите. А почему тогда получается 1 большая VM, котороая во многих View участвует? Почему её не нужно разбивать на более мелкие части, как это делаете с View? Или это что-то вроде медиатора, который устанавливает связь между различными частями модели в 1-м месте, но используется во многих местах? Т.е. другими словами мне пока не понятен смысл выносить код взаимодействия с моделью в отдельный класс, когда есть Codebehind у UserControl. В Вашем примере получается, что Codebehind во всех представлениях чаще всего останется пустым.



        1. oggr Автор
          23.09.2017 16:09

          1. Так вопрос в чем преимущество MVVM над MVP или MVC?
          Ну, вот конкретно в WPF в том, что MVVM «аппаратно» поддерживается WPF. View понимает и INotifyPropertyChange, и Observable и др. — ее не надо руками обновлять через презентер. Интерфейс вьюшки городить не надо. Биндинг в конце-концов. Мне MVP в WinForms'ах не нравился тем, что уж очень там много руками делать приходилось.


        1. oggr Автор
          23.09.2017 16:17

          2. Не чаще всего code behind останется пустым, а всегда будет оставаться пустым, если используется чистый MVVM.
          Одна VM для нескольких вьюшек используется, не столько для того, чтобы расшарить контекст, а скорее для того, чтобы разбить вью на физические части, а потом скомпоновать в общей вьюшке. Просто View в XAMLе могут быть достаточно… громоздкими, аляповатыми, знаете, со всеми этими <Grid.RowDefinitions>… да и вообще, как любой XMLобразный код.
          В вашем примере с привязками вы биндитесь к DependencyProperty, в случае же использования DataContext подойдут регулярные свойства. Они по объему кода гораздо скромнее


          1. xmetropol
            23.09.2017 18:46

            Никто не заставляет Вас биндиться к Dependency свойствам. Можно и в UserControl создавать обычные CLR свойства и реализовывать INotifyPropertyChanged (хотя это было бы странно, учитывая что наследуемся от DependencyObject). Вы не подумайте, что я придраться хочу и доказать, что MVVM плохо. Я задаю Вам простые вопросы с целью понять, для чего мне MVVM, почему я не могу считать Codebehind UserControl'а VM'ом. Название Вашей статьи предполагает, что Вы в полной мере объясните цель и причины использования MVVM в WPF. Вот я и хочу укрепить свои знания и получить еще одно видение.


            1. oggr Автор
              24.09.2017 00:14

              Хм, интересно) И при невыставленном DataContext работает такое использование регулярных свойств в качестве соурса биндинга.
              На мой взгляд то, где будет VM физически — это незначимая деталь реализации. Но можно дать такой ответ:
              В вашей конструкции при биндинге надо всякий раз указывать имя элемента управления, и второе: неясно, что делать, в случае нескольких View на один VM.


          1. IL_Agent
            23.09.2017 23:21

            Не понял первую фразу. Что значит “чистый mvvm”? И где же размещать логику вьюхи, не зависящую от модели, как не в code behind?


            1. oggr Автор
              24.09.2017 00:26

              Логика вьюхи? Если имеется ввиду всякие IsVisible, IsEnabled — то в VM.
              Если всякие украшательства, типа анимации — то в XAMLe. Если в XAML они не лезут, то тогда в код бехайнд.
              Чистый MVVM значит, что можно миксовать MVVM и MVP. Хотя можно с помощью windows.interactivity попытаться оставаться в рамках MVVM, но если программа сильно «вьюхоцентричная», то можно создать интерфейс вьюшки и вызывать его.


              1. IL_Agent
                24.09.2017 01:05

                Меня вот эта фраза смутила:

                code behind… всегда будет оставаться пустым, если используется чистый MVVM

                Использование code behind никак не противоречит MVVM, если он используется для вьюшной логики, т.е. не зависящей от модели. Например, если видимость зависит от состояния модели (наличие заказов заказов у клиента), то да, оно будет во вью-модели как IsVisible (или вообще через конвертер вычисляться). А если видимость зависит от размера окна, то во вью-модели ей делать нечего.
                А из вашей же фразы можно сделать вывод, что если code behind используется, то MVVM «грязный». И да, некоторые «умельцы», рассуждая так, доходят до того, что прокидывают ссылку на вью во вью-модель, пилят логику с ней там и хвастаются, что у них code-behind пустой :).


                1. oggr Автор
                  24.09.2017 13:11

                  Так вот как раз у этих «умельцев», несмотря на пустой код-бехайнд, — MVVM не чистый, а разбавленный )
                  А так — вполне согласен, что код-бехайнд может содержать код, если его наличие/отсутствие никак не сказывается на модели.


        1. xmetropol
          23.09.2017 18:40

          Да, вопрос именно в чем преимущество. MVVM позволяет нам писать код в декларативном виде (Xaml), используя событийную модель. Т.е. INotifyPropertyChanged, Binding, Multibinding, ICommand, Trigger и вся прочая ерунда, поддерживаемая самой платформой напрямую. Когда на собеседовании я спрашиваю у кандидата для чего MVVM, мне бы хотелось услышать объяснение, увидеть понимание сущности самого шаблона, способности объяснить его отличие от MVC, к примеру, а не просто потому что нам так сказали. Какой смысл пользоваться чем-то, когда не понимаешь смысла, не видишь отличий от других подходов. Рассказывая кому-либо о шаблоне проектирования, я бы начинал от классификации (структурный, поведенческий, порождающий), дальше цель шаблона и пример решаемых с его помощью задач, причина, по которой следует использовать тот или иной подход. Голословного «в WPF так принято» не достаточно.


      1. Deosis
        24.09.2017 09:38

        Один из плюсов использования связки mvvm и wpf это использование шаблонов. Такой подход позволяет заменить почти всех наследников UserControl на DataTemplate.
        Дополнительно упрощается поддержка вложенных vm.


      1. Durimar123
        25.09.2017 17:15

        Согласен с предыдущим автором — вы смогли понятно расписать как использовать шаблон MVVM, но зачем его использовать непонятно, и тем более, чем он лучше?

        Мое мнение, что лучше его вообще не использовать, кроме двух случаев.
        1 Это кросплатформенная программа тогда в идеале у каждой платформы меняется только View.
        2 У вас есть офигенный дизайнер который может ваять формы в Blend, а программеры уже просто подключают свои поля куда надо. (но для большинства это наверно из области фантастики)

        Больше я не вижу, чем MVVM лучше. Куча геморроя, с текстовыми значениями, без перехода по ссылкам, с диким пробросом данных в кривые контролы и попапы.
        А уж если кто-то «мудрый» решил подключить Caliburn…


    1. Sing
      24.09.2017 02:51

      Предположим есть UserControl, он состоит из 2-х частей View (Xaml) и Codebehind (cs). Во View значит я верстаю саму форму, кладу TextBox. В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам. Т.е. Codebehind UserControl'а это Ваш ViewModel. Так для чего же мне уже имея View и Codebehind создавать ещё один файл и выносить туда какой-либо код?

      Допустим, я сделел UserControl для отрезка дат (с даты — до даты, два DatePicker`а). И для своей модели сделал им привязку к, например, датам поиска вылета самолёта. Тут следует начать с того, что это нарушение принципа единственной обязанности, что скоро меня приведёт к печальным последствиям.

      Тут меня попросили сделать такой же контрол, но для дат выезда поездов. Что мне делать? Наследоваться? Копипастить? Я бы, всё-таки, хотел иметь возможность просто указать, откуда мне брать данные для начала и конца отрезка дат. Вот тут мне может помочь VM и не может помочь CodeBehind.


      1. xmetropol
        24.09.2017 10:06

        Я тут не очень понял суть проблемы. В моём понимании Вы создаете элемент управления DateRangeSelector, наследуясь от UserControl, в нём создаёте 2 DependencyProperty StartDate, EndDate. В Xaml кладёте 2 DatePicker и связываете их с этими свойствами. Во всех местах, где Вам нужно выбирать интервал из дат Вы кладёте этот элемент управления и связываете его свойства StartDate и EndDate со свойствами VM.


        1. Sing
          24.09.2017 11:58

          Проблема в том, что вы хотите делать это в Codebehind. Codebehind — это часть самого элемента и вы будете тащить её всюду с ним.


          1. xmetropol
            24.09.2017 13:06

            Я всё ещё не вижу проблемы. Вы говорите очевидные вещи, Codebehind — часть самого элемента, кто же спорит. Что означает, я буду её тащить всюду? Вы привели пример задачи, выбор интервала даты. Я Вам объяснил как её решить, и MVVM к этой задаче вообще не имеет отношения. Вот даже в этой задаче, скажем конечная дата не может быть меньше начальной. Вы это условие обеспечивать где собираетесь, Codebehind контрола выбора дат, или где?


            1. Sing
              24.09.2017 14:34

              В смысле не имеет отношения? Привязка контрола к данным не имеет отношения? Выше вы писали

              В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам.
              Если вы привяжетесь к одной какой-то модели, то эта привязка станет частью контрола, и использвоать в другом проекте вы его не сможете, не потащив за собой модель этого. Это очевидные вещи, да. Но вы почему-то о них спрашиваете.


              1. xmetropol
                24.09.2017 15:15

                Я задавал вопрос автору статьи про Codebehind и хотел выяснить разницу между VM и Codebehind. Вы же привели в пример конкретную задачу. Так вот элемент выбора интервала дат — это самостоятельный элемент управления, точно также как и DatePicker — элемент управления для выбора 1-й даты и они к MVVM вот вообще никакого отношения не имеют чуть более чем на 100%.

                Если вы привяжетесь к одной какой-то модели, то эта привязка станет частью контрола, и использвоать в другом проекте вы его не сможете, не потащив за собой модель этого. Это очевидные вещи, да. Но вы почему-то о них спрашиваете.

                Здесь я Вас уже перестал понимать, в контексте предложенной Вами задачи.


                1. Sing
                  24.09.2017 15:41

                  Я задавал вопрос автору статьи про Codebehind и хотел выяснить разницу между VM и Codebehind. Вы же привели в пример конкретную задачу.
                  Я привёл конкретный пример вашей же задачи для простоты восприятия.

                  Всё началось с:
                  Предположим есть UserControl, он состоит из 2-х частей View (Xaml) и Codebehind (cs). Во View значит я верстаю саму форму, кладу TextBox.
                  Любой UserControl должен быть самостоятельным элементом управления.

                  Затем
                  В Codebehind я создаю такие же команды и свойства, как это делаете Вы в своей ViewModel, и во View точно также создаю привязки к этим свойствам и командам. Т.е. Codebehind UserControl'а это Ваш ViewModel.

                  Некорректность утверждения о том, что «Codebehind UserControl'а это Ваш ViewModel» я вам в этой ветке и освещаю. Разница в том, что Codebehind — это часть самого элемента View, а VM же подсказывает View, откуда ему брать данные и какие команды использовать. VM может содержать ссылки на модель, Codebehind — нет. VM могут быть разные, предоставляющие разные данные и разные команды, Codebehind для контрола всегда один и тот же.

                  В итоге, могу предположить, что вы что-то своё понимаете под UserControl.


                  1. xmetropol
                    24.09.2017 17:07

                    Вы привели задачу для контрола выбора интервала дат в качестве примера как использовать MVVM и сейчас за уши притягиваете простоту восприятия. Для озвученной задачи, как я уже сказал выше, нужен Control, это не View модели, представляющей интервал дат. Так зачем на изначально некорректном примере доказывать правильность использования MVVM.
                    Вобщем, вступать в дальнейший спор (или продолжать) я не хочу.


  1. johnnymmc
    23.09.2017 23:44

    MVVM — это прекрасный и очень естественный (хотя на первый взгляд, для привыкших к другому, это и не очевидно) подход. Но проблема в прибитости WPF гвоздями к Windows: если хочешь, чтобы приложение было кроссплатформенным, приходится использовать WinForms, а там с MVVM туго.


    1. oggr Автор
      24.09.2017 00:28

      Думаю, появиться что-нибудь, типа UWP. Только будет не Universal Windows Platform, a просто — Universal Platform


    1. IL_Agent
      24.09.2017 01:16
      +1

      Что же в WinForms кросплатформенного? Наличие кое-какого порта под Mono? Xaml, благодаря Xamarin и разным проектам, вроде Avalonia, выглядят более кросплатформенно.


    1. gimbarr_dpr
      24.09.2017 13:12

      есть такая вещь, как Xamarin (forms, android, ios). Там не привязано к винде, кроссплатформенно и MVVM


  1. dmitry_dvm
    23.09.2017 23:48

    Автор, а вы ничего не путаете? Логика в модели? И логика работы с DAL тоже в модели? По-моему модель это именно модель данных, набор свойств с минимумом логики, необходимой для этих свойств. В вашем примере со сложением логике сложения, конечно, самое место в модели, но если логика чуть сложнее, то в модели ей не место.
    Пример выбран неудачно, мввм тут из пушки по воробьям .


    1. oggr Автор
      24.09.2017 00:38

      Где же быть логике, как не в модели? О_0 Бизнес-логика она в модели. DAL я трактую, как бедные репозитории, просто доступ к данным, но логика- дожна быть отдельно.
      Конечно сложение чисел с МВВМ — это оверкил, но позволяет сосредоточится на теории, не загромождая все реализацией. К тому же это не последний пример


  1. Sing
    24.09.2017 02:29

    Модель, в принципе, может не хранить никакого состояния. Т.е. она может вполне быть реализована статическим методом статического класса.
    В широком понимании модели, да. Но, как правило, под моделью понимается доменная модель.
    если у нас во View есть три текстовых поля, или три места, которые должны вводить/выводить данные — следовательно в VM (своего рода подложке) должны быть минимум три свойства, которые эти данные принимают/предоставляют.
    Это не так. В VM у нас может быть один объект и это могут быть его свойства.
    Синее — это VM, к которой эти три зеленых точки железно прибиты (прибиндены)
    Биндинги в WPF — это нечто прямо противоположное «железному прибитию». По умолчанию, они прописываются простыми строками и им плевать на то, есть ли свойство или нет — просто всё перестанет работать.
    public int Number1
    Пожалуйста, не называйте так свойства. И поля. И вообще ничего. В принципе, рекомендую уделить внимание качеству кода.
    Затем — операция добавление некоторого числа в коллекцию — это обязанность модели. VM не может залезать во внутренность модели и самостоятельно добавлять в коллекцию модели число, она обязана просить сделать это саму модель. В противном случае это будет нарушение принципа инкапсуляции.
    Тут немножко странные вещи описаны, начиная с того, что инкапсуляция — это не принцип, а механизм и нарушить её хоть и можно, но не таким способом.
    Для того, чтобы создать связь кнопки и VM, необходимо использовать DelegateCommand.
    Я надеюсь, что имелся в виду ICommand.
    //таким нехитрым способом мы пробрасываем изменившиеся свойства модели во View
    _model.PropertyChanged += (s, e) => { RaisePropertyChanged(e.PropertyName); };
    Увы, нет. Мы не «пробрасываем свойства», мы используем один из плохих приёмов программирования — программирование по совпадению. Мы вызываем событие у одного объекта, используя название изменившегося свойства другого. Да, в этом примере всегда будет передаваться Sum, и по совпадению (!) такое свойство есть у VM и оно даже (по совпадению!) отображает те данные что нам нужны. Изменить название свойства или его логику — всё поломается. Так делать нельзя.
    //проверка на валидность ввода — обязанность VM
    Смелое утверждение. Я вот считаю, что проверка на валидность того, что вводит пользователь — обязанность View. Если же проверка на валидность — обязанность VM, то как и где он будет уведомлять пользователя в случае ошибки?


    1. oggr Автор
      24.09.2017 13:50

      Это не так. В VM у нас может быть один объект и это могут быть его свойства

      Биндинги в WPF — это нечто прямо противоположное «железному прибитию»
      Не придирайтесь. Я имею ввиду, что сколько во вью открыто «слотов» для биндинга, столько должно ему предоставлять VM. А под словом «железно» имеется ввиду, что VM это не нечто, как модель — абстрактное в вакууме, а такое нечто, что зависит от вью, имеет открытых «слотов» столько, чтобы удовлетворить свою View
      … инкапсуляция — это не принцип, а механизм

      Инкапсуляция — это как раз принцип. А уж как именно вы инкапсулируете — это механизм.
      Увы, нет. Мы не «пробрасываем свойства», мы используем один из плохих приёмов программирования — программирование по совпадению…
      В статье акцент сделан в первую очередь на MVVM, а это удобная конструкция, избавляющая от необходимости прописывать каждое пробрасываемое свойство из модели. В реальных проектах такое не прокатывает и чревато — это понятно.
      … считаю, что проверка на валидность того, что вводит пользователь — обязанность View..
      Имелось ввиду валидация в конкретном случае. Вообще же — валидация сначала во View (уже даже самим типом контрола — используя RadioButton сложно ввести невалидное значение). Если нельзя во вью, то в VM. Если нельзя в VM, то задействуем Модель.


      1. Sing
        24.09.2017 14:50

        Не придирайтесь.
        Я не придираюсь к вам, я освещаю некоторые моменты, которые, неосведомлённый читатель поймёт неверно и сделает неверные выводы.
        модель — абстрактное в вакууме
        VM [...] такое нечто, что зависит от вью
        Эти две фразы неверны. VM не зависит от View, а наоборот. Это позволяет использовать VM с разными View.

        Модель — это вполне конкретная вещь.
        Инкапсуляция — это как раз принцип.
        Ну я не знаю, хотя бы в википедии посмотрите. Возможно, вы путаете её с принципом сокрытия данных.
        В статье акцент сделан в первую очередь на MVVM, а это удобная конструкция, избавляющая от необходимости прописывать каждое пробрасываемое свойство из модели.
        Ну так в примере вы именно тем и занимаетесь, что прописываете свойство модели в свойстве VM (это я про сумму). Если будет больше полей — будете больше пропихивать свойств.
        Если нельзя во вью, то в VM. Если нельзя в VM, то задействуем Модель.
        Вот такие «где получится — там и будет» рассуждения и приводят к СКС в стиле картинки №1.


        1. oggr Автор
          24.09.2017 15:53

          Эти две фразы неверны. VM не зависит от View, а наоборот. Это позволяет использовать VM с разными View.

          Я использую две методики написания программ в MVVM: 1.Model first 2. View first
          В обоих VM разрабатывается после View.
          Про модель в вакууме — значит, что она не зависит от VM и View, и ничего про них не знает. У нею своя атмосфера.


          1. Sing
            24.09.2017 17:22

            Я использую две методики написания программ в MVVM: 1.Model first 2. View first
            В обоих VM разрабатывается после View.
            Можно ссылки на описание этих методик?


    1. phoenixbk
      24.09.2017 15:43

      Смелое утверждение. Я вот считаю, что проверка на валидность того, что вводит пользователь — обязанность View. Если же проверка на валидность — обязанность VM, то как и где он будет уведомлять пользователя в случае ошибки?

      А использование интерфейса IDataErrorInfo для VM и указание соответствующих свойств в биндинге во View куда можно отнести? В общем-то получается для валидации задействовано и то, и другое. Возможно я не прав, но разве View должна знать какие данные верны, а какие нет? Она должна лишь уведомлять о том, что данные не верны (красное поле ввода, подсказка что не так).
      Я для себя делал так (уверен что найдется масса ошибок, но я пока ещё учусь):
      Есть правило валидации:


      public class ValidationRule
      {
          public ValidationRule(string columnName, Func<string> rule)
          {
              _columnName = columnName;
              _rule = rule;
          }
          private string _columnName;
          private readonly Func<string> _rule;
          public string ColumnName {
              get {
                  return _columnName;
              }
          }
          public Func<string> Rule {
              get {
                  return _rule;
              }
          }
      }

      И соответственно есть список правил. Их мы задаем в VM. И там же указываем что делать при изменении корректности всего списка правил (например блокируем/разблокируем кнопку сохранения). Через интерфейс IDataErrorInfo поля ввода во View получают информацию о корректности данных и в случае ошибки отображают заданную подсказку.


      public class ValidationObject : IDataErrorInfo
      {
          #region IDataErrorInfo
          private List<ValidationRule> _validationRules = new List<ValidationRule>();
          private Dictionary<string, bool> _propertiesCorrectly = new Dictionary<string, bool>();
          private Action<bool> _changingCorrect;
          public void AddValidationRule(ValidationRule rule)
          {
              if (_validationRules.Where(r => r.ColumnName == rule.ColumnName).Count() != 0)
                  return;
              _validationRules.Add(rule);
              _propertiesCorrectly.Add(rule.ColumnName, false);
          }
          public void SetActionOnChangingCorrect(Action<bool> act)
          {
              _changingCorrect = act;
          }
          string IDataErrorInfo.this[string columnName]
          {
              get
              {
                  ValidationRule rule = _validationRules.FirstOrDefault(x => x.ColumnName == columnName);
                  string result = null;
                  if (rule != null)
                  {
                      result = rule.Rule();
                      _propertiesCorrectly[rule.ColumnName] = (result == null) ? true : false;
                      _changingCorrect?.Invoke(!_propertiesCorrectly.Values.Contains(false));
                  }
                  return result;
              }
          }
          string IDataErrorInfo.Error
          {
              get
              {
                  throw new NotImplementedException();
              }
          }
          #endregion
      }


      1. oggr Автор
        24.09.2017 15:56

        Феникс, не генерируйте такие портянки)


        1. phoenixbk
          24.09.2017 17:55

          Это относится к коду, комментарию или всему вместе? :)


          1. oggr Автор
            24.09.2017 21:22

            Всему


      1. Sing
        24.09.2017 17:12

        Возможно я не прав, но разве View должна знать какие данные верны, а какие нет?
        А почему, собственно, нет? Уточню, что View должна знать о верности данных в контексте взаимодействия с ней пользователя. То есть, например, у нас может быть поле Price, которое в форме ввода не может быть больше 1000, но при этом сами данные — могут быть, если мы их получили в результате каких-то накруток налогов, например, либо получив автоматически из третьего источника. А в другой форме, по каким-то своим причинам, мы ограничим стоимость уже 100.

        При всём при этом, у нас есть возможность ограничить через DataAnnotations сами данные, например, указав, что цена не может быть отрицательной никогда и ни за что.

        Как дополнительный бонус, при просмотре XAML наглядно видно, какие поля какими ограничениями обладают.
        Есть правило валидации:
        Вы, возможно по совпадению, употребляете название библиотечного класса ValidationRule, не наследуясь от него и не используя его во View, где он и должен быть. Почитайте статью, там прописано и как использовать этот класс, и как валидировать во View.

        Ещё раз подчеркну, что во View валидировать необязательно, если только нет каких-то специфических требований, связанных только с конкретным View.

        Кстати, в целом, система валидации в WPF довольно кривая. Объединять ошибки из ValidationRule`s и DataAnnotations — это то ещё веселье. А если использовать виверы (тот же Validar) — то становится ещё веселее.


  1. kostey2204
    24.09.2017 13:14

    Может быть мой вопрос будет освещён во второй части статьи, но задам его сейчас.

    Допустим из одного окна (родительского), по нажатию кнопки показывается другое окно (дочернее).
    Где, согласно принципам MVVM, должны быть инстранцированы View и ViewModel дочернего окна?
    допустимо ли делать это в code behind родительского окна?


    1. oggr Автор
      24.09.2017 13:29

      Я это делаю в VM родительского окна:

      ShowSomeWindowCommand = new DelegateCommand<Window>(own =>
                  {               
                  //someVar и anotherVar используются в конструкторе SomeWindow, чтобы создать себе VM
                      var dlg = new SomeWindow(someVar, anotherVar) {Owner = own};
                      var result = dlg.ShowDialog();
                      ...
                  });

      В качестве CommandParameter передаю родительское window.


    1. igoriok
      25.09.2017 00:53

      В Caliburn Micro есть IWindowManager.ShowDialog(object viewModel). А в Prism это решалось event+behavior. Но однозначно не стоит создавать Window во ViewModel.


      1. oggr Автор
        25.09.2017 09:06

        Если Prism, то там есть InteractionRequest тогда. А что это вы мне запрещаете во ViewModel окошки клепать?)


        1. igoriok
          25.09.2017 12:03

          Я вам не могу ничего запрещать ) но на мой лично субъективный взгляд это немного не правильно. В таком случае лучше передавать в DelegateCommand просто object и через внешний Service/Manager связывать переданный ViewModel с соответствующей View, например, как это сделано в Caliburn.Micro:


          ConfirmCommand = new DelegateCommand<object>(owner =>
          {
            var confirmViewModel = new ConfirmViewModel();
            var settings = new Dictionary<string, object>
            {
              { "Owner", owner }
            };
          
            if (WindowManager.ShowDialog(confirmViewModel, null, settings) == true)
            {
              // do some action
            }
          }


  1. igoriok
    25.09.2017 00:58

    В ооочень больших проектах с MVVM влазишь в кашу из зависимостей, логики команд и свойств модели. Отчасти решается выносом команд в отдельные классы и добавлением контроллера. И получается уже гибрид MVVMC.