首页 > 解决方案 > 如何在 DataContext (INotifyPropertyChanged) 属性 (WPF - C# - XAML) 上设置点击事件?

问题描述

我想要完成的事情

我试图让我的嵌套菜单项更改显示的用户控件。用更多的技术术语来说,我试图:

  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#二传手

我已尝试添加此答案中建议的设置器,但我似乎无法从 to 创建点击MyMenu.cs事件MyMenuUserControl.cs

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

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

C# ICommand

我试过使用ICommand,在这个答案中建议,但我似乎无法从 to 创建中继MyMenu.cs命令MyMenuUserControl.cs

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


笔记

实际结构

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

我的菜单项:

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类型的属性。
这使我可以使用MyMenu'Map属性来确定要为给定MenuItem的单击显示哪个子视图。
是的,在地图上制作MainWindow地图可能更容易,但是我的逻辑MyMenu是一个最小的例子(有关更多信息,请参阅注释)。

公共部分类 MainWindow : Window { public 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;
}

}

标签: c#wpfxamlevent-handling

解决方案


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

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

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

简而言之,我会阅读一些有关 MVVM 的信息,因为它会使您的代码更清晰、更易于理解,并有望防止此类问题。


推荐阅读