Come già anticipato in un mio precedente post, l’approccio basato su file XAML only per creare applicazioni con pattern MVVM che permettano una personalizzazione spinta dell’interfaccia, si scontra con l’impossibilità di inserire uno user control SENZA codebehind all’interno di una window (o di una page).
Tanto per intenderci, faccio un parallelo con ASP.net, essendo il concetto molto simile a quello che supporta i Web User Control (ascx).
In ASP.net ho una pagina, che contiene uno user control. Questo viene “registrato” in cima alla pagina con una “direttiva” del tipo:
1: <%@ Register TagPrefix="dnn" TagName="LANGUAGE" Src="~/Admin/Skins/Language.ascx" %>
2: ...
3: omissis
4: ...
5: <dnn:LANGUAGE runat="server" id="dnnLANGUAGE" showMenu="False" showLinks="True" />
In pratica, diciamo al compilatore di ASP.net: “quando compili la pagina, riferisciti a quel file sul disco e gestiscilo inserendo nella pagina il frammento di codice ASP.net che gli compete, agganciando tutti gli eventi nel code behind…”.
Questo concetto manca in WPF. Innanzitutto una precisazione è d’obbligo: gli UserControl sono praticamente identici ai controlli Utente Web, ma non possono essere usati senza avere una classe di code-behind compilata. Questo è dovuto al fatto che, per referenziare uno user control in una window o in una page, bisogna includere un riferimento all’assembly ed al namespace che lo contiene. Vediamo due snippets della pagina e dello User Control “Standard”:
1: <Window x:Class="WpfApplication1.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:my="clr-namespace:WpfApplication1"
5: Title="Window1" Height="300" Width="300">
6: <Grid>
7: <my:StandardUC/>
8: </Grid>
9: </Window>
1: <UserControl x:Class="WpfApplication1.StandardUC"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
4: <StackPanel>
5: <TextBox x:Name="txtHello"></TextBox>
6: <Button x:Name="btnHello" Content="Click me!" Click="btnHello_Click"/>
7: </StackPanel>
8: </UserControl>
Quello che deve balzare agli occhi, è il riferimento xmlns:my=”clr-namespace:WpfApplication1” che indica dove trovare il controllo compilato, e non dice nulla riguardo alla parte XAML. Questo perchè in effetti, in fase di compilazione, la parte XAML viene tramutata in file .g (generated) e compilato come baml. A run time lo ritroveremo nel file .exe all’interno della sezione resources (Reflector rulez!)
Il punto è che, comunque, non c’è modo di specificare un riferimento ad un file fisico che verrà caricato dal disco a run-time. A questo punto sono d’obbligo un paio di citazioni: durante una chiacchierata virtuale con l’amico Andrea Boschin, e dopo aver parlato anche con il WPF-Guru Corrado Cavalli, è emersa una soluzione, che mi appresto a riportare.
Si tratta di creare una class CustomUserControl, la quale eredita da ContentControl, aggiungere una proprietà Source e sovrascrivere il metodo OnInitialize come si può vedere nel seguente snippet:
1: using System;
2: using System.Xml;
3: using System.Windows.Controls;
4: using System.Windows.Markup;
5:
6: namespace WpfApplication1
7: {
8: public class CustomUserControl : ContentControl
9: {
10: public string Source { get; set; }
11:
12: protected override void OnInitialized(EventArgs e)
13: {
14: base.OnInitialized(e);
15: XmlTextReader reader = new XmlTextReader(this.Source);
16: base.Content = XamlReader.Load(reader);
17: }
18: }
19: }
Ovviamente, questa diventa una specie di classe “base” per tutti i controlli Codebehind-less, per cui, quando li vorremo includere nella window dovremo fare come segue:
1: <Window x:Class="WpfApplication1.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:my="clr-namespace:WpfApplication1"
5: Title="Window1" Height="300" Width="300">
6: <Grid>
7: <my:CustomUserControl Source="UserControl1.xaml"/>
8: </Grid>
9: </Window>
Ovvero utilizzare il riferimento al namespace che contiene la classe base, impiegare il controllo CustomUserControl ed impostare il file XAML da caricare a run-time.
In questo modo è stato superato il limite degli User Control in applicazione MVVM che mantengono le interfacce XAML esterne rispetto al compilato. Nel caso di MVVM gli UserControl con adeguati comandi di binding possono essere utilizzati come DataTemplate in controlli list (ListBox e affini). Ringrazio ancora Andrea e Corrado per il supporto e l’ispirazione… è sempre un piacere potersi confrontare con voi!