2010-03-02 33 views

risposta

38

Per fare questo, non è possibile utilizzare i setter getter automatici & ed è necessario impostare IsDirty in ciascun setter.

Generalmente ho un metodo generico "setProperty" che accetta un parametro ref, il nome della proprietà e il nuovo valore. L'ho chiamato nel setter, consente un singolo punto in cui posso impostare isDirty e generare gli eventi di notifica di modifica, ad es.

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

Buona risposta (1) Il mio unico suggerimento sarebbe quello di implementare IChangeTracking (http://msdn.microsoft.com/en -us/library/system.componentmodel.ichangetracking.aspx) piuttosto che creare la tua proprietà IsDirty. –

+3

John Myczek: ero completamente inconsapevole di quell'interfaccia, dovrò cambiare i miei progetti domestici per usarlo immediatamente (o appena quel fastidioso cosa 'vita' permette). Grazie :) –

+1

Non lo sapevo o John –

4

Set IsDirty al vero in tutti i setter.

Si potrebbe anche considerare di rendere il setter per IsDirty privato (o protetto, se è possibile avere classi figlio con proprietà aggiuntive). Altrimenti potreste avere codice al di fuori della classe che nega il suo meccanismo interno per determinare la sporcizia.

1

Considerare attentamente lo scopo alla base del quale è necessario il rilevamento dell'oggetto? Supponiamo che se qualcosa come gli altri oggetti debbano fare qualcosa in base allo stato di un altro oggetto, allora si consideri l'implementazione dello observer design pattern.

Se si tratta di qualcosa di piccolo, prendere in considerazione l'implementazione dell'interfaccia INotifyPropertyChanged.

7

La soluzione di Dan è perfetta.

Un'altra opzione da considerare se si sta andando ad avere per fare questo su più classi (o forse si vuole una classe esterna di "ascoltare" per le modifiche alle proprietà):

  • implementare l'interfaccia INotifyPropertyChanged in una classe astratta
  • spostare la proprietà IsDirty alla classe astratta
  • Avere Class1 e tutte le altre classi che richiedono questa funzionalità per estendere la vostra classe astratta
  • Avere tutto i tuoi setter fuoco l'evento PropertyChanged implementato dalla classe astratta, passando il loro nome alla manifestazione
  • Nella tua classe base, intercettate l'evento PropertyChanged e impostare IsDirty al vero quando si spara

E 'un po' di lavora inizialmente per creare la classe astratta, ma è un modello migliore per la visualizzazione delle modifiche dei dati come qualsiasi altra classe può vedere quando cambia IsDirty (o qualsiasi altra proprietà).

La mia classe di base per questo appare come il seguente:

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

Qualsiasi altro modello poi basta estende BaseModel e chiede NotifyPropertyChanged in ogni setter.

+0

Ho visto soluzioni come questa che sono step cooler dove rimuove la stringa interamente e utilizza Lambda, non ho un collegamento a disposizione però :( –

+0

+1 L'interfaccia INotifyPropertyChanged è lo strumento di cui uno deve essere consapevole delle modifiche che si verifica in una proprietà. =) –

2

Se esiste un numero molto elevato di tali classi, tutte aventi lo stesso modello e spesso è necessario aggiornare le loro definizioni, considerare l'utilizzo della generazione del codice per sputare automaticamente i file di origine C# per tutte le classi, in modo da non è necessario mantenerli manualmente. L'input per il generatore di codice sarebbe semplicemente un semplice formato di file di testo che è possibile analizzare facilmente, indicando i nomi e i tipi delle proprietà necessarie in ogni classe.

Se ne esiste solo un piccolo numero o le definizioni cambiano molto di rado durante il processo di sviluppo, allora è improbabile che valga la pena, nel qual caso si potrebbe anche mantenerle a mano.

Aggiornamento:

Questo è probabilmente così sopra le righe per un semplice esempio, ma è stato divertente per capire!

In Visual Studio 2008, se si aggiunge un file chiamato CodeGen.tt al progetto e quindi incollare questa roba in esso, avrete la stoffa di un sistema di generazione di codice:

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

    } 

<# } #> 
} 

C'è una semplice stringa letterale chiamato src in cui si dichiara le classi è necessario, in un formato semplice:

Foo:  string Prop1, 
     int Prop2; 

Bar:  string FirstName, 
     string LastName, 
     int Age; 

così si può facilmente aggiungere centinaia di dichiarazioni simili. Ogni volta che salvi le modifiche, Visual Studio eseguirà il modello e produrrà CodeGen.cs come output, che contiene l'origine C# per le classi, completo della logica IsDirty.

