Click here to Skip to main content
15,884,176 members
Articles / Programming Languages / C#

A Simple Solution to Some Problems with Asynchrony in Silverlight

Rate me:
Please Sign up or sign in to vote.
4.69/5 (8 votes)
9 Mar 2010CPOL15 min read 24.9K   128   25   2
A small, extensible suite of classes that elegantly solve some common problems involving asynchrony and event handling that tend to occur when Silverlight and WCF are used together.
TaskManagerDemo

Introduction

When one begins developing real-world Silverlight applications for business, one inevitably faces problems that arise from user actions, reflected in events and event handling, interacting with the necessarily asynchronous nature of accessing WCF services from Silverlight. This article outlines a general solution to a few specific problems of this kind. The solution presented is of a general nature and is inherently extensible - suggesting that it may be used to solve, or to provide a foundation for the solution of additional problems that may arise in the future.

In some respects, the solution presented here anticipates features of C# 4.0 and thus it may be rendered obsolete with the release of C# 4.0. We did not attempt a general solution to the difficult, long-standing problems of parallel programming! However, the approach presented here has the advantages of working in C# 3.0 and being relatively simple. It works very well within its limited domain and can serve at least until a better approach becomes generally available.

Common Problems

First, let's briefly survey a set of problems that may be encountered.

  1. It may be appropriate to call a service method – here assuming that most of the service methods under consideration return information from a database – under several different circumstances, and as a result of the interactions of events and event handling, one may find the same service method being called from the UI two or three times in rapid succession, with the same set of arguments. Needless to say, this is undesirable due to the relatively high performance cost of service calls. (In a particular situation, this may or may not be a problem from a practical perspective, but it would always be preferable to prevent this sort of thing if it were sufficiently easy to do so.)
  2. In many situations, it is appropriate to call a set of service methods, displaying a spinner or some other "processing" indicator, and perhaps preventing access to the UI, until the results of all of the methods called have been received and the main data-related initializations of the UI are completed.
  3. In the situation just mentioned, it is also advantageous to accumulate any error messages returned from the sequence of service method calls and display them in sequence after turning off the spinner.
  4. Yet another interesting problem arises from the fact that although service methods A and B may be called in that order, their results may sometimes be returned in AB order and sometimes in BA order. It may be that the results of A are required in order to process the results of B correctly, or in the desired manner; in such cases, how can one ensure that processing the results of B is postponed, if necessary, until the results of A have been received?

