來翻新老專案 (3) - MapleLooker - TreeView 資料綁定
這次主軸是將 TreeView 的資料節點做資料綁定
原本是打算對播放器元件做一些重構,但調整 Slider 時碰到問題暫時沒法解決
所以就改重構 TreeView 的部份了
TreeView 如何處理資料綁定?
相較於一般的控制項如:CheckBox、RadioButton、TextBlock 等等,都是直接綁定 Text
或 IsChecked
等簡單屬性
而 TextList、TreeView 等等有關集合的物件,則通常會透過 ItemsSource
來管理物件集合
這類使用物件集合的物件有幾個對 UI 渲染非常重要的屬性:ItemTemplate
、ItemContainerStyle
、ItemContainerGenerator
等等
ItemsSource
原先使用 TreeView 時,都是自己手動建立 TreeViewItem (在 WinForm 則是 TreeNode),並手動將資料指定到 Tag
屬性方便使用
到了 Wpf 中,則可以使用 ItemsSource
直接管理資料集合,並在觸發事件時直接使用資料而非節點
ItemsSource 除了要求實做 IEnumerable
外,並無其他限制,可放入任何資料
ItemContainerGenerator
TreeView 會透過 ItemContainerGenerator 為每一個資料生成對應的節點控制項,也就是 TreeViewItem
ItemTemplate
ItemTemplate 主要負責節點控制項的客製化
先前 ItemContainerGenerator 已經幫忙為資料生成一個外殼 TreeViewItem
TreeViewItem 裡面則有一個 Header
屬性支援客製化模板,為每個資料建立自定義 UI 並依照資料進行綁定
ItemTemplate 會影響 Header
的呈現方式
若沒有什麼特殊需求,TreeView 的 ItemTemplate
使用基本的 TextBlock 來呈現節點資料即可
ItemContainerStyle
如果要對 ItemContainer 的數值進行綁定或額外設定,則需要設定 ItemContainerStyle
ItemContainerStyle 允許設定 ItemContainer 的各項配置,包含一般的屬性以及事件
藉由 ItemContainerStyle 可以在自訂節點上綁定 IsExpanded
、IsSelected
等屬性,並綁定 TreeViewItem 進行控制
若使用的控制項原本就有設定 ItemContainerStyle,設定 Style 之後會導致原本的配置被覆蓋
這時候就要在設定 Style 時特別設定 BaseOn
,基於原本的 Style 配置進行進一步的改動
開始修改 TreeView
了解 TreeView 有哪些東西,接著就是開始將原本手動建立 Item 的部份改為使用資料綁定與 Template
準備節點物件
首先是節點物件
Wpf 的 TreeView 和 WinForm 的 TreeView 運作上有很大的不同
最初 WinForm 的 TreeView 在任何跟 TreeNode 有關的事件、是否展開都是直接在 TreeView 設定
但是 Wpf 的事件與是否展開皆在 TreeViewItem 設定
這導致最初從 WinForm 轉移到 Wpf 之後,TreeView 的節點在讀取時會全部展開,造成 UI 一次載入大量節點造成卡頓
為了避免節點全部展開,我需要對節點展開的狀態進行控制
因此,需要建立自訂的節點類別,在連結資料的情況下,同時提供綁定在 TreeViewItem 上的屬性:
public class WzTreeNode : INotifyPropertyChanged |
上面的 INotifyPropertyChanged 是一個介面,要求類別提供一個 PropertyChanged
事件用於通知 UI 資料發生變化
任何 INotifyPropertyChanged 的物件都必須在 UI 執行緒呼叫 PropertyChanged
若是需要透過其他執行緒對這些物件進行改動,建議是透過 UI 的 Dispatcher 使用 Invoke 回到 UI 執行緒操作物件
或是將 PropertyChanged
事件拉到 UI 執行緒執行,而物件本體則可以不受限制的修改
ObservableCollection 是一個實作 INotifyPropertyChanged 的集合
對集合本身的操作如 Add
、Remove
、Clear
等都會通知 UI
由於 ObservableCollection 實作 INotifyPropertyChanged,所以必須在 UI 執行緒進行操作
修改 xaml
ItemsSource 一樣直接綁定到 MainForm 本身,建立一個 RootNodes
用於綁定
由於使用 ObservableCollection,因此不用對 MainForm 實作 INotifyPropertyChanged
public partial class MainForm : WpfUi.FluentWindow |
接著在 TreeView 上綁定 ItemsSource
為 RootNodes
並且設定 Style 跟 Template:
<TreeView |
上面的 HierarchicalDataTemplate 主要是給 TreeView 使用,用於未知深度的樹節點
若是 TextList 之類單純列表形式的控制項,只需要使用 DataTemplate 即可
與 DataTemplate 的不同之處在於對多綁了一次 Children
到 ItemsSource
上
Style 的部份有另外使用到 BaseOn
如同前面所說,設定 Style 會把原先預設的 Style 全部蓋掉
因此,透過 BaseOn
基於原本的 Style 做修改,才能避免蓋掉原本 Wpf.UI 設定的樣式
樣板的部份因為沒有特殊需求,所以只使用最簡單的 TextBlock
設定 ContextMenu
最早 WinForm 時期的 ContextMenu 是藉由 MouseClick
的右鍵點擊事件建立並顯示 ContextMenu
而到 Wpf 這邊,目前我還找不到好的方式處理這一塊
如果使用滑鼠點擊事件,我可以透過 sender
變數取得 TreeViewItem
但是超出第一層的 TreeViewItem 就沒辦法顯示選單
也嘗試過直接用 Style 裡面透過 Setter 設定 ContextMenu
但是一切到沒有選單的區塊點右鍵後再切回來, Wpf.UI 的樣式就會跑掉
最後折衷的作法,就是在自訂 Node 上設定 ContextMenu ,並綁定到 TextBlock 上
缺點是,只有在文字區塊點擊右鍵才會跳出選單
事件重構
因應前面的一些改動,因此一些事件的觸發都要做一些相對應的改動
同時也將輸入做一些調整以方便實做 ICommand
介面做綁定
這邊主要以關閉節點的事件做說明:
void Wz_Close() { |
重構之後變成這樣:
void CloseWzNode(WzTreeNode node) { |
主要的差異在於,事件本身已經是以資料為主,所以不用再透過 TreeViewItem 的 Tag
取得資料了
整個方法已經聚焦在 WzTreeNode 上,使得前面的巢狀 if 通通可以拔掉
再來是使用 is
判斷變數的同時,可以同時宣告變數
透過這個語法糖,就少了後續宣告變數的部份,程式也變得更加簡潔
最後透過 ?.
關鍵字,若節點本身並沒有資料,則不會執行 Dispose()
就少了一些強制轉型跟 null 的判斷
ViewBox 的部份後面會說明,這邊的程式已經是調整之後,也不需要手動傳遞數值到自訂控制項了
修改呈現物件資訊的自訂控制項 - ViewBox
ViewBox 主要是為了呈現 WZ 資料而建立的自訂控制項
原本都是手動將物件傳遞到控制項來更新控制項資訊
現在則要透過 xaml 的方式直接綁定 TreeView 的 SelectedItem
來呈現目前選擇的節點資訊
DependencyProperty
為了支援直接從 xaml 綁定屬性的功能,需要新增 DependencyProperty
有關相依性屬性的部份可以參考官方文件
這塊我還沒有深入研究,目前就是從網路上找資源跟著做而已 🙂
下面是針對 CurrentWzObject
屬性的配置:
public partial class WzViewBox : UserControl |
針對屬性 CurrentWzObject
,需要設定一樣名稱的 Property CurrentWzObjectProperty
然後在 CurrentWzObject
的 Getter 跟 Setter 都設定從自己身上取得對應 Property 的資料
這邊另外設定了 OnChanged 事件用於更新內部元件的資料
在 MainForm.xaml 中進行綁定
完成了 DependencyProperty 的設定後
再來就可以直接在 xaml 上使用屬性了
這邊直接對 CurrentWzObject
綁定 TreeView 的 SelectedItem.Value
:
<comp:WzViewBox |
最終成果
後記
原本是要先重構舊的程式
但看了看 MainForm 的程式,發現大多跟 UI 相關,其實非 UI 邏輯的程式量並不多
中途本來想先改 SoundPlayer 這個子控制項,但是調整聲音位置的部份一直沒辦法調好,所以就跑去改寫 TreeView
原本我在 ItemTemplate 設定 TreeViewItem,而且還忘記把 IsEnabled 設回 true,導致中途試的時候一直打不開 + 位置錯誤
折騰了好一陣子才終於把項目調整好,然後又碰到 Wpf.UI 的樣式被吃掉,找了好久才知道 BaseOn
這個屬性
幸好最後修改結果還算成功
ContextMenu 目前還沒有好的修改方式
試過好幾種辦法,都沒辦法有效的讓動態生成的選單適當的顯示出來
最後只好綁定到 TextBlock 上先讓選單正常運作
目前看起來效果還不錯,而且還順便把以前為了避免 TreeView 載入卡頓而設定的 Thread 給砍掉
整個 UI 的程式開始變得精簡,是好的開始 🙂