使用ViewModel绑定实现MVVM动态菜单UI

18
我正在与一个团队一起开发 LoB 应用程序。我们希望有一个动态的 Menu 控件,它可以根据登录用户的配置文件创建菜单。在以前的开发场景中(即 ASP.NET),我们通常会遍历描述集合的数据并动态生成 MenuItem。在 MVVM 中,我该如何做到这一点?我能否将描述菜单元素的 ViewModel 与 XAML 视图分离?
解决方案:
通过评论者的输入,我能够将 Menu 动态地绑定到来自 ViewModel 的数据。这篇 article 也非常有帮助。
XAML:
<HierarchicalDataTemplate DataType="{x:Type self:Menu}" ItemsSource="{Binding Path=Children, UpdateSourceTrigger=PropertyChanged}">
    <ContentPresenter Content="{Binding Path=MenuText}" RecognizesAccessKey="True"/>
</HierarchicalDataTemplate>

[...]

<Menu Height="21" Margin="0" Name="mainMenu" VerticalAlignment="Top" HorizontalAlignment="Stretch" 
      ItemsSource="{Binding Path=MenuItems, UpdateSourceTrigger=PropertyChanged}" ItemContainerStyle="{StaticResource TopMenuItems}">
    <Menu.Background>
        <ImageBrush ImageSource="/Wpf.Modules;component/Images/MenuBg.jpg" />
    </Menu.Background>
</Menu>

菜单 数据类:

public class Menu : ViewModelBase
{
    public Menu()
    {
        IsEnabled = true;
        Children = new List<Menu>();
    }

    #region [ Menu Properties ]

    private bool _isEnabled;
    private string _menuText;
    private ICommand _command;
    private IList<Menu> _children;

    public string MenuText
    {
        get { return _menuText; }
        set
        {
            _menuText = value;
            base.OnPropertyChanged("MenuText");
        }
    }

    public bool IsEnabled
    {
        get { return _isEnabled; }
        set
        {
            _isEnabled = value;
            base.OnPropertyChanged("IsEnabled");
        }
    }

    public ICommand Command
    {
        get { return _command; }
        set
        {
            _command = value;
            base.OnPropertyChanged("Command");
        }
    }

    public IList<Menu> Children
    {
        get { return _children; }
        set
        {
            _children = value;
        }
    }

    #endregion
}

在 Google 上花了一些时间后,我发现HierarchicalDataTemplate在动态菜单创建方面可能会有所帮助,并且与MVVM模式分开“关注点”。我目前还没有任何代码示例 :( - Raj
5个回答

18

尝试像这样:

public class MenuItemViewModel
{
    public MenuItemViewModel()
    {
        this.MenuItems = new List<MenuItemViewModel>();
    }

    public string Text { get; set; }

    public IList<MenuItemViewModel> MenuItems { get; private set; }
}

假设您的 DataContext 有一个名为 MenuItems 的属性,它是 MenuItemViewModel 的列表。然后,应该可以使用以下内容:

<Window x:Class="WpfApplication1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:self="clr-namespace:WpfApplication1"
        Title="Window1" Height="300" Width="300">
    <Window.Resources>
        <HierarchicalDataTemplate DataType="{x:Type self:MenuItemViewModel}"
                                  ItemsSource="{Binding Path=MenuItems}">
            <ContentPresenter Content="{Binding Path=Text}" />
        </HierarchicalDataTemplate>
    </Window.Resources>
    <DockPanel>
        <Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=MenuItems}" />
        <Grid />
    </DockPanel>
</Window>

嗨,有了这个,我可以将我的类绑定到菜单,但是如何按层次结构组织它们呢?每个菜单项都有一个parentId,那些没有parentId的是根菜单元素,我想在它们下面组织其余的菜单。是否有其他关于HierarchicalDataTemplate的资源?谢谢。 - Raj
还有,我该如何将XAML中的样式分配给MenuItem,因为它们在XAML中没有明确定义? - Raj
可以使用菜单的ItemContainerStyle属性分配样式到MenuItem。 - Raj
7
如果回答中包含如何将命令绑定到菜单,那么我会更喜欢这个答案! - Guge
2
Separator怎么办?假设我们有一个Separator模型类,如何使用分隔符构建菜单? - JobaDiniz
我刚开始学习WPF,虽然我已经能够让它工作了,但我认为最后几条评论都在暗示我所想知道的... .NET框架中没有预定义的MenuItem模型吗?这样我的视图模型就可以简单些? - Christopher Painter

14

这应该能帮助你到达目的地

<UserControl x:Class="WindowsUI.Views.Default.MenuView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:ViewModels="clr-namespace:WindowsUI.ViewModels"
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
    <Style TargetType="{x:Type MenuItem}">
        <Setter Property="Header" Value="{Binding Path=DisplayName}"/>
        <Setter Property="Command" Value="{Binding Path=Command}"/>
    </Style>
    <HierarchicalDataTemplate 
        DataType="{x:Type ViewModels:MenuItemViewModel}"
        ItemsSource="{Binding Path=Items}">
    </HierarchicalDataTemplate>
