15

I'm trying to bind column visibility to that of another element like this:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<StackPanel>
    <CheckBox x:Name="chkColumnVisible" Content="Show column" />
    <DataGrid x:Name="MyDataGrid" AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Column1" Visibility="{Binding ElementName=chkColumnVisible, Path=IsChecked, Converter={StaticResource BooleanToVisibilityConverter}}"/>
        </DataGrid.Columns>
    </DataGrid>
</StackPanel>

but I get this error in VS output:

System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=IsChecked; DataItem=null; target element is 'DataGridTextColumn' (HashCode=48860040); target property is 'Visibility' (type 'Visibility')

Is there a pure XAML way to accomplish this?

H.B.
  • 166,899
  • 29
  • 327
  • 400
Eren Ersönmez
  • 38,383
  • 7
  • 71
  • 92

3 Answers3

46

The columns of a DataGrid are abstract objects not appearing in the visual or logical tree. You cannot use ElementName and RelativeSource. Source in combination with x:Reference should work though:

Visibility="{Binding Source={x:Reference chkColumnVisible},
                     Path=IsChecked,
                     Converter={StaticResource BooleanToVisibilityConverter}}"
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • 1
    Thank you, it works! But so that I can sleep :)... I can definitely understand why RelativeSource wouldn't work, since it is relative to the target. But what on earth is the problem with ElementName?? I thought I was giving the binding an absolute source by using ElementName (and obviously I'm wrong!) so it wouldn't matter if the target is on the visual or logical tree or not. – Eren Ersönmez Jan 13 '12 at 08:38
  • @ErenErsonmez: `ElementName` uses the current namescope to resolve the name, and namescopes are dependant on the trees as far as i know. – H.B. Jan 13 '12 at 09:37
  • 3
    @ErenErsonmez: [MSDN](http://msdn.microsoft.com/en-us/library/system.windows.data.binding.elementname.aspx): `You can refer to elements in code only if they are registered to the appropriate NameScope through RegisterName. For more information, see `[`WPF XAML Namescopes`](http://msdn.microsoft.com/en-us/library/ms746659.aspx) -> `The names in a XAML namescope can be used to establish relationships between the XAML-defined names of objects and their instance equivalents in an object tree.` – H.B. Jan 13 '12 at 09:44
  • Thank you! I knew there had to be a much better answer other than using a proxy element. – Vash Apr 28 '21 at 13:43
15

I wrote a markupextension for it:

using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
using System.Xaml;

/// <summary>
/// Binds to the datacontext of the current root object or ElementName
/// </summary>
[MarkupExtensionReturnType(typeof(object))]
public class NinjaBinding : MarkupExtension
{
    private static readonly DependencyObject DependencyObject = new DependencyObject();
    private static readonly string[] DoNotCopy = { "Path", "Source", "ElementName", "RelativeSource", "ValidationRules" };
    private static readonly PropertyInfo[] CopyProperties = typeof(Binding).GetProperties().Where(x => !DoNotCopy.Contains(x.Name)).ToArray();
    public NinjaBinding()
    {
    }

    public NinjaBinding(Binding binding)
    {
        Binding = binding;
    }

    public Binding Binding { get; set; }

    private bool IsInDesignMode
    {
        get { return DesignerProperties.GetIsInDesignMode(DependencyObject); }
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        if (Binding == null)
        {
            throw new ArgumentException("Binding == null");
        }
        if (IsInDesignMode)
        {
            return DefaultValue(serviceProvider);
        }
        Binding binding = null;
        if (Binding.ElementName != null)
        {
            var reference = new Reference(Binding.ElementName);
            var source = reference.ProvideValue(serviceProvider);
            if (source == null)
            {
                throw new ArgumentException("Could not resolve element");
            }
            binding = CreateElementNameBinding(Binding, source);
        }
        else if (Binding.RelativeSource !=null)
        {
            throw new ArgumentException("RelativeSource not supported");
        }
        else
        {
            var rootObjectProvider = (IRootObjectProvider)serviceProvider.GetService(typeof(IRootObjectProvider));
            if (rootObjectProvider == null)
            {
                throw new ArgumentException("rootObjectProvider == null");
            }
            binding = CreateDataContextBinding((FrameworkElement) rootObjectProvider.RootObject, Binding);
        }

        var provideValue = binding.ProvideValue(serviceProvider);
        return provideValue;
    }

    private static Binding CreateElementNameBinding(Binding original, object source)
    {
        var binding = new Binding()
         {
             Path = original.Path,
             Source = source,
         };
        SyncProperties(original, binding);
        return binding;
    }

    private static Binding CreateDataContextBinding(FrameworkElement rootObject, Binding original)
    {
        string path = string.Format("{0}.{1}", FrameworkElement.DataContextProperty.Name, original.Path.Path);
        var binding = new Binding(path)
         {
             Source = rootObject,
         };
        SyncProperties(original, binding);
        return binding;
    }

    private static void SyncProperties(Binding source, Binding target)
    {
        foreach (var copyProperty in CopyProperties)
        {
            var value = copyProperty.GetValue(source);
            copyProperty.SetValue(target, value);
        }
        foreach (var rule in source.ValidationRules)
        {
            target.ValidationRules.Add(rule);
        }
    }

    private static object DefaultValue(IServiceProvider serviceProvider)
    {
        var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
        if (provideValueTarget == null)
        {
            throw new ArgumentException("provideValueTarget == null");
        }
        var dependencyProperty = (DependencyProperty)provideValueTarget.TargetProperty;
        return dependencyProperty.DefaultMetadata.DefaultValue;
    }
}

It enables binding to the DataContext of the current root object {Window, UserControl, ...}

Sample usage (Visible & Visibility are properties of the ViewModel):

<DataGrid>
    <DataGrid.Columns>
        <DataGridTextColumn Header="DataContext" Visibility="{dataGridBox:NinjaBinding Binding={Binding Visibility}}" />
        <DataGridTextColumn Header="Converter" Visibility="{dataGridBox:NinjaBinding Binding={Binding Visible, Converter={StaticResource BooleanToVisibilityConverter}}}" />
        <DataGridTextColumn Header="ElementName" Visibility="{dataGridBox:NinjaBinding Binding={Binding IsChecked, ElementName=CheckBox, Converter={StaticResource BooleanToVisibilityConverter}}}" />
    </DataGrid.Columns>
</DataGrid>
Johan Larsson
  • 17,112
  • 9
  • 74
  • 88
  • 2
    Does not work for me for some reason... `BindingExpression path error: 'ValidationColumnVisibility' property not found on 'object' ''NameFixupToken' (HashCode=55620207)'. BindingExpression:Path=ValidationColumnVisibility; DataItem='NameFixupToken' (HashCode=55620207); target element is 'DataGridTextColumn' (HashCode=62066456); target property is 'Visibility' (type 'Visibility')` – Bartosz Feb 01 '16 at 16:24
  • Worked perfecly with a DataTemplateColumn and DataGrid in my application. – RHaguiuda Sep 21 '18 at 11:54
1

The solution from Johan Larsson works perfectly, only the FallbackValue from the Binding isn't forwarded, so I've changed it like this:

private object DefaultValue(IServiceProvider serviceProvider)
{
    if (Binding.FallbackValue != null)
        return Binding.FallbackValue;

    var provideValueTarget = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
    if (provideValueTarget == null)
    {
        throw new ArgumentException("provideValueTarget == null");
    }
    var dependencyProperty = (DependencyProperty)provideValueTarget.TargetProperty;
    return dependencyProperty.DefaultMetadata.DefaultValue;
}

So it could be used like this, here for example for Binding to a Header:

<DataGridTemplateColumn Header="{dataGridBox:NinjaBinding Binding={Binding MyHeaderName1, FallbackValue=HeadingNr1}}" />
Coden
  • 2,579
  • 1
  • 18
  • 25