What I'm trying to accomplish
I'm trying to have my nested menu item change the shown user control. In more technical terms, I'm trying to:
- Get a click event attached to a nested
MenuItem
(from myMyMenu.cs
file - implementsINotifyPropertyChanged
), to... - Use
RoutedEventHandler
(maybe from theMyMenu.cs
file? - implementsUserControl
), to... - Call the
SwitchScreen
method (from myMainWindow.cs
file - implementsWindow
)
Where I'm getting stuck
I can't seem to find a way to add the click event to the appropriate menu item.
My current logic also requires the original sender to be passed as an argument so that I can identify the correct MySubview
to display.
XAML Handler
I've tried adding the click event in xaml as follows, but it only adds the handler to the first menu item level (not to nested menu item elements).
<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
I've tried adding a setter, suggested in this answer, but I can't seem to create a click event from MyMenu.cs
to MyMenuUserControl.cs
.
Style style = new Style();
style.BasedOn = menuItem2.Style;
style.Setters.Add(new EventSetter( /* ??? */ ));
C# ICommand
I've tried using ICommand
, suggested in this answer, but I can't seem to create a relay command from MyMenu.cs
to MyMenuUserControl.cs
.
I may be doing something wrong in one of these attempts, but I'm now past the point of playing around and ready to throw in the towel.
Notes
Actual structure
In reality, my actual code has n-nested foreach loops to generate the menu and I remove a level of nesting if a the foreach enumerable (e.g. myObjects
) only has one element.
The removal of a level of nesting also moves the click event up one level.
My final menu could look something like this:
My menu items:
- Item (menuItem1)
- Item (menuItem2)
- Item (menuItem3) + click event
- Item (menuItem3) + click event
- Item (menuItem2) + click event (see A)
- Item (menuItem2)
- Item (menuItem1) + click event (see B)
A: Only one menuItem3 is nested, so we remove it (it's redundant) and we move the click event up to menuItem2.
B: Only one menuItem2 is nested, and it only has one menuItem3. Both are removed as they're redundant and we move the click event is moved to menuItem1.
This is why I'd like to maintain the creation of the menu items in the MyMenu
class.
Other suggestions
I could be going about this completely wrong and I'm open to suggestions that change the way I'm going about this.
Code
MyMenu.cs
The constructor in this class generates my menu items and its sub-menu items.
This is where I'm trying to add a click event.
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
Minimal code sample UserControl (uses default xmlns attributes).MyMenuUserControl.xaml.cs
only has constructor with 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
Minimal code sample (same PropertyChangedEventHandler
and OnPropertyChanged()
code as MyMenu.cs
).
Constructor simply sets Menu
and Subviews
properties.
class MyDataContext : INotifyPropertyChanged { private MyMenu menu; public 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
contains a property of MyObject
type.
This allows me to use MyMenu
's Map
property to identify which Subview to display for a given MenuItem
's click.
Yes, making the map at the MainWindow
map might be easier, however the logic I have in MyMenu
is a minimal example (see Notes for more info).
public partial class 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;
}
}