WPF 的 UI 邏輯只在同一個線程中,這是學習 WPF 開發中你們幾乎都會學習到的經驗。若是但願作不一樣線程的 UI,你們也會想到使用另外一個窗口來實現,讓每一個窗口擁有本身的 UI 線程。然而,就不能讓同一個窗口內部使用多個 UI 線程嗎?git
答案實際上是——能夠的!使用 VisualTarget
便可。github
閱讀本文將收穫一份對 VisualTarget
的解讀以及一份我封裝好的跨線程 UI 控件 DispatcherContainer.cs。編程
微軟給 VisualTarget
提供的註釋是:多線程
提供跨線程邊界將一個可視化樹鏈接到另外一個可視化樹的功能。異步
註釋中說 VisualTarget
就是用來鏈接可視化樹(VisualTree
)的,並且能夠跨線程邊界。也就是說,這是一個專門用來使同一個窗口內部包含多個不一樣 UI 線程的類型。ide
因此,咱們的目標是使用 VisualTarget
顯示跨線程邊界的 UI。函數
VisualTarget
自己繼承自 CompositionTarget
,而不是 Visual
;其自己並非可視化樹的一部分。可是它的構造函數中能夠傳入一個 HostVisual
對象,這個對象是一個 Visual
,若是將此 HostVisual
放入原 UI 線程的可視化樹上,那麼 VisualTarget
就與主 UI 線程鏈接起來了。學習
另一半,VisualTarget
須要鏈接另外一個異步線程的可視化樹。然而,VisualTarget
提供了 RootVisual
屬性,直接給此屬性賦一個後臺 UI 控件做爲其值,即鏈接了另外一個 UI 線程的可視化樹。測試
總結起來,其實咱們只須要 new
一個 VisualTarget
的新實例,構造函數傳入一個 UI 線程的可視化樹中的 HostVisual
實例,RootVisual
屬性設置爲另外一個 UI 線程中的控件,便可完成跨線程可視化樹的鏈接。優化
事實上通過嘗試,咱們真的只須要這樣作就可讓另外一個線程上的 UI 呈現到當前的窗口上,同一個窗口。讀者能夠自行編寫測試代碼驗證這一點,我並不打算在這裏貼上試驗代碼,由於後面會給出完整可用的所有代碼。
雖然說 VisualTarget
的基本使用已經能夠顯示一個跨線程的 UI 了,可是其實功能仍是欠缺的。
一個典型的狀況是,後臺線程的這部分 UI 沒有鏈接到 PresentationSource
;而 Visual.PointFromScreen
、Visual.PointFromScreen
這樣的方法明確須要鏈接到 PresentationSource
才行。參見這裏:In WPF, under what circumstances does Visual.PointFromScreen throw InvalidOperationException? - Stack Overflow。
但是,應該如何將 RootVisual
鏈接到 PresentationSource
呢?我從 Microsoft.DwayneNeed 項目中找到了方法。這是源碼地址:Microsoft.DwayneNeed - Home。
作法是重寫屬性和方法:
public override Visual RootVisual
{
get => _visualTarget.RootVisual;
set
{
// 此處省略大量代碼。
}
}
protected override CompositionTarget GetCompositionTargetCore()
{
return _visualTarget;
}
Microsoft.DwayneNeed
中有 VisualTargetPresentationSource
的完整代碼,我本身只爲這個類添加了 IDisposable
接口,用於 Dispose
掉 VisualTarget
的實例。我須要這麼作是由於我即將提供可修改後臺 UI 線程控件的方法。
爲了讓整個多線程 UI 線程的使用行雲流水,我準備寫一個 DispatcherContainer
類來優化多線程 UI 的使用體驗。指望的使用方法是給這個控件的實例設置 Child
屬性,這個 Child
是後臺線程建立的 UI。而後一切線程同步相關的工做所有交給此類來完成。
在我整理後,使用此控件只需如此簡單:
<Grid Background="#FFEEEEEE">
<local:DispatcherContainer x:Name="Host"/>
</Grid>
await Host.SetChildAsync<MyUserControl>();
其中,MyUserControl
是控件的類型,能夠是你寫的某個 XAML 用戶控件,也能夠是其餘任何控件類型。用這個方法建立的控件,直接就是後臺 UI 線程的。
固然,若是你須要本身控制初始化邏輯,可使用委託建立控件。
await Host.SetChildAsync(() => { var box = new TextBox { Text = "呂毅 - walterlv", Margin = new Thickness(16), }; return box; });
下圖便是用以上代碼建立的後臺線程文本框。
甚至,你已經有線程的後臺 UI 控件了,或者你但願本身來建立後臺的 UI 控件,則能夠這樣:
// 建立一個後臺線程的 Dispatcher。
// 其中,UIDispatcher 是我本身封裝的方法,在 GitHub 上以 MIT 協議開源。
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Demo.WPF/Utils/Threading/UIDispatcher.cs
var dispatcher = await UIDispatcher.RunNewAsync("walterlv's testing thread");
// 使用這個後臺線程的 Dispatcher 建立一個本身的用戶控件。
var control = await dispatcher.InvokeAsync(() => new MyUserControl());
// 將這個用戶控件放入封裝好的 DispatcherContainer 中。
// DispatcherContainer 是我本身封裝的方法,在 GitHub 上以 MIT 協議開源。
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Demo.WPF/Utils/Threading/DispatcherContainer.cs
await Host.SetChildAsync(control);
注意到咱們本身建立的控件已經運行在後臺線程中了:
如下全部代碼都可點擊進入 GitHub 查看。
核心的代碼包含兩個類:
PresentationSource
的功能。(因爲我對 PresentationSource 的瞭解有限,此類絕大多數代碼都直接來源於 Microsoft.DwayneNeed - Home。)其餘輔助型代碼:
Dispatcher
。UIDispatcher
中的方法更好寫一些。DispatcherAsyncOperation
繼承的接口,可是不繼承也沒事,同樣能跑。這些輔助型代碼的含義能夠查看個人另外一篇博客:如何實現一個能夠用 await 異步等待的 Awaiter - walterlv的專欄 - CSDN博客。