I began to write this post a couple week ago but since then I couldn’t manage to find some spare time to complete it. I hope to remember all the points for which an explanation is worth.
This time I’ll port the first real feature of StockTrader to the Caliburn implementation. I’m going to transfer the central tab control containing the two main feature of StockTrader and start to restore the first of them.
First of all, I have to restore content areas in the shell, which I initially stripped away. I will replace the custom AnimatedTabControl (that deals with transition animation between the features) with a simple TabControl.
In Prims views are injected at bootstrap time by the various application modules into proper “regions”, that are named areas within the shell view. On the other hand, Caliburn enforce the concept of “Application Model”: a logical representation of the entire application that is aimed to model screen composition and interaction with a non-visual structure.
While leveraging Caliburn’s preferred approach, I want to keep the modular organization of original StockTrader, letting various module to register its features in the shell during bootstrap phase.
Let’s start with “Position” module. I modified PositionModule class, deriving it from CaliburnModule; the class is responsible (both in original and in my version) to register module-specific components and to start its default screen.
public class PositionModule : CaliburnModule { public PositionModule(IConfigurationHook hook) : base(hook) { } protected override IEnumerable<ComponentInfo> GetComponents() { yield return Singleton(typeof(IAccountPositionService), typeof(Services.AccountPositionService)); } protected override void Initialize() { var posSummary = ServiceLocator.GetInstance<PositionSummary.IPositionSummaryPresentationModel>(); ServiceLocator.GetInstance<IShellPresenter>().Open(posSummary); } }
I chose to explicitly register in the module class body only AccountPositionService (it could be a remote service), while all other client components are registered declaratively (see Auto-Registering Components in Caliburn documentation):
[PerRequest(typeof(IPositionSummaryPresentationModel))] public class PositionSummaryPresentationModel : Presenter, IPositionSummaryPresentationModel { ... }
In the Initialize method of the module, an instance of IPositionSummaryPresentationModel is obtained from the container and “opened” in the shell. IShellPresenter is a PresenterHost, so it is responsible of managing multiple content presenters keeping track of the “current” one.
Here is a point where Caliburn and Prism implementation and “philosophy” differs:
- Prism requires to register views instance into UI regions; even if regions are loosely referenced with strings and views instance are indirectly obtained by presentation model, this approach still seems too view-centric. In addition, the UI composition behaviour is not enforced in the interface of the region: there is no difference between regions supporting single or multiple views;
- Caliburn helps to define an application model driving the application parts composition; you don’t have to specify where and the opened presenter is shown: all visualization concerns are taken in account in the views.
Let’s see, for example, how to specify the visualization of the presenters managed by the shell:
in ShellView.xaml
<TabControl SelectedIndex="0" VerticalAlignment="Stretch" ItemContainerStyle="{StaticResource ShellTabItemStyle}" Background="{StaticResource headerBarBG}" ItemsSource="{Binding Presenters}"> </TabControl>
in TabItemResource.xaml
<Style x:Key="ShellTabItemStyle" TargetType="{x:Type TabItem}"> ... <Setter Property="Header" Value="{Binding DisplayName}" /> <Setter Property="ContentTemplate"> <Setter.Value> <DataTemplate> <ContentControl cal:View.Model="{Binding}" /> </DataTemplate> </Setter.Value> </Setter> ... </Style>
Presenters opened by shell view are displayed within a TabControl; the display name is print in the tab header, while the content of the presenter is put in the content area of the tab. cal:View.Model attached property is taking care to peek the correct view to display the presenter.
How is the correct view chosen for each presenter? Caliburn follows “Conventions over Configuration” philosophy, defining the concept of ViewStrategy (represented by IViewStrategy interface). This interface is responsible of selecting the right view based on the type of the presentation model class, following an application-wide convention.
The default implementation is well suited for projects with separated namespaces for views and presentation models; Stock Traders, on the contrary, follows the convention of using the namespace to group features, thus having presentation model in the same namespace of its corresponding view.
To use this convention I subclassed the default convention:
public class StockTraderViewStrategy : DefaultViewStrategy { public StockTraderViewStrategy(IAssemblySource assemblySource, IServiceLocator serviceLocator) : base(assemblySource, serviceLocator) { } protected override string MakeNamespacePart(string part) { return part; } protected override IEnumerable<string> ReplaceWithView(Type modelType, string toReplace) { // MyNamespace.SomethingPresentationModel -> MyNamespace.SomethingView // or MyNamespace.SomethingPresenter -> MyNamespace.SomethingView if (!string.IsNullOrEmpty(toReplace)) yield return modelType.Namespace + "." + modelType.Name.Replace(toReplace, "View"); } }
and registered it at application startup:
in App.xaml.cs
protected override void ConfigurePresentationFramework(Caliburn.PresentationFramework.PresentationFrameworkModule module) { module.UsingViewStrategy<Infrastructure.StockTraderViewStrategy>(); }
Finally, I have the first module loading and displaying its default screen in the shell: