Radical 1.0.1 (Vacuum): Memento
Ne avevo già parlato tempo addietro e mi ero pure ripromesso di fare una lunga serie post sull’argomento, poi come al solito il lavoro, l’accumularsi delle cose e le novità impellenti mi hanno fatto deviare… speriamo che sia l’occasione buona per riprendere quella serie.
Radical offre out-of-the-box un sistema di change tracking che vi consente di tenere traccia dello stato di un intero grafo con un costo di implementazione decisamente basso. Ora questo motore è disponibile anche per Silverlight e Windows Phone 7.
Vediamo un esempio utilizzando come base un’applicazione Silverlight, banalissima, basata su Model View ViewModel; come da tradizione avremo un bel modello:
Che mega sforzo direte voi… :-) Teniamo basso il profilo che già in questo post di carne al fuoco ne mettiamo in quantità industriale :-P
Parallelamente abbiamo anche una prima parte del ViewModel per il nostro amato trittico:
ViewModel che sarà esposto alla View dal ViewModel dell’applicazione:
La View infine è altrettanto banale:
Si limita a visualizzare le informazioni e ad esporre 2 dei comandi, tra quelli disponibili, per la gestione del Memento: Undo e Redo.<Grid> <TextBox Height="23" Text="{Binding Path=Entity.FirstName, Mode=TwoWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="156" /> <TextBox Height="23" Text="{Binding Path=Entity.LastName, Mode=TwoWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="156" /> <Button Content="Undo" Command="{Binding Path=UndoCommand}" Height="23" HorizontalAlignment="Left" VerticalAlignment="Top" Width="75" /> <Button Content="Redo" Command="{Binding Path=RedoCommand}" Height="23" HorizontalAlignment="Left" VerticalAlignment="Top"
Width="75" /> Grid>
Una volta mandato in esecuzione il tutto quello che otteniamo è una sequenza di questo tipo:
- All’avvio non ci sono modifiche pendenti quindi Undo e Redo non sono disponibili;
- modificando sia il cognome che il nome, basta anche una sola modifica, il tasto Undo diventa attivo;
- premendo Undo l’ultima modifica effettuata, la modifica del nome, viene rollbeccata;
- a questo punto si ativa anche il tasto Redo;
- Premendo Redo viene ripristinato lo stato precedente e quindi il nome è nuovamente modificato;
MainPageViewModel
L’applicazione è fatta così:
Nulla di trascendentale:public class MainPageViewModel : AbstractViewModel { readonly IChangeTrackingService service = new ChangeTrackingService(); public MainPageViewModel() { var observer = MementoObserver.Monitor( this.service ); this.UndoCommand = DelegateCommand.Create() .OnCanExecute( o => this.service.CanUndo ) .OnExecute( o => this.service.Undo() ) .AddMonitor( observer ); this.RedoCommand = DelegateCommand.Create() .OnCanExecute( o => this.service.CanRedo ) .OnExecute( o => this.service.Redo() ) .AddMonitor( observer ); var person = new Person() { FirstName = "Mauro", LastName = "Servienti" }; var entity = new PersonViewModel(); this.service.Attach( entity ); entity.Initialize( person, false ); this.Entity = entity; } private PersonViewModel _entity = null; public PersonViewModel Entity { get { return this._entity; } private set { if( value != this.Entity ) { this._entity = value; this.OnPropertyChanged( () => this.Entity ); } } } public ICommand UndoCommand { get; private set; } public ICommand RedoCommand { get; private set; } }
- abbiamo dichiarato e inizializzato il motore di change tracking, ci sono n pattern possibili per gestire questa cosa, diciamo che tipicamente il ciclo di vita di un sistema di change tracking è molto vicino al ciclo di vita di un DataContext;
- Nel costruttore inizializziamo i due comandi Undo e Redo agganciando un trigger che ne forzi la rivalutazione ogni quavolta il motore di change tracking notifica che ci sono state delle variazioni di stato;
- Creiamo un’istanza di Person, che ad esempio potrebbe arrivare da un repository;
- Creiamo un’istanza del PersonViewModel, anche qui ci sono n modi per ottenerla;
- Chiediamo al motore di change tracking di prendersene cura :-)
- infine inizializziamo e esponiamo la nostra istanza;
PersonViewModel
Anche qui in apparenza nulla di particolare se non per la sintassi un po’ esoterica :-):
MementoEntity funziona in maniera molto simile ad un DependencyObject offre quindi un sistema di storage basato su un Distionarypublic class PersonViewModel : MementoEntity { public void Initialize( Person person, Boolean registerAsTransient ) { if( registerAsTransient ) { this.RegisterTransient(); } this.SetInitialPropertyValue( () => this.FirstName, person.FirstName ); this.SetInitialPropertyValue( () => this.LastName, person.LastName ); } public String FirstName { get { return this.GetPropertyValue( () => this.FirstName ); } set { this.SetPropertyValue( () => this.FirstName, value ); } } public String LastName { get { return this.GetPropertyValue( () => this.LastName ); } set { this.SetPropertyValue( () => this.LastName, value ); } } }
Sintassi e internals
- SetInitialPropertyValue: imposta il valore iniziale di una proprietà e serve, internamente, da punto di riferimento per capire quando il valore di una proprietà cambia o ritorna allo stadio di valore iniziale;
- SetPropertyValue e GetPropertyValue: sono rispettivamente il set e il get accessor specializzati per accedere al sistema di storage interno di Entity/MementoEntity;
- RegisterTransient: ha lo scopo di informare il motore di change tracking che l’entità che sta gestendo è transiente;
Internamente lo storage è un dictionary la cui chiave è il nome della proprietà e il cui valore è un’istanza di PropertyValue, perchè PropertyValue e non Object? per evitare le operazioni di boxing e unboxing dato che tipicamente le proprietà esposte da un entity sono dei value type:IChangeTrackingService
Le “entity proprerty” sono infine corredate da metadati che descrivono alcuni possibili comportamenti:
Ma se tutto fosse “così semplice” non saremmo in grado di tenere traccia dello stato di un grafo, probabilmente avrete già notato la chiamata ad IChangeTrackingService.Attach( IMemento ) che ci fa intuire che il sistema di change tracking sia in grado di tracciare n oggetti contemporaneamente (collection comprese) gestendo correttamente lo stack delle modifiche e la loro sequenzialità.
Quindi?
Quindi succede che il property system esposto da Entity serve per avere un sistema interrogabile che contiene tutti i valori attuali delle proprietà senza bisogno di conoscere lo schema della classe concreta (PersonViewModel nel nostro esempio) mentre la gestione dello stack delle modifiche viene demandata sempre e comunque al change tracking service a cui l’entità è attached, in questo modo lo storage delle modifiche (IChange) è centralizzato e facilmente gestibile in qualsivoglia punto si abbia accesso al change tracking service corrente.
A questo punto è ovvio, ovvio de che direte voi… :-), che la gestione dello stato di un grafo si può ridurre a 2 banali chiamate ad IChangeTrackingService.Undo() e/o IChangeTrackingService.Redo(), ma anche a AcceptChanges() o RejectChanges() per fare altre cose abbastanza ovvie… oppure, decisamente meno ovvio a IChangeTrackingService.GetAdivisory()… di cui parleremo prossimamente.
Stay tuned & happy change tracking :-)
.m