MVVM в WPF - Как предупредить ViewModel об изменениях в Model... или я должен?

Я просматриваю статьи MVVM, главным образом и .

Мой конкретный вопрос: Как мне изменить изменения модели с модели на ViewModel?

В статье Джоша я не вижу, что он это делает. ViewModel всегда запрашивает модель для свойств. В примере с Rachel у нее действительно есть модельный инструмент INotifyPropertyChanged и вызывает события из модели, но они предназначены для потребления самим видом (более подробную информацию о том, почему она это делает) (см. Ее статью/код).

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

Я подозреваю, что могут быть ответы/комментарии формы "Зачем вы хотите это сделать?" комментарии, поэтому здесь описание моей программы. Я новичок в MVVM, поэтому, возможно, весь дизайн неисправен. Я кратко опишу его.

Я программирую что-то более интересное (по крайней мере, для меня!), чем классы "Клиент" или "Продукт". Я программирую BlackJack.

У меня есть представление, которое не имеет никакого кода и просто полагается на привязку к свойствам и командам в ViewModel (см. статью Джоша Смита).

К лучшему или худшему я придерживался мнения, что модель должна содержать не только классы, такие как PlayingCard, Deck, но также класс BlackJackGame, который сохраняет состояние всей игры и знает, когда игрок пошел на спад, дилер должен рисовать карты, и то, что текущий счет игрока и дилера (меньше 21, 21, бюст и т.д.).

Из BlackJackGame Я выставляю такие методы, как "DrawCard", и мне пришло в голову, что при рисовании карты такие свойства, как CardScore и IsBust, должны обновляться, и эти новые значения передаются в ViewModel. Возможно, это ошибочное мышление?

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

В моей модели ViewModel у меня есть логика, чтобы захватить фактическое изображение игровой карты (основанной на масти, ранге) и сделать ее доступной для представления. Модель не должна заботиться об этом (возможно, другой ViewModel просто использовал бы цифры вместо того, чтобы играть в карточные изображения). Конечно, возможно, некоторые скажут мне, что у модели не должно быть даже понятия о игре BlackJack, и что нужно обрабатывать в ViewModel?

Ответ 1

Если вы хотите, чтобы ваши модели предупреждали ViewModels об изменениях, они должны реализовывать INotifyPropertyChanged, а ViewModels должны подписаться на получение уведомлений PropertyChange.

Ваш код может выглядеть примерно так:

// Attach EventHandler
PlayerModel.PropertyChanged += PlayerModel_PropertyChanged;

...

// When property gets changed in the Model, raise the PropertyChanged 
// event of the ViewModel copy of the property
PlayerModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "SomeProperty")
        RaisePropertyChanged("ViewModelCopyOfSomeProperty");
}

Но обычно это необходимо только в том случае, если более чем один объект будет вносить изменения в данные модели, что обычно не так.

Если у вас когда-либо был случай, когда на самом деле у вас нет ссылки на ваше свойство Model, чтобы прикрепить к нему событие PropertyChanged, вы можете использовать систему сообщений, такую ​​как Prism EventAggregator или MVVM Light Messenger.

У меня есть краткий обзор систем обмена сообщениями в моем блоге, однако, чтобы обобщить его, любой объект может транслировать сообщение, и любой объект может подписаться на прослушивание конкретных Сообщения. Таким образом, вы можете транслировать PlayerScoreHasChangedMessage из одного объекта, а другой объект может подписаться на прослушивание этих типов сообщений и обновить его свойство PlayerScore, когда он услышит его.

Но я не думаю, что это необходимо для системы, которую вы описали.

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

Итак, у вас, вероятно, были бы простые объекты данных модели, подобные этим:

class CardModel
{
    int Score;
    SuitEnum Suit;
    CardEnum CardValue;
}

class PlayerModel 
{
    ObservableCollection<Card> FaceUpCards;
    ObservableCollection<Card> FaceDownCards;
    int CurrentScore;

    bool IsBust
    {
        get
        {
            return Score > 21;
        }
    }
}

и у вас будет объект ViewModel, например

public class GameViewModel
{
    ObservableCollection<CardModel> Deck;
    PlayerModel Dealer;
    PlayerModel Player;

    ICommand DrawCardCommand;

    void DrawCard(Player currentPlayer)
    {
        var nextCard = Deck.First();
        currentPlayer.FaceUpCards.Add(nextCard);

        if (currentPlayer.IsBust)
            // Process next player turn

        Deck.Remove(nextCard);
    }
}

(Над всеми объектами должны быть реализованы INotifyPropertyChanged, но я оставил их для простоты)

Ответ 2

Короткий ответ: это зависит от специфики.

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

  • Viewmodel создается и обертывает модель
  • Viewmodel подписывается на событие PropertyChanged
  • Viewmodel устанавливается как вид DataContext, свойства привязаны и т.д.
  • Просмотр действий триггеров на viewmodel
  • Метод отображения Viewmodel на модели
  • Модель обновляет себя
  • Viewmodel обрабатывает модель PropertyChanged и возвращает свой собственный PropertyChanged в ответ
  • Вид отражает изменения в его привязках, закрывая цикл обратной связи

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

