Skip to content

Instantly share code, notes, and snippets.

@walterlv
Last active August 8, 2022 07:38
Show Gist options
  • Save walterlv/4581ee10530a21ddf00f47b2cd680714 to your computer and use it in GitHub Desktop.
Save walterlv/4581ee10530a21ddf00f47b2cd680714 to your computer and use it in GitHub Desktop.
A UI container for async loading.
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;
}
}
}
@walterlv
Copy link
Author

@GlodenBoy 怀疑 UI 层级不对。是否新 UI 的层级在 LoadingView 的上面?

@GlodenBoy
Copy link

GlodenBoy commented Oct 21, 2020

@GlodenBoy 怀疑 UI 层级不对。是否新 UI 的层级在 LoadingView 的上面?

不太像层级不对,因为这段时间 UI也是会卡住的,无法拖动窗口,以下是我的逻辑:

A 是window,B,C 是2个 UserControl页面,

A 其中一个区域是承载B的,用Prism 框架的 RequestNavigate进行区域切换到C,

C的组件比较多,所以我AsyncBox 放在了C 包裹了整个C

image

从B点击后秒切到C,之后卡UI 无法移动window,2秒之后 UI全部加载出来,并且包括loadingView(为了好识别我改成了红色的圈)

========================

补充,另外并不是每次都这样,大概运行10次左右 有1次红圈会先出来,很奇怪。。不过能力有限,目前没有吃透老哥的代码,无法查出具体原因。。

@walterlv
Copy link
Author

@GlodenBoy AsyncBox 只是单纯为了缓解用户的等待焦虑加了个加载动画,本身并不会让 UI 响应(即 Window 是拖不动的)。如果等待期间 Loading 动画未出现,那么是 AsyncBox 的 bug。加我 QQ 450711383 我明天调查下。

@walterlv
Copy link
Author

是因为 AsyncBox 如果要写到 XAML 里面,要求卡只能卡在构造函数之后。如果构造函数卡了,就是卡在 XAML 上,救不回来的。解决方法有二:

  1. 将卡顿代码从构造函数移至后续(如 Loaded 事件中)
  2. 不在 XAML 中初始化,改用 C# 初始化

如果你试验成功,那么我更新下我的博客。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment