我目前需要实现一个功能,即在视图(View)中有任何字段被更改/更新时通知我的应用程序用户。
例如,如果用户在视图中更改了日期字段,然后试图关闭视图,则应用程序将显示一条消息,询问用户是继续并丢失更改还是取消以便他们可以点击保存按钮。
问题是:我如何检测到视图中的任何数据字段是否已更改?
希望这样说得清楚,先谢谢您,敬礼。
一种方法是利用IChangeTracking
和INotifyPropertyChanged
接口。
如果您创建一个抽象基类,使你的视图模型从中继承(ViewModelBase
),它实现了IChangeTracking
和INotifyPropertyChanged
接口,您可以让视图模型基类附加到属性更改通知(实际上是通知视图模型已被修改),并将IsChanged
属性设置为true,以指示视图模型为“脏”状态。
使用此方法,您依靠数据绑定通过属性更改通知来跟踪更改,并在进行任何提交后重置更改跟踪。
在您所描述的情况下,您可以处理您视图的Unloaded
或Closing
事件来检查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
}
setModel(MyModel m)
的方法,该方法与构造函数执行相同的操作。 - WiiMaxxMVVM中,视图(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
UpdateSourceTrigger=PropertyChanged
是否会更容易,因为如果用户需要按保存按钮,则不需要 IsTextBoxFocused
。 如果我们让 UpdateSourceTrigger
保持在 LostFocus
上并且通过输入而无需按钮进行保存,同样的事情也是正确的,因为如果他点击“x”,则将执行 LostFocus
并且我们将保存 ... - WiiMaxxIsTextBoxFocused
,b. 这取决于你的 IsDirty,如果你这样做 public bool IsDirty { get { return mytemp != model.mytemp;}}
你可以始终使用 changed 进行验证。c. 像我说的,如果你有一个保存按钮,你可以使用 UpdateSourceTrigger=PropertyChanged
,如果你没有,就使用 LostFocus
,因为无论你在改变后做什么,LostFocus
都会首先执行。 - WiiMaxx