RadicalCodePlex 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:
image
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:
image
ViewModel che sarà esposto alla View dal ViewModel dell’applicazione:
image
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:

image image image image
  1. All’avvio non ci sono modifiche pendenti quindi Undo e Redo non sono disponibili;
  2. modificando sia il cognome che il nome, basta anche una sola modifica, il tasto Undo diventa attivo;
  3. premendo Undo l’ultima modifica effettuata, la modifica del nome, viene rollbeccata;
  4. a questo punto si ativa anche il tasto Redo;
  5. 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 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:
image
Le “entity proprerty” sono infine corredate da metadati che descrivono alcuni possibili comportamenti:
image
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