This is the 3rd part of the series where in previous post we saw how to develop WPF applications using Caliburn Framework. In this post, we’ll see some more advanced usage where our shell instead of being just limited to displaying single view at a time, can have multiple ones in a Tabbed user interface.

I’m still using v1.1 of Caliburn framework. In this version naming are more toward MVP pattern but the framework can be used to implement WPF and Silverlight applications in both MVP and MVVM patterns. Next version of Caliburn will have better namings that are not design pattern specific. If you are using later versions of the framework, classes may have quite different namings.

Basic Shell

To recap, creating a basic shell in WPF using MVVM pattern that benefits Caliburn framework was fairly simple. You needed to create a View which is basically a UserControl for the shell and place a ContentControl in it which will be the placeholder for other views to open in. For the ViewModel part, there was a couple of base class to choose from but so far we’ve used PresenterManager which can host and display single “Presenter”.

<UserControl x:Class="iSales.Views.ShellView">
    <DockPanel LastChildFill="True">
	
        <ToolBar DockPanel.Dock="Top">
            <Button cal:Message.Attach="[Event Click] = [Action ShowCustomers]" Content="Customers" />
            <Button cal:Message.Attach="[Event Click] = [Action ShowOrders]" Content="Orders" />
        </ToolBar>

        <ContentControl cal:View.Model="{Binding CurrentPresenter}" />
		
    </DockPanel>
</UserControl>

Tabbed Interface

Let’s create a more sophesticate UI that allows us host multiple views at the same time. You may know that it is originally not possible to do MDI interface in WPF (there were community implementations though) however it would be possible to achieve similar results using a TabControl.

To convert our single-view to a tabbed-view there are two things to do. First we should change the base class of our ShellViewModel to MultiPresenterManager instead of PresenterManager, then we need to change the UI part (ShellView) to host a collection of IPresenters in a tabbed interface. To change the UI, remove the ContentControl and add a TabControl with its ItemsSource bound to Presenters property and SelectedItem bound to CurrentPresenter property. So far it all comes down to one line of code change on the UI:

<TabControl ItemsSource="{Binding Presenters}" SelectedItem="{Binding Path=CurrentPresenter}" />

Initial Tabbed View

As you can see the picture above, it is not what we expected, or is it? You may have guessed what is missing. We need to provide two things to the TabControl: A template for Header part where tab item’s header will be displayed, and another template for the content part of the tab item where our view will be displayed. For the header part, Caliburn already has a property named DisplayName which is designed to do just that.

<TabControl ItemsSource="{Binding Presenters}" 
            SelectedItem="{Binding Path=CurrentPresenter}">
	
    <TabControl.ItemTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding DisplayName}"/>
        </DataTemplate>
    </TabControl.ItemTemplate>
	
    <TabControl.ContentTemplate>
        <DataTemplate>
            <ContentControl cal:View.Model="{Binding}" />
        </DataTemplate>
    </TabControl.ContentTemplate>
	
</TabControl>

The result is a more elegant user interface and our tab control displays and hosts our presenters correctly.

Tabbed View

Closing Tabs

If you have noticed, one piece of the puzzle is still missing. How do we close a view once we have opened it? Let’s retemplate our ItemTemplate in the TabControl and add button with a glyph that will close the Presenter once clicked. First let’s add a method in our ShellView to close the passed presenter.

using Caliburn.PresentationFramework.ApplicationModel;

public class ShellViewModel : MultiPresenterManager, IShellViewModel
{
    public void ShutdownPresenter(IPresenter presenter)
    {
        this.Shutdown(presenter);
    }
}

Notice the Using keyword. There is an extension method for shutdown so we’re importing the extension method with using keyword and just pass the presenter to be shutdown. Here’s our complete new template:

<DataTemplate>
    <ContentControl>
        <DockPanel>
            <Button Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}" 
                Visibility="{Binding CanShutdown, Mode=TwoWay, Converter={StaticResource VisibilityConverter}}"
                cal:Message.Attach="[Event Click] = [Action ShutdownPresenter($datacontext)]"
		DockPanel.Dock="Right">
                    <Path Stretch="Fill" StrokeThickness="0.5" 
                          Stroke="#FF333333" Fill="#FF969696" 
                          Data="F1 M 2.28484e-007,1.33331L 1.33333,0L 4.00001,2.66669L 6.66667,6.10352e-005L 8,1.33331L 5.33334,4L 8,6.66669L 6.66667,8L 4,5.33331L 1.33333,8L 1.086e-007,6.66669L 2.66667,4L 2.28484e-007,1.33331 Z " 
                          HorizontalAlignment="Stretch" 
                          VerticalAlignment="Stretch"/>
            </Button>
            <TextBlock Text="{Binding DisplayName}" />
        </DockPanel>
    </ContentControl>
</DataTemplate>

Did you see the part where we are attaching a Message to the Click event of the button? In Caliburn you can attach a method of your ViewModel to the UI element. What’s cool about it (well there are many cool things about it) is that you can even pass the DataContext around and Caliburn will do the necessary casting for you so in our case, DataContext of the Button will be cast as an IPresenter and will be injected to the method when user clicks on the close button.

Final Outcome

One thing that you may experience when closing the Shell view (Main Window), is that by doing so, shell will give all the open presenters a chance to clean up and close gracefully. In case a presenter is not in a closeable state, the close operation on the entire application will be cancelled.

That’ all there is to it. Now we have an elegant tabbed UI where user can open and close each tab separately. You can grab the source code of this part from here.