Как динамически добавлять MenuItems (с заголовком) В меню WPF
[Edit #3] - любому, кто читает этот вопрос: do не при любых обстоятельствах использовать подход, изложенный в этом вопросе. это кодирование ужас. Я свободно признаю это, зная, что все программисты работали в угол в прошлом, и (особенно при изучении новой технологии) мы все были введены в заблуждение другими, благонамеренными разработчиками на interweb. Сначала прочтите ответ Роберта,затем этот вопрос. Пожалуйста.
[Edit #2b]
Я прошу прощения за длину этого вопроса - здесь есть вопрос (в конце!), но я хотел убедиться, что исходный код был явный. В любом случае.
[Edit #2] - заголовок вопроса изменен, чтобы более точно отражать... вопрос.
[Edit] - я обновил еще немного истории о том, как я оказался в дизайне / коде, который я сделал здесь: обязательное Блог. Если это поможет прояснить вопрос ниже, не стесняйтесь его прочитать...
исходный вопрос
приложение, над которым я работаю, использует 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 образец серии-Databound 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.
вопрос тогда, есть ли способ установить свойство заголовка MenuItem в событии Load таким образом, чтобы оно отображалось на UserControl правильно? Является тот факт, что элементы в заголовке не отображается, что указывает на проблему привязки данных? Если да, то как правильно привязать заголовок к переходному объекту (StackPanel, который был создан в обработчике событий load)?
Я открыт для изменения чего - либо в коде выше-это все своего рода прототипирование, пытаясь выяснить, как лучше всего обрабатывать создание динамического меню. Спасибо!
2 ответов
я признаюсь, что я не копал так глубоко в ваш пример, как, возможно, я должен, но всякий раз, когда я вижу код, который ищет визуальное дерево, я думаю, может ли это быть обработано более явно в модели представления?
мне кажется, в этом случае вы могли бы придумать довольно простую модель представления - объект, подвергающийText
, Image
, Command
и Children
свойства, например - а затем создать простой шаблон данных, который для представления его в виде MenuItem
. Затем все, что нужно изменить содержимое ваших меню, манипулирует этой моделью.
Edit:
посмотрев на то, что вы делаете более подробно, и два примера, на которые вы ссылаетесь в своем блоге, я бьюсь головой о стол. Оба эти разработчика, по-видимому, находятся под заблуждением, что способ установить свойства в элементах меню, которые генерируются шаблоном, - это поиск по визуальному дереву в 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>
меню в окне точно представляют то, что находится в коллекции, и динамически обновляются по мере добавления и удаления элементов, как для элементов верхнего уровня, так и для потомков.
в визуальном дереве нет лазания, нет ручного создания объектов, нет кода (кроме как в модели представления и в том, что заполняет коллекцию в первую очередь).
я построил довольно тщательно проработанный пример этого; вы можете скачать проект здесь.
другой возможный подход может заключаться в том, что меню является регионом и согласуется с соглашением, поэтому все представления, добавленные в этот регион, имеют ViewModel со свойством MenuHeader. Таким образом, адаптер региона может просто получить заголовок меню из контекста данных представления и установить его в элемент при добавлении.
что-то подобное делается в Prism с видами, добавленными в область вкладок. Вы можете прочитать больше здесь.
Я надеюсь, что это дает некоторые полезные руководство.
спасибо, Дэмиан!--1-->