Как установить событие щелчка на свойстве DataContext (INotifyPropertyChanged) (WPF - C# - XAML)? - PullRequest
1 голос
/ 27 марта 2020

Что я пытаюсь выполнить sh

Я пытаюсь, чтобы мой вложенный пункт меню изменил отображаемый пользовательский элемент управления. В более технических терминах я пытаюсь:

  1. Получить событие щелчка, прикрепленное к вложенному MenuItem (из моего MyMenu.cs файла - реализует INotifyPropertyChanged), к ...
  2. Использовать RoutedEventHandler (может быть из файла MyMenu.cs? - реализует UserControl), чтобы ...
  3. Вызвать метод SwitchScreen (из моего файла MainWindow.cs - реализует Window)

Где я застреваю

Кажется, я не могу найти способ добавить событие щелчка в соответствующий пункт меню. Мой текущий лог c также требует, чтобы исходный отправитель передавался в качестве аргумента, чтобы я мог определить правильный MySubview для отображения.

XAML Handler

Я попытался добавить Событие click в xaml выглядит следующим образом, но оно добавляет обработчик только к первому уровню элемента меню (не к вложенным элементам элемента меню).

<MenuItem ItemsSource="{Binding Reports, Mode=OneWay}" Header="Reports">
    <MenuItem.ItemContainerStyle>
        <Style TargetType="{x:Type MenuItem}">
            <EventSetter Event="Click" Handler="MenuItem_Click"/>
        </Style>
    </MenuItem.ItemContainerStyle>
</MenuItem>

C# Setter

Имею попытался добавить сеттер, предложенный в этом ответе , но я не могу создать событие щелчка от MyMenu.cs до MyMenuUserControl.cs.

Style style = new Style();
style.BasedOn = menuItem2.Style;

style.Setters.Add(new EventSetter( /* ??? */ ));

C# ICommand

Я пытался использовать ICommand, предложил в этот ответ , но я не могу создать команду реле от MyMenu.cs до MyMenuUserControl.cs.

Возможно, я совершил что-то не так в одной из этих попыток, но теперь я перестал играть и готов бросить полотенце.


Примечания

Фактическая структура

На самом деле мой настоящий код имеет n-вложенные циклы foreach для генерации меню, и я удаляю уровень вложенности, если в он каждый перечисляемый (например, myObjects) имеет только один элемент. Удаление уровня вложенности также перемещает событие щелчка вверх на один уровень. Мое окончательное меню может выглядеть примерно так:

Мои пункты меню:

  • Элемент (menuItem1)
    • Элемент (menuItem2)
      • Элемент ( menuItem3) + событие клика
      • Item (menuItem3) + событие клика
    • Item (menuItem2) + событие клика (см. A )
  • Item (menuItem1) + событие щелчка (см. B )

A : только один menuItem3 является вложенным, поэтому мы удаляем его (это избыточно) и перемещаем событие click до menuItem2.

B : только один menuItem2 является вложенным, и у него только один menuItem3. Оба удаляются, так как они избыточны, и мы перемещаем событие click, перемещаемое в menuItem1.

Именно поэтому я бы хотел сохранить создание пунктов меню в классе MyMenu.

Другие предложения

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


Код

MyMenu.cs

Конструктор в этом классе генерирует пункты моего меню и его подменю. Здесь я пытаюсь добавить событие клика.

class MyMenu : INotifyPropertyChanged
{
    private List<MenuItem> menuItems = new List<MenuItem>();
    public List<MenuItem> MenuItems
    {
         get { return menuItem; }
         set
         {
             menuItem = value;
             OnPropertyChanged();
         }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public List<Tuple<MyObject, MenuItem>> Map { get; private set; } = new List<Tuple<MyObject, MenuItem>>();

    public MyMenu(List<MyObject> myObjects)
    {
         foreach(MyObject myObject in myObjects)
         {
             MenuItem menuItem1 = new MenuItem { Header = myObject.Name };

             foreach(string s in myObject.Items)
             {
                 MenuItem menuItem2 = new MenuItem { Header = s };

                 // Add click event to menuItem2 here

                 menuItem1.Items.Add(menuItem2);
                 Map.Add(new Tuple<MyObject, MenuItem>(myObject, menuItem2));
             }
             MenuItem.Add(menuItem1);
         }
    }

    private void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

MyMenuUserControl.xaml

Пример минимального кода UserControl (используется атрибуты xmlns по умолчанию). MyMenuUserControl.xaml.cs имеет только конструктор с InitializeComponent();

<UserControl>
    <!-- xmlns default attributes in UserControl above removed for minimal code -->
    <Menu>
        <Menu.ItemsPanel>
            <ItemsPanelTemplate>
                <DockPanel VerticalAlignment="Stretch"/>
            </ItemsPanelTemplate>
        </Menu.ItemsPanel>
        <MenuItem ItemsSource="{Binding MenuItems, Mode=OneWay}" Header="My menu items"/>
    </Menu>
</UserControl>

MyDataContext.cs

Пример минимального кода (тот же код PropertyChangedEventHandler и OnPropertyChanged(), что и MyMenu.cs). Конструктор просто устанавливает свойства Menu и Subviews.

class MyDataContext: INotifyPropertyChanged {private MyMenu menu; publi c MyMenu Menu {get {return menu; } set {menu = value; OnPropertyChanged (); }}

private List<MySubview> mySubviews;
public List<MySubview> MySubviews
{
    get { return mySubviews; }
    set
    {
        mySubviews = value;
        OnPropertyChanged();
    }
}
// ... rest of code removed to maintain minimal code

}

MainWindow.xaml.cs

Subview содержит свойство типа MyObject. Это позволяет мне использовать MyMenu свойство Map, чтобы определить, какое подпредставление отображать для данного клика MenuItem. Да, сделать карту на карте MainWindow может быть проще, однако логика c, которую я имею в MyMenu, является минимальным примером (см. Примечания для получения дополнительной информации).

publi c частичный класс MainWindow: Window {publi c MainWindow () {InitializeComponent ();

    // I get my data here
    List<MyObject> myObjects = ...
    List<MySubview> mySubviews = ...

    DataContext = new MyDataContext(new MyMenu(myObjects), new MySubviews(mySubviews));
}

private void SwitchScreen(object sender, RoutedEventArgs e)
{
    MyDataContext c = (MyDataContext)DataContext;
    MyObject myObject = c.MyMenu.Map.Where(x => x.Item2.Equals(sender as MenuItem)).Select(x => x.Item1).First();
    MySubview shownSubview = c.MySubviews.Where(x => x.MyObject.Equals(myObject)).First();

    c.MySubviews.ForEach(x => x.Visibility = Visibility.Collapsed);
    shownSubview.Visibility = Visibility.Visible;
}

}

Ответы [ 2 ]

2 голосов
/ 27 марта 2020

Wpf предназначен для использования через шаблон MVVM. Похоже, вы пытаетесь манипулировать визуальным деревом напрямую, из-за чего, вероятно, возникли многие ваши проблемы, поскольку вы оказались на полпути между мирами.

Что такое MyMenu.cs? Он выглядит как модель представления, но содержит визуальные элементы (MenuItem). Виртуальные машины не должны содержать визуальных классов. Они являются абстракцией данных представления.

Похоже, что ваш MyMenuVM.cs должен просто выставить ваш List <MyObject>, и ваше меню просмотра должно привязаться к этому. MenuItem уже имеет встроенную ICommand (после того, как все меню созданы для нажатия), поэтому вам не нужно добавлять свои собственные обработчики кликов. Вместо этого вы связываете MenuItem.Command с командой в вашей виртуальной машине и, возможно, связываете CommandParameter, чтобы указать, какой MyObject запускает команду.

Короче говоря, я бы немного прочитал о MVVM, потому что это будет сделать ваш код намного чище, проще для понимания и, надеюсь, предотвратить подобные проблемы.

1 голос
/ 28 марта 2020

Menu может создавать свои предметы из ItemsSource, используя любой объект IEnumerable. Одна вещь, которую вы должны сделать - установить DataTemplate для сопоставления свойств MenuItem с вашими свойствами VM.

Я собрал несколько ссылок для вас, которые могут быть полезны для понимания того, как это можно сделать с MVVM:

  • RelayCommand класс - см. Релейный лог команд c раздел. С моей точки зрения (WPF newb ie) это лучший способ использования команд.
  • HierarchicalDataTemplate - то же, что DataTemplate, но с ItemsSource.
  • Трюк с разделителями - может помочь в создании меню, содержащего не только MenuItems в ItemsSource (проверено!)
  • ObservableCollection - используйте его вместо List для пользовательского интерфейса. Он запускает событие CollectionChanged внутри, когда вы динамически добавляете или удаляете предметы. А Control с ItemsSource обновляет свою компоновку немедленно, из коробки.

Почему бы не просто набор элементов управления?

Потому что вы может нарушить работу вашего приложения, вызвав исключение при попытке взаимодействия с элементами пользовательского интерфейса из разных Thread. Да, вы можете использовать Dispatcher.Invoke для исправления, но есть лучший способ избежать этого: просто используйте Binding. Таким образом, вы можете забыть о проблеме Dispatcher.Invoke -where.

Простой пример

Использование одного RelayCommand для всех MenuItem экземпляров.

MainWindow. xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp1"
        Title="MainWindow" Height="300" Width="400">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Window.Resources>
        <local:MenuItemContainerTemplateSelector x:Key="MenuItemContainerTemplateSelector"/>
        <Style x:Key="SeparatorStyle" TargetType="{x:Type Separator}" BasedOn="{StaticResource ResourceKey={x:Static MenuItem.SeparatorStyleKey}}"/>
        <Style x:Key="MenuItemStyle" TargetType="{x:Type MenuItem}">
            <Setter Property="Header" Value="{Binding Header}"/>
            <Setter Property="Command" Value="{Binding DataContext.MenuCommand, RelativeSource={RelativeSource AncestorType=Window}}"/>
            <Setter Property="CommandParameter" Value="{Binding CommandName}"/>
        </Style>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Menu Grid.Row="0" >
            <MenuItem Header="Menu" ItemsSource="{Binding MenuItems}" UsesItemContainerTemplate="True" ItemContainerTemplateSelector="{StaticResource MenuItemContainerTemplateSelector}">
                <MenuItem.Resources>
                    <HierarchicalDataTemplate DataType="{x:Type local:MyMenuItem}" ItemsSource="{Binding Items}" >
                        <MenuItem Style="{StaticResource MenuItemStyle}" UsesItemContainerTemplate="True" ItemContainerTemplateSelector="{StaticResource MenuItemContainerTemplateSelector}"/>
                    </HierarchicalDataTemplate>
                    <DataTemplate DataType="{x:Type local:MySeparator}">
                        <Separator Style="{StaticResource SeparatorStyle}"/>
                    </DataTemplate>
                </MenuItem.Resources>
            </MenuItem>
        </Menu>
    </Grid>
</Window>

RelayCommand.cs

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
    public void Execute(object parameter) => _execute(parameter);
}

MenuItemContainerTemplateSelector.cs

public class MenuItemContainerTemplateSelector : ItemContainerTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, ItemsControl parentItemsControl) =>
        (DataTemplate)parentItemsControl.FindResource(new DataTemplateKey(item.GetType()));
}

