28

下面是一个简单的枚举示例,它定义了一个对象的状态和一个显示该枚举实现的类。

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; }
    }
}

当用数据库中的数据填充类对象时,我们将枚举值设置为“clean”。为了将大部分逻辑排除在表示层之外,我们如何在属性更改时将枚举值设置为“脏”。

我在想一些类似的事情;

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

在类的每个属性的设置器中。

这听起来像一个好主意吗,是否有人对如何分配脏标志有更好的想法,而无需在表示层中这样做。

4

11 回答 11

42

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).

Obviously it is a trivial matter to use the enum approach (the only reason I didn't was to keep the example simple):

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

于 2009-04-30T07:00:11.687 回答
26

一种选择是在写入时更改它;另一种是保留所有原始值的副本,并在有人要求时计算脏值。这有一个额外的好处,您可以准确地知道哪些字段已更改(以及以何种方式),这意味着您可以发出最少的更新语句并使合并冲突解决稍微容易一些。

您还可以将所有脏污检查放在一个地方,因此它不会污染您的其余代码。

我并不是说它是完美的,但它是一个值得考虑的选择。

于 2009-04-30T06:03:20.793 回答
17

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. 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.

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.

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.

于 2009-04-30T07:15:37.813 回答
4

此方法基于此线程中提供的一组不同概念。我想我会把它放在那里给任何正在寻找一种干净有效的方法的人,就像我自己一样。

这种混合概念的关键在于:

  1. 您不想复制数据以避免膨胀和资源占用;
  2. 您想知道对象的属性何时从给定的原始/干净状态更改;
  3. 您希望 IsDirty 标志既准确又需要很少的处理时间/功率来返回值;和
  4. 您希望能够告诉对象何时再次认为自己是干净的。这在 UI 中构建/工作时特别有用。

鉴于这些要求,这就是我想出的,它似乎对我来说非常有效,并且在处理 UI 和准确捕获用户更改时变得非常有用。我还在下面发布了“如何使用”,向您展示我如何在 UI 中使用它。

物体

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();
    }
}

它很简单,可以满足我们所有的要求:

  1. 脏检查的数据不会重复...
  2. 这考虑了所有属性更改方案(请参阅下面的方案)...
  3. 当您调用 IsDirty 属性时,会执行一个非常简单且小型的 Equals 操作,并且可以通过 GetHashCode 覆盖完全自定义...
  4. 通过调用 MakeMeClean 方法,您现在又拥有了一个干净的对象!

当然,您可以对其进行调整以包含一堆不同的状态……这完全取决于您。此示例仅显示如何进行正确的 IsDirty 标志操作。

场景
让我们回顾一下这个场景,看看会发生什么:

  • 场景 1
    使用空构造函数创建新对象,
    属性名称从 "" 更改为 "James",
    调用 IsDirty 返回 True!准确的。

  • 场景 2
    使用“John”和 12345 的参数创建新对象,
    属性名称从“John”更改为“James”,
    属性名称从“James”更改回“John”,
    调用 IsDirty 返回 False。准确,我们也不必复制数据来做到这一点!

如何使用,一个 WinForms UI 示例
这只是一个示例,您可以通过 UI 以多种不同的方式使用它。

假设您有两种形式([A] 和 [B])。

第一个([A]) 是您的主表单,第二个([B]) 是一个允许用户更改 MySmartObject 中的值的表单。

[A] 和 [B] 表单都声明了以下属性:

public MySmartObject UserKey { get; set; }

当用户单击 [A] 表单上的按钮时,会创建 [B] 表单的实例,设置其属性并显示为对话框。

表单 [B] 返回后,[A] 表单根据 [B] 表单的 IsDirty 检查更新其属性。像这样:

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();
    }
}

此外,在 [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;
            }

        }

    }

正如您从上面的示例中看到的那样,这可能是一件非常有用的事情,因为它确实简化了 UI。

注意事项

  • 每次实现此功能时,您都必须将其自定义为您正在使用的对象。例如:不使用反射就没有“简单”的通用方法......如果使用反射,您会失去效率,尤其是在大型和复杂的对象中。

希望这可以帮助某人。

于 2015-05-22T18:56:11.787 回答
3

看看 PostSharp ( http://www.postsharp.org/ )。您可以轻松地创建一个将其标记为脏的属性,您可以将属性添加到需要它的每个属性,并将所有代码保存在一个地方。

粗略地说,创建一个具有您的状态的接口,使该类实现它。创建一个属性,该属性可以应用于属性并强制转换到您的界面,以便在某些内容更改标记的属性之一时设置值。

于 2009-04-30T07:57:43.453 回答
1

你的方法基本上就是我会怎么做。我只会删除 Status 属性的设置器:

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

而是添加一个功能

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

以及SetStatusDeleted()and SetStatusPurged(),因为我发现它更好地表明了意图。

编辑

阅读了 Jon Skeet 的答案后,我需要重新考虑我的方法;-) 对于简单的对象,我会坚持我的方式,但如果它变得更复杂,他的提议将导致更好的组织代码。

于 2009-04-30T06:06:36.807 回答
1

如果您的 Example_Class 是轻量级的,请考虑存储原始状态,然后将当前状态与原始状态进行比较以确定更改。如果不是,您的方法是最好的,因为在这种情况下查找原始状态会消耗大量系统资源。

于 2009-04-30T06:19:21.737 回答
1

除了“考虑让你的类型不可变”的建议之外,这是我写的(让 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;
     }
  }

你明白了.. 可能的改进:对 PropertyNames 使用常量并检查属性是否真的发生了变化。这里的一个缺点是

obj.SetProperty("ID", 700);         // will blow up int instead of long
obj.SetProperty<long>("ID", 700);   // be explicit or use 700L
于 2009-04-30T15:36:04.397 回答
1

这是我的做法。

在我不需要测试特定字段是否脏的情况下,我有一个抽象类:

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; }
}

}

还有一个界面

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

这允许我进行部分保存,并在有其他详细信息要保存时保留 IsDirty 状态。不完美,但涵盖了很多领域。

使用临时 IsDirty 状态的示例(为清楚起见,删除了错误包装和验证):

            area.SetIterimDirtyState();

            if (!UpdateClaimAndStatus(area))
                return false;

            area.MakeCleanIfInterimClean();

            return true;

这对大多数情况都有好处,但是对于某些类,我想使用原始数据的支持字段测试每个字段,并返回更改列表或至少更改字段的枚举。随着字段枚举的更改,我可以通过消息链将其向上推送,以选择性地更新远程缓存中的字段。

于 2017-07-27T21:30:18.580 回答
1

您还可以考虑将变量装箱,这会以性能为代价,但也有其优点。它非常简洁,您不会在不设置脏状态的情况下意外更改值。

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();
    }
}

然后挂钩一个回调,将你的类标记为脏。

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);
    }
}
于 2020-04-29T09:56:33.143 回答
-1

Another method is to override the GetHashCode() method to somthing like this:

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.

Edit2:

Since people are adverse to changing the hash code, instead of overriding the GetHashCode method, just call the method something else. The point is detecting a change not whether i use guids or hashcodes for object identification.

于 2009-04-30T07:06:46.150 回答