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.
- 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.)
- 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.
- 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.
- 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:
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:
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:
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:
private void serviceClient_GetProductsCompleted
(object sender, GetProductsCompletedEventArgs e)
{
if (e.Error == null)
{
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:
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:
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:
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:
private void serviceClient_GetOrdersCompleted
(object sender, GetOrdersCompletedEventArgs e)
{
if (getProductsTask.Status == TaskStatus.Completed)
{
if (e.Error == null)
{
ordersCollection = e.Result;
}
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.