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?" 如果您竭尽全力编写可能会被误认为是您工作环境附带的标准库的代码,那么您几乎永远不会出错。
所以我有一个自定义 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?" 如果您竭尽全力编写可能会被误认为是您工作环境附带的标准库的代码,那么您几乎永远不会出错。