简体   繁体   English

c#将类属性标记为脏

[英]c# marking class property as dirty

The following is a simple example of an enum which defines the state of an object and a class which shows the implementation of this enum.下面是一个简单的枚举示例,它定义了一个对象的状态和一个显示该枚举实现的类。

public enum StatusEnum
{
    Clean = 0,
    Dirty = 1,
    New = 2,
    Deleted = 3,
    Purged = 4
}


public class Example_Class
{
    private StatusEnum _Status = StatusEnum.New;

    private long _ID;
    private string _Name;

    public StatusEnum Status
    {
        get { return _Status; }
        set { _Status = value; }
    }

    public long ID
    {
        get { return _ID; }
        set { _ID = value; }
    }

    public string Name
    {
        get { return _Name; }
        set { _Name = value; }
    }
}

when populating the class object with data from the database, we set the enum value to "clean".当用数据库中的数据填充类对象时,我们将枚举值设置为“clean”。 with the goal of keeping most of the logic out of the presentation layer, how can we set the enum value to "dirty" when a property is changed.为了将大部分逻辑保留在表示层之外,我们如何在更改属性时将枚举值设置为“脏”。

i was thinking something along the lines of;我在想一些事情;

public string Name
{
    get { return _Name; }
    set 
    {
        if (value != _Name)
        {
               _Name = value; 
           _Status = StatusEnum.Dirty;
        }
    }   
}

in the setter of each property of the class.在类的每个属性的 setter 中。

does this sound like a good idea, does anyone have any better ideas on how the dirty flag can be assigned without doing so in the presentation layer.这听起来是个好主意吗,有没有人对如何分配脏标志有更好的想法,而无需在表示层中这样做。

When you really do want a dirty flag at the class level (or, for that matter, notifications) - you can use tricks like below to minimise the clutter in your properties (here showing both IsDirty and PropertyChanged , just for fun).当您确实想要在类级别(或者就此而言,通知)的脏标志时 - 您可以使用如下技巧来最小化您的属性中的混乱(这里同时显示IsDirtyPropertyChanged ,只是为了好玩)。

Obviously it is a trivial matter to use the enum approach (the only reason I didn't was to keep the example simple):显然,使用 enum 方法是一件小事(我没有这样做的唯一原因是保持示例简单):

class SomeType : INotifyPropertyChanged {
    private int foo;
    public int Foo {
        get { return foo; }
        set { SetField(ref foo, value, "Foo"); }
    }

    private string bar;
    public string Bar {
        get { return bar; }
        set { SetField(ref bar, value, "Bar"); }
    }

    public bool IsDirty { get; private set; }
    public event PropertyChangedEventHandler PropertyChanged;
    protected void SetField<T>(ref T field, T value, string propertyName) {
        if (!EqualityComparer<T>.Default.Equals(field, value)) {
            field = value;
            IsDirty = true;
            OnPropertyChanged(propertyName);
        }
    }
    protected virtual void OnPropertyChanged(string propertyName) {
        var handler = PropertyChanged;
        if (handler != null) {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

You might also choose to push some of that into an abstract base class, but that is a separate discussion您也可以选择将其中的一些推送到抽象基类中,但这是一个单独的讨论

One option is to change it on write;一种选择是在写入时更改它; another is to keep a copy of all the original values and compute the dirtiness when anyone asks for it.另一种方法是保留所有原始值的副本,并在有人要求时计算脏度。 That has the added benefit that you can tell exactly which fields have changed (and in what way) which means you can issue minimal update statements and make merge conflict resolution slightly easier.这有一个额外的好处,即您可以准确地知道哪些字段已更改(以及以何种方式更改),这意味着您可以发出最少的更新语句并使合并冲突的解决更容易一些。

You also get to put all the dirtiness-checking in one place, so it doesn't pollute the rest of your code.你还可以将所有的脏检查放在一个地方,这样它就不会污染你的代码的其余部分。

I'm not saying it's perfect, but it's an option worth considering.我并不是说它是完美的,但这是一个值得考虑的选择。

If you want to implement it in this way, and you want to reduce the amount of code, you might consider applying Aspect Oriented Programming.如果你想以这种方式实现它,并且你想减少代码量,你可以考虑应用面向切面编程。

You can for instance use a compile-time weaver like PostSharp , and create an 'aspect' that can be applied to properties.例如,您可以使用像PostSharp这样的编译时编织器,并创建一个可以应用于属性的“方面”。 This aspect then makes sure that your dirty flag is set when appropriate.这方面然后确保您的脏标志在适当的时候设置。

The aspect can look like this:该方面可能如下所示:

[Serializable]
[AttributeUsage(AttributeTargets.Property)]
public class ChangeTrackingAttribute : OnMethodInvocationAspect
{
    public override void OnInvocation( MethodInvocationEventArgs e )
    {
        if( e.Delegate.Method.ReturnParameter.ParameterType == typeof(void) )
        {
              // we're in the setter
              IChangeTrackable target = e.Delegate.Target as IChangeTrackable;

              // Implement some logic to retrieve the current value of 
              // the property
              if( currentValue != e.GetArgumentArray()[0] )
              {
                  target.Status = Status.Dirty;
              }
              base.OnInvocation (e);
        } 
    }  
} 

Offcourse, this means that the classes for which you want to implement ChangeTracking, should implement the IChangeTrackable interface (custom interface), which has at least the 'Status' property.当然,这意味着您要为其实现 ChangeTracking 的类应该实现IChangeTrackable接口(自定义接口),该接口至少具有“状态”属性。

You can also create a custom attribute ChangeTrackingProperty , and make sure that the aspect that has been created above, is only applied to properties that are decorated with this ChangeTrackingProperty attribute.您还可以创建自定义属性ChangeTrackingProperty ,并确保上面创建的方面仅应用于使用此ChangeTrackingProperty属性ChangeTrackingProperty属性。

For instance:例如:

public class Customer : IChangeTrackable
{
    public DirtyState Status
    {
        get; set;
    }

    [ChangeTrackingProperty]
    public string Name
    { get; set; }
}

This is a little bit how I see it.这是我的一点看法。 You can even make sure that PostSharp checks at compile-time whether classes that have properties that are decorated with the ChangeTrackingProperty attribute, implement the IChangeTrackable interface.您甚至可以确保 PostSharp 在编译时检查具有使用 ChangeTrackingProperty 属性修饰的属性的类是否实现了 IChangeTrackable 接口。

This method is based on a set of different concepts provided in this thread.此方法基于此线程中提供的一组不同概念。 I thought i'd put it out there for anyone that is looking for a way to do this cleanly and efficiently, as i was myself.我想我会把它发布给任何正在寻找一种干净有效的方法的人,就像我自己一样。

The key of this hybrid concept is that:这种混合概念的关键在于:

  1. You don't want to duplicate the data to avoid bloating and resource hogging;您不想复制数据以避免膨胀和资源占用;
  2. You want to know when the object's properties have changed from a given original/clean state;您想知道对象的属性何时从给定的原始/干净状态发生变化;
  3. You want to have the IsDirty flag be both accurate, and require little processing time/power to return the value;您希望 IsDirty 标志既准确又需要很少的处理时间/功率来返回值; and
  4. You want to be able to tell the object when to consider itself clean again.您希望能够告诉对象何时再次认为自己是干净的。 This is especially useful when building/working within the UI.这在 UI 中构建/工作时特别有用。

Given those requirements, this is what i came up with, and it seems to be working perfectly for me, and has become very useful when working against UIs and capturing user changes accurately.鉴于这些要求,这就是我想出的,它似乎对我来说很完美,并且在针对 UI 工作和准确捕获用户更改时变得非常有用。 I have also posted an "How to use" below to show you how I use this in the UI.我还在下面发布了“如何使用”来向您展示我如何在 UI 中使用它。

The Object物体

public class MySmartObject
{
    public string Name { get; set; }
    public int Number { get; set; }
    private int clean_hashcode { get; set; }
    public bool IsDirty { get { return !(this.clean_hashcode == this.GetHashCode()); } }

    public MySmartObject()
    {
        this.Name = "";
        this.Number = -1;
        MakeMeClean();

    }

    public MySmartObject(string name, int number)
    {
        this.Name = name;
        this.Number = number;
        MakeMeClean();
    }

    public void MakeMeClean()
    {
        this.clean_hashcode = this.Name.GetHashCode() ^ this.Number.GetHashCode();
    }

    public override int GetHashCode()
    {
        return this.Name.GetHashCode() ^ this.Number.GetHashCode();
    }
}

It's simple enough and addresses all of our requirements:它足够简单,可以满足我们的所有要求:

  1. The data is NOT duplicated for the dirty check...脏检查的数据不会重复......
  2. This takes into account all property changes scenarios (see scenarios below)...这考虑了所有属性更改方案(请参阅下面的方案)...
  3. When you call the IsDirty property, a very simple and small Equals operation is performed and it is fully customizable via the GetHashCode override...当您调用 IsDirty 属性时,将执行一个非常简单和小的 Equals 操作,并且它可以通过 GetHashCode 覆盖完全自定义...
  4. By calling the MakeMeClean method, you now have a clean object again!通过调用 MakeMeClean 方法,您现在又拥有了一个干净的对象!

Of course you can adapt this to encompass a bunch of different states... it's really up to you.当然,您可以将其调整为包含一系列不同的状态……这完全取决于您。 This example only shows how to have a proper IsDirty flag operation.此示例仅显示如何进行正确的 IsDirty 标志操作。

Scenarios场景
Let's go over some scenarios for this and see what comes back:让我们回顾一些场景,看看会返回什么:

  • Scenario 1场景一
    New object is created using empty constructor,使用空构造函数创建新对象,
    Property Name changes from "" to "James",属性名称从“”更改为“James”,
    call to IsDirty returns True!调用 IsDirty 返回 True! Accurate.准确的。

  • Scenario 2场景二
    New object is created using paramters of "John" and 12345,使用“John”和 12345 的参数创建新对象,
    Property Name changes from "John" to "James",属性名称从“John”更改为“James”,
    Property Name changes back from "James" to "John",属性名称从“James”变回“John”,
    Call to IsDirty returns False.调用 IsDirty 返回 False。 Accurate, and we didn't have to duplicate the data to do it either!准确,我们也不必复制数据来做到这一点!

How to use, a WinForms UI example如何使用,一个 WinForms UI 示例
This is only an example, you can use this in many different ways from a UI.这只是一个示例,您可以通过 UI 以多种不同方式使用它。

Let's say you have a two forms ([A] and [B]).假设您有两种形式([A] 和 [B])。

The first([A]) is your main form, and the second([B]) is a form that allows the user to change the values within the MySmartObject.第一个([A]) 是您的主窗体,第二个([B]) 是允许用户更改MySmartObject 中的值的窗体。

Both the [A] and the [B] form have the following property declared: [A] 和 [B] 形式都声明了以下属性:

public MySmartObject UserKey { get; set; }

When the user clicks a button on the [A] form, an instance of the [B] form is created, its property is set and it is displayed as a dialog.当用户单击 [A] 窗体上的按钮时,会创建 [B] 窗体的实例,设置其属性并显示为对话框。

After form [B] returns, the [A] form updates its property based on the [B] form's IsDirty check.表单 [B] 返回后,[A] 表单会根据 [B] 表单的 IsDirty 检查更新其属性。 Like this:像这样:

private void btn_Expand_Click(object sender, EventArgs e)
{
    SmartForm form = new SmartForm();
    form.UserKey = this.UserKey;
    if(form.ShowDialog() == DialogResult.OK && form.UserKey.IsDirty)
    {
        this.UserKey = form.UserKey;
        //now that we have saved the "new" version, mark it as clean!
        this.UserKey.MakeMeClean();
    }
}

Also, in [B], when it is closing, you can check and prompt the user if they are closing the form with unsaved changes in it, like so:此外,在 [B] 中,当它关闭时,您可以检查并提示用户是否正在关闭其中包含未保存更改的表单,如下所示:

    private void BForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        //If the user is closing the form via another means than the OK button, or the Cancel button (e.g.: Top-Right-X, Alt+F4, etc).
        if (this.DialogResult != DialogResult.OK && this.DialogResult != DialogResult.Ignore)
        {
            //check if dirty first... 
            if (this.UserKey.IsDirty)
            {
                if (MessageBox.Show("You have unsaved changes. Close and lose changes?", "Unsaved Changes", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
                    e.Cancel = true;
            }

        }

    }

As you can see from the examples above, this can be a very useful thing to have since it really streamlines the UI.正如您从上面的示例中看到的那样,这可能是一件非常有用的事情,因为它确实简化了 UI。

Caveats注意事项

  • Every time you implement this, you have to customize it to the object you're using.每次实现这一点时,您都​​必须根据您正在使用的对象对其进行自定义。 Eg: there's no "easy" generic way of doing this without using reflection... and if you use reflection, you lose efficiency, especially in large and complex objects.例如:在不使用反射的情况下,没有“简单”的通用方法可以做到这一点……如果使用反射,则会降低效率,尤其是在大型复杂对象中。

Hopefully this helps someone.希望这有助于某人。

Take a look at PostSharp ( http://www.postsharp.org/ ).看看 PostSharp ( http://www.postsharp.org/ )。 You can easily create a Attribute which marks it as dirty you can add the attrubute to each property that needs it and it keeps all your code in one place.您可以轻松创建一个将其标记为脏的属性,您可以将属性添加到需要它的每个属性中,并将所有代码保存在一个地方。

Roughly speaking Create an interface which has your status in make the class implement it.粗略地说,创建一个接口,该接口在使类实现它时具有您的地位。 Create an attribute which can be applied on properties and cast to your interface in order to set the value when something changes one of the marked properties.创建一个可以应用于属性并转换到您的界面的属性,以便在某个标记的属性发生变化时设置该值。

Here is how i do it.这是我如何做到的。

In cases where i do not need to test for specific fields being dirty, I have an abstract class:在我不需要测试特定字段是否脏的情况下,我有一个抽象类:

public abstract class SmartWrap : ISmartWrap
{
    private int orig_hashcode { get; set; }
    private bool _isInterimDirty;

    public bool IsDirty
    {
        get { return !(this.orig_hashcode == this.GetClassHashCode()); }
        set
        {
            if (value)
                this.orig_hashcode = this.orig_hashcode ^ 108.GetHashCode();
            else
                MakeClean();
        }
    }

    public void MakeClean()
    {
        this.orig_hashcode = GetClassHashCode();
        this._isInterimDirty = false;
    }

    // must be overridden to return combined hashcodes of fields testing for
    // example Field1.GetHashCode() ^ Field2.GetHashCode() 
    protected abstract int GetClassHashCode();

    public bool IsInterimDirty
    {
        get { return _isInterimDirty; }
    }

    public void SetIterimDirtyState()
    {
        _isInterimDirty = this.IsDirty;
    }

    public void MakeCleanIfInterimClean()
    {
        if (!IsInterimDirty)
            MakeClean();
    }

    /// <summary>
    /// Must be overridden with whatever valid tests are needed to make sure required field values are present.
    /// </summary>
    public abstract bool IsValid { get; }
}

} }

As well as an interface以及一个界面

public interface ISmartWrap
{
    bool IsDirty { get; set; }
    void MakeClean();
    bool IsInterimDirty { get;  }
    void SetIterimDirtyState();
    void MakeCleanIfInterimClean();
}

This allows me to do partial saves, and preserve the IsDirty state if there is other details to save.这允许我进行部分保存,并在有其他细节要保存时保留 IsDirty 状态。 Not perfect, but covers a lot of ground.不完美,但涵盖了很多领域。

Example of usage with interim IsDirty State (Error wrapping and validation removed for clarity):使用临时 IsDirty 状态的示例(为清楚起见,已删除错误包装和验证):

            area.SetIterimDirtyState();

            if (!UpdateClaimAndStatus(area))
                return false;

            area.MakeCleanIfInterimClean();

            return true;

This is good for most scenarios, however for some classes i want to test for each field with a backing field of original data, and either return a list of changes or at least an enum of fields changed.这适用于大多数场景,但是对于某些类,我想用原始数据的支持字段测试每个字段,并返回更改列表或至少更改字段的枚举。 With an enum of fields changed i can then push that up through a message chain for selective update of fields in remote caches.更改字段枚举后,我可以通过消息链将其向上推送,以选择性更新远程缓存中的字段。

You could also think about boxing your variables, which comes at a performance cost, but also has its merits.您还可以考虑将变量装箱,这会以性能为代价,但也有其优点。 It is pretty consise and you cannot accidentally change a value without setting your dirty status.它非常简洁,如果不设置脏状态,您就不会意外更改值。

public class Variable<T>
{
    private T _value;
    private readonly Action<T> _onValueChangedCallback;

    public Variable(Action<T> onValueChangedCallback, T value = default)
    {
        _value = value;
        _onValueChangedCallback = onValueChangedCallback;
    }

    public void SetValue(T value)
    {
        if (!EqualityComparer<T>.Default.Equals(_value, value))
        {
            _value = value;
            _onValueChangedCallback?.Invoke(value);
        }
    }

    public T GetValue()
    {
        return _value;
    }

    public static implicit operator T(Variable<T> variable)
    {
        return variable.GetValue();
    }
}

and then hook in a callback that marks your class as dirty.然后挂钩将您的类标记为脏的回调。

public class Example_Class
{
    private StatusEnum _Status = StatusEnum.New;

    private Variable<long> _ID;
    private Variable<string> _Name;

    public StatusEnum Status
    {
        get { return _Status; }
        set { _Status = value; }
    }

    public long ID => _ID;
    public string Name => _Name;

    public Example_Class()
    {
         _ID = new Variable<long>(l => Status = StatusEnum.Dirty);
         _Name = new Variable<string>(s => Status = StatusEnum.Dirty);
    }
}

Your approach is basically how I would do it.你的方法基本上就是我会怎么做。 I would just remove the setter for the Status property:我只想删除 Status 属性的设置器:

public StatusEnum Status
{
    get { return _Status; }
    // set { _Status = value; }
}

and instead add a function而是添加一个函数

public SetStatusClean()
{
    _Status = StatusEnum.Clean;
}

As well as SetStatusDeleted() and SetStatusPurged() , because I find it better indicates the intention.以及SetStatusDeleted()SetStatusPurged() ,因为我发现它更好地表明了意图。

Edit编辑

Having read the answer by Jon Skeet , I need to reconsider my approach ;-) For simple objects I would stick with my way, but if it gets more complex, his proposal would lead to much better organised code.阅读Jon Skeet答案后,我需要重新考虑我的方法 ;-) 对于简单的对象,我会坚持我的方式,但如果它变得更复杂,他的提议将导致更好的组织代码。

If your Example_Class is lightweight, consider storing the original state and then comparing the current state to the original in order to determine the changes.如果您的 Example_Class 是轻量级的,请考虑存储原始状态,然后将当前状态与原始状态进行比较以确定更改。 If not your approach is the best because stroing the original state consumes a lot of system resources in this case.如果不是,您的方法是最好的,因为在这种情况下,stroing 原始状态会消耗大量系统资源。

Apart from the advice of 'consider making your type immutable', here's something I wrote up (and got Jon and Marc to teach me something along the way)除了“考虑让你的类型不可变”的建议之外,这是我写的一些东西(并且让 Jon 和 Marc 一路教我一些东西)

public class Example_Class
{    // snip
     // all properties are public get and private set

     private Dictionary<string, Delegate> m_PropertySetterMap;

     public Example_Class()
     {
        m_PropertySetterMap = new Dictionary<string, Delegate>();
        InitializeSettableProperties();
     }
     public Example_Class(long id, string name):this()
     {   this.ID = id;    this.Name = name;   }

     private void InitializeSettableProperties()
     {
        AddToPropertyMap<long>("ID",  value => { this.ID = value; });
        AddToPropertyMap<string>("Name", value => { this.Name = value; }); 
     }
     // jump thru a hoop because it won't let me cast an anonymous method to an Action<T>/Delegate
     private void AddToPropertyMap<T>(string sPropertyName, Action<T> setterAction)
     {   m_PropertySetterMap.Add(sPropertyName, setterAction);            }

     public void SetProperty<T>(string propertyName, T value)
     {
        (m_PropertySetterMap[propertyName] as Action<T>).Invoke(value);
        this.Status = StatusEnum.Dirty;
     }
  }

You get the idea.. possible improvements: Use constants for PropertyNames & check if property has really changed.你明白了.. 可能的改进:为 PropertyNames 使用常量并检查属性是否真的改变了。 One drawback here is that这里的一个缺点是

obj.SetProperty("ID", 700);         // will blow up int instead of long
obj.SetProperty<long>("ID", 700);   // be explicit or use 700L

Another method is to override the GetHashCode() method to somthing like this:另一种方法是将 GetHashCode() 方法覆盖为如下所示:

public override int GetHashCode() // or call it GetChangeHash or somthing if you dont want to override the GetHashCode function...
{
    var sb = new System.Text.StringBuilder();

    sb.Append(_dateOfBirth);
    sb.Append(_marital);
    sb.Append(_gender);
    sb.Append(_notes);
    sb.Append(_firstName);
    sb.Append(_lastName);  

    return sb.ToString.GetHashCode();
}

Once loaded from the database, get the hash code of the object.从数据库加载后,获取对象的哈希码。 Then just before you save check if the current hash code is equal to the previous hash code.然后在保存之前检查当前哈希码是否等于之前的哈希码。 if they are the same, don't save.如果它们相同,则不保存。

Edit:编辑:

As people have pointed out this causes the hash code to change - as i use Guids to identify my objects, i don't mind if the hashcode changes.正如人们所指出的那样,这会导致哈希码发生变化——当我使用 Guids 来识别我的对象时,我不介意哈希码是否发生变化。

Edit2:编辑2:

Since people are adverse to changing the hash code, instead of overriding the GetHashCode method, just call the method something else.由于人们反对更改哈希码,因此不要覆盖 GetHashCode 方法,只需调用其他方法即可。 The point is detecting a change not whether i use guids or hashcodes for object identification.关键是检测变化而不是我是否使用 guid 或哈希码进行对象识别。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM