引言
在之前写的一篇文章【WPF — 如何以Binding方式隐藏DataGrid列】中,我先探索了 DataGridTextColumn
为什么不在可视化树结构内?又给出了解决方案,使用 Freezable
,该抽象类是 DependencyObject
的子类,能使用依赖属性在 Xaml
进行绑定,它承载了 DataContext
且有属性变化通知功能,触发 VisibilityConverter
转换器,实现了预期功能。
然后有群友问了这样一个问题:
这里有两个问题:
- 非可视化树中的元素不能通过
RelativeSource
或者ElementName
访问到可视化树中的数据,为何可以通过resource
的方式访问? Freezable
类为何能够中转数据,DependencyObject
不行?
那么本篇文章就来探索一下 Freezable
实现了上述功能的原理是什么?
原理探索
准备
我们还是使用上一篇文章中的示例,让后为了便于剖析源码,做了部分改动。
首先,准备自定义 Freezable
类:
public class CustomFreezable : Freezable
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(CustomFreezable));
public object Value
{
get => (object)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
protected override void OnChanged()
{
base.OnChanged();
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
}
protected override Freezable CreateInstanceCore()
{
return new CustomFreezable();
}
}
然后准备界面,但是这回跟之前不一样的是所有 DataGridTextColumn
列不在 XAML 中绑定,我们放在后台绑定:
<Window.Resources>
<local:VisibilityConverter x:Key="VisibilityConverter" />
<local:CustomFreezable x:Key="customFreezable" Value="{Binding IsVisibility, Converter={StaticResource VisibilityConverter}}" />
</Window.Resources>
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<DataGrid
x:Name="dataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Persons}"
SelectionMode="Single">
</DataGrid>
<CheckBox
Grid.Column="1"
Content="是否显示年龄列"
IsChecked="{Binding IsVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Grid>
然后准备 Code-Behind
代码,增加 InitDataGrid()
,手动绑定所有列。
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public MainWindow()
{
InitializeComponent();
Persons = new ObservableCollection<Person>() { new Person() { Age = 11, Name = "Peter" }, new Person() { Age = 19, Name = "Jack" } };
DataContext = this;
InitDataGrid();
}
private void InitDataGrid()
{
DataGridTextColumn columen1 = new DataGridTextColumn();
columen1.Header = "年龄";
columen1.Binding = new Binding("Age");
columen1.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
Binding binding = new Binding("Value");
binding.Source = FindResource("customFreezable");
BindingOperations.SetBinding(columen1, DataGridTextColumn.VisibilityProperty, binding);
dataGrid.Columns.Add(columen1);
DataGridTextColumn columen2 = new DataGridTextColumn();
columen2.Header = "姓名";
columen2.Binding = new Binding("Name");
columen2.Width = new DataGridLength(1, DataGridLengthUnitType.Star);
dataGrid.Columns.Add(columen2);
}
private bool isVisibility = true;
public bool IsVisibility
{
get => isVisibility;
set
{
isVisibility = value;
OnPropertyChanged(nameof(IsVisibility));
}
}
private ObservableCollection<Person> persons;
public ObservableCollection<Person> Persons
{
get { return persons; }
set { persons = value; OnPropertyChanged(); }
}
}
源码剖析
在源码剖析之前,如果大家还不会如何使用VS调试.Net源码,建议先阅读我的另一篇文章【编程技巧 — VS如何调试.Net源码】,学习如何调试源码。
接下来,在程序启动之前,我们在 CustomFreezable
的重载方法 OnChanged()
设置断点,然后使用VS调试源码,查看调用堆栈:
可以看到,从 InitDataGrid()
开始,到属性变化触发变化事件,整个流程都可以在调用堆栈中看到,我们可以逐帧分析,来解决开篇的两个问题。
剖析步骤
我们将上述调用链编号,逐步分析:
- 编号1:FindResource(…)
- 编号2:FrameworkElement.FindResourceInternal(…)
- 编号3:FindResourceInTree(…)
- 编号4:FetchResource(…)
- 编号5~6:GetValue(…),在这里已经获取到字典中资源了。
- 编号7~8 OnGettingValue(…)
- 编号9~10 AddInheritanceContext(…)
-
编号11~12 ProvideSelfAsInheritanceContext(…)
-
编号13 AddInheritanceContext(…)
后面的就不用看了,后面的就是因为 Freezable
更换了 InheritanceContext
触发了OnInheritanceContextChanged()
后又触发了 NotifyPropertyChange
。
接下来看看为什么当 IsVisibility
变化时,能通知到 Freezable
?
- NotifySubPropertyChange(…)
- FireChanged(…)
- GetChangeHandlersAndInvalidateSubProperties(…)
可以看到从1~9仅仅是 FindResource("customFreezable");
这一个方法所作的事情,主要是从资源字典中查询想要的对象,如果该对象是 Freezable
类型的,则将当前资源的 DataContent
的 Visual
绑定为 Freezable
的 InheritanceContext
,然后10~12,是该上下文在当前资源的 DataCobtent
触发 PropertyChanged
时,去InheritanceContext
中找出关联的 CallHandle
强制刷新,触发变化事件,达到联动效果。
那么从解析源码的过程中看,开篇的两个问题就都有了答案
-
非可视化树中的元素不能通过
RelativeSource
或者ElementName
访问到可视化树中的数据,为何可以通过resource
的方式访问?原因就是
FindResource
方法中,如果要查询的资源是Freezable
类型的,则会将当前资源的DataContent
的Visual
绑定到InheritanceContext
,所以Freezable
也就可以访问到可视化树中的数据了。 -
Freezable
类为何能够中转数据,DependencyObject
不行?从代码中,编号11~12 ProvideSelfAsInheritanceContext(…)也可以看出,绑定
InheritanceContext
时有一个必要条件就是该资源必须为Freezable
类型的才可以,我猜测这可能跟这个类的定义有关系,Freezable
类为WPF
中的对象提供了不可变性和性能优化的功能,同时也为动画、资源共享和跨线程安全性等方面提供了便利。 该类是更好地管理和优化 WPF 应用程序中的对象和资源的,所以可能不想让开发者随意使用吧,所以就仅提供该类能够拥有InheritanceContext
而没法使用DependencyObject
。
小结
Freezable
类除了上文示例中的用法,其实它这种间接绑定的方式可以解决很多场景,比如某个元素的属性并不是依赖属性,但是你就是想使用 Binding
的方式,让它动态变化,也可以使用上文示例的方式进行绑定。
好了,源码解析的过程其实还是比较复杂的,本文中其实也省略了一些源码阅读过程中细节,若大家阅读有疑问的地方,欢迎找我解疑,建议不明白的点,优先自行进行一下源码调试。
有错误之处,还请大家指正。