最近在学习Prism这一MVVM框架,凡是做UI界面的,不管是Winform也好还是WPF,都只有一个UI线程,我们不能在UI线程中执行耗时的操作,因为这样会阻塞UI线程,是的界面变得卡顿,十分影响用户体验。

    在Prism中,每一个View都会对应一个ViewModel,View中的操作控件比如Button之类的,可以绑定ViewModel中的DelegateCommand;内容显示控件可以绑定ViewModel中的相关Property或者ObservableCollection集合。当在DelegateCommand中执行一些操作后,可以更新这些Property或者集合,利用数据绑定,可以直接更新到UI界面上。通常这些操作默认是在UI线程上进行的。但对于一些耗时的操作,比如从数据库读取数据,访问第三方服务等等,我们需要把它放在工作线程里,当获取到需要的数据时,然后再返回UI线程进行界面更新。

    这里大体分两种情况来说明如何在非UI线程中更新UI界面。

耗时方法通过事件返回结果


    这种类型很常见,比如数据库同步,最近我写了一个数据库同步的程序,其中有个类SyncManager用来执行同步,其中的Start方法是一个同步的耗时方法,他SyncProcessUpdated和SyncProcessFinished事件来报告同步的进度。

<Button Command="{Binding StartSyncCommand}" IsEnabled="{Binding EnableSyncButton}" Content="开始同步"/>
class FlowWindowViewModel : BindableBase
{
    private DelegateCommand startSyncCommand;
    public DelegateCommand StartSyncCommand
    {
        get { return startSyncCommand; }
        set { SetProperty(ref startSyncCommand, value); }
    }

    private bool enableSyncButton;
    private readonly SyncManager syncManager;

    public bool EnableSyncButton
    {
        get { return enableSyncButton; }
        set { SetProperty(ref enableSyncButton, value); }
    }

    private IEventAggregator eventAggregator;
    public FlowWindowViewModel(SyncManager syncManager, IEventAggregator _eventAggregator)
    {
        enableSyncButton = true;
        startSyncCommand = new DelegateCommand(StartSync);
        this.syncManager = syncManager;
        this.syncManager.SyncProcessUpdated += SyncProcessUpdated;
        this.syncManager.SyncProcessFinished += SyncProcessFinished;
    }

    private void StartSync()
    {
        EnableSyncButton = false;
        Task.Factory.StartNew(() =>
        {
            syncManager.Start();
        });
    }

    private void SyncProcessUpdated(object sender, SyncProgress e)
    {
        TransferProduce transfer = sender as TransferProduce;
        string msg = $"{transfer.Name}-{e.Msg}-{e.Percent}%-{e.Status}";
        eventAggregator.GetEvent<AddNewLogEvent>().Publish(new NewLogModel { Message = msg });
    }

    private void SyncProcessFinished(object sender, bool e)
    {
        if (e)
        {
           EnableSyncButton = true;
        }
    }
}

     上面的程序,“开始同步”按钮绑定了StartSyncCommand,当点击“开始同步”时,首先将按钮设置为不可用,然后执行SyncManager的StartSync操作,这是一个非常耗时的操作,所以将他放在了Task工作线程里进行处理。在SyncProcessUpdate中,将进度使用事件发送出去,在SyncProcessFinished中,将“开始同步”按钮设置为可用。如果直接运行,是会报错的。

System.InvalidOperationException:“调用线程无法访问此对象,因为另一个线程拥有该对象。”

     所以在回调方法里,要重新切换到UI线程去更新属性或者集合。这里新建一个Execute帮助类:

public static class Execute
{
    private static Action<System.Action> executor = action => action();

    /// <summary>
    /// Initializes the framework using the current dispatcher.
    /// </summary>
    public static void InitializeWithDispatcher()
    {
        var dispatcher = Dispatcher.CurrentDispatcher;
        executor = action =>
        {
            if (dispatcher.CheckAccess())
                action();
            else dispatcher.BeginInvoke(action);
        };
    }

    /// <summary>
    /// Executes the action on the UI thread.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    public static void OnUIThread(this System.Action action)
    {
        executor(action);
    }
}

    相当简单,只需要在构造函数里调用一下InitializeWithDispatcher方法,然后后续直接使用OnUIThread即可。改造后如下:

class FlowWindowViewModel : BindableBase
{
    private DelegateCommand startSyncCommand;
    public DelegateCommand StartSyncCommand
    {
        get { return startSyncCommand; }
        set { SetProperty(ref startSyncCommand, value); }
    }

    private bool enableSyncButton;
    private readonly SyncManager syncManager;

    public bool EnableSyncButton
    {
        get { return enableSyncButton; }
        set { SetProperty(ref enableSyncButton, value); }
    }

    private IEventAggregator eventAggregator;
    public FlowWindowViewModel(SyncManager syncManager, IEventAggregator _eventAggregator)
    {
        enableSyncButton = true;
        eventAggregator = _eventAggregator;
        startSyncCommand = new DelegateCommand(StartSync);
        this.syncManager = syncManager;
        this.syncManager.SyncProcessUpdated += SyncProcessUpdated;
        this.syncManager.SyncProcessFinished += SyncProcessFinished;
        Execute.InitializeWithDispatcher();
    }

    private void StartSync()
    {
        EnableSyncButton = false;
        Task.Factory.StartNew(() =>
        {
            syncManager.Start();
        });
    }

    private void SyncProcessUpdated(object sender, SyncProgress e)
    {
        Execute.OnUIThread(() =>
        {
            TransferProduce transfer = sender as TransferProduce;
            string msg = $"{transfer.Name}-{e.Msg}-{e.Percent}%-{e.Status}";
            eventAggregator.GetEvent<AddNewLogEvent>().Publish(new NewLogModel { Message = msg });
        });
    }

    private void SyncProcessFinished(object sender, bool e)
    {
        if (e)
        {
            Execute.OnUIThread(() =>
            {
                EnableSyncButton = true;
            });
        }
    }
}

耗时方法直接返回结果


    还有一种情况就是耗时方法通过返回值来返回结果,如果将耗时方法放到工作线程里执行后,要将返回值放到UI线程里去更新绑定了UI元素的属性或者集合。

var result = await Task.Factory.StartNew(() =>TimeConsumingMethod(), TaskCreationOptions.LongRunning);
if (result.Count>0)
{
    UpdateUI(result);
}

    或者使用ContinueWith

var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew(() => TimeConsumingMethod(), TaskCreationOptions.LongRunning).ContinueWith(result =>
{
    UpdateUI(result);
}, uiScheduler);