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

解决 Silverlight 异步问题的一个简单方案

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (8投票s)

2009年10月19日

CPOL

15分钟阅读

viewsIcon

25441

downloadIcon

144

一套小巧且可扩展的类,可以优雅地解决在使用 Silverlight 和 WCF 结合时,与异步和事件处理相关的常见问题。

TaskManagerDemo

引言

当开始开发实际的商业 Silverlight 应用程序时,不可避免地会遇到由于用户操作(体现在事件和事件处理中)与从 Silverlight 访问 WCF 服务所固有的异步性交互而产生的问题。本文概述了一种针对此类具体问题的一些通用解决方案。所提出的解决方案具有通用性,并且本质上是可扩展的——这意味着它可以用于解决未来的其他问题,或为解决这些问题提供基础。

在某些方面,此处提出的解决方案预示了 C# 4.0 的一些特性,因此随着 C# 4.0 的发布,它可能会被淘汰。我们并未尝试对长期存在的并行编程难题进行通用解决方案的尝试!然而,此处提出的方法具有在 C# 3.0 中工作的优点,并且相对简单。它在其有限的范围内效果很好,并且至少可以一直使用,直到有更好的通用方法出现。

常见问题

首先,让我们简要概述一下可能遇到的问题集。

  1. 在几种不同情况下,可能需要调用服务方法——此处假设大多数考虑的服务方法都从数据库返回信息——并且由于事件和事件处理的交互,可能会发现同一个服务方法被 UI 调用两到三次,参数集相同。毋庸置疑,由于服务调用相对较高的性能成本,这是不可取的。(在特定情况下,这可能是一个实际问题,也可能不是,但如果足够容易做到,总会希望避免这种情况。)
  2. 在许多情况下,调用一组服务方法是合适的,显示一个旋转器或其他“正在处理”指示符,并且可能阻止访问 UI,直到所有被调用方法的返回结果都被接收,并且 UI 的主要数据相关初始化完成。
  3. 在刚才提到的情况下,累积从服务方法调用序列返回的任何错误消息,并在关闭旋转器后按顺序显示它们,也是有利的。
  4. 另一个有趣的问题源于这样一个事实:尽管服务方法 A 和 B 可以按顺序调用,但它们的结果有时可能以 AB 顺序返回,有时以 BA 顺序返回。可能 A 的结果对于正确处理 B 的结果是必需的,或者以期望的方式处理;在这种情况下,如何确保在必要时推迟处理 B 的结果,直到 A 的结果被接收?

上面提到的前两个问题可以使用布尔变量,或者也许是具有多个值的 `enum` 变量来解决。第三个问题可以通过在列表中累积异常来解决。第四个问题稍微难解决一些。可以通过调用 A 的 `Completed` 事件处理程序中的服务方法 B 来解决,但这可能会产生不良的性能影响,如果 A 和 B 都涉及耗时的数据库查询。(如果 A 的结果对于正确制定 B 的参数是必需的,则此策略变得必要;但在讨论的情况下并非如此。)

底层抽象

事实证明,一个通用的底层抽象将上述所有情况联系起来;正如一位同事曾经指出的,识别和利用通用抽象通常可以解决不仅眼前的问题,而且可以解决一整套相关问题。通过基于底层抽象开发一个类,您可能会发现在该类中可以现成地解决以前未预料到的问题;或者发现解决新问题变得更容易,因为只需要对现有类进行少量扩展,而不是从头开始创建新的自定义解决方案。这实际上就是最初为解决上述前两个问题而发明的类的情况;当第三个问题出现时,通过扩展该类很容易解决,后来第四个问题也通过相对简单的扩展得到了类似的解决。

本质上,所有特定问题共同的根本问题是任务管理。服务方法调用,或一组相邻/相关的服务调用,可以被视为一种任务——注意到一个任务(例如“获取此页面/窗口/面板所需的 数据”)通常可以分解为子任务(“获取数据集 A、B 和 C”)。

