WPF MVVM - как определить, является ли представление «грязным»,

17

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

Например, если пользователь изменяет поле даты в представлении, а затем пытается закрыть представление, приложение отобразит сообщение с просьбой о продолжении и потерях изменений или Отмена, чтобы они могли нажать кнопку «Сохранить».

Проблема заключается в следующем: как определить, что какое-либо из полей данных изменилось в представлении?

Надеюсь, что это имеет смысл, чем вы заранее, приветствует,

    
задан ElMatador 04.05.2011 в 23:44
источник
  • В MVVM вы спросите, загрязнена ли модель или, может быть, ViewModel. Не вид. –  Henk Holterman 04.05.2011 в 23:54

2 ответа

30

Один из подходов, который вы можете предпринять, - использовать IChangeTracking и интерфейсы INotifyPropertyChanged .

Если вы создаете абстрактный базовый класс, который ваши модели представлений наследуют от (ViewModelBase), который реализует интерфейсы IChangeTracking и INotifyPropertyChanged, вы можете подключить базовую модель вашего представления к уведомлению об изменениях свойств (фактически сигнализируя, что модель представления была изменено) и установит для свойства IsChanged значение true, чтобы указать, что модель просмотра «грязная».

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

В описанном вами случае вы можете обрабатывать Unloaded или Закрытие события вашего просмотра для проверки DataContext ; и если DataContext реализует IChangeTracking, вы можете использовать свойство IsChanged, чтобы определить, были ли сделаны какие-либо неприемлемые изменения.

Простой пример:

/// <summary>
/// Provides a base class for objects that support property change notification 
/// and querying for changes and resetting of the changed status.
/// </summary>
public abstract class ViewModelBase : IChangeTracking, INotifyPropertyChanged
{
    //========================================================
    //  Constructors
    //========================================================
    #region ViewModelBase()
    /// <summary>
    /// Initializes a new instance of the <see cref="ViewModelBase"/> class.
    /// </summary>
    protected ViewModelBase()
    {
        this.PropertyChanged += new PropertyChangedEventHandler(OnNotifiedOfPropertyChanged);
    }
    #endregion

    //========================================================
    //  Private Methods
    //========================================================
    #region OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e)
    /// <summary>
    /// Handles the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for this object.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">A <see cref="PropertyChangedEventArgs"/> that contains the event data.</param>
    private void OnNotifiedOfPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e != null && !String.Equals(e.PropertyName, "IsChanged", StringComparison.Ordinal))
        {
            this.IsChanged = true;
        }
    }
    #endregion

    //========================================================
    //  IChangeTracking Implementation
    //========================================================
    #region IsChanged
    /// <summary>
    /// Gets the object's changed status.
    /// </summary>
    /// <value>
    /// <see langword="true"/> if the object’s content has changed since the last call to <see cref="AcceptChanges()"/>; otherwise, <see langword="false"/>. 
    /// The initial value is <see langword="false"/>.
    /// </value>
    public bool IsChanged
    {
        get
        {
            lock (_notifyingObjectIsChangedSyncRoot)
            {
                return _notifyingObjectIsChanged;
            }
        }

        protected set
        {
            lock (_notifyingObjectIsChangedSyncRoot)
            {
                if (!Boolean.Equals(_notifyingObjectIsChanged, value))
                {
                    _notifyingObjectIsChanged = value;

                    this.OnPropertyChanged("IsChanged");
                }
            }
        }
    }
    private bool _notifyingObjectIsChanged;
    private readonly object _notifyingObjectIsChangedSyncRoot = new Object();
    #endregion

    #region AcceptChanges()
    /// <summary>
    /// Resets the object’s state to unchanged by accepting the modifications.
    /// </summary>
    public void AcceptChanges()
    {
        this.IsChanged = false;
    }
    #endregion

    //========================================================
    //  INotifyPropertyChanged Implementation
    //========================================================
    #region PropertyChanged
    /// <summary>
    /// Occurs when a property value changes.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    #endregion

    #region OnPropertyChanged(PropertyChangedEventArgs e)
    /// <summary>
    /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event.
    /// </summary>
    /// <param name="e">A <see cref="PropertyChangedEventArgs"/> that provides data for the event.</param>
    protected void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }
    #endregion

    #region OnPropertyChanged(string propertyName)
    /// <summary>
    /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyName"/>.
    /// </summary>
    /// <param name="propertyName">The <see cref="MemberInfo.Name"/> of the property whose value has changed.</param>
    protected void OnPropertyChanged(string propertyName)
    {
        this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }
    #endregion

    #region OnPropertyChanged(params string[] propertyNames)
    /// <summary>
    /// Raises the <see cref="INotifyPropertyChanged.PropertyChanged"/> event for the specified <paramref name="propertyNames"/>.
    /// </summary>
    /// <param name="propertyNames">An <see cref="Array"/> of <see cref="String"/> objects that contains the names of the properties whose values have changed.</param>
    /// <exception cref="ArgumentNullException">The <paramref name="propertyNames"/> is a <see langword="null"/> reference (Nothing in Visual Basic).</exception>
    protected void OnPropertyChanged(params string[] propertyNames)
    {
        if (propertyNames == null)
        {
            throw new ArgumentNullException("propertyNames");
        }

        foreach (var propertyName in propertyNames)
        {
            this.OnPropertyChanged(propertyName);
        }
    }
    #endregion
}
    
