Проблемы с пониманием использования MVVM при использовании WCF DTO в смарт-клиентском приложении WPF

Имея очень мало опыта в этой области, я пишу приложение смарт-клиента WPF, общающееся с бэкэндом WCF с использованием MVVM, и я действительно пытаюсь принять правильные решения из всей информации. Это приводит меня к набору вопросов, которые, я надеюсь, могут быть решены здесь людьми, более опытными в этой области.

В качестве примера один из экранов позволит ввести порядок и добавить строки заказа в порядок.

Что используется в качестве модели?

В службе WCF у меня есть следующий упрощенный DTO:

public OrderDTO
{
   string orderDetails { get; set; }
   List<OrderLineDTO> OrderLines { get; set; }
}

public OrderLineDTO
{
   int customerId { get; set; }
   int productId { get; set; }
   double quantity { get; set; }
}

И служба WCF, которая имеет следующий метод:

public OrderService Order
{
    CreateOrderResponse CreateOrder(OrderDTO order) 
}

В моем интеллектуальном клиенте WPF у меня есть ссылка на DTO, но, очевидно, он не реализует INotifyPropertyChanged, поскольку он предназначен исключительно для транспорта.

Вопросы

Будет ли рекомендованный подход преобразовать эти DTO в модель, которая реализует INotifyPropertyChanged с помощью Automapper или аналогичного? Или должен ли DTO использоваться в качестве модели непосредственно в ViewModel?

Общение между моделями просмотров

В настоящее время у меня есть вид заказа с 2 вкладками (Order и OrderLines) с ViewModels OrderViewModel и OrderLineViewModel. На вкладке заказа у меня есть ComboBox содержащий идентификаторы и имена клиентов. Когда я выбираю клиента в OrderView, мне нужно сообщить OrderLineView, что клиент был выбран так, чтобы ComboBox показывал только продукты, принадлежащие этому клиенту.

Вопросы

Как OrderViewModel связывается с OrderLineViewModel в этом сценарии?

Добавление строки заказа и применение логических/бизнес-правил

В качестве приложения уровня сервера будут использоваться несколько клиентов, например, ПК, мобильные устройства. Я хотел бы убедиться, что все бизнес-правила применяются в приложении уровня сервера. Например, когда добавляется строка заказа. если он относится к определенному типу продукта, он может быть добавлен только в том случае, если у клиента есть определенная сертификация.

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

Вопросы

Вы разрешаете пользователю добавлять строку недействительный, отправлять запрос на сервер, разрешать серверу применять соответствующие правила и возвращать ответ? Или вы каким-то образом применяете логику в смарт-клиентском приложении перед отправкой запроса на сервер?

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

Спасибо

Алекс

Edit: Спасибо всем за ваш вклад, поскольку он помог мне стать более понятным с точки зрения лучшего пути вперед. Все ответы были хорошими, но я решил принять ответ Ури, поскольку он лучше всего подходит моим мыслям на этом этапе. Тем не менее, я по-прежнему не уверен в том, что лучше всего обработать преобразование из идентификатора DTO в SelectedItem в ItemsSource, который является списком ViewModels. Я вижу, что конвертер может работать, но я попытаюсь найти другое решение. Спасибо Алекс

Ответы

Ответ 1

Вот мои размышления о ваших вопросах:

Вопрос: Будет ли рекомендованный подход преобразовать эти DTO в модель, которая реализовала INotifyPropertyChanged с помощью Automapper или аналогичного? Или следует использовать DTO в качестве модели непосредственно в viewmodel?

Ответ: Мой подход Мне больше всего нравится сдерживание. Я согласен с тобой, что у DTO не должно быть ничего, кроме геттеров и сеттеров. Держите его как можно более чистым, поэтому он не должен запускать INotifyPropertyChanged. Я также не думаю, что представление должно иметь прямой доступ к объектной модели (если по какой-либо другой причине у вас нет преимущества изменения свойства). Недостатком моего подхода является некоторый дополнительный код в ViewModel, но я думаю, что он того стоит.

public class VmBase : INotifyPropertyChanged {

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void raise( string propName )
    {
        if( PropertyChanged ) {
            PropertyChanged( this, new PropertyChangedEventArgs(propName) );
        }
    }
}

public class OrderLineVm : VmBase {
    private OrderLineDTO orderLine;

    public OrderLineVm( OrderLineDTO ol ) {
        orderLine = ol;
    }

    public OrderLineVm( ) {
        orderLine = new OrderLineDTO();
    }

    int customerId {
        get { return orderLine.customerId; }
        set { orderLine.customerId=value; raise("customerId"); }
    }

    int productId {
        get { return orderLine.productId; }
        set { orderLine.productId=value; raise("productId"); }
    }

