代码改变世界

深入Atlas系列:Web Sevices Access in Atlas(5) - 对于复杂数据类型的支持(中)

2006-10-15 21:43 Jeffrey Zhao 阅读(...) 评论(...) 编辑 收藏
  在前一片文章里,我们已经得到了从客户端获得的字符串反序列化以后的Dictionary,我们知道它的形式和JSON非常的相像,只是存放了一些基本类型的数据还有IDictionary已经IList对象。它们在被应用到真正用于调用Web Services方法之前,还需要将这个Dictionary里的数转化为强类型的参数对象。在Atlas中,是使用了WebServiceMethodData.StrongTypeParameters方法来获得一个包含强类型参数对象的字典的。因此我们直接开始分析这个方法吧。

一个包含着JSON信息的Dictionary已经作为rawParams参数被传入该方法,代码如下:
StrongTypeParameters方法

我们可以看到,真正起作用的方法是ObjectConverter.ConvertToType静态方法,它通过传入的对象信息,目标类型来获得最终我们需要的参数对象。那么this.Owner,这个WebServiceData对象的作用是什么呢?我们先来简单看一下ObjectConverter.ConvertToType静态方法的代码:
ConvertToType静态方法

可以发现,WebServiceData对象被作为了IJavaScriptSerializationContext类型的对象来使用。Microsoft.Web.Script.Serialization.IJavaScriptSerializationContext接口中最重要的方法就是GetType(string),它通过给定一个字符串形式的类型名,来获得这个类型对象Type。很自然,这个类型首先需要被IJavaScriptSerializationContext所识别。还记得在我的前一篇文章《深入Atlas系列:Web Sevices Access in Atlas(4) - 对于复杂数据类型的支持(上)》中“问题4”的错误吗?它的产生原因就是因为那个WebServiceData对象无法识别字符串“Jeffz.ComplexType.Employee”所表示的类型。既然无法识别,那么即使使用了“__serverType”来表示一个客户端对象的服务器端类型,也是无法转化的。那么为什么会无法识别这个对象呢?我们继续追踪,分析ConvertObjectToType方法的实现,代码如下:
ConvertObjectToType方法

从这里开始,Atlas对于支持复杂类型的扩展性一点点地被揭开了。在进行转换时,首先会调用JavaScriptConverter.ConverterExistsForType来检验是否有支持该Type的JavaScriptConverter。这里的JavaScriptConverter是指一个继承了Microsoft.Web.Script.Serialization.JavaScriptConverter抽象类的对象。在Atlas中可以可以根据配置文件来加载需要的JavaScriptConverter。事实上,在默认的Web.config文件中,已经为DataSet、DataTable和DataRow三个类写好了JavaScriptConverter。我们在Web.config类中可以发现这样的配置元素:
<microsoft.web>
    
<converters>
        
<add type="Microsoft.Web.Script.Serialization.Converters.DataSetConverter"/>
        
<add type="Microsoft.Web.Script.Serialization.Converters.DataRowConverter"/>
        
<add type="Microsoft.Web.Script.Serialization.Converters.DataTableConverter"/>
    
</converters>
    ……
</microsoft.web>

虽然没有在配置文件内将对象类型与JavaScriptConverter对应起来,但是在Microsoft.Web.Script.Serialization.JavaScriptConverter抽象类中有个virtual属性SupportedTypes,它返回了这个JavaScriptConverter能够支持的Type数组,默认为null。JavaScriptConverter就是依靠这个把具体的JavaScriptConverter子类与某个特定的类型关联起来。

在调用ObjectConverter.ConvertObjectToType方法时,首先会查找是不是已经有了目标type所对应的JavaScriptConverter(第11行),如果有的话,则会通过JavaScriptObjectSerializer.Serialize静态方法将需要转换的对象o序列化成字符串,然后再调用JavaScriptConverter.DeserializeString静态方法,将该字符串反序列化成Web Services方法所需要的参数对象。值得注意的是,在这两个静态方法中,都会再次查看是否有对应的JavaScriptConverter与所操作的对象相对应,如果有,则使用JavaScriptConverter进行操作,反之则使用Atlas内定的操作方法。

可是,是不是有朋友对于ObjectConverter.ConvertObjectToType方法中第13-16行的逻辑表示不解呢?为什么需要将传入的参数o通过它对应的JavaScriptConverter重新序列化成字符串,再交给目标类型的JavaScriptConverter再反序列化一次呢?其实这个思路似乎有点被JavaScriptObjectSerializer.Serialize静态方法的实现所误导了(确切的说是被在这里对于该静态方法的使用所误导)。这个静态方法的确会检查是否有与o的类型对应的JavaScriptConverter存在,但是我们仔细思考一下,这个o的类型会是什么呢?

