WPF RibbonGroup backed by ViewModel?

Topics: Conventions, Extensibility, Getting Started, UI Architecture
May 10, 2012 at 9:08 AM

Hello.

I am trying to dynamically populate a WPF Ribbon using Caliburn Micro without much luck. I'm fairly new to WPF/CM so I hope it is something obvious that I'm missing?

Specifically, I am trying to populate RibbonTab instances with RibbonGroups, each one backed by its own ViewModel and corresponding View.xaml.

At the moment, I have dynamically loaded RibbonTabs working, but I can't seem to work out how to get CM to instantiate the appropriate RibbonGroup View instances.

My Views/RibbonView.xaml is as follows:

<UserControl x:Class="MyProject.Views.RibbonView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ribbon="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"
    xmlns:cal="http://www.caliburnproject.org">
    <UserControl.Resources>
        <!-- RibbonTab -->
        <Style TargetType="{x:Type ribbon:RibbonTab}">
            <Setter Property="Header" Value="{Binding Header}" />
            <Setter Property="KeyTip" Value="{Binding ShortcutKey}" />
            <Setter Property="ItemsSource" Value="{Binding Groups}" />
        </Style>
    </UserControl.Resources>
    <ribbon:Ribbon x:Name="Tabs" /> 
</UserControl>

The MyProject.ViewModels.RibbonViewModel class contains a bunch of stuff for building Tab and Group definitions dynamically using MEF, but in also contains:

public class RibbonViewModel : PropertyChangedBase
{
    ...
    public BindableCollection<RibbonTabViewModel> Tabs
    {
        get { return _tabs; }
        set { _tabs = value; NotifyOfPropertyChange(() => Tabs);}
    }
    ...
}

Likewise, the corresponding MyProject.ViewModels.RibbonTabViewModel:

    public class RibbonTabViewModel : PropertyChangedBase
    {
        private String _header;
        private String _shortcutKey;
        private BindableCollection<IRibbonGroup> _groups;

        public String Header
        {
            get { return _header; }
            set { _header = value; NotifyOfPropertyChange(() => Header); }
        }

        public String ShortcutKey
        {
            get { return _shortcutKey; }
            set { _shortcutKey = value; NotifyOfPropertyChange(() => ShortcutKey); }
        }

        public BindableCollection<IRibbonGroup> Groups
        {
            get { return _groups; }
            set { _groups = value; NotifyOfPropertyChange(() => Groups); }
        }

        public RibbonTabViewModel(String name, String shortcutKey, BindableCollection<IRibbonGroup> groups)
        {
            _header = name;
            _shortcutKey = shortcutKey;
            _groups = groups;
        }
    }

Where IRibbonGroup:

public interface IRibbonGroup : INotifyPropertyChangedEx
{
    String Header { get; set; }
}

Each concrete implementation of IRibbonGroup is a ViewModel, for example MyProject.ViewModels.NavigationGroupViewModel class:

public class NavigationGroupViewModel : PropertyChangedBase, IRibbonGroup
{
    private String _header;
    public String Header
    {
        get { return _header; }
        set { _header = value; NotifyOfPropertyChange(() => Header); }
    }

    ... // Other properties specific to this ViewModel
}

It has a corresponding MyProject.Views.NavigationGroupView.xaml file:

<ribbon:RibbonGroup x:Class="MyProject.Views.NavigationGroupView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ribbon="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"
    xmlns:cal="http://www.caliburnproject.org"
    xmlns:helpers="clr-namespace:MyProject.Views.Helpers"
    Header="{Binding Header}"
 >
    <ribbon:RibbonGroup.Resources>
        <helpers:BooleanToVisibilityConverter x:Key="VisibilityConverter" />
    </ribbon:RibbonGroup.Resources>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center">
            <ribbon:RibbonMenuButton Label="Bookmarks" SmallImageSource="{StaticResource BookIcon}" ToolTip="Bookmarks Menu"
                                             CanUserResizeHorizontally="true"
                                             CanUserResizeVertically="true"
                                             cal:Message.Attach="[Event DropDownClosed] = [Action BookmarksMenuClosed]"
                                             >
                ... // Menu items
            </ribbon:RibbonMenuButton>
            ... // More controls
        </StackPanel>
