¥£$ ДЕУΙГ

漫谈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 MyTestServiceClient
  2:     {
  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:       try
  6:       {
  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 : ICommunicationObject
  2:     {
  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 : AsyncCompletedEventArgs
 16:         {
 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中再次进行反射得到,这可能是个小小的遗憾,但是无论怎样,我关于异步操作的讨论暂且到此,剩下的留给大家评说,希望本文对您有帮助。

    完整代码下载

posted @ 2010-06-09 14:22  devil0153  阅读(2335)  评论(3编辑  收藏  举报