Свойство класса маркировки С# как грязное
Ниже приведен простой пример перечисления, который определяет состояние объекта и класс, который показывает реализацию этого перечисления.
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;
}
}
}
в установщике каждого свойства класса.
это звучит как хорошая идея, есть ли у кого-нибудь лучшие идеи о том, как можно назначить грязный флаг без этого на уровне презентации.
Ответы
Ответ 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, просто вызовите метод что-то еще. Точка обнаруживает изменение, не использует ли я гиды или хэш-коды для идентификации объекта.