2
votes

I am trying to define a new column template for a datagrid that I can reuse across my application, but when I try and use it, I get:

System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=CanLogin; DataItem=null; target element is 'DataGridBetterCheckBoxColumn' (HashCode=56040243); target property is 'isChecked' (type 'Object')

XAML for Column:

<DataGridTemplateColumn x:Class="BACSFileGenerator.UserControls.DataGridBetterCheckBoxColumn"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:BACSFileGenerator.UserControls"
             mc:Ignorable="d" 
             x:Name="ColumnRoot"
             >
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <CheckBox IsChecked="{Binding isChecked, Source={x:Reference Name=ColumnRoot}}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Code Behind:

using System.Windows;
using System.Windows.Controls;

namespace BACSFileGenerator.UserControls
{

    public partial class DataGridBetterCheckBoxColumn : DataGridTemplateColumn
    {

        public object isChecked
        {
            get { return (object)GetValue(isCheckedProperty); }
            set { SetValue(isCheckedProperty, value); }
        }

        public static readonly DependencyProperty isCheckedProperty =
            DependencyProperty.Register("isChecked", typeof(object),
              typeof(DataGridBetterCheckBoxColumn), new PropertyMetadata(null));

        public DataGridBetterCheckBoxColumn()
        {
            InitializeComponent();
        }
    }
}

I am then trying to use it like this:

<DataGrid Margin="0,0,0,10" ItemsSource="{Binding UserAccessGrid}" CanUserAddRows="False" CanUserDeleteRows="False" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="User" Binding="{Binding User}" IsReadOnly="True"/>
                <uc:DataGridBetterCheckBoxColumn Header="Login" isChecked="{Binding CanLogin}"/>
                <uc:DataGridBetterCheckBoxColumn Header="Export Payments" isChecked="{Binding canExportPayments}"/>
                <uc:DataGridBetterCheckBoxColumn Header="Create File Layouts" isChecked="{Binding canCreateFileLayouts}"/>
                <uc:DataGridBetterCheckBoxColumn Header="Change User Access" isChecked="{Binding canChangeUserAccess}"/>
            </DataGrid.Columns>
</DataGrid>

Can anyone explain to me the proper way to do this?

1

1 Answers

2
votes

Lets say we have

public class ViewModel
{
   public bool CanBeUsed {get;set;}
   public List<Employee> Employees{get;set;}
}

Few points which might have confused you :

  1. There will be only one DataGridBetterCheckBoxColumn instantiated for a property. Multiple records doesn't mean multiple column instance for a property. Instead multiple DataGridCell are created for every DataGridColumn.

    But

    DataGridColumn is not a FrameworkElement or a Visual so, it won't appear in the VisualTree, and since it is not FrameworkElement so it doesn't have a DataContext property. Without DataContext how your Binding will work ? Ask your self. Since this Column cant have its DataContext set, so it must have either a ElementName, or a Source or a RelativeSource for its Binding to work.

    Now, we know that there will be only one instance of a DataGridColumn, so naturally its Binding should (made to) use the DataContext (collection property will be part of this) of DataGrid.

    Now, see your Binding, where is its Source / RelativeSource ? There isnt any. Now, will RelativeSource make any sense here ? As DataGridColumn does not appear in the VisualTree, so RelativeSource wont apply here. We are left with Source property. What we should set now for the Source ? Enter DataContext Inheritance .

    DataContext Inheritance

    DataContext inheritance will only work for FrameworkElement connected via VisualTree. So, we need a mechanism with which we can bring down this DataContext to our DataGridColumn. Enter Binding Proxy.

    public class BindingProxy : Freezable
    {
        #region Overrides of Freezable
    
        protected override Freezable CreateInstanceCore()
        {
             return new BindingProxy();
        }
    
        #endregion
    
        public object Data
        {
           get { return (object)GetValue(DataProperty); }
           set { SetValue(DataProperty, value); }
        }
    
        // Using a DependencyProperty as the backing store for Data.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
     }
    

    If we declare an instance of this BindingProxy as a Resource, we can get our Source.

    <DataGrid Margin="0,52,0,10" ItemsSource="{Binding Records}" CanUserAddRows="False" CanUserDeleteRows="False" AutoGenerateColumns="False">
       <DataGrid.Resources>
         <uc:BindingProxy x:Key="FE" Data="{Binding}"/>
       </DataGrid.Resources>
       <DataGrid.Columns>
          <DataGridTextColumn x:Name="dgt" Header="User" Binding="{Binding User}" IsReadOnly="True"/>
          <uc:DataGridBetterCheckBoxColumn isChecked="{Binding Data.CanBeUsed, Source={StaticResource FE}}" Header="CanLogin"/>
       </DataGrid.Columns>
    </DataGrid>
    

    Now, you will see that your nasty Binding Error is gone.

  2. To make your CheckBox binding to work properly, you need to handle its Loaded event.

      <DataGridTemplateColumn.CellTemplate>
         <DataTemplate>
           <CheckBox Loaded="CheckBox_Loaded"/>
         </DataTemplate>      
      </DataGridTemplateColumn.CellTemplate>
    

    Code :

      void CheckBox_Loaded(object sender, RoutedEventArgs e)
      {
          Binding b = new Binding();
          b.Path = new PropertyPath("isChecked");
          b.Mode = BindingMode.TwoWay;
          b.Source = this;
    
          CheckBox cb = sender as CheckBox;
    
          BindingOperations.SetBinding(cb , CheckBox.IsCheckedProperty, b);
      }
    

    But now, we have one logical problem here. All our CheckBox are now bounded to the DataContext property CanBeUsed which will remain same. You might be thinking that CanBeUsed should be a property of Employee which is ItemsSource and not DataContext of DataGrid. So, when you check / uncheck any CheckBox, all will respond same.

But, we want to bind our isChecked property to some property of Employee record which will remain diff for every DataGridRow. So, we need to now change our definition of isChecked, after which entire code will look like below :

public partial class DataGridBetterCheckBoxColumn : DataGridTemplateColumn
{
    public BindingBase isChecked { get; set; }

    public DataGridBetterCheckBoxColumn()
    {
        InitializeComponent();
    }

    void CheckBox_Loaded(object sender, RoutedEventArgs e)
    {          
        CheckBox cb = sender as CheckBox;

        BindingOperations.SetBinding(cb , CheckBox.IsCheckedProperty, isChecked);
    }
} 

Usage :

<uc:DataGridBetterCheckBoxColumn isChecked="{Binding CanLogin, Mode=TwoWay}" Header="CanLogin"/>

If I missed any point, do let me know.