I am writing refactoring a Silverlight program to consumes a portion of its existing business logic from a WCF service. In doing so, I've run into the restriction in Silverlight 3 that only allow开发者_JAVA技巧s asynchronous calls to WCF services to avoid cases where long-running or non-responsive service calls block the UI thread (SL has an interesting queuing model for invoking WCF services on the UI thread).
As a consequence, writing what once was straightforward, is becoming rapidly more complex (see the code examples at the end of my question).
Ideally, I would use coroutines to simplify the implementation, but sadly, C# does not currently support coroutines as a native language facility. However, C# does have the concept of generators (iterators) using the yield return
syntax. My idea is to re-purpose the yield keyword to allow me to build a simple coroutine model for the same logic.
I am reluctant to do this, however, because I am worried that there may be some hidden (technical) pitfalls that I'm not anticipating (given my relative inexperience with Silverlight and WCF). I am also worried that the implementation mechanism may not be clear to future developers and may hinder rather than simplify their efforts to maintain or extend the code in the future. I've seen this question on SO about re-purposing iterators to build state machines: implementing a state machine using the "yield" keyword, and while it's not exactly the same thing I'm doing, it does make me pause.
However, I need to do something to hide the complexity of the service calls and manage the effort and potential risk of defects in this type of change. I am open to other ideas or approaches I can use to solve this problem.
The original non-WCF version of the code looks something like this:
void Button_Clicked( object sender, EventArgs e ) {
using( var bizLogic = new BusinessLogicLayer() ) {
try {
var resultFoo = bizLogic.Foo();
// ... do something with resultFoo and the UI
var resultBar = bizLogic.Bar(resultFoo);
// ... do something with resultBar and the UI
var resultBaz = bizLogic.Baz(resultBar);
// ... do something with resultFoo, resultBar, resultBaz
}
}
}
The re-factored WCF version becomes quite a bit more involved (even without exception handling and pre/post condition testing):
// fields needed to manage distributed/async state
private FooResponse m_ResultFoo;
private BarResponse m_ResultBar;
private BazResponse m_ResultBaz;
private SomeServiceClient m_Service;
void Button_Clicked( object sender, EventArgs e ) {
this.IsEnabled = false; // disable the UI while processing async WECF call chain
m_Service = new SomeServiceClient();
m_Service.FooCompleted += OnFooCompleted;
m_Service.BeginFoo();
}
// called asynchronously by SL when service responds
void OnFooCompleted( FooResponse fr ) {
m_ResultFoo = fr.Response;
// do some UI processing with resultFoo
m_Service.BarCompleted += OnBarCompleted;
m_Service.BeginBar();
}
void OnBarCompleted( BarResponse br ) {
m_ResultBar = br.Response;
// do some processing with resultBar
m_Service.BazCompleted += OnBazCompleted;
m_Service.BeginBaz();
}
void OnBazCompleted( BazResponse bz ) {
m_ResultBaz = bz.Response;
// ... do some processing with Foo/Bar/Baz results
m_Service.Dispose();
}
The above code is obviously a simplification, in that it omits exception handling, nullity checks, and other practices that would be necessary in production code. Nonetheless, I think it demonstrates the rapid increase in complexity that begins to occur with the asynchronous WCF programming model in Silverlight. Re-factoring the original implementation (which didn't use a service layer, but rather had its logic embedded in the SL client) is rapidly looking to be a daunting task. And one that is likely to be quite error prone.
The co-routine version of the code would look something like this (I have not tested this yet):
void Button_Clicked( object sender, EventArgs e ) {
PerformSteps( ButtonClickCoRoutine );
}
private IEnumerable<Action> ButtonClickCoRoutine() {
using( var service = new SomeServiceClient() ) {
FooResponse resultFoo;
BarResponse resultBar;
BazResponse resultBaz;
yield return () => {
service.FooCompleted = r => NextStep( r, out resultFoo );
service.BeginFoo();
};
yield return () => {
// do some UI stuff with resultFoo
service.BarCompleted = r => NextStep( r, out resultBar );
service.BeginBar();
};
yield return () => {
// do some UI stuff with resultBar
service.BazCompleted = r => NextStep( r, out resultBaz );
service.BeginBaz();
};
yield return () => {
// do some processing with resultFoo, resultBar, resultBaz
}
}
}
private void NextStep<T>( T result, out T store ) {
store = result;
PerformSteps(); // continues iterating steps
}
private IEnumerable<Action> m_StepsToPerform;
private void PerformSteps( IEnumerable<Action> steps ) {
m_StepsToPerform = steps;
PerformSteps();
}
private void PerformSteps() {
if( m_StepsToPerform == null )
return; // nothing to do
m_StepsToPerform.MoveNext();
var nextStep = m_StepsToPerform.Current;
if( nextStep == null ) {
m_StepsToPerform.Dispose();
m_StepsToPerform = null;
return; // end of steps
}
nextStep();
}
There are all sorts of things that need to be improved in the above code. But the basic premise is to factor out the continuation pattern (creating an interception point for exception handling and various checks) while allowing the event-based async model of WCF to drive when each step is performed - basically when the last async WCF call completes. While on the surface this looks like more code, it's worth mentioning that PerformSteps()
and NextStep()
are reusable, only the implementation in ButtonClickCoRoutine()
would change with each different implementation site.
I'm not entirely sure I like this model, and I wouldn't be surprised if a simpler way existed to implement it. But I haven't been able to find one on the "interwebs" or MSDN, or anywhere else. Thanks in advance for the help.
You should definitely look at the Concurrency and Coordination Runtime. It uses iterators for exactly this purpose.
On the other hand, you should also look at Parallel Extensions and its approach to continuations. Parallel Extensions is part of .NET 4.0, whereas the CCR requires separate licensing. I would advise you to go with a framework written by people who eat, breathe and sleep this stuff though. It's just too easy to get details wrong on your own.
The Reactive Extensions for .NET provide a much cleaner model for handling this.
They provide extensions which let you write simple delegates against asynchronous events in a much, much cleaner manner. I recommend looking into them, and adapting them to this situation.
I didn't read your whole thing.
They use this strategy in CCR robotics studio, and a number of other projects use this strategy. An alternative is to use LINQ, see e.g. this blog for a description. The Reactive framework (Rx) is kinda built along these lines.
Luca mentions in his PDC talk that perhaps future version of C#/VB may add async primitives to the language.
In the meantime, if you can use F#, it is a winning strategy. Right now what you can do with F# here blows everything else right out of the water.
EDIT
To quote the example from my blog, suppose you have a WCF client that you want to call a couple methods on. The synchronous version might be written as
// a sample client function that runs synchronously
let SumSquares (client : IMyClientContract) =
(box client :?> IClientChannel).Open()
let sq1 = client.Square(3)
let sq2 = client.Square(4)
(box client :?> IClientChannel).Close()
sq1 + sq2
and the corresponding async code would be
// async version of our sample client - does not hold threads
// while calling out to network
let SumSquaresAsync (client : IMyClientContract) =
async { do! (box client :?> IClientChannel).OpenAsync()
let! sq1 = client.SquareAsync(3)
let! sq2 = client.SquareAsync(4)
do! (box client :?> IClientChannel).CloseAsync()
return sq1 + sq2 }
No crazy callbacks, you can use control constructs like if-then-else, while, try-finally, etc, write it almost exactly like you write straight-line code, and everything work, but now it's async. It's very easy to take a given BeginFoo/EndFoo pair of methods and make the corresponding F# async methods for use in this model.
You may also want to consider Jeffrey Richter's AsyncEnumerator which is part of his 'power threading' library. He worked together with the CCR team to develop CCR. The AsyncEnumerator is according to Jeffrey more 'lightweight' than the CCR. Personally I've played about with AsyncEnumerator but not with the CCR.
I haven't used it in anger though - so far I've found the limitations with using enumerators to implement coroutines too painful. Currently learning F# because of amongst other things async workflows (If I remember the name correctly) which look like they're full fledged coroutines or 'continuations' (I forget the correct name or the exact distinctions between the terms).
Anyway, here's some links:
http://www.wintellect.com/PowerThreading.aspx
Channel 9 video on AsyncEnumerator
MSDN Article
精彩评论