c # marcando la propiedad de clase como sucia

El siguiente es un ejemplo simple de una enumeración que define el estado de un objeto y una clase que muestra la implementación de esta enumeración.

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

al poblar el objeto de clase con datos de la base de datos, configuramos el valor enum para “limpiar”. con el objective de mantener la mayor parte de la lógica fuera de la capa de presentación, ¿cómo podemos configurar el valor enum como “sucio” cuando se cambia una propiedad?

estaba pensando algo así como;

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

en el setter de cada propiedad de la clase.

Si esto suena como una buena idea, ¿alguien tiene alguna idea mejor sobre cómo se puede asignar la bandera sucia sin hacerlo en la capa de presentación?

Cuando realmente quieres una bandera sucia en el nivel de clase (o, para el caso, las notificaciones), puedes usar trucos como a continuación para minimizar el desorden en tus propiedades (aquí muestra tanto IsDirty como PropertyChanged , solo por diversión).

Obviamente, es una cuestión trivial utilizar el enfoque enum (la única razón por la que no lo hice fue para mantener el ejemplo 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(ref T field, T value, string propertyName) { if (!EqualityComparer.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)); } } } 

También puede elegir incluir algo de eso en una clase base abstracta, pero eso es una discusión por separado

Una opción es cambiarlo en la escritura; otra es guardar una copia de todos los valores originales y calcular la suciedad cuando alguien lo solicite. Eso tiene el beneficio adicional de que puede decir exactamente qué campos han cambiado (y de qué manera), lo que significa que puede emitir sentencias de actualización mínimas y hacer que la resolución de conflictos de fusión sea un poco más fácil.

También puedes poner toda la comprobación de suciedad en un solo lugar, para que no contamine el rest de tu código.

No digo que sea perfecto, pero es una opción que vale la pena considerar.

Si desea implementarlo de esta manera y desea reducir la cantidad de código, puede considerar aplicar la Progtwigción Orientada a Aspectos.

Por ejemplo, puede usar un tejedor en tiempo de comstackción como PostSharp y crear un ‘aspecto’ que se puede aplicar a las propiedades. Este aspecto luego se asegura de que su bandera sucia esté configurada cuando corresponda.

El aspecto puede verse así:

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

Por supuesto, esto significa que las clases para las cuales desea implementar ChangeTracking deben implementar la interfaz IChangeTrackable (interfaz personalizada), que tiene al menos la propiedad ‘Estado’.

También puede crear un atributo personalizado ChangeTrackingProperty y asegurarse de que el aspecto que se creó anteriormente solo se aplique a las propiedades que están decoradas con este atributo ChangeTrackingProperty .

Por ejemplo:

 public class Customer : IChangeTrackable { public DirtyState Status { get; set; } [ChangeTrackingProperty] public string Name { get; set; } } 

Esto es un poco como lo veo. Incluso puede asegurarse de que PostSharp compruebe en tiempo de comstackción si las clases que tienen propiedades que están decoradas con el atributo ChangeTrackingProperty implementan la interfaz IChangeTrackable.

Eche un vistazo a PostSharp ( http://www.postsharp.org/ ). Puede crear fácilmente un atributo que lo marque como sucio, puede agregar el attrubute a cada propiedad que lo necesite y mantiene todo su código en un solo lugar.

Aproximadamente hablando Cree una interfaz que tenga su estado para que la clase lo implemente. Cree un atributo que se pueda aplicar a las propiedades y enviar a su interfaz para establecer el valor cuando algo cambie una de las propiedades marcadas.

Este método se basa en un conjunto de conceptos diferentes proporcionados en este hilo. Pensé que lo pondría allí para cualquiera que esté buscando la manera de hacerlo de manera limpia y eficiente, como yo mismo.

La clave de este concepto híbrido es que:

  1. No desea duplicar los datos para evitar la hinchazón y el acaparamiento de recursos;
  2. Desea saber cuándo las propiedades del objeto han cambiado desde un estado original / limpio dado;
  3. Desea que el indicador IsDirty sea preciso y requiera poco tiempo de procesamiento / potencia para devolver el valor; y
  4. Desea poder decirle al objeto cuándo considerarse limpio nuevamente. Esto es especialmente útil cuando se construye / trabaja dentro de la UI.

Dados esos requisitos, esto es lo que se me ocurrió, y parece estar funcionando perfectamente para mí, y se ha vuelto muy útil cuando se trabaja en contra de las IU y captura los cambios de usuario con precisión. También he publicado un “Cómo usar” a continuación para mostrarle cómo lo uso en la interfaz de usuario.

El objeto

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

Es lo suficientemente simple y aborda todos nuestros requisitos:

  1. Los datos NO están duplicados para el cheque sucio …
  2. Esto tiene en cuenta todos los escenarios de cambios de propiedad (ver escenarios a continuación) …
  3. Cuando llama a la propiedad IsDirty, se realiza una operación Equals muy simple y pequeña y es totalmente personalizable a través de la anulación de GetHashCode …
  4. Al llamar al método MakeMeClean, ¡ahora tiene un objeto limpio otra vez!

Por supuesto, puede adaptar esto para abarcar un grupo de diferentes estados … depende de usted. Este ejemplo solo muestra cómo tener una correcta operación de indicador IsDirty.

Escenarios
Repasemos algunos escenarios para esto y veamos qué viene:

  • escenario 1
    El nuevo objeto se crea usando el constructor vacío,
    El nombre de la propiedad cambia de “” a “James”,
    la llamada a IsDirty devuelve True! Preciso.

  • Escenario 2
    El objeto nuevo se crea usando los parámetros de “John” y 12345,
    El nombre de la propiedad cambia de “John” a “James”,
    El nombre de propiedad cambia de “James” a “John”,
    La llamada a IsDirty devuelve False. ¡Exacto, y no tuvimos que duplicar los datos para hacerlo tampoco!

Cómo usar, un ejemplo de interfaz de usuario de WinForms
Esto es solo un ejemplo, puedes usar esto de muchas formas diferentes desde una IU.

Digamos que tiene dos formas ([A] y [B]).

El primero ([A]) es su formulario principal, y el segundo ([B]) es un formulario que le permite al usuario cambiar los valores dentro de MySmartObject.

Tanto el formulario [A] como el [B] tienen declarada la siguiente propiedad:

 public MySmartObject UserKey { get; set; } 

Cuando el usuario hace clic en un botón en el formulario [A], se crea una instancia del formulario [B], se establece su propiedad y se muestra como un diálogo.

Después de que el formulario [B] regrese, el formulario [A] actualiza su propiedad en base al cheque IsDirty del formulario [B]. Me gusta esto:

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

Además, en [B], cuando se está cerrando, puede verificar e indicarle al usuario si está cerrando el formulario con cambios no guardados en él, como se muestra a continuación:

  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 (eg: 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; } } } 