这里的参数o是刚从JSON字符串转变过来的表示参数的对象之一,它的作用是“表现参数”。它的类型其实只是Int32,Double,String,DateTime等基本数据类型或者IDictionary以及IList对象中的一种。我想,开发人员应该不太会为这写类型写JavaScriptConverter吧?对于这两种对象的转换,Atlas已经支持地相当出色了。事实上,在Atlas中的确也没有为基础类型或者IDictionary以及IList这写类型准备JavaScriptConverter。在ObjectConverter.ConvertObjectToType方法中第14行,使用JavaScriptObjectSerializer.Serialize静态方法序列化o对象,其实只是将这个对象再转变回字符串,转变为客户端通过Request Body发送过来的原始字符串。

这样的话,逻辑就顺利了。将一个类型与JavaScriptConverter所绑定的目的,是为了能够自定义关于这个对象的序列化已经反序列化的操作。这两个操作应该是一体的。这个类需要负责将某个类型的对象输出成特别的JSON代码,也需要输出一段Javascript来告知客户端该如何序列化一个客户端对象(通过提供对象一个serialize函数,这一点在上一篇文章中提到过),在以后分析Atlas为某个Web Services生成客户端代理的原理时会详细说明这一点。可以想象,如果(仅仅是如果)在这之间,对象o找到了对应的JavaScriptConverter,得到的序列化字符串并不是客户端的原始输入,那么为目标Type设计的JavaScriptConverter在反序列化一个字符串时就会失败,或者导致不可预料的结果。

因此,其实JavaScriptConverter.DeserializeString方法接收到的字符串,应该(或者说在正常的情况下)就是客户端表示该对象的原始输入字符串。这样,我们就能做到对一个类型自定义一组扩展。这个方法常常被用于序列化于反序列化一组相互引用的对象,在下一片文章中,我将以范例来具体说明这一点。事实上,Atlas里已有的三个JavaScriptConverter,就是为了处理三个复杂类型DataSet,DataTable以及DataRow的序列化与反序列化操作,它们就是存在着相互引用的对象,Atlas也通过了添加JavaScriptConverter来处理这个问题。

如果没有找到目标type所对应的JavaScriptConverter,那么ObjectConverter.ConvertObjectToType方法就会使用Atlas的默认操作——或者说内部操作:ObjectConverter.ConvertObjectToTypeInternal方法来将o转换为目标type的对象,以便Web Services方法使用。接下来,我们来看一下它的实现:
ConvertObjectToTypeInternal方法

如果当前需要转换的对象为IDictionary类型或者IList类型,那么则调用相应的方法进行转换,这点稍后再作分析。如果这些o不是这两种类型,则说明它们是些基本类型了,例如Int32,Double,DateTime,Boolean等。在这里我们看到Atlas允许我们进行另外一种可以使用的扩展方式:使用自定义的System.ComponentModel.TypeConverter子类并使用System.ComponentModel.TypeConverterAttribute为类型的转换进行标注。熟悉ASP.NET控件开发的朋友可能会比较熟悉这个做法,为控件属性提供设计期支持时经常会使用这种做法。在Atlas中,如果我们的类型会从那些基本类型转换过来,那么我们就可以提供自定义TypeConverter的方式进行扩展。在今后的文章中,我将提供示例供大家参考。

在通过TypeDescriptor.GetConverter方法获得目标类型的TypeConverter后,会查看该TypeConverter是否能够从目前o对象的类型进行转换。如果可以,则返回转换后的结果。否则的话,则设法先将o对象转化为字符串,再使用TypeConverter的ConvertFromInvariantString方法进行转换。请注意,这里使用的方法都是XXXInvariantString,也就是说,它们是与系统的Culture相关的。举个例子,如果我们需要自定一个DateTime到一个特定类型的转换,传入TypeConverter中ConvertFromInvariantString方法的字符串参数,它的值会根据系统当前Culture环境的不同而产生变化。

事实上,对于目标类型本身就是参数类型(例如Int32)的情况,这段代码一般就会执行第36-39行的逻辑。例如对象o的类型是long,而目标类型为int,则这段代码就会先使用Int64Converter将o转变为字符串,再通过Int32Converter将这个字符串转化为我们需要的参数类型:Int32。

另外,通过理解这个逻辑,我们也能够解释下面的行为的原因:

如果参数为整型,那么Atlas会自动将客户端传入的字符串参数转化为整型以保证Web Services方法调用成功。例如,在我之前的文章《深入Atlas系列:Web Sevices Access in Atlas - 示例一 - 特别的访问方式》的范例中,我将txtAge的text属性绑定到serviceMethod的参数age上。这就相当于将一个字符串的值赋于Web Services方法的整型参数,通过使用Fiddler查看HTTP传输的数据也能发现,在Request Body中传递的数据也类似与“{ age : "20", ... }”。显然,在服务器端得到的age参数输入,其类型是一个字符串。但是,就是因为之前描述的逻辑,在真正使用这个字符串的值去进行Web Services方法调用之前,会被Int32Converter转换成为整型。这样,方法调用就成功了。

