Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Conducting MahApps.Metro HamburgerMenu with Caliburn Micro

I am having a few issues with using Caliburn Micro's Conductor<>.Collection.OneActive with MahApps.Metro HamburgerMenu. From a few samples, but none of them address my scenario.

All of my code is available in this Github repository.

I want to show a set of panes inside a HamburgerMenu. Each pane has a title and a display name:

public interface IPane : IHaveDisplayName, IActivate, IDeactivate
{
    PackIconModernKind Icon { get; }
}

In my case, IPane is implemented using PaneViewModel:

public class PaneViewModel : Screen, IPane
{
    public PaneViewModel(string displayName, PackIconModernKind icon)
    {
        this.Icon = icon;
        this.DisplayName = displayName;
    }

    public PackIconModernKind Icon { get; }
}

This has the following view:

<UserControl x:Class="CaliburnMetroHamburgerMenu.Views.PaneView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             Padding="12"
             Background="Pink">
    <StackPanel Orientation="Vertical">
        <TextBlock Text="Non-bound text" />
        <TextBlock x:Name="DisplayName" FontWeight="Bold" />
    </StackPanel>
</UserControl>

My shell view model is also quite simple. It inherits from Conductor<IPane>.Collection.OneActive, and takes in a list of panes that it adds to its Items collection:

public class ShellViewModel : Conductor<IPane>.Collection.OneActive
{
    public ShellViewModel(IEnumerable<IPane> pages)
    {
        this.DisplayName = "Shell!";

        this.Items.AddRange(pages);
    }
}

Now, this is very it gets fuzzy for me. This is an excerpt from ShellView.xaml:

<controls:HamburgerMenu 
    ItemsSource="{Binding Items, Converter={StaticResource PaneListToHamburgerMenuItemCollection}}"
    SelectedItem="{Binding ActiveItem, Mode=TwoWay, Converter={StaticResource HamburgerMenuItemToPane}}">

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

    <controls:HamburgerMenu.ItemTemplate>
        <DataTemplate>
                <Grid x:Name="RootGrid"
                      Height="48"
                      Background="Transparent">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="48" />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <iconPacks:PackIconModern 
                        Grid.Column="0"
                        Kind="{Binding Icon}" 
                        HorizontalAlignment="Center"
                        VerticalAlignment="Center"
                        Foreground="White" />

                    <TextBlock Grid.Column="1"
                               VerticalAlignment="Center"
                               FontSize="16"
                               Foreground="White"
                               Text="{Binding Label}" />
                </Grid>
        </DataTemplate>
    </controls:HamburgerMenu.ItemTemplate>
</controls:HamburgerMenu>

To make this work, I rely on two converters (who quite frankly do more than they should have to). One converter takes a ICollection<IPane> and creates a HamburgerMenuItemCollection with HamburgerMenuIconItems that are now contain a two-way link using the Tag properties of both the view model and the menu item.

class PaneListToHamburgerMenuItemCollection : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        var viewModels = value as ICollection<IPane>;

        var collection = new HamburgerMenuItemCollection();

        foreach (var vm in viewModels)
        {
            var item = new HamburgerMenuIconItem();
            item.Label = vm.DisplayName;
            item.Icon = vm.Icon;
            item.Tag = vm;
            vm.Tag = item;
            collection.Add(item);
        }

        return collection;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The second converter converts between the view model and the menu item using this Tag whenever the SelectedItem changes:

class HamburgerMenuItemToPane : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((IPane)value)?.Tag;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((HamburgerMenuIconItem)value)?.Tag;
    }
}

When I run this code, and click the items in the hamburger menu, the page switches every time. The issue is that when the app first runs, there is no selected pane, and you cannot set one using any of the activation overrides available in ShellViewModel (such as OnViewAttached or OnActivate, or event the constructor), as the converter code that hooks up the Tag hasn't run yet.

My requirements for a working solution:

  1. Caliburn's conductor must be in charge, as there are views and view models further down the stack that depend on the activation logic to run.
  2. It should be possible to activate the first item from Caliburn at some point during the activation of ShellViewModel
  3. Should respect separation of concerns, i.e. the view model should not know that a hamburger menu is being used in the view.

Please see the GitHub repository for a solution that should run straight away.

like image 242
Vegard Larsen Avatar asked Jan 30 '26 05:01

Vegard Larsen


1 Answers

I believe the issue is caused by the HamburgerMenu_Loaded method inside the control. If there is a selected item before the control loads, the content of the hamburger menu is replaced:

private void HamburgerMenu_Loaded(object sender, RoutedEventArgs e)
{
    var selectedItem = this._buttonsListView?.SelectedItem ?? this._optionsListView?.SelectedItem;
    if (selectedItem != null)
    {
        this.SetCurrentValue(ContentProperty, selectedItem);
    }
}

In your case, the ContentControl is removed and your Conductor cannot do its job.

I'm trying to see if this behavior can be changed in MahApps directly, by changing the code to something like this:

if (this.Content != null)
{
    var selectedItem = this._buttonsListView?.SelectedItem ?? this._optionsListView?.SelectedItem;
    if (selectedItem != null)
    {
        this.SetCurrentValue(ContentProperty, selectedItem);
    }
}
like image 132
Damien Chaib Avatar answered Feb 01 '26 19:02

Damien Chaib