IChangeTrackingService
Fatta questa doverosa premessa veniamo a quello che successe: un bel giorno una delle ragazze che lavoravano in contabilità da un cliente mi chiese (parlando dell'applicazione che avevo sviluppato e che lei utilizzava): "ma non ci sarebbe la possibilità di avere l'avanti e indiero come in Word?" la pima reazione fu un grosso punto di domanda... avanti e indietro?? ma che è... indagando poi scoprii che la ragazza si riferiva alle funzionalità di Undo/Redo offerte da Office.
La cosa mi incuriosì non poco e comincia a studiare il "meccanismo" (aka "pattern") che gestiva la cosa e la prima, ed unica veramente problematica, considerazione a cui giunsi era che per Word la cosa era "relativamente semplice" perchè Word sapeva molto bene cosa stava gestendo, e cioè il documento, mentre la mia velleità era quella di realizzare un motore che fosse in grado di maneggiare un generico grafo di oggetti.
La prima implementazione di tutto vide la luce qualche anno fa, e la ragazza della contabilità fu felice, realizzata con il fx 1.0. Era qualcosa di decisamente embrionale erano le stesse entities che tenevano traccia delle variazioni (o meglio solo della prima variazione) che subivano:
public string Company
{
get { return this._company; }
set
{
if(base.ObjectCache["Company"] == null)
base.ObjectCache["Company"] = this._company;
this._company = value;
}
}
Avevo semplicemente un dictionary dove il nome della proprietà era la chiave e il value il valore, veniva cachata solo ed esclusivamente la prima modifica e nulla di più. L'entity esponeva poi metodi e proprietà per giocare con la cache:
- AcceptChanges();
- AcceptChanges( Boolean recursive );
- RejectChanges();
- RejectChanges( Boolean recursive );
- GetHasChanges();
- GetHasChanges( Boolean recursive );
Adesso mi vengono i brividi ;-) ma 6 e più anni fa faceva la sua porca figura... il tempo e le necessità mi hanno fatto evolvere verso qualcosa di decisamente più funzionale, e finalmente oggi, anche molto più bello dal punto di vista del design.
Partiamo dai requisiti che possono essere sintetizzati in un una parola: "Memento" (che non è solo un bellissimo film).
I problemi:
- gestiamo un grafo non necessariamente noto a priori;
- gestiamo un grafo che può essere internamente gestito con il "lazy loading" e non vogliamo che il modello venga caricato tutto solo perchè dobbiano gestirne le variazioni di stato;
- vogliamo poter distinguere n track line per gestire in contemporanea grafi differenti senza che le modifiche vengano mischiate: faccio prima a fare un esempio che ha spiegare: se in VS fate modifiche su file diversi tutte le modifiche vengono tracciate ma, anche se le fate un po' qua e un po' la in maniera casuale, quando fate Undo viene fatta la cosa giusta sul file giusto ma soprattutto se fate Undo su un documento n volte vengono ripristinate solo le modifiche a quello specifico file anche se durante l'editing le avete mischiate, in quanto ad ordine, a quelle di un altro file.
Il framework 2.0 introduce 2 nuove interfacce IChangeTracking e IRevertibleChangeTracking, peraltro inutilizzate dal fx stesso, che servono per gestire proprio un motore di caching. La cosa fuorviante in questo caso è MSDN stessa che, per IChangeTracking ad esempio, recita:
"Defines the mechanism for querying the object for changes and resetting of the changed status."
Lasciando supporre che sia la entity a dover fornire/gestire queste informazioni.
In questo periodo ci si stava ponendo però un potenziale scenario in cui avremmo sprecato un sacco di risorse (cicli CPU, non ore/uomo) inultilmente proprio perchè la gestione dello stato era delegata alla stessa entity. Non mi dilungo sui motivi, sto già scrivendo troppo oggi... ;-) avremo l'occasione per parlarne presto.... e chi ha orecchie per intendere intenda.
Dato questo incipit in questo week-end diluviante mi sono messo d'impegno e ho cercato di capire cosa potevo fare, la prima cosa è stato come sempre guardare il resto del mondo: in molti usano l'interfaccia INotifyPropertyChanged, o meccanismi simili proxando l'interfaccia pubblica di una entity, ma devo dire che a me piace veramente poco:
- obbliga/permette di gestire le sole proprietà pubbliche;
- il valore cachato è necessariamente quello esposto mentre potrebbe essere necessario customizzare questo processo;
- in fase di "rollback/undo" è decisamente complesso capire, dall'interno della entity, che il set di una property è dovuto al rollback e non ad altro e questo è necessario per evitare di triggerare ancora il motore di caching portando a ricorsione;
- va ancora ancora benino finchè stiamo tentando di tracciare le modifche ad una singola entity molto semplice, la classica Person, ma se pensiamo ad un grafo complesso, ad esempio un Customer con Orders e relativi OrderItems e Products, Addresses ed altro l'interfaccia INotifyPropertyChanged è decisamente inadeguata;
using( TransactionScope ts = new TransactionScope() )
{
//Do something
ts.Complete();
}
qualsiasi operazione eseguiamo o oggetto istanziamo, all'interno del blocco using, è in grado di eseguire l'enlistment nella transazione in maniera automatica senza che ci sia passaggio di alcunchè tra il blocco di codice di esempio e gli oggetti che vengono usati, a qualsiasi livello si trovino. Altra cosa interessante è che se la transazione non c'è, perchè ad esempio non abbiamo creato un oggetto TransactionScope, il codice funziona perfettamente... e non è che sia poi così scontato ;-)
Quello a cui sono giunto è questo:
IChangeTrackingServiceProvider provider = ChangeTrackingServiceProvider.GetCurrent();
provider.CreateTrackingService();
IList
Person p = new Person();
p.FirstName = "Mauro";
p.LastName = "Servienti";
list.Add( p );
IChangeTrackingService svc = provider.GetTrackingService();
if( svc.IsChanged )
{
svc.RejectChanges();
}
Perchè il tutto funzioni non è necessario implementare nessuna interfaccia sulle entity che si stanno realizzando è però necessario sacrivere un minimo di codice per "collegare" i 2 mondi. Nell'esempio per la collection viene utilizzata un EntityCollection
Ecco quello che succede in esecuzione:
Non uso un blocco using, anche se IChangeTrackingService implementa IDisposable, perchè il ciclo di vita della entity, e quindi la sua gestione dello stato tendono ad andare oltre lo scope del singolo snippet di codice. Ogni metodo esposto dall'interfaccia IChangeTrackingServiceProvider ha svariati overload che permettono di raffinare/personalizzare il comportamento del motore di caching. Una volta recuparata uan reference all'IChangeTrackingService corrente oltre ad accettare o rifiutare (AcceptChanges() o RejectChanges()) è possibile eseguire un "Undo" progressivo delle modifiche apportate, banalmente una cosa del tipo:
while( svc.IsChanged )
{
svc.Undo();
}
E' infine possibile chiedere all'IChangeTrackingService di restituirci un IChangeSet che è l'insieme delle modifiche che sta tracciando.
IChangeSet cSet = svc.GetChangeSet();
Il metodo GetChangeSet() ha, per ora, un overload che accetta un IChangeSetFilter che è un oggetto che permette di "filtrare" dall'esterno quali modifiche debbano essere incluse nel changeset che si sta costruendo. Questa possibilità è ancora molto grezza ed è legata ad una possibile collaborazione con qualcosa che implementi il pattern "Unit Of Work" (vedasi NSK per i dettagli) al fine di poter scrivere una cosa del tipo:
using( IUnitOfWork uow = IServiceContainer.GetService
{
uow.Append( cSet );
uow.Commit();
cSet.AcceptChanges();
}
Anche se già funzionante la sua implementazione interna lascia molto a desiderare.
Lascio ad un futuro post i dettagli implementativi del tutto... per oggi ho già intasato troppo il muro di UGI.
.m