L’interfaccia IRevertibleChangeTracking, una possibile implementazione.

Di Mauro Servienti - Microsoft MVP

Introduzione

come abbiamo avuto modo di evidenziare più volte uno degli aspetti più importanti di un’applicazione è il suo aspetto esteriore, devo dire sinceramente che non condivido questa affermazione ma purtroppo questo è dettato dall’utente finale, il nostro cliente ultimo, che non sempre ha le conoscenze tecniche per giudicare il nostro operato e quindi il suo giudizio si basa principalmente su quello che vede, sulle possibilità che l’applicazione offre, sulla semplicità ed intuitività d’uso della stessa, in una parola usabilità.

Uno degli aspetti che ho sempre apprezzato, e che quindi ho sempre cercato di riprodurre nelle mie applicazioni, è la possibilità di informare l’utente che i dati, che l’applicazione sta trattando, necessitano di essere salvati; questa operazione è sicuramente doverosa alla chiusura dell’applicazione, ma è anche interessante la possibilità di cambiare lo stato dell’interfaccia utente durante l’uso, in base allo stato dei dati che stiamo trattando: potremmo ad esempio cambiare lo stato del pulsante “salva”, attivandolo e disattivandolo, potremmo aggiungere una scritta, o un semplice asterisco, alla barra del titolo per informare che ci sono delle modifiche non salvate.

 

panoramica della soluzione

Il nostro obiettivo è quindi, in prima istanza, quello di sapere se i dati che stiamo trattando abbiano o meno subito delle modifiche; ci sono fondamentalmente due possibili strade che possiamo percorrere per raggiungere il nostro scopo:

  • Realizzare un’infrastruttura che tiene traccia delle operazioni che facciamo e che quindi sappia dirci se abbiamo apportato delle modifiche;

  • Delegare ai dati la responsabilità di tenere traccia del loro stato;

La soluzione che implementeremo sceglie di seguire la seconda strada, vedremo nel dettaglio perchè a breve, e lo faremo facendo implementare alla base dati l’interfaccia IRevertibleChangeTracking che ha proprio lo scopo di tenere traccia dello stato dell’oggetto che stiamo trattando. L’interfaccia IRevertibleChangeTracking deriva dall’interfaccia IChangeTracking e aggiunge il supporto per la gestione del rollback. Vediamo nel dettaglio la firma di entrambe:

public interface IChangeTracking
{
bool IsChanged { get; }
void AcceptChanges();
}

La proprietà IsChanged (in sola lettura) serve per determinare se l’oggetto ha subito delle modifiche, il metodo AcceptChanges() viene invece chiamato per chiedere all’oggetto di confermare le modifiche che sono state apportate, se l’operazione va a buon fine la proprietà IsChanged torna ad essere false.

In questa prima fase siamo interessati principalmente alla proprietà IsChanged che ci da proprio la possibilità di sapere se l’elemento (la nostra base dati) è stato modificato o meno e comportarci di conseguenza. L’obiettivo, in un’ipotetica classe Person, è poter scrivere qualcosa del tipo:

Person p = new Person();
p.FirstName = "Mauro";
p.LastName = "Servienti";
Boolean isChanged = p.IsChanged;

Nonstante l’interfaccia IChangeTracking soddisfi le nostre esigenze vediamo comunque anche IRevertibleChangeTracking:

public interface IRevertibleChangeTracking : IChangeTracking
{
void RejectChanges();
}

Il metodo RejectChanges() serve per chiedere all’implementatore, la nostra base dati, di respingere le modifiche che ha subito, dall’ultima chiamata ad AcceptChanges(), ripristinando uno stato antecedente, in una parola di eseguire un Rollback.

Ci si potrebbe chiedere, a questo punto, perchè non utilizzare l’interfaccia IEditableObject presente già dalle primissime versioni del .NET Framework e ampiamente utilizzata dal framework stesso.

Cerchiamo di dirimere subito questo dubbio e capire perchè si è resa necessaria l’introduzione di una nuova interfaccia che sostanzialmente sembra volta a risolvere una problematica già affrontata: credo che uno dei motivi principali che hanno reso necessario l’introdurre una nuova interfaccia fosse proprio che l’interfaccia IEditableObject sia ampiamente utilizzata dal framework stesso, ad esempio dal motore di data binding.

Facciamo un esempio chiarificatore: quando mettiamo in binding una DataTable con una DataGridView succede che la DataGridView ad ogni cambiamento di riga (il cursore si sposta da una riga all’altra) chiama IEditableObject.BeginEdit() quando la selezione arriva su una riga e IEditableObject.EndEdit() quando la riga selezionata cambia, questo perchè l’elemento in binding con la singola riga implementa l’interfaccia IEditableObject. Questo stesso comportamento si ottiene se in binding con la griglia mettiamo una lista di classi che implementano l’interfaccia in oggetto, quindi il comportamente è determinato dall’interfaccia e non dalla DataTable.

Ci si rende subito conto che questo comportamento è in contraddizione con il nostro obiettivo perchè se selezioniamo una riga della griglia, apportiamo delle modifiche e nel momento in cui la riga selezionata cambia le modifiche vengono implicitamente accettate prevarichiamo qualsivoilgia volontà dell’utente di salvare o meno il suo lavoro.

Lo scopo dell’interfaccia IEditableObject infatti in questo caso è quello di permettere un rollback istantaneo delle modifiche in caso di pressione del tasto ESC mentre la selezione è all’interno di una riga della griglia, nel qual caso viene chiamato IEditableObject.CancelEdit(). Infine è doveroso sottolineare che IEditableObject non offre un sistema per sapere se ci sono modifiche pendenti o meno, infatti IEditableObject non ha nessuna proprietà IsChanged.

 

L’implementazione

L’implementazione dell’interfaccia IRevertibleChangeTracking può essere fatta in svariati modi, partiremo del più semplice, e poco o per nulla riutilizzabile, per approdare ad una soluzione generica che può essere facilmente riutilizzata. Vediamo subito un primo approccio:

class Sample : IRevertibleChangeTracking
{
//un campo privato per conservare il valore originale
private String originalValue = String.Empty;

//il campo privato che fa da backend per la proprietà
private String _value = String.Empty;

//La proprietà pubblica
public String Value
{
get { return this._value; }
set
{
if( value != this.Value )
{
/*
 * Se il valore in arrivo è diverso da
 * quello corrente
 */
if( !this.IsChanged )
{
/*
 * Salviamo il valore corrente solo ed 
 * esclusivamente la prima volta, quindi
 * solo quando IsChanged è false
 */
this.originalValue = this.Value;
}

/*
 * Impostiamo il nuovo valore
 */
this._value = value;
}
}
}

public void RejectChanges()
{
/*
 * Per eseguire il Rollback non facciamo
 * altro che riportare la nostra proprietà
 * al suo valore originale
 */
this._value = this.originalValue;
}

public void AcceptChanges()
{
/*
 * L'accettazione delle modifiche pendenti
 * comporta semplicemente che il valore
 * originale venga sovrascritto con quello 
 * corrente
 */
this.originalValue = this.Value;
}

public bool IsChanged
{
get 
{ 
/*
 * Ci sono modifiche se il valore corrente
 * è diverso da quello orginale
 */
return ( this.Value != this.originalValue ); 
}
}
}

In questo semplice esempio direi che i commenti nel codice sono più che sufficienti per spiegare ogni aspetto, quello che mi preme sottolineare è quanto questo codice sia farraginoso e nella sua semplicità complesso, poco manutenibile e per nulla riusabile: pensate solo alla complessità che ci troveremmo a dover fronteggiare se la nostra classe Sample avesse una decina di proprietà pubbliche.

Cerchiamo a questo punto di capire quali siano i requisiti che un motore che gestisce lo stato interno di un oggetto debba soddisfare:

  • Dobbiamo essere in grado di conservare una lista di modifiche;

  • dobbiamo sapere se abbiamo subito delle modifiche;

  • dobbiamo essere in grado di accettera le modifiche;

  • dobbiamo essere in grado di respingere le modifiche eseguendo un rollback;

Se quindi decidiamo di chiamare il nostro motore Cache potremmo avere una rudimentale firma molto simile a quella che segue:

public abstract class Cache
{

private ICollection _cacheStore;
protected ICollection CacheStore
{
get
{
if( this._cacheStore == null )
{
this._cacheStore = this.OnCreateCacheStore();
}

return this._cacheStore;
}
}

protected abstract ICollection OnCreateCacheStore();

protected Cache()
{

}

public Boolean IsChanged
{
get { return this.CacheStore.Count > 0; }
}

public abstract void AcceptChanges();
public abstract void RejectChanges();
}

Abbiamo la possibilità di sapere se ci sono modifiche (proprietà IsChanged), abbiamo l’opportunità di persistere queste modifiche o di eseguire un rollback (metodi AcceptChanges() e RejectChanges()), infine abbiamo una istanza (proprietà CacheStore) di una classe che implementa ICollection finalizzata proprio a contenere la lista delle modifiche, abbiamo anche in questo caso deciso di lasciare alla classi concrete la scelta del tipo di collezione che verrà realmente implementato.

In questa Cache di base però non abbiamo ancora la possibilità di inserire nuovi elementi: ho deciso di delegare questo lavoro ad una classe che specializza la classe Cache, qui esposta, perchè la metodologia con cui vengono inseriti nuovi elementi è strettamente legata al tipo di elementi che stiamo trattando e al tipo (inteso come System.Type) dell’oggetto che rappresenta la nostra base dati.

Prima di addentrarci nella struttura della cache vera e propria cerchiamo di capire quali siano le caratteristiche di un generico elemento che vi viene inserito:

  • dobbiamo essere in grado di sapere se un elemento gestito dal nostro motore sia quello originale o meno;

  • dobbiamo essere in grado di ripristinare un singolo elemento;

Una possibile definizione potrebbe essere quindi la seguente:

public abstract class CacheItem : IKey
{
protected CacheItem( Boolean isOriginal, Key key )
{
this._key = key;
this._isOriginal = isOriginal;
}

private Boolean _isOriginal;
public Boolean IsOriginal
{
get { return this._isOriginal; }
}

public abstract void Restore();

private Key _key;
public Key GetKey()
{
return this._key;
}
}

Questo prototipo di classe soddisfa entrambi i requisiti esposti.

Facciamo una piccola dissertazione sull’interfaccia IKey: IKey serve per definire una classe che espone una proprietà che può essere considerata come chiave primaria per quell’istanza; IKey è così definita:

public interface IKey 
{
Key GetKey();
}

C’è poi una definizione che fa uso dei Generics al fine di rendere fortemente tipizzato il tipo di chiave primaria esposta.

public interface IKey<T> : IKey where T : IComparable
{
Key<T> Key { get; }
}

La classe Key invece è una semplice classe che implementa IComparable e IConvertible (implementazioni che ometto per ragioni di spazio ma presenti nel codice di esempio allegato all’articolo):

[Serializable]
public abstract class Key : IConvertible, IComparable
{

}

Da questa classe deriva poi la vera e propria chiave primaria anch’essa fortemente tipizzata grazie all’uso dei Generics:

[Serializable]
public class Key<T> : Key where T : IComparable
{
private T value;

public Key() 
{
value = default( T );
}

public Key( T value )
{
if( value == null )
{
this.value = default( T );
}
else
{
this.value = value;
}
}

public override string ToString()
{
return ( this.value == null ) ? "" : this.value.ToString();
}

public T Value
{
get { return this.value; }
}

public static implicit operator Key<T>( T value )
{
return new Key<T>( value );
}

public static implicit operator T( Key<T> pk )
{
if( pk == null )
{
return default( T );
}
else
{
return pk.value;
}
}
}

