深入Atlas系列:Web Sevices Access in Atlas示例(2) - 自定义JavaScriptConverter处理循环引用对象

  在之前的一片文章《深入Atlas系列:Web Sevices Access in Atlas(4) - 对于复杂数据类型的支持(上)》中 我举了一个简单例子,说明了存在循环引用的对象在序列化时,服务器端和客户端都会出现异常。在这篇文章里,我将通过一个示例来说明如何通过自定义 JavaScriptConverter来处理存在循环引用的对象。这个示例所用的方式也基本上可以在处理复杂对象时使用,因为复杂对象最重要的一点就是 存在复杂引用,其余的特点,估计也就是成员较多了。

  同样,我们先定义存在循环引用的类。他们依旧是Boy和Girl,能够互相引用。代码如下:
Boy类与Girl类代码
 1 public class Boy
 2 {
 3     public string Name;
 4 
 5     public Girl Girlfriend;
 6 }
 7 
 8 public class Girl
 9 {
10     public string Name;
11 
12     public Boy Boyfriend;
13 }

  然后,我们就一一发现问题,并解决它:

一、解决服务器端序列化问题:

  我们先写需要使用的Web Services方法:
GetBoyWithGirlfriend方法
 1 [WebService(Namespace = "[url]http://tempuri.org/[/url]")]
 2 [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
 3 public class BoyAndGirl  : System.Web.Services.WebService {
 4 
 5     [WebMethod]
 6     public Boy GetBoyWithGirlfriend(string boyName, string girlName)
 7     {
 8         Boy boy = new Boy();
 9         boy.Name = boyName;
10 
11         Girl girl = new Girl();
12         girl.Name = girlName;
13 
14         boy.Girlfriend = girl;
15         girl.Boyfriend = boy;
16 
17         return boy;
18     }
19 }

  很显然,它们存在循环引用,如果使用了Atlas内部的JSON序列化方法的话,就会抛出异常。那么我们该怎么解决这个问题呢?方法就是自定 义一个JavaScriptConvert。在这里,我们写一个BoyConverter,首先重载它的SupportedTypes属性,表明这个 Converter所支持的类型。
SupportedTypes属性代码
 1 public class BoyConverter : JavaScriptConverter
 2 {
 3     protected override Type[] SupportedTypes
 4     {
 5         get
 6         {
 7             return new Type[] { typeof(Boy) };
 8         }
 9     }
10 }

  在之前文章中我们分析的代码可知,这个属性只有在初始化的Converter映射的时候被调用一次,所以我们能够直接new一个数组并返回,不会有任何的性能损失。

   接下来我们要做的事情就是重载JavaScriptConverter的Serialize方法了,它接受一个object对象作为参数,输出序列化结 果。由于我们需要序列化后的对象也保持着循环引用,因此完全使用JSON方式是无法做到的,那么我们该怎么做呢?我们知道,在客户端反序列化服务器端传来 的结果,使用的就是简单的“eval("(" + body + ")")”这样的方法,由于这一点,我们来看一下下面的代码被“反序列化”时会得到什么结果呢? 
function (){  return   100 ; }.call()

  得到的结果就是100!在这里,其实我们定义了一个函数,并过它的call方法进行了调用。在函数体中,我们可以写任意的代码,任意多的语句。这样,我们的做法就显而易见了。代码如下:
Serialize方法
 1 public override string Serialize(object o)
 2 {
 3     Boy boy = o as Boy;
 4 
 5     if (boy == null)
 6     {
 7         throw new ArgumentException();
 8     }
 9 
10     string result = 
11 @"function() 
12 
13     var boy = 
14     {
15         Name : " + JavaScriptObjectSerializer.Serialize(boy.Name) + @"
16         Girlfriend :
17         {
18             Name : " + JavaScriptObjectSerializer.Serialize(boy.Girlfriend.Name) + @"
19         }
20     };
21 
22     boy.Girlfriend.Boyfriend = boy;
23     return boy;
24 }.call()";
25 
26     return result;
27 }

  首先判断对象o是不是Boy类型,如果不是,那么抛出异常。然后就通过拼接字符串输出。事实上,对于一个良好实现的Serialize方法, 它需要判断传入的Boy对象的Girlfriend属性是否为null等信息,然后输出不同的结果。在这里,仅仅是一个演示,我就没有过多追究了。:)

  我们的Serialize方法写完了,然后就要使用它了。请注意,为了使用它,我们需要将它添加到Web.config文件的相应节点中,如下:
< microsoft .web >
    
< converters >
        ……
        
< add  type ="Jeffz.JavaScriptConvertDemo.BoyConverter"   />
    
</ converters >
    ……
</ microsoft.web >

  于是我们就来看一下它的使用代码,首先是HTML