ответ дан Oppositional 05.05.2011 в 01:26
  • спасибо, это было полезно для меня тоже –  ganeshran 03.10.2011 в 11:35
  • Я искал именно эту вещь для использования в моем приложении MVVM и WPF. Однако, когда я пытаюсь использовать его, IsChanged всегда прав. Есть предположения? –  Juan 12.12.2012 в 18:34
  • Этот подход не будет корректно определять грязное состояние для TextBox во время редактирования. Эта информация «заблокирована внутри» класса привязки WPF и не может быть доступна ни с помощью View, ни с ViewModel. Возможно реализовать пользовательскую привязку, но это очень тяжелая работа и требует использования нестандартного XAML. Трудно поверить, что это 2013 год, не так ли. –  Jack 23.02.2013 в 19:06
  • Эй, у меня проблема с этим подходом, потому что, когда я загружаю объекты из db, они автоматически получают IsChanged = true, потому что свойства установлены, прежде чем они будут доступны. Любые идеи, как сказать, не устанавливать IsChanged, когда значения поступают из базы данных? –  adminSoftDK 03.12.2014 в 10:34
  • @adminSoftDK просто вызывает функцию AcceptChanges после начальной загрузки. Если у вас есть список моделей, вы можете легко сделать это с помощью Linq DataOrViewModel.ForEach (x => x.AcceptChanges ()); –  Swifty 11.11.2015 в 12:25
12

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

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

Если вы хотите, чтобы изменения применялись к модели только по «ОК» или «Принять»,
привязать View к View-Model, которая не касается изменений в Model,
пока не будет применен ApplyCommand или AcceptCommand (который вы определяете и реализуете).

(Команды, которые привязаны к представлению, реализованы с помощью View-Model.)

Пример - VM:

public class MyVM : INotifyPropertyChanged
{
    public string MyText
    {
        get
        {
            return _MyText;
        }
        set
        {
            if (value == _MyText)
                return;

            _MyText = value;
            NotifyPropertyChanged("MyText");
        }
    }
    private string _MyText;

    public string MyTextTemp
    {
        get
        {
            return _MyTextTemp;
        }
        set
        {
            if (value == _MyTextTemp)
                return;

            _MyTextTemp = value;
            NotifyPropertyChanged("MyTextTemp");
            NotifyPropertyChanged("IsTextDirty");
        }
    }
    private string _MyTextTemp;

