65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF:非常有用的线程组件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (50投票s)

2009 年 12 月 30 日

CPOL

19分钟阅读

viewsIcon

132468

downloadIcon

1254

一个显示故障/繁忙状态和数据的线程组件。

目录

引言

正如许多人可能知道的,我写了很多关于 UI 工作和 WPF 技术的内容,过去我曾写过关于 线程 的文章,也写了很多关于我自己的 WPF MVVM 框架 Cinch 的内容,其中包含了很多帮助您以 MVVM 方式上手 WPF 的内容。

嗯,在使用 WPF 处理线程和在后台执行某些操作同时保持 UI 响应方面,有一件事一直让我很头疼。当然,我可以创建一个新的 Thread 或使用 ThreadPool,甚至使用 Cinch 中的酷炫 BackgroundTaskManager 类,它在通过内部持有的 BackgroundWorker 获取数据方面确实很有帮助,而且完全可进行单元测试,正如 这篇 Cinch 文章中所讨论的。我只是觉得还可以做得更好。

例如,我希望能够做到的是,让一个作为 View 的 DataContext 使用的 ViewModel(以允许绑定和测试)能够接受一个参数化的工作 delegate,该 delegate 将在后台线程上执行,并在线程操作进行时显示某种忙碌动画或状态,然后如果后台线程操作失败,它会在 View 的某个地方显示错误状态,或者如果后台线程获取相关数据时 **没有** 发生故障,它就会显示我所请求的、由参数化的工作 delegate 获取的实际数据。

我应该指出,这里包含的代码为每个线程操作都会创建一个新的 BackgroundWorker,每个操作都应返回一些单一的、可定义的数据块,例如单个 List<SomeClass>,甚至是其他一些耗时非常长的数据。我的意图 **从来不是** 要有一个全局的线程管理器来一次性获取所有内容,而是更侧重于对数据的微观管理。

用通俗易懂的语言来说,可以这样想(在这个斜体段落中,“我”和“我”指的是本文提供的演示应用代码):

您希望我显示一个可能需要一些时间来获取的 List<Contact>,没问题。我将创建一个 BackgroundWorker 来完成这项工作并管理此后台活动,并告知您进展如何,当我完成后,我将返回一个 List<Contact> 或一些错误消息。哦,您还希望我在同一个 View 上显示一个可能也需要一些时间来获取的 List<BankAccount>。嗯,那么,对于这个,我需要返回另一种类型的 List。实际上,它将是一个 List<BankAccount>,所以我需要另一个 BackgroundWorker 来完成它,并为您返回 List<BankAccount>

我知道这可能会创建几个线程,但内部来说,BackgroundWorker 使用了 ThreadPool,这很棒,所以我认为这不是问题,因为 .NET Framework 为我们处理了线程的管理。显然,如果一个 View 上有成百上千个数据列表,那么本文可能不适合您,您应该立即停止阅读。另一方面,如果您有少量数据列表或少量昂贵的数据需要获取,那么这段代码可能适合您。

总之,这就是我所设想的运作方式,在折腾了一段时间后,我认为我已经做到了。

我做了一些假设,如下所述:

  • 人们正在使用 WPF,并且对它相当精通。这 **绝对不是** 一个初学者文章。
  • 人们正在使用 MVVM 模式。
  • 人们认为,如果某个 View 的部分数据显示的是应获取但未能获取的数据的错误消息,这是一个好主意。
  • 人们乐于接受这样一个想法:一个数据项(例如一个数据列表)或昂贵的数据项由一个专用的 BackgroundWorker 管理对象(我们稍后会讲到)来获取。

实际问题

问题实际上是这样的:据我所见,在最好的情况下,很少有人花时间向用户提供很多反馈,更不用说在长时间运行的线程操作发生时了。事实上,一些 UI 会直接在 UI 线程上执行所有操作,让用户等待。好吧,有些人会改变图标为沙漏形或其他东西,并在某些事情发生时禁用按钮等。

如果我们有一个让用户了解进展的组件,那不是更好吗?他们启动一个长时间运行的操作,要么是主动请求,要么是打开需要它的 View,当发生这种情况时,用户会持续看到正在发生的事情,例如在工作时显示进度条,如果失败了,我们就直接在 UI 上显示原因,而不是在某个 MessageBox 中,而该 MessageBox 在他们接受“确定”后就消失了,然后尝试联系支持部门,却被问到 MessageBox 的消息是什么。哦,我关闭了它,抱歉。当然,当一切顺利时,只需隐藏进度条,**不要** 显示任何失败的 UI,而是显示他们想要但现在已经获取到的内容。

这就是问题,据我所见。

解决方案

那么解决方案是什么?好吧,解决方案显然是想出一些可以解决问题的方法,对吧?

嗯,本文解决了上述问题描述中的所有问题。它通过使用各种 WPF 技术和线程零碎来实现这一点。

正如我之前提到的,所附演示应用中提供的代码假设您正在使用 MVVM 模式。我并不是说您无法在不使用 MVVM 的情况下使其工作;只是,我认为 MVVM 非常棒,而且它确实有效,而且这是我在这篇文章的文本中唯一会描述的方式。

解决方案看起来是怎样的

演示代码利用了一个简单的想法:我们使用一个 WPF UserControl 来包装一部分内容,这将是您希望在后台线程中获取的 UI 数据。

