2010-03-02 14 views

Respuesta

38

Para hacer esto, no se puede usar realmente setters getter & automáticos, y debe configurar IsDirty en cada setter.

Generalmente tengo un método genérico "setProperty" que toma un parámetro ref, el nombre de la propiedad y el nuevo valor. Lo llamo en el setter, permite que un solo punto donde puedo establecer es Sucio y levantar eventos de notificación de cambio, p. Ej.

protected bool SetProperty<T>(string name, ref T oldValue, T newValue) where T : System.IComparable<T> 
    { 
     if (oldValue == null || oldValue.CompareTo(newValue) != 0) 
     { 
      oldValue = newValue; 
      PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name)); 
      isDirty = true; 
      return true; 
     } 
     return false; 
    } 
// For nullable types 
protected void SetProperty<T>(string name, ref Nullable<T> oldValue, Nullable<T> newValue) where T : struct, System.IComparable<T> 
{ 
    if (oldValue.HasValue != newValue.HasValue || (newValue.HasValue && oldValue.Value.CompareTo(newValue.Value) != 0)) 
    { 
     oldValue = newValue; 
     PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(name)); 
    } 
} 
+22

Buena respuesta (1) Mi única sugerencia sería implementar IChangeTracking (http://msdn.microsoft.com/en -us/library/system.componentmodel.ichangetracking.aspx) en lugar de crear su propia propiedad IsDirty. –

+3

John Myczek: no estaba al tanto de esa interfaz, cambiaré mis proyectos favoritos para usar esto inmediatamente (o tan pronto como ese molesto lo de 'vida' lo permite). Gracias :) –

+1

No sabía nada de eso John –

4

Conjunto IsDirty a cierto en todos sus incubadores.

También podría considerar hacer el setter para IsDirty privado (o protegido, si puede tener clases secundarias con propiedades adicionales). De lo contrario, podría tener un código fuera de la clase que niegue su mecanismo interno para determinar la suciedad.

1

¿Considera detenidamente el objetivo subyacente que se requiere el seguimiento de objetos? Supongamos que si es algo así como que otros objetos tienen que hacer algo en función del estado de otro objeto, entonces considere implementar el observer design pattern.

Si es algo pequeño, considere implementar la interfaz INotifyPropertyChanged.

7

La solución de Dan es perfecta.

Otra opción a considerar si usted va a tener que hacer esto en varias clases (o tal vez quiera una clase externa "escuchar" a los cambios en las propiedades):

  • implementar la interfaz INotifyPropertyChanged en una clase abstracta
  • Mover la propiedad IsDirty a su clase abstracta
  • Tener Class1 y todas las demás clases que requieren esta funcionalidad para ampliar su clase abstracta
  • Tener todos los emisores de fuego del PropertyChanged evento implementado por la clase abstracta, pasando en su nombre al evento
  • En su clase base, detectar el evento PropertyChanged y establecer IsDirty a cierto cuando se dispara

Es un poco de Trabajar inicialmente para crear la clase abstracta, pero es un modelo mejor para observar los cambios de datos que cualquier otra clase puede ver cuando cambia IsDirty (o cualquier otra propiedad).

Mi clase base para esto se parece a lo siguiente:

public abstract class BaseModel : INotifyPropertyChanged 
{ 
    /// <summary> 
    /// Initializes a new instance of the BaseModel class. 
    /// </summary> 
    protected BaseModel() 
    { 
    } 

    /// <summary> 
    /// Fired when a property in this class changes. 
    /// </summary> 
    public event PropertyChangedEventHandler PropertyChanged; 

    /// <summary> 
    /// Triggers the property changed event for a specific property. 
    /// </summary> 
    /// <param name="propertyName">The name of the property that has changed.</param> 
    public void NotifyPropertyChanged(string propertyName) 
    { 
     if (this.PropertyChanged != null) 
     { 
      this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); 
     } 
    } 
} 

Cualquier otro modelo a continuación, sólo se extiende BaseModel, y llama a NotifyPropertyChanged en cada colocador.

+0

que he visto soluciones como ésta, que son más frías paso en el que se elimina por completo la cadena y utiliza Lambda, no tienen un enlace en la mano, aunque :( –

+0

1 interfaz INotifyPropertyChanged es la herramienta que uno tiene que ser consciente de los cambios ocurriendo a una propiedad. =) –

2

Si hay un gran número de dichas clases, todas tienen el mismo patrón, y con frecuencia tiene que actualizar sus definiciones, considere usar generación de código para escupir automáticamente los archivos fuente C# para todas las clases, para que no tiene que mantenerlos manualmente. La entrada al generador de código sería simplemente un formato de archivo de texto simple que puede analizar fácilmente, indicando los nombres y tipos de las propiedades necesarias en cada clase.

Si solo hay un pequeño número de ellos, o las definiciones cambian con muy poca frecuencia durante el proceso de desarrollo, entonces es poco probable que valga la pena, en cuyo caso también puede mantenerlos a mano.

Actualización:

Ésta es probablemente muy por encima de un ejemplo sencillo, pero fue muy divertido para averiguar!

En Visual Studio 2008, si se agrega un archivo llamado CodeGen.tt a su proyecto y luego pegar esto en él, usted tiene los ingredientes de un sistema de generación de código:

<#@ template debug="false" hostspecific="true" language="C#v3.5" #> 
<#@ output extension=".cs" #> 
<#@ assembly name="System.Core" #> 
<#@ import namespace="System.Linq" #> 

<# 

// You "declare" your classes here, as in these examples: 

var src = @" 

Foo:  string Prop1, 
     int Prop2; 

Bar:  string FirstName, 
     string LastName, 
     int Age; 
"; 

// Parse the source text into a model of anonymous types 

Func<string, bool> notBlank = str => str.Trim() != string.Empty; 

var classes = src.Split(';').Where(notBlank).Select(c => c.Split(':')) 
    .Select(c => new 
    { 
     Name = c.First().Trim(), 
     Properties = c.Skip(1).First().Split(',').Select(p => p.Split(' ').Where(notBlank)) 
         .Select(p => new { Type = p.First(), Name = p.Skip(1).First() }) 
    }); 
#> 

// Do not edit this file by hand! It is auto-generated. 

namespace Generated 
{ 
<# foreach (var cls in classes) {#> class <#= cls.Name #> 
    { 
     public bool IsDirty { get; private set; } 
     <# foreach (var prop in cls.Properties) { #> 

     private <#= prop.Type #> _storage<#= prop.Name #>; 

     public <#= prop.Type #> <#= prop.Name #> 
     { 
      get { return _storage<#= prop.Name #>; } 
      set 
      { 
       IsDirty = true; 
       _storage<#= prop.Name #> = value; 
      } 
     } <# } #> 

    } 

<# } #> 
} 

Hay una cadena simple literal llamada src en el que se declara las clases que necesita, en un formato simple:

Foo:  string Prop1, 
     int Prop2; 

Bar:  string FirstName, 
     string LastName, 
     int Age; 

para que pueda añadir fácilmente cientos de declaraciones similares. Siempre que guarde los cambios, Visual Studio ejecutará la plantilla y generará CodeGen.cs como salida, que contiene la fuente de C# para las clases, completa con la lógica IsDirty.

Puede cambiar la plantilla de lo que se produce alterando la última sección, donde pasa por el modelo y produce el código. Si usó ASP.NET, es similar a eso, excepto que genera fuente C# en lugar de HTML.

1

Las respuestas de Dan y Andy Shellam son mis favoritas.

En cualquier caso, si desea mantener la SEGUIMIENTO de sus cambios, como en un registro más o menos, podría considerar el uso de un diccionario que agregaría todos sus cambios de propiedad cuando se notifique que se han modificado. Por lo tanto, podría agregar el cambio en su Diccionario con una clave única y realizar un seguimiento de sus cambios. Entonces, si desea recuperar en memoria el estado de su objeto, podría hacerlo de esta manera.

EDITAR Esto es lo que Bart de Smet utiliza para realizar un seguimiento de los cambios de propiedad a lo largo de LINQ a AD. Una vez que los cambios se han comprometido a AD, borra el Diccionario. Así que, cuando una propiedad cambia, ya que implementa la interfaz INotifyPropertyChanged, cuando una propiedad cambia realmente, que utiliza un diccionario> de la siguiente manera:

/// <summary> 
    /// Update catalog; keeps track of update entity instances. 
    /// </summary> 
    private Dictionary<object, HashSet<string>> updates 
     = new Dictionary<object, HashSet<string>>(); 

    public void UpdateNotification(object sender, PropertyChangedEventArgs e) 
    { 
     T source = (T)sender; 

     if (!updates.ContainsKey(source)) 
      updates.Add(source, new HashSet<string>()); 

     updates[source].Add(e.PropertyName); 
    } 

lo tanto, supongo que si Bart de Smet hizo eso, esto es de alguna manera una práctica a considerar.

+1

Un colega me reprendió recientemente por hacer algo similar, aparentemente esto incluirá tipos de valores como int y dobles en tipos .NET, lo que resulta en una penalización de rendimiento. No era tanto una preocupación en la aplicación pequeña (ish) en la que estoy trabajando, pero podría ser algo de lo que hay que tener cuidado en aplicaciones más grandes. –

+0

¡Interesante! Gracias por este comentario, quizás no me habría dado cuenta antes de hacerlo yo mismo. Seré advertido entonces. =) –

+1

Por lo tanto, cuando lo pienso de ella, Bart De Smet, de Microsoft, el que diseñó LINQ a AD en CodePlex, utiliza un diccionario > para realizar un seguimiento de los cambios de propiedad. Editaré mi respuesta para incluir el código de muestra. –

0

Esto es algo que se construye en la clase BusinessBase en el marco de Rocky Lhokta CLSA, por lo que siempre podía ir a ver cómo se hace ...

1

Sé que esto es un hilo viejo, pero creo enumeraciones no funcionará con la solución de Binary Worrier. Obtendrá un mensaje de error de tiempo de diseño que indica que el tipo de propiedad enum "no se puede usar como parámetro de tipo 'T' en el tipo o método genérico" ..."SetProperty (cadena, ref T, T) '. No hay conversión de boxeo ...".

que hace referencia este post StackOverflow para resolver el problema con las enumeraciones: C# boxing enum error with generics

0

Para apoyar las enumeraciones, por favor utilice la solución perfecta de Binary Worrier y añadir el código de abajo.

He agregado el soporte Enum para mi propio (y fue un dolor), supongo que esto es bueno agregar también.

protected void SetEnumProperty<TEnum>(string name, ref TEnum oldEnumValue, TEnum newEnumValue) where TEnum : struct, IComparable, IFormattable, IConvertible 
{ 
    if (!(typeof(TEnum).IsEnum)) { 
     throw new ArgumentException("TEnum must be an enumerated type"); 
    } 

    if (oldEnumValue.CompareTo(newEnumValue) != 0) { 
     oldEnumValue = newEnumValue; 
     if (PropertyChanged != null) { 
      PropertyChanged(this, new PropertyChangedEventArgs(name)); 
     } 
     _isChanged = true; 
    } 
} 

y puesto en práctica a través de:

Public Property CustomerTyper As CustomerTypeEnum 
     Get 
      Return _customerType 
     End Get 
     Set(value As ActivityActionByEnum) 
      SetEnumProperty("CustomerType", _customerType, value) 
     End Set 
    End Property 
0

Sé que ha pasado un tiempo desde que hizo esta. Si usted todavía está interesado en mantener sus clases limpia y sencilla sin necesidad de derivar a partir de clases base, sugeriría utilizar PropertyChanged.Fody que tiene una IsChanged Flag implementado

2

Puede implementar las IChangeTracking o IRevertibleChangeTracking interfaces, que ahora se incluyen en. NET Standard 2.0.

implementación es el siguiente:

IChangeTracking:

class Entity : IChangeTracking 
{ 
    string _FirstName; 
    public string FirstName 
    { 
    get => _FirstName; 
    set 
    { 
     if (_FirstName != value) 
     { 
     _FirstName = value; 
     IsChanged = true; 
     } 
    } 
    } 

    string _LastName; 
    public string LastName 
    { 
    get => _LastName; 
    set 
    { 
     if (_LastName != value) 
     { 
     _LastName = value; 
     IsChanged = true; 
     } 
    } 
    } 

    public bool IsChanged { get; private set; }  
    public void AcceptChanges() => IsChanged = false; 
} 

IRevertibleChangeTracking:

class Entity : IRevertibleChangeTracking 
{ 
    Dictionary<string, object> _Values = new Dictionary<string, object>(); 

    string _FirstName; 
    public string FirstName 
    { 
    get => _FirstName; 
    set 
    { 
     if (_FirstName != value) 
     { 
     if (!_Values.ContainsKey(nameof(FirstName))) 
      _Values[nameof(FirstName)] = _FirstName; 
     _FirstName = value; 
     IsChanged = true; 
     } 
    } 
    } 

    string _LastName; 
    public string LastName 
    { 
    get => _LastName; 
    set 
    { 
     if (_LastName != value) 
     { 
     if (!_Values.ContainsKey(nameof(LastName))) 
      _Values[nameof(LastName)] = _LastName; 
     _LastName = value; 
     IsChanged = true; 
     } 
    } 
    } 

    public bool IsChanged { get; private set; } 

    public void RejectChanges() 
    { 
    foreach (var property in _Values) 
     GetType().GetRuntimeProperty(property.Key).SetValue(this, property.Value); 
    AcceptChanges(); 
    } 

    public void AcceptChanges() 
    { 
    _Values.Clear(); 
    IsChanged = false; 
    } 
} 

Otra opción, la que más me gusta, es utilizar una biblioteca de seguimiento de los cambios, como TrackerDog, que genera todo el código repetitivo para usted, mientras que solo necesidad de proporcionar entidades POCO.

Hay más formas de lograr esto si no desea implementar todas las propiedades a mano. Una opción es utilizar una biblioteca de tejido, como Fody.PropertyChanged y Fody.PropertyChanging, y manejar los métodos de cambio para almacenar en caché valores antiguos y rastrear el estado del objeto. Otra opción es tener el gráfico del objeto almacenado como MD5 o algún otro hash, y reiniciarlo en cualquier cambio, puede que se sorprenda, pero si no espera un trillón de cambios y si lo solicita solo bajo demanda, puede funcionar realmente rápido.

Aquí es un ejemplo de implementación (Nota: requiere Json.NET y Fody/PropertyChanged:.

[AddINotifyPropertyChangedInterface] 
class Entity : IChangeTracking 
{ 
    public string UserName { get; set; } 
    public string LastName { get; set; } 

    public bool IsChanged { get; private set; } 

    string hash; 
    string GetHash() 
    { 
    if (hash == null) 
     using (var md5 = MD5.Create()) 
     using (var stream = new MemoryStream()) 
     using (var writer = new StreamWriter(stream)) 
     { 
     _JsonSerializer.Serialize(writer, this); 
     var hash = md5.ComputeHash(stream); 
     this.hash = Convert.ToBase64String(hash); 
     } 
    return hash; 
    } 

    string acceptedHash; 
    public void AcceptChanges() => acceptedHash = GetHash(); 

    static readonly JsonSerializer _JsonSerializer = CreateSerializer(); 
    static JsonSerializer CreateSerializer() 
    { 
    var serializer = new JsonSerializer(); 
    serializer.Converters.Add(new EmptyStringConverter()); 
    return serializer; 
    } 

    class EmptyStringConverter : JsonConverter 
    { 
    public override bool CanConvert(Type objectType) 
     => objectType == typeof(string); 

    public override object ReadJson(JsonReader reader, 
     Type objectType, 
     object existingValue, 
     JsonSerializer serializer) 
     => throw new NotSupportedException(); 

    public override void WriteJson(JsonWriter writer, 
     object value, 
     JsonSerializer serializer) 
    { 
     if (value is string str && str.All(char.IsWhiteSpace)) 
     value = null; 

     writer.WriteValue(value); 
    } 

    public override bool CanRead => false; 
    } 
} 
Cuestiones relacionadas