关于c#:MVVM动态菜单UI与ViewModel的绑定

MVVM Dynamic Menu UI from binding with ViewModel

我正在与LoB应用程序团队合作。我们希望有一个动态的Menu控件,该控件根据登录的用户配置文件创建菜单。在以前的开发方案(即ASP.NET)中,我们用于遍历描述集合的数据并动态生成MenuItem。在MVVM中,我该怎么做?我可以将XAML视图与描述菜单元素的ViewModel分开吗?

解决方案:

借助注释器的输入,我能够将Menu与ViewModel的数据动态绑定。这篇文章也很有帮助。

XAML:

1
2
3
4
5
6
7
8
9
10
11
12
<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>

Menu数据类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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
}


尝试类似这样的内容:

1
2
3
4
5
6
7
8
9
10
11
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的列表。这样的事情应该起作用,然后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>


这应该带您去哪里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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}"/>

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


此解决方案不需要在代码中包含任何代码,因此使它变得更简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        <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表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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()
        {

        }
    }

具体的MenuItems可以直接实例化,也可以通过继承来创建自己的SubType。


我知道这是一篇老文章,但是我需要它以及如何绑定命令。

关于Guge关于如何绑定命令的问题:
VMMenuItems是我的视图模型类中的属性,类型为

1
ObservableCollection<Menu>

和Menu是上面定义的类。 MenuItem \\的Command属性绑定到Menu类的Command属性。
在我看来,模型类

1
Menu.Command = _fou

其中

1
private ICommand _fou;

xaml

1
2
3
4
5
6
7
8
9
<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>

如果您想知道如何进行分隔符,那真的很容易。

下面的代码是我的ViewModel的一部分。由于XAML使用反射,所以我要做的就是返回\\'object \\',它可以是MenuItemViewModelSeparator或实际的MenuItem

我正在使用yield动态生成项目,因为它似乎对我来说读起来更好。即使我使用的是yield-如果项目更改,我仍然需要照常为"ContextMenu"引发PropertyChanged事件,但是在需要它之前,我不会不必要地生成列表。 >

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    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;
    }