DataTemplate для создания меню с MVVM

Я пытаюсь использовать DataTemplate для создания меню из моих ViewModels в отношении MVVM. По сути, я создал несколько классов, в которых будет храниться информация о структуре моего меню. Затем я хочу реализовать эту структуру меню как меню WPF с использованием DataTemplate.

У меня есть служба меню, которая позволяет различным компонентам регистрировать новые меню и элементы в меню. Вот как я организовал информацию в меню (ViewModel)

У меня есть следующие классы: MainMenuViewModel — содержит TopLevelMenuViewModelCollection (набор меню верхнего уровня).

TopLevelMenuViewModel — содержит MenuItemGroupViewModelCollection (набор групп пунктов меню) и имя для меню «Текст».

MenuItemGroupViewModel — содержит MenuItemViewModelCollection (набор пунктов меню).

MenuItemViewModel — содержит текст, URI изображения, команду, дочерние модели MenuItemViewModel.

Я хочу применить DataTemplate к предыдущим классам, чтобы преобразовать их в обычное меню.

MainMenuViewModel -> Меню

TopLevelMenuViewModel -> MenuItems с набором заголовков

MenuItemGroupViewModel -> Разделитель, за которым следует MenuItem для каждой MenuItemViewModel

MenuItemViewModel -> MenuItem (HeirarchicalDataTemplate)

Проблема в том, что я не понимаю, как создать несколько элементов MenuItem для MenuItemGroupViewModel. Шаблон меню хочет всегда создавать ItemContainer для каждого элемента, который является MenuItem. Поэтому я либо получаю свои MenuItems внутри MenuItem, которые явно не работают, либо не работают вообще. Я пробовал несколько вещей и до сих пор не могу понять, как заставить один элемент создавать более одного элемента 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>-->

Please Click the links to see a better picture of what I'm trying to do

Схема классов

Основное меню, которое я хочу создать


person Alan    schedule 29.02.2012    source источник
comment
слишком запутанно без картинки   -  person Jake Berger    schedule 29.02.2012
comment
Я добавил несколько ссылок на четкие изображения, это действительно очень просто, когда вы это видите   -  person Alan    schedule 29.02.2012
comment
возможно ли это с немного другим подходом? вместо групп вы можете просто создать замену для разделителя, например, статья показывает..   -  person Jake Berger    schedule 29.02.2012
comment
Может быть... Я сейчас смотрю на эту статью... Я просто подумал, что весь смысл подхода MVVM в том, что данные должны не знать о представлении. Я предполагал, что это меню может быть реализовано с помощью DataTemplate для создания меню любого типа, будь то главное меню, лента или другой интерфейс навигации по меню. Конечно, я мог бы создать его как UserControl и делать все в процедурном коде, но я думал, что DataTemplates — это то, что нужно... Думаю, мне, возможно, придется изменить свои данные, чтобы приспособить их к DataTemplates. Идея заключалась в том, что каждый модуль должен регистрировать группу элементов, которыми он владеет и управляет.   -  person Alan    schedule 29.02.2012
comment
проблема в том, что WPF видит TopLevel.MenuGroups и говорит Хорошо, давайте создадим MenuItem для каждой группы. когда я впервые запустил WPF и MVVM, я также много раз сталкивался с разделением данных. Но сложность должна где-то лежать, используете ли вы данные в конвертере или что-то еще.   -  person Jake Berger    schedule 01.03.2012
comment
Я понимаю... и да, это именно проблема. Знаете ли вы, что отвечает за создание этого MenuItem? Как меню знает, что у него есть элементы меню, а у списка есть элементы списка... и т. д.? Похоже, мне нужно либо A. сгладить мои данные, либо B. написать процедурный код, чтобы сделать это самостоятельно в пользовательском элементе управления.   -  person Alan    schedule 01.03.2012
comment
Вы знаете, мне сейчас интересно, должен ли я просто включить MenuItem в MenuItem и вместо этого применить собственный ItemContainerStyle (вместо ‹Separator/›) для форматирования интервала между группами команд?!   -  person Alan    schedule 01.03.2012
comment
Мне нужно было бы переопределить шаблон или стиль контейнера, чтобы не отображать всплывающее окно, интересно, возможно ли это. Вместо стрелки и всплывающего окна, возможно, разделитель с именем группы и некоторый интервал с дочерними элементами в панели стека.   -  person Alan    schedule 01.03.2012
comment
How does a Menu know it has MenuItems, and a ListBox has ListBoxItems.. etc? Через ItemsControl.ItemsSource   -  person Jake Berger    schedule 01.03.2012


Ответы (2)


Поскольку это довольно сложно, я обновил этот ответ загружаемым примером.

Пример службы PrismMenu

Моя цель состояла в том, чтобы позволить различным модулям регистрировать команды меню и группировать их вместе с заголовком и сортировать пункты меню в правильном порядке. Прежде всего, давайте покажем пример того, как выглядит меню.

Пример сгруппированного меню

Это полезно, например, в меню «Инструменты» может быть группа «Модуль1», в которой перечислены пункты меню для каждого инструмента, принадлежащего Модулю1, который Модуль1 может регистрировать независимо от других модулей.

У меня есть «служба меню», которая позволяет модулям регистрировать новые меню и пункты меню. У каждого узла есть свойство 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. Они включают в себя Path, который сообщает сервису, куда их поместить, SortIndex, который похож на TabIndex, который позволяет упорядочивать их в правильном порядке, и 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;
    }
}
person Alan    schedule 11.10.2012

Я думаю, что самая большая проблема с тем, что у вас есть сейчас, это то, как вы относитесь к группам пунктов меню. Все элементы меню внутри ваших групп должны принадлежать одному и тому же родителю, поэтому вы не можете использовать для них что-то вроде ItemsControl.

Вместо этого я бы сделал так, чтобы каждый TopLevelMenuItems открывал свойство ObservableCollection<MenuItems>, которое представляет собой доступную только для чтения коллекцию, содержащую все элементы меню из всех групп, с группами, разделенными значением 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; 
        }
    }
}

Затем ваши DataTemplates могут связать ваше меню с плоскими коллекциями и использовать триггер, чтобы определить, какие элементы являются null и должны быть нарисованы с помощью разделителя.

У меня, вероятно, этот синтаксис неправильный, но вот пример. Шаблон по умолчанию должен быть обычным пунктом меню, а DataTrigger используется для отображения другого шаблона для MenuItems с дочерними объектами или привязанными к 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, однако я обнаружил, что nulls отлично работает в других проектах, которые я сделал, поэтому не понимаю, почему я должен создавать больше работы для себя.

person Rachel    schedule 29.02.2012
comment
Спасибо за вашу помощь. Я понимаю, о чем вы говорите, я как бы понимаю, что меню, в котором он настроен, имеет плоский список других элементов меню. Я мог бы сгладить свои данные, чтобы связать HeirarchalDataTemplate с MenuItemViewModel (что я и сделал), но это противоречит тому, чего я пытаюсь достичь. Я пытаюсь реализовать шаблон MVVM в модульном приложении, используя Prism V4. По сути, когда каждый модуль загружается, он использует службу меню для регистрации группы MenuCommands, которые принадлежат модулю в меню верхнего уровня по умолчанию. - person Alan; 01.03.2012
comment
Итак, чтобы уточнить, MenuService будет предоставлять интерфейс другим модулям, что позволяет им создавать группу команд меню и добавлять их в меню. Затем служба создаст объекты MenuItemGroupViewModel, назовет их и сохранит в TopLevelMenuViewModel. Эти ViewModels представляют только данные, принадлежащие сервису, то есть команды с изображением и именем, которые могут быть выполнены. Предполагается, что он не зависит от DataTemplate, который используется для создания графического интерфейса, который пользователь может использовать для активации команд. - person Alan; 01.03.2012
comment
Следовательно, изменение моих классов ViewModel, чтобы заставить работать DataTemplate, не совсем правильно в соответствии с методами проектирования MVVM. Если я не могу понять, как это сделать с помощью DataTemplate, я думаю, что создам UserControl, добавлю меню в пользовательский элемент управления и просто напишу процедурный код для добавления элементов вместо использования DataTemplate. Я просто очень хотел использовать DataTemplate, потому что они должны быть проще, чище и менее подвержены ошибкам (?) - person Alan; 01.03.2012
comment
Спасибо за вашу помощь, Рэйчел, я наконец-то возвращаюсь к вам с тем, что я в итоге сделал. Вы были в основном правы, то, как я отношусь к своим группам, неправильно. Нет хорошего способа создать несколько пунктов меню из одной группы с помощью шаблона. Я также опубликую свое решение.. - person Alan; 11.10.2012