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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
是因为 AsyncBox 如果要写到 XAML 里面,要求卡只能卡在构造函数之后。如果构造函数卡了,就是卡在 XAML 上,救不回来的。解决方法有二:
如果你试验成功,那么我更新下我的博客。