Radical 1.0.1 (Vacuum): Memento
Posted by Mauro in Software Mason on Saturday 19 June 2010 at 3:09 PM
CodePlex Release: http://radical.codeplex.com/releases/view/46757
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:
<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>
Si limita a visualizzare le informazioni e ad esporre 2 dei comandi, tra quelli disponibili, per la gestione del Memento: Undo e Redo.
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;
Ok, ma dove sta il barbatrucco?
MainPageViewModel
L’applicazione è fatta così:
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; } }
Nulla di trascendentale:
- 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;
Cosa succede in fase di inizializzazione?
PersonViewModel
Anche qui in apparenza nulla di particolare se non per la sintassi un po’ esoterica :-):
public 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 ); } } }
MementoEntity funziona in maniera molto simile ad un DependencyObject offre quindi un sistema di storage basato su un Distionary<String, PropertyValue> che la stessa MementoEntity, in realtà Entity da cui MementoEntity eredita, utilizza per capire lo stato dell’entità e gestire le variazioni di stato.
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:
Le “entity proprerty” sono infine corredate da metadati che descrivono alcuni possibili comportamenti:
IChangeTrackingService
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









#1 da Davide Friday June 2010 alle 02:32
Ciao,
una domanda, e scusami se ti sembra "stupida" ;-) Le eventuali modifiche che effettui sul personviewmodel come le riporti sulla classe "originaria" person? Vengono effettuate in automatico?
Ciao e grazie,
Davide
#2 da Mauro Servienti Friday June 2010 alle 02:48
Tutto tranne che stupida :-) la risposta è dipende, ho un meccanismo automatico per i casi semplici e uno manuale per i casi complessi.
Tutto è argomento di un prossimo set di post.
.m
#3 da raffaeu Friday June 2010 alle 09:07
Ciao Mauro, posso chiederti quale e' la differenza principale da Radical 1.0 RTM e la version 1.0.1 Vacuum?
Quale mi consigli di iniziare a studiare per prima, per quel che riguarda MVVM?
#4 da Mauro Servienti Friday June 2010 alle 10:05
@Raffaeu: in questo momento la mia roadmap è di avere una v1.1 che porti in Silverlight e Windows Phone 7 tutto c0iò che c'è di portabile, a oggi Vacuum fissa alcuni bug della versione desktop e introduce il supporto per Silverlight e WP7.
Quindi puoi tranquillamente prendere Vacuum e saltare le parti che non ti interessano.
.m
#5 da raffaeu Tuesday June 2010 alle 01:39
Ci sono davvero tante cose interessanti. Sinceramente c'e' davvero 'tanta roba' ... ma per ora per di focalizzarmi sul Namespace Windows.
L' unica cosa che non riesco a trovare e' una tua implementazione di ICommand. Cosa passi, una cosa del genere nel tuo BaseCommand:
public BaseCommand(Predicate
#6 da Mauro Servienti Tuesday June 2010 alle 07:44
Ciao Raffaeu,
si chiama DelegateCommand: "Radical.Windows" "Presentation.Input", se non ricordo male, come folder nel progetto
.m
#7 da raffaeu Thursday June 2010 alle 01:19
A rigrazie! Mi interessa moltissimo la parte in cui gestisci i WeakEvent.
Ad avere tempo mi piacerebbe farci su un po' di docs e una piccola applicazioncina in stile NSK.
"Ad avere tempo ..." :-D