Как динамически добавлять MenuItems (с заголовком) в меню WPF - PullRequest
9 голосов
/ 16 сентября 2010

[Edit # 3] - всем, кто читает этот вопрос: не ни при каких обстоятельствах используйте подход, изложенный в этом вопросе. Это кодовый ужас . Я свободно признаю это, зная, что все программисты загнали себя в угол в прошлом, и (особенно при изучении новой технологии) все мы были введены в заблуждение другими благонамеренными разработчиками в сети. Сначала прочитайте ответ Роберта, а затем прочитайте этот вопрос. Пожалуйста.

[Правка # 2b]

Я прошу прощения за длину этого вопроса - здесь есть вопрос (в конце!), Но я хотел убедиться, что исходный код был явным. Так или иначе.

[Редактировать # 2] - название вопроса изменено, чтобы более точно отразить ... вопрос.

[Редактировать] - Я обновил еще немного истории о том, как я оказался в дизайне / коде, который я сделал здесь: Обязательное сообщение в блоге . Если это поможет прояснить вопрос ниже, не стесняйтесь читать его ...

Оригинальный вопрос

Приложение, над которым я работаю, использует Prism и WPF с несколькими модулями (в настоящее время 3), один из которых содержит меню приложения. Первоначально меню было статичным с зацепками в CompositeCommand / DelegateCommands, которые отлично работали для маршрутизации нажатий кнопок на соответствующего докладчика. Каждый MenuItem использовал StackPanel в своем заголовке, чтобы отобразить содержимое в виде комбинации изображения и текстовой метки - вот как я искал:

<Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent">
                <MenuItem Name="MenuFile" AutomationProperties.AutomationId="File">
                    <MenuItem.Header>
                        <StackPanel>
                            <Image Height="24" VerticalAlignment="Center" Source="../Resources/066.png"/>
                            <ContentPresenter Content="Main"/>
                        </StackPanel>
                    </MenuItem.Header>
                    <MenuItem AutomationProperties.AutomationId="FileExit" Command="{x:Static local:ToolBarCommands.FileExit}">
                        <MenuItem.Header>
                            <StackPanel>
                                <Image Height="24" VerticalAlignment="Center" Source="../Resources/002.png"/>
                                <ContentPresenter Content="Exit"/>
                            </StackPanel>
                        </MenuItem.Header>
                    </MenuItem>
                </MenuItem>
                <MenuItem  Name="MenuHelp" AutomationProperties.AutomationId="Help" Command="{x:Static local:ToolBarCommands.Help}">
                    <MenuItem.Header>
                        <StackPanel>
                            <Image Height="24" VerticalAlignment="Center" Source="../Resources/152.png"/>
                            <ContentPresenter Content="Help"/>
                        </StackPanel>
                    </MenuItem.Header>
                </MenuItem>               
            </Menu>

К сожалению, приложение стало немного сложнее, и желательно, чтобы другие модули регистрировали себя в меню - следовательно, я пытался сделать меню динамичным. Цель состоит в том, чтобы другие модули (через службу) могли добавлять команды в меню по желанию - например, модуль A добавит пункт меню в модуль панели инструментов, который вызывает обработчик в модуле A. Есть несколько отличных статей. В этой теме я рассмотрел следующие вопросы: Создание меню WPF с привязкой к данным с использованием HierarchicalDataTemplate и Серия примеров WPF - образец меню с привязкой к данным HierarchicalDataTemplate . Следуя совету в статье, мне удалось создать динамически построенное меню без явных проблем с привязкой данных - оно может создать меню с элементами, связанными с моей моделью представления, отражая структуру ObservableCollection в модели представления

В настоящее время мой XAML выглядит следующим образом:

<UserControl x:Class="Modules.ToolBar.Views.ToolBarView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:model="clr-namespace:Modules.ToolBar.PresentationModels"
    xmlns:local="clr-namespace:Modules.ToolBar">
    <UserControl.Resources>
        <model:ToolBarPresentationModel x:Key="modelData" />
        <HierarchicalDataTemplate DataType="{x:Type model:ToolbarObject}"
                                  ItemsSource="{Binding Path=Children}">
            <ContentPresenter Content="{Binding Path=Name}"
                              Loaded="ContentPresenter_Loaded"
                              RecognizesAccessKey="True"/>
        </HierarchicalDataTemplate>
    </UserControl.Resources>
    <UserControl.DataContext>
        <Binding Source="{StaticResource modelData}"/>
    </UserControl.DataContext>
        <Menu Height="48" Margin="5,0,5,0" Name="MainMenu" VerticalAlignment="Top" Background="Transparent"
            ItemsSource="{Binding}">
        </Menu>
    </Grid>
</UserControl>

Код для представления делает тяжелую работу в методе ContentPresenter_Loaded:

   private void ContentPresenter_Loaded(object sender, System.Windows.RoutedEventArgs e)
    {
    ContentPresenter presenter = sender as ContentPresenter;
    if (sender != null)
    {
        DependencyObject parentObject = VisualTreeHelper.GetParent(presenter);
        bool bContinue = true;
        while (bContinue
            || parentObject == null)
        {
            if (parentObject is MenuItem)
                bContinue = false;
            else
                parentObject = VisualTreeHelper.GetParent(parentObject);
        }
        var menuItem = parentObject as MenuItem;
        if (menuItem != null)
        {
            ToolbarObject toolbarObject = menuItem.DataContext as ToolbarObject;
            StackPanel panel = new StackPanel();
            if (!String.IsNullOrEmpty(toolbarObject.ImageLocation))
            {
                Image image = new Image();
                image.Height = 24;
                image.VerticalAlignment = System.Windows.VerticalAlignment.Center;
                Binding sourceBinding = new Binding("ImageLocation");
                sourceBinding.Mode = BindingMode.TwoWay;
                sourceBinding.Source = toolbarObject;
                image.SetBinding(Image.SourceProperty, sourceBinding);
                panel.Children.Add(image);
            }
            ContentPresenter contentPresenter = new ContentPresenter();
            Binding contentBinding = new Binding("Name");
            contentBinding.Mode = BindingMode.TwoWay;
            contentBinding.Source = toolbarObject;
            contentPresenter.SetBinding(ContentPresenter.ContentProperty, contentBinding);
            panel.Children.Add(contentPresenter);
            menuItem.Header = panel;
            Binding commandBinding = new Binding("Command");
            commandBinding.Mode = BindingMode.TwoWay;
            commandBinding.Source = toolbarObject;
            menuItem.SetBinding(MenuItem.CommandProperty, commandBinding);                    
        }
    }
}

Как вы можете видеть, я пытаюсь воссоздать комбинацию StackPanel / Image / Name исходного меню, просто делая это в приведенном ниже коде. Попытка сделать это не сработала так хорошо - хотя объекты меню, безусловно, создаются, они «не отображаются» как что-либо, кроме пустых, интерактивных объектов - StackPanel, Image, Name и т. Д. Не отображаются. , Интересно, что это также приводит к удалению исходного текста в ContentPresent в HierarchicalDataTemplate.

Тогда возникает вопрос, есть ли способ установить свойство Header объекта MenuItem в событии Load так, чтобы оно правильно отображалось в UserControl? Является ли тот факт, что элементы в заголовке не отображаются, свидетельствует о проблеме привязки данных? Если это так, что будет правильным способом связать заголовок с временным объектом (StackPanel, который был создан в обработчике события загрузки)?

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

Ответы [ 2 ]

9 голосов
/ 16 сентября 2010

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

В этом случае мне кажется, что вы могли бы придумать довольно простую модель представления - например, объект, обладающий свойствами Text, Image, Command и Children, изатем создайте простой шаблон данных, который будет представлен как MenuItem.Затем все, что нужно для изменения содержимого ваших меню, манипулирует этой моделью.

Редактировать:

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

Если вы используете это, довольно просто создать динамические меню описываемого вами типа.Вам нужен класс MenuItemViewModel, в котором реализован INotifyPropertyChanged и который предоставляет следующие общедоступные свойства:

string Text
Uri ImageSource
ICommand Command
ObservableCollection<MenuItemViewModel> Children

Используя это:

<Menu DockPanel.Dock="Top" ItemsSource="{DynamicResource Menu}"/>

, где ItemsSource - это ObservableCollection<MenuItemViewModel>и используя этот шаблон:

<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}" />
        <Label Content="{Binding Text}" />
    </StackPanel>
</HierarchicalDataTemplate>

меню в окне точно представляют, что находится в коллекции, и динамически обновляются по мере добавления и удаления элементов как для элементов верхнего уровня, так и для потомков.

В дереве визуалов нет карабкаясь, нет объектов, созданных вручную, нет кода (кроме как в модели представления и во всем, что заполняет коллекцию в первую очередь).

Я построил довольно тщательно проработанный пример этого;Вы можете скачать проект здесь .

0 голосов
/ 17 сентября 2010

Другим возможным подходом может быть использование меню в качестве региона и согласование соглашения, поэтому все представления, добавленные в этот регион, имеют ViewModel со свойством с именем MenuHeader. Таким образом, адаптер региона может просто получить заголовок меню из контекста данных представления и установить его для элемента при его добавлении.

Нечто подобное сделано в Prism с представлениями, добавленными в область вкладок. Вы можете прочитать больше здесь .

Я надеюсь, что это дает некоторые полезные рекомендации.

Спасибо, Damian

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...