DataTemplate для создания меню с MVVM - PullRequest
3 голосов
/ 29 февраля 2012

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

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

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

TopLevelMenuViewModel - Содержит MenuItemGroupViewModelC (коллекция групп элементов меню) и имя для меню «Текст»

MenuItemGroupViewModel - содержит MenuItemViewModelCollection (коллекция элементов меню)

MenuItemViewModel - содержит текст, изображение, команду, команду,children MenuItemViewModels

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

MainMenuViewModel -> Menu

TopLevelMenuViewModel -> MenuItems с установленным заголовком

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

MenuItemViewModel -> MenuItem (HeirarchicalDataTemplate)

1024

1024Я не вижу, как родынесколько MenuItems для 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>-->

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

Диаграмма классов

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

Ответы [ 2 ]

8 голосов
/ 11 октября 2012

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

PrismMenuServiceExample

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

Grouped Menu Example

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

У меня есть «служба меню», которая позволяет модулям регистрировать новые меню и пункты меню.Каждый узел имеет свойство Path, которое сообщает службе, где разместить меню.Этот интерфейс вероятен в инфраструктурном проекте, так что все модули могут разрешить его.

public interface IMenuService
{
    void AddTopLevelMenu(MenuItemNode node);
    void RegisterMenu(MenuItemNode node);
}

Затем я могу реализовать этот MenuService там, где это необходимо.(Инфраструктурный проект, отдельный модуль, возможно, Shell).Я продолжаю и добавляю некоторые «стандартные» меню, которые определены для всего приложения, хотя любой модуль может добавить новые меню верхнего уровня.

Я мог бы создать эти меню в коде, но вместо этого я вытащил их из ресурсовпотому что было проще записать их в 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;
    }
}
1 голос
/ 01 марта 2012

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

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

...