Search code examples
wpfxamldata-bindingattached-propertiesdatagridcolumnheader

Attached property in DataGridColumnHeader template not updating


What is my goal:

I want to create a custom template for a column header in DataGrid - DataGridColumnHeader. This template is supposed to fetch data from the attached property (in a real program, these will be properties such as: indicator whether the column is filtered, column title rotation angle, and so on)

Since DataGrid columns don't inherit DataContext I use method with BindingProxy class (description here: https://thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/).

Everything looks OK, but something is not right - if I use the attachet property in the DataGridColumnHeader template, notifications about changing this property do not work.

In the example below, I have bound the same property in different ways and everything works except for the DataGridColumnHeader template.

Does anyone know what I'm doing wrong and how to fix it?

MyApp

Reploduce problem:

The project is written in .Net Framework 4.7.1 To reproduce the problem, please create a new WPF project named "MyApp" for .Net Framework and add the following files to it:

File: MainWindow.xaml - window definition

<Window x:Class="MyApp.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:MyApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="950">

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>

            <!--Proxy to "transfer" DataContext to -->
            <local:BindingProxy x:Key="proxy" MyData="{Binding}" />
        </ResourceDictionary>
    </Window.Resources>
    
    
    
    <StackPanel Margin="10">

        <CheckBox IsChecked="{Binding SomeIndicator}" Content="- regular binding (without proxy object) - it works"/>
        
        <CheckBox IsChecked="{Binding MyData.SomeIndicator, Source={StaticResource proxy}}" Content="- binding via proxy object - it works"/>
        
        <CheckBox local:AttProp.MyAttProp="{Binding MyData.SomeIndicator, Source={StaticResource proxy}}"
                  Style="{StaticResource SimpleCheckBoxStyle}"/>


        <DataGrid ItemsSource="{Binding SomeData}" AutoGenerateColumns="False" Margin="0,20,0,20">
            <DataGrid.Columns>

                <DataGridTextColumn Binding="{Binding}">
                    <DataGridTextColumn.Header>
                        <CheckBox IsChecked="{Binding MyData.SomeIndicator, Source={StaticResource proxy}}"
                                  Content="- binding via proxy object (DataGrid header) - it works"/>
                    </DataGridTextColumn.Header>
                </DataGridTextColumn>

                <DataGridTextColumn Binding="{Binding}"
                                    HeaderStyle="{StaticResource SimpleHeaderStyle}"
                                    local:AttProp.MyAttProp="{Binding MyData.SomeIndicator, Source={StaticResource proxy}}"/>
            </DataGrid.Columns>
        </DataGrid>

        <WrapPanel HorizontalAlignment="Right">
            <Label Content="Use this button, please ===>" Foreground="Red"/>
            <Button Content="Toggle False/True" Command="{Binding ButtonClick}" Height="30" Width="150"/>
        </WrapPanel>
    </StackPanel>
</Window>

File Styles.xaml - extremely simplified styles

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:MyApp">

    
    <!--Custom style for DataGridColumnHeader-->
    <Style x:Key="SimpleHeaderStyle" TargetType="{x:Type DataGridColumnHeader}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
                    <CheckBox IsChecked="{TemplateBinding local:AttProp.MyAttProp}" Foreground="Red"
                              Content="- binding via attached property, proxy object and custom DataGridColumnHeader style - it doesn't work :( "
                              VerticalAlignment="Center"/>
                </ControlTemplate>
            </Setter.Value>

        </Setter>
    </Style>


    <!--Custom style for CheckBox-->
    <Style x:Key="SimpleCheckBoxStyle" TargetType="{x:Type CheckBox}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type CheckBox}">
                    <CheckBox IsChecked="{TemplateBinding local:AttProp.MyAttProp }"
                              Content="- binding via attached property, proxy object and custom CheckBox style - it works"/>
                </ControlTemplate>
            </Setter.Value>

        </Setter>
    </Style>
</ResourceDictionary>

File AttProp.cs - definition my attached property

using System.Windows;

namespace MyApp
{
    public class AttProp
    {
        //Create attached property
        public static readonly DependencyProperty MyAttPropProperty
            = DependencyProperty.RegisterAttached("MyAttProp", typeof(bool), typeof(AttProp), new PropertyMetadata(default(bool)));

        public static void SetMyAttProp(DependencyObject target, bool value)=>target.SetValue(MyAttPropProperty, value);
        public static bool GetMyAttProp(DependencyObject target) => (bool)target.GetValue(MyAttPropProperty);

    }
}

File BindingProxy.cs - definition of proxy class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;

namespace MyApp
{
    public class BindingProxy : Freezable
    {
        #region Overrides of Freezable

        protected override Freezable CreateInstanceCore()
        {
            return new BindingProxy();
        }

        #endregion

        public object MyData
        {
            get { return (object)GetValue(MyDataProperty); }
            set { SetValue(MyDataProperty, value); }
        }

        public static readonly DependencyProperty MyDataProperty =
            DependencyProperty.Register("MyData", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
    }
}

File ViewModel.cs - view model :)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace MyApp
{
    public class ViewModel : ICommand, INotifyPropertyChanged
    {
        #region Properties
        private bool _someIndicator = false;
        public bool SomeIndicator
        {
            get => _someIndicator;
            set
            {
                _someIndicator = value;
                OnPropertyChanged();
            }
        }

        public List<string> SomeData { get; set; } = new List<string>() { "AAA", "BBB", "CCC", "DDD" };

        public ICommand ButtonClick { get; set; }
        #endregion


        #region Constructor
        public ViewModel() => ButtonClick = this;
        #endregion


        #region INotifyPropertyChanged interface implementation
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged([CallerMemberName] String propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        #endregion


        #region ICommand interface implementation
        public event EventHandler CanExecuteChanged;
        public bool CanExecute(object parameter) => true;
        public void Execute(object parameter) => SomeIndicator =! SomeIndicator;
        #endregion
    }
}

I tried various ways of binding data in the template, but none of them brought the expected effect - changing the property does not affect the change of the checkbox in the header of the column to which the template is applied.


Solution

  • You are setting the attached property of the column itself but you bind to the DataGridColumnHeader in your Style.

    This should work:

    <Style x:Key="SimpleHeaderStyle" TargetType="{x:Type DataGridColumnHeader}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type DataGridColumnHeader}">
                    <CheckBox IsChecked="{Binding Column.(local:AttProp.MyAttProp), 
                                    RelativeSource={RelativeSource AncestorType=DataGridColumnHeader}}" Foreground="Red"
                                  Content="- binding via attached property, proxy object and custom DataGridColumnHeader style - it doesn't work :( "
                                  VerticalAlignment="Center"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>