简体   繁体   中英

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM