Получить выбранный пункт меню wpf
У меня есть меню и некоторые подменю
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "Select a Building",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "Building 4",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel { Header = "< 500",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel {Header = "Executives" },
new MenuItemViewModel {Header = "Engineers" },
new MenuItemViewModel {Header = "Sales" },
new MenuItemViewModel {Header = "Marketing"},
new MenuItemViewModel {Header = "Support"}
}
},
new MenuItemViewModel { Header = "500 - 999",
MenuItems = new ObservableCollection<MenuItemViewModel>
{
new MenuItemViewModel {Header = "Executives" },
new MenuItemViewModel {Header = "Engineers" },
new MenuItemViewModel {Header = "Sales" },
new MenuItemViewModel {Header = "Marketing"},
new MenuItemViewModel {Header = "Support"}
}
}
}
}
}
Я пытаюсь зафиксировать значение каждого выбора, который делает пользователь, и отобразить его в списке. Например, пользователь выбирает "Building 4", затем "500 - 999", а затем "support", так как эти значения выбираются, они заполняют список. У меня есть функция в MenuItemViewModel, которая называется Execute(), она получит заголовок последнего выбранного значения, т.е. "поддержка", но я не могу понять, как получить это значение в списке.
Вот ViewModel
public class MenuItemViewModel
{
private readonly ICommand _command;
string Value;
public ObservableCollection<Cafe> Cafes
{
get;
set;
}
public MenuItemViewModel()
{
_command = new CommandViewModel(Execute);
}
public string Header { get; set; }
public ObservableCollection<MenuItemViewModel> MenuItems { get; set; }
public ICommand Command
{
get
{
return _command;
}
}
public void Execute()
{
MessageBox.Show("Clicked at " + Header);
}
}
И, наконец, xaml для MenuItem и ListBox:
<Menu x:Name="buildingMenu" Margin="0,15,0,0" HorizontalAlignment="Left" Height="20" Width="200" ItemsSource="{Binding MenuItems}" >
<Menu.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding Command}" />
</Style>
</Menu.ItemContainerStyle>
<Menu.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=MenuItems}">
<TextBlock Text="{Binding Header}"/>
</HierarchicalDataTemplate>
</Menu.ItemTemplate>
</Menu>
<ListBox Name="selectionListBox" HorizontalAlignment="Right" Height="179" Margin="0,177,55,0" VerticalAlignment="Top" Width="500" />
Я пытался добавить заголовок в список в ViewModel, но я не могу получить ничего работать. Есть ли что-то похожее на SelectedValue комбинированного списка, которое можно использовать здесь? Спасибо за помощь
2 ответа
Это на самом деле намного сложнее, чем вы ожидаете, потому что вы используете меню таким образом, что они не предназначены для использования.
Прежде всего вам понадобится список элементов в вашей модели представления для привязки, например:
public ObservableCollection<string> ListItems { get; set; }
И соответствующая привязка в вашем ListBox:
<ListBox ItemsSource="{Binding ListItems}" Name="selectionListBox" />
Затем вам нужно будет заполнить этот список элементами по мере их нажатия. Это сложно с тем, как вы сделали вещи в данный момент, потому что ListItems
должен быть в вашей основной модели представления, но ваш обработчик команд находится в MenuItemViewModel. Это означает, что вам нужно либо добавить способ для MenuItemViewModel для вызова своего родителя, либо, что еще лучше, переместить обработчик команд в модель основного представления (чтобы он теперь был общим) и передать элементу MenuItems в свой объект контекста данных. Я использую MVVM Lite и в этой среде вы делаете что-то вроде этого:
private ICommand _Command;
public ICommand Command
{
get { return (_Command = (_Command ?? new RelayCommand<MenuItemViewModel>(Execute))); }
}
public void Execute(MenuItemViewModel menuItem)
{
// ... do something here...
}
Похоже, вы не используете MVVM Lite, но какой бы ни была ваша платформа, она будет поддерживать передачу параметра в вашу функцию execute, поэтому я оставлю это для вас, чтобы посмотреть. В любом случае привязки команд MenuItem теперь должны быть изменены, чтобы указывать на этот общий обработчик:
<Style TargetType="{x:Type MenuItem}">
<Setter Property="Command" Value="{Binding ElementName=buildingMenu, Path=DataContext.Command}" />
</Style>
Другое ваше требование - чтобы список заполнялся при выборе каждого элемента подменю, и здесь все становится неприятно. Подменю не запускают команды при закрытии, они запускают события SubmenuOpened и SubmenuClosed. События не могут быть привязаны к обработчикам команд в вашей модели представления, поэтому вместо этого вы должны использовать обработчики кода. Если вы явно декларировали свои MenuItems в XAML, вы можете просто установить их прямо там, но ваше Menu привязывается к коллекции, поэтому ваши MenuItems заполняются платформой, и вам нужно будет установить обработчик, добавив EventSetter в ваш стиль:
<Style TargetType="{x:Type MenuItem}">
<EventSetter Event="SubmenuOpened" Handler="OnSubMenuOpened" />
<Setter Property="Command" Value="{Binding ElementName=buildingMenu, Path=DataContext.Command}" />
</Style>
Это работает, так как вызывает указанную функцию-обработчик в вашем коде MainWindow:
private void OnSubmenuOpened(object sender, RoutedEventArgs e)
{
// uh oh, what now?
}
Проблема, конечно, в том, что в представлении существуют обработчики событий, но в MVVM это необходимо в модели представления. Если вы счастливы испортить ваши взгляды, подобные этому, чтобы заставить ваше приложение работать, проверьте этот SO вопрос на пример кода, показывающего, как заставить его работать. Однако, если вы такой же приверженец MVVM, как и я, вам, вероятно, понадобится решение, не включающее в себя выделение кода, и в этом случае вам снова придется обращаться к своей платформе. MVVM Lite обеспечивает поведение, которое позволяет вам преобразовывать события в команду, вы обычно используете это так:
<MenuItem>
<i:Interaction.Triggers xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<i:EventTrigger EventName="OnSubmenuOpened">
<cmd:EventToCommand Command="{Binding SubmenuOpenedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
Проблема с поведением в том, что вы не можете установить их в стиле. Стили применяются ко всем объектам, поэтому EventSetter работает нормально. Поведения должны быть созданы для каждого объекта, для которого они используются.
Итак, последняя часть головоломки состоит в том, что если у вас есть случаи в MVVM, где вам нужно установить поведение в стиле, то вам нужно будет использовать решение, опубликованное vspivak в его статье " Использование поведения System.Windows.Interactivity и действий в Стили WPF/Silverlight. Я сам использовал это в коммерческих проектах, и это хорошее общее решение большой проблемы перенаправления обработчиков событий на обработчики команд в стилях.
Надеюсь это ответит на твой вопрос. Я уверен, что это кажется нелепо запутанным, но то, что вы пытаетесь сделать, это довольно патологический сценарий, выходящий за рамки обычного использования WPF.
Положите
Click
событие для любого, что создается, и когда событие запускается, получить элемент данных из источника
DataContext
которые посеяли
MenuItem
.
Пример
Я создал недавний список операций, которые были необходимы в виде подменю в
Recent
меню. Я посеял подсписок как образец типа
MRU
который использовался для загрузки меню * через
ItemsSource
и проводился в
ObservableCollection<MRU>() MRUS
:
Xaml
<MenuItem Header="Recent"
Click="SelectMRU"
ItemsSource="{Binding Path=MRUS}">
...
</MenuItem>
Код позади
private void SelectMRU(object sender, RoutedEventArgs e)
{
try
{
var mru = (e.OriginalSource as MenuItem).DataContext as MRU;
if (mru is not null)
{
VM.ClearAll();
this.Title = mru.Name;
ShowJSON(File.ReadAllText(mru.Address));
}
}
catch (Exception ex)
{
VM.Error = ex.Demystify().ToString();
}
}
public class MRU
{
public string Name { get; set; }
public string Address { get; set; }
public string Data { get; set; }
public bool IsValid => !string.IsNullOrWhiteSpace(Name);
public bool IsFile => !string.IsNullOrWhiteSpace(Address);
public bool IsData => !IsFile;
public MRU(string address)
{
if (string.IsNullOrWhiteSpace(address)
|| !File.Exists(address)) return;
Address = address;
Name = System.IO.Path.GetFileName(address);
}
// This will be displayed as a selectable menu item.
public override string ToString() => Name;
}
Обратите внимание, что полный код меню можно просмотреть в общедоступном примере градиентного меню с MRU