DataTemplate用MVVM生成Menu

时间:2012-02-29 16:14:51

标签: wpf wpf-controls prism-4

我正在尝试使用DataTemplate从我的ViewModels创建一个关于MVVM的菜单。基本上,我已经创建了几个类来存储有关我的Menu结构的信息。然后我想要使用DataTemplate将菜单结构视为WPF菜单。

我有一个菜单服务,允许不同的组件在菜单中注册新菜单和项目。这是我组织菜单信息(ViewModel)

的方式

我有以下课程: MainMenuViewModel - 包含TopLevelMenuViewModelCollection(顶级菜单的集合)

TopLevelMenuViewModel - 包含MenuItemGroupViewModelCollection(一组菜单项的集合),以及菜单'Text'的名称

MenuItemGroupViewModel - 包含MenuItemViewModelCollection(菜单项的集合)

MenuItemViewModel - 包含text,image uri,command,children MenuItemViewModels

我想要做的是将DataTemplate应用于以前的类,将它们转换为普通菜单。

MainMenuViewModel - >菜单

TopLevelMenuViewModel - >带标题集的MenuItems

MenuItemGroupViewModel - >分隔符后跟每个MenuItemViewModel

的MenuItem

MenuItemViewModel - > MenuItem(HeirarchicalDataTemplate)

问题是我没有看到如何为MenuItemGroupViewModel生成多个MenuItem 。 Menu模板希望始终为每个项目创建一个ItemContainer,它是一个MenuItem。因此,我要么最终在MenuItem中使用我的MenuItems,这显然不起作用,或者根本不起作用。我已经尝试了几件事,仍然无法弄清楚如何让一个项目产生多个MenuItem。

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

请点击链接以查看我正在尝试做的更好的图片

Class Diagram

Basic Menu I want to Make

2 个答案:

答案 0 :(得分:8)

因为这有点复杂,我用一个可下载的例子更新了这个答案。

PrismMenuServiceExample

我的目标是允许不同的模块注册菜单命令并将它们与标题组合在一起,并按正确的顺序对菜单项进行排序。首先,让我们展示菜单的样子。

Grouped Menu Example

这很有用,例如“工具”菜单可以有一个“Module1”组,其中列出了属于Module1的每个工具的菜单项,Module1可以独立于其他模块注册。

我有一个“菜单服务”,它允许模块注册新菜单和菜单项。每个节点都有一个Path属性,用于通知服务放置菜单的位置。此接口可能位于基础结构项目中,因此所有模块都可以解析它。

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>

好的,这里列出了定义菜单系统结构的类型......(不是它的样子)

MainMenuNode基本上存在,因此您可以轻松地为其创建不同的模板。你可能是一个菜单栏或代表整个菜单的东西。

public class MainMenuNode
{
    public MainMenuNode()
    {
        Menus = new MenuItemNodeCollection();
    }

    public MenuItemNodeCollection Menus { get; private set; }
}

这是每个MenuItem的定义。它们包括一个告诉服务放置它们的路径,一个类似于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) { }
}

以下是我最终对MenuItems进行分组的方法..每个人都有一个GroupDescription

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}" />

使这项工作的关键是弄清楚如何使用适当的排序定义注入我的CollectionView并将定义分组到我的DataTemplate中。我就这样做了:

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

答案 1 :(得分:1)

我认为你现在拥有的最大问题是你对菜单项组的处理方式。您组内的所有MenuItem都必须属于同一个父级,因此您不能使用ItemsControl之类的内容。

相反,我会让每个TopLevelMenuItems公开ObservableCollection<MenuItems>的属性,这是一个包含所有组中所有菜单项的只读集合,其中的组以{{1}分隔} value,可用于标识分隔符。

例如,

null

然后,您的DataTemplates可以将您的菜单绑定到展平的集合,并使用触发器来识别哪些项目为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用于显示具有子对象的MenuItem的不同模板,或者绑定到DataTrigger个对象的模板。

null

当然,您可以使用实际对象而不是<Style TargetType="{x:Type MenuItem}"> <Setter Property="Template" Value="{StaticResource DefaultMenuItemTemplate}" /> <Style.Triggers> <DataTrigger Binding="{Binding }" Value="{x:Null}"> <Setter Property="Template" Value="{StaticResource SeparatorTemplate}" /> </DataTrigger> <DataTrigger Binding="{Binding HasItems}" Value="True"> <Setter Property="Template" Value="{StaticResource SubMenuItemTemplate}" /> </DataTrigger> </Style.Triggers> </Style> 值来识别您的null,但我发现Separators在我已经完成的其他项目中工作得很好我明白为什么我应该为自己创造更多的工作。