请注意,任何任务都经历一组特定的状态。最初,虽然任务已确定并“准备开始”,但它处于 `NotStarted` 状态。启动后,任务会 `InProgress` 一段时间,最后达到 `Completed` 状态。

重复性任务会经历这些状态的循环。对于可以执行多次的任务,`Completed` 状态对于所有意图和目的都等同于 `NotStarted` 状态(前提是执行任务多次是有意义的),只是如果任务 `Completed`,它必须在此之前 `InProgress`。为了完整性,以及为了在多线程环境中可能获得的实际好处,我们添加了 `Unspecified` 状态——也可以称为“未完全初始化”——作为任务的默认初始状态。因此,尽管一个布尔 `InProgress` 状态可能就足够了,但我们发现使用以下 `enum` 来跟踪任务的当前状态更为满意。

public enum TaskStatus
{
    Unspecified = 0,
    NotStarted,
    InProgress,
    Completed
}

值得注意的是,状态转换存在约束;但事实证明,它们无法通过简单的表格显示。约束封装在 `TaskManager` 类的一组属性中(分别为 `CanBeSetUnspecified`、`CanBeSetNotStarted`、`CanBeSetInProgress` 和 `CanBeSetCompleted`),该类将在下文介绍。制定此类约束的方法不止一种,因此属性的定义非常宽松,并标记为 `virtual`,以便在适当的情况下用更严格的定义覆盖它们(反之亦然,如果用户认为它们过于严格,或者只是没有实际用途,它们可以被覆盖以始终返回 `true`)。

除了 `TaskManager` 基类之外,还有几个派生类——第一个是 `AsyncServiceCallManager`,它实际上只是一个别名;第二个是 `QueuedAsyncServiceCallManager`,它添加了一些有用的功能。

TaskManager / AsyncServiceCallmanager 类

每个 `TaskManager` 对象都有一个 `Name` 属性,该属性应为每个 `TaskManager` 实例设置一个唯一的名称。类的唯一构造函数接受一个 `Name` 参数。传递空字符串或 `null` 会导致构造函数生成一个 `GUID` 字符串并将其分配为对象的名称。但是,通常建议分配一个唯一且有意义的用户定义名称。`TaskManager` 对象的 `Name` 在构造后不能更改。

也许 `TaskManager` 对象最重要的属性是它的 `Status`(`TaskStatus` 类型)。`CanBeSet...` 属性使对象的客户端和对象本身能够知道对 `Status` 的特定赋值是否会或将成功。对 `Status` 的无效赋值将静默失败,而不是抛出异常,因此为了清晰起见,强烈建议在尝试赋值之前检查其有效性,如果有可能尝试失败。

在许多情况下,`TaskManager` 对象不会单独使用;相反,将构建一个 `TaskManager` 对象树(通常是一棵小树,尽管支持任意大小的树),反映父任务分解为几个子任务。提供了一系列相当广泛的方法和属性来方便处理 `TaskManager` 树。

