在控件模板中,数据网格的依赖属性未被设置

4

我有一些自定义datagrid的需求,因此我创建了自己的datagrid扩展WPF datagrid。下面是一些相关的小代码 -

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

我在一个窗口中创建了它的实例,并将 SelectionMode 设置为 Single,这样做得非常好,属性会被设置为数据网格的 Single。到目前为止一切都很好。
但是如果我将我的 DataGrid 放置在 ControlTemplate 中,SelectionMode 就永远不会被设置为 Single。例如,如果我在 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代码中的某个bug吗?

注意 - 如果我在DataGrid构造函数中注释掉设置SelectionMode为Extended的那一行,它将可以正常工作。我知道这是默认值,并且有很多方法来设置默认值,但我想知道为什么它在一个情况下可以正常工作,在另一个情况下却不行。

3个回答

2
这是一个好问题,要回答它需要了解WPF引擎如何创建这两个DataGrid实例。
对于直接位于Window下的第一个DataGrid实例,在调用Window构造函数中的InitializeComponents()时创建。我不会深入介绍InitializeComponents的工作原理,但简单来说,它调用方法System.Windows.Application.LoadComponent()LoadComponent()加载位于传递的URI位置的XAML文件,并将其转换为由XAML文件根元素指定的对象实例。在这样做的同时,它首先调用要创建的元素的默认构造函数,然后再设置属性中提到的DependancyProperties
现在,您放置在ControlTemplate内部的第二个实例。当应用ControlTemplate于元素时,将创建该实例。如果您没有应用Template,则永远不会创建该实例。在应用Template时,将调用ControlTemplate.LoadContent()以创建ControlTemplate的根元素。现在,LoadContent()采用不同的方式创建在controlTemplate中定义的UIElements。它确实为每个元素调用默认构造函数,但是当它要设置DependancyProperties时,它会运行多个检查来决定属性值将是什么。简而言之,它检查是否已经在元素实例上设置了特定的DependancyProperty的任何值(即该值不是默认值,而是DependancyObject的ValueDictionary中实例的本地值条目),则不考虑在xaml中指定的值。因此,在这种情况下,当LoadComponents调用DataGrid的默认构造函数时,我们设置了SelectionModeProperty值。在加载内容时,ControlTemplate会检查它并返回相同的值,并忽略在xaml中指定的值。
这对所有控件都适用,不仅适用于DataGrid

“Extended”是默认值,为什么它仍然没有应用由“XAML”提供的值?你有任何参考(任何链接)来验证你的说法吗?我也相信这与ContentControl的加载行为有关,但我想知道这种行为的确切原因。如果您可以提供任何参考资料,那将是很好的。 - Rohit Vats
一旦您明确设置了DependancyProperty,它就具有本地值...默认值是指在注册依赖属性时指定的静态值。 - Nitin
非常有趣的阅读。如果您有更多的文档参考/链接,我也会感兴趣! - Sphinxxx
很抱歉,我没有这方面的文档...但是反射ControlTemplate.LoadContent()可以让你了解它是如何创建对象和设置属性的。 - Nitin
@nit,Sphinxxx - 最终我成功找到了与我的答案中提到的相同原因的确切原因 :) - Rohit Vats

1
抱歉,我无法回答您关于为什么使用ControlTemplate时它不起作用的问题,但是我可以为您提供更好的方法,在扩展类中设置继承属性的默认值,这可能会解决您的问题。
使用DependencyProperty.OverrideMetadata方法,可以为继承的DependencyProperty提供新的元数据和默认值。您可以使用static构造函数来设置SelectionMode属性的自定义默认值,如下所示:
static ExtendedDataGrid()
{
    SelectionModeProperty.OverrideMetadata(typeof(ExtendedDataGrid), 
        new FrameworkPropertyMetadata(2));
}

更新 >>>

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

我建议采用此替代方法来设置默认值,因为您说如果注释掉构造函数中设置SelectionMode属性默认值为SelectionMode.Extended的行,则问题会消失。我认为如果您使用这种方法来替换该行,则问题可能会消失。


