Ne abbiamo parlato non poco sia io che Corrado e l’argomento è stato ripreso durante il corso su Wpf che abbiamo tenuto questa settimana, a proposito:
trovo che sia decisamente produttivo, in particolare per la platea, partecipare ad un corso dove la docenza è in pair, con 2 docenti, con un background tecnico paragonabile, ma con un background lavorativo diverso perchè porta un arricchimento non da poco.
…torniamo “in topic” che è meglio :-)
L’obiettivo è poter avere, in Blend, questo:
image
Quindi semplicemente una design time experience degna del suo nome. L’inghippo, nel mio caso, è che usando M-V-VM, seguendo l’approccio ViewModel first, il ViewModel è fatto più o meno così:
sealed class SubjectsListViewModel :
    ItemsListViewModel<ISubjectsListView, Subject, Subject, Subject>,
    ISubjectsListViewModel
{
    public SubjectsListViewModel( IDispatcher dispatcher, 
ISubjectsListView view,
IMessageBroker broker,
IDataContextFactory dataContextFactory,
ISubjectsManagementService subjectsService,
IUserInteractionService uiService ) : base( dispatcher, view, broker, SelectionType.Extended ) {

...
}
}
il tutto viene gestito dal fido Castle quindi quel costruttore per me è decisamente friction-less, in ottica design time data però quella cosa è decisamente scomoda principalmente per 2 motivi:
  1. la classe non è pubblica e lo xaml si lamenta se cercate di usarla;
  2. la classe non ha un costruttore pubblico di default senza parametri e questo la rende definitivamente inutilizzabile via xaml;
L’approccio che ho usato fino ad ora funziona senza nessun problema ma è dispendioso in termini di tempo di setup… e questo non è friction-less… allora il cervelletto ravana ravana e stamattina durante il solito viaggio sul lebbrosario di turno mi è venuta una malsana idea… mi sono detto:
  1. perchè non riprendi quello che hai lasciato in sospeso…?
  2. e lo adatti a questo scenario che è decisamente più semplice?
Detto fatto, una mezz’ora e il giochetto era up & runnin’:
<Window x:Class="SubjectsListViewDocument"
        xmlns:design="http://schemas.topics.it/wpf/subjects/design"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d design">

    <Window.Resources>
        <design:Sample x:Key="sample" />
    Window.Resources>

    <Grid d:DataContext="{StaticResource sample}">
quella classe “Sample” è così definita, scroccando un po’ di sintassi a Fluent NHibernate:
public class Sample : DesignTimeHost<ISubjectsListViewModel>
{
    public Sample()
    {
        this.Expose( o => o.Items )
            .WithValue( new EntityView<Subject>( new List<Subject>()
            {
                new Person(){ FirstName = "Mauro", LastName = "Servienti" },
                new Company(){ CompanyName = "topics.it" }
            } ) )
            .AsReadOnly();
    }
}
e quello che mi resta da fare è limitarmi a fare quello che farei di solito (funziona perfettamente anche nei vari “property editor” di Blend):
<ListView ItemsSource="{Binding Path=Items}"...
Ottenendo il risultato di cui sopra, comodo semplice e definibile addirittura in un assembly separato. Complicandomi la vita ho cercato di esporre come design time data un modello ben più complesso (un master detail con un grafo decisamente ostico) incappando nei “solitinon pochi problemi che però stavolta possono essere brillantemente risolti:
var address1 = MockRepository.GenerateStub<IAddressViewModel>();
address1.Street = "sample street 1";

var address2 = MockRepository.GenerateStub<IAddressViewModel>();
address2.Street = "sample street 2";

this.Expose( e => e.Addresses )
    .AsReadOnly()
    .WithValue( new EntityView<IAddressViewModel>( new List<IAddressViewModel>()
    {
        address1,
        address2
    } ) );
utilizzando un tool di mocking, del resto siamo a design time, e producendo di fatto qualcosa di editabile in maniera umana, molto umana:
image
Finalmente perchè prima quel coso era ridotto ad un groviglio inguardabile, e adesso posso farlo diventare quello che serve diventi.
Adesso la domanda potrebbe essere: ma visto che funziona anche con un tool di mocking perchè non usare solo quello?
Cerca le differenze… :-)
image image
La cosa interessante è che, per avere a design time quel comportamento, ci possiamo limitare solo ed esclusivamente a questo:
<design:Sample x:Key="sample" DesignTimeAreDisplayedDataStale="[False]/[True]" />
Nonostante quel “pannello di warning” sia così definito:
<Border Grid.Row="0"
        Background="#FFFFFB9C"
        Visibility="{Binding AreDisplayedDataStale, Converter={StaticResource boolVisConv}}"
        BorderBrush="#FFFFF630"
        BorderThickness="1"
Quella proprietà “DesignTimeAreDisplayedDataStale” non esiste sul modello, e quindi non esisterebbe per il tool di mocking, ma esiste per il DesignTimeHost che è anche in grado di notificarne il cambiamento:
public sealed class Sample : DesignTimeHost<ISubjectsListViewModel>
{
    public Sample()
    {
        this.Expose( o => o.Items )
            .WithValue( new EntityView<Subject>( new List<Subject>()
            {
                new Person(){ FirstName = "Mauro", LastName = "Servienti" },
                new Company(){ CompanyName = "topics.it" }
            } ) )
            .AsReadOnly();

        this.Expose( o => o.AreDisplayedDataStale )
.BoundTo( this, s => s.DesignTimeAreDisplayedDataStale )
.WithLiveValue( () => this.DesignTimeAreDisplayedDataStale ) .AsReadOnly(); } private Boolean _designTimeAreDisplayedDataStale = false; public Boolean DesignTimeAreDisplayedDataStale { get { return this._designTimeAreDisplayedDataStale; } set { if( value != this.DesignTimeAreDisplayedDataStale ) { this._designTimeAreDisplayedDataStale = value; this.OnPropertyChanged( e => e.AreDisplayedDataStale ); } } } }
Quindi:
  • la proprietà AreDisplayedDataStale, esposta dal modello, è registrata con un live value, un delegato, e non con un valore statico come le altre;
  • viene aggiunta una proprietà DesignTimeAreDisplayedDataStale che non esiste sul modello e che al momento della variazione notifica il suo cambiamento come una normalissima proprietà…;
  • …proprietà che il DesignTimeHost tiene monitorata (BoundTo()) e, al momento della variazione, notifica a sua volta la variazione della proprietà AreDisplayedDataStale
…a questo punto il buon Blend, che ha istanziato il nostro DesignTimeHost, reagisce alla notifica e viene a rileggersi il valore della proprietà che non esiste invocando di fatto il delegato che rappresenta il live-value… si lo so sembra cervellotico ma funziona che è un piacere. La cosa più bella è che questo mi permette anche di simulare lo stato di abilitazione / disabilitazione degli ICommand esposti dal ViewModel.
Concludendo…
Con pochissimo codice siamo in grado di dare a Blend un set di dati, da usare a Design Time, anche molto complesso permettendoci di avere una Design Time Experience veramente di qualità.
.m