4

我有一些自定义数据网格的要求,所以我创建了自己的扩展 WPF 数据网格的数据网格。下面贴出相关的小代码——

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

我在一个窗口中创建它的实例并设置SelectionModeSingle它工作得很好,并且属性被设置Single为dataGrid。到目前为止一切都很好。

但如果我将我的 DataGrid 放在 ControlTemplate 中,SelectionMode则永远不会设置为Single. SelectionMode 只是例如,如果我在 DataGrid 的构造函数中明确设置该值,则不会通过 XAML 设置 DP。

复制问题的小样本在这里 -

 <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>

对于第二个 DataGrid,它工作正常,但不适用于放置在 ControlTemplate 内的 DataGrid。为什么会有这种奇怪的行为?它是 DataGrid 代码中的一些错误吗?

注意- 如果我在 DataGrid 构造函数中注释我明确将 SelectionMode 设置为 Extended 的行,它将正常工作。我知道这是默认值,删除后它在两种情况下都可以正常工作(也有很多方法可以设置默认值),但我想知道为什么它在一种情况下有效,而在另一种情况下无效。

4

3 回答 3

2

这是一个很好的问题,要回答它需要深入了解 WPF 引擎是如何创建这两个DataGrid.

对于 that 的第一个实例DataGrid是您的直接子级Window,该实例是在调用InitializeComponents()的构造函数时创建的Window。我不会深入了解它是如何InitializeComponents工作的,但只是一个 jist,它调用方法System.Windows.Application.LoadComponent()LoadComponent()加载XAML位于传入的文件,URI并将其转换为由XAML文件的根元素指定的对象的实例. 这样做时,它首先调用要创建的元素的默认构造函数,然后再次设置DependancyProperties属性中提到的。

现在,您放置在ControlTemplate. 该实例将在何时ControlTemplate应用于元素时创建。如果您不应用Template,则永远不会创建实例。在应用 时TemplateControlTemplate.LoadContent()会调用 来创建 的根元素ControlTemplate。现在采用不同的 LoadContent()粗略方法来创建. 它确实为每个元素调用了默认构造函数,但是在设置 时,它会运行多次检查以确定属性值是什么。简而言之,它检查是否已经为元素实例上的特定值设置了任何值(即,该值不是默认值,它是该实例的 ValueDictionary 的本地值条目UIElementscontrolTemplateDependancyPropertiesDependancyPropertyDependancyObject) 它不考虑 xaml 中指定的值。因此,在这种情况下,当LoadComponents调用 DataGrid 的默认构造函数时,我们设置了该SelectionModeProperty值。加载内容时,ControlTemplate检查它并返回相同的值并忽略 xaml 中指定的值。

这不仅适用于所有控件DataGrid

于 2013-10-25T03:18:55.310 回答
1

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>
于 2013-10-25T18:21:49.960 回答
1

很抱歉,我无法回答您关于为什么在ControlTemplate使用问题。

DependencyProperty可以使用该DependencyProperty.OverrideMetadata方法为继承的新元数据提供默认值。SelectionMode您可以使用如下构造函数为属性设置自己的默认值static

static ExtendedDataGrid()
{
    SelectionModeProperty.OverrideMetadata(typeof(ExtendedDataGrid), 
        new FrameworkPropertyMetadata(2));
}

更新>>>

如果将SelectionMode枚举替换为表示所需值的整数,则代码将编译。我只使用了该SelectionMode.Extended值(现在替换为它的整数值 - 2),因为这就是在示例中使用的。

SelectionMode我建议使用这种设置默认值的替代方法,因为您说如果您在构造函数中将属性的默认值设置为的行注释掉,您的问题就会消失SelectionMode.Extended。我认为如果你用它来替换那条线,那么你的问题可能会消失。

于 2013-10-24T20:03:41.517 回答