Search code examples
c#wpftreeviewcontextmenucaliburn.micro

Caliburn.micro get treeview's selected item by contextmenu onclick


I have a task to use a contextmenu in treeview and pass selected treeview's item to ViewModel by clicking on contextmenu element.

Here is my xaml:

<Window.Resources>
  <HierarchicalDataTemplate x:Key="Ufps"
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Id}" />
            <TextBlock Margin="5 0 0 0" Text="{Binding Name}" />
        </StackPanel>
  </HierarchicalDataTemplate>
</Window.Resources>
........
........
<TreeView x:Name="TrvUfpsDictionary" Height="222" Canvas.Left="25" 
  Canvas.Top="280" Width="545"
  Background="AliceBlue" 
  ItemsSource="{Binding Path=Ufps, Mode=OneWay}" 
  ItemTemplate="{StaticResource Ufps}">
        <TreeView.ContextMenu>
          <ContextMenu>
            <MenuItem Header="Add Element"
             cal:Message.Attach="[Event Click] = [Action AddElement(TrvUfpsDictionary.SelectedItem)]"
                />
                ................
            </ContextMenu>
        </TreeView.ContextMenu>           
    </TreeView>
........
<Button Content="Test" Canvas.Left="475" Canvas.Top="568" Width="75"
 cal:Message.Attach="[Event Click] = [Action AddElement(TrvUfpsDictionary.SelectedItem)]"/>

And here is simple ViewModel's code:

  public class UserSettingsViewModel : PropertyChangedBase
  {
   ..........

   public void AddElement(object selectedItem)
    {         
       MessageBox.Show("Element added! "+selectedItem.?GetHashCode());            
    }
   ..........
  }

Now I've stuck with it. When I've selected treeview's item and then I've pressed the "Test" button - it works fine, it pass the selected item to "AddElement" in my VM. BUT when I do the same with contextmenu - it always pass null. Did I miss something?

EDIT I've made a simple app with the problem described. https://github.com/whizzzkey/WpfApp1


Solution

  • You might have to move the Context Menu further into the TreeView, into the Item Template and add Context Menu to the Label/TextBlock you have in nodes.

    For example, consider the following Employee tree (emulating since I do not know your data structure),

    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Positions}" >
            <Label Content="{Binding DepartmentName}"/>
            <HierarchicalDataTemplate.ItemTemplate>
                <HierarchicalDataTemplate ItemsSource="{Binding Employees}" >
                    <Label Content="{Binding PositionName}"
                        Tag="{Binding DataContext, ElementName=TestControl}" >
                        <Label.ContextMenu>
                            <ContextMenu
                                cal:Action.TargetWithoutContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                                <MenuItem Header="Add Element"
                                    cal:Message.Attach="[Event Click] = [Action AddElement($datacontext)]"/>
                            </ContextMenu>
                        </Label.ContextMenu>
                    </Label>
                    <HierarchicalDataTemplate.ItemTemplate>
                        <DataTemplate>
                            <Label Content="{Binding EmployeeName}"
                                Tag="{Binding DataContext, ElementName=TestControl}">
                                <Label.ContextMenu>
                                    <ContextMenu
                                        cal:Action.TargetWithoutContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                                        <MenuItem Header="Add Element"
                                            cal:Message.Attach="[Event Click] = [Action AddElement($datacontext)]" />
                                    </ContextMenu>
                                </Label.ContextMenu>
                            </Label>
                        </DataTemplate>
                    </HierarchicalDataTemplate.ItemTemplate>
                </HierarchicalDataTemplate>
            </HierarchicalDataTemplate.ItemTemplate>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
    

    There are couple of important points to note here. Since your method exists in ViewModel and you have to ensure that the DataContext is pointing to your ViewModel instead of the Item Type that is bound to node.

    For this, you need can make use of cal:Action.TargetWithoutContext. The following line the Label definition ensure we have access to the View's DataContext.

    Tag="{Binding DataContext, ElementName=TestControl}" 
    

    While the following line ensures the we get our bindings right (to ViewModel). TestControl is the x:Name for your UserControl

    cal:Action.TargetWithoutContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}"
    

    Finally the Click Action would be modified as following.

    cal:Message.Attach="[Event Click] = [Action AddElement($datacontext)]"
    

    This would ensure your ViewModel's Action is called with the right parameter passed.

    Update

    Based on your comment and code,following are the changes required.

    Window Definition : Add x:Name

    <Window
        x:Class="WpfApp1.Views.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="http://www.caliburnproject.org"
        Title="XmlData Tree Test"
        x:Name="TestControl"
        Width="250"
        Height="350">
    

    Root Hierarchical Template Associating Item source with Tag is placed on the TextBlock, also the Relative Source has Self.

    <HierarchicalDataTemplate DataType="root" ItemsSource="{Binding XPath=./*}" >
        <StackPanel Orientation="Horizontal">
            <TextBlock Margin="0" Text="ROOT"
                Tag="{Binding DataContext, ElementName=TestControl}">
                <TextBlock.ContextMenu>
                    <ContextMenu
                    cal:Action.TargetWithoutContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Header="Add Element"
                            cal:Message.Attach="[Event Click] = [Action AddElement($datacontext)]" />
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
        </StackPanel>
    </HierarchicalDataTemplate>
    

    Hierarchical Template for Node

    <HierarchicalDataTemplate
        DataType="Node"
        ItemsSource="{Binding XPath=./*}">
        <StackPanel Orientation="Horizontal">
            <TextBlock Margin="0" Text="Node:" />
            <TextBlock Margin="5,0,0,0"
                Tag="{Binding DataContext, ElementName=TestControl}"
                Text="{Binding XPath=@name}" >
                <TextBlock.ContextMenu>
                    <ContextMenu
                        cal:Action.TargetWithoutContext="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Header="Add Element"
                            cal:Message.Attach="[Event Click] = [Action AddElement($datacontext)]" />
                    </ContextMenu>
                </TextBlock.ContextMenu>
            </TextBlock>
        </StackPanel>
    </HierarchicalDataTemplate>
    

    Output Example, For Root

    enter image description here

    For Node, enter image description here