Modulaire applicaties in Silverlight
Inleiding
Weer kostte het bloed, zweet en tranen om de wijzigingen in de requirements van de klant door te voeren in de applicatie. De halve architectuur moest hiervoor overhoop. Als je dit vooraf geweten had, had je de deadline misschien wel gehaald.
Herkenbaar? Vooral nieuwere technologieën zoals Silverlight en WPF kunnen je gemakkelijk in dit soort situaties doen belanden. Je applicatie modulair opbouwen had in een dergelijke situatie uitkomst kunnen bieden. Het modulair opzetten van een applicatie brengt een aantal grote voordelen met zich mee. Door een applicatie in verschillende modules op te zetten, worden de verschillende onderdelen minder afhankelijk van elkaar. Separation of Concerns is eenvoudiger na te streven. Ook wordt het gemakkelijker om met een team aan een applicatie te werken. De verschillende modules kunnen, zonder afhankelijk te zijn van elkaar, ontwikkelt en onderhouden worden. Doordat er van tevoren nagedacht moet worden wat in welke modules moet gaan komen, wordt er beter nagedacht over de architectuur van de applicatie en zul je in een later stadium minder snel voor onaangename verrassingen komen te staan.
Composite Application Guidance
Om je hierbij te helpen heeft het Patterns & Practices team van Microsoft hiervoor de Composite Application Guidance geschreven. Deze handleiding/toolkit is bedoeld je te helpen bij het ontwikkelen van enterprise applicaties in WPF of Rich Internet Applications, RIAs, in Silverlight. Er worden een aantal conventies, patterns en examples in besproken die je helpen bij het ontwikkelen van Composite applicaties.
De Composite Application Guidance maakt gebruik van Unity, de dependency injection container van P&P. Één van de grote voordelen van het gebruik van Unity is dat het ontkoppelen van een applicatie simpeler wordt. Het defineren, of registreren, van objecten gebeurt op een centrale plaats. Dependency injection containers zijn er in allerlei soorten en maten en je bent dan ook niet verplicht Unity te gebruiken, maar doordat deze ook is ontwikkeld door P&P is het gebruik binnen de CAG wel het meest voor de hand liggend.
Om het gebruik van de Composite Application Guidance te vereenvoudigen zijn er een aantal assemblies nodig; de Composite Application Library. Het Patterns & Practices team heeft deze beschikbaar gesteld met sourcecode, voorbeelden en een referentie implementatie. Ze zijn te downloaden van http://shrinkster.com/19qe. Nadat deze gedownload en geinstalleerd zijn moeten ze gebuild worden. Dit is eenvoudig te doen door “Desktop & Silverlight - Open Composite Application Library.bat” te starten en de solution die geopend wordt te builden. De assemblies worden geplaatst in \CAL\Silverlight\Composite.UnityExtensions\bin\Debug.
Aan de hand van een simpel voorbeeld wil ik laten zien hoe je je applicaties op een manier kunt opzetten zodat deze beter te onderhouden en te testen zijn. De applicatie wordt gemaakt in Silverlight en zou gebruikt kunnen worden om code snippets mee te beheren. Ik ga ervan uit dat, als je tegen de eerder genoemde problemen bent aangelopen, je enige ervaring hebt met Silverlight of WPF. Je moet in elk geval bekend zijn met concepten als XAML, routed commands en databinding.
De demo applicatie