所以当它运行时,看起来有点像这样

所附代码利用 AdornerLayer 来显示不同的装饰器,具体取决于后台线程操作的状态。如果后台线程操作正在忙碌,则显示 BusyAdorner。如果后台线程操作失败且 **不** 忙碌,则显示 FailedAdorner。如果后台线程操作 **不** 忙碌且 **不** 失败,则隐藏 BusyAdornerFailedAdorner,只留下 UI 中显示的原始数据,这些数据现在已在后台线程上获取。

您可以在下面的 装饰器部分阅读更多关于装饰器工作原理的内容。

解决方案如何工作

在这一系列小节中,我将概述所附演示代码的结构。需要注意的是,您不需要了解很多内容,只需将其包含在您的项目中,并按照 如何在您自己的 WPF 应用中使用此想法部分中的建议进行操作。但如果您像我一样,您会想在知道如何使用它之前就全面了解细节,所以这一部分就是关于这个的。

关于演示应用的序言

所附的演示应用程序显然必须演示整个想法。因此,有一个“延迟”代码允许用户选择或不选择后台线程操作是否应在运行时失败。用户可以通过单击演示代码 UI 上的按钮来完成此操作。

现在,这 **显然** 只是测试代码,**绝不应** 在生产代码中使用;它所做的只是切换一个“ShouldFail”标志,该标志在后台线程 delegate 中进行检查。在演示代码中,它看起来像这样:

Func<Dictionary<String, Object>, 
   ThreadableItem<List<StuffData>>> taskFunc = (inputParams) =>
{
    try
    {
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        #region TEST EXAMPLE CODE, YOU SHOULD NOT DO THIS IN PRODUCTION CODE
        if (ShouldFail)
        {
            throw new InvalidOperationException(
                "InvalidOperationException occurred\r\n\r\n" +
                "This Exception has been raised inside " + 
                "the Window1ViewModel delegate " +
                "which is the actual payload for the " + 
                "ThreadableItemViewModel<T> TaskFunc delegate.\r\n\r\n" +
                "Which is obvioulsy not what you would " + 
                "do in a production system.\r\n\r\n" +
                "You would more typically catch your own business Exceptions " +
                "(say from talking to WCF) and then rethrow them. " +
                "This just demomstrated how all this hangs together");
        }
        else
        {
            List<StuffData> data = new List<StuffData>();
            for (int i = 0; i < (Int32)inputParams["loopMax"]; i++)
            {
                data.Add(new StuffData(String.Format("The Text Is {0}",i),i));
                Thread.Sleep(5); //simulate time going by
            }
            //work is done at this point, so return new ThreadableItem with the
            //actual data in it, and no failure message
            return new ThreadableItem<List<StuffData>>(data, String.Empty);
        }
        #endregion
    }
    //something went wrong so return a new ThreadableItem with the failed
    //message in it
    catch(Exception ex)
    {
        return new ThreadableItem<List<StuffData>>(null, ex.Message);
    }
};

它只是在那里演示代码的预期工作方式。在实际的生产代码中,您不会这样做;您更可能做类似这样的事情:

Func<Dictionary<String, Object>, ThreadableItem<List<Client>>> taskFunc = (inputParams) =>
{
    try
    {
        try
        {
            //Obtain a list of Clients that work in particular BusinessArea 
            Service<IGateway>.Use((client) =>
            {
                RetrieveDataByQueryRequest request = new RetrieveDataByQueryRequest();
                request.Query = new Query().SelectAll(BusinessEntityType.Client)
                    .Where(new Filter("BusinessAreaId", 
                        FieldOpeartor.Equals,(Int32)inputParams["businessArea"]);

                RetrieveDataByQueryResponse response = 
                    (RetrieveDataByQueryResponse)client.Execute(request);

                return new ThreadableItem<List<Client>>(response.Clients, String.Empty);
            });
        }
        //catch WCF FaultExceptions
        catch (FaultException<SerializationException> sex)
        {
            //throw actual Exception which is caught it outer catch
            throw new BusinessException("A serialization issue has occurred");
        }
        //catch WCF FaultExceptions
        catch (FaultException<InvalidArgumentException> iaex)
        {
            //throw actual Exception which is caught it outer catch
            throw new BusinessException("One of the arguments is invalid");
        }

    }
    //something went wrong so return a new ThreadableItem with the failed
    //message in it
    catch(Exception ex)
    {
        return new ThreadableItem<List<Client>>(null, ex.Message);
    }
};

虽然这现在看起来可能有点棘手,但请放心,我们稍后将详细介绍那个相当难看的 Func<T,TResult> delegate 声明。需要注意的重要一点是,演示代码使用了一些糟糕的测试代码,您应该将其视为需要用 **您的真实** 代码替换的演示代码,就像上面看到的代码块一样。

MVVM

正如我所说的,演示代码使用了 MVVM 模式。您可能会问为什么。原因很简单;MVVM 允许我们直接绑定到 ViewModel,并且 ViewModel 也为单元测试提供了非常好的入口点。因此,所附演示代码中有各种 ViewModel,其中主要的有:

ViewModelBase:所有其他 ViewModel 继承的简单 INotifyPropertyChanged 基类。

这是它的样子,没什么花哨的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace ThreadingComponent
{
    /// <summary>
    /// Base class for all ViewModels, simply provides INPC support
    /// </summary>
    public class ViewModelBase : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Notify using pre-made PropertyChangedEventArgs
        /// </summary>
        /// <param name="args"></param>
        protected void NotifyPropertyChanged(PropertyChangedEventArgs args)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, args);
            }
        }

        /// <summary>
        /// Notify using String property name
        /// </summary>
        protected void NotifyPropertyChanged(String propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion
    }
}