MenuItemViewModel.cs

public interface IMyMenuItem
{
}
public class MySeparator : IMyMenuItem
{
}
public class MyMenuItem : IMyMenuItem, INotifyPropertyChanged
{
    private string _commandName;
    private string _header;
    private ObservableCollection<IMyMenuItem> _items;
    public string Header
    {
        get => _header;
        set
        {
            _header = value;
            OnPropertyChanged();
        }
    }
    public string CommandName
    {
        get => _commandName;
        set
        {
            _commandName = value;
            OnPropertyChanged();
        }
    }
    public ObservableCollection<IMyMenuItem> Items
    {
        get => _items ?? (_items = new ObservableCollection<IMyMenuItem>());
        set
        {
            _items = value;
            OnPropertyChanged();
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

MainViewModel.cs

public class MainViewModel : INotifyPropertyChanged
{
    private ObservableCollection<IMyMenuItem> _menuItems;
    private ICommand _menuCommand;

    public ObservableCollection<IMyMenuItem> MenuItems
    {
        get => _menuItems ?? (_menuItems = new ObservableCollection<IMyMenuItem>());
        set
        {
            _menuItems = value;
            OnPropertyChanged();
        }
    }

    public ICommand MenuCommand => _menuCommand ?? (_menuCommand = new RelayCommand(param =>
    {
        if (param is string commandName)
        {
            switch (commandName)
            {
                case "Exit":
                    Application.Current.MainWindow.Close();
                    break;
                default:
                    MessageBox.Show("Command name: " + commandName, "Command executed!");
                    break;
            }
        }
    }, param =>
    {
        return true; // try return here false and check what will happen
    }));
    public MainViewModel()
    {
        MenuItems.Add(new MyMenuItem() { Header = "MenuItem1", CommandName = "Command1" });
        MenuItems.Add(new MyMenuItem() { Header = "MenuItem2", CommandName = "Command2" });
        MyMenuItem m = new MyMenuItem() { Header = "MenuItem3" };
        MenuItems.Add(m);
        m.Items.Add(new MyMenuItem() { Header = "SubMenuItem1", CommandName = "SubCommand1" });
        m.Items.Add(new MySeparator());
        m.Items.Add(new MyMenuItem() { Header = "SubMenuItem2", CommandName = "SubCommand2" });
        m.Items.Add(new MyMenuItem() { Header = "SubMenuItem3", CommandName = "SubCommand3" });
        MenuItems.Add(new MySeparator());
        MenuItems.Add(new MyMenuItem() { Header = "Exit", CommandName = "Exit" });
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Screenshot

...