Last active
August 8, 2022 07:38
-
-
Save walterlv/4581ee10530a21ddf00f47b2cd680714 to your computer and use it in GitHub Desktop.
A UI container for async loading.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.ComponentModel; | |
using System.Threading.Tasks; | |
using System.Windows; | |
using System.Windows.Controls; | |
using System.Windows.Markup; | |
using System.Windows.Media; | |
using System.Windows.Threading; | |
using Walterlv.Demo; | |
using Walterlv.Demo.Utils.Threading; | |
using DispatcherDictionary = System.Collections.Concurrent.ConcurrentDictionary<System.Windows.Threading.Dispatcher, Walterlv.Demo.Utils.Threading.DispatcherAsyncOperation<System.Windows.Threading.Dispatcher>>; | |
namespace Walterlv.Windows | |
{ | |
[ContentProperty(nameof(Child))] | |
public class AsyncBox : FrameworkElement | |
{ | |
/// <summary> | |
/// 保存外部 UI 线程和与其关联的异步 UI 线程。 | |
/// 例如主 UI 线程对应一个 AsyncBox 专用的 UI 线程;外面可能有另一个 UI 线程,那么对应另一个 AsyncBox 专用的 UI 线程。 | |
/// </summary> | |
private static readonly DispatcherDictionary RelatedAsyncDispatchers = new DispatcherDictionary(); | |
private UIElement _child; | |
private readonly HostVisual _hostVisual; | |
private VisualTargetPresentationSource _targetSource; | |
private UIElement _loadingView; | |
private readonly ContentPresenter _contentPresenter; | |
private bool _isChildReadyToLoad; | |
private Type _loadingViewType; | |
public AsyncBox() | |
{ | |
_hostVisual = new HostVisual(); | |
_contentPresenter = new ContentPresenter(); | |
Loaded += OnLoaded; | |
} | |
public UIElement Child | |
{ | |
get => _child; | |
set | |
{ | |
if (Equals(_child, value)) return; | |
if (value != null) | |
{ | |
RemoveLogicalChild(value); | |
} | |
_child = value; | |
if (_isChildReadyToLoad) | |
{ | |
ActivateChild(); | |
} | |
} | |
} | |
public Type LoadingViewType | |
{ | |
get | |
{ | |
if (_loadingViewType == null) | |
{ | |
throw new InvalidOperationException( | |
$"在 {nameof(AsyncBox)} 显示之前,必须先为 {nameof(LoadingViewType)} 设置一个 {nameof(UIElement)} 作为 Loading 视图。"); | |
} | |
return _loadingViewType; | |
} | |
set | |
{ | |
if (value == null) | |
{ | |
throw new ArgumentNullException(nameof(LoadingViewType)); | |
} | |
if (_loadingViewType != null) | |
{ | |
throw new ArgumentException($"{nameof(LoadingViewType)} 只允许被设置一次。", nameof(value)); | |
} | |
_loadingViewType = value; | |
} | |
} | |
/// <summary> | |
/// 返回一个可等待的用于显示异步 UI 的后台 UI 线程调度器。 | |
/// </summary> | |
private DispatcherAsyncOperation<Dispatcher> GetAsyncDispatcherAsync() => RelatedAsyncDispatchers.GetOrAdd( | |
Dispatcher, dispatcher => UIDispatcher.RunNewAsync("AsyncBox")); | |
private UIElement CreateLoadingView() | |
{ | |
var instance = Activator.CreateInstance(LoadingViewType); | |
if (instance is UIElement element) | |
{ | |
return element; | |
} | |
throw new InvalidOperationException($"{LoadingViewType} 必须是 {nameof(UIElement)} 类型"); | |
} | |
private async void OnLoaded(object sender, RoutedEventArgs e) | |
{ | |
if (DesignerProperties.GetIsInDesignMode(this)) return; | |
var dispatcher = await GetAsyncDispatcherAsync(); | |
_loadingView = await dispatcher.InvokeAsync(() => | |
{ | |
var loadingView = CreateLoadingView(); | |
_targetSource = new VisualTargetPresentationSource(_hostVisual) | |
{ | |
RootVisual = loadingView | |
}; | |
return loadingView; | |
}); | |
AddVisualChild(_contentPresenter); | |
AddVisualChild(_hostVisual); | |
await LayoutAsync(); | |
await Dispatcher.Yield(DispatcherPriority.Background); | |
_isChildReadyToLoad = true; | |
ActivateChild(); | |
} | |
private void ActivateChild() | |
{ | |
var child = Child; | |
if (child != null) | |
{ | |
_contentPresenter.Content = child; | |
AddLogicalChild(child); | |
InvalidateMeasure(); | |
} | |
} | |
private async Task LayoutAsync() | |
{ | |
var dispatcher = await GetAsyncDispatcherAsync(); | |
await dispatcher.InvokeAsync(() => | |
{ | |
if (_loadingView != null) | |
{ | |
_loadingView.Measure(RenderSize); | |
_loadingView.Arrange(new Rect(RenderSize)); | |
} | |
}); | |
} | |
protected override int VisualChildrenCount => _loadingView != null ? 2 : 0; | |
protected override Visual GetVisualChild(int index) | |
{ | |
switch (index) | |
{ | |
case 0: | |
return _contentPresenter; | |
case 1: | |
return _hostVisual; | |
default: | |
return null; | |
} | |
} | |
protected override IEnumerator LogicalChildren | |
{ | |
get | |
{ | |
if (_isChildReadyToLoad) | |
{ | |
yield return _contentPresenter; | |
} | |
} | |
} | |
protected override Size MeasureOverride(Size availableSize) | |
{ | |
if (_isChildReadyToLoad) | |
{ | |
_contentPresenter.Measure(availableSize); | |
return _contentPresenter.DesiredSize; | |
} | |
var size = base.MeasureOverride(availableSize); | |
return size; | |
} | |
protected override Size ArrangeOverride(Size finalSize) | |
{ | |
if (_isChildReadyToLoad) | |
{ | |
_contentPresenter.Arrange(new Rect(finalSize)); | |
var renderSize = _contentPresenter.RenderSize; | |
LayoutAsync().ConfigureAwait(false); | |
return renderSize; | |
} | |
var size = base.ArrangeOverride(finalSize); | |
LayoutAsync().ConfigureAwait(false); | |
return size; | |
} | |
} | |
} |
是因为 AsyncBox 如果要写到 XAML 里面,要求卡只能卡在构造函数之后。如果构造函数卡了,就是卡在 XAML 上,救不回来的。解决方法有二:
- 将卡顿代码从构造函数移至后续(如 Loaded 事件中)
- 不在 XAML 中初始化,改用 C# 初始化
如果你试验成功,那么我更新下我的博客。
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@GlodenBoy AsyncBox 只是单纯为了缓解用户的等待焦虑加了个加载动画,本身并不会让 UI 响应(即 Window 是拖不动的)。如果等待期间 Loading 动画未出现,那么是 AsyncBox 的 bug。加我 QQ 450711383 我明天调查下。