Invented such a scheme.

Created a class

public class MyCommand : ICommand, ICollection<MyCommand> 

That is, each command includes more nested commands (nested menus)

I add a property in the window class

 public MyCommand Menu { get; set; } 

In the designer doing this

 <Menu ItemsSource="{Binding Path=Menu}"/> 

And also add to the window resources

 <HierarchicalDataTemplate DataType="{x:Type local:MyCommand}" ItemsSource="{Binding}"> <TextBlock Text="{Binding Path=Text}"/> </HierarchicalDataTemplate> 

Everything. In the code I form a tree from MyCommand , and the menu in the window is created automatically by the tree. The MyCommand class implements merging, so I can easily add additional items to the main menu (for example, the menu created in the TabItem content).

Everything is ingenious and simple :), only I don’t know how to separate menu items with separators. I’m not using containers (MenuItem), so I can’t just pick up and add new Separator() . Advise something for this.

    2 answers 2

    The separator is part of the view, not the view model. The idea of ​​one-to-one mapping between abstract commands and controls in the entire application is flawed. Such an approach can be used in some cases, for example, when generating a list of recently opened files in the menu or providing an abstract interface for managing the presentation to the plugin (and I would think about alternatives).

    You can push the separator into the list. Just need to make a hierarchy of classes ("menu item", "menu item-separator", "menu item-command"), sprinkle with templates and binders ... But think well: is this correct? If you follow the one-to-one mapping path between controls and view models, you will turn MVVM into a farce: you will have a simplified view projection in the view view layer and the view will be completely controlled from the view view layer. The view model itself will turn into a cross between a controller and a presenter. This is no longer MVVM.

    In short, do not do that.

    • Advise a different way. I do it for the first time and nothing else has occurred to me. Task. There is an application. The application can perform some commands. He has several windows of the same type. Windows can perform commands. Each window has TabControl and tabs. The content of each tab can perform commands. The question is how to stuff it all in the menu? That is, the displayed main menu should include: application commands, window commands, and open tab commands. - iRumba
    • I did not understand what one-to-one mapping means. Try to describe in more detail what my mistakes are, please, pointing to the code that I attached to the question. - iRumba
    • one
      @iRumba You have a GUI that is described not in a view, but in a view-model, the problem with your code is this. The menu should be completely built in the view, from the view I refer to operations from the view model. If several menus need to be merged, then this is a view-level task, not a view model. Accordingly, take out the merger in attached behaviors, inherit the menu, but at least write in the code-behind - any this option will be architecturally more correct. - Athari
    • So the menu has a tree structure. Just like the treeview. I can build a tree dynamically, but why is dynamic menu construction considered wrong? Explain the difference, please. - iRumba
    • And one more question. Suppose I add a new window to the project. This creates two files: Window.xaml and Window.xaml.cs. Xaml here is a View, and cs is a ViewModel? - iRumba

    I will answer not entirely on this question, but also about the menu. Simply, in the comments on one question said that I will show

    option how to build a menu ...

    Separators are also used here.

    To build a menu, you can implement the AppCommandService service, which allows you to register an ICommand implementation in yourself and contains the Menu property to which you can snap from xaml .

    I’ll say at once that this is not an ideal implementation of such a service, also because you cannot dynamically add and remove menu items. There is a built-in IoC container - Windsor, which in principle can be replaced with another container or replaced with a collection.


    The meaning is:

    1) give the command an attribute that contains a string with the item names separated by \ :

     [Menu(@"Файл\Сохранить Как\", "Сохранить Как PDF", Order = 2, Break = MenuBreak.Before)] public class SaveAsPdfCommand: ICommand { ... } //order - порядковый номер при отображении меню //Break - указывать, если надо поставить СЕПАРАТОР 

    2) Register this command in the service

     CommandService.RegisterCommand<SaveAsPdfCommand>(); //CommandService = new AppCommandService() - публичное свойство вью модели 

    3) Xaml to Xaml :

     <Menu ItemsSource="{Binding CommandService.Menu}"/> 

    Implementation:

    Attribute:

     [AttributeUsage(AttributeTargets.Class, AllowMultiple=true)] public class MenuAttribute : Attribute { public string Path { get; protected set; } public string Name { get; protected set; } public MenuAttribute(string path, string name) :this(path) { Name = name; } protected MenuAttribute (string path) { Path = path; Break = MenuBreak.None; } public int Order { get; set; } public MenuBreak Break { get; set; } public static MenuAttribute Extract(Type t) { var attributes = t.GetCustomAttributes(typeof(MenuAttribute), false).OfType<MenuAttribute>(); if (attributes == null || attributes.Count() == 0) return null; MenuAttribute result = attributes.FirstOrDefault(); //здесь можно вставить условие по выбору атрибута, если их много return result; } } public enum MenuBreak : byte { None, Before, After, Both } 

    Service:

     public class AppCommandService { private IWindsorContainer _InternalContainer = new WindsorContainer(new XmlInterpreter(new StaticContentResource(@"<configuration></configuration>"))); public void RegisterCommand<T>() where T: ICommand { _InternalContainer.Register(Component.For<ICommand, T>().LifeStyle.Singleton); } public T ResolveCommand<T>() where T : ICommand { return _InternalContainer.Resolve<T>(); } public void Execute<T>(object parameter = null) where T : ICommand { var command = _InternalContainer.Resolve<T>(); if (command != null && command.CanExecute(parameter)) { command.Execute(parameter); } } public void ExecuteOnAppDispatcher<T>(object parameter = null) where T : ICommand { Application.Current.Dispatcher.BeginInvoke(new Action<object>(Execute<T>), parameter); } #region MENU //MENU... private List<string> _DefaultMenuPaths = new List<string>(); public void RegisterMenuPath(string path) { _DefaultMenuPaths.Add(path); } //сборка дерева меню из аттрибутов private IEnumerable<MenuItem> GetMenuItems() { var handlers = _InternalContainer.Kernel.GetHandlers(typeof(ICommand)); var items = ( from x in handlers let attr = MenuAttribute.Extract(x.ComponentModel.Implementation) where attr != null orderby attr.Order let command = _InternalContainer.Resolve(x.ComponentModel.Implementation) as ICommand select new { attr, command} ).ToArray(); var generatedItems = new Dictionary<string, MenuItem>(); var menuRoots = new List<MenuItem>(); foreach (var item in _DefaultMenuPaths) { var path = item.Split('\\'); String currentPath = String.Empty; MenuItem parent = null; MenuItem lastItem = null; foreach (var menuPath in path) { currentPath += menuPath + "\\"; parent = lastItem; if (!generatedItems.TryGetValue(currentPath, out lastItem)) { lastItem = new MenuItem() { Header = menuPath }; generatedItems.Add(currentPath, lastItem); if (parent != null) { parent.Items.Add(lastItem); } else { menuRoots.Add(lastItem); } } } } foreach (var item in items) { var path = PreparePath(item.attr.Path).Split('\\'); MenuItem lastItem = null; MenuItem parent = null; String currentPath = String.Empty; foreach (var menuPath in path) { parent = lastItem; currentPath += menuPath + "\\"; if (!generatedItems.TryGetValue(currentPath, out lastItem)) { lastItem = new MenuItem() { Header = menuPath }; generatedItems.Add(currentPath, lastItem); if (parent != null) { parent.Items.Add(lastItem); } else { menuRoots.Add(lastItem); } } } var menu = new MenuItem { Header = item.attr.Name, Command = item.command }; if (lastItem != null) { if (item.attr.Break == MenuBreak.Before || item.attr.Break == MenuBreak.Both) { lastItem.Items.Add(new Separator()); } lastItem.Items.Add(menu); if (item.attr.Break == MenuBreak.After || item.attr.Break == MenuBreak.Both) { lastItem.Items.Add(new Separator()); } } else { menuRoots.Add(menu); } } return menuRoots; } protected ObservableCollection<MenuItem> _MenuItems = new ObservableCollection<MenuItem>(); public ObservableCollection<MenuItem> MenuItems { get { return _MenuItems; } } public void GenerateMenuItems() { MenuItems.Clear(); foreach (var item in GetMenuItems()) { MenuItems.Add(item); } } #endregion //добвление child - контейнера, чтобы резолвить команды с инъекциями public AppCommandService(IWindsorContainer container) { container.AddChildContainer(_InternalContainer); } //пустой конструктор, когда не надо вставлять зависимости public AppCommandService() { } private static string PreparePath(string path) { if (String.IsNullOrWhiteSpace(path)) return String.Empty; if (path[path.Length - 1] == '\\') { return path.Substring(0, path.Length - 1); } else { return path; } } } 
    • one
      Hm [Menu(@"Файл\Сохранить Как\", "Сохранить Как PDF", Order = 2, Break = MenuBreak.Before)] - this is again the coding of the presentation at the model level. - VladD
    • Doubtful decision. Even if you score on all sorts of MVVM and SRP, still stuffing separators and the order of items in the attributes will turn into a nightmare when editing. Yes, and the creation of separate classes for each team will not see every day. At the same time, DI hints at a large project, and in a large project the similar architecture of death is similar. - Athari
    • I understand you. Although for our project such a solution came up. There is a daddy with commands (the names of the points are taken from constants) One team performs one independent action - opens a window or tab with a report (we have Prism). Although it is possible, in other cases, this solution will not work. Let it then hang as a possible option. - Andrey K.
    • If anything, then you can replace the attribute with another class (class x) that would be created in the view model. And the rest is about the same. The view model would stuff these command classes into a service that would be its property and form the menu tree. Class x would implement ICommand and have additional properties, such as order, the presence of a separator and, most importantly, the path - by the same logic as in this example. - Andrey K.
    • one
      Thank you, but they will not understand me if I force the developers of the tabs to be soared with the menu. My task is to make life easier for them, and not to complicate it :( - iRumba