Afbeelding 1: Snipz Client
De demo applicatie, “Snipz”, bestaat uit vier delen. Eén Server en drie client projecten.
Snipz is het project dat gestart wordt aan de client zijde. Hier wordt alles geinitialiseerd en de dependency injection container gevuld. In dit project zit ook de Shell. De shell is de primaire window waar de UI in zit. De UI wordt in de vorm van Views uit de modules gelezen en getoont in een Region.
Het tweede project is Snipz.Edit. Dit is de voorbeeld module in de demo applicatie. De module staat volledig op zichzelf en heeft geen enkele afhankelijkheid met het Snipz project. De module toont een editor voor de snippets.
Snipz.API wordt gedeeld tussen Snipz en Snipz.Edit. In dit project bevinden zich twee interfaces. Snipz.Edit gebruikt deze interfaces, maar ze zijn geïmplementeerd in Snipz. Unity wordt in Snipz.Edit gebruikt om de interfaces te instanciëren. Hierdoor kan Snipz.Edit los van de rest getest worden.
Het vierde en laatste project is Snipz.Web. Dit is het server gedeelte van de demo applicatie. Hier zouden services geplaatst kunnen worden die de data uit een database lezen. Om de demo applicatie simpel te houden heb ik dit niet geimplementeerd. De aspx pagina wordt alleen gebruikt om de silverlight applicatie te tonen.
Het opzetten van de applicatie

