Radical 1.0.1 (Vacuum): Memento

Print Content | More

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<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:

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


Radical , Silverlight , Windows Phone 7

7 comments

Related Post

  1. #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. #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. #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. #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. #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 canExecute, Action execute)
    {
    this.canExecute = canExecute;
    this.execute = execute;
    }

  6. #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. #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