HTML代码
1 <h1>Get Boy with Girlfriend</h1>
2 <div>Boy's Name: <input type="text" id="txtGetBoyName" /></div>
3 <div>Girl's Name: <input type="text" id="txtGetGirlName" /></div>
4 <input type="button" onclick="getBoyWithGirlfriend()" value="Get!" />
5 <div id="GetBoyWithGirlfriend"></div>

  点击“Get!”按钮将会调用Web Services方法,并将结果显示在“GetBoyWithGirlfriend”这个DIV中。所需的Javascript如下:
getBoyWithGirlfriend函数代码
 1 function getBoyWithGirlfriend()
 2 {
 3     Sys.Net.ServiceMethod.invoke(
 4         "BoyAndGirl.asmx",
 5         "GetBoyWithGirlfriend",
 6         null,
 7         {
 8             boyName : $("txtGetBoyName").value,
 9             girlName : $("txtGetGirlName").value
10         },
11         onComplete);
12             
13     function onComplete(boy)
14     {
15         var result;
16         if (boy && boy.Girlfriend &&
17             boy.Girlfriend.Boyfriend == boy)
18         {
19             result = "You did a good job! <br />";
20             result += "The boy is " + boy.Name + "";
21             result += "and his girlfriend is " + boy.Girlfriend.Name + '.';
22         }
23         else
24         {
25             result = "There must be something wrong with your converter.";
26         }
27                 
28         $("GetBoyWithGirlfriend").innerHTML = result;
29     }
30 }

  代码非常简单,就不多说了,如果得到了正确结果,那么会在DIV中显示“You did a good job!”等字样,我们直接来看一下使用效果吧:

  打开之后:


  在文本框内填上信息,并点击“Get!”按钮。得到正确结果了:


  我们看一下Fiddler获得的信息:


  很明显,我们自定义的BoyConverter生效了。:)



二、解决客户端序列化和服务器端反序列化的问题:

  再写一个Web Services方法,接收一个Boy类型的对象作为参数:
SetBoyWithGirlfriend方法代码
 1 [WebMethod]
 2 public string SetBoyWithGirlfriend(Boy boy)
 3 {
 4     if (boy == null || boy.Girlfriend == null
 5         || boy.Girlfriend.Boyfriend != boy)
 6     {
 7         throw new ArgumentException("The boy needs a girlfriend.");
 8     }
 9 
10     return "You did a good job!<br />" +
11         "The boy is " + boy.Name +
12         ", and his girlfriend is " + boy.Girlfriend.Name + ".";
13 }

  这次我们先来看HTML代码,和前一个例子的代码非常相似:
HTML代码
1 <h1>Set Boy with Girlfriend</h1>
2 <div>Boy's Name: <input type="text" id="txtSetBoyName" /></div>
3 <div>Girl's Name: <input type="text" id="txtSetGirlName" /></div>
4 <input type="button" onclick="setBoyWithGirlfriend()" value="Set!" />
5 <div id="SetBoyWithGirlfriend"></div>

  在客户端,如果构造了循环引用的对象,如果使用了默认的序列化方法,将会无限递归最终Stack Overflow。在《深入Atlas系列:Web Sevices Access in Atlas(4) - 对于复杂数据类型的支持(上)》中,我们看出我们可以为对象附加一个serialize方法来自定义序列化方式。那么我们就来看一下这个方法的代码:
serialize方法代码
 1 function serialize()
 2 {
 3     var serialize = this.serialize;
 4     delete this.serialize;
 5     delete this.Girlfriend.Boyfriend;
 6     
 7     var result = Sys.Serialization.JSON.serialize(this);
 8     
 9     this.serialize = serialize;
10     this.Girlfriend.Boyfriend = this;
11     
12     return result;
13 }

  我们最终还是会使用Sys.Serialization.JSON.serialize方法来序列化一个客户端对象,不过在这之前需要进行处 理。首先delete该对象上的serialize方法,否则调用Sys.Serialization.JSON.serialize方法时又将进入当前 的serialize函数。接下来的delete操作删除循环引用,最后才使用Sys.Serialization.JSON.serialize方法序 列化该对象。注意,在得到了序列化结果之后,需要this对象的状态恢复,例如serialize函数和循环引用。

  然后,在服务器端,我们在BoyConverter内重载Desirialize方法,目的是得到反序列化后的Boy对象。代码如下:
Deserialize方法代码
 1 public override object Deserialize(string s, Type t)
 2 {
 3     if (t != typeof(Boy))
 4     {
 5         throw new ArgumentException();
 6     }
 7 
 8     Boy boy = (Boy)base.Deserialize(s, t);
 9     boy.Girlfriend.Boyfriend = boy;
10 
11     return boy;
12 }

  首先判断t的类型是不是Boy,然后调用基类的Deserialize方法进行反序列化。这个方法会调用Atlas内部的反序列化方法,而且 避开对于某个反序列化类型寻找相应Converter的逻辑,否则又会出现了无限递归,最终抛出StackOverflowException。最后,当 然就是恢复boy对象与boy.Girlfriend之间的循环引用了。

  我们来看一下客户端的Javascript代码:
setBoyWithGirlfriend方法
 1 function setBoyWithGirlfriend()
 2 {
 3     var boyObj = new Object();
 4     boyObj.Name = $("txtSetBoyName").value;
 5     boyObj.serialize = Function.createDelegate(boyObj, serialize);
 6     
 7     boyObj.Girlfriend = new Object();
 8     boyObj.Girlfriend.Name = $("txtSetGirlName").value;
 9     boyObj.Girlfriend.Boyfriend = boyObj;
10     
11     Sys.Net.ServiceMethod.invoke(
12         "BoyAndGirl.asmx",
13         "SetBoyWithGirlfriend",
14         null,
15         {boy : boyObj},
16         onComplete);
17         
18     function onComplete(result)
19     {
20         if (result)
21         {
22             $("SetBoyWithGirlfriend").innerHTML = result;
23         }
24         else
25         {
26             $("SetBoyWithGirlfriend").innerHTML = 
27                 "There must be something wrong with your converter."
28         }
29     }
30 }

  越来越感受到Function.createDelegete方法(第5行)的重要性。如果没有它,很多功能的实现都会麻烦许多。

  我们来看一下使用效果。首先,打开页面:


  在文本框内填写信息,并点击“Set!”按钮:


  说明客户端的序列化操作和反序列化操作都成功了!


三、实现更良好的客户端序列化和服务器端反序列化的代码:

  事实上,我们的序列化代码实现还不够良好。例如,如果boy.Girlfriend.Boyfriend不是boy怎么办呢(……)?因此我们不能在客户端和服务器端简单地处理序列化和反序列化操作。我们需要做一个“标记”。

  我们就在Girl对象中作标记吧,将其代码修改如下:
Girl类修改后代码
1 public class Girl
2 {
3     public string Name;
4 
5     public Boy Boyfriend;
6 
7     [XmlIgnore]
8     public bool __HasClientBoyfriend = false;
9 }

  需要注意的是__HasClientBoyfriend变量的作用只是为了标记,在序列化Girl类型对象时不应该输出,所以我们使用XmlIgnoreAttribute来禁止其被序列化。

  我们在客户端的serialize方法自然也需要有些修改:
serialize方法修改后代码
 1 function serialize()
 2 {
 3     var serialize = this.serialize;
 4     delete this.serialize;
 5     if (this.Girlfriend && this.Girlfriend.Boyfriend == this)
 6     {
 7         this.Girlfriend.__HasClientBoyfriend = true;
 8         delete this.Girlfriend.Boyfriend;
 9     }
10     
11     var result = Sys.Serialization.JSON.serialize(this);
12 
13     this.serialize = serialize;
14     if (this.Girlfriend.__HasClientBoyfriend)
15     {
16         this.Girlfriend.Boyfriend = this;
17         delete this.Girlfriend.__HasClientBoyfriend;
18     }
19 
20     return result;
21 }

  在序列化的时候判断了是否的确有循环引用,如果有的话,则在this.Girlfriend对象上做一个__HasClientBoyfriend的标记。

  最后需要修改的就是BoyConverter的Deserialize方法了:
修改后Deserialize方法代码
 1 public override object Deserialize(string s, Type t)
 2 {
 3     if (t != typeof(Boy))
 4     {
 5         throw new ArgumentException();
 6     }
 7 
 8     Boy boy = (Boy)base.Deserialize(s, t);
 9 
10     if (boy.Girlfriend != null && boy.Girlfriend.__HasClientBoyfriend)
11     {
12         boy.Girlfriend.Boyfriend = boy;
13     }
14 
15     return boy;
16 }

  现在,只有当boy.Girlfriend的__HasClientBoyfriend标记为true时才重新建立循环引用。如果仅仅是一个 普通的boy对象,则不会有多余的操作。如果有朋友感兴趣的话,可以使用Fiddler查看Girl类型的序列化结果,可以发现 __HasClientBoyfriend没有被序列化输出,这就是XmlIgnoreAttribute的效果。

  事实上,上面的代码多少还有点小问题,不过个人认为,作为一个示例来说,这已经足够了。


   在这篇文章中,我演示了JavaScriptConverter的基本使用,但是它的作用还不止这些。不知道大家有没有发现,在上面的示例中如果需要在 客户端作序列化操作,则需要在客户端与服务器端同时进行代码维护。显然这种做法非常不利于分发和部署组件。在今后的示例中,我将配合Atlas为Web Service方法建立Proxy的讲解,演示如何使用JavaScriptConverter来将所有的代码都在服务器端进行维护。不过那会涉及到目前 所有的文章,另外还包括以后会讲到的Atlas为Web Services方法建立Proxy方面的内容。结合Atlas的方方面面,能使我们的开发更为成熟。  


  点击这里下载示例源文件。

本文出自 “赵��” 博客,转载请与作者联系!

你可能感兴趣的:(JavaScript,职场,休闲,Atlas)