Anche in questo caso parte dell’implementazione è stata omessa per chiarezza ma è presente nel codice allegato. Lo scopo principale di tutto questo è semplicemente, come già abbiamo detto, di permettere ad un oggetto di esporre una propietà che abbia valenza di chiave primaria senza obbligare l’oggetto a legarsi a filo doppio al tipo di quella proprietà.

Tornando in tema, una possibile implementazione di CacheItem è la seguente:

public sealed class ObjectCacheItem<T> : CacheItem
{
ObjectCacheItemRestoreCallback<T> callback = null;

public ObjectCacheItem( String key, T value, ObjectCacheItemRestoreCallback<T> callback, Boolean isOriginal )
: base( isOriginal, new Key<String>( key ) )
{
this._value = value;
this.callback = callback;
}

public Key<String> Key
{
get { return ( Key<String> )this.GetKey(); }
}

private T _value;
public T Value
{
get { return this._value; }
}

public override void Restore()
{
if( callback != null )
{
callback( this.Value );
}
}
}

Analizziamo subito nel dettaglio l’implementazione che abbiamo appena esposto:

  • l’oggetto ObjectCacheItem è innanzitutto di tipo generico e questo è fondamentale perchè ci permette di stabilire il tipo di dato che verrà contenuto al momento del suo uso senza essere obbligati a legarci ad un semplice System.Object che avrebbe comportato anche tutta una serie di problemi legati al boxing e all’unboxing dei ValueType.

  • ObjectCacheItem espone il valore che memorizza attraverso la proprietà Value;

  • ObjectCacheItem accetta, nel costruttore 4 parametri:

    • Una stringa che rappresenta una chiave univoca che identifica l’elemento che stiamo memorizzando;

    • l’elemento che vogliamo memorizzare;

    • Un valore booleano che determina se l’elemento è la versione originale o una modifica successiva;

    • Un delegato di tipo ObjectCacheItemRestoreCallback così definito:

delegate void ObjectCacheItemRestoreCallback<T>( T value );

    che rappresenta la funzione che verrà chiamata dal metodo Restore() al fine di ripristinare il valore contenuto nell’istanza corrente dell’ObjectCacheItem, tra breve daremo anche una valida motivazione per questa scelta;

ObjectCacheItem espone infine un semplice metodo Restore() che altro non fa altro che invocare il delegato che è stato passato nel costruttore;

Come facciamo ora a memorizzare le eventuali modifiche che vengono apportate ad un’ipotetica classe Person, quella dell’esempio di inzio articolo? Abbiamo innanzitutto bisogno di un contenitore che sia in grado di gestire i nostri ObjectCacheItem(s) e che sia di tipo Cache, cominciamo quindi con il definire una classe ObjectCache:

public class ObjectCache : Cache
{
protected override System.Collections.ICollection OnCreateCacheStore()
{
/*
 * OnCreateCacheStore() viene chiamato dalla classe di
 * base quando ha bisogno di creare lo storage per conservare
 * gli elementi cachati, dobbiamo eseguire l'override di questo
 * metodo per fornire il tipo di storage che più ci aggrada
 */
return new SortedDictionary<String, CacheItem>();
}

public ObjectCache()
: base()
{ 

}

public virtual void Insert<T>( String key, T value, ObjectCacheItemRestoreCallback<T> callback )
{
if( !this.IsCachingSuspended )
{
SortedDictionary<String, CacheItem> dictionary = ( SortedDictionary<String, CacheItem> )this.CacheStore;

if( !dictionary.ContainsKey( key ) )
{
/*
 * Inseriamo in cache solo ed esclusivamente la prima
 * modifica per quell'elemento, questa cache non gestice
 * modifiche multiple sulla stessa Key e quindi non ha
 * neanche senso tracciarle...
 */
ObjectCacheItem<T> item = new ObjectCacheItem<T>( key, value, callback, true );
dictionary.Add( key, item );
}
}
}

public override void AcceptChanges()
{
/*
 * Semplicemente svuota la cache
 * */
( (IDictionary)this.CacheStore ).Clear();
}

public override void RejectChanges()
{
if( this.IsChanged )
{
/*
 * Deve reimpostare l'oggetto allo stato originale. Per fare
 * ciò iteriamo tutti gli elementi presenti in cache e 
 * chiamiamo Restore() su ognuno di essi
 * Lo facciamo in senso opposto in questo modo le proprietà
 * vengo ripristinate in ordine inverso alla sequenza delle 
 * modifiche
 * */
SortedDictionary<String, CacheItem> dictionary = ( SortedDictionary<String, CacheItem> )this.CacheStore;

/*
 * facciamo questo travaso di reference(s) in una List<T> 
 * perchè dictionary.Values *non* implementa IList e 
 * quindi non ha l'indexer per accedere agli elementi
 */
List<CacheItem> values = new List<CacheItem>( ( IEnumerable<CacheItem> )dictionary.Values );
for( Int32 i = values.Count; i > 0; i-- )
{
values[ i - 1 ].Restore();
}

values.Clear();
values = null;

/*
 * Abbiamo ripristinato, svuotiamo la cache
 */
dictionary.Clear();
}
}
}

Vediamo nel dettaglio cosa succede: quando deve essere memorizzata una nuova modifica viene chiamato il metodo Insert() a cui vengono passati una chiave univoca per identificare la modifica, il valore da memorizzare e il delegato che deve essere invocato per ripristinare la modifica. Il metodo Insert(), se la chiave che viene passata non è stata già memorizzata, crea un’stanza di ObjectCacheItem e la aggiunge a quelle che già sta tracciando.

A questo punto le cose si fanno decisamente semplici:

  • per sapere se ci sono modifiche (proprietà IsChanged) ci basta controllare se stiamo o meno tracciando degli elementi quindi controlliamo se CacheStore.Count è maggiore di 0;

  • per accettare le modifiche pendenti (metodo AcceptChanges()) ci limitiamo a eliminare tutte le modifiche pendenti che stiamo tracciando;

  • infine per eseguire un rollback (metodo RejectChanges()) ad uno stato precedente non facciamo altro che chiamare Restore() su ogni elemento che abbiamo precedentemente memorizzato; per garantire consistenza chiamiamo Restore() su ogni elemento in ordine inverso a come sono state inserite le modifiche in questo modo il ripristino della base dati avviene con ordine esattamente contrario all’ordine in cui sono state apportate le modifiche;

Siamo infine arrivati al momento cruciale, il collegamento tra la nostre base dati (la classe Person) e il nostro motore di Cache.

Concediamoci subito un commento positivo: il fatto di essere arrivati fino a questo punto senza aver mai menzionato in che modo la base dati si relaziona con la Cache ci garantisce che l’incapsulamento è stato rispettato e ci garantisce anche che la portabilità della soluzione è massima.

Vediamo quindi l’implementazione della classe Person:

class Person : IRevertibleChangeTracking, INotifyPropertyChanged
{
[NonSerialized]
private ObjectCache _innerCache;

protected ObjectCache InnerCache
{
get
{
if( this._innerCache == null )
{
this._innerCache = new ObjectCache();
}

return this._innerCache;
}
}

La classe Person è proprietaria della sua Cache, ne detiene un’istanza che viene inizializzata al primo uso, seguento il ben noto pattern LazyLoading.

public void RejectChanges()
{
if( this.IsChanged )
{
this.InnerCache.RejectChanges();
}
}

public void AcceptChanges()
{
if( this.IsChanged )
{
this.InnerCache.AcceptChanges();
}
}

public bool IsChanged
{
get { return ( this._innerCache != null && this.InnerCache.IsChanged ); }
}

L’implementazione di IRevertibleChangeTracking è decisamente semplice e non fa nulla di più che delegare alla Cache le richieste fatte all’istanza della classe Person.

La classe implementa anche l’interfaccia INotifyPropertyChanged per rendere bidirezionale il supporto al data binding, l’interfaccia in questione non è necessaria ai fini dell’implementazione della Cache e si rimanda a questo tip per eventuali approfondimenti.

const String FIRSTNAME_KEY = "FirstName";
private String _firstName;
public String FirstName
{
get { return this._firstName; }
set
{
if( this._firstName != value )
{
this.InnerCache.Insert<String>
( FIRSTNAME_KEY, this._firstName, new ObjectCacheItemRestoreCallback<String>( FirstNameRestoreCallback ) );
this._firstName = value;
this.OnPropertyChanged( new PropertyChangedEventArgs( FIRSTNAME_KEY ) );
}
}
}

void FirstNameRestoreCallback( String value )
{
this._firstName = value;
this.OnPropertyChanged( new PropertyChangedEventArgs( FIRSTNAME_KEY ) );
}

Analizziamo infine una delle proprietà pubbliche esposte dalla classe Person: FirstName.

FirstName sul setter verifica che il valore in arrivo sia diverso da quello corrente, nel qual caso procede con la memorizzazione in cache di quello attuale prima di procedere alla sostituzione; da notare che al metodo Insert() della classe ObjectCache viene passato anche il delegato che punta alla funzione che si deve occupare di ripristinare una modifica a seguito di una chiamata a RejectChanges().

Ho deciso di implementare il Restore() in questo modo e di non delegare alla Cache questa operazione perchè ritengo che l’unico che conosca quali siano i passaggi corretti per eseguire un rollback sia solo ed esclusivamente l’oggetto Person stesso.

Facciamo un esempio un po’ più complesso che però ci permette di chiarire meglio questo punto: immaginiamo che la classe Person esponga, sotto forma di proprietà pubblica, un riferimento ad un’altro oggetto (ad esempio Child) che non è un value type ma bensì un reference type, nel momento in cui vogliamo memorizzare in Cache la sostituzione del Child potrebbe essere sensato memorizzare solo l’ID del Child che viene sostituito e non tutta l’istanza; a questo punto è chiaro che non è possibile delegare alla Cache il ripristino dell’istanza precedente di Child perchè la Cache non ha nessuna nozione di come trasformare un ipotetico ID in un’istanza della classe Child.

 

Conclusioni

Abbiamo visto quanto possa essere semplice realizzare un motore che gestisce la cache delle modifiche apportate ad un oggetto a prescindere dal tipo dell’oggetto che lo implementa. Una volta implementato il tutto siamo in grado di soddisfare al primissimo requisito che ci siamo posti ad inizio articolo, siamo cioè in grado di scrivere una cosa di questo tipo:

Person p = new Person();

Console.WriteLine( p.IsChanged ); //questo stamperà: false

p.FirstName = "Mauro";
p.LastName = "Servienti";

Console.WriteLine( p.IsChanged ); //questo stamperà: true

Ma siamo andati oltre abbiamo anche implementato un sistema che ci consente di eseguire un rollback delle modifiche apportate all’istanza del nostro oggetto.

Le possibili evoluzioni ed implementazioni di questo sistema sono molte come ad esempio:

  • gestire lo stato delle modifche apportate ad una collection, al fine di sapere se un elemento è stato rimosso, spostato o aggiunto alla stessa stregua di quello che la DataTable fa con le sue DataRow;

  • gestire una Cache progressiva che ci consenta di scorrere le modifiche in avanti e indietro come fanno ad esempio molti editor di testo;

Possiamo però dire che la strada è stata aperta e il percorso iniziato.

Risorse:

System.ComponentModel.IRevertibleChangeTracking (MSDN)

System.ComponentModel.INotifyPropertyChanged (MSDN)

Allegati:

http://downloads.topics.it/msdn/IRevertibleChangeTracking /IRevertibleChangeTracking.zip