Canacourse
Canacourse

Reputation: 1865

WPF command not working for submenu items in MVVM application

I have a menu which is built from a collection at runtime. This is all working as shown.

Working Menu But if the menu contains child items (Child1, Child2 etc) the ReactiveCommand MenuCommand is never called.

If I remove all children from the menu so that the menu only contains parent items then MenuCommand is called. I am fairly new to WPF. I have re-created the problem in a sample app (code below). There are no visible binding errors in VS.

    public partial class MainWindow : Window
                {
                    public MainWindow()
                    {
                        InitializeComponent();
                        DataContext = new MainWindowViewModel();
                    }
                }


            public class Service
            {
                public Service(string menuHeading, string menuSubHeading)
                {
                    MenuHeading = menuHeading;
                    MenuSubHeading = menuSubHeading;
                }

                public string MenuHeading { get; set; }
                public string MenuSubHeading { get; set; }
            }


            public static class MenuBuilder
            {
                public static ReactiveList<MenuItem> Build(ReactiveList<Service> services)
                {
                    ReactiveList<MenuItem> menuItems = new ReactiveList<MenuItem>();

                    foreach (var service in services)
                    {
                        AddOrUpdate(menuItems, service);
                    }

                    return menuItems;
                }

                private static void AddOrUpdate(ReactiveList<MenuItem> menu, Service service)
                {
                    if (menu.Any((_ => _.Header.ToString() == service.MenuHeading)))
                    {
                        var item = menu.FirstOrDefault(x => x.Header.ToString() == service.MenuHeading);
                        item.Items.Add(new MenuItem() { Header = service.MenuSubHeading }); 
                        //if above line removed MenuCommand works
                    }
                    else
                    {
                        menu.Add(new MenuItem() { Header = service.MenuHeading });
                        var item = menu.FirstOrDefault(x => x.Header.ToString() == service.MenuHeading);
                        item.Items.Add(new MenuItem() { Header = service.MenuSubHeading }); 
                        //if above line removed MenuCommand works
                    }
                }
            }


            public class MainWindowViewModel : ReactiveObject
                    {
                        public MainWindowViewModel()
                        {
                            MenuCommand = ReactiveCommand.Create<Object>(selectedItem => OnMenuItemSelected(selectedItem));
                            MenuCommand.Execute().Subscribe();
                        }

                        public ReactiveCommand<Object, Unit> MenuCommand { get; }

                        private ReactiveList<MenuItem> servicesMenu;

                         private ReactiveList<Service> Services = new ReactiveList<Service>()
                        {
                          new Service("Parent1", "Child1"),
                          new Service("Parent2", "Child1"),
                          new Service("Parent2", "Child2"),
                        };


                        public ReactiveList<MenuItem> ServicesMenu
                        {
                            get
                            {
                                if (servicesMenu == null)
                                {
                                    servicesMenu = MenuBuilder.Build(Services);
                                    return servicesMenu;
                                }
                                else
                                {
                                    return servicesMenu;
                                }
                            }
                        }

                        private void OnMenuItemSelected(Object selectedItem)
                        {
                             //This method is only called when the menu does not contain any child items
                        } 
                    }


<Grid>
        <StackPanel Orientation="Vertical">
            <Button Name="Button" Content="Button" Padding="5" HorizontalAlignment="Left" 
                    Tag="{Binding RelativeSource={RelativeSource Self}, Path=DataContext}">

                <Button.ContextMenu>
                    <ContextMenu x:Name="MainMenu" ItemsSource="{Binding ServicesMenu}"
                            DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <ContextMenu.ItemContainerStyle>
                            <Style TargetType="MenuItem">
                                <Setter Property="Command"
                                        Value="{Binding DataContext.MenuCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Button}}}" />
                                <Setter Property="CommandParameter"
                                        Value="{Binding RelativeSource={RelativeSource Self}}" />
                            </Style>
                        </ContextMenu.ItemContainerStyle>
                    </ContextMenu>
                </Button.ContextMenu>
            </Button>
        </StackPanel>
    </Grid>

Updated XAML after suggestions form Glenn

 <Grid>
    <StackPanel Orientation="Vertical">
        <Button Name="Button" Content="Button" Padding="5" HorizontalAlignment="Left" 
                Tag="{Binding RelativeSource={RelativeSource Self}, Path=DataContext}">

            <Button.ContextMenu>
                <ContextMenu x:Name="MainMenu" ItemsSource="{Binding ServicesMenu}"
                        DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">

                    <ContextMenu.ItemContainerStyle>
                        <Style TargetType="MenuItem">
                            <Setter Property="Header" Value="{Binding Header}" />
                            <Setter Property="Command" Value="{Binding Command}" />
                            <!--<Setter Property="Command" Value="{Binding MenuCommand}" /> was also tried-->

                            <Setter Property="CommandParameter" Value="{Binding}" />

                        </Style>
                    </ContextMenu.ItemContainerStyle>
                </ContextMenu>
            </Button.ContextMenu>
        </Button>
    </StackPanel>
</Grid>

Upvotes: 1

Views: 942

Answers (1)

Glenn Watson
Glenn Watson

Reputation: 2888

I suspect this is because child items placement target wouldn't be the Button like you expect, it would be the parent MenuItem.

One way I've gotten around this in the past is using MVVM approach for these type of menu items.

Create a Menu Item VM (you call them Service above) for your items (similar to what you already doing). In the VM have a Command property and pass in your command as part of it's constructor. Then you can just do {Binding MenuCommand} from your Item Container Style.

Also don't create the MenuItem's directly in your ViewModel, instead just bind direct to the Services. I would also recommend creating your sub-services as a ObservableCollection directly inside your Service, then in your item container set the ItemsSource property to bind to the sub-children of your Services.

Upvotes: 1

Related Questions