Я описываю такую ​​конструкцию в другом вопросе MVVM здесь.

Ответ 3

Довольно старая нить, но после многого поиска я придумал свое собственное решение: PropertyChangedProxy

С помощью этого класса вы можете легко зарегистрировать кого-то еще NotifyPropertyChanged и предпринять соответствующие действия, если он уволен для зарегистрированного свойства.

Вот пример того, как это может выглядеть, когда у вас есть свойство модели "Статус", которое может измениться по своему усмотрению, и затем должно автоматически уведомить ViewModel о том, чтобы запустить его свойство PropertyChanged в качестве свойства "Статус", чтобы вид также уведомлено:)

public class MyModel : INotifyPropertyChanged
{
    private string _status;
    public string Status
    {
        get { return _status; }
        set { _status = value; OnPropertyChanged(); }
    }

    // Default INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class MyViewModel : INotifyPropertyChanged
{
    public string Status
    {
        get { return _model.Status; }
    }

    private PropertyChangedProxy<MyModel, string> _statusPropertyChangedProxy;
    private MyModel _model;
    public MyViewModel(MyModel model)
    {
        _model = model;
        _statusPropertyChangedProxy = new PropertyChangedProxy<MyModel, string>(
            _model, myModel => myModel.Status, s => OnPropertyChanged("Status")
        );
    }

    // Default INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        var handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

и здесь сам класс:

/// <summary>
/// Proxy class to easily take actions when a specific property in the "source" changed
/// </summary>
/// Last updated: 20.01.2015
/// <typeparam name="TSource">Type of the source</typeparam>
/// <typeparam name="TPropType">Type of the property</typeparam>
public class PropertyChangedProxy<TSource, TPropType> where TSource : INotifyPropertyChanged
{
    private readonly Func<TSource, TPropType> _getValueFunc;
    private readonly TSource _source;
    private readonly Action<TPropType> _onPropertyChanged;
    private readonly string _modelPropertyname;

    /// <summary>
    /// Constructor for a property changed proxy
    /// </summary>
    /// <param name="source">The source object to listen for property changes</param>
    /// <param name="selectorExpression">Expression to the property of the source</param>
    /// <param name="onPropertyChanged">Action to take when a property changed was fired</param>
    public PropertyChangedProxy(TSource source, Expression<Func<TSource, TPropType>> selectorExpression, Action<TPropType> onPropertyChanged)
    {
        _source = source;
        _onPropertyChanged = onPropertyChanged;
        // Property "getter" to get the value
        _getValueFunc = selectorExpression.Compile();
        // Name of the property
        var body = (MemberExpression)selectorExpression.Body;
        _modelPropertyname = body.Member.Name;
        // Changed event
        _source.PropertyChanged += SourcePropertyChanged;
    }

    private void SourcePropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == _modelPropertyname)
        {
            _onPropertyChanged(_getValueFunc(_source));
        }
    }
}

Ответ 4

Ваш выбор:

  • Внедрение INotifyPropertyChanged
  • События
  • POCO с прокси-манипулятором

Как я вижу, INotifyPropertyChanged является фундаментальной частью .Net. то есть его в System.dll. Реализация его в вашей "Модели" сродни реализации структуры событий.

Если вы хотите использовать чистый POCO, вам нужно эффективно управлять своими объектами через прокси/службы, а затем ваш ViewModel уведомляется об изменениях, слушая прокси.

Лично я просто свободно реализую INotifyPropertyChanged, а затем использую FODY, чтобы сделать для меня грязную работу. Он выглядит и чувствует POCO.

Пример (с помощью FODY to IL: Weave с помощью PropertyChanged raisers):

public class NearlyPOCO: INotifyPropertyChanged
{
     public string ValueA {get;set;}
     public string ValueB {get;set;}

     public event PropertyChangedEventHandler PropertyChanged;
}

то вы можете заставить свой ViewModel прослушивать PropertyChanged для любых изменений; или изменения свойств.

Красота маршрута INotifyPropertyChanged, вы связали его с Extended ObservableCollection. Таким образом, вы сбрасываете свои объекты poco в коллекцию и слушаете коллекцию... если что-то меняется, где-нибудь, вы узнаете об этом.

Я буду честен, это могло бы присоединиться к обсуждению "Почему не было обработано с помощью компилятора без изменений в INotifyPropertyChanged", которое делится на: Каждый объект в С# должен иметь средство для уведомления, если какая-либо его часть была изменена; т.е. реализовать INotifyPropertyChanged по умолчанию. Но это не так, и лучший маршрут, требующий наименьшего усилия, - использовать IL Weaving (в частности FODY).

Ответ 5

Уведомление на основе INotifyPropertyChanged и INotifyCollectionChanged - именно то, что вам нужно. Чтобы упростить вашу жизнь с подпиской на изменения свойств, проверять имя свойства во время компиляции, избегая утечек памяти, я бы посоветовал вам использовать PropertyObserver из Фонд MVVM Джоша Смита. Поскольку этот проект является открытым исходным кодом, вы можете добавить только этот класс в свой проект из источников.

