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博客。