Search code examples
c#wpfcaliburn.microcommandbinding

Dynamic menus and submenus with Caliburn Micro, how can I bind the commands?


I am using Caliburn Micro in a WPF project where I want plugin components to be able to populate a toolbar. Each plugin gets a top level menu item and can populate it with submenus if they choose.

Example

<ToolBar>
    <Menu x:Name="ToolBarMenuItems">
        <Menu.ItemContainerStyle>
            <Style TargetType="{x:Type MenuItem}">
                <Setter Property="Header" Value="{Binding Path=Text}" />
                <Setter Property="ItemsSource" Value="{Binding Path=Children}" />
            </Style>
        </Menu.ItemContainerStyle>
    </Menu>
</ToolBar>

ToolBarMenuItems is a BindableCollection of an interface:

public BindableCollection<IMenuItemViewModel> ToolBarMenuItems { get; set; }

public interface IMenuItemViewModel
{
    string Text { get; set; }

    ObservableCollection<IMenuItemViewModel> Children { get; set; }

    bool CanRunCommand();

    void RunCommand();
}

This works fine as far as creating the menu goes, the problem is connecting the menu items to the RunCommand/CanRunCommand. Dynamic menus with Caliburn micro explained how to do it on a MenuItem:

<Menu>
    <MenuItem x:Name="ToolBarMenuItems" DisplayMemberPath="Text" Header="MyMenu" cal:Message.Attach="RunToolbarCommand($originalsourcecontext)"/>
</Menu>

Where RunToolBarCommand is a method in the view model public void RunToolbarCommand(IMenuItemViewModel menuItem) and $originalsourcecontext refers to setting the following in the bootstrapper

MessageBinder.SpecialValues.Add("$originalsourcecontext", context =>
{
    var args = context.EventArgs as RoutedEventArgs;
    var fe = args?.OriginalSource as FrameworkElement;
    return fe?.DataContext;
});

But as I said, I need to do this for the entire menu and not just on a MenuItem, but I don't know how to use Caliburn Micro to bind the methods.


Solution

  • Add a setter to your Style that sets the cal:Message.Attach attached property and passes the $executionContext:

    <Menu x:Name="ToolBarMenuItems">
        <Menu.ItemContainerStyle>
            <Style TargetType="{x:Type MenuItem}">
                <Setter Property="Header" Value="{Binding Path=Text}" />
                <Setter Property="ItemsSource" Value="{Binding Path=Children}" />
                <Setter Property="cal:Message.Attach" Value="RunCommand($executionContext)" />
            </Style>
        </Menu.ItemContainerStyle>
    </Menu>
    

    You need the context in your interface and implementation to be able to stop the event from bubbeling:

    public void RunCommand(ActionExecutionContext context)
    {
        if (context?.EventArgs is RoutedEventArgs routedEventArgs)
            routedEventArgs.Handled = true;
    
        MessageBox.Show("Run!");
    }
    

    Please refer to this question for more information about this.