Чтобы понять, как использовать PropertyObserver, прочитайте эту статью.

Кроме того, посмотрите глубже на Reactive Extensions (Rx). Вы можете открыть IObserver <T> из своей модели и подписаться на нее в модели просмотра.

Ответ 6

Я отстаивал направленную модель → View Model → Просмотр потока изменений в течение длительного времени, как вы можете видеть в разделе "Поток изменений" моего MVVM от 2008. Это требует реализации INotifyPropertyChanged для модели. Насколько я могу судить, это стало обычной практикой.

Поскольку вы упомянули Джоша Смита, посмотрите свой класс PropertyChanged. Это вспомогательный класс для подписки на событие модели INotifyPropertyChanged.PropertyChanged.

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

Ответ 7

Я нашел эту статью полезной: http://social.msdn.microsoft.com/Forums/vstudio/en-US/3eb70678-c216-414f-a4a5-e1e3e557bb95/mvvm-businesslogic-is-part-of-the-?forum=wpf

Мое резюме:

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

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

Уведомления о изменениях данных и проверка данных происходят на каждом уровне (вид, модель представления и модель).

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

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

Отбросы статьи:

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

Вид - это уровень представления - все, что связано с фактическим напрямую взаимодействуя с пользователем.

ViewModel - это в основном "клей", свойственный вашему приложение, которое связывает их вместе.

У меня есть хорошая диаграмма, показывающая, как они взаимодействуют:

http://reedcopsey.com/2010/01/06/better-user-and-developer-experiences-from-windows-forms-to-wpf-with-mvvm-part-7-mvvm/

В вашем случае - позволяет решить некоторые особенности...

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

Операции "за кулисами без представления, например, для записи в БД, отправка электронной почты и т.д.": это действительно часть "Domain Specific Операции" на моей диаграмме и действительно является чисто частью Модели. Это то, что вы пытаетесь открыть через приложение. ViewModel выступает в качестве моста для раскрытия этой информации, но операции - это чистая модель.

Операции для ViewModel: ViewModel требует больше, чем просто INPC - ему также нужна любая операция, специфичная для вашего приложения (а не ваша бизнес-логика), такая как сохранение настроек и состояния пользователя, и т.д. Это будет варьироваться в зависимости от приложения. к примеру, даже при взаимодействии той же "модели".

Хороший способ подумать об этом - скажите, что вы хотите сделать 2 версии своих система заказа. Первый - в WPF, а второй - в сети интерфейс.

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

В приложении WPF пользовательский интерфейс (что взаимодействует зритель с) является "представлением" - в веб-приложении это в основном код, который (по крайней мере, в конце концов) превращается в javascript + html + css на клиенте.

ViewModel - это остальная часть "клея", которая требуется для модель (эти операции, связанные с заказом), чтобы заставить ее работать с конкретной технологией/уровнем просмотра, которые вы используете.

Ответ 8

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

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

В модели просмотра я сохранил ссылку на объект в модели и подписался на событие CollectionChanged наблюдаемого коллекционирования, например: ModelObject.ChildElements.CollectionChanged += new CollectionChangedEventHandler(insert function reference here)...

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

Ответ 9

Ребята сделали замечательную работу, ответив на это, но в таких ситуациях я действительно чувствую, что шаблон MVVM - это боль, поэтому я бы пошел и использовал подход Supervising Controller или Passive View и отпустил систему привязки, по крайней мере, для объекты модели, которые самостоятельно генерируют изменения.

Ответ 10

Мне кажется, что это действительно важный вопрос - даже когда нет давления на это. Я работаю над тестовым проектом, который включает TreeView. Есть пункты меню и такие, которые отображаются на команды, например Delete. В настоящее время я обновляю модель и модель представления из модели представления.

Например,

public void DeleteItemExecute ()
{
    DesignObjectViewModel node = this.SelectedNode;    // Action is on selected item
    DocStructureManagement.DeleteNode(node.DesignObject); // Remove from application
    node.Remove();                                // Remove from view model
    Controller.UpdateDocument();                  // Signal document has changed
}

Это просто, но, похоже, имеет очень простой недостаток. Типичный unit test выполнит команду, а затем проверит результат в модели представления. Но это не проверяет правильность обновления модели, так как оба обновления обновляются одновременно.

Поэтому, возможно, лучше использовать такие методы, как PropertyObserver, чтобы позволить обновлению модели инициировать обновление модели представления. Тот же самый unit test теперь будет работать только в том случае, если оба действия были успешными.

Это не потенциальный ответ, я понимаю, но, похоже, стоит потушить там.

Ответ 11

Нет ничего плохого в реализации INotifyPropertyChanged внутри модели и прослушивать ее внутри ViewModel. На самом деле вы даже можете усеивать свойство модели прямо в XAML: {Binding Model.ModelProperty}

Что касается зависимых/рассчитанных свойств только для чтения, я не видел ничего лучше и проще: https://github.com/StephenCleary/CalculatedProperties. Это очень просто, но невероятно полезно, это действительно "формулы Excel для MVVM" - просто работает так же, как Excel, распространяя изменения в ячейках формулы без дополнительных усилий с вашей стороны.