c#将class属性标记为脏

时间:2009-04-30 05:56:20

标签: c# class properties

以下是定义对象状态的枚举的简单示例,以及显示此枚举实现的类。

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

在该类的每个属性的setter中。

这听起来是个好主意,是否有人对如何在表示层中分配脏标志有更好的想法。

11 个答案:

答案 0 :(得分:37)

当你确实想要在班级(或者,就此而言,通知)上使用脏标志时 - 你可以使用下面的技巧来最小化你的属性中的混乱(这里显示IsDirty和{{ 1}},只是为了好玩)。

显然使用枚举方法是一件小事(我没有的唯一原因是保持示例简单):

PropertyChanged

您也可以选择将其中的一部分推入抽象基类,但这是一个单独的讨论

答案 1 :(得分:22)

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

您还可以将所有肮脏检查放在一个地方,这样就不会污染您的其余代码。

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

答案 2 :(得分:16)

如果要以这种方式实现它,并且希望减少代码量,可以考虑应用面向方面编程。

例如,您可以使用像PostSharp这样的编译时编织器,并创建可应用于属性的“方面”。这方面确保在适当的时候设置脏标志。

方面可能如下所示:

[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,这意味着您要实现ChangeTracking的类应该实现IChangeTrackable接口(自定义接口),该接口至少具有“Status”属性。

您还可以创建自定义属性ChangeTrackingProperty,并确保上面创建的方面仅应用于使用此ChangeTrackingProperty属性修饰的属性。

例如:

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

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

这是我看到的一点点。 您甚至可以确保PostSharp在编译时检查具有使用ChangeTrackingProperty属性修饰的属性的类是否实现了IChangeTrackable接口。

答案 3 :(得分:3)

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

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

答案 4 :(得分:3)

此方法基于此线程中提供的一组不同概念。我想我会把它放在那里,为那些正在寻找一种方法来干净利落地做到这一点的人,就像我一样。

这种混合概念的关键是:

  1. 您不希望复制数据以避免膨胀和资源占用;
  2. 您想知道对象的属性何时从给定的原始/清除状态更改;
  3. 您希望IsDirty标志既准确,又需要很少的处理时间/功率来返回值;和
  4. 您希望能够告诉对象何时再次考虑自己清洁。这在UI中构建/工作时特别有用。
  5. 鉴于这些要求,这就是我提出的,它似乎对我来说非常有用,并且在处理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方法,您现在可以再次使用干净的对象了!
    5. 当然,你可以适应这种情况来涵盖一系列不同的状态......这真的取决于你。此示例仅显示如何进行正确的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;
                  }
      
              }
      
          }
      

      从上面的示例中可以看出,这可能是非常有用的,因为它真的简化了用户界面。

      <强>注意事项

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

      希望这有助于某人。

答案 5 :(得分:1)

你的方法基本上就是我的方法。我会的 删除Status属性的setter:

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

而是添加一个函数

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

以及SetStatusDeleted()SetStatusPurged(),因为我发现它更能表明意图。

修改

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

答案 6 :(得分:1)

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

答案 7 :(得分: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&amp;的常量;检查财产是否真的发生了变化。 这里的一个缺点是

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

答案 8 :(得分: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 State的示例(为清晰起见,错误包装和验证已删除):

            area.SetIterimDirtyState();

            if (!UpdateClaimAndStatus(area))
                return false;

            area.MakeCleanIfInterimClean();

            return true;

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

答案 9 :(得分:0)

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

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

答案 10 :(得分:-1)

另一种方法是将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();
}

从数据库加载后,获取对象的哈希码。然后在您保存之前检查当前哈希码是否等于先前的哈希码。如果它们是相同的,请不要保存。

修改

正如人们所指出的那样,这会导致哈希码发生变化 - 因为我使用Guids来识别我的对象,我不介意哈希码是否会发生变化。

<强> EDIT2:

由于人们不喜欢更改哈希代码,而不是覆盖GetHashCode方法,只需调用方法即可。重点是检测一个变化,而不是我是否使用guids或hashcodes进行对象识别。