首先,你的代码无法编译。此外,默认值本身就是Extended,因此根本不需要设置默认值。设置默认值不是问题所在。我想知道如果dataGrid不在ControlTemplate中,为什么它能够工作。行为应该是一致的。 - Rohit Vats
我已经知道你在这里发布的内容了。相反,应该使用默认值 DataGridSelectionMode.Extended,你使用了错误的枚举类型。另外,在问题中我提到过,我知道通过注释掉这一行可以解决问题,但我需要知道 ContentControl 完全忽略它的原因。 - Rohit Vats
我知道这段代码可以编译通过 - ExtendedDataGrid.SelectionModeProperty.OverrideMetadata(typeof(ExtendedDataGrid),new FrameworkPropertyMetadata(DataGridSelectionMode.Extended)); - Rohit Vats

1

在使用反编译工具Reflector挖掘PresentationFramework程序集中的代码后,我终于找到了这个问题的确切根本原因。正如nit所提到的那样,此行为适用于所有依赖属性和所有控件,而不仅仅是DataGrid。

这一切都与依赖属性值优先级有关,即在设置依赖属性时哪个值具有优先权。(该枚举值存储在WindowsBase.dll程序集的BaseValueSourceInternal中)

DependencyObject class 包含 UpdateEffectiveValue 方法,该方法负责通过在 DataGrid 实例上调用 SetValue 方法来设置任何 DP 的最终实际值。 UpdateEffectiveValue 方法在调用 DP 的 SetValue 方法之前包含许多逻辑。

有趣的检查正在阻止通过ControlTemplate设置它,这个检查(仅在新值优先级高于旧值优先级时才会设置DP的值,否则返回而不设置DP) -

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

在第一种情况下,即DataGrid是窗口的直接子元素时,DP属性设置步骤如下:

  1. WPF引擎从上到下读取BAML(编译后的XAML),一旦遇到DataGrid,就会创建一个实例。
  2. 从构造函数中设置选择模式DP时,DependencyObject类的SetValueCommon方法被调用。它将旧值和新值传递给UpdateEffectiveValue方法。
  3. 现在,通过SetValueCommon方法,旧值的BaseValueSourceInternal为Unknown,而新值的BaseValueSourceInternal被设置为Local。因此,它从上述if检查中传递并设置DP。
  4. 现在,在创建DataGrid实例之后,从BAML逐个读取与DataGrid相关联的所有属性,并在遇到每个DP时调用SetValueCommon方法。
  5. 由于SetValueCommon方法将新值设置为BaseValueSourceInternal.Local,而旧值已经是BaseValueSourceInternal.Local。所以优先顺序相同,因此单个值在DP上设置。

在第二种情况中,DataGrid被放置在ControlTemplate内部 -
  1. 在WPF引擎读取BAML时,如果DataGrid包含在ContentControl中,则DataGrid不会被创建。只有当ContentControl在GUI上呈现时才会创建它。此时,Framework的ApplyTemplate方法被调用,该方法调用LoadContent方法来加载模板。
  2. LoadContent内部调用了更多的方法,最终创建了一个DataGrid实例,并设置了DP,就像前面一样,使用BaseValueSourceInternal.Local设置当前值集。
  3. 现在,一旦dataGrid实例被创建,就会调用ApplyTemplatedParentValue方法,该方法尝试通过调用UpdateEffectiveValue方法设置其上找到的所有DP。
  4. DP上设置的当前值使用BaseValueSourceInternal.Local进行设置,但是它尝试设置的新值使用BaseValueSourceInternal.ParentTemplate进行设置。
  5. 因此,最终当它进入UpdateEffectiveValue方法时,由于ParentTemplate优先级低于Local,所以上述if条件失败。因此,SetValue永远不会在带有新值的DP上调用,这就是我们从构造函数中注释掉该行代码时正常工作的原因,因为旧值的BaseValueSourceInternal为Unknown,而新值的BaseValueSourceInternal为ParentTemplate。

DP 的优先级顺序 中所述,通过动画设置的属性比本地值更具有优先权。因此,如果我们通过动画在 ControlTemplate 中设置属性,它应该可以正常工作。我尝试了一下,它完全可以正常工作 -

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

1
感谢你抽出时间发布你的发现 :) - Sphinxxx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接