Building a Visual Studio Style Tabbed Interface with Caliburn

Monday, January 18, 2010 by Nigel Sampson

In the application I'm currently working I'm using an interface patten similar to Visual Studio with multiple tabs (work items) open at any one time. Caliburn has some great utiltity baked in for this style of application with their IPresenter Component Model.  Ultimately we'll be building something that looks like:

Visual Studio Style Tabs

Note: There was an issue in the v1 RTW that caused the following example to function incorrectly. The 1.1 branch in the Codeplex Repository correct this.

While this post is mainly targeting WPF a lot of it (minus the ContextMenu) can be used in Silverlight as well.

I'm going to assume you have some experience with setting up Caliburn and won't dig too much into that if not I'd start at the Caliburn documentation. By default Caliburn wants to use IServiceLocator to create the child presenters, I've created a simple IViewModelFactory to allow me to plug extra functionality into this process, for this article the implementation is as follows.

public class DefaultViewModelFactory : IViewModelFactory

{

    private readonly IServiceLocator serviceLocator;

 

    public DefaultViewModelFactory(IServiceLocator serviceLocator)

    {

        if(serviceLocator == null)

            throw new ArgumentNullException("serviceLocator");

 

        this.serviceLocator = serviceLocator;

    }

 

    public T Create<T>() where T : IPresenter

    {

        return serviceLocator.GetInstance<T>();

    }

}

We'll start with building the ViewModel for our application, we'll name it WorkspaceViewModel and because we'll be dealing with multiple "child" presenters and the idea of a current presenter we'll inherit this class from MultiPresenterManager. After that we'll add some methods to manage the child presenters, specially CloseThis, CloseAll and CloseAllButThis. We'll also create a dummy method to open a sample "child view", more like the WorkspaceViewModel will be responding to events from an event aggregator to open child views.

public class WorkspaceViewModel : MultiPresenterManager

{

    private readonly IViewModelFactory viewModelFactory;

 

    public WorkspaceViewModel(IViewModelFactory viewModelFactory)

    {

        this.viewModelFactory = viewModelFactory;

    }

 

    public void OpenSimpleViewModel()

    {

        var viewModel = viewModelFactory.Create<SimpleViewModel>();

 

        this.Open(viewModel);

    }

 

    public void CloseAll()

    {

        foreach(Presenter presenter in Presenters.ToList())

        {

            presenter.Close();

        }

    }

 

    public void ClosePresenter(Presenter selectedPresenter)

    {

        selectedPresenter.Close();

    }

 

    public void CloseAllButThis(Presenter selectedPresenter)

    {

        foreach(Presenter presenter in Presenters.ToList())

        {

            if(presenter == selectedPresenter)

                continue;

 

            presenter.Close();

        }

    }

}

Now we have the ViewModel we need to build the corresponding view. To we'll need a couple of things to begin with, obviously a TabControl to display our child presenters and little button to open new presenters.

<TabControl Grid.Row="1" x:Name="Presenters" ItemsSource="{Binding Presenters}" SelectedItem="{Binding CurrentPresenter}">

    <TabControl.ItemTemplate>

        <DataTemplate>

            <Grid>

                <Grid.ColumnDefinitions>

                    <ColumnDefinition Width="*" />

                </Grid.ColumnDefinitions>

                <TextBlock Text="{Binding DisplayName}" />

            </Grid>

        </DataTemplate>

    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>

        <DataTemplate>

            <ContentControl caliburn:View.Model="{Binding}" />

        </DataTemplate>

    </TabControl.ContentTemplate>

</TabControl>

This should be enough to get started, the button is attached to the OpenPresenter and when clicked the Presenters collection is changed which opens another tab in the TabControl.

<Button Margin="4" Content="Open" caliburn:Message.Attach="OpenSimpleViewModel"/>

Now lets hook up the familar context menu on the tabs, we'll need to do a bit of wrangling with the Message Target in Caliburn as we want to target the actual presenter and not the current DataContext (in the TabItem it's the child presenter and not the workspace presenter). The simplest way to do this is to bind to something outside the tab control. We can then attach messages for CloseThis, CloseAll and CloseAllButThis to each menu item.

<UserControl.Resources>

    <FrameworkElement x:Key="DataContextReference"/>

</UserControl.Resources>

<UserControl.DataContext>

    <Binding Mode="OneWayToSource" Path="DataContext" Source="{StaticResource DataContextReference}"/>

</UserControl.DataContext>

<ContextMenu caliburn:Action.TargetWithoutContext="{Binding DataContext, Source={StaticResource DataContextReference}}">

    <MenuItem Header="Close" caliburn:Message.Attach="ClosePresenter($dataContext)" />

    <MenuItem Header="Close All But This" caliburn:Message.Attach="CloseAllButThis($dataContext)" />

    <MenuItem Header="Close All" caliburn:Message.Attach="CloseAll" />