方法

  • `AddChild` - 重载接受一个已构造的 `TaskManager` 对象或一个 `TaskManager` 的所需名称作为参数。
  • `AddChildren` - 接受一个已构造的 `TaskManager` 对象数组。
  • `RemoveChild` - 重载接受对象引用或匹配子对象 `Name` 的字符串;尝试删除指定的子对象并返回一个布尔值,指示成功(`true`)或失败(`false`)。
  • `FindChild`、`FindDescendant` - 尝试查找具有指定名称的子对象(仅探测一个层级向下)或后代对象(探测到树的叶子);如果成功则返回对象,否则返回 `null`。
  • `FindFirstChild`、`FindNextChild` - 用于遍历子列表,按顺序返回每个同级对象。`FindNextChild` 以 `TaskManger` 对象(当前子对象)作为参数。
  • `WalkToNextFrom` - 遍历任务树——如果最初以根对象作为参数调用,则每遍历一次后代对象,最后遍历根对象。
  • `ForEach` - 对以当前节点为根的任务子树中的每个成员执行指定的操作。(2010 年 3 月 5 日新增)
  • `GetChildStatus`、`GetDescendantStatus` - 类似于 `FindChild` 和 `FindDescendant`,不同之处在于返回的是对象的 `Status` 而不是对象本身。
  • `SetAllStatus` - 尝试将当前 `TaskManager` 对象及其所有子对象的 `Status` 设置为指定值。
  • 2010 年 3 月 5 日更新,公开了四个 `public` 重载
    • SetAllStatus(TaskStatus status)
    • SetAllStatus(TaskStatus status, bool treatAsRoot)
    • SetAllStatus(TaskStatus status, Predicate<TaskManager> filterPredicate)
    • SetAllStatus(TaskStatus status, bool treatAsRoot, Predicate<TaskManager> filterPredicate)
    请参阅可下载演示项目中的 `DemoNewFeatures` 方法,了解如何使用第二种和第三种重载的示例。

属性

  • `Parent` - 返回当前 `TaskManager` 对象的父对象(如果适用),否则返回 `null`。
  • `RootParent` - 返回当前 `TaskManager` 对象所属树的根。
  • `ChildList` - 以列表形式返回当前 `TaskManager` 对象的子对象。

从上述“自助餐”式的功能中,我们一致选择了 `AddChild`、`AddChildren`、`WalkToNextFrom` 和 `SetAllStatus` 作为我们经常使用的“美味佳肴”;因此,如果存在创建单独的、更精简的 `TaskManagerBase` 类的理由或动机,这些就是属于该基类的功能。

其他重要属性

  • `AutoComplete` - 布尔值,默认为 `false`。如果设置为 `true`,则一旦所有子任务的 `Status` 属性都设置为 `Completed`,对象的 `Status` 将自动设置为 `Completed`。
  • `Tag` - 任何类型的对象,可用于任何目的。迄今为止最受欢迎的用途是累积子任务的错误信息(异常),以便在整个任务集完成时集中处理这些错误。

事件

  • ChildAdded
  • ChildRemoved
  • StatusChanged

我们发现最后一个事件很有用,并且还没有机会使用前两个事件,尽管可以想象它们有用的情况。

前面提到 `AsyncServiceCallManager` 只是 `TaskManager` 的一个别名。碰巧 `TaskManager` 的创建是由于需要管理异步服务方法调用和这些调用的批处理及其结果;然而,`TaskManager` 确实只是一个(可分解的)任务的通用管理器。我们通过提供一个别名解决了这种差异,即需要一个非常具体的名称和一个非常通用的名称。(`AsyncServiceCallManager` 继承自 `TaskManager`,但没有添加新功能。)我们通常使用一种树形结构,其中 `TaskManager` 对象是根对象,而 `AsyncServiceCallManager` 对象是子对象;这准确地表示了这样一个事实,即一组异步服务方法调用在某些方面被集体视为一个单一的复合任务。

在代码中使用 TaskManager

在实践中这是什么样子的?第一组示例代码解决了本文开头提到的**问题 2 和 3**。

这是一组示例声明

private TaskManager initializationTask = new TaskManager("init") { AutoComplete = true };
private AsyncServiceCallManager getProductsTask = 
		new AsyncServiceCallManager("getProducts");
private AsyncServiceCallManager getOrdersTask = new AsyncServiceCallManage("getOrders");

在客户端类的构造函数中,我们可能会找到类似这样的代码

initializationTask.AddChildren(new TaskManager[] { getProductsTask, getOrdersTask });
initializationTask.StatusChanged += 
	new EventHandler<EventArgs>(initializationTask_StatusChanged);
DataLoadingSpinner.Start();
initializationTask.SetAllStatus(TaskStatus.InProgress);
serviceClient.GetProductsAsync(...);
serviceClient.GetOrdersAsync(...);

