Thursday, May 28, 2015

Binding properties of DataGridColumns

For WPF 4.0 or later

Similar to my earlier blog entry on binding the ItemsSource of a DataGridComboBoxColumn, it is also difficult to bind other properties of all flavors of DataGridColumns. I recently wanted to be able to disable a DataGridColumn when a property was set in the window's DataContext. I was using MVVM at the time but this solution works more generically.

I Googled the problem and found several possible solutions, but the one that caught my eye was to create an invisible ContentControl and create a static resource that referenced it. You set the DataContext of the FrameworkElement to the View-Model class (or the code behind) and then reference the static resource in the dependency object's binding. Once you have defined the Framework Element and the StaticResource once on a page, you can use it in multiple places on that page.

There are other techniques that solve the problem solution wide, but they require a lot more up-front work.

I'll offer my solution using a property in the code-behind. MVVM programmers will know how to tweak it for their needs.

Consider the following window that has a data grid with one column. There is also a CheckBox that can be checked to put the grid's Name column into read-only mode. Here is the XAML and code behind.

<Window x:Class="BindingDataGridColumn.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"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <CheckBox Content="Is ReadOnly?" IsChecked="{Binding IsReadOnly}"></CheckBox>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Names}" Margin="0,40,0,0">
        <DataGrid.Columns>
            <DataGridTextColumn Header="Name" Binding="{Binding Path=.}" IsReadOnly="{Binding IsReadOnly}"></DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
    </Grid>
</Window>
----------------------------------------------------------------------------
using System;
using System.Windows;
using System.Collections.ObjectModel;
using System.ComponentModel;

namespace BindingDataGridColumn
{
    public partial class MainWindow : Window, INotifyPropertyChanged
            {
        public ObservableCollection<String> Names { get; set; }

        private Boolean _IsReadOnly = false;
        public Boolean IsReadOnly
        {
            get { return _IsReadOnly; }
            set 
            {
                if (value != _IsReadOnly)
                {
                    _IsReadOnly = value;
                    NotifyPropertyChanged("IsReadOnly");
                }
            }
        }

        public MainWindow()
        {
            Names = new ObservableCollection<String> { "Bob", "Mike" };
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String PropertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
        }
    }
}


When we run this application we see we can modify the names even if we check the Is Readonly CheckBox. Also, when we look at the Output window we see the familiar message...

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

To solve this problem we need to do three things...
    1. Add a resource to the Window that has the same data context as the window...
      <FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
    2. Add a ContentControl and make it invisible. Its content will be the FrameworkElement we just defined, so its data context will be the same as the window...
      <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
    3. Use the new ProxyElement as the source in the DataGridColumn's binding. You can re-use the ProxyElement on all DataGridColumns that need it.
      Source={StaticResource ProxyElement}
So now the XAML looks like this. Note there are no code changes required...

<Window x:Class="BindingDataGridColumn.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"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
    </Window.Resources>
    <Grid>
        <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/>
        <CheckBox Content="Is ReadOnly?" IsChecked="{Binding Path=IsReadOnly}"></CheckBox>
    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding Names}" Margin="0,40,0,0">
        <DataGrid.Columns>
                <DataGridTextColumn Header="Name" Binding="{Binding Path=.}" IsReadOnly="{Binding Path=DataContext.IsReadOnly, Source={StaticResource ProxyElement}}"></DataGridTextColumn>
        </DataGrid.Columns>
    </DataGrid>
    </Grid>
</Window>

Note we had to change the Binding Path because the Framework Element does not have an IsReadOnly property, but its DataContext does.

No comments:

Post a Comment