In questi giorni mi sono imbattuto in una classica domanda di chi passa da Windows Form a WPF e cioè: “User Control WPF, come si fa?” (Racconto liberamente ispirato da un thread aperto sul forum di DotNetHell).
La mia risposta è stata quella che potete leggere nel thread e in sostanza ci porta all’oggetto di questo articolo, la UI Composition.
Per evitare di rispiegare concetti che già si trovano in rete e che probabilmente storpierei, vi rimando ancora una volta ai post di Mauro Servienti (che dopo tutta questa pubblicità mi deve una birra!!)
· UI Composition :: Thread.Start() – Primi concetti sulla UI Composition
· UI Composition :: IndexOf() – Concetti parte 2
· UI Composition :: La Shell
· UI Composition :: Il processo di discovery
· UI Composition :: RegionService, RegionManager(s) & Region(s)
· UI Composition :: Astrazione... (intermezzo)
· UI Composition :: IMessageBroker, Castle Windsor e le facility
· UI Composition :: "Navigation"
· UI Composition :: Una “region” dentro un Popup
· UI Composition :: I Moduli
E poi i post legati a Radical, con i concetti applicati e a volte anche rivoluzionati, per andare in contro alle reali esigenze:
· Radical - Il dado è tratto…
· Radical – UI Composition: concepts
· Radical – UI Composition: inject content at runtime
· Radical.Windows.Presentation: bootstrap conventions
· Radical.Windows.Presentation: runtime conventions
E poi la documentazione ufficiale su CodePlex, che però non è completa, ed è questo che mi ha spinto a creare qualche tutorial legato a questo framework:
· Radical Documentation on GitHub
WPF UI Composition in Action
In questo progetto di esempio andremo a creare un applicazione che riproduce a grandi (grandissime) linee la schermata Start introdotta in Windows 8, quindi un applicazione con un menu a tutto schermo, che ti permette di aprire le varie funzionalità (app), una alla volta, a tutto schermo, in modalità single-app esattamente come in Windows 8. Le funzionalità/app saranno sviluppate come viste (UserControl) che verranno visualizzate utilizzando le potenzialità della UI Composition di Radical, sfruttando a pieno le Region. All’interno di queste app vedremo poi come sia possibile sfruttare altri meccanismi di UI Composition, in modo da coprire la maggior parte dei metodi con cui Radical ci viene in aiuto.
Ok lo so, non assomiglia per niente a Windows 8, ma l’idea è quella, e con WPF vi garantisco che è possibile arrivare allo stesso identico risultato, con la maggior parte dell’interfaccia scritta in XAML, senza code-behind da impazzire.
Prima di iniziare sappiate che, questo tutorial è stato creato in modo che al termine di ogni punto, sia possibile eseguire l’applicazione e vederne lo stato di avanzamento, lasciandovi il tempo di soffermarvi e riflettere su quello che si è creato, per poi proseguire al punto successivo. Non vi consiglio di scaricare il progetto finito perché credo si possano acquisire più concetti creandoli di mano propria anche se si è fortemente guidati.
1 – Creare il progetto base MVVM con Radical
Seguire il precedente post “[WPF] – Tutorial: MVVM in un Minuto con Radical”, chiamando il progetto “WpfMvvmUICompositionInAction”.
Ricordatevi di ereditare da AbstractViewModel sulla classe MainViewModel, altrimenti inserendo la region senza implementare IViewModel, si ha un eccezione.
2 – Aggiungere nella cartella “Presentation” la classe helper “ShellKnownRegions.cs”
Questa classe contiene tutte le regioni conosciute, cioè tutte quelle parti, solitamente della shell principale, che sono conosciute a priori, e che nell’applicazione ci si potrà riferire direttamente sapendo che esistono e sono sempre visibili.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfMvvmUICompositionInAction.Presentation
{
public class ShellKnownRegions
{
public static string MainContentRegion = "MainContentRegion";
}
}
La nostra shell è particolarmente semplice, perchè composta da una sola regione centrale a tutto schermo (vedi punto 5). Di seguito un esempio di una shell applicativa con qualche contenuto in più:
3 – Aggiungere nella cartella "Presentation” la classe helper “KnownApps.cs”
Questa classe è un semplice enum che elenca tutte le “app” a disposizione dell’utente:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfMvvmUICompositionInAction.Presentation
{
public enum KnownApps
{
CompositeApp,
CommonApp
}
}
4 – Aggiungere la StartScreen
All’interno della cartella “Presentation”, aggiungere due nuovi items che rappresenteranno lo start screen dell’applicazione:
· Un item di tipo UserControl (WPF) chiamato “ShellStartScreenView.xaml”
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.ShellStartScreenView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:pr="clr-namespace:WpfMvvmUICompositionInAction.Presentation"
mc:Ignorable="d"
d:DesignHeight="397" d:DesignWidth="750">
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<WrapPanel Margin="5,5,5,0" Orientation="Vertical">
<WrapPanel.Resources>
<Style TargetType="Button">
<Setter Property="Width" Value="248"/>
<Setter Property="Height" Value="120"/>
<Setter Property="Margin" Value="0,0,5,5"/>
<Setter Property="Content" Value="I'm a tile, trust me!"/>
<Setter Property="Command" Value="{Binding Path=OpenView}"/>
</Style>
</WrapPanel.Resources>
<Button Content="Composite App" CommandParameter="{x:Static pr:KnownApps.CompositeApp}" />
<Button Content="Common App" CommandParameter="{x:Static pr:KnownApps.CommonApp}" />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
<Button />
</WrapPanel>
</ScrollViewer>
</Grid>
</UserControl>
· Una classe per il ViewModel associato “ShellStartScreenViewModel.cs”, per ora vuoto.
Il prefisso “Shell” è importante, perché è una keyword che fa parte delle convenzioni Radical, e serve ad istruire il dependency container di radical (che usa il castle windsor container), a registrare la View e il suo ViewModel come singleton. Stessa cosa vale per il prefisso “Main”. Al contrario le altre classi sotto il namespace Presentation che finiscono per *View (quindi View e ViewModel), vengono registrate nel container come transient, e quindi ogni volta che vengono richieste, una nuova istanza viene creata.
Per la spiegazione precisa delle convenzioni radical, riferirsi all’articolo “bootstrap conventions”; mentre per le differenze di registrazione del componente nel container (singleton, transient) riferirsi direttamente alla documentazione dei Lifestyle delcastle windsor.
5 – Aggiungere la Region principale della Shell
Nella window “MainView.xaml” aggiungere il ContentPresenter con “attaccato” il behaviour RegionService di Radical, per renderlo a tutti gli effetti una Region:
<Window x:Class="WpfMvvmUICompositionInAction.Presentation.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:rg="http://schemas.topics.it/wpf/radical/windows/presentation/regions"
xmlns:pr="clr-namespace:WpfMvvmUICompositionInAction.Presentation"
Title="MainView" Height="600" Width="800">
<ContentPresenter rg:RegionService.Region="{rg:ContentPresenterRegion {x:Static pr:ShellKnownRegions.MainContentRegion}}" />
</Window>
In Radical ci sono diversi tipi di Region:
· IContentRegion: regione capace di ospitare un singolo contenuto alla volta. Una concreta implementazione è il “ContentPresenterRegion” che può essere collegato ad un WPF “ContentPresenter”.
· IElementsRegion: è stato creato per essere capace di ospitare più di un elemento alla volta, dove ogni elemento è visibile nello stesso momento. Una concreta implementazione è il “PanelRegion” che può essere collegato a qualsiasi tipo dipannello WPF (Canvas, DockPanel, Grid, StackPanel, …).
· ISwitchingElementsRegion: questo è simile al precedente, è creato per ospitare più di un elemento alla volta, ma dove un solo elemento alla volta è visibile nello stesso momento, aggiungendo il concetto di elemento attivo. Una concreta implementazione è il “TabControlRegion” che può essere collegato a dei “TabItem(s)”.
In questa applicazione di esempio utilizzeremo solo le “ContentPresenterRegion”, lascio a voi come esercizio, quello di provare tutte le altre implementazioni built-in ed eventualmente di crearne qualcuna custom, come quella fatta per il Menunell’esempio che trovate nei codici sorgenti di Radical.
6 – Aggiungere al progetto la cartella “Messaging” e “Handlers”
La cartella “Handlers” deve essere creata sotto alla cartella “Messaging”, e definisce la posizione di default, dove Radical, basandosi su precise convenzioni, guarda alla ricerca di MessageHandler:
Nel prossimo punto andremo a vedere come sfruttare le cartelle create precedentemente.
7 – Visualizzare la StartScreen all’avvio dell’applicazione (Messaging)
In questa fase andiamo ad introdurre il concetto di Message Broker e ovviamente vedremo come Radical ci viene in aiuto. Solito discorso, evitiamo di riscrivere cose già dette, quindi per la spiegazione del Message Broker vi rimando a questo articolo “IMessageBroker”.
In poche parole Radical ci aiuta implementando un suo MessageBroker, che viene creato allo startup dell’applicazione e che è vivo per tutta la durata dell’applicazione. Un messaggio viene, per la maggior parte dei casi, lanciato da un ViewModel, e per fare questo è sufficiente dichiarare la dipendenza all’interfaccia IMessageBroker (aggiungendo il parametro nel costruttore del ViewModel dove ci serve il message broker), e come per magia il dependency container ci inietterà il componente, e potremo lanciare il nostro messaggio utilizzando ad esempio il metodo “Broadcast”.
Anche ricevere un messaggio, è veramente semplice, perché Radical, sempre attraverso delle precise convenzioni, ci mette a disposizione delle cartelle “magiche”, che lui controlla alla ricerca di Message Handlers; parlo della cartella “Messaging” e in particolare della cartella “Handlers”: ogni tipo oggetto definito all’interno del namespace “*.Messaging.Handlers” viene considerato un Message Broker Message Handler, istanziato come singleton e registrato come “ascoltatore” del tipo di messaggio definito nella classe stessa. Se la convenzione non piace, può sempre essere cambiata, oppure si possono utilizzare direttamente i metodi dell’interfaccia IMessageBroker, “Subscribe”.
Con queste premesse andiamo ad aggiungere la classe “ApplicationBootCompletedHandler.cs” all’interno della cartella “Handlers”:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Messaging;
using Topics.Radical.Windows.Presentation.ComponentModel;
using Topics.Radical.Windows.Presentation.Messaging;
using WpfMvvmUICompositionInAction.Presentation;
namespace WpfMvvmUICompositionInAction.Messaging.Handlers
{
class ApplicationBootCompletedHandler : AbstractMessageHandler<ApplicationBootCompleted>, INeedSafeSubscription
{
readonly IViewResolver viewResolver;
readonly IRegionService regionService;
public ApplicationBootCompletedHandler(IViewResolver viewResolver, IRegionService regionService)
{
this.viewResolver = viewResolver;
this.regionService = regionService;
}
public override void Handle(object sender, ApplicationBootCompleted message)
{
this.regionService.GetKnownRegionManager<MainView>()
.GetRegion<IContentRegion>(ShellKnownRegions.MainContentRegion)
.Content = this.viewResolver.GetView<ShellStartScreenView>();
}
}
}
Come vedete sono state dichiarate delle dipendenze sotto forma di proprietà del costruttore, che verranno passate in automatico dal container Castle Windsor creato per noi dal bootstrapper di Radical. Il metodo “Handle” di questa classe verrà richiamato ogni qual volta il messaggio “ApplicationBootCompleted” viene lanciato, e nel caso specifico di questo messaggio, solo una volta per tutta la durata dell’applicazione. La gestione del messaggio è poi abbastanza semplice, si utilizza il RegionService per prendere il riferimento alla MainContentRegion, che è la nostra regione conosciuta della Shell (ShellKnownRegion), e gli iniettiamo attraverso la proprietà Content la View che vogliamo aprire allo startup dell’applicazione, per fare questo utilizziamo un altro componente di Radical che è il ViewResolver, che sostanzialmente ci offre un metodo GetView, che dato il tipo della View, ne restituisce l’istanza (nuova o quella creata in precedenza in base al Lifestyle), promettendo di valorizzarne il DataContextcon il suo ViewModel associato e di eseguire tutte le configurazioni necessarie a Radical.
Potete trovare tutti i messaggi predefiniti di Radical nella documentazione ufficiale: Built-in messages.
8 – Aggiungere e visualizzare nuove “app”
Come dicevo prima le “app” sono semplicemente degli UserControl che vengono iniettati nella regione “MainContentRegion” della “MainView”, e che occupano tutto lo spazio a disposizione della Window.
Incominciamo aggiungendo la nostra prima “app” che chiameremo inutilmente “Composite App”, dove al suo interno andremo ad utilizzare più metodi della UI Composition, a differenza dell’altra app “Common App” dove vedremo semplicemente come riutilizzare lo stesso componente (CommonView.xaml) in più app/viste semplicemente definendo la region specifica in tutti i punti dove vogliamo caricare il componente.
Sempre all’interno della cartella “Presentation” aggiungiamo i seguenti items:
· Un item di tipo UserControl (WPF) chiamato “CompositeAppView.xaml”
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.CompositeAppView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:rg="http://schemas.topics.it/wpf/radical/windows/presentation/regions"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel>
<Button Content="Start" Command="{Binding Path=OpenStartScreen}"/>
<Label Content="Hello World!"/>
</StackPanel>
</Grid>
</UserControl>
· Una classe per il ViewModel associato “CompositeAppViewModel.cs”
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Windows.Input;
using Topics.Radical.Windows.Presentation;
using WpfMvvmUICompositionInAction.Messaging;
namespace WpfMvvmUICompositionInAction.Presentation
{
public class CompositeAppViewModel : AbstractViewModel
{
readonly IMessageBroker messageBroker;
public ICommand OpenStartScreen { get; private set; }
public CompositeAppViewModel(IMessageBroker messageBroker)
{
Debug.WriteLine("CompositeAppViewModel()");
this.messageBroker = messageBroker;
this.OpenStartScreen = DelegateCommand.Create()
.OnExecute(p => OpenStartScreenHandler());
}
public void OpenStartScreenHandler()
{
Debug.WriteLine("OpenStartScreenHandler()");
this.messageBroker.Broadcast(this, new OpenStartScreenMessage());
}
}
}
Creiamo i componenti per gestire il messaggio di apertura della nostra app, aggiungendo le seguenti classi:
· Nella cartella “Messaging” aggiungere la classe “OpenCompositeAppMessage.cs”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfMvvmUICompositionInAction.Messaging
{
public class OpenCompositeAppMessage
{
}
}
· Nella cartella “Messaging” aggiungere la class “OpenStartScreenMessage.cs”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfMvvmUICompositionInAction.Messaging
{
public class OpenStartScreenMessage
{
}
}
· Nella cartella “Handlers” aggiungere la classe “OpenStartScreenMessageHandler.cs”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Messaging;
using Topics.Radical.Windows.Presentation.ComponentModel;
using WpfMvvmUICompositionInAction.Presentation;
namespace WpfMvvmUICompositionInAction.Messaging.Handlers
{
class OpenStartScreenMessageHandler : AbstractMessageHandler<OpenStartScreenMessage>, INeedSafeSubscription
{
readonly IViewResolver viewResolver;
readonly IRegionService regionService;
public OpenStartScreenMessageHandler(IViewResolver viewResolver, IRegionService regionService)
{
this.viewResolver = viewResolver;
this.regionService = regionService;
}
public override void Handle(object sender, OpenStartScreenMessage message)
{
this.regionService.GetKnownRegionManager<MainView>()
.GetRegion<IContentRegion>(ShellKnownRegions.MainContentRegion)
.Content = this.viewResolver.GetView<ShellStartScreenView>();
}
}
}
· Nella cartella “Handlers” aggiungere la classe “OpenCompositeAppMessageHandler.cs”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Messaging;
using Topics.Radical.Windows.Presentation.ComponentModel;
using WpfMvvmUICompositionInAction.Presentation;
namespace WpfMvvmUICompositionInAction.Messaging.Handlers
{
class OpenCompositeAppMessageHandler : AbstractMessageHandler<OpenCompositeAppMessage>, INeedSafeSubscription
{
readonly IViewResolver viewResolver;
readonly IRegionService regionService;
public OpenCompositeAppMessageHandler(IViewResolver viewResolver, IRegionService regionService)
{
this.viewResolver = viewResolver;
this.regionService = regionService;
}
public override void Handle(object sender, OpenCompositeAppMessage message)
{
this.regionService.GetKnownRegionManager<MainView>()
.GetRegion<IContentRegion>(ShellKnownRegions.MainContentRegion)
.Content = this.viewResolver.GetView<CompositeAppView>();
}
}
}
Infine aggiungiamo nel ViewModel della StartScreen (“ShellStartScreenViewModel.cs”), le istruzioni per aprire le nostre app:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Windows.Input;
using Topics.Radical.Windows.Presentation;
using WpfMvvmUICompositionInAction.Messaging;
namespace WpfMvvmUICompositionInAction.Presentation
{
public class ShellStartScreenViewModel : AbstractViewModel
{
readonly IMessageBroker messageBroker;
public ICommand OpenView { get; private set; }
public ShellStartScreenViewModel(IMessageBroker messageBroker)
{
Debug.WriteLine("ShellStartScreenViewModel()");
this.messageBroker = messageBroker;
this.OpenView = DelegateCommand.Create()
.OnExecute(OpenViewHandler);
}
public void OpenViewHandler(object parameter)
{
Debug.WriteLine("OpenViewHandler({0})", parameter);
if (parameter == null) return;
var app = (KnownApps)parameter;
switch (app)
{
case KnownApps.CompositeApp:
this.messageBroker.Broadcast(this, new OpenCompositeAppMessage());
break;
case KnownApps.CommonApp:
//this.messageBroker.Broadcast(this, new OpenCommonAppMessage());
break;
}
}
}
}
In pratica abbiamo definito un comando ICommand (vedi MVVM Commading e Radical MVVM) che viene impostato come Command delle nostre tile (pulsanti della ShellStartScreenView). Al comando viene associato un metodo che non fa altro che lanciare il messaggio definito dalla classe “OpenCompositeAppMessage” per notificare l’intenzione di aprire l’applicazione “Composite App”, di conseguenza l’handler creato in precedenza andrà ad intercettare il messaggio e inietterà nella regione principale il contenuto della vista “CompositeAppView.xaml” con relativo ViewModel “CompositeAppViewModel.cs”.
9 – La cartella “Partial”: un altro modo per definire il contenuto di una Region
· Prima di tutto dobbiamo aggiungere all’interno della cartella “Presentation” una nuova cartella “Partial”, poi all’interno di quest’ultima crearne un’altra chiamata “DetailContentRegion”.
Inutile dire che il nome di queste cartelle hanno un senso ben preciso, perché fanno parte delle convenzioni Radical: tutto quello che rientra nel namespace “*.Presentation.Partial.*” viene considerato come il contenuto della regione, e l’ultimotoken del namespace è il nome della regione in cui si inietterà.
· Nella cartella “DetailContentRegion” aggiungiamo un UserControl (WPF) chiamato “DetailView.xaml”
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.Partial.DetailContentRegion.DetailView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:rg="http://schemas.topics.it/wpf/radical/windows/presentation/regions"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="Aquamarine">
<StackPanel>
<Label Content="This is the detail of the world!"/>
</StackPanel>
</Grid>
</UserControl>
· Sempre nella stessa cartella aggiungiamo la classe “DetailViewModel.cs”
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Topics.Radical.Windows.Presentation;
namespace WpfMvvmUICompositionInAction.Presentation.Partial.DetailContentRegion
{
public class DetailViewModel : AbstractViewModel
{
public DetailViewModel()
{
Debug.WriteLine("DetailViewModel()");
}
}
}
· Aggiungere nell’UserControl “CompositeAppView.xaml” il placeholder (ContentPresenter) per la region precedentemente creata
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.CompositeAppView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:rg="http://schemas.topics.it/wpf/radical/windows/presentation/regions"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<StackPanel>
<Button Content="Start" Command="{Binding Path=OpenStartScreen}"/>
<Label Content="Hello World!"/>
<ContentPresenter rg:RegionService.Region="{rg:ContentPresenterRegion Name=DetailContentRegion}"/>
</StackPanel>
</Grid>
</UserControl>
10 – Iniettare il contenuto di una Region sfruttando l’attributo “InjectViewInRegion”
Nella cartella “Presentation” aggiungiamo questi items:
· Un nuovo UserControl (WPF) chiamato “CommonView.xaml”
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.CommonView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="Moccasin">
<StackPanel>
<Label Content="This is the common view!"/>
<StackPanel Orientation="Horizontal">
<Label Content="The random number is: "/>
<Label Content="{Binding Path=RandomNumber}"/>
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
· Nella classe code-behind della vista “CommonView.xaml.cs” impostare la proprietà “InjectViewInRegion”
using System.Windows.Controls;
using Topics.Radical.Windows.Presentation.ComponentModel.Regions;
namespace WpfMvvmUICompositionInAction.Presentation
{
/// <summary>
/// Interaction logic for CommonView.xaml
/// </summary>
[InjectViewInRegion(Named = "CommonContentRegion")]
public partial class CommonView : UserControl
{
public CommonView()
{
InitializeComponent();
}
}
}
· Una nuova classe “CommonViewModel.cs”
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Topics.Radical.Windows.Presentation;
namespace WpfMvvmUICompositionInAction.Presentation
{
public class CommonViewModel : AbstractViewModel
{
public int RandomNumber
{
get
{
return this.GetPropertyValue(() => this.RandomNumber);
}
set
{
this.SetPropertyValue(() => this.RandomNumber, value);
}
}
public CommonViewModel()
{
Debug.WriteLine("CommonViewModel()");
var rnd = new Random();
this.GetPropertyMetadata(() => this.RandomNumber)
.WithDefaultValue(rnd.Next());
}
}
}
· Aggiungiamo nella vista “DetailView.xaml” la nostra nuova region “CommonContentRegion”
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.Partial.DetailContentRegion.DetailView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:rg="http://schemas.topics.it/wpf/radical/windows/presentation/regions"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid Background="Aquamarine">
<StackPanel>
<Label Content="This is the detail of the world!"/>
<ContentPresenter rg:RegionService.Region="{rg:ContentPresenterRegion Name=CommonContentRegion}"/>
</StackPanel>
</Grid>
</UserControl>
A questo punto aprendo la nostra “Composite App” dovremmo ritrovarci con una cosa del genere:
11 – Aggiungiamo una nuova “app” e riutilizziamo la vista “CommonView”
Nella cartella “Presentation” aggiungiamo questi items
· Un nuovo UserControl (WPF) chiamato “CommonAppView.xaml”
<UserControl x:Class="WpfMvvmUICompositionInAction.Presentation.CommonAppView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:rg="clr-namespace:Topics.Radical.Windows.Presentation.Regions;assembly=Radical.Windows.Presentation"
mc:Ignorable="d" d:DesignWidth="351" d:DesignHeight="267">
<Grid>
<StackPanel>
<Button Content="Start" Command="{Binding Path=OpenStartScreen}"/>
<TextBlock TextWrapping="Wrap">
Below you can see the common content region content, that shows the same view (CommonView.xaml) that you have also on the Composite App:
</TextBlock>
<ContentPresenter rg:RegionService.Region="{rg:ContentPresenterRegion Name=CommonContentRegion}"/>
</StackPanel>
</Grid>
</UserControl>
· Una nuova classe per il ViewModel “CommonAppViewModel.cs”
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Windows.Input;
using Topics.Radical.Windows.Presentation;
using WpfMvvmUICompositionInAction.Messaging;
namespace WpfMvvmUICompositionInAction.Presentation
{
public class CommonAppViewModel : AbstractViewModel
{
readonly IMessageBroker messageBroker;
public ICommand OpenStartScreen { get; private set; }
public CommonAppViewModel(IMessageBroker messageBroker)
{
Debug.WriteLine("CommonAppViewModel()");
this.messageBroker = messageBroker;
this.OpenStartScreen = DelegateCommand.Create()
.OnExecute(p => OpenStartScreenHandler());
}
public void OpenStartScreenHandler()
{
Debug.WriteLine("OpenStartScreenHandler()");
this.messageBroker.Broadcast(this, new OpenStartScreenMessage());
}
}
}
· Nella cartella “Messaging” aggiungere la class “OpenCommonAppMessage.cs”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfMvvmUICompositionInAction.Messaging
{
public class OpenCommonAppMessage
{
}
}
· Nella cartella “Handlers” aggiungere la classe “OpenCommonAppMessageHandler.cs”
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Messaging;
using Topics.Radical.Windows.Presentation.ComponentModel;
using WpfMvvmUICompositionInAction.Presentation;
namespace WpfMvvmUICompositionInAction.Messaging.Handlers
{
class OpenCommonAppMessageHandler : AbstractMessageHandler<OpenCommonAppMessage>, INeedSafeSubscription
{
readonly IViewResolver viewResolver;
readonly IRegionService regionService;
public OpenCommonAppMessageHandler(IViewResolver viewResolver, IRegionService regionService)
{
this.viewResolver = viewResolver;
this.regionService = regionService;
}
public override void Handle(object sender, OpenCommonAppMessage message)
{
this.regionService.GetKnownRegionManager<MainView>()
.GetRegion<IContentRegion>(ShellKnownRegions.MainContentRegion)
.Content = this.viewResolver.GetView<CommonAppView>();
}
}
}
· Scommentiamo la riga che lancia il messaggio dalla “ShellStartScreenViewModel.cs”
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Topics.Radical.ComponentModel.Messaging;
using Topics.Radical.Windows.Input;
using Topics.Radical.Windows.Presentation;
using WpfMvvmUICompositionInAction.Messaging;
namespace WpfMvvmUICompositionInAction.Presentation
{
public class ShellStartScreenViewModel : AbstractViewModel
{
readonly IMessageBroker messageBroker;
public ICommand OpenView { get; private set; }
public ShellStartScreenViewModel(IMessageBroker messageBroker)
{
Debug.WriteLine("ShellStartScreenViewModel()");
this.messageBroker = messageBroker;
this.OpenView = DelegateCommand.Create()
.OnExecute(OpenViewHandler);
}
public void OpenViewHandler(object parameter)
{
Debug.WriteLine("OpenViewHandler({0})", parameter);
if (parameter == null) return;
var app = (KnownApps)parameter;
switch (app)
{
case KnownApps.CompositeApp:
this.messageBroker.Broadcast(this, new OpenCompositeAppMessage());
break;
case KnownApps.CommonApp:
this.messageBroker.Broadcast(this, new OpenCommonAppMessage());
break;
}
}
}
}
E questo è il risultato finale:
12 - …e alla fine
…la vostra solution dovrebbe assomigliare a questa:
Vedrete che in debug, nella schermata di output di visual studio, viene stampata qualche riga di debug, che vi serviranno per farvi capire quando i ViewModel vengono istanziati, in particolare guardate la differenza tra la ShellStartScreenViewModel e un qualsiasi altro ViewModel (tranne il MainViewModel).
Trovate il progetto completo sul mio repository github CommunityResources (WpfMvvmUICompositionInAction).