</ribbon:RibbonGroup>

Now, the symptoms I have are as follows:

  • The tabs render as expected (i.e. multiple Tabs, with correct Header and KeyTip shortcuts etc)
  • There are the correct number of RibbonGroup elements within the tab, however:
  • The RibbonGroup itself is rendered as a string showing "MyProject.ViewModels.NaviationGroupViewModel", rather than the actual View defined in NavigationGroupViewModel.xaml

(Note: I also tried making the NavigationGroupView.xaml define a UserControl with nested ribbon:RibbonGroup, but it didn't work either).

I had partial success by adding this additional markup to RibbonView.xaml under <UserControl.Resources> :

        <!-- RibbonGroup -->
        <Style TargetType="{x:Type ribbon:RibbonGroup}" BasedOn="{StaticResource RibbonControlStyle}">
            <Setter Property="Header" Value="{Binding Header}" />
        </Style>

With this additional markup, the name of the RibbonGroup is correctly rendered, but the view defined in NavigationGroupView.xaml is still not used!

How do I get it to use my ViewModel and View for each RibbonGroup? I know I'm missing something here..

I would have expected the binding in <ribbon:Ribbon x:Name="Tabs" /> to instantiate specific RibbonGroup instances according to the CM naming convention and my supplied concrete ViewModel and Views, however I suspect it is merely creating vanilla RibbonGroup instances, which would explain why the additional RibbonGroup style template causes it to be labeled correctly but not show the View...

Can anyone please help me with this? Do I need to define a ConventionManager.AddElementConvention<RibbonGroup>() or something to make it correctly resolve Views from ViewModels and bind it in the ribbon? If so, how would I do it?

Thanks in advance for any guidance you can give!

Scott

 

xmlns:helpers="clr-namespace:MyProject.Views.Helpers"
May 10, 2012 at 10:32 AM

Have you provided a proper RibbonTab view? If not, note that in this case you are using convention to populate tabs, but even if you provided a view for your groups, you have not instructed the framework to search for views when a ribbon group is displayed. The fact that you see a simple string instead of groups, means that the tab is trying to display the group searching for a DataTemplate which is not present.

May 11, 2012 at 12:05 AM

Thanks for the suggestion. I just tried adding a RibbonTabView.xaml to go with RibbonTabViewModel.cs as follows (and removed the previous style setter resources in RibbonView.xaml):

 

<ribbon:RibbonTab x:Class="MyProject.Views.RibbonTabView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ribbon="http://schemas.microsoft.com/winfx/2006/xaml/presentation/ribbon"
    xmlns:cal="http://www.caliburnproject.org"
    Header="{Binding Header}" KeyTip="{Binding ShortcutKey}"
    x:Name="Groups"
    >
</ribbon:RibbonTab>

Now I appear to have exactly the same problem as described in this post (i.e. the tab labels are missing, and consist just the model class string "MyProject.ViewModels.RibbonTabViewModel"):

 

    http://caliburnmicro.codeplex.com/discussions/352528

The screenshot in that post is exactly what I am seeing.

Turing on CM debugging, I can see that the Tabs ItemTemplate is applied, but there is no mention of "Setting DC of RibbonTabView to RibbonTabViewModel", which is what I get for all the other working CM view bindings:

 

Caliburn.Micro.ConventionManager: 2012-05-11 07:54:34,161 [1] INFO  - ViewModel bound on Ribbon.
Caliburn.Micro.ViewModelBinder: 2012-05-11 07:54:34,387 [1] INFO  - Binding MyProject.Views.RibbonView and MyProject.ViewModels.RibbonViewModel.
Caliburn.Micro.Action: 2012-05-11 07:54:34,414 [1] INFO  - Setting DC of MyProject.Views.RibbonView to MyProject.ViewModels.RibbonViewModel.
Caliburn.Micro.Action: 2012-05-11 07:54:34,442 [1] INFO  - Attaching message handler MyProject.ViewModels.RibbonViewModel to MyProject.Views.RibbonView.
Caliburn.Micro.ConventionManager: 2012-05-11 07:54:35,242 [1] INFO  - ItemTemplate applied to Tabs.
Caliburn.Micro.ViewModelBinder: 2012-05-11 07:54:35,268 [1] INFO  - Binding Convention Applied: Element Tabs.
Caliburn.Micro.ViewModelBinder: 2012-05-11 07:54:35,295 [1] INFO  - Binding Convention Applied: Element Ribbon.

Any idea why the RibbonTabView is not being applied? Could there be a problem with the ItemTemplate binding for Tabs being the incorrect type or something?

Thanks again for your help.

Scott

May 11, 2012 at 2:16 AM

from what I see, all you did was take out the usercontrol and named it RibbonTabView, making the actual control the base for the view, but the framework doesn't understand because of that and the fact it doesn't actually know anything about the Ribbon and its parts in convention format.

Therefore it has a hissy and defaults to what it knows that is ToString().

May 11, 2012 at 10:35 AM

Ok, I reviewed the issue, and checked how I approached this problem in a previous project.

The problem is common to a lot of specialized ItemsControl-derived controls (such as MenuItem), and is related to the fact that unless the ItemsSource is a collection of items accepted by the IsItemItsOwnContainer as containers, whatever you will pass to the collection will be 'wrapped' into a newly created container. In this case, moreover, both ItemTemplate and ItemTemplateSelectors do not work as expected... (check with ILSpy the PrepareContainerForItemOverride function).

This situation lead me to design a special class (ViewCollection), used to resolve views using the ViewLocator and bind them to their view-model, so that the ItemsSource can be directly bound to views, instead of view-models. The idea is quite simple: leverage view-location and binding internally, exposing just the already prepared views.

Attached, a project containing the class I'm talking about and a simple repro project.

Note that there is a chance that you could use a custom conventions to try to leverage the Ribbon without using the above collection, but I think such convention could be really complex: as I said, you need to provide proper containers in the ItemsSource for Ribbon (RibbonTab), RibbonTab (RibbonGroup) and RibbonGroup (RibbonControl), so an option could be to create 'shadow' ViewModelItemsSource properties, and react to their changes to leverage the concrete Items/ItemsSource.

Another option could be to make the collection part of a convention, but I find this approach a last-resort in case the ItemsControl is really databind-unfriendly...

May 14, 2012 at 1:05 AM

That is a nice solution, very consistent with the way the rest of the CM views work. Thanks.

FYI, I also found another solution that works, but is not quite as elegant:

The idea is to use a conventions-based approach to populate the RibbonTabs and RibbonGroups, with style setters in RibbonView.xaml resources as follows:

 

        <!-- RibbonTab -->
        <Style TargetType="{x:Type ribbon:RibbonTab}">
            <Setter Property="Header" Value="{Binding Header}" />
            <Setter Property="KeyTip" Value="{Binding ShortcutKey}" />
            <Setter Property="ItemsSource" Value="{Binding Groups}" />
        </Style>
        <!-- RibbonGroup -->
        <DataTemplate x:Key="GroupTemplate">
            <ContentControl cal:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
        </DataTemplate>
        <Style TargetType="{x:Type ribbon:RibbonGroup}">
            <Setter Property="Header" Value="{Binding Header}" />
            <Setter Property="ItemsSource" Value="{Binding Control}" />
            <Setter Property="cal:Bind.Model" Value="{Binding}" />
            <Setter Property="ItemTemplate" Value="{StaticResource GroupTemplate}" />
        </Style>

By adding only a single ControlViewModel to the RibbonGroup ItemsSource binding and forcing the ItemTemplate to use the CM ContentControl representation, we effectively get the ability to use a standard CM ViewModel for the RibbonGroup content.