【问题标题】:Build FixedDocument without blocking UI在不阻塞 UI 的情况下构建 FixedDocument
【发布时间】:2017-06-11 19:50:45
【问题描述】:

我正在为DocumentViewer 生成一个文档。它很慢,所以我想释放 UI 线程。使用 async/await 我得到一个异常,“调用线程必须是 STA”。我相信我需要编组通过 UI 线程传递/返回的值,但我似乎无法使其工作。我以各种方式尝试过 Dispatcher.Invoke。

有人知道如何使用 async/await 来做到这一点吗?

这是一个可以粘贴到新的 WPF 项目 (WpfApp1) 中的苗条的工作示例:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <DocumentViewer Document="{Binding Document}"/>
</Window>
public partial class MainWindow : Window, INotifyPropertyChanged
{
    public MainWindow()
    {
        InitializeComponent();
        RebuildDocument(); // Called various places
    }

    public double Length { get; set; } = 100;

    FixedDocument document;
    public FixedDocument Document
    {
        get { return document; }
        set { if (document == value) return; document = value; OnPropertyChanged(); }
    }

    async void RebuildDocument()
    {
        Document = await GenerateDocument(Length);
    }

    private static async Task<FixedDocument> GenerateDocument(double length)
    {
        return await Task.Run(() =>
        {
            // Dummy work
            return new FixedDocument() {
                Pages = { new PageContent() { Child = new FixedPage() {
                        Width = length, Height = length,
                        Children = { new TextBlock() { Text = "dummy page" }}}}}};
        });
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged([CallerMemberName] string propertyName = null)
    { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
}

【问题讨论】:

  • 问题与async/await无关。问题是GenerateDocument 的主体必须在 STA 线程中运行。因为这些是正在实例化的 UI 元素。尝试使用 MVVM 方法。

标签: wpf async-await


【解决方案1】:

我至少能想到两种方法:

  • 在 UI 线程的后台任务中构建您的 FixedDocument,同时在最内层循环中大量产生(并观察取消):

    private static async Task<FixedDocument> GenerateDocument(double length, CancellationToken token = default(CancellationToken))
    {
        var doc = new FixedDocument();
        while (!complete)
        {
            token.ThrowIfCancellationRequested();
            // ...
            doc.Children.Add(anotherChild)
            // ...
            // yield to process user input as often as possible
            await System.Windows.Threading.Dispatcher.Yield(System.Windows.Threading.DispatcherPriority.Input);
        }
    }  
    
  • 在一个新的工作 WPF 线程上构建它(因为 FixedDocument 是一个 DispatcherObject 并且需要一个 Dispatcher 循环),在那里将其序列化为 XAML,然后将其反序列化为您原来的 FixedDocument 的新实例用户界面线程。我不确定 直接反序列化 是否足够快,不会在您的情况下阻塞 UI 线程,但至少有 XamlReader.LoadAsync 您应该能够在不阻塞的情况下异步调用。
    这是一个完整的概念验证代码(为简洁起见,取消逻辑略过):

    private async Task<FixedDocument> GenerateDocumentAsync(double length)
    {
        System.IO.Stream streamIn;
        using (var worker = new DispatcherThread())
        {
            streamIn = await worker.Run(() =>
            {
                var doc = new FixedDocument()
                {
                    Pages = { new PageContent() { Child = new FixedPage() {
                    Width = length, Height = length,
                    Children = { new TextBlock() { Text = "dummy page" }}}}}
                };
    
                var streamOut = new System.IO.MemoryStream();
                XamlWriter.Save(doc, streamOut);
                return streamOut;
            });
        }
    
        streamIn.Seek(0, System.IO.SeekOrigin.Begin);
        var xamlReader = new XamlReader();
        var tcs = new TaskCompletionSource<bool>();
        AsyncCompletedEventHandler loadCompleted = (s, a) =>
        {
            if (a.Error != null)
                tcs.TrySetException(a.Error);
            else
                tcs.TrySetResult(true);
        };
        xamlReader.LoadCompleted += loadCompleted;
        try
        {
            var doc = xamlReader.LoadAsync(streamIn);
            await tcs.Task;
            return (FixedDocument)doc;
        }
        finally
        {
            xamlReader.LoadCompleted -= loadCompleted;
        }
    }
    

    public class DispatcherThread: IDisposable
    {
        readonly Thread _dispatcherThread;
        readonly TaskScheduler _taskScheduler;
    
        public DispatcherThread()
        {
            var tcs = new TaskCompletionSource<TaskScheduler>();
    
            _dispatcherThread = new Thread(() =>
            {
                var dispatcher = Dispatcher.CurrentDispatcher;
                dispatcher.InvokeAsync(() =>
                    tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext()));
                Dispatcher.Run();
            });
    
            _dispatcherThread.SetApartmentState(ApartmentState.STA);
            _dispatcherThread.IsBackground = false;
            _dispatcherThread.Start();
    
            _taskScheduler = tcs.Task.Result;
        }
    
        public void Dispose()
        {
            if (_dispatcherThread.IsAlive)
            {
                Run(() => 
                    Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.Send));
                _dispatcherThread.Join();
            }
        }
    
        public Task Run(Action action, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
        }
    
        public Task<T> Run<T>(Func<T> func, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler);
        }
    
        public Task Run(Func<Task> func, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        }
    
        public Task<T> Run<T>(Func<Task<T>> func, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        }
    }
    

【讨论】:

  • 问题不是异步/等待问题。问题是代码不是 MVVM。 PageContentUIElement。因此不能在后台线程中实例化。异常来自Task.Run
  • @Aron,高度参与文档处理和可视化,我怀疑您尝试将 MVVM 模式应用于 大型复杂 FixedDocument 会遥不可及。虽然文档对象本身可以(并且应该)是 ViewModel 的可出价属性,但您不希望为其单独的 UIElement 子对象执行此操作,除非它是一个非常简单的文档。如果将 MVVM 应用于大型 XAML 文档的整个对象模型,那么 UI 体验会很糟糕。
  • 并且FixedDocument 可以很好地在后台线程上实例化(只要它是 WPF Dispatcher 线程,如答案中所述)。它只是不能跨不同的线程使用(@Vimes 尝试这样做的方式),但它可以通过序列化来克隆。
  • 我几乎不会将 WPF Dispatcher 线程称为后台线程。更多辅助 UI 线程。当然也有应用程序运行多个 UI 线程的代码示例,但当然,这里是龙。
  • 原来,我的问题不是页数,而是文档中的单个元素(Viewport3D)需要 2 秒来构建。如果我在构建每一个之间都这样做,产量是可以接受的。我得到了与第二个解决方案类似的方法:将慢速元素渲染到 RenderTargetBitmaps,一旦冻结,它就可以通过线程边界。如果我有时间,我想切换到文档序列化/反序列化方法。应该可以正常工作,只要 LoadAsync 符合我们的预期。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-01
  • 2020-01-26
  • 1970-01-01
  • 2012-09-15
  • 1970-01-01
  • 1970-01-01
  • 2020-02-06
相关资源
最近更新 更多