漫谈Silverlight(1)封装异步操作
承接上篇,继续来讨论异步操作的话题。
在上一篇的讨论中,有园友提出了Service的粒度问题,我觉得说的很有道理,对于Silverlight的RIA应用,合理的设计Service的接口,控制粒度也是与传统的编程思路有差异的地方,对原子操作的合并再发布对于RIA的Service设计是很必要的,我把它称作合理的重用其它Service,关于这方面的经验,还请大家积极讨论分享设计经验,我这全当是抛砖引玉了。
言归正传,既然我们无可避免要适应异步的编程模型,那么让我们来尝试将异步操作封装的更加易于操作吧。我认为异步操作最麻烦的就是异步上下文的传递,在一段逻辑上连续的操作中,通常伴随着“全局”变量,这个“全局”变量可能在某个Page上或者某个UserControl上,亦或就是在某个静态变量上,这种类似的编程经验让我们闻到了一种危险的味道,大量“全局”的变量会使得我们的程序越来越难理解和维护,这也促使我决定改造ServiceClient的使用,下面分享我的一些拙见。
微软为我们提供的ServiceClient是基于事件的异步模型,这点让我感觉使用起来很不爽,我比较习惯于callback的编程模式,事件是多播的,使用不当很容易造成多次订阅,我的一个同事就犯过类似的错误,而且对于返回值的使用也让我很不爽,我实在搞不懂微软为什么要这样暴露ServiceClient,我希望的使用模式类似于这样:(以上篇的GetTaskAsync为例)
1: //封装方式
2: private void GetTaskAsync(string taskId, Action<Task> callback);3: //调用方式
4: private void Test4()
5: {6: TestServiceClient client = new TestServiceClient();7: client.GetTaskAsync("task id", (task) =>
8: {9: //work with task
10: });11: }
service中的N个参数作为客户端相应Async方法的前N的参数保持一致(N可以为0),另外再加一个Action<返回值>作为最后一个参数(如果service方法中无返回值则为Action),而Async方法的返回值始终为void,我这样封装的一个理由是在Action的回调中我可以继续用类似的方式调用Service,而整个上下文在各个操作中都是共享的:
1: private void Test4()
2: {3: TestServiceClient client = new TestServiceClient();4: object bizObject = new object();5: client.GetTaskAsync("task id", (task) =>
6: {7: //bizObject.xxx()
8: client.DoWorkAsync(() =>9: {10: //bizObject.yyy()
11: client.DoOtherWorkAsync(() =>12: {13: //bizObject.zzz()
14: });15: });16: });17: }
我个人比较喜欢这样的代码,虽然要注意.Net环境下的“假上下文”所产生的副作用问题(参见老赵的这篇文章“警惕匿名方法造成的变量共享”),但这段代码看起来更像是“同步”操作,代码似乎在连续的执行,你可以在每个Action的开始打上断点连续的调试,给你的感觉代码就像是在顺序执行一样,注意只是感觉上是,那我们应该怎么样实现呢?
1: private void GetTaskAsync(string taskId, Action<Task> callback)2: {3: TestServiceClient client = new TestServiceClient();4: client.GetTaskCompleted += (s, args) =>5: {6: callback(args.Result);7: };8: client.GetTaskAsync(taskId);9: }
这样看起来似乎是那么回事了,跟我们最终想要的形式很接近了,但我们总不能为每一个Service操作写上这样一段代码吧,你会说可以把某一个Service的操作封装到一个单独的类中,然后类中都是这样的代码,暴露这样的接口,像这样:
1: public class MyTestServiceClient2: {3: private TestServiceClient client;
4: public MyTestServiceClient()
5: {6: TestServiceClient client = new TestServiceClient();7: client.GetTaskCompleted += new EventHandler<GetTaskCompletedEventArgs>(client_GetTaskCompleted);8: }9: private void client_GetTaskCompleted(object sender, GetTaskCompletedEventArgs e)
10: {11: var action = e.UserState as Action<Task>;12: if (action != null) action(e.Result);
13: }14: public void GetTaskAsync(string taskId, Action<Task> callback)15: {16: client.GetTaskAsync(taskId, callback);17: }18: //...
19: }
这样看起来比较舒服了,我们可以用MyTestServiceClient替代TestServiceClient来实现上面的代码,可同时我发现这要做大量重复的Coding,我为每个Service接口订阅事件处理函数,事件处理函数里面处理callback,再暴露每个Async调用的接口方法,我们似乎在做svcutil.exe该做的事情,有人说可以啊!就做代码生成,有兴趣的童鞋可以试试这个:)
既然是重复的事情就可以找到规律,就可以让我们找到方法再次封装,受到上篇文章中提到的封装同步的代码的启发,我想到了泛型,还想到了反射,先看看这个牛人是如何封装他所谓的在异步中同步的代码的:
1: void CallWcfSynchronouslyUsingChannelManager()2: {3: var simpleService = ChannelManager.Instance.GetChannel<ISimpleService>();4: string result = string.Empty;5: try6: {7: /* Perform synchronous WCF call. */
8: result = SynchronousChannelBroker.PerformAction<string, string>(9: simpleService.BeginGetGreeting, simpleService.EndGetGreeting, "there");
10: }11: catch (Exception ex)12: {13: DisplayMessage(string.Format("Unable to communicate with server. {0} {1}",14: ex.Message, ex.StackTrace));15: }16: DisplayGreeting(result);17: }
可以看到,他的封装方式是:TReturn PerformFunction<TArg1,TArg2,…>(Service.Beging…,Service.End…,TArg1 arg1,TArg2 arg2,…);
而我的封装方式也是类似的:void PerformFunction<TArg1,TArg2,…>(Service.…Async,TArg1 arg1,TArg2 arg2,…,Action<TReturn> callback);
并且为了适应任意的Service我将基类做成了泛型类并命名为ServiceProxy:
1: public class ServiceProxy<T> where T : ICommunicationObject2: {3: private T serviceClient;
4: public ServiceProxy(T serviceClient)
5: {6: this.serviceClient = serviceClient;7: }8: public T ServiceClient
9: {10: get
11: {12: return this.serviceClient;13: }14: }15: }
最简单的情况,如果被封装的ServiceClient包含一个无参数无返回值的方法,则在ServiceProxy中的对外接口是这样的:
1: public void PerformFunction(Action function, Action<Exception> callback)2: {3: var func = GetFunc(function);
4: var userState = callback;5: func.Invoke(function.Target, new object[] { userState });
6: }
代码很少,关键的代码在于GetFunc方法中,要从传入的function中提取所有的信息,首先要找到最后一个参数为object UserState的同名方法:
1: private static MethodInfo GetMethodWithUserState(Delegate function)2: {3: var paramTypes = function.Method.GetParameters()
4: .Select(info => info.ParameterType)
5: .Concat(new Type[] { typeof(object) })6: .ToArray();7: return function.Target.GetType()
8: .GetMethod(function.Method.Name, paramTypes);
9: }
然后是获得该方法的Completed事件以及事件相应的委托:
1: private static Tuple<EventInfo, Delegate> GetEventAndHandler(Delegate function)2: {3: var srcMethod = function.Method;
4: var methodName = srcMethod.Name;5: if (!methodName.EndsWith("Async"))6: throw new ArgumentException(methodName + " must be ends with Async");
7: var target = function.Target;
8: var targetType = target.GetType();9: var completeEventName = methodName.Substring(0, methodName.Length - 5) + "Completed";
10: EventInfo eventInfo = targetType.GetEvent(completeEventName);11: var eventArgsName = completeEventName + "EventArgs";
12: Type eventArgsType = targetType.Assembly.GetType(targetType.Namespace + "." + eventArgsName);
13: var handlerMethod = CompleteEventHandlerMethodInfo.MakeGenericMethod(eventArgsType);14: Type handlerType = Type.GetType(String.Format("System.EventHandler`1[[{0},{1}]]", eventArgsType.FullName, AssemblyName));15: Delegate handler = Delegate.CreateDelegate(handlerType, handlerMethod);16: return new Tuple<EventInfo, Delegate>(eventInfo, handler);17: }
看完这段代码,先说个题外话,这里我用了Tuple作为返回值,因为要返回两个值,您可能会觉得不专业,我倒是觉得在返回多个值的场景下很实用(至少比用out看起来更舒服),虽然这可能违背了Tuple的设计初衷,问题是我也不知道Tuple是为了什么场景而设计的,希望高手指点:)
至于获得事件和委托的实现,如果您了解并熟悉反射的相关知识,代码并不难懂,只需要注意如何生成泛型的方法的写法System.EventHandler`1[[TypeFullName,AssemblyName]],获得了所有我们需要的参数之后,我们就可以动态构造我们想要的一切了,这里我们是从CompleteEventHandlerMethodInfo中为泛型版本生成具体类型的方法的,那CompleteEventHandlerMethodInfo是什么呢:
1: private static MethodInfo completeEventHandlerMethodInfo;
2: private static MethodInfo CompleteEventHandlerMethodInfo
3: {4: get
5: {6: if (completeEventHandlerMethodInfo == null)
7: {8: var currentType = typeof(ServiceProxy<T>);9: completeEventHandlerMethodInfo = currentType.GetMethod("CompleteEventHandler");
10: }11: return completeEventHandlerMethodInfo;12: }13: }14: public static void CompleteEventHandler<TArgs>(object sender, TArgs eventArgs)
15: where TArgs : AsyncCompletedEventArgs16: {17: if (eventArgs.Cancelled) return;
18: if (eventArgs.Error != null)19: {20: if (eventArgs.UserState is Action<Exception>)21: (eventArgs.UserState as Action<Exception>)(eventArgs.Error);
22: else if (eventArgs.UserState is Action<object, Exception>)23: (eventArgs.UserState as Action<object, Exception>)(null, eventArgs.Error);
24: else
25: throw eventArgs.Error;
26: }27: else
28: {29: if (eventArgs.UserState is Action<Exception>)30: (eventArgs.UserState as Action<Exception>)(null);31: else if (eventArgs.UserState is Action<object, Exception>)32: {33: object result = eventArgs.GetType().GetProperty("Result").GetValue(eventArgs, null);
34: (eventArgs.UserState as Action<object, Exception>)(result, null);35: }36: }37: }38:
CompleteEventHandlerMethodInfo所指向的是static void CompleteEventHandler<TArgs>(object sender, TArgs eventArgs) where TArgs : AsyncCompletedEventArgs
这里处理的两种回调,将异常一起考虑在内,在异常发生时,异常将作为回调函数的第二个参数被传入,结果则被传入null,这样在回调中我们可以对Service访问的异常也进行处理,另外此处我还有一个疑问,虽然在AsyncCompletedEventArgs上提供了Cancelled属性,但是我不知道如何cancel一个已经调用了的Service操作,我并没有找到类似的Cancel方法,所以还是要请教高人,我如何Cancel一个Service操作呢?
在获得调用方法的同时,我们同时获得了该方法相应的Completed事件和事件处理函数,剩下的工作就是组合这些操作了:
1: protected MethodInfo GetFunc(Delegate function)
2: {3: if (!function.Target.Equals(this.ServiceClient))4: throw new Exception("must use ServiceClient object");
5: if (!this.FunctionCache.ContainsKey(function))6: {7: var func = GetMethodWithUserState(function);
8: var args = GetEventAndHandler(function);
9: args.Item1.AddEventHandler(function.Target, args.Item2);
10: FunctionCache.Add(function, func);11: }12: return FunctionCache[function];
13: }
将事件处理函数订阅到事件,然后缓存在FunctionCache中,这样再次调用同一方法时就不用再次反射了,至此,我们再回头看看本文开始的那段代码使用ServiceProxy后是什么模样吧:
1: var client = new TestServiceClient();2: var proxy = new ServiceProxy<TestServiceClient>(client);3: proxy.PerformFunction(client.GetTaskAsync,"task id",(Exception error,Task task)=>4: {5: if(error == null)6: {7: //do work with task
8: task.xxx();9: }10: else
11: {12: //error handle
13: }14: });
在使用时要注意的一个限制就是PerformFunction的第一个参数必须是ServiceProxy代理的对象发出的,并且有了类型推断使得我们不必在PerformFunction后写上<string>这样的代码,看起来还是很舒服的,完整代码可以在文章最后找到。
总结一下,也许你会觉得这样的封装怪模怪样,但我的目的仅在于让我们的代码更优雅,更易于维护,通俗点说就是看起来更舒服,您如果有不同看法欢迎一起讨论,并且本文中留下了几个疑问,希望大家赐教:
(1)Tuple是为了什么需求而设计的?
(2)如何Cancel一个Service操作?
另外,我在具体使用上述代码的过程中碰到一个问题,对于Service中定义的方法如果含有out object arg的时候,在Silverlight中是将out object作为AsyncCompletedEventArgs的一个属性返回的,而参数列表中并没有,所以对于包含out修饰的Service接口只能在CompleteEventHandler中再次进行反射得到,这可能是个小小的遗憾,但是无论怎样,我关于异步操作的讨论暂且到此,剩下的留给大家评说,希望本文对您有帮助。
转载请遵循此协议:署名 - 非商业用途 - 保持一致