    public bool IsTextDirty
    {
        get
        {
            return MyText != MyTextTemp;
        }
    }

    public bool IsMyTextBeingEdited
    {
        get
        {
            return _IsMyTextBeingEdited;
        }
        set
        {
            if (value == _IsMyTextBeingEdited)
                return;

            _IsMyTextBeingEdited = value;

            if (!value)
            {
                MyText = MyTextTemp;
            }

            NotifyPropertyChanged("IsMyTextBeingEdited");
        }
    }
    private bool _IsMyTextBeingEdited;


    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

Пример - Вид:

    <Label Content="{Binding MyText}" />

    <!-- You can translate the events to commands by using a suitable framework -->
    <!-- or use code behind to update a new dependency property as in this example -->
    <TextBox
        LostFocus="TextBox_LostFocus"
        GotFocus="TextBox_GotFocus"
        Text="{Binding Path=MyTextTemp, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
        />

Пример - вид - код позади:

    public MainWindow()
    {
        InitializeComponent();

        SetBinding(IsTextBoxFocusedProperty,
            new Binding
            {
                Path = new PropertyPath("IsMyTextBeingEdited"),
                Mode = BindingMode.OneWayToSource,
            });
    }

    private void TextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        IsTextBoxFocused = false;
    }

    private void TextBox_GotFocus(object sender, RoutedEventArgs e)
    {
        IsTextBoxFocused = true;
    }

    #region IsTextBoxFocused

    /// <summary>
    /// Gets or Sets IsTextBoxFocused
    /// </summary>
    public bool IsTextBoxFocused
    {
        get
        {
            return (bool)this.GetValue(IsTextBoxFocusedProperty);
        }
        set
        {
            this.SetValue(IsTextBoxFocusedProperty, value);
        }
    }

    /// <summary>
    /// The backing DependencyProperty behind IsTextBoxFocused
    /// </summary>
    public static readonly DependencyProperty IsTextBoxFocusedProperty = DependencyProperty.Register(
      "IsTextBoxFocused", typeof(bool), typeof(MainWindow), new PropertyMetadata(default(bool)));

    #endregion
    
ответ дан Danny Varod 05.05.2011 в 00:00
  • Неправильно. В WPF для элемента управления TextBox по умолчанию используется UpdateSourceTrigger = LostFocus. Это означает, что View может быть грязным, а ViewModel - нет. Причина для LostFocus заключается в том, что в противном случае частичные изменения (например, для DateTime повысят ошибку проверки). Это ошибка дизайна в WPF. Надежные приложения должны учитывать IsDirty = ViewModel.IsDirty || View.IsDirty ... –  Jack 22.02.2013 в 12:49
  • @Jack, представление может состоять из множества различных типов элементов управления. По умолчанию для текстовых полей необходимо предотвратить обновление модели просмотра при каждом нажатии клавиши. Проверка того, изменились ли тексты во время редактирования, это, вероятно, плохая идея. –  Danny Varod 22.02.2013 в 18:16
  • Итак, этот комментарий не должен содержать 566 символов? И кнопки «Сохранить» и «Отмена» в форме должны оставаться отключенными, даже если пользователь изменил текст в текстовом поле? Давай! –  Jack 23.02.2013 в 08:59
  • @Jack, добавьте две привязки в текстовое поле, то есть два пути с обычным триггером, и один из них - один из способов источника для другого свойства в виртуальной машине с помощью триггера смены. Проверьте второе свойство в виртуальной машине, чтобы узнать, загрязнено ли состояние, а не вид. –  Danny Varod 25.02.2013 в 01:33
  • @DannyVarod, AHA! магическое заклинание - UpdateSourceTrigger = PropertyChanged ... спасибо! И по внешнему виду вещей, если меня интересует только грязный (а не «редактирование»), я могу обойтись без какого-либо кода. –  Benjol 13.03.2013 в 11:09
Показать остальные комментарии