ThreadableItemViewModelBase:这是 ThreadableItemViewModel<T> 基于的 ViewModel 的基类,它允许处理 AdornerLayer 并包装数据的 UserControl 在不关心当前后台线程操作的实际通用类型的情况下挂钩 INotifyPropertyChanged 属性更改监视器。此类基本上只公开所有 ThreadableItemViewModel<T> 基于的 ViewModel **将** 需要使用的常用属性。

这是它的样子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ThreadingComponent
{
    /// <summary>
    /// Provides a base class for <see cref="ThreadableItemViewModelBase">
    /// ThreadableItemViewModel</see> classes. The reason a base class is needed
    /// is that the <see cref="ThreadableHostControl">ThreadableHostControl</see> 
    /// needs to listen to certain INPC value changes to swap the Adorners in and out. 
    /// Now as inheritors of this class use Generics the <see cref="ThreadableHostControl">
    /// ThreadableHostControl</see> needs a common INPC ancestor that it can listen
    /// to INPC property changes from. This base class enables that behaviour.
    /// </summary>
    public class ThreadableItemViewModelBase : ViewModelBase
    {
        #region Data
        private Boolean isBusy = false;
        private Boolean failed = false;
        private String  errorMessage = String.Empty;
        #endregion

        #region Public Properties
        public bool IsBusy
        {
            get { return isBusy; }
            set 
            {
                isBusy = value;
                NotifyPropertyChanged("IsBusy");
            }
        }

        public bool Failed
        {
            get { return failed; }
            set
            {
                failed = value;
                NotifyPropertyChanged("Failed");
            }
        }

        public String ErrorMessage
        {
            get { return errorMessage; }
            set
            {
                errorMessage = value;
                NotifyPropertyChanged("ErrorMessage");
            }
        }
        #endregion
    }
}

ThreadableItemViewModel<T>:这个通用 ViewModel 继承自 ThreadableItemViewModelBase。它的工作是管理后台线程操作。因此,它具有各种促进后台线程活动运行管理的属性。T 的通用类型应该是您想要的后台线程活动的 **类型**。因此,例如,如果我期望得到一个 List<Client>,那么 T 将是 List<Client>

您需要为要执行的每个后台活动拥有一个这样的 ThreadableItemViewModel<T>。在演示应用程序中,只有一个关于某种虚构模型数据“StuffData”的 List,所以在我的主 ViewModel (Window1ViewModel) 中,我有一个 ThreadableItemViewModel<List<StuffData>> 的单个实例,用于管理 List<StuffData> 的检索。

这是它的样子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Threading;

namespace ThreadingComponent
{
    /// <summary>
    /// This ViewModel contains all the data required to run a work delegate
    /// (the TaskFunc Func<Dictionary<String,Object>,ThreadableItem<T>>) and also
    /// exposes methods to allow the work delegate to be run on a new thread. The 
    /// Data obtained by running the background thread is exposed on the Data
    /// property, and the BgWorker is also exposed as a property so that it can be
    /// used within Unit tests.
    /// </summary>
    /// <typeparam name="T">The return type of the threading operation</typeparam>
    public class ThreadableItemViewModel<T> : ThreadableItemViewModelBase
    {
        #region Data
        private ThreadableItem<T> data;
        private Func<Dictionary<String,Object>,ThreadableItem<T>> taskFunc;
        private BackgroundTaskManager<ThreadableItem<T>> bgWorker;
        private Dictionary<String, Object> parameters = null;
        #endregion

        #region Private Methods
        /// <summary>
        /// Sets up the actual BackgroundTaskManager<T> component
        /// </summary>
        private void SetupWorker()
        {
            if (taskFunc == null)
                throw new NullReferenceException("TaskFunc can not be null");

            bgWorker = new BackgroundTaskManager<ThreadableItem<T>>(
                 () =>
                 {
                     return taskFunc(Parameters);
                 },
                 (result) =>
                 {
                     Data = result;
                 });

            BgWorker.BackgroundTaskStarted -= BackgroundTaskStarted;
            BgWorker.BackgroundTaskCompleted -= BackgroundTaskCompleted;

            BgWorker.BackgroundTaskStarted += BackgroundTaskStarted;
            BgWorker.BackgroundTaskCompleted += BackgroundTaskCompleted;
        }

        /// <summary>
        /// Event that is raised when the background work is Completed. This event
        /// is raised by the internal BackgroundTaskManager<T> component
        /// </summary>
        private void BackgroundTaskCompleted(object sender, EventArgs args)
        {
            //The order that these properties IS IMPORTANT, as it dictates
            //which Adorner will be shown
            IsBusy = false;
            Failed = !String.IsNullOrEmpty(Data.Error);

        }

