WPF 中使用模板的递归绑定

Recursive binding in WPF with templates

所以我有一个自定义 class,旨在包含格式如下的菜单项和子菜单项:

public class ApplicationMenuItem
{
    public ImageSource Image { get; set; }
    public string Text { get; set; }
    public string Tooltip { get; set; }
    public ICollection<ApplicationMenuItem> Items { get; set; }
    public EventHandler Clicked {get;set;}
    public void Click(object sender, EventArgs e)
    {
        if (Clicked!=null)
            Clicked(this, e);
    }
    public ApplicationMenuItem(string Text)
    {
        this.Text = Text;
        Items = new List<ApplicationMenuItem>();
    }
    public ApplicationMenuItem()
    {
        Items = new List<ApplicationMenuItem>();
    }

}

在有人问我为什么不继承 Menu 或只是创建一个 Menu 对象并绑定它之前,这是因为这个 class 可以在不支持的平台和框架上使用' 必须使用 Menu UI 对象,更不用说这个 class 将驱动导航菜单、上下文菜单、侧边栏、工具栏等....

我的问题是,如您所见,我有一个自引用列表 Items 包含在其中以允许子菜单;绑定一级菜单元素很容易,但是如何在 WPF 中为其元素创建模板时递归绑定子元素?

这是一个递归 XAML 模板的示例,它完全按照您定义的方式使用 ApplicationMenuItem class(除了我将其放在名为 Wobbles 的名称空间中) .这还没有完成,可发布的代码。但它演示了一个递归 DataTemplate,以及一些额外的好东西,比如显示弹出窗口。您可以将 IsEnabled 属性 添加到您的菜单项 class 并在 XAML 中使用设置颜色的附加触发器以及驱动 SubmenuPopup.IsOpen。如果你想支持水平分隔符,你可以添加一个 属性 bool ApplicationMenuItem.IsSeparator 并给模板一个触发器,当 属性 是 [=19= 时用水平线替换下面的网格内容].

RecursiveTemplate.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:wbl="clr-namespace:Wobbles"
    >

    <DataTemplate DataType="{x:Type wbl:ApplicationMenuItem}">
        <Grid
            Name="RootGrid"
            Background="BlanchedAlmond"
            Height="Auto"
            UseLayoutRounding="True"
            SnapsToDevicePixels="True"
            >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="24" />
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="24" />
            </Grid.ColumnDefinitions>

            <Image
                Grid.Column="0"
                Source="{Binding Image}"
                />
            <Label
                Grid.Column="1"
                Content="{Binding Text}"
                />
            <Border
                Name="PopupGlyphBorder"
                Grid.Column="2"
                VerticalAlignment="Stretch"
                HorizontalAlignment="Stretch"
                Background="{Binding ElementName=RootGrid, Path=Background}"
                >
                <Path
                    Height="10"
                    Width="5"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Center"
                    Data="M 0,0 L 5,5 L 0,10 Z"
                    Fill="Black"
                    />
            </Border>

            <Popup
                Name="SubmenuPopup"
                PlacementTarget="{Binding ElementName=PopupGlyphBorder}"
                Placement="Right"
                StaysOpen="True"
                >
                <Border
                    BorderBrush="DarkGoldenrod"
                    BorderThickness="1"
                    >
                    <ItemsControl
                        Name="SubmenuItems"
                        ItemsSource="{Binding Items}"
                        />
                </Border>
            </Popup>
        </Grid>
        <DataTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="RootGrid" Property="Background" Value="Wheat" />
            </Trigger>

            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="IsMouseOver" Value="True" />
                    <Condition SourceName="SubmenuItems" Property="HasItems" Value="True" />
                </MultiTrigger.Conditions>
                <Setter TargetName="SubmenuPopup" Property="IsOpen" Value="True" />
            </MultiTrigger>

            <Trigger SourceName="SubmenuItems" Property="HasItems" Value="False">
                <Setter TargetName="PopupGlyphBorder" Property="Visibility" Value="Hidden" />
            </Trigger>
        </DataTemplate.Triggers>
    </DataTemplate>

</ResourceDictionary>

MainWindow.xaml

<Window
    x:Class="RecursiveTemplate.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:wbl="clr-namespace:Wobbles"
    Title="MainWindow" 
    Height="350" 
    Width="525"
    >

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="RecursiveTemplate.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>

    <Window.DataContext>
        <wbl:TestViewModel />
    </Window.DataContext>

    <Grid>
        <StackPanel Orientation="Vertical">
            <StackPanel Orientation="Horizontal">
                <ContentControl
                    Content="{Binding Menu}"
                    Width="100"
                    Height="24"
                    />
            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

ViewModel.cs

namespace Wobbles
{
    public class TestViewModel
    {
        public TestViewModel()
        {
            Menu = CreateMenu();
        }

        public Wobbles.ApplicationMenuItem Menu { get; protected set; }

        protected Wobbles.ApplicationMenuItem CreateMenu()
        {
            var m = new Wobbles.ApplicationMenuItem("Menu");

            var msub = new Wobbles.ApplicationMenuItem("Submenu");
            msub.Items.Add(new Wobbles.ApplicationMenuItem("Sub Sub 1"));
            msub.Items.Add(new Wobbles.ApplicationMenuItem("Sub Sub 2"));
            //  LOL
            msub.Items.Add(msub);

            m.Items.Add(msub);

            m.Items.Add(new Wobbles.ApplicationMenuItem("Foo"));
            m.Items.Add(new Wobbles.ApplicationMenuItem("Bar"));
            m.Items.Add(new Wobbles.ApplicationMenuItem("Baz"));

            return m;
        }
    }
}

尼特、吹毛求疵、抱怨和简短的布道

使用 XAML,我建议练习使用 ObservableCollection<T> 而不是 List<T>。如果在构造 UI 之后集合中的项目发生变化,ObservableCollection<T> 将导致 UI 适当更新。出于同样的原因,您会希望 ApplicationMenuItem 实现 INotifyPropertyChanged。我也更喜欢支持 ICommand Command 属性 以及 Click 事件,并且我会根据标准进一步命名 Click 事件 Click XAML练习。

"What Would XAML Do?" 如果您竭尽全力编写可能会被误认为是您工作环境附带的标准库的代码,那么您几乎永远不会出错。