</ContextMenu>

Now the only thing we've left to do is recreate the the drop down and close box in the top right corner. The combobox is bound to the same values as the tab control and the button is attached to the CloseThis method using the currently selected tab as the parameter.

<Button Margin="4" Content="x" caliburn:Message.Attach="ClosePresenter(Presenters.SelectedItem)"/>

<ComboBox ItemsSource="{Binding Presenters}" SelectedItem="{Binding CurrentPresenter}" Margin="4" DisplayMemberPath="DisplayName"/>

The full xaml for the view is as follows.

<UserControl

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:caliburn="http://www.caliburnproject.org"

    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

    x:Class="CompiledExperience.Azure.Explorer.Client.Shell.Views.WorkspaceView"

   mc:Ignorable="d" d:DesignWidth="503.507" d:DesignHeight="384.96"

   >

    <UserControl.Resources>

        <FrameworkElement x:Key="DataContextReference"/>

    </UserControl.Resources>

    <UserControl.DataContext>

        <Binding Mode="OneWayToSource" Path="DataContext" Source="{StaticResource DataContextReference}"/>

    </UserControl.DataContext>

    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="Auto" />

            <RowDefinition/>

        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">

            <Button Margin="4" Content="Open" caliburn:Message.Attach="OpenSimpleViewModel"/>

            <Button Margin="4" Content="x" caliburn:Message.Attach="ClosePresenter(Presenters.SelectedItem)"/>

            <ComboBox ItemsSource="{Binding Presenters}" SelectedItem="{Binding CurrentPresenter}" Margin="4" DisplayMemberPath="DisplayName"/>

        </StackPanel>

        <TabControl Grid.Row="1" x:Name="Presenters" ItemsSource="{Binding Presenters}" SelectedItem="{Binding CurrentPresenter}">

            <TabControl.ItemTemplate>

                <DataTemplate>

                    <Grid>

                        <Grid.ContextMenu>

                            <ContextMenu caliburn:Action.TargetWithoutContext="{Binding DataContext, Source={StaticResource DataContextReference}}">

                                <MenuItem Header="Close" caliburn:Message.Attach="ClosePresenter($dataContext)" />

                                <MenuItem Header="Close All But This" caliburn:Message.Attach="CloseAllButThis($dataContext)" />

                                <MenuItem Header="Close All" caliburn:Message.Attach="CloseAll" />

                            </ContextMenu>

                        </Grid.ContextMenu>

                        <Grid.ColumnDefinitions>

                            <ColumnDefinition Width="*" />

                        </Grid.ColumnDefinitions>

                        <TextBlock Text="{Binding DisplayName}" />

                    </Grid>

                </DataTemplate>

            </TabControl.ItemTemplate>

            <TabControl.ContentTemplate>

                <DataTemplate>

                    <ContentControl caliburn:View.Model="{Binding}" />

                </DataTemplate>

            </TabControl.ContentTemplate>

        </TabControl>

    </Grid>

</UserControl>

There's a good Stack Overflow question about styling the tab control like Visual Studio so I'll leave to that to someone with actual design skills but from here you're pretty much complete. Some stuff you'll most likely want to extend it with is dealing with duplicate tabs, you may not want to have two options tabs open and so on.

Shout it kick it on DotNetKicks.com

View Comments

Convention Based Event Aggregation in WPF and Silverlight Applications

Wednesday, December 16, 2009 by Nigel Sampson

One thing that can get pretty tedious very quickly in building composite UI's in Silverlight and WPF is wiring up an event aggregator. Typically if a service or view model wants to subscribe to an event in the application you would declare a dependency on IEventAggregator and call Subscribe on it passing the method to invoke when the event is published.

The Event Aggregator we'll be using for this example is based off something like the "Suck Less Event Aggregator" and the introduction of non generic methods required for this example. The interface for this example looks as follows.

public interface IEventAggregator

{

    void Subscribe<T>(Action<T> action);

    void Subscribe<T>(Action<T> action, bool keepSubscriberAlive);

 

    void Subscribe(Type eventType, Delegate action);

    void Subscribe(Type eventType, Delegate action, bool keepSubscriberAlive);

 

    void Unsubscribe<T>(Action<T> action);

    void Unsubscribe(Type eventType, Delegate action);

 

    void Publish<T>(T eventToPublish);

}

This seems pretty standard interface the problem is that over a whole applications worth of services and view models it's a lot of repetitious code to subscribe to all the events we're interested in. Lots of the view models will have dependencies on the event aggregator which could end mean a lot of repetitious unit tests.

What I'm going to try and show today is how to use some conventions to simplify your code. We'll adopt the convention that any public method on a View Model named "OnX" on a and has one parameter should be subscribed to an event (the type of the event will be based on the parameter type).

I'm currently using Caliburn to build my WPF applications but Prism is the same in this regard. Both don't have an explicit way to construct a new View Model, they simply want you to depend on the IServiceLocator or IUnityContainer and use it to Resolve your new ViewModel. Since we'll be wanting to insert some functionality into this process we'll create our own IViewModelFactory it'll be pretty simple and just shift the dependency on IServiceLocator into a specific place. The interface and implementation are.

public interface IViewModelFactory

{

    T Create<T>() where T : IPresenter;

}

 

public class DefaultViewModelFactory : IViewModelFactory

{

    private readonly IServiceLocator serviceLocator;

 

    public DefaultViewModelFactory(IServiceLocator serviceLocator)

    {

        if(serviceLocator == null)

            throw new ArgumentNullException("serviceLocator");

 

        this.serviceLocator = serviceLocator;

    }

 

    public T Create<T>() where T : IPresenter

    {

        var viewModel = serviceLocator.GetInstance<T>();

 

        return viewModel;

    }

}

We'll use the Decorator Pattern to add extra functionality to our View Model factory, our EventAggregatorViewModelFactory takes a dependency on another IViewModelFactory and uses that to actually create the viewModel, we'll simply do some extra work on the result before passing back to the caller. The Decorator pattern is great in this regard in that I can plug even more functionality into the pipeline of view model creation without requiring each piece of functionality to know about any of the others.

public class EventAggregationViewModelFactory : IViewModelFactory

{

    private readonly IViewModelFactory viewModelFactory;

    private readonly IEventAggregator eventAggregator;

 

    public EventAggregationViewModelFactory(IViewModelFactory viewModelFactory, IEventAggregator eventAggregator)

    {

        this.viewModelFactory = viewModelFactory;

        this.eventAggregator = eventAggregator;

    }

 

    public T Create<T>() where T : IPresenter

    {

        var viewModel = viewModelFactory.Create<T>();

 

        AttachSubscriptions(viewModel);

 

        return viewModel;

    }

 

    protected void AttachSubscriptions<T>(T viewModel) where T : IPresenter

    {

        var methods = from m in viewModel.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)

                      where IsEventSubscriptionMethod(m)

                      select m;

 

        foreach(var method in methods)

        {

            var eventType = method.GetParameters()[0].ParameterType;

            var delegateType = Expression.GetActionType(eventType);

            var @delegate = Delegate.CreateDelegate(delegateType, viewModel, method);

 

            eventAggregator.Subscribe(eventType, @delegate);

        }

    }

 

    protected virtual bool IsEventSubscriptionMethod(MethodInfo method)

    {

        return method.Name.StartsWith("On") && method.GetParameters().Length == 1;

    }

}

AttachSubscriptions is where the extra functionality is added, we first find all methods on the ViewModel that meet the convention (note the logic is in a method that can be overridden to change the convention). Once we have the methods we're interested in we create a delegate  (Action<T>) based on the event type and the method, we then call subscribe on the event aggreator using the event type and our delegate. Pretty simple really, the only complicated part is that most event aggregators don't come with a non generic interface, this could be avoided if you're ok with using reflection to invoke one of the generic methods instead.

I'm wiring the entire thing together using Ninject and the following code.

kernel.Bind<IViewModelFactory>().To<EventAggregationViewModelFactory>().InSingletonScope();

kernel.Bind<IViewModelFactory>().To<DefaultViewModelFactory>().WhenInjectedInto<EventAggregationViewModelFactory>().InSingletonScope();

With this in place we can now simplify an example view model from the following

public class DependentCustomerViewModel : Presenter

{

    private readonly IEventAggregator eventAggregator;

 

    public DependentCustomerViewModel(IEventAggregator eventAggregator)

    {

        this.eventAggregator = eventAggregator;

    }

 

    protected override void OnInitialize()

    {

        base.OnInitialize();

 

        eventAggregator.Subscribe<CustomerCreatedEvent>(OnCustomerCreated);

    }

 

    protected void OnCustomerCreated(CustomerCreatedEvent customerCreatedEvent)

    {

        // Business logic for customer creation

    }

}

to

public class ConventionCustomerViewModel : Presenter

{

    public void OnCustomerCreated(CustomerCreatedEvent customerCreatedEvent)

    {

        // Business logic for customer creation

    }

}

Shout it kick it on DotNetKicks.com

View Comments

Caliburn: Binding Conventions

Wednesday, December 02, 2009 by Nigel Sampson

Lately I've been playing around with Caliburn in WPF (and a little in Silverlight). One of the things I've really enjoyed is the convention based binding between the View and the ViewModel (Presenter). The default binding (DefaultBinder) helps with the more complex screen activiation by binding the Presenters collection and exposed Presenter objects on the ViewModel. It does all this binding based on the name of the control and the exposed property.

