|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
Contents
IntroductionI like to create and explore, and Silverlight offers an interesting opportunity to explore a new environment; one in which some things, like the asynchronous web service model, work a little differently than we are used to when developing for the desktop CLR. So you may have heard that synchronous web service calls are not possible in Silverlight 2. No more synchronous web request, Calling Web Services Synchronously, omg Silverlight asynchronous is evil, Consuming services using Silverlight 2, Silverlight 2 Hangs..., Call the Service Asynchronously Well, in fact, they are. In this article I'll show you how to perform synchronous web service calls, and introduce you to some types that make performing synchronous calls with generated channel proxies a breeze. I'll also provide a quick overview of the Microsoft Silverlight Unit Testing framework. Some readers may initially protest that synchronous web service calls have no place in a principally GUI application, because it risks blocking the UI thread, leading to an unresponsive UI etc. And while I agree that it's critical not to block the UI thread as to maintain an application's responsiveness, I do believe there is a legitimate case for synchronous web service calls on non-UI threads. You see, after I ported some of my recent projects from 1.1 to Silverlight 2, I found that not only was the asynchronous model difficult and unnecessary in many situations, it also meant having to rethink the architecture in order to make them async-compatible. For complex scenarios, where background processing is employed, the mandatory asynchronous model is inelegant. In essence, if one is not working on the UI thread, then why not have synchronous service calls at one's disposal? Certainly the desktop CLR allows their use (think WPF XBAP), why not the Silverlight CLR? There is a good synopsis of the for and against arguments here. It was only after some experimentation that I hit upon the techniques that I am going to share with you in this article. Silverlight WCF RecapUntil recently, with Silverlight 1.1, synchronous web service calls were indeed supported. Visual Studio generated the synchronous methods in the channel proxies and interfaces. There was one catch however: any web service call could only take place on the UI thread; otherwise an So what is happening here? Well, Silverlight 2 web services still have thread affinity with the UI thread, it's just now there is some behind the scenes processing taking place. When a web service call is made, it is placed in a queue to be actioned by the UI thread. Let's look at how web service calls are generally done in Silverlight 2. Firstly, we generate a proxy by creating a service reference. We then subscribe to the var client = new SimpleServiceClient();
client.GetGreetingCompleted += ((sender, e) => { DisplayGreeting(e.Result); });
client.GetGreetingAsync("SimpleServiceClient");
When the Turning Asynchronous Calls SynchronousThat's all well and good, but what if we are developing a set of class libraries in a disjoint and modular fashion, with contracts that haven't been written with asynchronous execution in mind, and where return values are expected? It's quite a constraint, and one that may force us to violate implementation visibility by requiring consumers to know that they shouldn't expect to receive a return value, because a web service call may occur. Clearly being able to perform synchronous service calls is a useful thing. Their omission was not to prevent browser lockups but was, according to Peter Bromberg, to allow for cross-browser support as Silverlight needs to implement the NPAPI plugin model. First AssumptionsYou may, like the author did, assume that you could simulate a synchronous web service call by simply using e.g. a ManualResetEvent to block until a result is achieved. This incorrect approach is demonstrated in the following excerpt. (Don't use this code) var resetEvent = new ManualResetEvent(false);
var client = new SimpleServiceClient();
client.GetGreetingCompleted += ((sender, e) =>
{
DisplayGreeting(e.Result);
resetEvent.Set();
});
client.GetGreetingAsync("SimpleServiceClient");
resetEvent.WaitOne(); /* This will block indefinately. */
Unfortunately this doesn't get us anywhere. The reason is that the instigation of the service call must be performed on the UI thread. When we call To further demonstrate this point, let's take a look at our previous example, but with a slight modification: var client = new SimpleServiceClient();
client.GetGreetingAsync("SimpleServiceClient");
/* Sleep for a moment to demonstrate that the call doesn't occur until later. */
Thread.Sleep(1000);
/* Subscribing to the event on the UI thread, after the service call, still works! */
client.GetGreetingCompleted += ((sender, e) => { DisplayGreeting(e.Result); });
Notice that the subscription to the GetGreetingCompleted event occurs after the actual GetGreetingAsync call. The result, however, is the same as before; the late subscription does not prevent the GetGreetingCompleted handler from being called. What is happening here is that the web service call is queued. Nothing happens until the UI thread has a chance to invoke it. The
We can see now why blocking the UI thread doesn't work, and it's a good thing that it doesn't. So, what's the answer? Well, we can't call the service synchronously from the UI thread. But, we can call it in a synchronous manner from a non-UI thread using a ThreadPool.QueueUserWorkItem(delegate
{
var channelFactory = new ChannelFactory<ISimpleService>("*");
var simpleService = channelFactory.CreateChannel();
var asyncResult = simpleService.BeginGetGreeting("Daniel", null, null);
string greeting = null;
try
{
greeting = simpleService.EndGetGreeting(asyncResult);
}
catch (Exception ex)
{
DisplayMessage(string.Format(
"Unable to communicate with server. {0} {1}",
ex.Message, ex.StackTrace));
}
DisplayGreeting(greeting);
});
The So, we see that care must be taken in order to avoid making the mistake of attempting this technique from the UI thread. If this happens, our Silverlight application will hang. Unfortunately the Enter the SynchronousChannelBroker. The SynchronousChannelBroker is a class I have written that links all the steps together, and makes performing synchronous web service calls easy and safe. To assist in adapting dynamic service channels, I have created several public delegate IAsyncResult BeginAction<TActionArgument1>( TActionArgument1 argument1, AsyncCallback asyncResult, object state); Much like the generic So why can't we build a 'SynchronousChannelFactory' that will use the method signatures present in the service contract? Well we could, but it would be a quite a task. Visual Studio's code generator produces interfaces that use the aync pattern " In addition to providing us with the means to perform synchronous web service calls, I have also included a var simpleService = ChannelManager.Instance.GetChannel<ISimpleService>(); string result = string.Empty; try { /* Perform synchronous WCF call. */ result = SynchronousChannelBroker.PerformAction<string, string>( simpleService.BeginGetGreeting, simpleService.EndGetGreeting, "there"); } catch (Exception ex) { DisplayMessage(string.Format("Unable to communicate with server. {0} {1}", ex.Message, ex.StackTrace)); } DisplayGreeting(result); Of course it's all strongly typed, and the generic types must match the parameter types in the service interface methods. In the above example we retrieve the
The public static TReturn PerformAction<TReturn, TActionArgument1>( BeginAction<TActionArgument1> beginAction, EndAction<TReturn> endAction, TActionArgument1 argument1) { EnsureNonUIThread(); var beginResult = beginAction(argument1, null, null); var result = endAction(beginResult); return result; } We see that in order to prevent the service call from happening on the UI thread, we call The three main classes of interest are shown in the following diagram.
UISynchronizationContextThe included Sometimes we might like to use the UISynchronizationContext.Instance.Initialize(Dispatcher); The Managing Channels in an Efficient MannerAs mentioned earlier, the
Caching of the channel is done in the following manner within the readonly Dictionary<Type, object> channels = new Dictionary<Type, object>(); readonly object channelsLock = new object(); public TChannel GetChannel<TChannel>() { Type serviceType = typeof(TChannel); object service; lock (channelsLock) { if (!channels.TryGetValue(serviceType, out service)) { /* We don't cache the factory as it contains a list of channels * that aren't removed if a fault occurs. */ var channelFactory = new ChannelFactory<TChannel>("*"); service = channelFactory.CreateChannel(); var communicationObject = (ICommunicationObject)service; communicationObject.Faulted += OnChannelFaulted; channels.Add(serviceType, service); communicationObject.Open(); /* Explicit opening of the channel * avoids a performance hit. */ ConnectIfClientService(service, serviceType); } } return (TChannel)service; } When a channel faults we remove it from the cache as shown in the following excerpt: /// <summary>
/// Called when a channel faults.
/// Removes the channel from the cache so that it is
/// replaced when it is next required.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="System.EventArgs"/>
/// instance containing the event data.</param>
void OnChannelFaulted(object sender, EventArgs e)
{
var communicationObject = (ICommunicationObject)sender;
communicationObject.Faulted -= OnChannelFaulted;
lock (channelsLock)
{
var keys = from pair in channels
where pair.Value == communicationObject
select pair.Key;
/* Remove all items matching the channel.
* This is somewhat defensive as there should only be one instance
* of the channel in the channel dictionary. */
foreach (var key in keys.ToList())
{
channels.Remove(key);
}
}
}
Functional Programming with Known Service Contract Method SignaturesIf the service channel contains the method signatures Assuming that we will mostly be looking to use generated proxies, I wanted to provide a way to call a method to initiate the service channel when the channel is created. To do this I chose to use reflection to look for a method called /// Attempts to excercise the <code>IServiceContract.InitiateConnection</code> method
/// on the specified service if that service is an <code>IServiceContract</code>.
/// That is, if the service has a method with the signature InitiateConnection(string),
/// it will be invoked in a functional manner.
/// </summary>
/// <param name="service">The service to attempt a connection.</param>
/// <param name="serviceType">Type of the service for logging purposes.</param>
/// <exception cref="TargetInvocationException">Occurs if the service implements <code>IServiceContract</code>,
/// and the call to ConnectFromClient results in a <code>TargetInvocationException</code></exception>
void ConnectIfClientService(object service, Type serviceType)
{
var beginMethodInfo = serviceType.GetMethod("BeginInitiateConnection");
if (beginMethodInfo == null)
{
return;
}
beginMethodInfo.Invoke(service, new object[] { ChannelIdentifier, new AsyncCallback(ar =>
{
var endMethodInfo = serviceType.GetMethod("EndInitiateConnection");
if (endMethodInfo == null)
{
return;
}
try
{
var result = (string)endMethodInfo.Invoke(service, new object[] {ar});
Debug.WriteLine("Connected from client successfully. Result: " + result);
/* TODO: Do something with the result such as log the it somewhere. */
}
catch (InvalidCastException)
{
/* TODO: log that web server has invalid ConnectFromClient signature. */
}
}), null });
}
The Microsoft Silverlight Testing FrameworkProvided herewith is a brief summary of my experience so far with the Microsoft Silverlight Unit Testing Framework. I have been pleased by the quality of the framework, and the Silverlight developers have eaten their own dog food, as they say, with this one. We will not go into too much detail on how to use the unit testing framework, but I thought it worth briefly discussing how to set it up and perform asynchronous unit testing with it, since that is directly relevant. Getting Started with the Microsoft Silverlight Unit Testing FrameworkThe framework comes in the form of some Visual Studio templates. I must admit that I was a bit lazy installing the templates, and rather chose to create a new Silverlight application, reference the assemblies shown below, and amend the Application_Startup method in the App.xaml.
void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = UnitTestSystem.CreateTestPage();
}
Once this was done, I was away. Asynchronous TestsAs it turns out, writing unit tests for methods invoked asynchronously is easy with the Silverlight Testing tools. To accomplish asynchronous testing the [TestMethod]
[Asynchronous]
public void InvokeSynchronouslyShouldPerformActionSynchronouslyFromNonUIThread()
{
ThreadPool.QueueUserWorkItem(delegate
{
CallInvokeSynchronouslyWithAction();
TestComplete();
});
}
When running the test project we see the results in the browser window.
ConclusionFor whatever reason, I believe the Silverlight team has taken the right approach with synchronous web service calls. While synchronous web service calls are not prohibited, they will cause ones application to become unresponsive if performed from the UI thread. Thus, we have seen that there is some missing infrastructure that really should be there to prevent us from stepping on our own toes. We have, however, come some way to address the problem in the included download. In some ways we get the best of both worlds, blocking the UI thread is prohibited, yet we can still fallback to our familiar synchronous model when using background processing. I would like to see the inclusion of a standard mechanism for performing synchronous web service calls from non-UI threads, as there is a legitimate case for them. Until then, please feel free to use the library provided with this article. I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better. HistoryNovember 2008
| ||||||||||||||||||||