我有一个包含5个属性的类。
如果任何值被分配给这些字段中的任何一个,则另一个值(例如IsDIrty)将变为true。
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;}
}
答案 0 :(得分:43)
要做到这一点,你不能真正使用自动吸气剂和放大器。 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));
}
}
答案 1 :(得分:14)
您可以实现现在包含在.NET Standard 2.0中的IChangeTracking
或IRevertibleChangeTracking
接口。
实施如下:
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;
}
}
答案 2 :(得分:7)
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
,并在每个设置器中调用NotifyPropertyChanged
。
答案 3 :(得分:4)
在所有设置器中将IsDirty
设置为true。
您也可以考虑将IsDirty
的setter设为private(如果您的子类具有其他属性,则可以使用protected)。否则你可能会在类之外使用代码来否定其确定肮脏的内部机制。
答案 4 :(得分:3)
如果有大量此类类,都具有相同的模式,并且您经常需要更新其定义,请考虑使用代码生成自动吐出所有类的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之外,它与此类似。
答案 5 :(得分:1)
仔细考虑需要对象跟踪的根本目的?假设它是否像其他对象必须基于另一个对象的状态做某事,那么考虑实现observer design pattern。
如果它很小,可以考虑实现INotifyPropertyChanged接口。
答案 6 :(得分:1)
Dan's和Andy Shellam的答案都是我的最爱。
无论如何,如果你想改变你的TRACK,就像在日志中那样,你可能会考虑使用一个Dictionary,它会在收到通知更改时添加你所有的属性更改。因此,您可以使用唯一键将更改添加到词典中,并跟踪您的更改。然后,如果你希望Roolback在内存中你的对象的状态,你可以这样。
修改强> 以下是Bart de Smet用于跟踪整个LINQ到AD的房地产变化的信息。一旦将更改提交给AD,他就会清除词典。因此,当一个属性发生变化时,因为他实现了INotifyPropertyChanged接口,当一个属性实际发生变化时,他使用了一个Dictionary&gt;如下:
/// <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这样做,这在某种程度上是一种考虑的做法。
答案 7 :(得分:1)
我知道这是一个旧线程,但我认为Enumerations不适用于Binary Worrier的解决方案。你将得到一个设计时错误信息,即enum属性Type“不能在泛型类型或方法中用作类型参数'T'”...“SetProperty(string,ref T,T)'。没有装箱转换......“。
我引用了这个stackoverflow帖子来解决枚举问题:C# boxing enum error with generics
答案 8 :(得分:0)
这是Rocky Lhokta BusinessBase框架中的CLSA课程中的内容,所以你总是可以去看看它是如何完成的......
答案 9 :(得分:0)
要支持枚举,请使用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
答案 10 :(得分:0)
我知道你问这个问题已经有一段时间了。如果您仍然有兴趣让您的课程干净简单而不需要从基类派生,我建议您使用PropertyChanged.Fody实施的IsChanged Flag
答案 11 :(得分:0)
可以通过多种方式跟踪变更的优缺点。这里有一些想法:
观察者模式
在.NET中,最常见的方法是实现INotifyPropertyChanged
,INotifyPropertyChangeing
和/或IObservable
(另请参见Introduction to Rx)。提示:最简单的方法是将ReactiveUI库中的ReactiveObject
用作基础对象。
使用此接口,您可以跟踪属性何时更改或更改。因此,这是对“实时”变化做出反应的最佳方法。
您还可以实现变更跟踪器,以跟踪更复杂情况下的所有情况。更改跟踪器可能在内部具有更改列表-列出所需的属性名称,值和时间戳。然后,您可以查询此列表以获取所需的信息。想一想EventSourcing模式。
序列化和差异
如果要查看对象是否已更改以及已更改的内容,可以序列化原始版本和当前版本。
此版本的一个版本是序列化为JSON并计算JSON Patch。您可以为此使用JsonPatchDocument<T>
类(另请参见JsonPatchDocument Class)。差异会告诉您发生了什么变化。 (另请参见this question)
手动
然后,还有一种具有多个属性以保持原始状态和当前状态的方法,并且可能是一个布尔值,它告诉您该字段是否已更改,但之后又更改回原始值。
这很容易实现,但是在处理更复杂的场景时可能不是最佳方法。
答案 12 :(得分:-1)
您可以使用 Roslyn 的 Source Generators,我找到了一个 roslyn-sdk github 示例。
我还创建了此示例的 cleaned version。 在我的版本中,生成器使用 .NET Standard 2.0,与 .NET Framework 4.7.2 兼容
您可以根据需要随意克隆和修改。