</UserControl.Resources>
<Menu DockPanel.Dock="Top" ItemsSource="{Binding Path=Items}"/>

请注意,在我的示例中,我的菜单项具有称为Command的ICommand类型属性。


6

这个解决方案不需要任何代码在后台,这使得它更简单。

        <Menu>
            <MenuItem ItemsSource="{Binding Path=ChildMenuItems}" Header="{Binding Path=Header}">
                <MenuItem.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type vm:MenuItemViewModel}" ItemsSource="{Binding ChildMenuItems}">
                        <MenuItem Header="{Binding Path=Header}" Command="{Binding Path=Command}"/>
                    </HierarchicalDataTemplate>
                    <DataTemplate DataType="{x:Type vm:SeparatorViewModel}">
                        <Separator>
                            <Separator.Template>
                                <ControlTemplate>
                                    <Line X1="0" X2="1" Stroke="Black" StrokeThickness="1" Stretch="Fill"/>
                                </ControlTemplate>
                            </Separator.Template>
                        </Separator>
                    </DataTemplate>
                </MenuItem.Resources>
            </MenuItem>
        </Menu>

而 MenuItem 的表示方式如下:

public class MenuItemViewModel : BaseViewModel
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="MenuItemViewModel"/> class.
        /// </summary>
        /// <param name="parentViewModel">The parent view model.</param>
        public MenuItemViewModel(MenuItemViewModel parentViewModel)
        {
            ParentViewModel = parentViewModel;
            _childMenuItems = new ObservableCollection<MenuItemViewModel>();
        }

        private ObservableCollection<MenuItemViewModel> _childMenuItems;
        /// <summary>
        /// Gets the child menu items.
        /// </summary>
        /// <value>The child menu items.</value>
        public ObservableCollection<MenuItemViewModel> ChildMenuItems
        {
            get
            {
                return _childMenuItems;
            }
        }

        private string _header;
        /// <summary>
        /// Gets or sets the header.
        /// </summary>
        /// <value>The header.</value>
        public string Header
        {
            get
            {
                return _header;
            }
            set
            {
                _header = value; NotifyOnPropertyChanged("Header");
            }
        }

        /// <summary>
        /// Gets or sets the parent view model.
        /// </summary>
        /// <value>The parent view model.</value>
        public MenuItemViewModel ParentViewModel { get; set; }

        public virtual void LoadChildMenuItems()
        {

        }
    }

具体的菜单项可以直接实例化,或者您可以通过继承创建自己的子类型。

MenuItem.Resources 中的 HierarchicalDataTemplate 必须有一个键,否则 XAML 无法编译,会导致 error MC3022: All objects added to an IDictionary must have a Key attribute or some other type of key associated with them 错误。 - AntonK

4
我知道这是一篇旧文章,但我需要这个并且需要绑定命令的方法。关于Guge提出的如何绑定命令的问题:VMMenuItems是我视图模型类的一个属性,类型为。
ObservableCollection<Menu>

并且菜单是上面定义的类。菜单项的命令属性被绑定到菜单类的命令属性。 在我的视图模型类中。

Menu.Command = _fou

在哪里

private ICommand _fou;

The xaml

<ListView.ContextMenu>
    <ContextMenu ItemsSource="{Binding Path=VMMenuItems}">
           <ContextMenu.ItemContainerStyle>
                <Style TargetType="{x:Type MenuItem}">                                    
                        <Setter Property="Command" Value="{Binding Command}"/>
                  </Style>
            </ContextMenu.ItemContainerStyle>
      </ContextMenu>                    
</ListView.ContextMenu>

1

如果你想知道如何添加分隔符,那么这其实非常简单。

下面的代码是我的ViewModel的一部分。由于XAML使用反射,所以我只需要返回一个“object”,它可以是MenuItemViewModelSeparator,或者(如果出于某种奇怪的原因我需要)一个实际的MenuItem

我使用yield来动态生成项目,因为这样看起来更好。即使我使用了yield,如果项目发生变化,我仍然需要像往常一样引发"ContextMenu"PropertyChanged事件,但在需要之前我不会不必要地生成列表。

    public IEnumerable<object> ContextMenu
    {
        get
        {
            // ToArray() needed or else they get garbage collected
            return GetContextMenu().ToArray();
        }
    }

    public IEnumerable<object> GetContextMenu()
    {
        yield return new MenuItemViewModel()
        {
            Text = "Clear all flags",
        };

        // adds a normal 'Separator' menuitem
        yield return new Separator();

        yield return new MenuItemViewModel()
        {
            Text = "High Priority"
        };

        yield return new MenuItemViewModel()
        {
            Text = "Medium Priority"
        };

        yield return new MenuItemViewModel()
        {
            Text = "Low Priority"
        };

        yield break;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接