Часть 2: MVVM: полное понимание (+WPF)

  • Tutorial
image

В этой статье в качестве примера у нас будет программа чуть посложнее, а именно — торговый автомат, реализация которого часто встречается в качестве тестового задания до собеседования. Будут рассмотрены взаимодействие нескольких View с одним VM и наоборот, будет показан подход «View first» и будет показан не итоговый код, с рассказом какая часть для чего нужна (ссылка для скачивания кстати Vending Machine (программный код), а будет продемонстрирован весь процесс создания и, самое главное, последовательный ход мысли.

Но перед этим я постараюсь еще раз ответить на вопрос, который обычно не задают люди, имеющие опыт отладки неструктурированных проектов, а именно: «Так зачем все-таки нужен паттерн MVVM?»

Если формально и коротко, то паттерн MVVM используется в первую очередь для разделения ответственности, для повышения читабельности, управляемости, поддерживаемости и тестируемости кода. Программный продукт состоит из модели (доменной модели и бизнес-логики) и инфраструктурного кода в соотношении, допустим, 20% на 80%. Инфраструктурный код должен быть простым, понятным, чуть ли не автоматным — как Scaffolding. А вот модель…

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

Конкретно MVVM, а не, скажеи MVP или MVC, в WPF используется потому, что MVVM «аппаратно» поддерживается WPF. View понимает и INotifyPropertyChange, и Observable и др. — не надо ничего руками обновлять через презентер и т.д. Например MVP в WinForm's требовал больше инфраструктурного кода, причем ручного, и там преимущества разделения ответственности омрачалось бОльшим объемом черновой работы.

Задача


Вернемся к задаче. У неё вот такая формулировка: создать программу, эмулирующую взаимодействие человека с автоматом по продаже снеков/напитков.

Интерфейс программы должен отображать:

  1. Содержимое бумажника пользователя (изначально по 10 купюр/монет одного номинала) и его покупки.
  2. Содержимое деньгохранилища автомата (изначально по 100 купюр/монет одного номинала)
  3. Список возможных для покупки в автомате продуктов (в автомате изначально по 100 единиц каждого наименования)
  4. Текущий кредит в автомате (то, сколько денег туда вложил пользователь)

Интерфейс программы должен позволять:

  1. Внесение денежные средств пользователем в автомат
  2. Совершение покупок продуктов в автомате
  3. Требовать и получать, иногда, сдачу

Плюсом будет:

  1. Список товаров с ценами не задается жестко в коде
  2. Номиналы монет/купюр не задаются жестко в коде
  3. Соблюдение формального паттерна MVVM
  4. Настройка минимального доступа к полям и свойствам классов модели
  5. Красивый дизайнъ!

Последнее мы с вами сделаем вряд ли, а вот всё остальное — вполне.

В первой части мы использовали методику «Model first»:

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

Особенность такого подхода состоит в том, что мы должны заранее четко представлять модель, ее возможности. То, какие свойства и методы она будет предоставлять наружу, как будет устроено ее взаимодействие с интерфейсом. Но на первом этапе разработки мы даже не знаем, нужно ли будет то или иное взаимодействие. Нам нужны дополнительные точки опоры, в дополнение к описанному поведению в ТЗ. Такие точки опоры может нам предоставить интерфейс, т.е. View и VM к нему. В VM мы могли бы сформулировать клиентский код, т.е. тот код общего доступа (public), который мы бы хотели видеть в модели. Т.е. методика такая:

Методика «View first»:

  1. Нарисовать интерфейс программы — View
  2. Разработать VM к этим View, и сформировать клиентский код (код вызова модели)
  3. Имея интерфейс взаимодействия модели, реализовать её структуру и внутреннюю логику

Утром скетчи, вечером модель.

В создании интерфейса пользователя мало MVVM-специфичного, но этот пункт не обойти, поэтому давайте приступим к пункту №1.

Создание интерфейса


В ТЗ читаем, что нам нужно отобразить бумажник пользователя и его покупки и интерфейс автомата. Давайте разделим интерфейс физически (т.е. по разным файлам) на две части: одна для пользователя, другая для автомата. Это нужно, чтобы XAML файлы были поменьше. Работать с большими XAML файлами — (лично мне) неудобно. Тем более такое разбиение нам не будет ниего стоить, в WPF это делать очень просто: создать пару UserControl'ов — UserView.xaml и AutomatView.xaml, и использовать их в главном View — MainView.xaml. А DataContext они (UserView.xaml и AutomatView.xaml) будут использовать из главной формы. Т.е. если им не указывать DataContext, они как бы поднимаются по логическому дереву и натыкаются на DataContext главной формы, в которой они расположены, и используют его.

Начнем с UserView.xaml. Нам нужно тут отобразить содержимое бумажника и покупки. Покупки — это однозначно ListBox. А бумажник — это всего лишь число? Сумма наличности? Нет. В ТЗ сказано, что у пользователя есть по 10 купюр каждого номинала. Т.е. это тоже ListBox разных купюр с указанием количества. Давайте его релизуем:

UserView.xaml:

<!-- Монеты/купюры -->
<ListBox ItemsSource="{Binding UserWallet}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Image Width="32" Height="32" Source="{Binding Icon}"></Image>
                <Label Content="{Binding Name}"/>
                <Label Content="{Binding Amount}"/>
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Сам листбокс у нас биндится к несуществующему пока свойству UserWallet (кошелек пользователя), а его Item показывают также несуществующие Name («5 рублей» или «2 рубля», к примеру), Amount и Icon (иконку купюры, если это купюра или монеты соответственно). Icon — просто заведомо неудачная попытка выполнить дополнительнй пункт 5 из ТЗ: «Красивый дизайн». Кстати, добавьте в проект эту пару картинок в Каталог решения (Solution folder) «Images». В свойствах укажите Build action: resource. «Coin.png» и «Banknote.png» соответственно.

Листбокс с покупками принципиально отличаться не будет (разве что иконки добавлять не будем)

UserView.xaml:

<!--Покупки-->
<DockPanel>
  <Label DockPanel.Dock="Top" Content="Корзина пользователя"/>
  <ListBox ItemsSource="{Binding UserBuyings}">
    <ListBox.ItemTemplate>
      <DataTemplate>
        <StackPanel Orientation="Horizontal">
          <Label Content="{Binding Name}"/>
          <Label FontWeight="DemiBold" Content="{Binding Price}"/>
          <Label Content="{Binding Amount}"/>
        </StackPanel>
      </DataTemplate>
    </ListBox.ItemTemplate>
  </ListBox>
</DockPanel>

Давайте обрамим это, как и положено, в два столбца Grid'а и UserControl. И добавим сумму наличности пользователя:

UserView.xaml:

<UserControl ...>
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <!--Кошелек-->
    <DockPanel>
      <Label DockPanel.Dock="Top" Content="Наличность пользователя"/>
      <!--Сумма-->
      <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal">
          <Label Content="Итоговая сумма:"/>
          <Label Content="{Binding UserSumm}"/>
      </StackPanel>
      <!-- Монеты/купюры -->
      <ListBox ... />
    </DockPanel>
    <!--Покупки-->
    <DockPanel Grid.Row="0" Grid.Column="1" .../>
  </Grid>
</UserControl>

Так, пользователь готов. Теперь приступим к реализации интерфейса для автомата. По ТЗ необходимо показывать деньгохранилище и возможные покупки — т.е. также, как и у пользователя. Следовательно попробуем вырезать эти DataTemplate'ы из файла UserView.xaml для переиспользования. Эти DataTemplate'ы можно выложить отдельными файлами и использовать как Merged Resource Dictionary, но мы просто рапсположим их в ресурсах в главном View.

MainView.xaml:

<Window ...>
<Window.Resources>
    <!-- Шаблон данных для продуктов в корзине/в наличии -->
    <!-- Обратите внимание на аттрибут DataType (о нём ниже) -->
    <DataTemplate DataType="{x:Type local:ProductVM}">
      <StackPanel Orientation="Horizontal">
        <Label Content="{Binding Name}"/>
        <Label FontWeight="DemiBold" Content="{Binding Price}"/>
        <Label Content="{Binding Amount}"/>
      </StackPanel>
    </DataTemplate>
    <!-- Шаблон данных для денег в кошельке/деньгохранилище -->
    <DataTemplate DataType="{x:Type local:MoneyVM}">
      <StackPanel Orientation="Horizontal">
        <Image Width="32" Height="32" Source="{Binding Icon}"></Image>
        <Label Content="{Binding Name}"/>
        <Label Content="{Binding Amount}"/>
      </StackPanel>
    </DataTemplate>
  </Window.Resources>
  <!-- Можно сразу подключить (и создать) нашу VM - MainViewVM.cs -->
  <Window.DataContext>
      <local:MainViewVM/>
  </Window.DataContext>
  <!-- Грид с двумя колонками, слева интерфейс пользователя, справа - интерфейс автомата (пока пустой)  -->
  <!-- В качестве DataContext и тот и другой будут использовать DataContext этого окна  -->
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <local:UserView Margin="10" />
    <local:AutomatView Grid.Column="1" Margin="10"/>
  </Grid>
</Window>

Обратите внимание на DataType в DataTemplate'тах. Это такая хитрая штука в WPF, делающая следующее: когда в качестве контента какого-нибудь элемента (в данном случае ListBoxItem) назначается объект указанного типа (в данном случае ProductVM или MoneyVM), тогда этот объект становиться DataContext'ом этого элемента, а в качестве контента выступает этот шаблон. ProductVM или MoneyVM — это VM для этих шаблонов, которые мы пока еще не создали. Можно создать пока все три VM:

Файл MainViewVM.cs:

public class MainViewVM : BindableBase { }
public class ProductVM { }
public class MoneyVM { }

Да, подключите Prism (6.3.0, семерка под Wpf пока не работает) и отнаследуйте MainViewVM от BindableBase.

Т.е. еще раз, что проиходит: ListBox в качестве ItemsSource использует List например. Для каждого элемента в этом листе создается ListBoxItem и его содержимому присваивается этот объект типа ProductVM. WPF видит, что у него есть DataTemplate для типа ProductVM, и этот DataTemplate присваивает в качестве содержимого для этого ListBoxItem, а сам объект ProductVM используется в качестве DataContext и к нему осуществяется Binding. Если в качестве ItemsSource ListBox'a использовать массив, где лежат не только ProductVM, но еще и MoneyVM (если оба отнаследованны от общего базового класса, например BindableBase), то и DataTemplate'ы будут к ним применены разные!

Осталось реализовать AutomatView.xaml.

AutomatView.xaml:

<UserControl ...>
  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <!--Монетоприемник-->
    <DockPanel Grid.Row="0" Grid.Column="1">
      <Label DockPanel.Dock="Top" Content="Монетоприемник"/>
      <!--Кредит-->
      <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
        <Label Content="Кредит:"/>
        <Label Content="{Binding Credit}"/>
      </StackPanel>
      <!--Деньгохранилище-->
      <ListBox ItemsSource="{Binding AutomataBank}" />
    </DockPanel>
    <!--Товары автомата-->
    <DockPanel Grid.Row="0" Grid.Column="0">
      <Label DockPanel.Dock="Top" Content="Товары"/>
      <ListBox ItemsSource="{Binding ProductsInAutomata}"/>
    </DockPanel>
  </Grid>
</UserControl>

Читаем ТЗ далее: программа должна позволять… вносить деньги в автомат, совершать покупки и получать сдачу.

Можно рядом с каждым продуктом в ListBox'e «Товары автомата» приделать кнопочку, по нажатию на которую будет совершаться покупка.

Точно также в монетоприемнике, в ListBox'е «Деньгохранилище» можно к каждой купюре/монете приделать кнопку, по которой пользователь будет вносить деньги в автомат.

Чтобы эти кнопки не отображались в части интерфейса связанной с пользовтелем, надо задать необходимые свойства «Show...».

А рядом с текстовым полем, обозначающем кредит, можно создать кнопку «Вернуть сдачу».

Внесем необходимые изменения:

<!-- Шаблон данных для продуктов в корзине/в наличии -->
<DataTemplate DataType="{x:Type local:ProductVM}">
  <StackPanel Orientation="Horizontal">
    <Button Visibility="{Binding IsBuyVisible}" Command="{Binding BuyCommand}">+</Button>
...
<!-- Шаблон данных для денег в кошельке/деньгохранилище -->
<DataTemplate DataType="{x:Type local:MoneyVM}">
  <StackPanel Orientation="Horizontal">
    <Button Visibility="{Binding IsInsertVisible}" Command="{Binding InsertCommand}">+</Button>
...
<!--Кредит-->
  <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom">
      <Button Command="{Binding GetChange}" Margin="5">Вернуть сдачу</Button>
...

Все, этап №1 в целом окончен. Переходим к созданию VM.

Создание ViewModels


Мы уже создали (создайте, если еще не) классы MainViewVM, ProductVM и MoneyVM.
Если у вас есть ReSharper, и если вы добавите в файлы UserView.xaml и AutomatView.xaml в верхний грид такую строчку:

<Grid d:DataContext="{d:DesignInstance {x:Type local:MainViewVM}}">

которая укажет WPF редактору тип DataContext (но на runtime это не скажется никак), то через Alt+Enter можно добавить соответствующие поля в классы VM. Если ReSharper'a у вас нет, можно сделать это руками:

public class MainViewVM : BindableBase {
  public int UserSumm { get; }
  public ObservableCollection<MoneyVM> UserWallet { get; }
  public ObservableCollection<ProductVM> UserBuyings { get; }
  public DelegateCommand GetChange { get; }
  public int Credit { get; }
  public ReadOnlyObservableCollection<MoneyVM> AutomataBank { get; }
  public ReadOnlyObservableCollection<ProductVM> ProductsInAutomata { get; }
}
public class ProductVM {
  public Visibility IsBuyVisible { get; }
  public DelegateCommand BuyCommand { get; }
  public string Name { get; }
  public string Price { get; }
  public int Amount { get; }
}
public class MoneyVM {
  public Visibility IsInsertVisible { get; }
  public DelegateCommand InsertCommand { get; }
  public string Icon { get; }
  public string Name { get; }
  public int Amount { get; }
}

Видите, у нас почти автоматом создались три VM. Теперь можно их реализовывать последовательно, свойство за свойством. Мы вольны писать такой клиентский код, который бы нам хотелось чтобы был. Например: UserSumm => _user.UserSumm; Т.е. подразумеваеться, что есть некоторый объект _user класса модели User, у которого есть свойство UserSumm. Давайте даже создадим такой класс. У нас будет теперь появляться модель, а вернее — точки соприкосновения модели и VM, которые сформируют некоторые внешние границы модели.
Только теперь небольшое отступление.

В ТЗ указано, что мы должны обеспечивать "… минимально необходимый доступ к полям и свойствам классов модели" (из клиентского кода). Такое требование должно быть не только в этом ТЗ, а вообще занимать почетное место в принципах вашего software-строения. Клиентский код не должен случайно (или умышленно) вторгаться в модель, заставляя ее приходить в незапланированное состояние. Тем более, что вы сейчас разрабатываете код, связанный с анонимными денежными операциями. Представьте, что вы сейчас внесете в код ошибку, используя которую пользователи по всей стране выпьют бесплатно кофе на 6 млн рублей, и этот убыток через суд повесят на вас, вы будете принудительно работать на эту софтверную компанию и до конца жизни кодить на связке Delphi + 1C за печеньки.

В общем, модель наша будет состоять из нескольких классов. И надо сделать так, чтобы из одного класса 'A' модели можно было вызывать метод SomeMethod() другого класса 'B' модели, а из нашего клиентского кода этот метод B.SomeMethod() вызывать было бы нельзя.

Чтобы такого добиться, можно конечно сделать класс B внутренним приватным классом класса A, и реализовывать в нем интерфейс, который экспозить наружу… Но вообще для таких целей есть специально предусмотренное решение — модификатор доступа internal. Т.е. надо всего лишь выделить модель в отдельный проект в решении. Таким образом мы сможем воспользоваться модификатором internal, физически разнесем наш клиентский код от кода модели. Теперь эту отдельную модель можно легко использовать, например, в веб-решении.

Создадим проект библиотеку классов, назовем её VendingMachine.Model, добавим туда класс модели User и создадим у него свойство UserSumm

В MainViewVM объявим приватную переменную _user типа User и создадим ее в конструкторе:
Файл MainViewVM:

public class MainViewVM : BindableBase {
  public MainViewVM() {
    _user = new User();
  }
  public int UserSumm => _user.UserSumm;
  //...
  private User _user;
}

Следующим пунктом мы натыкаемся на деньги — UserWallet, который для нас коллекция MoneyVM. Читаем ТЗ: "… Номиналы монет/купюр не задаются жестко в коде".

Т.е. есть некоторый набор номиналов (рубль, два, пять, десять), который откуда-то приходит (из базы данных, из конфигурационного файла, из веб-сервиса и т.д.). Нам необходимо, чтобы у пользователя и у автомата был один и тот же набор номиналов, и чтобы нельзя было вдруг создать некоторый свой номинал (монету в 8 рублей, например). Если надо запретить создавать — тут хорошо подойдет приватный конструктор. Если все же некоторый набор номиналов нужен — то подойдет фабричный метод, возващающий такой лист номиналов. Или, если не планируется многопоточность (а она не планируется), можно использовать статический список. Давайте так и сделаем.

//структура, а не класс, чтобы сравнение была сразу по значению, а не по ссылке
public struct Banknote {
  //представим, что список пришел из базы данных
  public static readonly IReadOnlyList<Banknote> Banknotes = new[] {
    new Banknote("Рубль", 1, true),
    new Banknote("Два рубля", 2, true),
    new Banknote("Пять рублей", 5, true),
    new Banknote("Десять рублей", 10, false),
    new Banknote("Пятьдесят рублей", 50, false),
    new Banknote("Сто рублей", 100, false),
  };
  private Banknote(string name, int nominal, bool isCoin) {
    Name = name;
    Nominal = nominal;
    IsCoin = isCoin;
  }
  public string Name { get; }
  public int Nominal { get; }
  public bool IsCoin { get; } //монета ли это. Нужно для красоты
}

Теперь второе. У нас есть номинал, но UserWallet у нас — это такой массив из пар номинал/количество. Как стопки фишек в казино: стопочка по $1, стопочка фишек по $250 и т.д. Нам нужна как раз такая стопочка (Stack):

public class MoneyStack {
  public MoneyStack(Banknote banknote, int amount) {
      Banknote = banknote;
      Amount = amount;
  }
  public Banknote Banknote { get; }
  public int Amount { get; }
}

Мы могли использовать структуры типа Dictionary, но чутье программиста подсказывает нам, что понадобятся функции типа уменьшить количество, увеличить и т.д. Сейчас мы их не добавляем, т.к. нет клиентского кода, их вызывающего. Пока такого кода нет, мы эти функции не добавляем. Это как с событиями — мы не добавляем в разрабатываемый нами контрол события (например двойного клика мышкой), пока нет обработчика для него. Иначе мы можем создать десятки событий, из которых нам понадобятся два-три. Так же и с классом: мы можем создать много различных внешних функций, из которых нам понадобятся лишь пара, и то — не с той сигнатурой.

Теперь, соответственно, обновим класс User и добавим туда UserWallet. Как и положено, UserWallet будет ReadOnlyObservableCollection и для обеспечения это коллекции будет другая — приватная коллекция. Кроме того, по ТЗ пользователю положено при инициализации выдать по 10 купюр каждого достоинства. Сделаем это в конструкторе пользователя.

User.cs:

public class User {
  public User() {
    //кошелек пользователя
    _userWallet = new ObservableCollection<MoneyStack>
       (Banknote.Banknotes.Select(b => new MoneyStack(b, 10)));
    UserWallet = new ReadOnlyObservableCollection<MoneyStack>(_userWallet);
  }
  public ReadOnlyObservableCollection<MoneyStack> UserWallet { get; }
  private readonly ObservableCollection<MoneyStack> _userWallet;
...
}

Теперь обновим конструктор MainViewVM. Т.к. в User у нас коллекция объектов класса модели MoneyStack, а в MainViewVK коллекция классов VM — MoneyVM, то мы должны сделать некоторые преобразования. В MoneyVM создать конструктор, принимающий MoneyStack.

Затем сначала при инициализации, а потом при изменении коллекции мы должны добавлять соответствующую VM (изменения модели единичные, поэтому конструкция a.NewItems?.Count == 1 — работает):

public MainViewVM() {
  _user = new User();
//преобразовать коллекцию в конструкторе
  UserWallet = new ObservableCollection<MoneyVM>(_user.UserWallet.Select(ms => new MoneyVM(ms)));
//преобразовывать каждый добавленный или удаленный элемент из модели
  ((INotifyCollectionChanged) _user.UserWallet).CollectionChanged += (s, a) =>  {
    if(a.NewItems?.Count == 1) UserWallet.Add(new MoneyVM(a.NewItems[0] as MoneyStack));
    if (a.OldItems?.Count == 1) UserWallet.Remove(UserWallet.First(mv => mv.MoneyStack == a.OldItems[0]));
  };
}

Соответственные изменения вносим в MoneyVM. Принимаем MoneyStack в качестве параметра и присваиваем ее к свойству для чтения MoneyStack — для удобства последующего поиска. Видимость кнопки у нас зависит от наличия команды InsertCommand. Возвращаем также изображение, количество, имя банкноты:

public class MoneyVM {
  public MoneyStack MoneyStack { get; }
  public MoneyVM(MoneyStack moneyStack) {
      MoneyStack = moneyStack;
  }
  public Visibility IsInsertVisible => InsertCommand == null ? Visibility.Collapsed : Visibility.Visible;
  public DelegateCommand InsertCommand { get; }
  public string Icon => MoneyStack.Banknote.IsCoin ? "..\\Images\\coin.jpg" : "..\\Images\\banknote.png";
  public string Name => MoneyStack.Banknote.Name;
  public int Amount => MoneyStack.Amount;
}

Продолжаем реализовывать MainViewVM. На очереди ObservableCollection UserBuyings.

UserBuyings реализуется очень похоже на предыдущую конструкцию. Также создаем класс модели Product с закрытым конструктором. Там точно так же создаем коллекцию доступных в программе продуктов (типа из базы данных). Точно так же создаем ProductStack. И точно также преобразовываем из ProductStack в ProductVM.

Product.cs:

public class Product {
  //представим, что список посредством web service
  public static IReadOnlyList<Product> Products = new List<Product>()   {
    new Product("Кофе",12),
    new Product("Кофе подороже", 25),
    new Product("Чай",6),
    new Product("Чипсы",23),
    new Product("Батончик",19),
    new Product("Нечто",670),
  };
  private Product(string name, int price)   {
    Name = name;
    Price = price;
  }
  public string Name { get; }
  public int Price { get; }
}

ProductStack.cs:
public class ProductStack {
  public ProductStack(Product product, int amount) {
    Product = product;
    Amount = amount;
  }
  public Product Product { get; }
  public int Amount { get; }
}

В классе User создаем примерно такую же ReadOnlyObservableCollection. Разве что в конструкторе теперь не снабжаем пользователя всеми наименованиями товаров по 10 штук, т.к. это не задано в ТЗ:

public class User {
  public User() {
    ...
    //продукты пользователя
    UserBuyings = new ReadOnlyObservableCollection<ProductStack>(_userBuyings);
  }
  public ReadOnlyObservableCollection<ProductStack> UserBuyings { get; }
  private readonly ObservableCollection<ProductStack> _userBuyings = new ObservableCollection<ProductStack>();
  ...
}

Соответственным образом обновляем ProductVM:

public class ProductVM {
  public ProductStack ProductStack { get; }
  public ProductVM(ProductStack productStack) {
      ProductStack = productStack;
  }
  public Visibility IsBuyVisible => BuyCommand == null ? Visibility.Collapsed : Visibility.Visible;
  public DelegateCommand BuyCommand { get; }
  public string Name => ProductStack.Product.Name;
  public string Price => $"({ProductStack.Product.Price} руб.)";
  public Visibility IsAmountVisible => BuyCommand == null ? Visibility.Collapsed : Visibility.Visible;
  public int Amount => ProductStack.Amount;
}

И, наконец, конструктор в MainViewVM:

public MainViewVM() {
 ...
  //покупки пользователя
  UserBuyings = new ObservableCollection<ProductVM>(_user.UserBuyings.Select(ub => new ProductVM(ub)));
  ((INotifyCollectionChanged)_user.UserBuyings).CollectionChanged += (s, a) =>  {
      if (a.NewItems?.Count == 1) UserBuyings.Add(new ProductVM(a.NewItems[0] as ProductStack));
      if (a.OldItems?.Count == 1) UserBuyings.Remove(UserBuyings.First(ub => ub.ProductStack == a.OldItems[0]));
  };
}

Синхронизация модели и VM покупок пользователя — и модели и VM кошелька пользователя — практически идентичная. Мало того, на очереди такие же синхронизации в случае с деньгохранилищем и товарами внутри автомата. Поэтому, чтобы избежать дублирования кода, напишем такую функцию синхронизации:

private static void Watch<T, T2>
  (ReadOnlyObservableCollection<T> collToWatch, ObservableCollection<T2> collToUpdate, Func<T2, object> modelProperty) {
    ((INotifyCollectionChanged)collToWatch).CollectionChanged += (s, a) =>  {
      if (a.NewItems?.Count == 1) collToUpdate.Add((T2)Activator.CreateInstance(typeof(T2), (T) a.NewItems[0]));
      if (a.OldItems?.Count == 1) collToUpdate.Remove(collToUpdate.First(mv => modelProperty(mv) == a.OldItems[0]));
    };
  }

И будем использовать ее в конструкторе следующим образом:

Watch(_user.UserWallet, UserWallet, um => um.MoneyStack);
Watch(_user.UserBuyings, UserBuyings, ub => ub.ProductStack);

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

Далее: в классе MainViewVM, который мы последовательно реализуем, остались еще нереализованными свойства и команды, относящиеся к автомату. Давайте их реализуем, благо дело сейчас пойдет быстрее, т.к. для денег и продуктов модели мы уже создали. Как и в случае с классом User мы создадим класс модели Automata и в нем такие же две коллекции для продуктов и денег. Также реализуем в нем свойство Credit.

public class Automata  {
  public Automata()   {
    //деньгохранилище автомата
    _automataBank = new ObservableCollection<MoneyStack>
        (Banknote.Banknotes.Select(b => new MoneyStack(b, 100)));
    AutomataBank = new ReadOnlyObservableCollection<MoneyStack>(_automataBank);
    //продукты автомата
    _productsInAutomata =
        new ObservableCollection<ProductStack>(Product.Products.Select(p => new ProductStack(p, 100)));
    ProductsInAutomata = new ReadOnlyObservableCollection<ProductStack>(_productsInAutomata);
  }

  public ReadOnlyObservableCollection<MoneyStack> AutomataBank { get; }
  private readonly ObservableCollection<MoneyStack> _automataBank;
  public ReadOnlyObservableCollection<ProductStack> ProductsInAutomata { get; }
  private readonly ObservableCollection<ProductStack> _productsInAutomata;
  public int Credit { get; }
}

Соответственно в классе MainViewVM добавим приватное поле класса Automata и инициализируем в конструкторе коллекции:

public class MainViewVM : BindableBase {
  public MainViewVM() {
  ...
    _automata = new Automata();
    //деньги автомата
    AutomataBank = new ObservableCollection<MoneyVM>(_automata.AutomataBank.Select(a => new MoneyVM(a)));
    Watch(_automata.AutomataBank, AutomataBank, a => a.MoneyStack);
    //товары автомата
    ProductsInAutomata = new ObservableCollection<ProductVM>(_automata.ProductsInAutomata.Select(ap => new ProductVM(ap)));
    Watch(_automata.ProductsInAutomata, ProductsInAutomata, p => p.ProductStack);
  }
  ...
  private Automata _automata;
}

Поведение модели


Теперь нам осталось только реализовать поведение модели при вносе наличности, покупке и требовании сдачи.

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

Сейчас, для того, чтобы продолжить разработку, нам необходимо объединить пользователя и автомат в рамках одной сущности. Это объединение обычно диктуется этим предварительным эскизом. Мы же, в нашем случае — просто создадим жесткое соединение одного произвольного пользователя и одного автомата в рамках объекта класса, например, PurchaseManager. Соответственно, объекту именно этого класса мы будем адресовать наши, еще не реализованные, запросы на поведение модели.

Почему мы не можем остаться в рамках классов User и Automata? В принципе, конечно, можно. Смотрите: нам необходима возможность взять у пользователя некоторую сумму денег и внести эту сумму денег в автомат. Такую операцию в VM мы совершить не можем, т.к. это допустимо только в модели. Т.е. эту операцию должен осуществлять или класс User или класс Automata. Согласно принципу разделения Single responsibility, эту ответственность должна быть возложена на третий класс, осуществляющий их взаимодействие. Поэтому создадим класс PurchaseManager и отредактируем наш MainViewVM.cs на использование этого класса, вместо самостоятельного создания User и Automat.

PurchaseManager.cs:
public class PurchaseManager {
 public User User { get; } = new User();
 public Automata Automata { get; } = new Automata();
}
MainView.cs:
public class MainViewVM : BindableBase {
  private PurchaseManager _manager;
  public MainViewVM() {
    _manager = new PurchaseManager();
    _user = _manager.User;
    _automata = _manager.Automata;
    ...
  }
  ...
}

Теперь, внесение денег у нас осуществляется по нажатию на кнопку и последующему вызову DelegateCommand InsertCommand вьюмодели MoneyVM. Есть разные способы пробросить такую коммуникацию между VM и моделью. Можно передавать DelegateCommand в конструкторе VM. Можно передавать целиком модель(PurchaseManager), это вообще самый универсальный способ и мы можем делать это вполне безопасно, — устройство модели, благодаря инкапсуляции, нам это вполне позволяет. Внесем соответствующие правки:

Конструктор MoneyVM:

public MoneyVM(MoneyStack moneyStack, PurchaseManager manager = null) {
  MoneyStack = moneyStack;
  if (manager != null) //по умолчанию Null, если же нет, то тогда задаем DelegateCommand
    InsertCommand = new DelegateCommand(()=>{
      manager.InsertMoney(MoneyStack.Banknote);
    });
}

Соответственно поменяем и вызов функции Watch, что бы передавать модель в конструкторе класса MainViewVM. Но только для пользователя, для автомата такое делать не надо. (Хотя ничего страшного не случиться, даже появится незапланированная возможность внесения денег и в правой и в левой сторонах интерфейса).

Теперь необходимо реализовать функцию InsertMoney. Она должна извлечь из пользователя определенную банкноту, и, в случае успеха, занести ее в автомат. Функция извлечения банкноты из пользователя должна быть доступна из модели, но при этом недоступна из клиентского кода — в этом нам поможет уже упоминавшийся модификатор доступа internal.

PurchaseManager.cs:

public void InsertMoney(Banknote banknote) {
  if (User.GetBanknote(banknote))   //если у пользователя такую купюру получили,
      Automata.InsertBanknote(banknote);  //то сунуть ее в автомат
}

User.cs:

//если такой MoneyStack в наличии, то попробовать вытащить из него одну купюру/монету
//вернуть false в случае неудачи
internal bool GetBanknote(Banknote banknote) {
  if(_userWallet.FirstOrDefault(ms => ms.Banknote.Equals(banknote))?.PullOne() ?? false) {
    RaisePropertyChanged(nameof(UserSumm)); //обновилась сумма наличности пользователя!
    return true;
  }
  return false;
}
//сумма наличности пользователя
public int UserSumm { get { return
  _userWallet.Select(b => b.Banknote.Nominal * b.Amount).Sum(); } }

MoneyStack.cs:

internal bool PullOne() {
  if (Amount > 0) { --Amount;
    return true; }
  return false;
}

Automata.cs:

//поместить купюру в отделение для соответственной купюры
internal void InsertBanknote(Banknote banknote)    {
  _automataBank.First(ms => ms.Banknote.Equals(banknote)).PushOne();
  Credit += banknote.Nominal;
}
//кредит
private int credit;
public int Credit { get { return credit; }
    set  { SetProperty(ref credit, value); }}

и опять MoneyStack.cs:

internal void PushOne() => ++Amount;


Теперь надо вызывать INotifyPropertyChanged для уведомления View.

Соответственно MoneyStack наследуется от BindableBase и Amount делает уведомление:

public class MoneyStack : BindableBase {
...
  private int _amount;
  public int Amount {
    get { return _amount; }
    set { SetProperty(ref _amount, value); }
  }
}

и MoneyVM также наследуется от BindableBase и это уведомление пробрасывается — конструктор MoneyVM:

...
moneyStack.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Amount)); };

