Search code examples
c#wpfavaloniaui

WPF (Avalonia) Reusable ContextMenu Style


I have been following this tutorial to create a dynamic context menu in Avalonia: https://docs.avaloniaui.net/docs/controls/menu

I now have this image with an attached context menu:

<Image>
    <Image.ContextMenu>
        <ContextMenu Items="{Binding MenuItems}">
            <ContextMenu.Styles>
                <Style Selector="MenuItem">
                    <Setter Property="Header" Value="{Binding Header}"/>
                    <Setter Property="Items" Value="{Binding Items}"/>
                    <Setter Property="Command" Value="{Binding Command}"/>
                    <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
                </Style>
            </ContextMenu.Styles>
        </ContextMenu>
    </Image.ContextMenu>
</Image>

MenuItems is an IReadOnlyList<MenuItemViewModel> where MenuItemViewModel contains Header, Items, Command and CommandParameters on which my MenuItems are bound.

This works fine, I just fill my IReadOnlyList<MenuItemViewModel> from my containing ViewModel and the context menu is correctly created.

Now I would like to be able to declare the whole context menu xaml code in a Style or a template so I don't have to write again all this code when I use this dynamic custom menu but I'm not sure how to do this correctly.

I'd like to have something like this:

<Image>
    <Image.ContextMenu>
       <ContextMenu Items="{Binding MenuItems}" Template="{x:StaticResource DynamicContextMenu}" />
    </Image.ContextMenu>
</Image>

Avalonia doesn't exactly works like WPF, they use something called Templated Controls and their no real documentation how to use it. Maybe a simple style is enough.

Edit I managed to somewhat achieved it:

<Image.ContextMenu>
    <ContextMenu Items="{Binding MenuItems}">
         <ContextMenu.Styles>
             <StyleInclude Source="/Assets/Resources/Styles/DynamicMenuItem.axaml" />
         </ContextMenu.Styles>
    </ContextMenu>
</Image.ContextMenu>

DynamicMenuItem.axaml:

<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style Selector="MenuItem">
        <Setter Property="Header" Value="{Binding Header}"/>
        <Setter Property="Items" Value="{Binding Items}"/>
        <Setter Property="Command" Value="{Binding Command}"/>
        <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
    </Style>
</Styles>

I will now create a custom context menu that uses this style by default. If someone has an easiest way to achieve it, feel free to answer :)


Solution

  • I managed to get it kinda like I wanted. I created an Avalonia User Control that I made inherit ContextMenu instead of Control:

    public partial class DynamicContextMenu : ContextMenu, IStyleable
    {
        Type IStyleable.StyleKey => typeof(ContextMenu);
        public DynamicContextMenu()
        {
            InitializeComponent();
        }
    
        private void InitializeComponent()
        {
            AvaloniaXamlLoader.Load(this);
        }
    }
    

    Also deriving from IStyleable is really important to redefine StyleKey otherwise the control has no style and won't be rendering.

    The associated axaml:

    <ContextMenu xmlns="https://github.com/avaloniaui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                 mc:Ignorable="d"
                 x:Class="XXXX.CustomControls.DynamicContextMenu">
        <ContextMenu.Styles>
            <StyleInclude Source="/Assets/Resources/Styles/DynamicMenuItem.axaml" />
        </ContextMenu.Styles>
    </ContextMenu>
    

    The refered style DynamicMenuItem.axaml :

    <Styles xmlns="https://github.com/avaloniaui"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
        <Style Selector="MenuItem">
            <Setter Property="Header" Value="{Binding Header}"/>
            <Setter Property="Icon" Value="{Binding Icon}"/>
            <Setter Property="Items" Value="{Binding Items}"/>
            <Setter Property="Command" Value="{Binding Command}"/>
            <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
        </Style>
    </Styles>
    

    Now I can just use the custom control anywhere like this :

    <Image
    ...
    >
        <Image.ContextMenu>
            <cc:DynamicContextMenu Items="{Binding MenuItems}"/>
        </Image.ContextMenu>
    </Image>
    

    MenuItems behing an IList of MenuItemViewModel : public IReadOnlyList MenuItems { get; set; }

    and MenuItemViewModel.cs :

    public class MenuItemViewModel : ReactiveObject
    {
        private string? header;
        public string? Header { get => header; set => this.RaiseAndSetIfChanged(ref header, value); }
        private object? icon;
        public object? Icon { get => icon; set => this.RaiseAndSetIfChanged(ref icon, value); }
        public IReactiveCommand? Command { get; set; }
        private object? commandParameter;
        public object? CommandParameter { get => commandParameter; set => this.RaiseAndSetIfChanged(ref commandParameter, value); }
        public IList<MenuItemViewModel>? Items { get; set; }
    }