Wpf 使用MVVM生成菜单的数据模板
我试图使用DataTemplate从我的ViewModels中创建一个关于MVVM的菜单。基本上,我已经创建了几个类来存储关于菜单结构的信息。然后,我想使用数据模板将该菜单结构实现为WPF菜单 我有一个菜单服务,它允许不同的组件在菜单中注册新的菜单和项目。以下是我如何组织菜单信息(ViewModel) 我有以下课程: MainMenuViewModel-包含顶级MenuViewModelCollection(顶级菜单的集合) TopLevelMenuViewModel-包含MenuItemGroupViewModelCollection(菜单项组的集合)和菜单“文本”的名称 MenuItemGroupViewModel-包含MenuItemViewModel集合(菜单项集合) MenuItemViewModel-包含文本、图像uri、命令、子MenuItemViewModels 我想做的是对前面的类应用一个数据模板,将它们转换为一个普通菜单。 MainMenuViewModel->菜单 TopLevelMenuViewModel->设置标题的菜单项 MenuItemGroupViewModel->分隔符,后跟每个MenuItemViewModel的MenuItem MenuItemViewModel->MenuItem(HierarchicalDataTemplate) 问题是我不知道如何为MenuItemGroupViewModel生成多个MenuItem。菜单模板希望始终为作为菜单项的每个项创建一个ItemContainer。因此,我要么将我的菜单项放在一个显然不起作用的菜单项中,要么根本不起作用。我已经尝试了好几种方法,但仍然不知道如何使单个项目产生多个菜单项Wpf 使用MVVM生成菜单的数据模板,wpf,wpf-controls,prism-4,Wpf,Wpf Controls,Prism 4,我试图使用DataTemplate从我的ViewModels中创建一个关于MVVM的菜单。基本上,我已经创建了几个类来存储关于菜单结构的信息。然后,我想使用数据模板将该菜单结构实现为WPF菜单 我有一个菜单服务,它允许不同的组件在菜单中注册新的菜单和项目。以下是我如何组织菜单信息(ViewModel) 我有以下课程: MainMenuViewModel-包含顶级MenuViewModelCollection(顶级菜单的集合) TopLevelMenuViewModel-包含MenuItemGro
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:--">
<!-- These data templates provide the views for the menu -->
<!-- MenuItemGroupView -->
<Style x:Key="MenuItemGroupStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="qqq" />
<!-- Now what? I don't want 1 item here..
I wanted this to start with a <separator /> and list the MenuItemGroupViewModel.MenuItems -->
</Style>
<!-- TopLevelMenuView -->
<Style x:Key="TopLevelMenuStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Header" Value="{Binding Text}" />
<Setter Property="ItemsSource" Value="{Binding MenuGroups}" />
<Setter Property="ItemContainerStyle" Value="{StaticResource MenuItemGroupStyle}"/>
</Style>
<!-- MainMenuView -->
<DataTemplate DataType="{x:Type local:MainMenuViewModel}">
<Menu ItemsSource="{Binding TopLevelMenus}" ItemContainerStyle="{StaticResource TopLevelMenuStyle}" />
</DataTemplate>
<!-- MenuItemView -->
<!--<HierarchicalDataTemplate DataType="{x:Type local:MenuItemViewModel}"
ItemsSource="{Binding Path=Children}"
>
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Command"
Value="{Binding Command}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding ImageSource}" />
<TextBlock Text="{Binding Text}" />
</StackPanel>
</HierarchicalDataTemplate>-->
请单击链接查看我正在尝试做的事情的更好图片
我认为你现在面临的最大问题是处理菜单项组的方式。组中的所有菜单项都必须属于同一父菜单项,因此不能对它们使用类似于
ItemsControl
的功能
相反,我会让每个TopLevelMenuItems
公开ObservableCollection
的属性,这是一个只读集合,包含来自所有组的所有菜单项,组之间用一个null
值分隔,该值可用于标识分隔符
比如说,
public class TopLevelMenu
{
public ObservableCollection<MenuItem> MenuItems
{
get
{
// Would be better to maintain a private collection for this instead of creating each time
var collection = new ObservableCollection<MenuItem>();
foreach(MenuGroup group in MenuGroups)
{
if (collection.Length > 0)
collection.Add(null); // Use null as separator placeholder
foreach(MenuItem item in group.MenuItems)
collection.Add(item);
}
// Will return a collection containing all menu items in all groups,
// with the groups separated by a null value
return collection;
}
}
}
当然,您可以使用实际对象而不是
null
值来标识分隔符
,但是我发现null
在其他项目中效果很好,我不明白为什么我应该为自己创建更多的工作。因为这有点复杂,我用一个可下载的例子更新了这个答案
我的目标是允许不同的模块注册菜单命令,并将它们与标题一起分组,并按适当的顺序对菜单项进行排序。首先,让我们展示一个菜单的示例
这是很有用的,例如,“工具”菜单可以有一个“模块1”组,其中列出了属于模块1的每个工具的菜单项,模块1可以独立于其他模块进行注册
我有一个“菜单服务”,它允许模块注册新的菜单和菜单项。每个节点都有一个路径属性,用于通知服务将菜单放置在何处。此接口可能位于基础设施项目中,因此所有模块都可以解决它
public interface IMenuService
{
void AddTopLevelMenu(MenuItemNode node);
void RegisterMenu(MenuItemNode node);
}
然后,我可以在适当的地方实现该MenuService。(基础设施项目,单独的模块,可能是外壳)。我继续添加一些应用程序范围内定义的“默认”菜单,尽管任何模块都可以添加新的顶级菜单
我本可以用代码创建这些菜单,但我将它们从资源中取出,因为在资源文件中用XAML编写它们更容易。我正在将该资源文件添加到我的应用程序资源中,但您可以直接加载它
public class MainMenuService : IMenuService
{
MainMenuNode menu;
MenuItemNode fileMenu;
MenuItemNode toolMenu;
MenuItemNode windowMenu;
MenuItemNode helpMenu;
public MainMenuService(MainMenuNode menu)
{
this.menu = menu;
fileMenu = (MenuItemNode)Application.Current.Resources["FileMenu"];
toolMenu = (MenuItemNode)Application.Current.Resources["ToolMenu"];
windowMenu = (MenuItemNode)Application.Current.Resources["WindowMenu"];
helpMenu = (MenuItemNode)Application.Current.Resources["HelpMenu"];
menu.Menus.Add(fileMenu);
menu.Menus.Add(toolMenu);
menu.Menus.Add(windowMenu);
menu.Menus.Add(helpMenu);
}
#region IMenuService Members
public void AddTopLevelMenu(MenuItemNode node)
{
menu.Menus.Add(node);
}
public void RegisterMenu(MenuItemNode node)
{
String[] tokens = node.Path.Split('/');
RegisterMenu(tokens.GetEnumerator(), menu.Menus, node);
}
#endregion
private void RegisterMenu(IEnumerator tokenEnumerator, MenuItemNodeCollection current, MenuItemNode item)
{
if (!tokenEnumerator.MoveNext())
{
current.Add(item);
}
else
{
MenuItemNode menuPath = current.FirstOrDefault(x=> x.Text == tokenEnumerator.Current.ToString());
if (menuPath == null)
{
menuPath = new MenuItemNode(String.Empty);
menuPath.Text = tokenEnumerator.Current.ToString();
current.Add(menuPath);
}
RegisterMenu(tokenEnumerator, menuPath.Children, item);
}
}
}
下面是我的资源文件中预定义菜单之一的示例:
<!-- File Menu Groups -->
<menu:MenuGroupDescription x:Key="fileCommands"
Name="Files"
SortIndex="10" />
<menu:MenuGroupDescription x:Key="printerCommands"
Name="Printing"
SortIndex="90" />
<menu:MenuGroupDescription x:Key="applicationCommands"
Name="Application"
SortIndex="100" />
<menu:MenuItemNode x:Key="FileMenu"
x:Name="FileMenu"
Text="{x:Static inf:DefaultTopLevelMenuNames.File}"
SortIndex="10">
<menu:MenuItemNode Group="{StaticResource fileCommands}"
Text="_Open File..."
SortIndex="10"
Command="{x:Static local:FileCommands.OpenFileCommand}" />
<menu:MenuItemNode Group="{StaticResource fileCommands}" Text="Recent _Files" SortIndex="20"/>
<menu:MenuItemNode Group="{StaticResource fileCommands}" Text="Con_vert..." SortIndex="30"/>
<menu:MenuItemNode Group="{StaticResource fileCommands}"
Text="_Export"
SortIndex="40"
Command="{x:Static local:FileCommands.ExportCommand}" />
<menu:MenuItemNode Group="{StaticResource fileCommands}" Text="_Save" SortIndex="50"/>
<menu:MenuItemNode Group="{StaticResource fileCommands}" Text="Save _All" SortIndex="60"/>
<menu:MenuItemNode Group="{StaticResource fileCommands}"
Text="_Close"
SortIndex="70"
Command="{x:Static local:FileCommands.CloseCommand}" />
<menu:MenuItemNode Group="{StaticResource printerCommands}" Text="Page _Setup..." SortIndex="10"/>
<menu:MenuItemNode Group="{StaticResource printerCommands}" Text="_Print..." SortIndex="10"/>
<menu:MenuItemNode Group="{StaticResource applicationCommands}"
Text="E_xit"
SortIndex="10"
Command="{x:Static local:FileCommands.ExitApplicationCommand}" />
</menu:MenuItemNode>
下面是每个菜单项的定义。它们包括一个告诉服务把它们放在哪里的路径,一个有点像TabIndex的SortIndex,它允许它们按正确的顺序组织,还有一个GroupDescription,它允许您将它们放在“组”中,这些“组”可以有不同的样式和排序
[ContentProperty("Children")]
public class MenuItemNode : NotificationObject
{
private string text;
private ICommand command;
private Uri imageSource;
private int sortIndex;
public MenuItemNode()
{
Children = new MenuItemNodeCollection();
SortIndex = 50;
}
public MenuItemNode(String path)
{
Children = new MenuItemNodeCollection();
SortIndex = 50;
Path = path;
}
public MenuItemNodeCollection Children { get; private set; }
public ICommand Command
{
get
{
return command;
}
set
{
if (command != value)
{
command = value;
RaisePropertyChanged(() => this.Command);
}
}
}
public Uri ImageSource
{
get
{
return imageSource;
}
set
{
if (imageSource != value)
{
imageSource = value;
RaisePropertyChanged(() => this.ImageSource);
}
}
}
public string Text
{
get
{
return text;
}
set
{
if (text != value)
{
text = value;
RaisePropertyChanged(() => this.Text);
}
}
}
private MenuGroupDescription group;
public MenuGroupDescription Group
{
get { return group; }
set
{
if (group != value)
{
group = value;
RaisePropertyChanged(() => this.Group);
}
}
}
public int SortIndex
{
get
{
return sortIndex;
}
set
{
if (sortIndex != value)
{
sortIndex = value;
RaisePropertyChanged(() => this.SortIndex);
}
}
}
public string Path
{
get;
private set;
}
和一系列菜单项:
public class MenuItemNodeCollection : ObservableCollection<MenuItemNode>
{
public MenuItemNodeCollection() { }
public MenuItemNodeCollection(IEnumerable<MenuItemNode> items) : base(items) { }
}
没有图片太让人困惑了我添加了一些链接来清晰图片,当你看到有可能使用稍微不同的方法时,它真的很简单?除了“组”,你能不能为分隔符创建一个替身,如下图所示..也许。。我现在正在看那篇文章。。我只是在想MVVM方法的全部要点是“数据”应该不知道“视图”。我想象这个菜单可以通过一个DataTemplate来实现,以创建任何类型的菜单,无论是主菜单、功能区还是其他菜单导航界面。当然,我可以将其创建为用户控件,并在过程代码中执行所有操作,但我认为DataTemplates是最好的选择。。。我想我可能不得不修改我的数据以适应数据模板。想法是每个模块将注册它拥有和控制的一组项目。问题是WPF看到的是
TopLevel.MenuGroups
,并说“好的,让我们为每个组创建一个MenuItem。”当我第一次启动WPF和MVVM时,我也多次遇到“数据分离”。但是,复杂性必须存在于某个地方,无论您是使用转换器中的数据还是其他什么..谢谢您的帮助。我明白你的意思,我有点意识到菜单设置为有一个其他菜单的平面列表。我可以展平我的数据,这样我就可以将一个HeirArchildataTemplate绑定到MenuItemViewModel(我已经这样做了),但这与我试图实现的目标背道而驰
[ContentProperty("Children")]
public class MenuItemNode : NotificationObject
{
private string text;
private ICommand command;
private Uri imageSource;
private int sortIndex;
public MenuItemNode()
{
Children = new MenuItemNodeCollection();
SortIndex = 50;
}
public MenuItemNode(String path)
{
Children = new MenuItemNodeCollection();
SortIndex = 50;
Path = path;
}
public MenuItemNodeCollection Children { get; private set; }
public ICommand Command
{
get
{
return command;
}
set
{
if (command != value)
{
command = value;
RaisePropertyChanged(() => this.Command);
}
}
}
public Uri ImageSource
{
get
{
return imageSource;
}
set
{
if (imageSource != value)
{
imageSource = value;
RaisePropertyChanged(() => this.ImageSource);
}
}
}
public string Text
{
get
{
return text;
}
set
{
if (text != value)
{
text = value;
RaisePropertyChanged(() => this.Text);
}
}
}
private MenuGroupDescription group;
public MenuGroupDescription Group
{
get { return group; }
set
{
if (group != value)
{
group = value;
RaisePropertyChanged(() => this.Group);
}
}
}
public int SortIndex
{
get
{
return sortIndex;
}
set
{
if (sortIndex != value)
{
sortIndex = value;
RaisePropertyChanged(() => this.SortIndex);
}
}
}
public string Path
{
get;
private set;
}
public class MenuItemNodeCollection : ObservableCollection<MenuItemNode>
{
public MenuItemNodeCollection() { }
public MenuItemNodeCollection(IEnumerable<MenuItemNode> items) : base(items) { }
}
public class MenuGroupDescription : NotificationObject, IComparable<MenuGroupDescription>, IComparable
{
private int sortIndex;
public int SortIndex
{
get { return sortIndex; }
set
{
if (sortIndex != value)
{
sortIndex = value;
RaisePropertyChanged(() => this.SortIndex);
}
}
}
private String name;
public String Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
RaisePropertyChanged(() => this.Name);
}
}
}
public MenuGroupDescription()
{
Name = String.Empty;
SortIndex = 50;
}
public override string ToString()
{
return Name;
}
#region IComparable<MenuGroupDescription> Members
public int CompareTo(MenuGroupDescription other)
{
return SortIndex.CompareTo(other.SortIndex);
}
#endregion
#region IComparable Members
public int CompareTo(object obj)
{
if(obj is MenuGroupDescription)
return sortIndex.CompareTo((obj as MenuGroupDescription).SortIndex);
return this.GetHashCode().CompareTo(obj.GetHashCode());
}
#endregion
}
<local:MenuCollectionViewConverter x:Key="GroupViewConverter" />
<!-- The style for the header of a group of menu items -->
<DataTemplate x:Key="GroupHeaderTemplate"
x:Name="GroupHeader">
<Grid x:Name="gridRoot"
Background="#d9e4ec">
<TextBlock Text="{Binding Name}"
Margin="4" />
<Rectangle Stroke="{x:Static SystemColors.MenuBrush}"
VerticalAlignment="Top"
Height="1" />
<Rectangle Stroke="#bbb"
VerticalAlignment="Bottom"
Height="1" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Name}"
Value="{x:Null}">
<Setter TargetName="gridRoot"
Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<!-- Binds the MenuItemNode's properties to the generated MenuItem container -->
<Style x:Key="MenuItemStyle"
TargetType="MenuItem">
<Setter Property="Header"
Value="{Binding Text}" />
<Setter Property="Command"
Value="{Binding Command}" />
<Setter Property="GroupStyleSelector"
Value="{x:Static local:MenuGroupStyleSelectorProxy.MenuGroupStyleSelector}" />
</Style>
<Style x:Key="TopMenuItemStyle"
TargetType="MenuItem">
<Setter Property="Header"
Value="{Binding Text}" />
<Setter Property="Command"
Value="{Binding Command}" />
<Setter Property="GroupStyleSelector"
Value="{x:Static local:MenuGroupStyleSelectorProxy.MenuGroupStyleSelector}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Children.Count}"
Value="0">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding}"
Value="{x:Null}">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<!-- MainMenuView -->
<DataTemplate DataType="{x:Type menu:MainMenuNode}">
<Menu ItemsSource="{Binding Menus, Converter={StaticResource GroupViewConverter}}"
ItemContainerStyle="{StaticResource TopMenuItemStyle}" />
</DataTemplate>
<!-- MenuItemView -->
<HierarchicalDataTemplate DataType="{x:Type menu:MenuItemNode}"
ItemsSource="{Binding Children, Converter={StaticResource GroupViewConverter}}"
ItemContainerStyle="{StaticResource MenuItemStyle}" />
[ValueConversion(typeof(MenuItemNodeCollection), typeof(IEnumerable))]
public class MenuCollectionViewConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(IEnumerable))
throw new NotImplementedException();
CollectionViewSource src = new CollectionViewSource();
src.GroupDescriptions.Add(new PropertyGroupDescription("Group"));
src.SortDescriptions.Add(new SortDescription("Group", ListSortDirection.Ascending));
src.SortDescriptions.Add(new SortDescription("SortIndex", ListSortDirection.Ascending));
src.Source = value as IEnumerable;
return src.View;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value.GetType() != typeof(CollectionViewSource))
throw new NotImplementedException();
return (value as CollectionViewSource).Source;
}
#endregion
}
public static class MenuGroupStyleSelectorProxy
{
public static GroupStyleSelector MenuGroupStyleSelector { get; private set; }
private static GroupStyle Style { get; set; }
static MenuGroupStyleSelectorProxy()
{
MenuGroupStyleSelector = new GroupStyleSelector(SelectGroupStyle);
Style = new GroupStyle()
{
HeaderTemplate = (DataTemplate)Application.Current.Resources["GroupHeaderTemplate"]
};
}
public static GroupStyle SelectGroupStyle(CollectionViewGroup grp, int target)
{
return Style;
}
}