我有一个包含5个属性的类。
如果为这些字段中的任何字段分配了任何值,则另一个值(例如Is DIrty)将改变为真。
public class Class1
{
bool IsDIrty {get;set;}
string Prop1 {get;set;}
string Prop2 {get;set;}
string Prop3 {get;set;}
string Prop4 {get;set;}
string Prop5 {get;set;}
}
要做到这一点,你不能真正使用自动getter和setter,你需要在每个setter中设置IsDirty。
我通常有一个“setProperty”泛型方法,它接受ref参数,属性名称和新值。我在setter中调用它,允许我可以设置isDirty的单个点并提高Change通知事件,例如
protected bool SetProperty<T>(string name, ref T oldValue, T newValue) where T : System.IComparable<T>
{
if (oldValue == null || oldValue.CompareTo(newValue) != 0)
{
oldValue = newValue;
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name));
isDirty = true;
return true;
}
return false;
}
// For nullable types
protected void SetProperty<T>(string name, ref Nullable<T> oldValue, Nullable<T> newValue) where T : struct, System.IComparable<T>
{
if (oldValue.HasValue != newValue.HasValue || (newValue.HasValue && oldValue.Value.CompareTo(newValue.Value) != 0))
{
oldValue = newValue;
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name));
}
}
要支持枚举,请使用Binary Worrier的完美解决方案并添加以下代码。
我为自己添加了Enum支持(这很痛苦),我想这也很好。
protected void SetEnumProperty<TEnum>(string name, ref TEnum oldEnumValue, TEnum newEnumValue) where TEnum : struct, IComparable, IFormattable, IConvertible
{
if (!(typeof(TEnum).IsEnum)) {
throw new ArgumentException("TEnum must be an enumerated type");
}
if (oldEnumValue.CompareTo(newEnumValue) != 0) {
oldEnumValue = newEnumValue;
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
_isChanged = true;
}
}
并实施:
Public Property CustomerTyper As CustomerTypeEnum
Get
Return _customerType
End Get
Set(value As ActivityActionByEnum)
SetEnumProperty("CustomerType", _customerType, value)
End Set
End Property
我知道你问这个问题已经有一段时间了。如果你仍然有兴趣让你的课程干净简单,而不需要从基类派生,我建议使用PropertyChanged.Fody实现IsChanged Flag
您可以实现IChangeTracking
或IRevertibleChangeTracking
接口,现在包含在.NET Standard 2.0中。
实施如下:
IChangeTracking
:
class Entity : IChangeTracking
{
string _FirstName;
public string FirstName
{
get => _FirstName;
set
{
if (_FirstName != value)
{
_FirstName = value;
IsChanged = true;
}
}
}
string _LastName;
public string LastName
{
get => _LastName;
set
{
if (_LastName != value)
{
_LastName = value;
IsChanged = true;
}
}
}
public bool IsChanged { get; private set; }
public void AcceptChanges() => IsChanged = false;
}
IRevertibleChangeTracking
:
class Entity : IRevertibleChangeTracking
{
Dictionary<string, object> _Values = new Dictionary<string, object>();
string _FirstName;
public string FirstName
{
get => _FirstName;
set
{
if (_FirstName != value)
{
if (!_Values.ContainsKey(nameof(FirstName)))
_Values[nameof(FirstName)] = _FirstName;
_FirstName = value;
IsChanged = true;
}
}
}
string _LastName;
public string LastName
{
get => _LastName;
set
{
if (_LastName != value)
{
if (!_Values.ContainsKey(nameof(LastName)))
_Values[nameof(LastName)] = _LastName;
_LastName = value;
IsChanged = true;
}
}
}
public bool IsChanged { get; private set; }
public void RejectChanges()
{
foreach (var property in _Values)
GetType().GetRuntimeProperty(property.Key).SetValue(this, property.Value);
AcceptChanges();
}
public void AcceptChanges()
{
_Values.Clear();
IsChanged = false;
}
}
我最喜欢的另一个选项是使用更改跟踪库,例如TrackerDog,它为您生成所有样板代码,同时只需要提供POCO实体。
如果您不想手动实现所有属性,还有更多方法可以实现此目的。一种选择是使用编织库,例如Fody.PropertyChanged和Fody.PropertyChanging,并处理更改方法以缓存旧值并跟踪对象状态。另一种选择是将对象的图形存储为MD5或其他一些哈希值,并在任何更改时重置它,您可能会感到惊讶,但如果您不期望有太多变化,并且如果您只是按需请求它,它可以真正起作用快速。
这是一个示例实现(注意:需要Json.NET和Fody/PropertyChanged:
[AddINotifyPropertyChangedInterface]
class Entity : IChangeTracking
{
public string UserName { get; set; }
public string LastName { get; set; }
public bool IsChanged { get; private set; }
string hash;
string GetHash()
{
if (hash == null)
using (var md5 = MD5.Create())
using (var stream = new MemoryStream())
using (var writer = new StreamWriter(stream))
{
_JsonSerializer.Serialize(writer, this);
var hash = md5.ComputeHash(stream);
this.hash = Convert.ToBase64String(hash);
}
return hash;
}
string acceptedHash;
public void AcceptChanges() => acceptedHash = GetHash();
static readonly JsonSerializer _JsonSerializer = CreateSerializer();
static JsonSerializer CreateSerializer()
{
var serializer = new JsonSerializer();
serializer.Converters.Add(new EmptyStringConverter());
return serializer;
}
class EmptyStringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(string);
public override object ReadJson(JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
=> throw new NotSupportedException();
public override void WriteJson(JsonWriter writer,
object value,
JsonSerializer serializer)
{
if (value is string str && str.All(char.IsWhiteSpace))
value = null;
writer.WriteValue(value);
}
public override bool CanRead => false;
}
}
Dan的解决方案非常完美。
另一个选择是考虑你是否必须在多个类上执行此操作(或者您希望外部类“监听”属性的更改):
INotifyPropertyChanged
接口IsDirty
属性移动到抽象类Class1
和所有其他需要此功能的类来扩展您的抽象类PropertyChanged
事件,并将他们的名字传递给事件PropertyChanged
事件并在其触发时将IsDirty
设置为true最初创建抽象类有点工作,但它是一个更好的模型,用于监视数据更改,因为任何其他类在IsDirty
(或任何其他属性)更改时都可以看到。
我的基类如下所示:
public abstract class BaseModel : INotifyPropertyChanged
{
/// <summary>
/// Initializes a new instance of the BaseModel class.
/// </summary>
protected BaseModel()
{
}
/// <summary>
/// Fired when a property in this class changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Triggers the property changed event for a specific property.
/// </summary>
/// <param name="propertyName">The name of the property that has changed.</param>
public void NotifyPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
任何其他模型然后只是扩展BaseModel
,并在每个setter中调用NotifyPropertyChanged
。
在所有的二传手中将IsDirty
设置为true。
您也可以考虑将IsDirty
的setter设为private(如果您的子类具有其他属性,则可以保护它)。否则你可能会在类之外有代码,否定其内部机制来确定肮脏。
如果有大量这样的类,都具有相同的模式,并且您经常需要更新它们的定义,请考虑使用代码生成自动为所有类吐出C#源文件,这样您就没有了手动维护它们。代码生成器的输入只是一个简单的文本文件格式,您可以轻松解析,说明每个类所需的属性的名称和类型。
如果它们只有少数,或者定义在开发过程中很少发生变化,那么它就不值得付出努力,在这种情况下你也可以手工维护它们。
更新:
对于一个简单的例子来说,这可能是最重要的,但要弄清楚这很有趣!
在Visual Studio 2008中,如果您将一个名为CodeGen.tt
的文件添加到项目中然后将其粘贴到其中,您将拥有代码生成系统的功能:
<#@ template debug="false" hostspecific="true" language="C#v3.5" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#
// You "declare" your classes here, as in these examples:
var src = @"
Foo: string Prop1,
int Prop2;
Bar: string FirstName,
string LastName,
int Age;
";
// Parse the source text into a model of anonymous types
Func<string, bool> notBlank = str => str.Trim() != string.Empty;
var classes = src.Split(';').Where(notBlank).Select(c => c.Split(':'))
.Select(c => new
{
Name = c.First().Trim(),
Properties = c.Skip(1).First().Split(',').Select(p => p.Split(' ').Where(notBlank))
.Select(p => new { Type = p.First(), Name = p.Skip(1).First() })
});
#>
// Do not edit this file by hand! It is auto-generated.
namespace Generated
{
<# foreach (var cls in classes) {#> class <#= cls.Name #>
{
public bool IsDirty { get; private set; }
<# foreach (var prop in cls.Properties) { #>
private <#= prop.Type #> _storage<#= prop.Name #>;
public <#= prop.Type #> <#= prop.Name #>
{
get { return _storage<#= prop.Name #>; }
set
{
IsDirty = true;
_storage<#= prop.Name #> = value;
}
} <# } #>
}
<# } #>
}
有一个名为src
的简单字符串文字,您可以在其中以简单的格式声明所需的类:
Foo: string Prop1,
int Prop2;
Bar: string FirstName,
string LastName,
int Age;
因此,您可以轻松添加数百个类似的声明。每当您保存更改时,Visual Studio将执行模板并生成CodeGen.cs
作为输出,其中包含类的C#源,以及IsDirty
逻辑。
您可以通过更改最后一个部分来更改生成的模板,它在模型中循环并生成代码。如果您使用过ASP.NET,那么除了生成C#源而不是HTML之外,它与此类似。
仔细考虑需要对象跟踪的根本目的?假设它是否像其他对象必须基于另一个对象的状态做某事,那么考虑实现observer design pattern。
如果它的小东西考虑实现INotifyPropertyChanged接口。
我知道这是一个老线程,但我认为Enumerations不适用于Binary Worrier的解决方案。你将得到一个设计时错误信息,即enum属性Type“不能在泛型类型或方法中用作类型参数'T'”...“SetProperty(string,ref T,T)'。没有装箱转换......“。
我引用了这个stackoverflow帖子来解决枚举问题:C# boxing enum error with generics
Dan和Andy Shellam的答案都是我的最爱。
无论如何,如果你想保持TRACK你的变化,就像在日志中那样,你可能会考虑使用一个Dictionary,它会在通知你发生变化时添加你所有的属性变化。因此,您可以使用唯一键将更改添加到词典中,并跟踪您的更改。然后,如果你希望Roolback在内存中你的对象的状态,你可以这样。
编辑以下是Bart de Smet用于跟踪整个LINQ到AD的房产变化的信息。一旦将更改提交给AD,他就会清除词典。因此,当属性更改时,因为他实现了INotifyPropertyChanged接口,当属性实际更改时,他使用Dictionary>,如下所示:
/// <summary>
/// Update catalog; keeps track of update entity instances.
/// </summary>
private Dictionary<object, HashSet<string>> updates
= new Dictionary<object, HashSet<string>>();
public void UpdateNotification(object sender, PropertyChangedEventArgs e)
{
T source = (T)sender;
if (!updates.ContainsKey(source))
updates.Add(source, new HashSet<string>());
updates[source].Add(e.PropertyName);
}
所以,我想如果Bart de Smet这样做,这在某种程度上是一种考虑的做法。
这是在Rocky Lhokta的BusinessBase框架中的CLSA类中构建的,所以你总是可以去看看它是如何完成的......