The first two problems mentioned above can be solved using Boolean variables, or perhaps enum variables with more than two values. The third problem can be solved by accumulating exceptions in a list. The fourth problem is a bit more difficult to solve. It is possible to call service method B from the Completed event handler of A, but this can create an undesirable performance hit if A and B both involve long-running database queries. (If the results of A are required in order to correctly formulate the arguments to B, this strategy becomes necessary; but it's not necessary in the situation under discussion.)

Underlying Abstraction

It turns out that a common underlying abstraction relates all of the situations described above; and as a colleague once pointed out to me, identifying and leveraging a common abstraction can often enable one to solve not only the problem(s) at hand, but an entire set of related problems. Developing a class based on the underlying abstraction, one may find in such a class ready-made solutions to previously unforeseen problems; or find that the solution to a new problem is made easier because only a small extension to the existing class is required, as opposed to "starting from scratch" with creating a new, custom solution. This actually turned out to be the case with the class originally invented to solve the first two problems above; when the third problem arose, it was easily solved by extending the class, and later the fourth problem was similarly solved through relatively simple extensions.

Essentially, the root problem, common to all of the specific problems mentioned, is one of managing tasks. A service method call, or a set of adjacent / related service calls, can be thought of as a type of task - noticing that a task (such as "get required data for this page / window / panel") can often be decomposed into sub-tasks ("get data sets A, B and C").

Please notice also that any task goes through a set of specific states. Initially, although the task has been identified and is "ready to start," it is in a NotStarted state. After being initiated, the task is InProgress for a period of time, and finally it reaches a Completed state.

A repetitive task goes through a cycle of these states. For a task that can be performed more than once, he Completed state is, for all intents and purposes, equivalent to the NotStarted state (provided that it makes sense to perform a task more than once) except that if a task is Completed it must have previously been InProgress. For completeness, and for some potential practical benefit in multi-threaded environments, we add the Unspecified state – which could also be termed "incompletely initialized" – as a task's default initial state. Thus, although it's arguable that a Boolean InProgress state might be sufficient, we find it more satisfying to use the following enum to track a task's current state:

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

It's worthwhile to note that constraints on state transitions exist; but it turns out that they cannot be shown via a simple table. The constraints are embodied as a set of properties (CanBeSetUnspecified, CanBeSetNotStarted, CanBeSetInProgress and CanBeSetCompleted, respectively) in the TaskManager class, to be introduced next. There is more than one way to formulate such constraints, so the properties are defined in a very lax way and marked virtual so that they can be overridden with stricter definitions if appropriate (or conversely, they could be overridden to always return true if a user deemed them to be overly restrictive, or just of no practical use).

In addition to the TaskManager base class, there are a couple of derived classes – the first of which, AsyncServiceCallManager, is really just an alias; the second, QueuedAsyncServiceCallManager, adds some useful functionality.

TaskManager / AsyncServiceCallmanager Class(es)

Every TaskManager object has a Name property that should be set to a unique name for each TaskManager instance. The class's only constructor takes a Name argument. Passing an empty or null string causes the constructor to generate a GUID string and assign it as the object's name. However, assigning a unique and meaningful user-defined name is generally recommended. A TaskManager object's Name cannot be changed after construction.

Perhaps the most important property of a TaskManager object is its Status (of type TaskStatus). The CanBeSet... properties enable the object's client(s), and the object itself, to know whether a particular assignment to Status would or will succeed. An invalid assignment to Status will fail quietly, rather than throw an exception, so for clarity it is strongly recommended to check an assignment's validity before attempting the assignment, if there is any possibility that the attempt will fail.

In many cases, a TaskManager object will not be used alone; instead, a tree of TaskManager objects (usually a small tree, although trees of arbitrary size are supported) will be constructed, reflecting the breakdown of a parent task into several child sub-tasks. A pretty extensive list of methods and properties are provided to facilitate working with a TaskManager tree:

Methods

  • AddChild - Overloads accept as argument either an already-constructed TaskManager object or the desired Name for a TaskManager to be constructed.
  • AddChildren - Accepts an array of already-constructed TaskManager objects.
  • RemoveChild - Overloads accept either an object reference or a string matching a child object's Name; attempts to remove the designated child object and returns a Boolean value indicating success (true) or failure (false).
  • FindChild, FindDescendant - Attempts to find a child (probing only one level downward) or descendant (probing to the leaves of the tree) object with the specified name; returns the object if successful, otherwise null.
  • FindFirstChild, FindNextChild - Used to traverse the list of children, returning each sibling object in succession. FindNextChild takes a TaskManger object (the current child) as argument.
  • WalkToNextFrom - Walks the task tree - returning each descendant object and lastly the root of the tree exactly once, if initially called with the root object as argument.
  • ForEach - Executes a specified action on each member of the task sub-tree rooted in the current node. (New as of 5th March, 2010)
  • GetChildStatus, GetDescendantStatus - Similar to FindChild and FindDescendant, except that the object's Status is returned rather than the object itself.
  • SetAllStatus - Attempts to set the Status of the current TaskManager object and all of its children to the specified value.
  • Updated 5th March, 2010 to expose four public overloads:
    • SetAllStatus(TaskStatus status)
    • SetAllStatus(TaskStatus status, bool treatAsRoot)
    • SetAllStatus(TaskStatus status, Predicate<TaskManager> filterPredicate)
    • SetAllStatus(TaskStatus status, bool treatAsRoot, Predicate<TaskManager> filterPredicate)
    See the DemoNewFeatures method in the downloadable demo project for an example of how to use the second and third overloads in particular.

Properties

  • Parent - Returns the current TaskManager object's parent object, if applicable, otherwise null.
  • RootParent - Returns the root of the tree to which the current TaskManager object belongs.
  • ChildList - Returns the children of the current TaskManager object as a list.

From the above "buffet" of features, we have consistently selected AddChild, AddChildren, WalkToNextFrom and SetAllStatus as the "tasty morsel" items we use consistently; so if there were reasons or motivation to create a separate, leaner TaskManagerBase class, those are the features that belong in that base class.

Additional Important Properties

  • AutoComplete - Boolean, false by default. If set to true, the object's Status will be set to Completed automatically as soon as all of its child tasks' Status properties are set to Completed.
  • Tag - Just an object that can be set to anything, for any purpose. Its most popular use thus far is to accumulate error information (exceptions) for child tasks so that the errors can be dealt with in a batch when the entire set of tasks has been completed.

Events

  • ChildAdded
  • ChildRemoved
  • StatusChanged

We have found the last event to be useful, and haven't yet had occasion to use the first two, although it is possible to imagine situations in which they would be useful.

It was mentioned above that AsyncServiceCallManager is just an alias for TaskManager. It happens that the creation of TaskManager was specifically motivated by a need to manage asynchronous service method calls and batches of such calls, and their outcomes; and yet, TaskManager really is just a general manager for (de-)composable tasks. We resolved this dissonance, involving the need for a very specific name and also a very general one, by simply providing an alias. (AsyncServiceCallManager derives from TaskManager but adds no new functionality.) We commonly use a tree structure in which a TaskManager object is the root object and AsyncServiceCallManager objects are the children; this accurately represents the fact that a set of async service method calls is being collectively treated, in some respects, as a single composite task.

Using TaskManager in Code

What does this look like in practice? The first set of example code addresses problems 2 and 3 mentioned at the beginning of this article.

Here's a set of example declarations:

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

In the client class' constructor, we might find code like this:

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

We're assuming here that faults are handled as described here. In that case, the ...Completed event handlers for one of the service method calls made in the previous example would look something like this:

C#
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;
}

... and the structure and details of the other service method would be very similar.