One thing it doesn't do is bind any simple exposed properties to the view, such has simple strings, collections and so forth. To implement this we'll create a new IBinder, to ensure we keep all the existing conventions we'll inherit our new ConventionBinder from DefaultBinder and override ApplyBindingConventions.

The logic for this will be fairly simple, we'll loop through all public instance variables on the model and look for a control of the same name. If we find one we'll look up which property to bind to and ensure the types are comparable, after that we create the Binding object based on the type of property we're binding to.

public class ConventionBinder : DefaultBinder

{

    private readonly Dictionary<Type, DependencyProperty> defaultProperties = new Dictionary<Type, DependencyProperty>

    {

        {typeof(TextBox), TextBox.TextProperty},

        {typeof(TextBlock), TextBlock.TextProperty},

        {typeof(ItemsControl), ItemsControl.ItemsSourceProperty}

    };

 

    public ConventionBinder(IServiceLocator serviceLocator, IActionFactory actionFactory, IMessageBinder messageBinder)

        : base(serviceLocator, actionFactory, messageBinder)

    {

    }

 

    protected override void ApplyBindingConventions(DependencyObject element, object model)

    {

        base.ApplyBindingConventions(element, model);

 

        if(!(element is FrameworkElement))

            return;

 

        foreach(var property in model.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance))

        {

            var bindingElement = FindControl(element, property.Name);

 

            if(bindingElement == null)

                continue;

 

            var defaultProperty = GetDefaultProperty(bindingElement.GetType());

 

            if(defaultProperty == null)

                continue;

 

            if(!defaultProperty.PropertyType.IsAssignableFrom(property.PropertyType))

                continue;

 

            BindingOperations.SetBinding(bindingElement, defaultProperty, new Binding(property.Name)

            {

                Mode = GetDirectionForProperty(property)

            });

        }

    }

 

    private DependencyProperty GetDefaultProperty(Type elementType)

    {

        do

        {

            if(defaultProperties.ContainsKey(elementType))

                return defaultProperties[elementType];

 

            elementType = elementType.BaseType;

 

        } while(elementType != null);

 

        return null;

    }

 

    protected virtual BindingMode GetDirectionForProperty(PropertyInfo property)

    {

        if(property.CanRead && property.CanWrite)

            return BindingMode.TwoWay;

 

        if(property.CanRead)

            return BindingMode.OneWay;

 

        if(property.CanWrite)

            return BindingMode.OneWayToSource;

 

        return BindingMode.Default;

    }

}

Now we just need to configure Caliburn to use the new binder. Now all properties will be bound to the controls with the appropriate name.

protected override void ConfigurePresentationFramework(PresentationFrameworkModule module)

{

    module.UsingBinder<ConventionBinder>();

}

The list of default properties is in no way exhaustive, just add to it when you want a new convention.

Shout it kick it on DotNetKicks.com

View Comments

Is being Blendable worth it?

Tuesday, November 17, 2009 by Nigel Sampson

So I've been going over this with a few others lately, is ensuring your project is "Blendable" worth it?

The Model, View, ViewModel pattern does a great job in separating the UI from the logic for each screen, at some point however they have to be integrated. This has been the part I've been showing in the screencasts of this series. Going over this I feel there are a few areas that ensure good usage of Expression Blend.

  1. Being able to create and edit a View using Blend, ultimately this means a parameter-less constructor no business logic in the constructor (shouldn't be doing this with MVVM anyway).

  2. Being able to integrate the View and ViewModel, originally I tried to use the Blend UI as much as possible for this, but ultimately I don't think it's feasible, a lot of the integration will be done in xaml. The Blend UI doesn't have enough features to be able to do all the integration, it could be possible with a plethora of Behaviors for each custom task but it could get complicated.

Overall while Blend is a great tool for putting together your view it doesn't do well for integrating the view and view model. I think it's still useful to ensure Blend is usable, but limiting yourself to only what you can do in the Blend UI is counter productive.

View Comments

Blendable MVVM: Frameworks

Sunday, November 08, 2009 by Nigel Sampson

I've had a few emails from people wanting to see some of the concepts here made into a simple MVVM framework. To be honest I don't believe it's worth it. Most of ideas here are prevalent in other MVVM frameworks and they've all done a fantastic pulling them together in a cohesive manner.  Some one's I've been looking at for use in a WPF project are:

  • Caliburn: This library is very rich and features, it's probably the most "different" from the others. Of particular interest is the "Convention over Configuration" for binding the View to the ViewModel. I'm looking at possibly integrating this with Prism (for modularity and view composition).

  • MVVM Light: A the name says, a lighter library, very much a long the lines of the Blendable MVVM series I wrote about here. It was excellent additions like event aggregation and templates to make your life a lot easier. Highly reccomend.

View Comments

Viewing posts 1 to 5 of 69.