    double quantity {
       ...
    }
}

Через чудо сбора мусора OrderLineDTO будет создан только один раз (когда он исходит от сервера) и будет жить до тех пор, пока это необходимо. Существуют два открытых конструктора: один с DTO (как правило, при использовании объектов, идущих с сервера), и один, созданный на клиенте.

Для OrderVm это немного сложнее, так как вы хотели бы иметь ObservableCollection (vs. List) OrderLineVm (по сравнению с OrderLineDTO), поэтому сдерживание не будет работать. Также обратите внимание, что orderLines имеет только getter (вы добавляете и удаляете строки заказа из него, но вы не меняете весь список. Выделите его один раз во время построения).

public class OrderVm : VmBase {
    private string _orderDetails;
    public string orderDetails {
        get { return _orderDetails;
        set { _orderDetails=value; raise("orderDetails"); }
    }

    private ObservableCollection<OrderLineVm> _orderLines;
    public ObservableCollection<OrderLineVm> orderLines { 
        get { return _orderLines; }
    }
}

Вопрос: Как бы OrderViewModel взаимодействовать с OrderLineViewModel в этом сценарии?

Ответ: Если требуется общение, действительно, вы должны сделать это самым простым способом. Оба класса View Model находятся на одинаковых уровнях. OrderVm ссылается на список OrderLineVm, и если вам требуется сообщение из класса OrderLineVm для заказа, просто держите ссылку.

Однако я бы сильно сказал, что общение не требуется. После того, как представление связано должным образом, я не вижу причин для такого общения. Свойство Mode привязки должно быть "двухсторонним", поэтому все изменения в пользовательском интерфейсе будут изменены в Model View. Добавление, удаление в список строк заказа автоматически отразится на представлении благодаря уведомлениям, отправленным из ObservableCollection.

Вопросы:. Вы разрешаете пользователю добавлять недопустимую строку, отправляющую запрос на сервер, чтобы сервер применял соответствующие правила и возвращал ответ? Или вы каким-то образом применяете логику в смарт-клиентском приложении перед отправкой запроса на сервер?

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

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

EDIT: (комментарий от Alex):

Отображение выпадающего списка. Я думаю, что источником вашей путаницы является то, что на самом деле есть два независимых элемента ItemsSource (и, следовательно, два отдельных контекста данных): существует один список строк заказа, а встроенный в каждую строку заказа список ProductID, которые являются элементами, заполняющими combobox. Только свойство SelectedItem является свойством ProductLine. Как правило, список возможных ProductID должен быть глобальным для приложения (или порядка). У вас будет свойство ProductID свойства всей формы и присвоить ему имя (например, x: Key или x: Name). Затем в элементе ComboBox просто ссылайтесь на этот список:

<ComboBox ItemsSource="{Binding Source={StaticResource ProductIDs}}"
          SelectedItem="{Binding Path=productId}"
          />

Ответ 2

Чтобы ответить на ваши вопросы по очереди...

1) Если вам не нужны ваши свойства, чтобы уведомить пользовательский интерфейс, когда они меняются, тогда нет необходимости использовать INotifyPropertyChanged - и, на мой взгляд, вы можете привязать модель непосредственно к представлению. Нет необходимости добавлять дополнительный слой, если он не добавляет никаких дополнительных функций. Однако в большинстве приложений вы захотите изменить состояние объекта модели с помощью пользовательского интерфейса. В этом случае вам нужно будет добавить объекты View Model, которые реализуют INotifyPropertyChanged. Вы можете либо создать модель представления, которая адаптирует модель, то есть делегирование свойств базовой модели, либо скопировать состояние объекта модели в эквивалентную модель представления.

Чтобы избежать написания большого количества довольно похожего кода, то есть того же объекта домена, который представлен как объект модели и объект модели представления, я стараюсь использовать, по возможности, генерировать код. Мне нравится использовать XML для описания моей модели и шаблонов T4 для codegen.

2) Как OrderViewModel сообщить OrderLineViewModel? напрямую! Понятия звучат довольно тесно, я бы предположил, что порядок имеет несколько строк порядка? В этом случае только одна точка зрения ссылается на другую. Нет необходимости в причудливых посредниках, если они тесно связаны внутри вашего домена.

3) Хороший вопрос! Я согласен с тем, что сервер должен применять проверку. Независимо от того, дублируете ли вы эту проверку на клиенте, зависит от ваших требований. Если ваше общение с сервером происходит быстро и часто, вы можете обеспечить хороший пользовательский интерфейс, обмениваясь информацией с сервером, когда пользователь редактирует заказы и предоставляет подтверждение при переходе от поля к полю. Однако во многих случаях это нецелесообразно. Чаще всего применяется простая проверка в клиентском приложении, но позволяет серверу выполнять более сложные проверки, например проверку на уникальность и т.д.

Надеюсь, что это поможет.

Ответ 3

Я считаю, что реальный вопрос заключается в том, насколько верно для шаблона MVVM вы бы хотели быть?

Идея MVVM, а также аналогичные модели, такие как MVC и MVP, - это разделение проблем. В то время как я тоже занимался этой темой, я подробно рассмотрел, что шаблон пытается выполнить, и выбор стал легче.

С MVVM у вас есть три проблемы: вид (V), модель (M) и ViewModel (VM). Кажется довольно очевидным, не так ли? Но спросите себя, что на самом деле беспокоит и что происходит, если мы начнем смешивать проблемы - так же, как это происходит, когда мы смешиваем проблемы в другом месте. Наш код становится сложнее изменить.

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

(Это свежо в моем сознании, потому что я только что предпринял такое усилие, и настоящие приложения MVVM были привязаны к reskin, в то время как остальные занимали в 10-25 раз больше времени!)

Этот же сценарий влияет и на "back-end" шаблона.

Цель Модели - переносить данные в/из любого механизма устойчивости, который вы используете с вашим приложением. Это может быть веб-служба, база данных, текстовый файл и т.д. Просто потому, что WCF добавляет такие возможности, как INotifyPropertyChanged, не означает, что рекомендуется использовать их. Помните, что Microsoft занимается разработкой инструментов. Чтобы продать эти инструменты, они должны работать в самых разных ситуациях и на разных уровнях. Например, службы RIA отлично подходят для быстрых и грязных приложений, но быстро разбиваются, когда применяются к реальным решениям (по крайней мере, по моему опыту).

Итак, что произойдет, если вы используете модель сдерживания и передаете все ваши свойства делегату объекту модели, находящемуся в состоянии в вашей модели ViewModel, и характер изменений модели? Или модель не делает все, что вам нужно. Факт в том, что ViewModel должен быть адаптером, который дает пользовательскому интерфейсу то, что ему нужно для работы. Между моделью не должно быть отношения 1:1, но я знаю, что это происходит.

Что произойдет, если через 6 месяцев вы решите пойти с услугой REST вместо WCF? Теперь у вас нет поддержки INPC в вашей модели, потому что вы не имеете дело с автогенерированными прокси-классами. Хотя это не так осязаемо, как изменения пользовательского интерфейса, здесь применяются те же идеи, и поэтому шаблон разделяет модель.

Мой совет состоит в том, чтобы пойти с вашим первым инстинктом и использовать AutoMapper для сопоставления данных, содержащихся в объекте Model, в вашем ViewModel и наоборот. AutoMapper позволяет легко справляться с проблемами несоответствия импеданса, с которыми вы можете столкнуться, и дать вам одно место для внесения изменений, если одна или другая часть контракта изменится.

2

У вас есть объектная модель, и в этом случае события, обратные вызовы и т.д. совершенно законны. Я бы сказал, что ваш OrderViewModel содержит коллекцию объектов OrderLineViewModel. Дочерние объекты могут содержать ссылку на родителя (OrderViewModel) и вытаскивать из него выбранного клиента.

3

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

Что касается вопроса о бизнес-правилах с округлением, я бы сказал нет. Я пытаюсь обеспечить соблюдение многих бизнес-правил в клиентском приложении. Во-первых, он улучшает пользовательский интерфейс и снижает сетевой трафик, необходимый клиенту. Одно правило большого пальца я следую за тем, что я никогда не разрешаю пользователю сохранять недопустимый объект. Примечание. Неточные или неполные данные не совпадают с недействительными. Недопустимые данные приводят к исключениям.

Ответ 4

У меня есть 2-летний опыт создания "богатых клиентов" (в WPF).

В моем интеллектуальном клиенте WPF у меня есть ссылка на DTO, но ясно он не реализует INotifyPropertyChanged, поскольку он предназначен исключительно для транспорт.

Неверно
WCF будет автоматически внедрять INPC на каждом DTO, по умолчанию.
Лучше всего было бы использовать DTO как ViewModels для простых представлений, а для более сложных видов использовать композицию "pattern".

Общение между моделями просмотров

"Лучшая" практика (читайте: что почти все делают) заключается в том, чтобы использовать слабую структуру событий, чтобы держать вещи слабо связанными. Самый известный из них - IEventAggregator из библиотеки PRISM, но есть несколько вариантов реализации.

Добавление строки заказа и применение логических/бизнес-правил

Подумайте о клиенте так же, как о веб-странице: не доверяйте ему. Это код .NET, и все мы знаем, как легко его взломать.
Вот почему вы должны иметь проверки безопасности, реализованные в вашей службе WCF.

НТН,

Бабы.