0

I have a tree view nicely connected to my model with a MVVM pattern. I'm running into an issue with the context menu. I do not know how to bind to an element outside the tree. I have created a sample that demonstrates what I'm trying. I hope someone explain to me what I'm doing wrong.

In the sample I have five TextBlocks in the header, each binding differently to the data. And I have a context menu with five entries, again binding differently. The commentary says which binding works and which doesn't. There's even a difference between the header and the context menu.

Some bindings try to bind to another element in the UI another to a property in the view model.

Here's the XAML:

<Window x:Class="WpfApp7_TreeView.MainWindow"
    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:WpfApp7_TreeView"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800"
    x:Name="root">
<Window.DataContext>
    <local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Text="{Binding SomeProperty}" /> <!-- ok -->
    <TextBlock Grid.Row="1" Text="Text from XAML" x:Name="tb" /> <!-- ok -->

    <TreeView Grid.Row="2" HorizontalAlignment="Stretch" ItemsSource="{Binding Departments}">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate DataType="{x:Type local:Department}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Margin="5" Text="{Binding DepartmentName }" />   <!-- ok -->
                    <TextBlock Margin="5" Text="{Binding ElementName=tb, Path=Text}"/>   <!-- ok -->
                    <TextBlock Margin="5" Text="{Binding SomeProperty}"/>  <!-- no show -->
                    <TextBlock Margin="5" Text="{Binding ElementName=root, Path=DataContext.SomeProperty}"/> <!-- ok -->
                    <TextBlock Margin="5" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window},
                        Path=DataContext.SomeProperty}"/>  <!-- ok -->
                    <StackPanel.ContextMenu>
                        <ContextMenu>
                            <MenuItem Header="Some command" /> <!-- ok -->
                            <MenuItem Header="{Binding ElementName=tb, Path=Text}" />   <!-- no show -->
                            <MenuItem Header="{Binding SomeProperty}" />    <!-- no show -->
                            <MenuItem Header="{Binding ElementName=root, Path=DataContext.SomeProperty}"/>  <!-- no show -->
                            <MenuItem Header="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window},
                                Path=DataContext.SomeProperty}" />   <!-- no show -->
                        </ContextMenu>
                    </StackPanel.ContextMenu>
                </StackPanel>
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Grid>

And here's the view model:

    using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace WpfApp7_TreeView
{
    public class MainWindowViewModel : ViewModelBase
    {
        private string someProperty = "Text from the viewmodel";
        public string SomeProperty
        {
            get { return someProperty; }
            set { someProperty = value; OnPropertyChanged("SomeProperty"); }
        }

        public MainWindowViewModel()
        {
            Departments = new List<Department>()
            {
                new Department("Department 1"),
                new Department("Department 2")
            };
        }

        private List<Department> departments;
        public List<Department> Departments
        {
            get {return departments;  }
            set { departments = value; OnPropertyChanged("Departments"); }
        }
    }

    public class Department : ViewModelBase
    {
        public Department(string depname)
        {
            DepartmentName = depname;
        }
        public string DepartmentName { get; set; }
    }

    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propname)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propname));
            }
        }
    }
}
Johannes Schacht
  • 930
  • 1
  • 11
  • 28
  • You cannot generally refer to elements or visual ancestors in a `ContextMenu` because it resides in its own element tree. What property do you want to bind to? – mm8 Jun 23 '20 at 15:14
  • there is no Positions in the VM. and whatever it is it probably doesn't have a SomeProperty – Bizhan Jun 23 '20 at 15:30
  • think a menu just a completely different window, having (almost) no knowledge of the owner. I personally find much more comfortable handling the context menu at the code side: intercept the right-click on the element, then open the context menu manually. In this way is straightforward to set its data context, hence the bindings. – Mario Vernari Jun 23 '20 at 15:36
  • Thank you Bizhan. "Positions" was an artefact, those were the items of the Department. I removed it no, does not have impact on the behavior. – Johannes Schacht Jun 23 '20 at 16:36
  • Hi mm8, I want to bind so something comparable to "SomeProperty". – Johannes Schacht Jun 23 '20 at 16:37

1 Answers1

1

TreeViewItem DataContexts get set to their list item, so the SomeProperty binding in the parent HierarchicalDataTemplate needs to use a RelativeSource binding instead. Change this:

<TextBlock Margin="5" Text="{Binding SomeProperty}"/>  <!-- no show -->

... to this:

<TextBlock Margin="5" Text="{Binding DataContext.SomeProperty, RelativeSource={RelativeSource AncestorType=TreeView}}"/>

With respect to your ContextMenu, you are correct in noting that bindings have to be part of the visual tree. The solution to this is to bind via an intermediate Binding Proxy instead. Generally speaking you would bind to the parents DataContext, rather than directly to another control, but it can be done both ways:

<TreeView Grid.Row="2" HorizontalAlignment="Stretch" ItemsSource="{Binding Departments}">
    <TreeView.Resources>
        <local:BindingProxy x:Key="TextBlockProxy" Data="{Binding Text, ElementName=tb}" />
        <local:BindingProxy x:Key="MainViewModelProxy" Data="{Binding}" />
    </TreeView.Resources>
    <TreeView.ItemTemplate>
    ...etc...

And then in the ContextMenu:

<!-- Binds to the TextBlock's Text property -->
<MenuItem Header="{Binding Data, Source={StaticResource TextBlockProxy}}" />

<!-- Binds to the main view model's SomeProperty -->
<MenuItem Header="{Binding Data.SomeProperty, Source={StaticResource MainViewModelProxy}}" />
Mark Feldman
  • 15,731
  • 3
  • 31
  • 58
  • Now I found a similar answer: https://stackoverflow.com/questions/7660967/wpf-error-cannot-find-governing-frameworkelement-for-target-element- that even doesn't need a BindingProxy class. Just uses a FrameworkElement. – Johannes Schacht Jun 23 '20 at 20:55
  • That's actually the same thing, just using a control as the binding proxy instead (which may or may not always be suitable). You also have to be very careful putting framework elements in resource dictionaries, particularly in cases where the binding might be referenced more than once, because resources generally only get created once. Specifying `x:Shared="False"` will usually get around this, but in practice I've found the use of a dedicated binding proxy to cause far fewer issues in the long run. – Mark Feldman Jun 23 '20 at 21:04