在这里,我们假设故障已按 此处 描述的方式处理。在这种情况下,上一个示例中一个服务方法调用的 `...Completed` 事件处理程序会是这样的

private void serviceClient_GetProductsCompleted
	(object sender, GetProductsCompletedEventArgs e)
{
    if (e.Error == null)
    {
        // Save result collection locally.
        productsCollection = e.Result;
    }
    else
    {
        getProductsTask.Tag = e.Error;
    }
    getProductsTask.Status = TaskStatus.Completed;
}

……而其他服务方法的结构和细节会非常相似。

当两个获取数据任务都完成后,我们想要进行一些清理工作。为此,我们在 UI 类的构造函数中充实了订阅了父任务 `StatusChanged` 事件的事件处理程序。

private void initializationTask_StatusChanged(object sender, EventArgs e)
{
    if (initializationTask.Status == TaskStatus.Completed)
    {
        DataLoadingSpinner.Stop();
        DisplayTaskErrors(initializationTask);
    }
}

`DisplayTaskErrors` 方法使用 `WalkToNextFrom` 方法遍历任务树,并(至少)显示分配给 `Tag` 属性的每个 `FaultException` 的 `Message` 属性。

如何防止对服务方法的重复调用(**问题 1**)?有时事件处理会导致一系列服务调用,这些调用以相同的参数快速连续地传递给相同的方法;因此,服务参数和结果的序列化和反序列化,以及可能涉及的任何数据库查询,都会被多次执行——不必要地,除了第一次之外。 (这可能是由于您的代码存在缺陷,在这种情况下,更好的解决方案是找出您做得不对的地方并加以修复;在这个例子中,我们将假设您的代码是好的,并且问题仍然存在,或者只是需要一个权宜之计。)为了防止这种重复的服务调用问题发生,我们只需使用一个 `TaskManager` 对象及其 `Status` 属性。假设我们有一个 `LoadData` 方法,它从几个不同的事件处理程序调用

private void LoadData(int ID, string constraint)
{
    if (loadDataTask.Status != TaskStatus.InProgress)
    {
        loadDataTask.Status = TaskStatus.InProgress;
        serviceClient.GetMyDataAsync(ID, constraint);
    }
}

private void serviceClient_GetMyDataCompleted
	(object sender, GetMyDataCompeltedEventArgs e)
{
    ...
    loadDataTask.Status = TaskStatus.Completed;
}

UI 中的事件处理比通过服务进行的数据库往返要快得多,因此即使 `LoadData` 被调用多次,只有第一次调用才会导致对服务方法(`GetMyDataAsync`)的调用。

QueuedAsyncServiceCallManager 类

最后,我们准备解决强制处理两个或多个服务方法的结果的特定顺序的问题(**问题 4**)。对于这个问题,我们使用了一个派生类 `QueuedAsyncServiceCallManager`。该类提供了两个属性和一个方法,扩展了 `TaskManager` 和 `AsyncServiceCallManager` 类。

属性

  • `Prerequisite` - 一个 `TaskManager` 对象;如果分配的值非空,则在处理与此 `QueuedAsyncServiceCallManager` 关联的服务方法的结果之前,必须将分配对象的 `Status` 设置为 `Completed`。
  • `ResultsReturned` - 指示尽管 `TaskManager` 对象的 `Status` 等于 `InProgress`,但实际上已从关联的服务调用返回了结果并已处理。

方法

  • `EnqueueResultHandling` - 用于将结果的处理推迟到 `Prerequisite` 对象的 `Status` 设置为 `Completed` 为止。

一个特殊的 `EventHandler` 子类 `GenericEventHandler` 也参与其中——正如稍后将要说明的。在这种情况下,“通用”指的不是类型泛型,而是 `GenericEventHandler` 作为某个(任意)特定委托 `EventHandler<T>` 实例的占位符的作用。

使用 QueuedAsyncServiceCallManager