如果无法使用TypeConverter将对象o转换为目标类型的话,就尝试着直接将o使用在方法调用上了。在上面的代码中可以发现,Atlas使用了目标type的IsAssignableFrom方法来检测“直接赋值”的合法性。最后,如果发现对象o,不管使用什么方式都无法将其转换为目标type的话,则抛出了InvalidOperationException,表示类型转换的失败。

刚才详细讨论的逻辑是在最后对于一个“值”的转换。但是一般来说,在Atlas转换数据类型时难免会遇到IDictionary和IList对象的嵌套。在这时候,就要使用代码所看到的ConvertDictionaryToObject方法和ConvertListToObject方法了。首先我们来分析一下ConvertListToObject方法代码:
ConvertListToObject方法

这个方法的作用是将IList对象list转换成type类型的对象。自然,它在实际使用中也是一个列表或者数组。在这个方法中,根据Type格式的不同,转换后的数据类型可能是ArrayList,Array或者Generic类型List<T>。可以简单地认为,在Atlas中所支持的“列表”类型或者“数组”类型就只有这些。因此,我们在Web Services方法中应该使用这些以及它们的兼容类型,例如IList<T>,ICollection<T>或者IList等。对于无法转换的数据类型,只能不得已地抛出InvalidOperationException了。我们在使用Atlas访问Web Services方法时应该完全避免使用这样的数据类型。当然,我们能够添加自己的扩展,以支持我们需要的数据类型。在下一片文章中我将给出相应的范例。

在上面的方法中,我们可以看到,它总是调用了AddItemToList方法来填充一个“列表”对象。其实我们也能够想象得到它的实现方式:它枚举了源列表的每个元素,调用ConvertObjectToType方法将其转换成合适的对象,然后将其填充至目标列表中。对象的转换总是在大量的间接递归中实现的。

与上述的ConvertListToObject方法相对应是ConvertDictionaryToObject方法。相比于前者返回的是一个“列表”,而后者则返回的则好比是一个“元素”的对象。代码如下:
ConvertDictionaryToObject方法

在这里可以发现,如果客户端传来的对象指定了__serverType,那么将使用__serverType所表示的值从IJavaScriptSerializationContext对象中获得真正使用的类。请注意,它和目标type所表示的类,完全可以不是同一个!产生的对象是目标type的接口实现亦可,产生的对象是目标type的子类亦可,只要能够正确地调用Web Services方法的类型都可以。这就给了我们对于使用Atlas调用Web Services方法有了更高的灵活性,其能力甚至已经凌驾于Web Services本身!这就是Atlas的强大之处。以后将会出现与此相关的示例,敬请留意。:)

等拿到了真正的目标对象类型后,就使用Activator.CreateInstance方法进行对象构造。最后枚举Dictionary中的每一个key,并将以之对应的value和对象传入AssignToPropertyOrField方法进行public属性或者成员变量的赋值。该方法首先再次使用ConvertObjectToType方法递归地将value转换为合适的对象,然后尝试寻找与key对应的属性的set方法,如果没有的话,则再寻找与key对应的Public Field,最后设值。

就这样,一个对象就成功地被构造出来了。



通过分析这篇文章的代码,我们至少可以发现一下几点:
  1. 在Atlas中,通过实现JavaScriptConverter的子类可以自定一个特定类型的序列化和反序列化能力。
  2. 在Atlas中,通过实现TypeConverter并使用TypeConverterAttribute标记可以为一个特定类型,自定义从基本类型(例如Int32,Double,DateTime,Boolean等)的反序列化的能力。
  3. Atlas访问Web Services方法对于范型类型(甚至对于普通数据类型)的支持,是不完整的,不过已经足够大多数应用的需求。而且对于不支持的数据类型,我们可以通过自定义的方式提供对它们的支持。
  4. Atlas访问Web Services方法有非常灵活的可控性,它的功能甚至超过普通的Web Services访问,能够做到普通Web Services无法做到的有用功能。
  在今后的文章中,我会一一提供对于这些功能的示例。



到现在为止,我们对于服务器端反序列化客户端数据的方式已经分析地差不多了,不过还缺少对于那个IJavaScriptSerializationContext,也就是那个WebServiceData对象中GetType方法的分析与理解。那么这个对象这个方法到底是如何工作的呢?另外,得到了调用Web Services方法的结果以后,它又是怎么被序列化并输出是客户端的呢?

我们将在下一篇文章中讨论这些问题。