We want to do some cleanup when both data-getting tasks have been completed. For that purpose, we flesh out the event handler that is subscribed to the parent task's StatusChanged event in the UI class' constructor:

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

The DisplayTaskErrors method uses the WalkToNextFrom method to walk the task tree and (at minimum) display the Message property of each FaultException found assigned to a Tag property.

How does one prevent redundant calls to a service method (problem 1)? Sometimes event handling results in several service calls, in rapid sequence, that pass the same arguments to the same method(s); so the serializing and deserializing of arguments to and results from the service, as well as whatever database query(s) may be involved, gets done several times - unnecessarily, all times but the first. (This might be due to flaws in your code, in which case the better solution would be to find out what you're doing wrong and fix it; for this example, we will assume that your code is fine and the problem is still happening, or simply that an expedient solution is desired.) To prevent this redundant service call problem from occurring, we simply use a TaskManager object with its Status property. Let's assume that we have a LoadData method that is called from several different event handlers:

C#
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;
}

Event handling in the UI proceeds much more rapidly than the round trip to the database through the service, so even if LoadData gets called several times, only the first call will result in a call to the service method (GetMyDataAsync).

QueuedAsyncServiceCallManager Class

Finally, we are ready to tackle the problem of enforcing a particular order on the processing of the results of two or more service methods (problem 4). For this issue, we use a derived class, QueuedAsyncServiceCallManager. This class offers two properties and a method that expand on the capabilities of the TaskManager and AsyncServiceCallManager classes.

Properties

  • Prerequisite - A TaskManager object; if a non-null value is assigned, the assigned object's Status must be equal to Completed before the results of the service method associated with this QueuedAsyncServiceCallManager will be processed.
  • ResultsReturned - Indicates that although a TaskManager object's Status equals InProgress, in fact results have been returned from the associated service call and have been processed.

Method

  • EnqueueResultHandling - Used to defer the processing of results until the Prerequisite object's Status is set to Completed.

A special EventHandler sub-class, GenericEventHandler, also gets involved - as will be illustrated shortly. In this case, "generic" refers not so much to type genericity as to the role that a GenericEventHandler plays as a stand-in for an instance of some (any) specific delegate EventHandler<T>.

Using QueuedAsyncServiceCallManager

No magic is involved; in fact, some special coding is required to make results processing "enqueue-able." The following example modifies our first example to show how Prerequisite and EnqueueResultHandling are used. Let's say that products must be processed prior to orders because we are doing some relational joining of the result sets on-the-fly in the UI code. (Whether this is a good idea or not isn't relevant here; it's just an example of something that one might want to do that would require both result sets to have been received in the UI.)

First, we will make the latter TaskManager object a QueuedAsyncServiceCallManager object and assign the other AsyncServiceCallManager object to its Prequisite property:

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

The only other "special" bits of code relate to the ...Completed event handling:

C#
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);
}

I hope that the above code is self-explanatory and that the reader will "get it" with just a little bit of study. The only additional bit of information that may be needed is that getOrdersTask handles the getProductsTask.StatusChanged event and when that task is completed, if a method has been enqueued, that method is called. In this case, that method would be GetOrdersCompletedHelper, which directly calls serviceClient_GetOrdersCompleted. The latter method then detects that the prerequisite task has been completed and does its processing which requires both result sets.

TaskManager, AsyncServiceCallManager and QueuedAsyncServiceCallManager are designed to be run in a single-threaded environment. Changes may be required for them to work dependably in a situation where they are accessed by multiple threads.

The Demo Project

The downloadable demo project is a game (with a very simple UI) that asks the user to correlate city names (in the State of Washington, USA) with their postal ZIP Codes. There are two versions of the game, one more difficult than the other. The version in effect is selected via a Boolean constant within the source code. Although the game may be interesting in itself because it demonstrates a way to implement a quiz in which items in a pair of sets are related to each other via a many-to-many relationship, it also demonstrates all of the problems mentioned above and how to solve them using the TaskManager, AsyncServiceCallManager and QueuedAsyncServiceCallManager classes. Additionally, the project modifies a TaskManager tree after its initial construction and first use, and illustrates one possible motivation for doing so: the game's initialization requires three coordinated service method calls, whereas refreshing data thereafter requires only two.

Points of Interest

In summary, we've seen how to encapsulate in a small family of classes the solutions to certain common problems that can bedevil developers using Silverlight 3.0 with WCF services. If you encounter any of the common problems listed in the Introduction, the classes described above may be helpful to you.

History

  • 18th October, 2009: Initial version
  • 5th March, 2010: Public overloads added to SetAllStatus method, ForEach method added, and code exercising the new features added to the the downloadable demo project.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Concur
United States United States
George Henry has worked as a software developer for more than 20 years. He is currently employed by Concur in Bellevue, Washington, USA.

Comments and Discussions

 
Question.NET 4 ? Pin
Bizounours19-May-10 22:41
Bizounours19-May-10 22:41 
AnswerRe: .NET 4 ? Pin
George Henry 195420-May-10 6:31
George Henry 195420-May-10 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.