没有什么魔法;事实上,需要一些特殊的编码才能使结果处理“可排队”。以下示例修改了我们的第一个示例,以展示 `Prerequisite` 和 `EnqueueResultHandling` 的使用。假设在 UI 代码中进行某些关系连接联接时,产品必须在订单之前处理。(这是否是一个好主意并不重要;这只是一个示例,说明了您可能想做的一件事,它需要同时在 UI 中收到两个结果集。)

首先,我们将后者 `TaskManager` 对象设为一个 `QueuedAsyncServiceCallManager` 对象,并将其 `Prequisite` 属性设置为另一个 `AsyncServiceCallManager` 对象

private TaskManager initializationTask = new TaskManager("init") { AutoComplete = true };
private AsyncServiceCallManager getProductsTask = 
			new AsyncServiceCallManager("getProducts");
private QueuedAsyncServiceCallManager getOrdersTask = 
    new QueuedAsyncServiceCallManager("getOrders") { Prerequisite = getProductsTask };

其他“特殊”代码只与 `...Completed` 事件处理有关

private void serviceClient_GetOrdersCompleted
	(object sender, GetOrdersCompletedEventArgs e)
{
    if (getProductsTask.Status == TaskStatus.Completed)
    // if (getOrdersTask.Prequisite.Status == TaskStatus.Completed) // to the same effect.
    {
        if (e.Error == null)
        {
            ordersCollection = e.Result;
            // HERE do your join(s) or whatever data manipulation 
	   // involves both result sets.
        }
        else
        {
            getOrdersTask.Tag = e.Error;
        }
        getOrdersTask.Status = TaskStatus.Completed;
    }
    else
    {
        getOrdersTask.EnqueueResultsHandling(new GenericEventHandler(), this, e);
    }
}

private void GetOrdersCompletedHelper(object sender, object e)
{
    serviceClient_GetOrdersCompleted(sender, (GetOrdersCompletedEventArgs)e);
}

我希望上述代码能够自学成才,并且读者只需稍加研究就能“理解”它。唯一可能需要的额外信息是,`getOrdersTask` 处理 `getProductsTask.StatusChanged` 事件,当该任务完成后,如果有一个方法已被排队,则该方法将被调用。在这种情况下,该方法将是 `GetOrdersCompletedHelper`,它直接调用 `serviceClient_GetOrdersCompleted`。后者方法随后检测到先决条件任务已完成,并进行需要两个结果集进行处理。

`TaskManager`、`AsyncServiceCallManager` 和 `QueuedAsyncServiceCallManager` 被设计为在单线程环境中运行。在多线程访问它们的情况下,可能需要进行更改才能使其可靠运行。

演示项目

可下载的演示项目是一个游戏(UI 非常简单),要求用户将华盛顿州(美国)的城市名称与其邮政编码相关联。游戏有两种版本,一种比另一种更难。生效的版本是通过源代码中的一个布尔常量选择的。虽然游戏本身可能很有趣,因为它演示了一种实现测验的方法,其中一对集合中的项目通过多对多关系相关联,但它也演示了上述所有问题以及如何使用 `TaskManager`、`AsyncServiceCallManager` 和 `QueuedAsyncServiceCallManager` 类来解决它们。此外,该项目在 `TaskManager` 树的初始构造和首次使用后对其进行了修改,并说明了这样做的一种可能动机:游戏的初始化需要三个协调的服务方法调用,而之后刷新数据只需要两个。

关注点

总之,我们已经看到了如何在一个小类家族中封装解决方案,以解决使用 Silverlight 3.0 和 WCF 服务时可能困扰开发者的某些常见问题。如果您遇到引言中列出的任何常见问题,上述类可能对您有所帮助。

历史

  • 2009 年 10 月 18 日:初始版本
  • 2010 年 3 月 5 日:向 `SetAllStatus` 方法添加了 `Public` 重载,添加了 `ForEach` 方法,并将使用新功能的代码添加到了可下载演示项目中。
© . All rights reserved.