Search code examples
wpfdata-bindingdatagridcontextmenucaliburn.micro

Caliburn.Micro: How to bind a function to a context menu item in the RowDetailsTemplate of a DataGrid?


I have a datagrid of Items that includes a datagrid of SubItems for the row details. There is a context menu for the main "Items" datagrid with functions to add or remove items, this is working fine. There is also a context menu for the row details datagrid "SubItems" with similar functions that I cannot get to bind properly. If executed i get the following exception "System.Exception: 'No target found for method AddSubItem.'"

I have desperately tried many other solutions that were suggested on other posts but nothing has worked so far. I appreciate any insight. Below is my test code.

ShellViewModel:

using System;
using Caliburn.Micro;
using ContextMenuTest.Models;

namespace ContextMenuTest.ViewModels
{
    public class ShellViewModel : Screen
    {
        public static Random rnd = new Random();
        private BindableCollection<ItemModel> items = new BindableCollection<ItemModel>();
        public BindableCollection<ItemModel> Items
        {
            get { return items; }
            set
            {
                items = value;
                NotifyOfPropertyChange(() => Items);
            }
        }

        private ItemModel selectedItem = new ItemModel(rnd);
        public ItemModel SelectedItem
        {
            get { return selectedItem; }
            set
            {
                selectedItem = value;
                NotifyOfPropertyChange(() => SelectedItem);
            }
        }

        private SubItemModel selectedSubItem = new SubItemModel(rnd);
        public SubItemModel SelectedSubItem
        {
            get { return selectedSubItem; }
            set
            {
                selectedSubItem = value;
                NotifyOfPropertyChange(() => SelectedSubItem);
            }
        }

        public ShellViewModel()
        {
            for(int i = 0; i < 10; i++)
            {
                Items.Add(new ItemModel(rnd));
            }
        }

        public void AddItem()
        {
            Items.Add(new ItemModel(rnd));
        }

        public void RemoveItem()
        {
            Items.Remove(SelectedItem);
        }

        public void AddSubItem(object e)
        {
            var _item = e as ItemModel;
            _item.SubItems.Add(new SubItemModel(rnd));
        }

        public void RemoveSubItem(object e)
        {
            var _item = e as ItemModel;
            _item.SubItems.Remove(SelectedSubItem);
        }
    }
}

ShellView:

<Window x:Class="ContextMenuTest.Views.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ContextMenuTest.Views"
        xmlns:cal="http://www.caliburnproject.org"
        mc:Ignorable="d"
        Title="ShellView" Height="450" Width="800"
        >

    <Grid Grid.Row="1">
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <DataGrid x:Name="Items" Grid.Row="0" AutoGenerateColumns="False" CanUserResizeColumns="True" CanUserAddRows="false"
                        VerticalAlignment="Stretch" VerticalScrollBarVisibility="Auto" MinHeight="100" SelectedItem="{Binding SelectedItem}">
            <DataGrid.ContextMenu>
                <ContextMenu cal:Action.TargetWithoutContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
                    <MenuItem Header="Add Item" cal:Message.Attach="AddItem()"/>
                    <MenuItem Header="Remove Item" cal:Message.Attach="RemoveItem()"/>
                </ContextMenu>
            </DataGrid.ContextMenu>

            <DataGrid.Columns >
                <DataGridTextColumn Header="ID" Binding="{Binding Path=ID, UpdateSourceTrigger=PropertyChanged}" Width="*" IsReadOnly="True"/>
                <DataGridTextColumn Header="Name" Binding="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" Width="*"/>
            </DataGrid.Columns>

            <DataGrid.RowDetailsTemplate>
                <DataTemplate>
                    <DataGrid ItemsSource="{Binding Path=SubItems}"  AutoGenerateColumns="False" CanUserResizeColumns="True" CanUserAddRows="false"
                                VerticalAlignment="Stretch" VerticalScrollBarVisibility="Auto" MinHeight="100" 
                                  SelectedItem="{Binding Path=SelectedSubItem, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid}}">
                        <DataGrid.ContextMenu>
                            <ContextMenu cal:Action.TargetWithoutContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
                                <MenuItem Header="Add Sub Item" cal:Message.Attach="AddSubItem($dataContext)"/>
                                <MenuItem Header="Remove Sub Item" cal:Message.Attach="RemoveSubItem($dataContext)"/>
                            </ContextMenu>
                        </DataGrid.ContextMenu>
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="ID" Binding="{Binding Path=ID}" Width="auto" IsReadOnly="True"/>
                            <DataGridTextColumn Header="Name" Binding="{Binding Path=Name, UpdateSourceTrigger=PropertyChanged}" Width="auto"/>
                            <DataGridTextColumn Header="Value" Binding="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}" Width="auto"/>
                        </DataGrid.Columns>
                    </DataGrid>
                </DataTemplate>
            </DataGrid.RowDetailsTemplate>
        </DataGrid>
    </Grid>
</Window>

ItemModel:

using Caliburn.Micro;
using System;

namespace ContextMenuTest.Models
{
    public class ItemModel
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public BindableCollection<SubItemModel> SubItems { get; set; } = new BindableCollection<SubItemModel>();

        public ItemModel(Random rnd)
        {
            ID = Guid.NewGuid();
            Name = "Item " + ID.ToString().Substring(3, 5);

            for (int i = 0; i < 5; i++)
            {
                SubItems.Add(new SubItemModel(rnd));
            }
        }
    }
}

SubItemModel:

using System;

namespace ContextMenuTest.Models
{
    public class SubItemModel
    {
        public Guid ID { get; set; }
        public string Name { get; set; }
        public int Value { get; set; }

        public SubItemModel(Random rnd)
        {
            ID = Guid.NewGuid();
            Name = "SubItem " + ID.ToString().Substring(3,5);
            Value = rnd.Next();
        }
    }
}

Solution

  • Try using TagHelper. It works for your requirement.sample code is as below.

    <Grid Background="White" HorizontalAlignment="Stretch" Height="200" Name="GridLayout">
        <ListBox x:Name="ListBoxItems">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Grid Tag="{Binding DataContext, ElementName=GridLayout}">
                        <Grid.ContextMenu>
                            <ContextMenu Name="cm" cal:Action.TargetWithoutContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                                <MenuItem Header="Open"
                                          cal:Message.Attach="Open($dataContext)">
                                </MenuItem>
                            </ContextMenu>
                        </Grid.ContextMenu>
    
                        <TextBlock VerticalAlignment="Center">
                            .. text..
                        </TextBlock>
                    </Grid>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>