Afbeelding 2: Nieuwe applicatie
Maak in visual studio een nieuw Silverlight project en geef deze een pakkende naam. Als voorbeeld heb ik gekozen voor de naam Snipz. Selecteer hierbij het ASP.Net Web Site web project type.
De solution die nu gemaakt is bestaat uit twee projecten. Één voor de client (Snipz) en één voor de server (Snipz.Web).
Voeg in het Snipz project references toe naar:
Microsoft.Practices.Composite
Microsoft.Practices.Composite.Presentation
Microsoft.Practices.Composite.UnityExtensions
Microsoft.Practices.Unity
Rename de Mainpage class naar Shell en het Mainpage.xaml bestand naar Shell.xaml. In de shell worden de views weergegeven. De views worden gedefineerd in modules en geplaatst in een Region. Je kunt de Region zien als een placeholder voor views. De Region weet niets van de views, en de view weet niets van de Region waar deze in wordt weergegeven. In de shell kunnen ook algemene UI elementen geplaatst worden zoals een menu of een statusbalk. De views van Snipz worden weergegeven in een ContentControl. Dit zorgt ervoor dat er maar één view tegelijkertijd kan worden weergegeven. Je kunt ook een ItemsControl of zelfs een tab control gebruiken om de views weer te geven. De views worden behandeld als content van deze controls, dus onder elkaar als items in een list of per tab in een tab control. Om de Region te defineren gebruik je de attached property RegionName van de class RegionManager (zie listing 1).
Listing 1
Voeg een nieuwe class toe aan het Snipz project. Noem deze Bootstrapper. De Bootstrapper class moet afgeleid worden van UnityBootstrapper. Deze class bevat een groot aantal virtual methods die kunnen worden gebruikt. De bootstrapper is verantwoordelijk voor de initialisatie van de Composite Application Library. De bootstrapper zorgt ervoor dat je meer controle krijgt over hoe je applicatie in elkaar zit.
In een standaard Silverlight applicatie wordt de main window gestart vanuit App.xaml. In een applicatie die gebruikt maakt van de CAL wordt dit gedaan door de bootstrapper, door de virtual method CreateShell().
De Shell class wordt geinstancieerd door de Unity container en toegekend aan de RootVisual van de current application.
Om modules te kunnen gebruiken moet er een ModuleCatalog gedefineerd worden. Dit gebeurd in de override van de GetModuleCatalog() method. Later zullen hier de verschillende modules aan toegevoegd worden.
public class BootStrapper:UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
var shell = Container.Resolve<Shell>();
Application.Current.RootVisual = shell;
return shell;
}
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
return catalog;
}
}
Listing 2
Het initialiseren van de bootstrapper gebeurt op de plaatst waar voorheen de main page geinstancieerd werd, in App.xaml.cs in de afhandeling van het Startup event. In plaats van MainPage wordt nu een nieuwe bootstrapper aangemaakt en gestart.
private void Application_Startup(object sender,
StartupEventArgs e)
{
BootStrapper bootStrapper = new BootStrapper();
bootStrapper.Run();
}
Listing 3
Op dit moment kan de applicatie gecompileerd en uitgevoerd worden. Er gebeurt alleen nog niet zo veel.
De Module
Voeg een nieuwe Silverlight Library toe aan de solution en noem deze Snipz.Edit. Voeg, om ook hier de Composite Application library te kunnen gebruiken, de volgende references toe:
Microsoft.Practices.Composite
Microsoft.Practices.Composite.Presentation
Microsoft.Practices.Unity
Hernoem de class “class” naar EditModule. Om deze class al module herkenbaar te maken aan de applicatie moet deze afgeleid zijn van de IModule interface. De IModule interface heeft één method die geïmplementeerd moet worden; Initialize(). Deze method wordt aangeroepen tijdens het initialisatie proces van de module.
public class EditModule:IModule
{
public void Initialize()
{
}
}
Listing 4
Om deze module te kunnen gebruiken moet deze toegevoegd worden aan de Bootstrapper in de GetModuleCatalog() method. Dit kan door in het Snipz project een reference te maken naar het Snipz.Edit project en de GetModuleCatalog() method aan te passen als in listing 5.
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog()
.AddModule(typeof(Snipz.Edit.EditModule));
return catalog;
}
Listing 5
Het mooie van deze constructie is dat je runtime kunt bepalen welke modules er geladen moeten worden. De Composite Application Library voorziet ook in mogelijkheden om modules uit xaml of uit een configuratie bestand te lezen.
Data
De module moet ergens data vandaan halen en naar toe kunnen sturen. In een “echt” project heb je waarschijnlijk een webservice waar je tegenaan praat. De webservice haalt de data uit een database en schrijft deze erin weg. In het client gedeelte heb je een class die de calls naar de server afhandelt. In het geval van het voorbeeld heeft de service 2 methods: LoadSnippets en SaveSnippet. Het project bevat ook een data class, Snippet, om een code snippet te beschrijven.
Doordat de interfaces van de service class en de Snippet data toegankelijk moeten zijn in de modules, is een gedeelde assembly nodig. Voeg hiervoor een nieuw project toe en noem deze Snipz.API. Delete de class.cs file, voeg een nieuwe interface toe en noem deze ISnipzService zoals in listing 6.
public interface ISnipzService
{
void SaveSnippet(ISnippet snippet);
List<ISnippet> LoadSnippets();
}
Listing 6
Voeg nog een interface toe aan het Snipz.API en noem deze ISnippet. De ISnippet bevat beschrijvingen voor 4 string properties zodat deze later in de view gebruikt kunnen worden.
public interface ISnippet
{
string Tags { get; set; }
string SnippetText { get; set; }
string Title { get; set; }
string Language { get; set; }
}
Listing 7
Om gebruik te kunnen maken van beide interfaces moeten ze geïmplementeerd worden. Dit gebeurt in het project waar ook de bootstrapper te vinden is, Snipz. Voeg een reference toe vanuit het Snipz project naar de Snipz.API assembly. Voeg een nieuwe class toe en noem deze Snippet. Leidt de class af van ISnippet en implementeer de members.
public class Snippet:ISnippet
{
public string Tags { get; set; }
public string SnippetText { get; set; }
public string Title { get; set; }
public string Language { get; set; }
}
}
Listing 8
De server-side implementatie van de service is gaat buiten de scope van dit artikel. Om toch een paar handvatten te geven, en straks commanding te kunnen laten zien is er toch een client-side implementatie nodig, of in elk geval een concrete implementatie van de ISnipzService interface. De LoadSnippets() method geeft een lijst terug met 2 snippets.
public class SnipzService:ISnipzService
{
public void SaveSnippet(ISnippet snippet)
{
//send the snippet to the server…
}
public List<ISnippet> LoadSnippets()
{
return new List<ISnippet>
{
new Snippet {
Language = "C#",
SnippetText = "Console.Writeline(\"Hello world\");",
Tags = "Hello-world",
Title = "Hello World"},
new Snippet{
Language = "C#",
SnippetText = "Console.ReadLine();",
Tags = "readline",
Title = "Readline"}
};
}
}
Listing 9
Om de Snippet class en de SnipzService class beschikbaar te maken in de module moet deze geregistreerd worden in de dependency injection container in de bootstrapper. De registratie vindt plaats door middel van een interface en de concrete implementatie. De bootstrapper zorgt er dan voor de container met de juiste informatie bij de module terecht komt. Door een override te maken op de ConfigureContainer() method in de bootstrapper class kunnen eigen classes aan de container toegevoegd worden. Er kan optioneel een LifetimeController worden meegegeven die bepaald hoe lang een object blijft bestaan. In het geval van de service wordt er een ContainerControlledLifetimeManager meegegeven. Hiermee wordt aangegeven dat, elke keer als de ISnipzServer wordt opgevraagd door middel van de Resolve() method, de ResolveAll() method of als de container wordt gebruikt als parameter in een constructor, dezelfde instance van de class wordt gebruikt. Als er geen parameter wordt meegegeven, wordt er een nieuwe instance aangemaakt.
protected override void ConfigureContainer()
{
Container.RegisterType<ISnipzService,
SnipzService>(
new ContainerControlledLifetimeManager());
Container.RegisterType<ISnippet, Snippet>();
base.ConfigureContainer();
}
Listing 10
Model-View-View Model
Maak in het module project een reference naar Snipz.API om deze te kunnen gebruiken. Voeg nu een interface toe aan de module Snipz.Edit en noem deze IEditModel. Deze class zal gaan communiceren met de service om snippets te laden en te saven. De LoadSnippet() method geeft één snippet terug aan de hand van een index waarde. De snippet wordt opgevraagd via de SnipzService. De SaveSnippet() method geeft een ISnippet door aan de service. NewSnippet() geeft een lege snippet terug.
public interface IEditModel
{
ISnippet LoadSnippet(int index);
void SaveSnippet(ISnippet snippet);
ISnippet NewSnippet();
}
Listing 11
Maak nu een nieuwe class en noem deze EditModel. Implementeer de IEditModel interface. Voeg een constructor toe aan de class en geef hem twee parameters. De eerste parameter ontvangt een ISnipzService, de tweede een IUnityContainer. Doordat de class geïnstancieerd gaat worden door Unity, krijgt deze vanzelf de juiste waarden voor de parameters mee. Om de constructor parameters in de geïmplementeerde methods te kunnen gebruiken, worden deze opgeslagen in private readonly fields. Als je kijkt naar de uitwerking in listing 12, dan zie je dat er overal gebruik wordt gemaakt van de interfaces. Zelfs het maken van een nieuwe snippet wordt gedaan aan de hand van een interface. In plaats van het keyword new te gebruiken wordt de Resolve() method van de UnityContainer aangeroepen. De Container.Resolve() method krijgt als type ISnippet mee. Er wordt een nieuwe Snippet geïnstancieerd, omdat in de bootstrapper tegen Unity is verteld dat deze overal waar een ISnippet gevraagd een concrete Snippet moet worden aangemaakt. Hierdoor verdwijnt de afhankelijkheid op Snippet. De implementatie van ISnippet kan in de bootstrapper worden aangepast, zonder dat de module hiervoor hoeft te worden gewijzigd.
public class EditModel : IEditModel
{
private readonly ISnipzService _service;
private readonly IUnityContainer _container;
public EditModel(ISnipzService service,
IUnityContainer container)
{
_service = service;
_container = container;
}
public ISnippet LoadSnippet(int index)
{
return _service.LoadSnippets()[index];
}
public void SaveSnippet(ISnippet snippet)
{
_service.SaveSnippet(snippet);
}
public ISnippet NewSnippet()
{
return _container.Resolve<ISnippet>();
}
}
Listing 12
De data moet natuurlijk worden weergegeven en kunnen worden gewijzigd. Voeg hiervoor een nieuwe Silverlight UserControl toe aan het Snipz.Edit project en noem deze EditView. De User-Interface zelf is niet zo heel bijzonder. Het is niets meer dan een grid met een paar tekstboxen voor de invoer van de verschillende properties voor de snippets en 3 knoppen voor de acties naar de service (zie afbeelding 3).

