近乎自动的 INotifyPropertyChanged、自动的 IsDirty 以及自动的 ChangeTracking






4.42/5 (12投票s)
一种易于重构的 INotifyPropertyChanged 实现,无需使用反射。
引言
WPF… 感觉我需要为我尝试做的一切都构建变通方法。在 WPF 之前,我很少需要 INotifyPropertyChanged
。
但既然我们需要它,为什么不充分利用它呢。由于我想要一个所有未来对象的基础类,在所有未来的项目中,我想要一个非常简洁的东西。
- 以最佳方式实现
INotifyPropertyChanged
(易于重构且性能最佳) - 自动设置
IsDirty
标志 - 自动更改跟踪,以便记录已更改的内容(就像在 SharePoint 中获取警报时一样,我喜欢那样!)
- 更改跟踪也应导出为文本,以便我可以将其写入 SQL
当然,我不用告诉你,代码必须始终确保最少的工作量。而且,它应该是易于重构的等等… 我想要完美的代码。
代码
/// <summary>
/// This object automatically implements notify property changed
/// </summary>
public class NotifyPropertyChangeObject : INotifyPropertyChanged
{
/// <summary>
/// Track changes or not.
/// If we're working with DTOs and we fill up the DTO
/// in the DAL we should not be tracking changes.
/// </summary>
private bool trackChanges = false;
/// <summary>
/// Changes to the object
/// </summary>
public Dictionary<string, object> Changes { get; private set; }
/// <summary>
/// Is the object dirty or not?
/// </summary>
public bool IsDirty
{
get { return Changes.Count > 0; }
set { ; }
}
/// <summary>
/// Event required for INotifyPropertyChanged
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// This constructor will initialize the change tracking
/// </summary>
public NotifyPropertyChangeObject()
{
// Change tracking default
trackChanges = true;
// New change tracking dictionary
Changes = new Dictionary<string, object>();
}
/// <summary>
/// Reset the object to non-dirty
/// </summary>
public void Reset()
{
Changes.Clear();
}
/// <summary>
/// Start tracking changes
/// </summary>
public void StartTracking()
{
trackChanges = true;
}
/// <summary>
/// Stop tracking changes
/// </summary>
public void StopTracking()
{
trackChanges = false;
}
/// <summary>
/// Change the property if required and throw event
/// </summary>
/// <param name="variable"></param>
/// <param name="property"></param>
/// <param name="value"></param>
public void ApplyPropertyChange<T, F>(ref F field,
Expression<Func<T, object>> property, F value)
{
// Only do this if the value changes
if (field == null || !field.Equals(value))
{
// Get the property
var propertyExpression = GetMemberExpression(property);
if (propertyExpression == null)
throw new InvalidOperationException("You must specify a property");
// Property name
string propertyName = propertyExpression.Member.Name;
// Set the value
field = value;
// If change tracking is enabled, we can track the changes...
if (trackChanges)
{
// Change tracking
Changes[propertyName] = value;
// Notify change
NotifyPropertyChanged(propertyName);
}
}
}
/// <summary>
/// Get member expression
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression"></param>
/// <returns></returns>
public MemberExpression GetMemberExpression<T>(Expression<Func<T,
object>> expression)
{
// Default expression
MemberExpression memberExpression = null;
// Convert
if (expression.Body.NodeType == ExpressionType.Convert)
{
var body = (UnaryExpression)expression.Body;
memberExpression = body.Operand as MemberExpression;
}
// Member access
else if (expression.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpression = expression.Body as MemberExpression;
}
// Not a member access
if (memberExpression == null)
throw new ArgumentException("Not a member access",
"expression");
// Return the member expression
return memberExpression;
}
/// <summary>
/// The property has changed
/// </summary>
/// <param name="propertyName"></param>
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Convert the changes to an XML string
/// </summary>
/// <returns></returns>
public string ChangesToXml()
{
// Prepare base objects
XDeclaration declaration = new XDeclaration("1.0",
Encoding.UTF8.HeaderName, String.Empty);
XElement root = new XElement("Changes");
// Create document
XDocument document = new XDocument(declaration, root);
// Add changes to the document
// TODO: If it's an object, maybe do some other things
foreach (KeyValuePair<string, object> change in Changes)
root.Add(new XElement(change.Key, change.Value));
// Get the XML
return document.Document.ToString();
}
}
使用代码
public class Person : NotifyPropertyChangeObject
{
private string firstName;
private string lastName;
private int age;
/// <summary>
/// FirstName
/// </summary>
[DefaultValue("")]
public string FirstName
{
get { return firstName; }
set { ApplyPropertyChange<Person,
string>(ref firstName, o => o.FirstName, value); }
}
/// <summary>
/// LastName
/// </summary>
[DefaultValue("")]
public string LastName
{
get { return lastName; }
set { ApplyPropertyChange<Person,
string>(ref lastName, o => o.LastName, value); }
}
/// <summary>
/// Age
/// </summary>
[DefaultValue(0)]
public int Age
{
get { return age; }
set { ApplyPropertyChange<Person, int>(ref age, o => o.Age, value); }
}
}
就这样了…
关于代码的一些说明
- 我们的类
Person
继承自NotifyPropertyChangeObject
。 - 当我们更改属性的值时,神奇的事情发生了。
- 我们将 私有变量、属性(易于重构)和 新值 传递过去。
- 之后,我们检查“新”值是否与旧值匹配。如果是这样,则不应发生任何事情。
- 如果值不匹配,我们想要更改该值并通知发生了更改。
- 同时,我们还将该更改添加到 Changes 字典中。
- 可以安全地假设,如果字典包含更改,则对象是脏的。
最后,还有 Reset
、StartTracking
和 StopTracking
方法。这些与更改跟踪有关。
如果我在 DAL 中填充 DTO,我不想将其标记为脏。所以在开始之前,我调用 StopTracking
,完成时,我调用 StartTracking
。
稍后,如果我保存对象,我希望它是干净的(不是脏的),所以我调用 Reset
方法。
最后,您可以调用 ChangesToXml
以获取所有更改的字符串表示形式。然后可以将其写入 SQL 或其他…
示例应用程序
在文章的顶部,您可以下载一个可用的项目。我还添加了一个示例,用于无法继承 NotifyPropertyChangeObject
的情况,例如在使用 Entity Framework 或 LINQ to SQL 时…