Como puede ver en los ejemplos anteriores, esto puede ser muy útil ya que optimiza la interfaz de usuario.

Advertencias

  • Cada vez que implemente esto, debe personalizarlo para el objeto que está utilizando. Por ejemplo, no hay una forma genérica “fácil” de hacerlo sin usar la reflexión … y si usa la reflexión, pierde eficiencia, especialmente en objetos grandes y complejos.

Espero que esto ayude a alguien.

Su enfoque es básicamente cómo lo haría. Simplemente eliminaría el setter para la propiedad Status:

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

y en su lugar agregar una función

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

Además de SetStatusDeleted() y SetStatusPurged() , porque me parece mejor indica la intención.

Editar

Después de leer la respuesta de Jon Skeet , necesito reconsiderar mi enfoque 😉 En el caso de los objetos simples, me quedaría a mi manera, pero si se vuelve más complejo, su propuesta daría lugar a un código mucho mejor organizado.

Si su Example_Class es liviana, considere almacenar el estado original y luego comparar el estado actual con el original para determinar los cambios. De lo contrario, su enfoque es el mejor porque, en este caso, anotar el estado original consume muchos recursos del sistema.

Además del consejo de ‘considere hacer que su tipo sea inmutable’, aquí hay algo que escribí (y Jon y Marc me enseñaron algo en el camino)

 public class Example_Class { // snip // all properties are public get and private set private Dictionary m_PropertySetterMap; public Example_Class() { m_PropertySetterMap = new Dictionary(); InitializeSettableProperties(); } public Example_Class(long id, string name):this() { this.ID = id; this.Name = name; } private void InitializeSettableProperties() { AddToPropertyMap("ID", value => { this.ID = value; }); AddToPropertyMap("Name", value => { this.Name = value; }); } // jump thru a hoop because it won't let me cast an anonymous method to an Action/Delegate private void AddToPropertyMap(string sPropertyName, Action setterAction) { m_PropertySetterMap.Add(sPropertyName, setterAction); } public void SetProperty(string propertyName, T value) { (m_PropertySetterMap[propertyName] as Action).Invoke(value); this.Status = StatusEnum.Dirty; } } 

Obtendrá la idea … posibles mejoras: use constantes para PropertyNames y compruebe si la propiedad realmente ha cambiado. Un inconveniente aquí es que

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

Así es como lo hago.

En los casos en que no necesito probar que los campos específicos estén sucios, tengo una clase abstracta:

 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(); } ///  /// Must be overridden with whatever valid tests are needed to make sure required field values are present. ///  public abstract bool IsValid { get; } } 

}

Además de una interfaz

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

Esto me permite hacer salvaciones parciales, y preservar el estado IsDirty si hay otros detalles para guardar. No es perfecto, pero cubre mucho terreno.

Ejemplo de uso con estado IsDirty provisional (se eliminó el error y la validación para mayor claridad):

  area.SetIterimDirtyState(); if (!UpdateClaimAndStatus(area)) return false; area.MakeCleanIfInterimClean(); return true; 

Esto es bueno para la mayoría de los escenarios, sin embargo, para algunas clases quiero probar para cada campo con un campo de respaldo de datos originales, y devolver una lista de cambios o al menos una enumeración de campos modificados. Con una enumeración de campos modificados, puedo enviarlos a través de una cadena de mensajes para la actualización selectiva de campos en cachés remotos.

Otro método es anular el método GetHashCode () a algo así:

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

Una vez cargado desde la base de datos, obtenga el código hash del objeto. Luego, justo antes de guardar, compruebe si el código hash actual es igual al código hash anterior. si son iguales, no guardes.

Editar:

Como han señalado las personas, esto hace que cambie el código hash: como utilizo Guids para identificar mis objetos, no me importa si el hashcode cambia.

Edit2:

Dado que las personas son adversas a cambiar el código hash, en lugar de anular el método GetHashCode, simplemente llame al método de otra manera. El objective es detectar un cambio, no si utilizo guids o hashcodes para la identificación de objetos.