构建FixedDocument而不阻止UI

时间:2017-05-06 01:25:20

标签: wpf async-await

我为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)); }
}

1 个答案:

答案 0 :(得分:0)

我至少可以想到两种方法:

  • 在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线程上构建它(因为FixedDocumentDispatcherObject并且需要Dispatcher循环),在那里将其序列化为XAML,然后将其反序列化为原始UI线程上的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();
        }
    }