2006年7月26日星期三

初次使用Atlas JavaScript (Part 3 - 实现自己的应用)

在这里,我假设你肥肥的客户端是基于MVC设计,同时Web Service的内容是以数据为中心的。

在说客户端之前,必须先说说服务器端如何设计。如果服务器端你习惯了3层设计,那就保留你以前的数据访问层和业务逻辑层设计方法,用一模一样的方法完成这两层,只不过我们不再有通过Page表现的UI,而是通过Web Service直接暴露一些业务逻辑层的调用,这就是服务器端需要完成的一切。看起来没有了Page轻松多了,是不是?因为那些工作都转嫁给客户端了。接着要开始用Atlas做我们的客户端了,在这此之前你需要读一下Atlas的各种官方/非官方文档,了解一下它的使用方法,同时读一下它的JavaScript源代码了解一下它的内部结构。

我们首先来看看客户端的Model如何实现,客户端Model应该是用来存储数据并和Web Service打交道,所以它应该设计为一个处于客户端的业务逻辑层。然而这不是一个单纯的业务逻辑层,因为真正的业务逻辑在服务器端,客户端的Model仅仅是在客户端暴露和服务器端业务逻辑层一致的调用。同时它也内涵一个小小的数据访问层,这个数据访问层服务则Web Service打交道,需要包括批量调用和数据缓存与索引等能力。现在我们的分层看起来应该是这样的:
Server Data Access Layer <-> Server Business Layer <-> Client Data Access Layer <-> Client Business Layer
我想肯定有人要问,为什么不把整个业务逻辑层搬到客户端,然后就可省去服务器端的业务逻辑层咯,让客户端的数据访问层和服务器端的数据访问层直接沟通就好了。这样做的最大问题就是,你不知道客户端提交的数据是否一定经过了客户端那个业务逻辑层的处理,用户直接提交一些不符合业务逻辑的数据到服务器端就麻烦了。

继续说我们的客户端Model需要具有的能力。例如服务器端业务层有GetProductById这个方法用于返回一个Product对象,同时客户端的Controller也需要调用此方法,那就应该在客户端的Model暴露GetProductById这个方法。最简单的办法,当然是每次客户端执行GetProductById时,都通过Web Service调用服务器端的GetProductById,然而这却是最低效率的做法。想要提高效率的话,肯定要在客户端缓存数据,这时候客户端就会缓存一些Product对象,而GetProductById方法则需要根据一定的策略决定应该优先搜索缓存还是直接访问Web Service。缓存策略是需要根据应用制定的,什么情况下应该判定缓存过期是没有通则的,这需要你自己去设计。而缓存的方式则有比较固定的做法,就是利用JavaScript特有的关联数组作索引。例如ProductId是Guid,而且使用之前说到的GuidConverter直接转换为String,那就可以创建一个Product.IdIndex对象,然后以ProductId作为关联数组的键来保存Product对象,这样就可以通过Product.IdIndex[ProductId]方法检索一个该ProductId对应的Product对象了。

Model相关的对象,例如Product类和Product.IdIndex对象,可以完全不使用任何Atlas特性,仅仅是普通JavaScript类,因为它们完全不使用Atlas的任何特性都能够好好工作。但你也可以考虑让他们继承自Sys.Component,同时按照Atlas的方式来声明其属性、方法、事件,这样他们能够获得更好的封装性,对debug.dump更好的支持,以及可能更低的执行效率。

然后轮到说View了。View可以说是最简单的部分。说它简单,是因为它和服务器端的View没什么不同,只要你懂得制作服务器端控件,你就知道如何继承Sys.UI.Control制作客户端控件,然后和在Page中一样直接往DOM扔控件就可以了,而且还不需要管SessionState和ViewState这样的东西。有问题吗?例如需要数据,或者有事情需要报告吗?请举手,通过调用或者事件告诉Controller,Controller会通过Model为你处理好这一切,View需要管的仅仅就是用户看得到摸得着的东西。

最后需要搞定的就是Controller了,这时候你有两个选择:使用JavaScript或者使用XmlScript。如果Model和View已经让你写JavaScript写到手痛,那么这时候你可以考虑使用XmlScript来写Controller。你首先把View相关的元素用HTML声明,然后在XmlScript中声明相关的控件以及它们的属性如何绑定到对应的Model对象,那就行了。但如果在Controller里面设计动态创建控件,又或者涉及复杂的数据绑定方式,XmlScript就无法满足你的需求了,这时候还是老老实实地用JavaScript写吧。

2006年7月23日星期日

初次使用Atlas JavaScript (Part 2 - Web Service扩展)

Atlas对Web Service两方面的扩展包括:
1.自动生成javascript代理类的代码
2.在javascript代理类调用时使用JSON表达式进行数据交换

通过看Atlas项目的web.config,你可以发现*.asmx现在关联到了一个新的handler,也就是ScriptHandlerFactory,这个handler的行为很简单,如果请求是REST方式则用新的RestHandlerFactory来处理,否则交回给原来的WebServiceHandlerFactory处理。RestHandlerFactory也有一个选择分支,如果请求的是*.asmx/js,那就是请求客户端代理类的代码,使用RestClientProxyHandler处理,否则使用RestHandler处理,下面详细说说它们的原理:

先看看Web Service的引用,当你在ScriptManager添加一个Service引用的时候,实际添加的是到该*.asmx/js的script引用,然后就得到了代理类的代码,之后你就可以直接在javascript中使用该Web Service的类名称(包括完整的namespace)调用,参数列表则和该Web Service的参数列表一致,最后加上一个参数声明该调用的回调函数。RestClientProxyHandler需要生成如此一个代理类,就需要通过反射扫描所有的Web Services函数签名,然后将函数相关的所有类型注册到客户端。这个注册并不简单,因为Web Service有可能涉及任何类型,包括用户自定义的类型。如果是.NET Framework的一些基础类型,Atlas知道如何将它们转换为对应的javascript对应类;如果是Enum,Atlas也能够自动转换为对应的javascript代码;但遇到了不是基础的struct或者object,Atlas自身就不懂得如何将它们转换为对应的,这时候就要寻找是否有对应的JavaScriptConverter了。

JavaScriptConverter是处理服务器端和客户端之间对象串行话/并行话的基类。现在用创建一个GuidConverter的目标来举例,首先通过SupportedTypes属性声明自己支持的服务器端类型为Guid吧,再通过GetClientTypeName返回客户端类型的名称为String,这样RestClientProxyHandler就知道生成代理类时Guid类型的参数在客户端对应为String类型。

接着说说RestHandler,它处理Web Service参数的时候使用的是JSON表达式。和RestClientProxyHandler一样,对于能够处理的类型它自己能够进行串行话/并行话转换,对于不能够处理的参数它寻找对应的JavaScriptConverter帮助。

继续上面GuidConverter的例子,我们需要实现Serialize和Deserialize这两个函数,将Guid串行话为String仅需要这样:
return "\"" + ((Guid)o).ToString("D") + "\"" //o是待串行话object
而将String并行话为Guid则仅需要这样:
return (new Guid(s.Replace("\"", string.Empty))); //s是待并行话的String的表达式
最后在web.config的converter节注册一下GuidConverter,Web Service就能够正确的和客户端交换类型为Guid的数据了。

系统自带的JavaScriptConverter包括DataSetConverter、DataTableConverter、DataRowConverter,注意这3各类都没有实现Deserialize所以仅能用于将数据传送到客户端。

初次使用Atlas JavaScript (Part 1 - JavaScript扩展)

Atlas的服务器端控件已经说了,现在说说客户端脚本。Atlas的脚本分为xml-script和javascript两部分:xml-script作为一种描述性语言主要用来做Controller,直接放在页面里面;javascript经过Atlas库扩展后可以用来写Atlas特有的客户端控件,而其内容最好放在独立的js文件中通过引用来使用。xml-script我还没有专门研究过,不是太懂其机制,所以现在先说说其javascript扩展。

首先,Atlas对javascript语言本身作了一定的扩展,例如让javascript支持继承,还有抽象函数、寒暑重写等概念,可以说是极力模仿C#风格。这对于习惯用C#的人来说是好事情,而且内联而非prototype方式声明外露函数和属性也确保了内部变量的不会被外部访问到,从而确保了类的封装性,但这样做却让Altas的执行效率显得比其它framework要差一些。另外Atlas还对javascript本身的String、Array、Date、Number作了简单的扩展,目的是为了模仿.NET类库对应的类的常用方法,方便javascript编写者。关于Atlas对javascript的扩展,详细可以参考这篇ASP.NET Atlas对JavaScript的扩展。(由于Atlas还在不断改进中,无论是官访文档还是自己研究Atlas源代码者写的文档都仅仅是关注到作者认为应该关注的方面,所以资料不一定全面,而且还不一定适用于日后的版本,所以最好还是使用时参考Atlas的源代码。)

当然,Atlas不可能全面扩展javascript的对象让他们拥有所有服务器端对应对象的常用方法,这时候你就需要自己扩展了。例如Date对象没有AddYears等方法,而你的程序需要这方面的Date对象操作便利,你就可以模仿着服务器端的方法去写对应的方法。模仿的最简单办法就是用Reflector打开.NET Framework看,看看它是怎样实现的,然后你改用javascript实现。由于.NET Framework系统类的实现方法效率不可能太低,所以按照它的方法去做效率应该也不会差,不过你自己有更高效的方法就更好了。

2006年7月19日星期三

Atlas 的服务器端控件 - 易用但不灵活

首先,我会把Atlas服务器端控件区分为两类,纯服务器端控件和客户端逻辑封装控件。前者类似WebControl派生出来的控件那样,它自身并非直接呈现(render)一个客户端元素的实例就算,而是拥有复杂的服务器端逻辑,它呈现出来的HTML由交错的规则决定着;至于后者,则类似HtmlControl派生出来的控件,它所做的基本上就是将自身呈现为一个单一的客户端元素,其服务器端属性比较直接的生成客户端元素的属性或者子元素,不过客户端逻辑封装控件输出的不是HTML,而是Atlas特有的xml-script。

纯服务器端控件是Atlas服务器端的重点,这些控件在Microsoft.Web.UI而不在Microsoft.Web.UI.Controls,由此也可以看出他们的核心地位,它们是ScriptMananger(ScriptManagerProxy)和UpdatePanel。

首先说说ScriptManager,它都是一个非常有用的控件,扔一个ScriptManager到Page上面,它就会自动帮你生成引用Atlas核心js的引用,这样Atlas函数库就能够被使用,xml-script也能被解释。对于一个ScriptManager,你可以向它添加Script和Service引用。

可添加Script引用的包括Atlas内置的Script模块(AtlasUIDragDrop、AtlasUIGlitz、AtlasUIMap)和是你自己的js文件,如果添加的是js文件你可以使用"~"路径运算符以确保无论在什么路径的页面都能正确引用这个js文件。

添加Service引用的Service是ASP.NET Web Service,添加引用后你可以直接在JavaScript或xml-script中调用该Web Service而无须管后台是怎么实现的。另外,ScriptManager还有一个重要的属性,就是EnablePartialRendering,启用后ScriptManager就会去分辨一个PostBack是正常提交的PostBack还是XMLHttpRequest发回来的异步PostBack,如果是后者它就会优化处理,详细下面再解释。

然后说说UpdatePanel,这就是Atlas让你最容易开发一个“看起来很Ajax”的应用的地方。所谓的UpdatePanel,其实就是圈定一个区域,里面的控件或者外部注册为异步调用的控件触发PostBack时并不真正提交页面而是用XMLHttpRequest发送异步PostBack,之后分析从服务器端返回的HTML并更新这个区域内的HTML代码。你可以设置一个UpdatePanel不是每次都需要更新内容,当一个异步PostBack发生时就根据一堆的Trigger设置来判断一个UpdatePanel是否在本次异步PostBack时需要更新,同时ScriptManager开启了EnablePartialRendering的话则只有需要更新的区域内的HTML才会被发送到客户端,这样就可以节省沟通成本。

虽然这样看起来UpdatePanel非常灵活,区域有多大以及什么条件下才更新都能够设置,一眼看起来设置适当的话非常好用。但其实并不存在“设置适当”,UpdatePanel并不是万能,它不能像普通控件那里放在任何区域之内、包容任何控件、在页面生命周期的任何阶段声明,所以不是你喜欢怎样放就怎样放。而且当UpdatePanel多起来Trigger也不容易设置完美,总会导致应该更新而没更新的情况,而那个时候你就会想还是把所有UpdatePanel设置为总是更新算了。

UpdatePanel最坏的地方我认为是破坏对象结构。服务器端控件和客户端的DOM都是树状结构,目标就是保持逻辑的局部性,所以Response.Write和Document.Write这种破坏局部性的操作方式我觉得都不应该被使用,而UpdatePanel区域的刷新就如Response.Write一样,一刷就是整个区域。虽然在服务器端区域内的还是控件,还是可以按控件进行逻辑处理,但是在客户去这样一刷,DOM元素都被破坏掉了,区域内增加删除修改了哪些DOM元素完全没办法知道,这时候该区域内外的View和Model都独立,要建立一个跨越区域边界的Controller也不容易,当内部的View和Modal被整个替换掉的时候,Controller完全不知道变更的细节,而需要自己去适应新送上来的View和Model。对此暂时我觉得可行的解决方案就是建立一个内部的Controller,让该Contoller负责和外部Contoller的沟通,而该Controller由服务器端生成,所以它可以知道新旧的View和Model之间的变更细节。

简单来说,用UpdatePanel就如用不闪烁的IFrame一样,传统的做法能够马上用上,但效率不高,灵活性也低,最后你会发现不如整个页面都放在一个大的UpdatePanel里面,让它“看起来不闪烁”就算了。

2006年7月8日星期六

ViewState - ASP.NET 的一个特有存储容器

首先,我不确定是不是只有ASP.NET由ViewState,也不确认它有多特有,只是觉得这个东西对于Web开发MVC分离的进步很有帮助。

所谓的ViewState,就是用来存放关于View的State的地方。以前的存储容器包括Cookies, Session, Application, Cache, Hidden,有时候连传递变量用的QueryString也用作存储容器,但都不是专门用来存储View相关信息的地方,然而由于没有专门存放View相关信息的地方,所以人们只好乱放。不怕过期失效的变量,多数人会选择放在Session里,而且跨页面不会丢失,用户访问几个别的页面回来还能通过Session恢复本页的View。如果需要延长一些时间,而数据又不是很多的话,可以放Cookies,和Session类似。而数据真的很短,而且页面总是提交给自己的情况下,用QueryString作为一些跨页面生命周期的变量的保存方式也可以。而如果页面可以只用Post不用Get传递的话,那么Hidden也是一个很好的选择,因为Hidden容量大,不在地址留下信息。在ASP.NET当中,就是设计到大多数情况都是Post(除了直接链接),所以用Hidden存放和View相关的信息是非常适合的。

既然用Hidden存放就可以了,为什么需要ViewState这样的统一管理呢?最显然的理由就是加密。ViewState不是给用户看或者修改的信息,仅仅是因为View的状态会在页面生命周期之间丢失,所以我们要将这些信息输出到HTML再等Post的时候取回来,对ViewState加密(至少校验)能够确保View正确无误的恢复。ASP.NET内置的ViewState可以方便的设置校验和加密,只要你把可以序列化的对象存进去,它就能够自动序列化并输出到HTML,同时该对象的保存与恢复是跟控件名以及在控件树中的位置相关的,就如ASP.NET控件的其他特性一样,确保了树中不同位置的同ID控件的数据不会被混淆。简而言之,ViewState是保存跨页面生命周期有关变量的最好容器,但它又不能够跨出页面范围称为会话变量的容器(那应该是Session负责的哦),所以是真正符合其名称用来保存View的State的。

有很多ASP.NET的新手不知道ViewState的用途,认为它是ASP.NET的内部对象,平时还是仅用ASP那套公开的对象好了,那就会带来很多麻烦。例如有GridA和GridB两个页面,点击一个条目查看明细都回转到Details页面,同时Details页面要提供返回原来页面的途径(包括原本GridView所浏览到的分页)。如何存储原本页面的状态呢,包括它来自GridA还是GridB以及原来的GridView所在的分页?有人选择用QueryString传递给Details页面,让Details页面构造返回链接时再通过QueryString把状态传回去。显然,原来页面的状态不是对Details的Query(查询),那么用QueryString传递给Details页面是不合适的。也有人选择用Session传递,但是这个状态仅仅做一次传递,难道我对你说一句话也算是Session(会话)?当然不算。用Hidden是一个方法,但是基于我上面所说的ViewState对Hidden的改良,在这种情况下就应该用ViewState作为原本GridA或GirdB的View状态的保存方式,并且在经过Details页面返回之后再还原。需要说明的是,只有ASP.NET 2.0才在内部支持跨页面PostBack,ASP.NET 1.x不支持跨页面PostBack,也无法跨页面接受和保持ViewState。

最后就是使用ViewState需要注意的地方。过量使用ViewState当然有损效率,但这对于网站的数据正确性来说还不至于造成漏洞,真正会造成漏洞的就是ViewState重名。上面不是说了ViewState是根据控件树位置和控件ID保存所以不会重名的吗?在树是静态的情况下确实是这样,但是如果树是动态创建的,而ID则是可能在不同的语义下重复使用的,那就可能破坏数据的正确性了。例如一个DataControl的子控件自动命名为Control_1, Control_2, Control_3等,如果在PostBack后该DataControl被再次DataBind,那么DataSource可能已经改变了,所以需要清除所有的子控件并重新根据DataSource创建它们,此时别忘记了调用ClearChildState方法把子控件的ViewState和ControlState都清除掉,否则当你重新创建子控件而它们的名称还是Control_1, Control_2, Control_3等的时候,就可能会把原来的ViewState加载回去。

2006年7月1日星期六

OpenID - 继claimID之后

首页在这里:openid.net

OpenID的目的是一个真正的分布式Identity系统,可以用于验证你是某URL的Owner。在范例中,它假设你有一个Blog,然后你在另外一个支持OpenID的Blog发表评论,在评论后你就可以输入你的OpenID登录。所谓你的OpenID,就是一个URL,例如你的Blog的URL。在收到评论提交和OpenID后,该Blog系统就会去你的URL检索OpenID信息,并到指定的OpenID服务器验证你的登录,验证成功就确认你是来自该URL的用户。

如果用过Google Sitemaps,你就知道要查看该站点的统计信息必须先确认你是该站点的Owner,而方法就是Google会要求你对该站点的首页插入一个指定meta标记,然后Google检测到该标记后就确认你是该站点的Owner了。Google Sitemaps是由Google统一确认你的Owner身份,而OpenID则允许你在你的页面中通过<link rel="openid.server">指向任何一个OpenID验证服务器,这就是它所谓分布式的地方。至于指向的Server通过何种方法确认你是该URL的Owner则是不确定的,例如如果该Server就是你的BSP(Blog Service Provider),那么你提供你的Blog的URL和密码它验证,这是很简单的事情。