Подтвердить что ты не робот

Свойство класса маркировки С# как грязное

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

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

при заполнении объекта класса данными из базы данных мы устанавливаем значение enum для "очистки". с целью сохранения большей части логики из уровня представления, как мы можем установить значение enum на "грязное" при изменении свойства.

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

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

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

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

4b9b3361

Ответ 1

Если вам действительно нужен грязный флаг на уровне класса (или, если на то пошло, уведомления), вы можете использовать трюки, как показано ниже, чтобы минимизировать беспорядок в ваших свойствах (здесь показаны как 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));
        }
    }
}

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

Ответ 2

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

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

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

Ответ 3

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

Вы можете, например, использовать ткача во время компиляции, например 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);
        } 
    }  
} 

Это означает, что классы, для которых вы хотите реализовать ChangeTracking, должны реализовать интерфейс IChangeTrackable (пользовательский интерфейс), который имеет как минимум свойство "Status".

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

Например:

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

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

Объект

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

Это достаточно просто и отвечает всем нашим требованиям:

  • Данные не дублируются для грязной проверки...
  • Это учитывает все сценарии изменений свойств (см. сценарии ниже)...
  • Когда вы вызываете свойство IsDirty, выполняется очень простая и маленькая операция Equals и полностью настраивается с помощью переопределения GetHashCode...
  • Вызывая метод MakeMeClean, у вас теперь есть чистый объект!

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

Сценарии
Перейдем к некоторым сценариям для этого и посмотрим, что вернется:

  • Сценарий 1
    Новый объект создается с помощью пустого конструктора,
    Название свойства изменяется с "на" Джеймс",
    вызов IsDirty возвращает True! Точная.

  • Сценарий 2
    Новый объект создается с помощью параметров "Джон" и 12345,
    Название свойства изменяется с "Джон" на "Джеймс",
    Название свойства меняется с "Джеймс" на "Джон" ,
    Вызов 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;
            }

        }

    }

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

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

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

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

Ответ 6

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

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

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

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

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

Edit

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

Ответ 7

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

Ответ 8

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

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

Ответ 9

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

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

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;

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

Ответ 10

Другим методом является переопределение метода 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:

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

Edit2:

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