【问题标题】:How to set a click event on DataContext (INotifyPropertyChanged) property (WPF - C# - XAML)?如何在 DataContext (INotifyPropertyChanged) 属性 (WPF - C# - XAML) 上设置点击事件?
【发布时间】:2020-03-28 17:58:55
【问题描述】:

我想要完成的事情

我正在尝试让我的嵌套菜单项更改显示的用户控件。用更专业的术语来说,我试图:

  1. 获取附加到嵌套MenuItem 的点击事件(来自我的MyMenu.cs 文件-实现INotifyPropertyChanged),以...
  2. 使用RoutedEventHandler(可能来自MyMenu.cs 文件?- 实现UserControl),以...
  3. 调用SwitchScreen 方法(来自我的MainWindow.cs 文件-实现Window

我卡住的地方

我似乎找不到将点击事件添加到相应菜单项的方法。
我当前的逻辑还要求将原始发件人作为参数传递,以便我可以识别要显示的正确MySubview

XAML 处理程序

我尝试如下在 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,在 this answer 中建议,但我似乎无法创建从 MyMenu.csMyMenuUserControl.cs 的点击事件。

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

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

C# ICommand

我尝试过使用ICommand,在this answer 中提出建议,但我似乎无法创建从MyMenu.csMyMenuUserControl.cs 的中继命令。

我可能在其中一次尝试中做错了什么,但我现在已经过了玩耍的地步,准备认输。


注意事项

实际结构

实际上,我的实际代码具有 n 嵌套 foreach 循环来生成菜单,如果 foreach 可枚举(例如 myObjects)只有一个元素,我将删除一层嵌套。
移除一层嵌套也会将点击事件上移一层。
我的最终菜单可能如下所示:

我的菜单项:

  • 项目 (menuItem1)
    • 项目 (menuItem2)
      • 项目(menuItem3)+点击事件
      • 项目(menuItem3)+点击事件
    • 项目(menuItem2)+点击事件(见A
  • 项目(menuItem1)+点击事件(见B

A:只有一个 menuItem3 是嵌套的,因此我们将其移除(这是多余的)并将点击事件向上移动到 menuItem2。

B:menuItem2只有一个嵌套,menuItem3只有一个。两者都被删除,因为它们是多余的,我们将点击事件移动到 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

最小代码示例(PropertyChangedEventHandlerOnPropertyChanged() 代码与 MyMenu.cs 相同)。 构造函数只需设置MenuSubviews 属性。

类 MyDataContext : INotifyPropertyChanged { 私人 MyMenu 菜单; 公共 MyMenu 菜单 { 得到{返回菜单; } 放 { 菜单=价值; 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 类型的属性。
这使我可以使用MyMenuMap 属性来确定为给定MenuItem 的点击显示哪个子视图。
是的,在MainWindow 地图上制作地图可能更容易,但是我在MyMenu 中的逻辑是一个最小示例(有关更多信息,请参阅Notes)。

公共部分类 MainWindow : Window { 公共主窗口() { 初始化组件();

    // 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;
}

}

【问题讨论】:

    标签: c# wpf xaml event-handling


    【解决方案1】:

    Wpf 旨在通过 MVVM 模式使用。您似乎正试图直接操纵视觉树,这可能是您的许多问题的根源,因为您似乎处于世界之间。

    MyMenu.cs 是什么?它看起来像一个视图模型,但它包含可视项(MenuItem)。 VM 不应包含任何可视类。它们是视图的数据抽象。

    看起来您的MyMenuVM.cs 应该只公开您的List &lt;MyObject&gt;,并且您的视图菜单应该绑定到它。 MenuItem 已经有一个内置的ICommand (在所有菜单都是为点击而制作的),所以你不需要添加自己的点击处理程序。相反,您将 MenuItem.Command 绑定到 VM 中的命令,并可能绑定 CommandParameter 以提供哪个 MyObject 正在触发该命令。

    简而言之,我会阅读一些有关 MVVM 的内容,因为它可以让您的代码更清晰、更易于理解,并有望避免此类问题。

    【讨论】:

      【解决方案2】:

      Menu 可以使用任何IEnumerable 对象从ItemsSource 构建其项目。您应该做的一件事 - 设置 DataTemplate 用于将 MenuItem 的属性映射到您的 VM 属性。

      我为您收集了一些链接,这些链接可能有助于您了解如何使用 MVVM 完成它:

      • RelayCommand 类 - 请参阅中继命令逻辑部分。从我(WPF 新手)的角度来看,这是使用命令的最佳方式。
      • HierarchicalDataTemplate - 与 DataTemplate 相同,但使用 ItemsSource
      • Trick with Separators - 可能有助于在其 ItemsSource 中制作不仅包含 MenuItems 的菜单(已测试!)
      • ObservableCollection - 使用它而不是 List 用于 UI 目的。当您动态添加或删除项目时,它会在内部触发 CollectionChanged 事件。使用 ItemsSource 的 Control 会立即更新其布局,开箱即用。

      为什么不只是控件的集合?

      因为您可能会在尝试与来自不同Thread 的 UI 元素交互时破坏您的应用程序,从而导致异常。是的,您可以使用Dispatcher.Invoke 进行修复,但有一个更好的方法可以避免它:只需使用Binding。因此,您可能会忘记 Dispatcher.Invoke-everywhere 问题。

      简单示例

      对所有 MenuItem 实例使用单个 RelayCommand

      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));
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-07-06
        • 2011-06-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多