Afbeelding 3
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition />
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="72"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox x:Name="Snippet" Margin="2"
Grid.ColumnSpan="2" Grid.Row="3"
VerticalScrollBarVisibility="Auto"
TextWrapping="Wrap" AcceptsReturn="True" />
<TextBox x:Name="Tags" Margin="2" Grid.Column="1"
Grid.Row="4" />
<TextBox x:Name="Language" Margin="2" Grid.Column="1"
Grid.Row="1" />
<TextBox x:Name="Title" Margin="2" Grid.Column="1" />
<TextBlock x:Name="TitleText" Text="Title:"
Margin="2" HorizontalAlignment="Right"
VerticalAlignment="Center" />
<TextBlock x:Name="LanguageText" Text="Language:"
Margin="2" Grid.Row="1"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
<TextBlock x:Name="SnippetText" Text="Snippet:"
Margin="2" Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
<TextBlock x:Name="TagsText" Text="Tags:"
Margin="2" Grid.Row="4"
HorizontalAlignment="Right"
VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Grid.Row="5"
Grid.Column="1" Margin="2">
<Button Content="Save" Width="50" Margin="2" />
<Button Content="Load" Width="50" Margin="2" />
<Button Content="Clear" Width="50" Margin="2" />
</StackPanel>
</Grid>
Listing 13
Om de Model en de View aan elkaar te koppelen is er een ViewModel nodig. Voeg hiervoor weer een interface toe en noem de IEditViewModel. In de interface komt een readonly property naar de view IEditView. De moet beschikbaar zijn voor de IModule implementatie zodat deze toegevoegd kan worden aan de Region in de Shell. Ook is er een ISnippet property nodig om de View aan te kunnen binden. In de Composite Application Library is ook de mogelijkheid opgenomen om Commands te gebruiken. Deze zijn ook te vinden in de interface om ze te kunnen binden aan de knoppen in de view.
public interface IEditViewModel
{
IEditView View { get;}
ISnippet Snippet { get; set; }
DelegateCommand<string> SaveCommand { get; set; }
DelegateCommand<string> LoadCommand { get; set; }
DelegateCommand<string> ResetCommand { get; set; }
}
Listing 14
Voeg een nieuwe class toe aan de Snipz.Edit module voor de implementatie van de IEditViewModel class en noem deze EditViewModel. Implementeer de IEditViewModel interface. Implementeer, om ervoor te zorgen dat UI bijgewerkt wordt als de Snippet wijzigd, ook de INotifyPropertyChanged interface van Silverlight.
public class EditViewModel:IEditViewModel,
INotifyPropertyChanged
{
public DelegateCommand<string> SaveCommand { get; set;}
public DelegateCommand<string> LoadCommand { get; set;}
public DelegateCommand<string> ResetCommand { get; set;}
private ISnippet _snippet;
public ISnippet Snippet
{
get { return _snippet; }
set { _snippet = value;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs("Snippet"));
}
}
public IEditView View { get; private set; }
public event
PropertyChangedEventHandler PropertyChanged;
}
Listing 15
De EditViewModel class heeft een constructor om alle properties correct te initialiseren. De constructor heeft twee parameters nodig welke deze meekrijgt van de UnityContainer. Ook geeft de EditViewModel class zichzelf door aan de View. Deze method wordt dadelijk toegevoegd. De commands worden geïnstancieerd met delegates naar methods.
private IEditModel _model;
public EditViewModel( IEditView view, IEditModel model)
{
_model = model;
SaveCommand = new DelegateCommand<string>(SaveHandler);
LoadCommand = new DelegateCommand<string>(LoadHandler);
ResetCommand =
new DelegateCommand<string>(ResetHandler);
View = view;
View.SetViewModel(this);
}
Listing 16
Deze methods zien er uit zoals in listing 17. Er gebeurt in deze methods niets meer dan aanroepen naar de EditModel class. De LoadHandler() zal in een werkelijke implementatie wat complexer zijn. In dit voorbeeld wordt altijd dezelfde snippet geladen, in werkelijkheid zou er een andere view getoond kunnen worden om een snippet te zoeken in de database.
private void ResetHandler(string obj)
{ Snippet = _model.NewSnippet(); }
private void SaveHandler(string obj)
{ _model.SaveSnippet(Snippet); }
private void LoadHandler(string obj)
{ Snippet = _model.LoadSnippet(1);}
Listing 17
Om de ViewModel aan de View te koppelen is er een laatste interface nodig. Voeg deze toe aan het project en noem deze IEditView.
public interface IEditView
{
void SetViewModel(object viewModel);
}
Listing 18
Implementeer deze interface in de codebehind van de EditView.xaml. Dit is, naast de constructor, de enige code die in de codebehind komt te staan.
public partial class EditView : IEditView
{
public void SetViewModel(object viewModel)
{
DataContext = viewModel;
}
…
Listing 19
Voeg in de xaml de bindings toe voor de properties van de Snippet, zodat de tekstboxen dit kunnen weergeven en de gebruiker de tekst kan wijzigen. Zet de mode van de binding op TwoWay om ervoor te zorgen dat de data wordt teruggegeven aan de Model. Voeg de regel hier onder ook toe voor de andere drie tekstboxen.
Text="{Binding Path=Snippet.SnippetText, Mode=TwoWay}"
Listing 20
Voeg om de commando’s te kunnen gebruiken de volgende regel toe aan de UserControl definitie.
xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Commands;assembly=Microsoft.Practices.Composite.Presentation"
Listing 21
Als de onderstaande regel wordt toegevoegd aan de Save button, wordt zordra er op de button geklikt wordt de delegate in de ViewModel uitgevoerd. Voeg de regel ook toe voor de andere twee knoppen.
cal:Click.Command="{Binding Path=SaveCommand}"
Listing 22
Nu de bindings op zijn plek zitten kan de View aan de Region worden toegevoegd. Dit gebeurt in de EditModule class. De constructor van de EditModule class krijgt van Unity een IRegionManager voor de regions en een IUnityContainer mee. Deze worden bewaard om gebruikt te worden vanuit de Initialize() method.
private readonly IRegionManager _regionManager;
private readonly IUnityContainer _container;
public EditModule(IRegionManager regionManager,
IUnityContainer container )
{
_regionManager = regionManager;
_container = container;
}
Listing 23
De Initialize() method moet eerst de drie gebruikt interfaces registeren bij de Unity container, zodat deze op andere plaatsen in de assembly gebruikt kunnen worden. Daarna wordt de EditViewModel geïnstancieerd door middel van de Resolve() method van de container. Ook wordt de view aan de “MainRegion” region toegevoegd.
public void Initialize()
{
_container.RegisterType<IEditView, EditView>();
_container.RegisterType<IEditModel, EditModel>();
_container.RegisterType<IEditViewModel,
EditViewModel>();
var ViewModel = _container.Resolve<IEditViewModel>();
_regionManager.Regions["MainRegion"].Add(presenter.View);
}
Listing 24
De applicatie is nu klaar om gecompileerd en gestart te worden.
Conclusie
Het Patterns & Practices team heeft een zeer complete handleiding en library geschreven die zowel in Silverlight als WPF gebruikt kan worden. Ik heb hier maar een klein gedeelte van het arsenaal aan functies kunnen bespreken, maar ik denk dat het genoeg is om mee te starten. De hoge mate van Testability en Separation of concerns zullen zeker bijdragen aan beter onderhoudbare applicaties en minder verrassingen achteraf.
Links
Composite Application Guidance for WPF and Silverlight: http://msdn.microsoft.com/en-us/library/dd458809.aspx
Patterns & Practices: http://msdn.microsoft.com/en-us/practices/default.aspx
Sources behorend bij dit artikel.