WPF MVVM - 如何检测视图是否“Dirty”

23

我目前需要实现一个功能,即在视图(View)中有任何字段被更改/更新时通知我的应用程序用户。

例如,如果用户在视图中更改了日期字段,然后试图关闭视图,则应用程序将显示一条消息,询问用户是继续并丢失更改还是取消以便他们可以点击保存按钮。

问题是:我如何检测到视图中的任何数据字段是否已更改?

希望这样说得清楚,先谢谢您,敬礼。


17
在MVVM中,你会询问Model或者ViewModel是否有变更,而不是View。 - H H
3个回答

37

一种方法是利用IChangeTrackingINotifyPropertyChanged接口。

如果您创建一个抽象基类,使你的视图模型从中继承(ViewModelBase),它实现了IChangeTrackingINotifyPropertyChanged接口,您可以让视图模型基类附加到属性更改通知(实际上是通知视图模型已被修改),并将IsChanged属性设置为true,以指示视图模型为“脏”状态。

使用此方法,您依靠数据绑定通过属性更改通知来跟踪更改,并在进行任何提交后重置更改跟踪。

在您所描述的情况下,您可以处理您视图的UnloadedClosing事件来检查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
}

我正在寻找在MVVM和WPF应用程序中使用的这个东西,但是当我尝试使用它时,IsChanged总是为真。有什么想法吗? - Juan
这种方法无法正确检测 TextBox 在编辑期间的脏状态。这些信息被“锁定在”WPF Binding类中,既不能被View也不能被ViewModel访问。虽然可以实现自定义绑定,但这需要非常艰苦的工作,并且需要使用非标准的XAML才能正常工作。很难相信这是2013年了。 - Jack
1
嘿,我在使用这种方法时遇到了问题,因为当我从数据库加载实体时,它们会自动变成IsChanged = true,因为属性被设置了,而在访问之前。有什么想法告诉它不要在值来自数据库时设置IsChanged吗? - adminSoftDK
@adminSoftDK,您可以通过构造函数重载注入您的模型,并直接设置私有变量,而无需使用属性。或者,您可以创建一个名为setModel(MyModel m)的方法,该方法与构造函数执行相同的操作。 - WiiMaxx
2
@adminSoftDK 在初始加载后只需调用AcceptChanges函数即可。如果有一个模型列表,您可以轻松使用Linq DataOrViewModel.ForEach(x => x.AcceptChanges())来完成它。 - Swifty

13

MVVM中,视图(View)与视图模型(View-Model)进行绑定,而视图模型又与模型(Model)进行绑定。

视图不能是脏的(dirty),因为其更改会立即反映到视图模型(View-Model)上。

如果希望仅在“确定”或“接受”时将更改应用于模型(Model),
请将视图(View)绑定到一个不直接应用更改到模型(Model)的视图模型(View-Model)上,
直到执行ApplyCommand或AcceptCommand(由你定义和实现的命令)。

(视图(View)绑定的命令是由视图模型(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

2
在WPF中,TextBox控件默认的UpdateSourceTrigger = LostFocus。这意味着View可能是脏的,而ViewModel不是。LostFocus的原因是否则部分编辑(例如DateTime)将引发验证错误。这是WPF中的设计缺陷。强大的应用程序必须考虑IsDirty = ViewModel.IsDirty || View.IsDirty... - Jack
1
那么这个评论框不应该显示还剩下566个字符吗?即使用户更改了文本框中的文本,表单上的保存和取消按钮也应该保持禁用状态吧?得了吧! - Jack
1
@DannyVarod,AHA!神奇的咒语是“UpdateSourceTrigger = PropertyChanged”……谢谢!从事情的样子来看,如果我只关心脏数据(而不是“编辑”),那么我可以不需要任何代码支持。 - Benjol
@DannyVarod 如果仅设置 UpdateSourceTrigger=PropertyChanged 是否会更容易,因为如果用户需要按保存按钮,则不需要 IsTextBoxFocused。 如果我们让 UpdateSourceTrigger 保持在 LostFocus 上并且通过输入而无需按钮进行保存,同样的事情也是正确的,因为如果他点击“x”,则将执行 LostFocus 并且我们将保存 ... - WiiMaxx
@DannyVarod 到 a. 所以你不需要 IsTextBoxFocused,b. 这取决于你的 IsDirty,如果你这样做 public bool IsDirty { get { return mytemp != model.mytemp;}} 你可以始终使用 changed 进行验证。c. 像我说的,如果你有一个保存按钮,你可以使用 UpdateSourceTrigger=PropertyChanged,如果你没有,就使用 LostFocus,因为无论你在改变后做什么,LostFocus 都会首先执行。 - WiiMaxx
显示剩余5条评论

-1
想法:检查实体状态:
问题是它指的是整个视图,因此当在任何编辑之前选择新参与者(刷新表单)时,该值也为“已修改”。保存后,如果没有其他更改且我们不切换参与者,则该值为“未更改”。

 

 


目前你的回答不够清晰。请编辑并添加更多细节,以帮助其他人理解它如何回答所提出的问题。你可以在帮助中心找到有关如何撰写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接