Freezable ---探索WPF中Freezable承载数据的原理
引言
在之前寫的一篇文章【W(wǎng)PF --- 如何以Binding方式隱藏DataGrid列】中,我先探索了 DataGridTextColumn 為什么不在可視化樹(shù)結(jié)構(gòu)內(nèi)?又給出了解決方案,使用 Freezable ,該抽象類是 DependencyObject 的子類,能使用依賴屬性在 Xaml 進(jìn)行綁定,它承載了 DataContext 且有屬性變化通知功能,觸發(fā) VisibilityConverter轉(zhuǎn)換器,實(shí)現(xiàn)了預(yù)期功能。
然后有群友問(wèn)了這樣一個(gè)問(wèn)題:
這里有兩個(gè)問(wèn)題:
- 非可視化樹(shù)中的元素不能通過(guò)
RelativeSource或者ElementName訪問(wèn)到可視化樹(shù)中的數(shù)據(jù),為何可以通過(guò)resource的方式訪問(wèn)? -
Freezable類為何能夠中轉(zhuǎn)數(shù)據(jù),DependencyObject不行?
那么本篇文章就來(lái)探索一下 Freezable實(shí)現(xiàn)了上述功能的原理是什么?
原理探索
準(zhǔn)備
我們還是使用上一篇文章中的示例,讓后為了便于剖析源碼,做了部分改動(dòng)。
首先,準(zhǔn)備自定義 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();
}
}
然后準(zhǔn)備界面,但是這回跟之前不一樣的是所有 DataGridTextColumn 列不在 XAML 中綁定,我們放在后臺(tái)綁定:
<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>
然后準(zhǔn)備 Code-Behind 代碼,增加 InitDataGrid() ,手動(dòng)綁定所有列。
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(); }
}
}
源碼剖析
在源碼剖析之前,如果大家還不會(huì)如何使用VS調(diào)試.Net源碼,建議先閱讀我的另一篇文章【編程技巧 --- VS如何調(diào)試.Net源碼】,學(xué)習(xí)如何調(diào)試源碼。
接下來(lái),在程序啟動(dòng)之前,我們?cè)?CustomFreezable 的重載方法 OnChanged() 設(shè)置斷點(diǎn),然后使用VS調(diào)試源碼,查看調(diào)用堆棧:
可以看到,從 InitDataGrid() 開(kāi)始,到屬性變化觸發(fā)變化事件,整個(gè)流程都可以在調(diào)用堆棧中看到,我們可以逐幀分析,來(lái)解決開(kāi)篇的兩個(gè)問(wèn)題。
剖析步驟
我們將上述調(diào)用鏈編號(hào),逐步分析:
- 編號(hào)1:FindResource(...)
- 編號(hào)2:FrameworkElement.FindResourceInternal(...)
- 編號(hào)3:FindResourceInTree(...)
- 編號(hào)4:FetchResource(...)
- 編號(hào)5~6:GetValue(...),在這里已經(jīng)獲取到字典中資源了。
- 編號(hào)7~8 OnGettingValue(...)
- 編號(hào)9~10 AddInheritanceContext(...)
-
編號(hào)11~12 ProvideSelfAsInheritanceContext(...)
-
編號(hào)13 AddInheritanceContext(...)
后面的就不用看了,后面的就是因?yàn)?Freezable 更換了 InheritanceContext 觸發(fā)了OnInheritanceContextChanged()后又觸發(fā)了 NotifyPropertyChange。
接下來(lái)看看為什么當(dāng) IsVisibility 變化時(shí),能通知到 Freezable?
- NotifySubPropertyChange(...)
- FireChanged(...)
- GetChangeHandlersAndInvalidateSubProperties(...)
可以看到從1~9僅僅是 FindResource("customFreezable"); 這一個(gè)方法所作的事情,主要是從資源字典中查詢想要的對(duì)象,如果該對(duì)象是 Freezable類型的,則將當(dāng)前資源的 DataContent的 Visual 綁定為 Freezable的 InheritanceContext ,然后10~12,是該上下文在當(dāng)前資源的 DataCobtent 觸發(fā) PropertyChanged時(shí),去InheritanceContext 中找出關(guān)聯(lián)的 CallHandle 強(qiáng)制刷新,觸發(fā)變化事件,達(dá)到聯(lián)動(dòng)效果。
那么從解析源碼的過(guò)程中看,開(kāi)篇的兩個(gè)問(wèn)題就都有了答案
-
非可視化樹(shù)中的元素不能通過(guò)
RelativeSource或者ElementName訪問(wèn)到可視化樹(shù)中的數(shù)據(jù),為何可以通過(guò)resource的方式訪問(wèn)?原因就是
FindResource方法中,如果要查詢的資源是Freezable類型的,則會(huì)將當(dāng)前資源的DataContent的Visual綁定到InheritanceContext,所以Freezable也就可以訪問(wèn)到可視化樹(shù)中的數(shù)據(jù)了。 -
Freezable類為何能夠中轉(zhuǎn)數(shù)據(jù),DependencyObject不行?從代碼中,編號(hào)11~12 ProvideSelfAsInheritanceContext(...)也可以看出,綁定
InheritanceContext時(shí)有一個(gè)必要條件就是該資源必須為Freezable類型的才可以,我猜測(cè)這可能跟這個(gè)類的定義有關(guān)系,Freezable類為WPF中的對(duì)象提供了不可變性和性能優(yōu)化的功能,同時(shí)也為動(dòng)畫、資源共享和跨線程安全性等方面提供了便利。 該類是更好地管理和優(yōu)化 WPF 應(yīng)用程序中的對(duì)象和資源的,所以可能不想讓開(kāi)發(fā)者隨意使用吧,所以就僅提供該類能夠擁有InheritanceContext而沒(méi)法使用DependencyObject。
小結(jié)
Freezable 類除了上文示例中的用法,其實(shí)它這種間接綁定的方式可以解決很多場(chǎng)景,比如某個(gè)元素的屬性并不是依賴屬性,但是你就是想使用 Binding 的方式,讓它動(dòng)態(tài)變化,也可以使用上文示例的方式進(jìn)行綁定。
好了,源碼解析的過(guò)程其實(shí)還是比較復(fù)雜的,本文中其實(shí)也省略了一些源碼閱讀過(guò)程中細(xì)節(jié),若大家閱讀有疑問(wèn)的地方,歡迎找我解疑,建議不明白的點(diǎn),優(yōu)先自行進(jìn)行一下源碼調(diào)試。
總結(jié)
以上是生活随笔為你收集整理的Freezable ---探索WPF中Freezable承载数据的原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 构建健康游戏环境:DFA算法在敏感词过滤
- 下一篇: 记一次 .NET某收银软件 非托管泄露分