c # маркировка свойства класса как грязного - PullRequest
23 голосов
/ 30 апреля 2009

Ниже приведен простой пример перечисления, определяющего состояние объекта, и класса, показывающего реализацию этого перечисления.

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». с целью удержания большей части логики вне уровня представления, как мы можем установить значение enum в «грязное» при изменении свойства.

Я думал о чем-то вроде;

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

в установщике каждого свойства класса.

это звучит как хорошая идея, есть ли у кого-нибудь лучшие идеи о том, как может быть назначен грязный флаг, без этого на уровне представления.

Ответы [ 10 ]

37 голосов
/ 30 апреля 2009

Если вам действительно нужен грязный флаг на уровне класса (или, в этом отношении, уведомления) - вы можете использовать приемы, подобные приведенным ниже, чтобы свести к минимуму беспорядок в ваших свойствах (здесь показаны IsDirty и PropertyChanged, просто для удовольствия).

Очевидно, что использовать метод 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));
        }
    }
}

Вы также можете добавить часть этого в абстрактный базовый класс, но это отдельное обсуждение

22 голосов
/ 30 апреля 2009

Один из вариантов - изменить его при записи; Другой способ - сохранить копию всех исходных значений и вычислить степень загрязненности, когда кто-либо просит об этом. Это дает дополнительное преимущество, заключающееся в том, что вы можете точно указать , какие поля изменились (и каким образом), что означает, что вы можете выдавать минимальные операторы обновления и немного облегчить разрешение конфликтов слияния.

Вы также можете поместить всю проверку на грязность в одном месте, чтобы она не загрязняла остальную часть вашего кода.

Я не говорю, что это идеально, но стоит рассмотреть этот вариант.

16 голосов
/ 30 апреля 2009

Если вы хотите реализовать это таким образом и хотите уменьшить объем кода, вы можете рассмотреть возможность применения Аспектно-ориентированного программирования.

Например, вы можете использовать ткача во время компиляции, например 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 голосов
/ 22 мая 2015

Этот метод основан на наборе различных понятий, представленных в этой теме. Я думал, что выложу это для всех, кто ищет способ сделать это чисто и эффективно, как и я сам.

Ключ этой гибридной концепции заключается в следующем:

  1. Вы не хотите дублировать данные, чтобы избежать вздутия и нехватки ресурсов;
  2. Вы хотите знать, когда свойства объекта изменились с данного исходного / чистого состояния;
  3. Вы хотите, чтобы флаг IsDirty был как точным, так и требующим небольшого времени / мощности обработки для возврата значения; и
  4. Вы хотите быть в состоянии указать объекту, когда снова считать себя чистым. Это особенно полезно при сборке / работе в пользовательском интерфейсе.

Учитывая эти требования, это то, что я придумал, и, похоже, оно отлично работает для меня, и стало очень полезным при работе с пользовательским интерфейсом и точном отслеживании изменений пользователя. Я также разместил ниже «Как использовать», чтобы показать вам, как я использую это в пользовательском интерфейсе.

Объект

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
    Новый объект создается с помощью пустого конструктора,
    Имя свойства изменяется с "" на "Джеймс",
    вызов IsDirty возвращает True! Точная.

  • Сценарий 2
    Новый объект создается с использованием параметров «Джон» и 12345,
    Имя свойства меняется с «Джон» на «Джеймс»,
    Имя свойства меняется с «Джеймс» на «Джон»,
    Call to IsDirty возвращает False. Точно, и нам не нужно было дублировать данные, чтобы сделать это!

Как использовать, пример пользовательского интерфейса WinForms
Это всего лишь пример, вы можете использовать его различными способами в пользовательском интерфейсе.

Допустим, у вас есть две формы ([A] и [B]).

Первая ([A]) - это ваша основная форма, а вторая ([B]) - это форма, которая позволяет пользователю изменять значения в MySmartObject.

Форма [A] и [B] имеет следующее объявленное свойство:

public MySmartObject UserKey { get; set; }

Когда пользователь нажимает кнопку в форме [A], создается экземпляр формы [B], устанавливается его свойство и он отображается в виде диалога.

После возврата формы [B] форма [A] обновляет свое свойство на основе проверки IsDirty формы [B]. Как это:

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

        }

    }

Как видно из приведенных выше примеров, это может быть очень полезным, поскольку оно действительно упрощает пользовательский интерфейс.

Предостережения

  • Каждый раз, когда вы реализуете это, вы должны настроить его для объекта, который вы используете. Например: нет "простого" общего способа сделать это без использования отражения ... и если вы используете отражение, вы теряете эффективность, особенно в больших и сложных объектах.

Надеюсь, это кому-нибудь поможет.

3 голосов
/ 30 апреля 2009

Посмотрите на PostSharp (http://www.postsharp.org/). Вы можете легко создать атрибут, который помечает его как грязный, вы можете добавить атрибут к каждому свойству, которое нуждается в нем, и он сохраняет весь ваш код в одном месте.

Грубо говоря, создайте интерфейс, в котором ваш статус будет реализован в классе. Создайте атрибут, который можно применить к свойствам и привести к вашему интерфейсу, чтобы установить значение, когда что-то изменит одно из отмеченных свойств.

1 голос
/ 28 июля 2017

Вот как я это делаю.

В тех случаях, когда мне не нужно проверять наличие грязных полей, У меня есть абстрактный класс:

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;

Это хорошо для большинства сценариев, однако для некоторых классов я хочу протестировать каждое поле с резервным полем исходных данных и либо вернуть список изменений, либо, по крайней мере, список измененных полей. С измененным списком полей я могу затем передать это через цепочку сообщений для выборочного обновления полей в удаленных кешах.

1 голос
/ 30 апреля 2009

Помимо совета 'подумайте над тем, чтобы сделать ваш тип неизменным', я кое-что написал (и заставил Джона и Марка научить меня чему-то по пути)

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
1 голос
/ 30 апреля 2009

Если ваш Example_Class является легковесным, рассмотрите возможность сохранения исходного состояния, а затем сравните текущее состояние с исходным, чтобы определить изменения. Если нет, то ваш подход - лучший, потому что в этом случае очистка исходного состояния потребляет много системных ресурсов.

1 голос
/ 30 апреля 2009

Ваш подход в основном так, как я бы это сделал. Я бы просто удалите установщик для свойства Status:

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

и вместо этого добавьте функцию

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

А также SetStatusDeleted() и SetStatusPurged(), потому что я считаю, что это лучше указывает на намерение.

Редактировать

Прочитав ответ Джона Скита , мне нужно пересмотреть свой подход ;-) Для простых объектов я бы придерживался своего пути, но если он станет более сложным, его предложение приведет к гораздо лучшему организованный код.

0 голосов
/ 30 апреля 2009

Другой метод заключается в переопределении метода 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();
}

После загрузки из базы данных получите хеш-код объекта. Затем перед сохранением проверьте, равен ли текущий хеш-код предыдущему хеш-коду. если они одинаковые, не сохраняйте.

Edit:

Как указали люди, это приводит к изменению хеш-кода - поскольку я использую Guids для идентификации своих объектов, я не против, если хеш-код изменяется.

Edit2:

Поскольку люди не хотят изменять хэш-код, вместо переопределения метода GetHashCode, просто вызовите метод как-нибудь еще. Смысл заключается в обнаружении изменений, а не в том, использую ли я направляющие или хеш-коды для идентификации объекта.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...