        /// <summary>
        /// Event that is raised when the background work is Started. This event
        /// is raised by the internal BackgroundTaskManager<T> component
        /// </summary>
        private void BackgroundTaskStarted(object sender, EventArgs args)
        {
            //The order that these properties IS IMPORTANT, as it dictates
            //which Adorner will be shown
            IsBusy = true;
            Failed = false;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Run the work delegate on a new thread
        /// </summary>
        public void Run()
        {
            SetupWorker();
            bgWorker.RunBackgroundTask();
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// The actual work delegate. This must ALWAYS take a 
        /// Dictionary<String,Object> as the 1st parameter, which is the collection
        /// of parameters that the work delegate may need to do its work. This can obviously
        /// be null. The T is the expected return type for the threading operation
        /// </summary>
        public Func<Dictionary<String,Object>,ThreadableItem<T>> TaskFunc
        {
            set
            {
                taskFunc = value;
            }
        }

        /// <summary>
        /// A Dictionary of key/value pairs, where each pair is a parameter
        /// that the work delegate (TaskFunc) may need to do its work
        /// </summary>
        public Dictionary<String, Object> Parameters
        {
            get { return parameters; }
            set { parameters = value; }
        }

        /// <summary>
        /// The BackgroundTaskManager exposed so you can use it within
        /// Unit tests. See the actual <see cref="BackgroundTaskManager">
        /// BackgroundTaskManager</see> code for how to do that
        /// </summary>
        public BackgroundTaskManager<ThreadableItem<T>> BgWorker
        {
            get { return bgWorker; }
        }

        /// <summary>
        /// The actual Data that is the result of running the 
        /// background threading operation
        /// </summary>
        public ThreadableItem<T> Data
        {
            get { return data; }
            set
            {
                data = value;
                if (data != null)
                    this.ErrorMessage = data.Error;
                NotifyPropertyChanged("Data");
            }
        }
        #endregion
    }
}

在这个“坏小子”中需要注意的最重要的事情是公共属性:

  • TaskFunc:这是一个 Func delegate,它 **就是** 您希望执行的实际后台工作。
  • Parameters:一个 Dictionary<String,Object>,它被馈送到 TaskFunc Func delegate 中,这是您的参数集合,在运行后台操作时可能很有用。
  • BgWorker:实际的 BackgroundTaskManager<T>,它被公开出来,以便单元测试可以设置一个 AutoResetEvent,并且只 **等待** 操作发生一段时间,然后假定它失败了。BackgroundTaskManager<T> 类直接来自我的 Cinch MVVM 框架,并在 这篇 Cinch 文章中进行了讨论。
  • Data:是实际数据,其类型为 ThreadableItem<T>,那么一个这样的东西是什么样的呢?好吧,ThreadableItem<T> 是一个简单的类,它代表后台操作的结果,所以它有一个 DataObject 和一个 Error。一次只期望其中一个具有实际值。

如果操作成功运行,则 ThreadableItem<T>DataObject 将是用户所请求的 T 类型的一个实例,而 ThreadableItem<T>Error 将是一个空字符串。

如果操作失败,则 ThreadableItem<T>DataObject 将为 null,而 ThreadableItem<T>Error 将是异常消息字符串。

为了清晰起见,这是 ThreadableItem<T> 类的样子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ThreadingComponent
{
    /// <summary>
    /// This class represents a threadable item, which can have
    /// both an valid payload dataobject which is specified using
    /// the generic, and also an error string value
    /// </summary>
    /// <typeparam name="T">The type of the return value for the threading
    /// operation that will use a ThreadableItem</typeparam>
    public class ThreadableItem<T> : ViewModelBase
    {
        #region Data
        private T dataObject;
        private String error;
        #endregion

        #region Constructor
        public ThreadableItem(T dataObject, String error)
        {
            this.DataObject = dataObject;
            this.Error = error;
        }
        #endregion

        #region Public Properties
        public T DataObject
        {
            get { return dataObject; }
            set
            {
                dataObject = value;
                NotifyPropertyChanged("DataObject");
            }
        }

        public String Error
        {
            get { return error; }
            set 
            {
                error = value;
                NotifyPropertyChanged("Error");
            }
        }
        #endregion
    }
}

到目前为止,我们在 ViewModel 中涵盖的所有内容,您很可能 **不会** 更改,所以还需要涵盖最后一个,那就是如何实际使用我们上面看到的那些。在演示应用程序中,有一个名为 Window1.xaml 的单个 Window,它有一个单个 ViewModel(以允许绑定)称为 Window1ViewModel,它管理 Window1 想要执行的所有操作。

Window1ViewModel 是如何使用其余代码库的一个示例,尽管正如我之前所说的,它包含测试代码,以便您可以看到所有这些是如何组合在一起的,并且您 **将** 需要在生产代码中对其进行更改。同样,我前面已经给出了一个示例。

命令

虽然严格来说不属于本文的主要内容,但我建议使用某种 DelegateCommand,或者 Josh SmithRelayCommand,或者 Marlon GrechSimpleCommand(我使用的是这个),所有这些都允许您的 UI 绑定到 ViewModel 公开的 ICommand 并实际在 ViewModel 中运行代码。我推荐 ICommand 的原因是因为您可以直接在 ViewModel 的 ICommand.CanExecute 中禁用一个按钮,如果此时有后台线程操作正在进行。有关此示例,请参阅 Window1ViewModel

显然,这仅适用于线程操作是由于按钮单击或用户启动的某个操作而发生的。

多线程

由于本文完全是关于一个后台线程组件/想法,您可能会期望有很多关于线程的内容。嗯,实际上,其中很多都已为您抽象化了。

涉及的步骤几乎是这样的(您可以使用所附的演示 Window1ViewModel 代码作为编写自己的 ViewModel 代码以使用本文想法/概念的基础):

步骤 1:公开属性并选择 T

创建一个 ViewModel,并公开一个 ThreadableItemViewModel<T> 作为公共属性,然后您可以使用 ThreadableHostControl UserControl 进行绑定,该 UserControl 在下一节中进行描述,其中通用 T 显然必须用正确的类型限定;例如,这是一个有效的属性声明:

public ThreadableItemViewModel<List<StuffData>> ThreadVM
{
    get { return threadVM; }
}
步骤 2:连接后台工作委托

现在我们已经将 ThreadableItemViewModel<T> 作为公共属性公开,并为其选择了返回类型,我们可以看看如何获取后台线程中的结果。这很容易实现;我们只需要设置后台工作委托,方法如下:

Func<Dictionary<String, Object>, ThreadableItem<List<StuffData>>> taskFunc = (inputParams) =>
{
    try
    {
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        // REGION BELOW FOR TESTING ONLY
        #region TEST EXAMPLE CODE, YOU SHOULD NOT DO THIS IN PRODUCTION CODE
        if (ShouldFail)
        {
            throw new InvalidOperationException(
                "InvalidOperationException occurred\r\n\r\n" +
                "This Exception has been raised inside the Window1ViewModel delegate " +
                "which is the actual payload for the " + 
                "ThreadableItemViewModel<T> TaskFunc delegate.\r\n\r\n" +
                "Which is obvioulsy not what you would do in a production system.\r\n\r\n" +
                "You would more typically catch your own business Exceptions " +
                "(say from talking to WCF) and then rethrow them. " +
                "This just demomstrated how all this hangs together");
        }
        else
        {
            List<StuffData> data = new List<StuffData>();
            for (int i = 0; i < (Int32)inputParams["loopMax"]; i++)
            {
                data.Add(new StuffData(String.Format("The Text Is {0}",i),i));
                Thread.Sleep(5); //simulate time going by
            }
            //work is done at this point, so return new ThreadableItem with the
            //actual data in it, and no failure message
            return new ThreadableItem<List<StuffData>>(data, String.Empty);
        }
        #endregion
    }
    //something went wrong so return a new ThreadableItem with the failed
    //message in it
    catch(Exception ex)
    {
        return new ThreadableItem<List<StuffData>>(null, ex.Message);
    }
};

//setup Threading task function
threadVM.TaskFunc = taskFunc;

这里要观察的重要一点是,我们返回的是 ThreadableItem<T>,其中 T 的类型是 List<StuffData>。请记住,Func<T,TResult> 仅仅是一个 delegate,其外观如下:

public delegate TResult Func<T, TResult>(T arg);

所以实际上,我们只是说我们有一个委托,它接受 Dictionary<String, Object> 作为输入参数,并返回 ThreadableItem<List<StuffData>> 作为返回值。很简单,对吧?

Dictionary<String, Object> 充当参数集合,这些参数可能是线程工作项所需要的,所以我们需要像这样配置它:

//setup Threading parameters
Dictionary<String, Object> parameters = new Dictionary<String, Object>();
parameters.Add("loopMax", 30000);
threadVM.Parameters = parameters;

如果您的工作委托 **不** 需要任何参数即可完成其工作,请将 Parameters = null

步骤 3:运行后台工作项

最后要做的是运行后台工作项,这和下面一样简单:

threadVM.Run();

这将在 ThreadableItem<T> 代码内部执行以下操作:

/// <summary>
/// Run the work delegate on a new thread
/// </summary>
public void Run()
{
    SetupWorker();
    bgWorker.RunBackgroundTask();
}

/// <summary>
/// Sets up the actual BackgroundTaskManager<T> component
/// </summary>
private void SetupWorker()
{
    if (taskFunc == null)
        throw new NullReferenceException("TaskFunc can not be null");

    bgWorker = new BackgroundTaskManager<ThreadableItem<T>>(
         () =>
         {
             return taskFunc(Parameters);
         },
         (result) =>
         {
             Data = result;
         });

    BgWorker.BackgroundTaskStarted -= BackgroundTaskStarted;
    BgWorker.BackgroundTaskCompleted -= BackgroundTaskCompleted;

    BgWorker.BackgroundTaskStarted += BackgroundTaskStarted;
    BgWorker.BackgroundTaskCompleted += BackgroundTaskCompleted;
}

此代码使用了 Cinch 中的一个很棒的 BackgroundTaskManager 类,该类在通过内部持有的 BackgroundWorker 获取数据方面很有帮助,并且完全可进行单元测试,正如 这篇 Cinch 文章中所讨论的。需要注意的重要一点是,这里可以看到它只是调用了原始的 taskFunc 属性,也就是您刚刚使用 Func<T,TResult> delegate 指定的那个,还记得吗?

Func<Dictionary<String, Object>, ThreadableItem<List<StuffData>>> taskFunc = (inputParams) =>
{
  ....
  ....
  ....
  ....
};

//setup Threading task function
threadVM.TaskFunc = taskFunc;

它还将 Func<T,TResult> delegate 和参数 Dictionary<String, Object> 传递进去,这些参数可以在线程委托工作项内部使用。

我想我最好用截图来证明这一切都有效。这是使用上述工作项代码的截图:

警告

上面的代码只是为了演示故障代码的预期工作方式。一位读者提醒我,之前的文章(是的,这篇文章被重新发布了)占用了 400MB 的内存。以前之所以如此,是因为我旧的工作委托会创建 5,000,000 个对象在内存中,这正如您可以想象的,是一个真正的问题。现在,我们在生产环境中绝不会这样做。我已经修改了代码,使其使用更适度的 30,000 个对象,这会占用大约 19MB 的内存。所以感谢那位用户发现了这个问题。很高兴不是我做错了什么。呼。

事实上,在我重新发布之后,同一用户 Insomniac Geek 又提出了一个相当明智的建议,那就是使用少量的项目(1000 个)并使用 System.Threading.Thread.Sleep(5) 来模拟一些工作,所以我现在就是这样做的。因为即使有 30,000 个项目,后台线程也只在几毫秒内完成。所以感谢用户 Insomniac Geek

在实际的生产代码中,您不会这样做,您更可能做类似这样的事情:

Func<Dictionary<String, Object>, ThreadableItem<List<Client>>> taskFunc = (inputParams) =>
{
    try
    {
        try
        {
            //Obtain a list of Clients that work in particular BusinessArea 
            Service<IGateway>.Use((client) =>
            {
                RetrieveDataByQueryRequest request = new RetrieveDataByQueryRequest();
                request.Query = new Query().SelectAll(BusinessEntityType.Client)
                    .Where(new Filter("BusinessAreaId", 
                        FieldOpeartor.Equals,(Int32)inputParams["businessArea"]);

                RetrieveDataByQueryResponse response = 
                    (RetrieveDataByQueryResponse)client.Execute(request);

                return new ThreadableItem<List<Client>>(response.Clients, String.Empty);
            });
        }
        //catch WCF FaultExceptions
        catch (FaultException<SerializationException> sex)
        {
            //throw actual Exception which is caught it outer catch
            throw new BusinessException("A serialization issue has occurred");
        }
        //catch WCF FaultExceptions
        catch (FaultException<InvalidArgumentException> iaex)
        {
            //throw actual Exception which is caught it outer catch
            throw new BusinessException("One of the arguments is invalid");
        }

    }
    //something went wrong so return a new ThreadableItem with the failed
    //message in it
    catch(Exception ex)
    {
        return new ThreadableItem<List<Client>>(null, ex.Message);
    }
};

//setup Threading task function
threadVM.TaskFunc = taskFunc;

装饰器

既然您知道有一个公开的 ThreadableItemViewModel<T> 属性,它使用了内部的 ThreadableItem<T>,我们可以想象在 UI 中使用它。这是通过在您自己的 ViewModel 中公开一个 ThreadableItemViewModel<T> 属性来完成的,如下所示:

public ThreadableItemViewModel<List<StuffData>> ThreadVM
{
    get { return threadVM; }
}

在这种情况下,通用 T 显然是 List<StuffData>。那么我们如何在实际的 View 中使用它呢?好吧,让我们来看看,好吗?

<local:ThreadableHostControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
    ThreadableItem="{Binding ThreadVM}">

    <ListView Background="WhiteSmoke" BorderBrush="Black" BorderThickness="5"
              ItemsSource="{Binding ThreadVM.Data.DataObject}">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Text" Width="500" 
                                DisplayMemberBinding="{Binding Path=Text}"/>
                <GridViewColumn Header="Age" Width="100" 
                                DisplayMemberBinding="{Binding Path=Age}"/>
            </GridView>
        </ListView.View>
    </ListView>
    
</local:ThreadableHostControl>

可以看到有一个 ThreadableHostControl UserControl,它利用了 ViewModel 公开的 ThreadableItemViewModel<List<StuffData>> ThreadVM 属性。

这里有两件巧妙的事情在进行:

让内容成为内容

在 WPF 中,有很多种做事的方式,但我总是喜欢保持我的 XAML 尽可能干净。一部分技巧是理解框架,但也知道哪些类支持哪些属性。UserControl 有一个 Content 属性,所以我们只需要用它来承载我们想要显示并在后台线程中获取的数据。在这种情况下,这将是一个绑定到 ListViewList<StuffData>

我们可以通过几个 UI 控件的 Visibility 属性来切换来实现一些技巧,但这会增加 XAML,哎哟。更好的方法是让内容成为内容,然后我们可以将额外的内容放在内容之上,在一个称为 AdornerLayer 的层中。

如果您不了解 AdornerLayer,您应该阅读这个 MSDN 链接:http://msdn.microsoft.com/en-us/library/ms743737.aspx

支持装饰器

ThreadableHostControl.ThreadableItem 属性绑定到一个 ThreadableItemViewModel<T> 的实例,该实例继承自 ThreadableItemViewModelBase,正如我之前提到的,它是所有 ThreadableItemViewModel<T> 的基类,它支持几个属性,即:

  • IsBusy
  • 失败
  • ErrorMessage

那么 ThreadableHostControl UserControl 如何利用它通过 Binding 获取的 ThreadableItemViewModelBase 属性呢?这很有趣;让我们来看看,好吗?

ThreadableHostControlThreadableItem DependencyProperty 更改事件调用一个内部方法,名为 SetupPropertyWatcher()

private static void OnThreadableItemChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
{
    if (e.NewValue != null)
    {
        ((ThreadableHostControl)d).SetupPropertyWatcher(
            (ThreadableItemViewModelBase)e.NewValue);
    }
}

所以,查看 SetupPropertyWatcher() 方法,我们可以看到它为 ThreadableItemViewModelBase DP 实例的各种属性设置了 INotifyPropertyChanged 属性监视器。

/// <summary>
/// Watches the IsBusy/Failed INPC properties of the 
/// ThreadableItemViewModelBase and when they change, 
/// swaps in/out the correct Adorner to suit the current 
/// state of the ThreadableItemViewModelBase
/// </summary>
/// <param name="item">The ThreadableItemViewModelBase to 
/// watch INPC changes on</param>
private void SetupPropertyWatcher(ThreadableItemViewModelBase item)
{
    if (threadableItemObserver != null)
    {
        threadableItemObserver.UnregisterHandler(n => n.IsBusy);
        threadableItemObserver.UnregisterHandler(n => n.Failed);
    }
    threadableItemObserver = new PropertyObserver<ThreadableItemViewModelBase>(item);
    threadableItemObserver.RegisterHandler(n => n.IsBusy, this.IsBusyChanged);
    threadableItemObserver.RegisterHandler(n => n.Failed, this.FailedChanged);
}

注意:我正在使用 Josh Smith 非常出色的 PropertyObserver 来设置原始 INPC 对象上的监视器方法。

如果我们跟着其中一个来看,比如 Failed -> FailedChanged() 方法,我们就会看到会发生什么:

/// <summary>
/// Shows the FailedAdorner
/// </summary>
private void FailedChanged(ThreadableItemViewModelBase vm)
{
    if (vm.IsBusy)
        return;

    //If the users chose to throw an Exception on a null AdornerLayer
    //throw an Exception. The user may change this setting in the App.Config
    //which obviously impacts how the code works, but the Background
    //thread will still run, its just that the Adorners that this class
    //manages will not be shown. Which is not how the code was intended
    //to work. It would be better to find out why the AdornerLayer is null
    adornerLayer = AdornerLayer.GetAdornerLayer(this);

    if (shouldThrowExceptionOnNullAdornerLayer && adornerLayer == null)
        throw new NotSupportedException(
            "The ThreadableHostControl will only work correctly\r\n" +
            "if there is an AdornerLayer found and it is not null");

    if (adornerLayer != null)
    {
        if (vm.Failed)
        {
            SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
            failedAdorner = new FailedAdorner(this, vm.ErrorMessage);
            adornerLayer.Add(failedAdorner);
        }
        else
        {
            SafeRemoveAll(new List<CustomAdornerBase>() { failedAdorner, busyAdorner });
        }
    }
    //repaint
    InvalidateControl();
}

看这个方法是如何用于在 AdornerLayer 中显示一个 FailedAdorner(如果找到了且不为 null)。最后一块拼图是看看实际的 FailedAdorner 是什么样子的。嗯,它非常简单(有一个 CustomAdornerBase 基类,但我就不让您看了);它看起来像这样:

using System;
using System.Collections;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Collections.ObjectModel;

namespace ThreadingComponent
{
    /// <summary>
    /// Failed adorner that is shown when the threading 
    /// operation fails
    /// </summary>
    public class FailedAdorner : CustomAdornerBase, IResizableAdornerControl
    {
        #region Data
        private FailedUserControl failedUserControl;
        #endregion

        #region Constructor

        public FailedAdorner(FrameworkElement adornedCtrl, String text)
            : base(adornedCtrl)
        {
            failedUserControl = new FailedUserControl();
            failedUserControl.ErrorMessage = text;
            failedUserControl.Margin = new Thickness(0);
            host.Children.Add(failedUserControl);
        }

        #endregion 

        #region IResizableAdornerControl
        public void ResizeToFillAvailableSpace(Size availableSize)
        {
            host.Width = availableSize.Width - 5;
            host.Height = availableSize.Height - 5;
            failedUserControl.ResizeToFillAvailableSpace(availableSize);
        }
        #endregion
    }
}

它所做的只是在 AdornerLayer 中承载一个 FailedUserControl。所以最后一步是看看 FailedUserControl 是什么样子的。嗯,这是它的 XAML:

<UserControl x:Class="ThreadingComponent.FailedUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto" HorizontalAlignment="Stretch" 
             VerticalAlignment="Stretch">
    <Grid Margin="0,3,0,0" HorizontalAlignment="Stretch" 
          VerticalAlignment="Stretch" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <StackPanel Orientation="Horizontal" 
               HorizontalAlignment="Stretch" 
               Background="Black">
            <Image Source="../Images/Failed.png" 
               Height="45" Margin="5"/>
            <Label FontFamily="Arial" FontSize="24" 
               FontWeight="Bold" Content="Failed" 
               HorizontalAlignment="Left" 
               HorizontalContentAlignment="Left" 
               Foreground="White"
               VerticalAlignment="Center" 
               VerticalContentAlignment="Center"/>
        </StackPanel>


        <TextBox Grid.Row="1" 
             TextWrapping="Wrap" BorderThickness="0"
             BorderBrush="Transparent" Background="Transparent"
             IsReadOnly="True" FontSize="16" 
             FontWeight="Bold" 
             HorizontalAlignment="Stretch" 
             VerticalAlignment="Stretch"
             Text="The following error occurred whilst trying to obtain the data:"/>

        <TextBox Grid.Row="2" TextWrapping="Wrap" 
             BorderThickness="0" Margin="0,10,0,0"
             BorderBrush="Transparent" 
             Background="Transparent"
             IsReadOnly="True"
             HorizontalAlignment="Stretch" 
             VerticalAlignment="Stretch"
             Text="{Binding Path=ErrorMessage}"/>
    </Grid>

</UserControl>

可以看出 FailedUserControl 使用 ErrorMessage 属性来显示与线程操作相关的错误。FailedAdorner 响应实际的 ThreadableItemViewModelBase.Failed 属性更改,将 FailedUserControl 上的 ErrorMessage 属性 DP 设置给 FailedUserControl

这是 FailedUserControl 的代码隐藏:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ThreadingComponent
{
    /// <summary>
    /// Control that is shown when the threading 
    /// operation fails
    /// </summary>
    public partial class FailedUserControl : UserControl, IResizableAdornerControl
    {
        #region Constructor
        public FailedUserControl()
        {
            this.DataContext = this;
            InitializeComponent();
        }
        #endregion

        #region DPs
        #region ErrorMessage

        /// <summary>
        /// ErrorMessage Dependency Property
        /// </summary>
        public static readonly DependencyProperty ErrorMessageProperty =
            DependencyProperty.Register("ErrorMessage", 
            typeof(String), typeof(FailedUserControl),
            new FrameworkPropertyMetadata(null));

        /// <summary>
        /// Gets or sets the ErrorMessage property.
        /// </summary>
        public String ErrorMessage
        {
            get { return (String)GetValue(ErrorMessageProperty); }
            set { SetValue(ErrorMessageProperty, value); }
        }

        #endregion
        #endregion

        #region IResizableAdornerControl
        public void ResizeToFillAvailableSpace(System.Windows.Size availableSize)
        {
            this.Height = availableSize.Height;
            this.Width = availableSize.Width;
        }
        #endregion
    }
}

注意:ThreadableItemViewModelBase.IsBusy 的工作方式与此相同,只是它有一个 BusyAdorner 和一个 BusyUserControl

配置

正如我在本文各处所说,所附演示代码的 ThreadableHostControl UserControl 使用了 AdornerLayer,它有时可能会返回 null(例如,如果您正在使用自己的 AdornerDecorator,或者处于某些复杂的控件中,例如 Infragistics 的控件或功能强大的 Ribbon),因此,所附演示代码的 ThreadableHostControl UserControl 可能无法按计划工作。

为了解决这个问题,用户可以选择如何处理,方法是使用 App.Config 设置“shouldThrowExceptionOnNullAdornerLayer”,该设置指示应用程序在无法获取 AdornerLayer 时抛出异常。

如果用户选择将“shouldThrowExceptionOnNullAdornerLayerApp.Config 值设置为“false”,那么线程应该会按预期工作;只是忙碌或失败的装饰器将不会显示,用户将不得不找到其他方法来处理后台线程操作的 IsBusyFailed 状态。

显然,如果此设置保持为“true”并且用户找出 AdornerLayer.GetAdornerLayer(this) 返回 null 的原因,对每个人来说都会更好。

如何在您自己的 WPF 应用中使用此想法

您真正需要做的就是在您的应用程序中使用这一切:

  1. 将图像复制到您的 Images/ 文件夹;如果您将图像存储在其他地方,则需要修改 FailedUserControlBusyUserControl
  2. 将以下文件复制到您自己的应用程序中:
    • BusyAdorner.cs
    • CustomAdornerBase.cs
    • FailedAdorner.cs
    • IResizableAdorner.cs
    • SimpleCommand.cs(如果您想使用 ICommand
    • BusyUserControl.xaml/cs
    • CircularProgressBar.xaml/cs
    • FailedUserControl.xaml/cs
    • ThreadableHostControl.xaml/cs
    • Images 文件夹:将这些放在您喜欢的地方,但请参阅步骤 1
    • BackgroundTaskManager.cs
    • ThreadableItem.cs
    • PropertyObserver.cs
    • ThreadableItemViewModel.cs
    • ThreadableItemViewModelBase.cs
    • ViewModelBase.cs
    • 您可以 **参考** Window1ViewModel 作为基础,但您应该创建自己的 ViewModel 来满足 **您的** 需求。
    • App.config **必须** 包含 AppSettings 键“ShouldThrowExceptionOnNullAdornerLayer”。
  3. 创建正确类型的 ThreadableItemViewModel<T>,并将其作为属性从驱动您视图的 ViewModel 中公开,其中 T 被固定为某种类型。
  4. 创建正确的 ThreadableItemViewModel<T>.TaskFunc 属性值,该值预计为 Func<Dictionary<String,Object>,ThreadableItem<T>> 类型。

限制

不支持取消线程工作项委托。

就这些

目前我只想说这些。我必须说,对我来说,在大型 WPF 应用中这是一个真正的问题,而这段代码将解决我们那个项目上非常实际的问题。如果您也觉得这可能在您的 WPF 应用中有用,您能否花点时间发表评论或投票?非常感谢。

© . All rights reserved.