È possibile modificare il modello di ciò che viene prodotto modificando l'ultima sezione, dove scorre attraverso il modello e produce il codice. Se hai usato ASP.NET, è simile a questo, tranne che per generare sorgenti C# invece di HTML.

1

Le risposte di Dan's e Andy Shellam sono le mie preferite.

In ogni caso, se si desidera mantenere TRACK delle modifiche, come in un registro o così, si potrebbe prendere in considerazione l'uso di un dizionario che aggiungerebbe tutte le modifiche alle proprietà quando vengono notificate che sono state modificate. Pertanto, puoi aggiungere la modifica al tuo dizionario con una chiave univoca e tenere traccia delle tue modifiche. Quindi, se desideri eseguire il routback in memoria dello stato del tuo oggetto, puoi farlo in questo modo.

EDIT Ecco cosa usa Bart de Smet per tenere traccia dei cambiamenti di proprietà nel corso LINQ to AD. Una volta che le modifiche sono state commesse in AD, cancella il dizionario. Così, quando una proprietà cambia, perché ha implementato l'interfaccia INotifyPropertyChanged, quando una proprietà effettivamente cambiato, si utilizza un dizionario> come segue:

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

Quindi, credo che se Bart de Smet ha fatto, questo è in qualche modo una pratica da considerare.

+1

Recentemente mi sono lamentato da un collega per aver fatto qualcosa di simile - apparentemente questo imposterà tipi di valore come int e raddoppia in tipi .NET, con conseguente penalizzazione delle prestazioni. Non era tanto preoccupante per la piccola (ish) app a cui sto lavorando, ma potrebbe essere qualcosa a cui prestare attenzione nelle app più grandi. –

+0

Interessante! Grazie per questo commento, forse non mi sarei accorto di questo prima di farlo da solo. Sarò avvertito allora. =) –

+1

Quindi, quando ci penso, Bart de Smet, di Microsoft, colui che ha progettato LINQ to AD su CodePlex, utilizza un dizionario > per tenere traccia delle modifiche alle proprietà. Modificherò la mia risposta per escludere il codice di esempio. –

0

Questo è qualcosa che è costruito nella classe BusinessBase nel quadro di Rocky Lhokta CLSA, quindi si può sempre andare a vedere come si fa ...

1

So che questo è un vecchio thread, ma penso enumerazioni non funzionerà con la soluzione di Binary Worrier. Si otterrà un messaggio di errore in fase di progettazione che la proprietà enum Type "non può essere utilizzata come parametro di tipo" T "nel tipo o metodo generico" ..."SetProperty (string, ref T, T)". Non c'è conversione di boxe ... ".

ho fatto riferimento questo post StackOverflow per risolvere il problema con le enumerazioni: C# boxing enum error with generics

0

Per supportare le enumerazioni, si prega di utilizzare la soluzione perfetta di Binary Worrier e aggiungere il codice qui sotto.

Ho aggiunto il supporto Enum per il mio (ed è stato un dolore), immagino che sia bello aggiungere anche questo.

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

e attuato tramite:

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

So che è stato un po 'che lei ha chiesto questo. Se siete ancora interessati a mantenere le vostre classi pulito e semplice, senza la necessità di derivare da classi base, vorrei suggerire di usare PropertyChanged.Fody che ha un IsChanged Flag attuata

2

È possibile implementare i IChangeTracking o IRevertibleChangeTracking interfacce, ora incluso in. NET Standard 2.0.

implementazione è la seguente:

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

Un'altra opzione, che mi piace di più, è quello di utilizzare una libreria il rilevamento delle modifiche, come TrackerDog, che genera tutto il codice boilerplate per te, mentre necessità di fornire entità POCO.

Ci sono altri modi per ottenere questo se non si desidera implementare tutte le proprietà a mano. Un'opzione consiste nell'utilizzare una libreria di tessitura, come ad esempio Fody.PropertyChanged e Fody.PropertyChanging, e gestire i metodi di modifica per memorizzare nella cache i vecchi valori e tenere traccia dello stato dell'oggetto. Un'altra opzione è quella di avere il grafico dell'oggetto memorizzato come MD5 o qualche altro hash e resettarlo dopo ogni modifica, potresti essere sorpreso, ma se non ti aspetti modifiche di zillion e se lo richiedi solo su richiesta, può funzionare davvero veloce.

Ecco un esempio di implementazione (Nota: richiede Json.NET e 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; 
    } 
} 
Problemi correlati