Freezable ---探索WPF中Freezable承载数据的原理
引言
在之前寫的一篇文章【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 的方式,讓它動態變化,也可以使用上文示例的方式進行綁定。
好了,源碼解析的過程其實還是比較復雜的,本文中其實也省略了一些源碼閱讀過程中細節,若大家閱讀有疑問的地方,歡迎找我解疑,建議不明白的點,優先自行進行一下源碼調試。
總結
以上是生活随笔為你收集整理的Freezable ---探索WPF中Freezable承载数据的原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 构建健康游戏环境:DFA算法在敏感词过滤
- 下一篇: 高二能出国留学吗?400分也能上名校,你