не забудем получать уведомления и от изменения свойств UserSumm и Credit в конструкторе MainViewVM:

_user.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(UserSumm)); };
_automata.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Credit)); };

Мы можем теперь уверенно вставлять купюры и монеты в купюро/монето- приемник! И даже увеличивается кредит в автомате. Давайте уже что-нибудь купим. Покупка будет осуществляться по точно такому же принципу, как и вставка купюр. Тоже будем в ProductVM передавать нашу модель и вызывать у нее соответственные методы.

конструктор ProductVM:

public ProductVM(ProductStack productStack, PurchaseManager manager = null)
{
  ProductStack = productStack;
  productStack.PropertyChanged
    += (s, a) => { RaisePropertyChanged(nameof(Amount)); };

  if (manager != null)
    BuyCommand = new DelegateCommand(() => {
        manager.BuyProduct(ProductStack.Product);
    });
}

PurchaseManager.cs:

public void BuyProduct(Product product) {
    if (Automata.BuyProduct(product))
        User.AddProduct(product);
}

Automata.cs:

internal bool BuyProduct(Product product) {
    if(Credit >= product.Price && _productsInAutomata.First(p=>p.Product.Equals(product)).PullOne()) {
        Credit -= product.Price;
        return true;
    }
    return false;
}

User.cs:

internal void AddProduct(Product product) {
   var stack = _userBuyings.FirstOrDefault(b => b.Product == product);
   if (stack == null)
       _userBuyings.Add(new ProductStack(product, 1));
   else
       stack.PushOne();
}

ProductStack.cs:

public int Amount {
    get { return _amount; }
    set { SetProperty(ref _amount, value); }
}

internal bool PullOne() {
    if (Amount > 0) {
        --Amount;
        return true;
    }
    return false;
}
internal void PushOne() => ++Amount;

INotifyPropertyChanged уведомления View в конструкторе ProductVM:

...
productStack.PropertyChanged += (s, a) => { RaisePropertyChanged(nameof(Amount)); };

Последняя функциональность — получение сдачи


Алгоритм очень простой — необходимо посмотреть, есть ли у автомата достаточно денег для сдачи, и если да — собрать набор купюр (сначала крупные, потом все мельче) и передать пользователю.

class PurchaseManager {
  ...
  public void GetChange()  {
    IEnumerable<MoneyStack> change;
    if (Automata.GetChange(out change))
        User.AppendMoney(change);
  }
}

//класс Automata
internal bool GetChange(out IEnumerable<MoneyStack> change) {
    change = new List<MoneyStack>();
    if (Credit == 0) return false;

    var creditToReturn = Credit;
    var toReturn = new List<MoneyStack>();
    foreach (var ms in _automataBank.OrderByDescending(m => m.Banknote.Nominal)) {
        if (creditToReturn >= ms.Banknote.Nominal) {
            toReturn.Add(new MoneyStack(ms.Banknote, creditToReturn / ms.Banknote.Nominal));
            creditToReturn -= (creditToReturn / ms.Banknote.Nominal) * ms.Banknote.Nominal;
        }
    }
    if (creditToReturn != 0) return false; //денег не набирается, ничего не возвращаем

    foreach (var ms in toReturn) //возвращаем
        for (int i = 0; i < ms.Amount; ++i)  //по одной монетке правда
        _automataBank.First(m => Equals(m.Banknote, ms.Banknote)).PullOne();
    change = toReturn;
    Credit = 0;
    return true;
}

//класс User
internal void AppendMoney(IEnumerable<MoneyStack> change)
{
  foreach (var ms in change)
    for(int i=0; i<ms.Amount;++i)
      UserWallet.First(m => Equals(m.Banknote.Nominal, ms.Banknote.Nominal)).PushOne();
  RaisePropertyChanged(nameof(UserSumm));
}

Все. Окончательный вариант можно взять отсюда: Vending Machine (программный код)
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 39
  • 0
    как показала практика в вендинге программирование интерфейска далеко не самое сложное. потому его хоть как реализуй.
    • 0
      Смотря на что фокусироваться. Может, что как раз наоборот) Модель — простая, VM — автоматом делается, а вот интерфейс попробуй нарисуй, чтобы красивый был, интуитивный, удобный, эргономичный и т.д.
      • 0
        Если VM у вас делается автоматом, то вам она скорее всего не нужна.
        • 0
          Это образно же) Скажем, в полуавтоматическом режиме делаются
      • 0
        Поэтому вендинговые автоматы имеют инструкцию из 10 пунктов на передней панели для программирования покупателей.
      • 0
        А где посмотреть «часть 1»?

        Окончательный вариант можно взять отсюда:

        Ссылка отсутствует
      • 0
        С WPF лучше переопределять OnStartup, и создавать главное окно там.
        Причина в том, что указание StartupUri не позволяет задать DataContext снаружи окна. А хардкодирование DataContext'а внутри XAML неправильно, так как:

        • Согласно паттерну MVVM, View не должно руководить VM, и знать о нём должно как можно меньше. То есть код, обслуживающий UI, не должен создавать себе VM сам.
        • Чаще всего VM должна получить ещё какие-то аргументы в конструкторе (например, модельные объекты), а это не получится сделать, если VM создаётся в XAML'е (откуда XAML может знать о модели?).
        • Иногда перед открытием главного окна нужна дополнительная логика, требующая показа своего маленького окна (например, предложение обновить программу). Код, выясняющий это и при некоторых условиях показывающий дополнительное окно, проще разместить в OnStartup.
        • Главная VM может понадобиться вам для ещё чего-нибудь. Поэтому если её создаёт класс App, он может сохранить ссылку на неё у себя, чтобы другие (например, побочное окно) могли быть привязаны к тому же экземпляру VM. Иначе вам придётся лезть за главной VM в экземпляр главного окна.


        Не мое, но практика показала что это наиболее разумный подход.
        • +1
          Еще лучше взять Caliburn.Micro и забыть о 90% шаблонного геморроя.
          • 0
            Кхм, ну тогда уж лучше брать Stylet и забыть о 95%.
            • 0
              «Stylet is a small but powerful ViewModel-first MVVM framework for WPF, which allows you to write...»

              Я использую Model-first — подход, иногда View-first — подход, но ViewModel-first даже затрудняюсь представить:) Надо посмотреть на сие чудо
          • 0

            Не со всем согласен. По пунктам:


            1. Как раз наоборот, ViewModel ничего не знает про View, а вот View знает всё про вьюмодель, все свойства, команды и т.д. Очень сомневаюсь, что вы передаёте контекст данных для каждой вьюшки приложения в коде… Это очень неудобно.
            2. И вы будете создавать конструктор и передавать в него все эти параметры руками? Это плохая идея, чтобы этого не делать — придумали DI.
            3. Для главного окна — да, обновление, авторизация и т.д. Тут согласен.
            4. Опять тот же самый DI, только хранить экземпляры для повторного использования нужно в виде синглтонов.
              Если мы поняли, что контекст данных можно получать из контейнера, то остаётся сделать следующий очевидный шаг — декларировать его прямо в XAML через расширение разметки:
              DataContext="{ext:Container {x:Type viewModels:IViewModel}}"

            Расширение разметки очень простое:
            ```C#
            public class ContainerExtension: MarkupExtension
            {
            private readonly Type _type;
            private object _dataContext;


                public ContainerExtension(Type type)
                {
                    _type = type;
                } 



            }

            • 0
              View знает всё про вьюмодель, все свойства, команды и т.д.

              Не все. View известен только публичный интерфейс ViewModel. Она не должна знать, откуда брать и как создавать ViewModel. Этим должна заниматься точка сборки.


              Если мы поняли, что контекст данных можно получать из контейнера, то остаётся сделать следующий очевидный шаг — декларировать его прямо в XAML через расширение разметки:

              Зачем заново придумывать data template?
              Плюс в таком виде это не DI, а Service Locator, отягощенный глобальной переменной.

            • 0
              Я вообще пользуюсь Prism. Там (начиная с пятой версии) View и ViewModel можно связывать по настраиваемому Name Convention, т.е. по определенному шаблону имени. А параметры конструктора VM через dependency injection.
            • +1
              Честно говоря, я не знаю, какое понимание смогут вынести из этих статей начинающие.
              Видно кучу шаблонного кода, большой оверхед без видимой пользы, и куча ViewModels, которые с тем же успехом мог сделать кодогенератор.
              Это не удивительно, так как суть паттерна MVVM не просто в отделении представления от модели, а в отделении поведения представления от поведения модели.
              Для модели важны консистентность, производительность, отказоустойчивость.
              Для представления важно удобство проведения операций в модели для пользователя -человека с наглядной обратной связью, контролем возможных ошибок, возможностью изменить параметры до принятия необратимого решения.
              ViewModel нужна как раз для реализации человеко-удобного поведения отдельно от конкретной реализации ввода-вывода.
              В статьях этого нет — и ViewModel превращается в скорее вредную, чем бесполезную прокладку.
              • 0
                ViewModel — это не вредная прокладка, а некоторый прочный каркас, к которому прибивается View. Модель может быть такой, облакообразной, как перьевые облака, грозовая бесформенная куча, к которой не прибиндиться, а во ViewModel может быть такой код:
                public IsEditVisible { get; } => (_model.SomeProperty[«SomeIndex»] * _model.GetSomeValue) < _model2.AdjustionCorrelator;
                Вьюшка не может собрать по модели — отображать ей кнопку Edit, или нет. А вот к VM одному свойству она прибиндиться может.
                • 0

                  IsEditVisible — свойства с таким именем во ViewModel быть не должно, ViewModel ничего не знает о видимости по построению.


                  Модель может быть такой, облакообразной, как перьевые облака, грозовая бесформенная куча, к которой не прибиндиться

                  Из этого совершенно не понятно, почему модель именно такая, и что мешает спроектировать ее иначе.


                  во ViewModel может быть такой код

                  Если ViewModel можно заменить простым маппером, то она не нужна.

                  • 0
                    Спроектировать иначе модель для нужд вью и VM? Если модель используется в проекте в другом месте, если она используется в Вебе и т.д. — перепроектировывать ее для нужд WPF нет необходимости, т.к. именно VM возьмет на себя обязанность спрямить для вьюшки все причудливые изгибы модели.
                    IsEditVisible — свойства с таким именем во ViewModel быть не должно, ViewModel ничего не знает о видимости по построению.

                    Слушайте, ну у вас какая-то своя философия MVVM, напишите статью о ней и мы там побеседуем о_0
                    • 0
                      > Спроектировать иначе модель для нужд вью и VM?

                      Нужды вью впрямую модели не касаются — именно для этого и вводится VM. Нужды VM формируют интерфейс модели вкупе с основной бизнес задачей (продажей товаров покупателям).

                      > Если модель используется в проекте в другом месте

                      … то к нашим услугам адаптеры, мосты и прочие медиаторы. Трехслойные паттерны появились совсем не потому, что модели задавались кем-то внешним. Это способ проектирования и реализации непосредственно приложений с UI.

                      > Слушайте, ну у вас какая-то своя философия MVVM

                      У меня нет никакой отдельной философии. Это очень простая вещь — краеугольный камень всех многослойных UI-паттернов. Непосредственно за ввод-вывод информации для человека отвечает слой View и никто больше. Видимость конкретного элемента управления — вопрос исключительно для View. Он может решаться с помощью привязки к свойству ViewModel, но само это свойство всегда выражается в терминах поведения и состояния приложения, а не View, которое может быть абсолютно любым (например, текстовым на шрифте Брайля).
                      Если у вас ViewModel знает про Edit и что он Visible — вы переложили на нее часть ответственности View и нарушили паттерн.
                      • 0
                        Нужды VM формируют интерфейс модели вкупе с основной бизнес задачей (продажей товаров покупателям).

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

                        Вот вроде говорим об одном… но выводы делаем различные.
                        Вот допустим у нас в представленной задаче кнопочки с покупками определенных товаров были бы видимы только тогда, когда кредита хватало бы на их покупку.
                        Модели все равно — видны кнопочки или не видны — даже если вы нажмете на такую кнопку: если денег в кредите недостаточно — модель все равно не продаст вам товар. Теперь, если мы (чисто из визуальных нужд) захотим скрыть кнопочки тех товаров, для покупки которых кредит недостаточен, тогда я прописываю во ViewModel if(Credit < MyPrice) BuyVisible = false — условно говоря. А View у меня биндится к BuyVisible. Или это я поставил в кондицию DelegateCommand такую конструкцию. Но в любом случае эту видимость я задаю вo ViewModel. Модели же, напоминаю, пофиг. Она не продаст, даже если ткнуть в невидимую кнопку.
                        • 0
                          > Теперь, если мы (чисто из визуальных нужд) захотим скрыть кнопочки тех товаров, для покупки которых кредит недостаточен, тогда я прописываю во ViewModel if(Credit < MyPrice) BuyVisible = false — условно говоря.

                          Не BuyVisible, а CanBuy — ViewModel ничего не знает и не хочет знать о видимости.
                          Но о доступности того или иного действия сообщить может.
                          • 0
                            … короче однохренственно :)
                            • 0
                              Данный пример показывает доступность действия «CanBuy» о котором ViewModel может сообщить.
                              А если речь идёт не о доступности действия, а о доступности колонки в Grid на основе роли пользователя.
                              Например, нужно скрывать колонку с запрплатой, если у пользователя нет соответствующих прав на её простмотр.

                              В этом случае допускается иметь во ViewModel свойство SalaryVisible (или SalaryAllowed)?
                              • 0
                                По мне, так этот Salary кандидат на то, чтобы оказаться в модели
                                • 0
                                  Если у пользователя нет прав — этих данных вообще не должно быть на клиенте. Соответственно, visible неприменимо.
                                  • 0
                                    Это ведь довольно распространенная задача — скрыть/показать некоторые информационные поля во View в зависимости от привилегий пользователя.
                                    Как тогда по вашему должно выглядеть идеалогически правильное MVVM-решение данной задачи?
                                    • 0
                                      ViewModel может выставлять список логических колонок как ObservableCollection
                    • 0
                      К сожалению, в интернете оч туго с грамотными примерами MVVM. Очень часто у них View Обращается к тем или иным часятм Model напрямую. Тут же хоть не до конца все рассмотрено, но грамотнее чем в большинстве примеров что я встречал.
                      К тому же полезные коментарии :).
                      • 0
                        куча ViewModels, которые с тем же успехом мог сделать кодогенератор
                        Какой генератор, подскажите, вообще какой best practice для разработки поз WPF сейчас?
                        • 0
                          Я использую Prism. Вам советую прочитать Adam Nathan: WPF Unleashed, потом посмотреть видеокурсы Брайана Лагунаса про Prism, а потом, собственно, на него и перейти
                      • 0

                        В private static void Watch<T, T2>
                        неправильное условие в фильтре


                        if (a.OldItems?.Count == 1) collToUpdate.Remove(collToUpdate.First(mv => modelProperty(mv) == a.NewItems[0]));`
                        • 0
                          Действительно! Спасибо
                          В статье просто исправил, а в скачиваемом примере — забыл
                          • 0

                            а почему не использовать ограничения на типы вроде where T: class, new()


                            это позволило бы избавиться от активатора и писать более понятный и красивый код в виде
                            new T();

                            • 0
                              констрейнт new для конструктора по умолчанию, а нам нужно параметры передать
                              • 0
                                Окей, но можно тогда было бы сделать условный интерфейс IInitializable(ну или как-то ещё назвать) вроде

                                internal interface IInitializable 
                                {
                                    Inject(params object[] p);
                                }
                                


                                и таким образом заставить корректно инициализироваться объект.

                                Ну, это довольно общий случай, тут мог бы подойти вариант попроще, может.

                                Просто не уверен, что рефлексия — это достаточно хороший в данном случае вариант для создания, хотя я могу быть и не прав :)
                                • 0
                                  Нет, направление вашей мысли верное, только это несколько затеняет предмет статьи — MVVM. Такую функцию (Watch) я конечно бы не потащил в enterprise, но в статье я всячески убираю посторонние детали, чтобы сосредоточиться на основных существенных моментах MVVM
                              • 0

                                Компилятор C# преобразует new T(); в Activator.CreateInstance(typeof(T));
                                SO Тесты

                          • 0
                            Использование такого рода подписок на PropertyChanged, а так же на CollectionChanged чревато утечками памяти. Особенно в тех случаях, когда вью модели создаются динамически. Гораздо лучше использовать мессенжеры для обмена данными между вьюмоделями, или слабо связанные события. Мессенжер чем еще хорош, что позволяет практически полностью устранить связанность между вью моделями.
                            А использование асинхронного мессенжера позволит отложить перестройку вьюх, если это необходимо, до более подходящего момента.

                            Кроме того, вместо источников данных в виде ObservableCollection лучше использовать связку List и ICollectionView. Тогда при изменении данных вьюха не будет перестраиваться при каждом чихе, а будет лишь тогда, когда будет вызван Refresh() для ICollectionView.
                            Но в данном примере это не обязательно, так как динамических данных нет, все статично.
                            • 0
                              Это не enterprise код, мы тут фокусируемся не на том, как подписываться, — от этого я абстрагируюсь. Фокус на MVVM.
                              Что касается ObservableCollection, так смысл как раз в том, чтобы она не руками обновлялась Refresh()'ем, а автоматически. Вьшка вся не перестроится — это не веб:) — автоматически произойдет частичное обновление вью, как и задумывалось.
                              В конкретной программе есть коллекция с динамическим размером — это список покупок пользователя.

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