Search code examples
c#wpfxamldatagridwpfdatagrid

Dependency property not getting set for datagrid in control template


I have some requirement to customize datagrid, so i create my own dataGrid extending WPF datagrid. Small relevant code posted below -

public class ExtendedDataGrid : DataGrid
{
    public ExtendedDataGrid()
    {
        this.SelectionMode = DataGridSelectionMode.Extended;
    }
}

I create its instance in a window and set SelectionMode to Single which works perfectly fine and property gets set to Single for the dataGrid. So far all good.

But in case i place my DataGrid in ControlTemplate, SelectionMode never gets set to Single. SelectionMode is just for example no DP is getting set via XAML if i explicitly set that value in constructor of DataGrid.

Small sample replicating the problem is here -

 <Grid>
    <Grid.Resources>
        <ControlTemplate x:Key="MyTemplate">
            <local:ExtendedDataGrid ItemsSource="{Binding Collection,
                                                RelativeSource={RelativeSource   
                                                  Mode=FindAncestor, 
                                                  AncestorType=Window}}"
                                    SelectionMode="Single">
                <local:ExtendedDataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding}"/>
                </local:ExtendedDataGrid.Columns>
            </local:ExtendedDataGrid>
        </ControlTemplate>
    </Grid.Resources>
    <ContentControl Template="{StaticResource MyTemplate}"/>
    <local:ExtendedDataGrid ItemsSource="{Binding Collection,
                                          RelativeSource={RelativeSource 
                                          Mode=FindAncestor,
                                          AncestorType=Window}}"
                            Grid.Row="1" SelectionMode="Single">
        <local:ExtendedDataGrid.Columns>
            <DataGridTextColumn Binding="{Binding}"/>
        </local:ExtendedDataGrid.Columns>
    </local:ExtendedDataGrid>
</Grid>

For second DataGrid its working fine but not working for DataGrid placed inside ControlTemplate. Why this weird behaviour? Is it some bug in DataGrid code?

Note - It will work fine if i comment the line from DataGrid constructor where i am explicitly setting SelectionMode to Extended. I know that's default value and after removing that it will work fine for both cases (also there are many ways to set default value) but i want to know why it works in one case and not in other.


Solution

  • Finally after bit of digging code in PresentationFramework assembly using reflector, i was able to find out the exact RCA for this issue. As mentioned by nit, this behaviour is valid for all DP's and for all Controls not just DataGrid.

    This is all to do with Dependency Property Value Precedence that which value is given precedence over other while setting DP's. (Enum is BaseValueSourceInternal which store this precedence order of DP's in WindowsBase.dll assembly)

    DependencyObject class contains method UpdateEffectiveValue which is responsible for setting final actual value on any DP by calling SetValue method on DataGrid instance. UpdateEffectiveValue method contains lot of logic before actually calling SetValue method on DP.

    Interesting check which is stopping from setting it via ControlTemplate is this one (It checks if new value precedence order is higher than old value precedence order only in that case value will be set on DP otherwise return without setting DP) -

    if ((newEntry.BaseValueSourceInternal != BaseValueSourceInternal.Unknown)
        && (newEntry.BaseValueSourceInternal < oldEntry.BaseValueSourceInternal))
    {
        return (UpdateResult) 0;
    }
    

    In first case where dataGrid is direct child of window, DP property is set by these steps -

    1. WPF engine read BAML(compiled XAML) from top to bottom and once it encounter DataGrid, creates an instance of it.
    2. From constructor when we set selection mode DP, SetValueCommon method of DependencyObject class gets called. It pass on old value and new value to method UpdateEffectiveValue.
    3. Now, old value's BaseValueSourceInternal is Unkown and new value's BaseValueSourceInternal is set as Local by SetValueCommon method. Hence, it gets passed from the if check mentioned above and DP gets set.
    4. Now, after creating a DataGrid instance, all attributes associated with DataGrid are read from BAML one by one and SetValueCommon method is called on every DP encountered.
    5. Since, SetValueCommon method's set new value with BaseValueSourceInternal.Local and old value is already BaseValueSourceInternal.Local. So precedence order is same, that's why Single value gets set on DP.

    In second case where DataGrid is placed inside ControlTemplate -

    1. DataGrid is not created when WPF engine is reading BAML since it is contained inside a ContentControl. It will be created only when ContentControl is rendered on GUI. Framework's ApplyTemplate gets called which calls LoadContent method to load the template.
    2. LoadContent internally calls much more methods which at last create an instance of DataGrid and set DP like in previous case which set current value set with BaseValueSourceInternal.Local.
    3. Now, once dataGrid instance is created ApplyTemplatedParentValue method gets called which tries to set all the DP's found on it by calling method UpdateEffectiveValue.
    4. Current value set on DP is set with BaseValueSourceInternal.Local but new value which it tries to set is set as BaseValueSourceInternal.ParentTemplate.
    5. So, finally when it goes to method UpdateEffectiveValue, the if condition mentioned above fails since ParentTemplate precedence order is lower than Local. Hence, SetValue never gets called on DP with new value which is a reason than when we comment the line from constructor it's working fine since old value's BaseValueSourceInternal is Unknown and new value's BaseValueSourceInternal is ParentTemplate.

    As mentioned in the DP's precedence order that property set via animation holds more preference than Local value. So, ideally if we set the property in ControlTemplate via animation, it should work fine. I tried it out and it works completely fine -

    <ControlTemplate x:Key="MyTemplate">
        <local:ExtendedDataGrid ItemsSource="{Binding Collection,
                                             RelativeSource={RelativeSource
                                           Mode=FindAncestor, AncestorType=Window}}">
             <local:ExtendedDataGrid.Columns>
                 <DataGridTextColumn Binding="{Binding}"/>
             </local:ExtendedDataGrid.Columns>
             <local:ExtendedDataGrid.Triggers>
                 <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                     <BeginStoryboard>
                         <Storyboard>
                            <ObjectAnimationUsingKeyFrames 
                                 Storyboard.TargetProperty="SelectionMode">
                                <DiscreteObjectKeyFrame KeyTime="00:00:00"
                                  Value="{x:Static DataGridSelectionMode.Single}"/>
                            </ObjectAnimationUsingKeyFrames>
                         </Storyboard>
                     </BeginStoryboard>
                 </EventTrigger>
              </local:ExtendedDataGrid.Triggers>
        </local:ExtendedDataGrid>
    </ControlTemplate>