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 :)
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; }
}