.NET 企业库 知识分享--知识产生力量,成功源于分享!
RSS

导航







专业.NET技术社区

专业.NET技术社区


快速搜索

高级搜索 »

注意:此页面是Spring.NET框架 1.1 参考文档的一部分。


编辑

19.1. 简介

很多开发人员不喜欢ASP.NET是因为它不是“真正的MVC”(Model-View-Controller),因为控制器的逻辑与页面视图耦合的太过紧密。Page类的事件处理器就很能说明这个问题:一般来说,ASP.NET会在事件处理器中直接引用视图元素,比如输入控件等。在此我们不打算从理论上讨论什么是“真正的MVC”,也不去管使用“看上去更美”的MVP或表示层(Presentation)模型将ASP.NET这种基于窗体的技术转化为基于请求的传统MVC模式是否适合,在最为重要的一点上,我们同意批评家们的看法:控制器的逻辑,比如写在ASP.NET页面事件处理器中的代码,不应该依赖于视图元素。

不过话说回来,ASP.NET其实有很多好的方面。服务端窗体和控件大幅度提高了开发人员的效率,并简化了页面的(HTML)标记。同时,由于每个控件都知道怎样根据用户的浏览器来输出正确的HTML标记,所以很多跨浏览器的问题也变得很容易处理。ASP.NET可以将自定义逻辑与页面的生命周期挂钩,也支持自定义HTTP处理管道等等非常强大的功能。最后,ASP.NET为Web开发定义了一个相当有用的抽象层,这使开发人员可以和强类型的服务端控件进行交互,而不必去处理诸如Form和QueryString这种基于字符串的HTTP请求。

由于ASP.NET本身的这些优点,我们决定不在Spring.NET中重新开发一套“纯粹、真正的MVC”的Web框架,而采用一种更为实际的方式:扩展ASP.NET,以消除其中的大部分弊端。

前面讲过,code-behind类中的事件处理器不应该直接处理ASP.NET的UI控件。事件处理器应该操作页面的表示层模型,比如领域对象或是ADO.NET的DataSet对象。为此,Spring.NET实现了一个双向数据绑定的框架,来处理页面控件与数据模型之间的映射关系。数据绑定框架还可以透明的进行数据类型转换和输出的格式化,所以开发人员可以在事件处理器中使用强类型的数据(领域)对象。

在企业级应用中,应用程序的控制流程也是一个需要关注的问题,Spring.NET的Web框架对此也有相应的解决方案。在传统的ASP.NET应用程序中,开发人员一般会在某个行为执行后调用Response.Redirect或Server.Transfer方法跳转到其它页面。一般来说这会导致在页面中对目标URL进行硬编码。Spring.NET使用结果映射(Result Mapping)来解决这一问题,开发人员可以为每个行为结果指定一个别名,再通过别名映射到目标URL,这一切都配置在外部文件中,很容易修改。将来, Spring.NET还会实现一个过程管理框架,将结果映射归纳到一个单独的层次,使开发人员可以用非常简单的方法来控制复杂的页面流程。

在2.0版之前,ASP.NET自身的本地化功能相当有限。虽然VS.NET 2003可以为每个页面和用户控件都生成一个对应的本地资源文件,但ASP.NET基础框架自己却不知道去用它。也就是说,不论何时需要访问本地化资源,开发人员都只能直接使用资源管理器(resource manager)来自己处理,Spring.NET认为问题不应该这样解决。Spring.NET的Web框架(后文中直接称为Spring.Web)通过本地资源文件和定义在IoC容器内(且供容器使用)的全局资源,为本地化提供了全面的支持。

Spring.Web能够对ASP.NET页面和控件进行依赖注入。也就是说,利用Spring.NET的IoC容器,开发人员可以很轻松的将服务对象注入到Web控件中去。

前面提到的这些功能可以看做是Spring.Web的“核心”,除此之外,Spring.Web也包括一系列可能对大多数开发人员都很有用的“次要功能”,比如在ASP.NET 1.1中使用2.0的功能(如母版页,即Master Page),再比如说对向导页面的支持(也就是能处理跨越多次请求和/或表单提交的业务过程的页面)。

为实现上述的功能,Spring.NET必须扩展(继承)标准ASP.NET的Page类和UserControl类。也就是说,要想充分利用 Spring.Web的功能(特别是双向数据绑定、本地化和结果映射),Code-behind中的类型也必须继承Spring.Web指定的基类,比如 Spring.Web.UI.Page;不过,对页面进行依赖注入以及其它强大的功能则不需要依赖Spring.Web的任何类。之所以强调这一点,是为了让开发人员了解:要想充分利用Spring.Web的功能,只能让应用程序的表示层与Spring.Web紧耦合。当然,是否要这样做,决定权还是在用户手中。

最后,请留意随Spring.NET(1.1版)一同发布的还有几个Spring.Web的快速入门程序,以及一个完整的参考程序: SpringAir。简单的快速入门程序是学习Spring.Web的最好途径;SpringAir参考程序则完全使用Spring.Web创建前端表示层,其中包含很多Spring.Web的最佳开发实践。所以,如果您正在阅读本章内容,别忘了参考该程序的源码(参见第二十九章,SpringAir - 参考程序)。(按:目前第二十九章没有内容,但是1.1 Preview3 的安装包已经包含了完整的SpringAir源码)

顶部

编辑

19.2. 自动装载应用程序上下文和应用程序上下文嵌套

顶部

编辑

19.2.1.配置

Spring.Web自然也是以Spring.NET IoC容器为基础的,Spring.Web在内部大量使用了IoC容器的功能。也就是说,如果使用Spring.Web建立应用程序,所有控制器(即 ASP.NET页面)都可以用Spring.NET统一的XML方式进行配置。Spring.Web用PageHandlerFactory类来(自动)装载和配置IoC容器,并从IoC容器中获取合适的页面来响应HTTP请求。(按:没有在容器中配置的页面则由ASP.NET自行管理。如果在 Web.Config的httpHandlers节点中添加了以下httpHandler:

  <add verb="*" path="ContextMonitor.ashx" type="Spring.Web.Support.ContextMonitor, Spring.Web"/>

在调试时,就可以用http://webdir/ContextMonitor.ashx查看当前容器内的对象。)

在Spring.Web中,IoC容器的初始化由PageHandlerFactory在后台自动进行,这一过程对开发人员来说是完全透明的,不再需要任何形式的显式初始化(比如通过new操作符或服务定位器ContextRegistry)。为使IoC容器的自动创建生效,需要将下面的配置加入到 Spring.Web应用程序根目录的web.config文件中(其中verb和path的属性值当然可以根据实际情况改变):

<system.web>

    <httpHandlers>
        <add verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web"/>
    </httpHandlers>
    ...
</system.web>

请注意,这段配置只需要添加到根目录的web.config中(也就是Web应用程序最顶层虚拟目录中的web.config文件)。

这段配置告知ASP.NET:Spring.Web的PageHandlerFactory类会负责创建.aspx页面、向页面中注入依赖项(若有需要),然后用此页面来响应HTTP请求。

除配置Spring.Web的HttpHandler之外,还需要在web.config文件中定义根应用程序上下文。最终完成的配置文件大概是下面这个样子(当然会随具体情况有所不同):

<?xml version="1.0" encoding="utf-8"?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
          <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>
        </sectionGroup>
    </configSections>

    <spring>

        <context>
            <resource uri="~/Config/CommonObjects.xml"/>
            <resource uri="~/Config/CommonPages.xml"/>

            <!-- TEST CONFIGURATION -->
            <!--
            <resource uri="~/Config/Test/Services.xml"/>

            <resource uri="~/Config/Test/Dao.xml"/>
            -->
      
            <!-- PRODUCTION CONFIGURATION -->

            <resource uri="~/Config/Production/Services.xml"/>
            <resource uri="~/Config/Production/Dao.xml"/>
      
        </context>

    </spring>

    <system.web>
        <httpHandlers>
            <add verb="*" path="*.aspx" type="Spring.Web.Support.PageHandlerFactory, Spring.Web"/>
        </httpHandlers>
    </system.web>

</configuration>

在这段配置中,有几点非常重要:
  1. 必须配置<spring>和<context>节点的处理器(按:请参考第四章的相关内容)。如果需要在同一台服务器上运行多个Spring.Web应用程序,也可以将名为spring的整个< sectionGroup>节点放在machine.config文件中。
  2. 必须显式指定<context>节点的type属性值为 "Spring.Context.Support.WebApplicationContext, Spring.Web"。这样才能保证Spring.Web功能的正常运行(比如处理请求和会话范围内的对象定义)。(按:这点与Windows应用的容器配置不同,要特别注意)
  3. 必须在<spring>节点中定义一个根上下文,如果对象定义保存在独立的配置文件(比如包含服务和业务层对象定义的XML文件)中,需要在<context>节点的< resource>子节点中注册这些文件。文件的路径可以是完整路径或URL,也可以象上例中一样使用相对路径。容器会将相对路径中的配置文件按照默认的资源类型来处理,对WebApplicationContext来说,默认资源类型就是WebResource类型。
  4. 请注意,同一容器中的对象定义不必保存在同一种资源中(即不需要都是file://、或都是http://、或都是assembly://)。也就是说,容器可以先从一个程序集内嵌资源(assembely://)中载入对象定义,然后再从网络资源中载入其它对象定义。

顶部

编辑

19.2.2.上下文嵌套

ASP.NET本身也提供了配置数据的继承机制,开发人员可以用Web应用程序子目录中的配置数据覆盖上层目录中的配置数据。

比如说,Web应用程序根目录下的web.config文件可以覆盖machine.config文件中的设置,而Web应用程序子目录下的 web.config文件又可以用相同的方式覆盖根目录下的web.config文件。子目录的web.config也可以添加上层目录未曾出现过的配置内容。

Spring.Web利用ASP.NET的这一机制来进行应用程序上下文的嵌套。可以在子目录的web.config中配置新的对象定义,也可以覆盖上层web.config中已有的对象定义(覆盖的行为仅在当前子目录中有效)。

对于开发人员来说,可以以虚拟目录为单位将应用程序分为不同的组件,每一个组件都可以在自己虚拟目录中的web.config中创建自己的应用程序上下文。位于下层的应用程序上下文一般只包括本组件内部的对象定义,并且可以覆盖父上下文的部分定义(例如菜单)。(按:也就是说,在 Spring.Web应用中,一个应用程序上下文,或者说一个组件是由一个虚拟目录自然反映的,而应用程序上下文之间的继承关系则是由虚拟目录的层次隐式维系的:在根目录web.config中配置的是整个应用的根容器,子目录中配置的就是子容器;在同一虚拟目录中只能配置一个应用程序上下文,而不能象 Windows应用那样出现嵌套的<context>节点。)

因为子组件的对象定义一般较少,所以建议开发人员将子组件中的对象定义直接写在web.config文件中,不必再专门使用外部资源。这样,一个组件的web.config文件就和下面的配置差不多:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <configSections>
    <sectionGroup name="spring">
       <section name="context" type="Spring.Context.Support.WebContextHandler, Spring.Web"/>
       <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>
    </sectionGroup>
  </configSections>
  
  <spring>
    <context type="Spring.Context.Support.WebApplicationContext, Spring.Web">
        <resource uri="config://spring/objects"/>
    </context>

    <objects xmlns="http://www.springframework.net">
        <object type="MyPage.aspx" parent="basePage">
              <property name="MyRootService" ref="myServiceDefinedInRootContext"/>
              <property name="MyLocalService" ref="myServiceDefinedLocally"/>
              <property name="Results">
                 <!-- ... -->
              		
        
        
    
  


<context>节点中的<resource>节点告知IoC容器从配置文件的spring/objects节点中读取对象定义。

当然,我们可以(也应该)将<configSections>节点中和spring有关的<section>节点移到上层(或根)web.config文件中;甚至,如果需要在同一台服务器上运行多个Spring.Web应用程序,就应该移到machine.config文件中。

有一点非常重要:这些组件级的容器可以引用其父容器中的对象定义。一般来说,如果在当前容器中找不到某个对象定义,Spring.NET会在所有的父容器中进行查找,直到找到为止(若最终没能找到就抛出异常)。

顶部

编辑

19.3. 为ASP.NET页面进行依赖注入

Spring.Web以ASP.NET为的功能基础:Spring.Web使用code-behind文件中的类型作为MVC模式中的控制器,就能说明这一点。在以MVC为基础的(Web)应用程序中,控制器一般都是对一或多个服务对象的瘦包装器(thin wrapper);在开发Spring.Web时,Spring.NET团队就意识到了向页面控制器中注入服务对象的重要性。因此,Spring.Web 为ASP.NET页面的依赖注入提供了一流的支持。开发人员可以用标准的Spring.NET配置将任何服务对象(实际上,任何对象)注入到页面中,不需要依赖自定义的服务定位器,也不用自行在容器中手工查找服务对象。

一旦配置好了Spring.Web的应用程序上下文,开发人员就能轻松的为页面创建对象定义,并将它们组合成一个完整的Web应用程序。

<objects xmlns="http://www.springframework.net">

  <object id="basePage" abstract="true">
    <property name="MasterPageFile" value="~/Web/StandardTemplate.master"/>
  </object>

  <object type="Login.aspx">
      <property name="Authenticator" ref="authenticationService"/>
  </object>
  
  <object type="Default.aspx" parent="basePage"/>

</objects>

在本例中,定义了三个对象:
  1. 首先是一个抽象定义,在应用程序中,其它页面可以将它作为基页面,从中继承配置数据。在本例中,这个抽象定义只是简单的规定了要引用哪个页面作为Master页,但一般来说,可以在这类对象定义中配置与本地化有关的依赖项,以及图像、脚本和CSS样式表的根文件夹。
  2. 第二个对象定义则定义了一个登录页面,但没有继承上面抽象定义的信息,也没有使用母版页。从这个对象定义中,我们可以看到如何将服务对象注入到页面对象中(假定authenticationService已经在其它地方定义了)。
  3. 最后一个对象是应用程序的首页,它继承了抽象的basePage定义以便使用母版页,除此之外没有定义任何依赖项。

在配置ASP.NET页面的对象定义时,与普通的对象稍有不同。从上面的配置中可以看出,type属性的值实际上是页面的.aspx文件名,且文件路径是相对于当前容器所在目录的。因为上面配置的容器是根容器,所以Login.aspx和Default.aspx必须要在应用程序的虚拟根目录下。母版页则是用绝对路径定义的,因为(位于子目录下的)子容器中的对象也需要引用它。

您可能已经发现,Login和Default页面没有id或name属性。很显然这不满足Spring.NET容器的一般要求,因为普通的对象定义是必须指定id或name的(内嵌对象定义除外)。实际上,这里是故意省略了id或name属性。因为在Spring.Web应用程序中,页面控制器一般都以.aspx文件名作为页面对象的标识符。如果页面对象定义没有指定id,Spring.Web就会用.aspx的文件名作为对象的标识符(会截去路径信息和扩展名,只使用文件名)。

当然,开发人员仍然可以显式指定页面对象的id或name;如果需要多次重用某个页面,并且需要为其注入不同的依赖项时,就需要显式指定id了。

(按:在Spring.Web的应用程序上下文中,如果为MasterPage(不管是System.Web.UI.MasterPage还是 Spring.Web.UI.MasterPage)配置了对象定义,那么这个对象定义会始终被容器认为是抽象的,abstract属性不起作用,也无法通过GetObject方法从容器中获取MasterPage对象。如果使用ASP.NET 2.0本身的MasterPage,容器不会为它进行依赖注入,也无法在MasterPage中应用Spring.Web的任何高级特性,特别是本地化和语言文化管理。

所以如果使用Spring.Web建立Web应用,应尽量使模版页继承Spring.Web.UI.MasterPage。在此请注意,如果模版页继承自Spring.Web.UI.MasterPage,那么它的内容页就必须继承自Spring.Web.UI.Page,否则会发生运行时错误。

另外,如果使用ASP.NET本身的MasterPage,通过实现IApplicationContextAware接口,模版页也可以在自身代码中获取容器的引用。关于IApplicationContextAware接口作用和定义,请参考第四章的相关内容。)

顶部

编辑

19.3.1.为Web控件进行依赖注入

Spring.Web也能够对页面中的Web控件(包括用户控件和标准控件)进行依赖注入。注入的方式有两种:以类型为依据进行全局注入;或者对某个页面中的某个控件进行局部注入。

以类型为依据进行全局注入时,需要以控件的类型全名为标识符创建一个抽象的对象定义;注意此处的类型全名不能包含程序集名称。

<object id="MyProject.MyControl" abstract="true">
   <!-- inject dependencies here... -->
</object>


针对单个控件进行局部注入时,需要创建一个以被注入控件的UniqueID为id属性值的抽象对象定义,如下:

<object id="myContainerControl:myTargetControl" abstract="true">
   <!-- inject dependencies here... -->
</object>

要确保这两种对象定义都是抽象的(也就是在<object>节点中添加abstract="true"属性)。

要获取控件UniqueID,最简单的方式是将应用程序的Trace打开,然后在页面Trace信息的control hierarchy节点中去找某个控件的UniqueID。这确实是、、、有点糟糕。(按:打开trace的方法是在web.config的 system.web节点中添加一个节点,比如:
<trace enabled="true" requestLimit="20" pageOutput="true" traceMode="SortByTime" localOnly="true" writeToDiagnosticsTrace="true"/>


请注意,可以同时为一个控件的对象定义配置这两种方式的注入,这时,如有必要容器会合并对象定义,用局部注入的值将全局注入的值覆盖掉。

考虑到应用程序上下文的作用域,我们需要注意应该将全局控件的对象定义配置在根容器中,以便能在整个Web应用程序执行期间对其进行统一的注入。相对于全局控件,局部控件最好还是定义在各个组件的容器中,以避免不同组件的对象间发生命名冲突。

顶部

编辑

19.4. 在ASP.NET 1.1中使用母版页

Spring.Web为ASP.NET 1.1所扩展的母版页功能与ASP.NET 2.0是非常相似的。

为使用母版页,需要在Web应用程序中定义一个页面作为布局模板,在其中定义内容占位符以便其它页面可以对其进行引用和填充。下面是一个母版页(Master.aspx)的代码:

<%@ Control language="c#" Codebehind="MasterLayout.ascx.cs" AutoEventWireup="false" Inherits="MyApp.Web.UI.MasterLyout" %>
<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >

<html>
    <head>
        <title>Master Page</title>
        <link rel="stylesheet" type="text/css" href="<%= Context.Request.ApplicationPath %>/css/styles.css">
        <spring:ContentPlaceHolder id="head" runat="server"/>
    </head>
    <body>
        <form runat="server">
            <table cellPadding="3" width="100%" border="1">
                <tr>
                    <td colspan="2">
                        <spring:ContentPlaceHolder id="title" runat="server">
                            <!-- default title content -->
                        </spring:ContentPlaceHolder>
                    </td>
                </tr>
                <tr>
                    <td>
                        <spring:ContentPlaceHolder id="leftSidebar" runat="server">
                            <!-- default left side content -->
                        </spring:ContentPlaceHolder>
                    </td>
                    <td>
                        <spring:ContentPlaceHolder id="main" runat="server">
                            <!-- default main area content -->
                        </spring:ContentPlaceHolder>
                    </td>
                </tr>
            </table>

        </form>
    </body>
</html>

从上面的代码可以看出,母版页定义了页面的整体布局,其它页面可以覆盖其中的四个内容占位符。Mater Page也可以在占位符中定义默认的内容,如果使用它的页面没有覆盖某个占位符,就会显示默认内容。

下面是一个使用了母版页的子页面 (Child.aspx):

<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %>
<%@ Page language="c#" Codebehind="Child.aspx.cs" AutoEventWireup="false" Inherits="ArtFair.Web.UI.Forms.Child" %>
<html>
    <body>

        <spring:Content id="leftSidebarContent" contentPlaceholderId="leftSidebar" runat="server"> 
            <!-- left sidebar content -->
        </spring:Content>
        
        <spring:Content id="mainContent" contentPlaceholderId="main" runat="server"> 
            <!-- main area content -->
        </spring:Content>

    </body>
</html>

在这个页面中,<spring:Content/>控件使用contentPlaceholderId属性来指定要填充或覆盖母版页上哪个内容占位符。由于子页面没有定义顶部和标题占位符的内容,所以母版页会显示自身的默认内容。

ContentPlaceHolder和Content控件可以定义在任何ASP.NET标记内部,包括:HTML、标准 ASP.NET控件、用户控件等等。

使用VS.NET 2003时的问题

一般来说,子页面中的<html>和<body>标签可以省略,因为母版页中已经定义过了。但是如果这两个标签被省略VS.NET 2003的代码提示就会不起作用,所以在编辑时使用这两个标签会比较舒服点。在页面显示时,子页面中的<html>和< body>标签将会被忽略。

顶部

编辑

19.4.1.将子页面与母版页关联

可以用Spring.Web.UI.Page类的Master属性为页面指定母版页。另外还有一个MasterFile属性,可以用文件名指定母版页。

建议使用IoC容器来为页面关联母版页。请看下面的配置:

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">

 <object id="masterPage" type="~/Master.aspx" />
 
 <object id="basePage" abstract="true">
 <property name="Master" ref="masterPage"/>
 </object>
 
 <object type="Child.aspx" parent="basePage">
 <!-- inject other objects that page needs -->
 </object>

</objects>

通过修改其中抽象的对象定义,开发人员可以很容易的为Web应用程序中多个页面同时更改母版页。当然,对特定的上下文或页面来说,可以直接用Master或MasterFile属性覆盖抽象定义中设置的母版页。

顶部



编辑

19.5. 双向数据绑定

目前,ASP.NET的数据绑定存在一个问题:它是单向的。数据绑定可以将页面上的控件绑定到数据模型并显示其信息,但是无法在表单提交时将控件的值提取出来。Spring.Web为ASP.NET添加了双向数据绑定的功能,开发人员可以为页面定义数据绑定规则,在页面生存期内的适当时机,这些绑定规则就会自动被应用。

ASP.NET也不支持回传时的模型管理。的确,它可以用ViewState管理视图状态,但是ViewState只能处理控件的状态,不能处理与控件绑定的表示层模型对象的状态。开发人员一般只能利用HTTP的Session对象在回传时存储数据模型。这就又导致了很多本不应该出现的样板式代码, Spring.Web通过一系列简单模型管理方法,可以消除这些样板代码。

请注意,要想使用双向数据绑定,必须将表示层与Spring.Web紧耦合;这是因为双向数据绑定要求页面扩展Spring.Web的 Page类(而非常规的System.Web.UI.Page类)。

Spring.Web数据绑定的用法很简单。开发人员只需要覆盖受保护的InitializeDataBindings虚方法,在其中为页面定义一系列数据绑定规则。此外,还需要覆盖三个模型管理方法:InitializeModel,LoadModel,SaveModel。我们还是通过 SpringAir参考程序的代码来了解这些功能。首先请看页面的定义:

<%@ Page Language="c#" Inherits="TripForm" CodeFile="TripForm.aspx.cs" %>

<asp:Content ID="body" ContentPlaceHolderID="body" runat="server">
    <div style="text-align: center">
        <h4><asp:Label ID="caption" runat="server"></asp:Label></h4>
        <table>
            <tr class="formLabel">
                <td> </td>
                <td colspan="3">
                    <spring:RadioButtonGroup ID="tripMode" runat="server">
                        <asp:RadioButton ID="OneWay" runat="server" />
                        <asp:RadioButton ID="RoundTrip" runat="server" />
                    </spring:RadioButtonGroup>
                </td>
            </tr>
            <tr>
                <td class="formLabel" align="right">
                    <asp:Label ID="leavingFrom" runat="server" /></td>
                <td nowrap="nowrap">
                    <asp:DropDownList ID="leavingFromAirportCode" runat="server" />
                </td>
                <td class="formLabel" align="right">
                    <asp:Label ID="goingTo" runat="server" /></td>
                <td nowrap="nowrap">
                    <asp:DropDownList ID="goingToAirportCode" runat="server" />
                </td>
            </tr>
            <tr>
                <td class="formLabel" align="right">
                    <asp:Label ID="leavingOn" runat="server" /></td>
                <td nowrap="nowrap">
                    <spring:Calendar ID="departureDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />
                </td>
                <td class="formLabel" align="right">
                    <asp:Label ID="returningOn" runat="server" /></td>
                <td nowrap="nowrap">
                    <div id="returningOnCalendar">
                        <spring:Calendar ID="returnDate" runat="server" Width="75px" AllowEditing="true" Skin="system" />
                    </div>
                </td>
            </tr>
            <tr>
                <td class="buttonBar" colspan="4">
                    <br/>
                    <asp:Button ID="findFlights" runat="server"/></td>
            </tr>
        </table>
    </div>

 </asp:Content>

我们先不要管为什么页面中所有Label控件都没有定义文本值,稍后讲到本地化的时候会讨论这个问题。现在要关心的是:页面中定义了一系列输入控件:tripMode单选按钮组、leavingFromAirportCode和goingToAirportCode下拉列表,以及两个 Spring.NET的日历控件:departureDate和returnDate。

下面我们来看看要绑定到这个窗体上的模型:

namespace SpringAir.Domain
{
    [Serializable]
    public class Trip
    {
        // fields
        private TripMode mode;
        private TripPoint startingFrom;
        private TripPoint returningFrom;

        // constructors
        public Trip()
        {
            this.mode = TripMode.RoundTrip;
            this.startingFrom = new TripPoint();
            this.returningFrom = new TripPoint();
        }

        public Trip(TripMode mode, TripPoint startingFrom, TripPoint returningFrom)
        {
            this.mode = mode;
            this.startingFrom = startingFrom;
            this.returningFrom = returningFrom;
        }

        // properties
        public TripMode Mode
        {
            get { return this.mode; }
            set { this.mode = value; }
        }

        public TripPoint StartingFrom
        {
            get { return this.startingFrom; }
            set { this.startingFrom = value; }
        }

        public TripPoint ReturningFrom
        {
            get { return this.returningFrom; }
            set { this.returningFrom = value; }
        }
    }

    [Serializable]
    public class TripPoint
    {
        // fields
        private string airportCode;
        private DateTime date;

        // constructors
        public TripPoint()
        {}

        public TripPoint(string airportCode, DateTime date)
        {
            this.airportCode = airportCode;
            this.date = date;
        }

        // properties
        public string AirportCode
        {
            get { return this.airportCode; }
            set { this.airportCode = value; }
        }

        public DateTime Date
        {
            get { return this.date; }
            set { this.date = value; }
        }
    }

    [Serializable]
    public enum TripMode 
    {
        OneWay,
        RoundTrip
    }
}

Trip类使用两个TripPoint类型的属性StartingFrom和ReturningFrom分别表示出发和返程日期。同时用一个TripMode枚举类型的属性来表示旅程是单程还是往返。

最后,Code-behind文件中的类将所有对象组合在一起:

public class TripForm : Spring.Web.UI.Page
{
    // model 
    private Trip trip;
    public Trip Trip
    {
        get { return trip; }
        set { trip = value; }
    }

    // service dependency, injected by Spring IoC container
    private IBookingAgent bookingAgent;
    public IBookingAgent BookingAgent
    {
        set { bookingAgent = value; }
    }

    // model management methods
    protected override void InitializeModel()
    {
        trip = new Trip();
        trip.Mode = TripMode.RoundTrip;
        trip.StartingFrom.Date = DateTime.Today;
        trip.ReturningFrom.Date = DateTime.Today.AddDays(1);
    }

    protected override void LoadModel(object savedModel)
    {
        trip = (Trip) savedModel;
    }

    protected override object SaveModel()
    {
        return trip;
    }

    // data binding rules 
    protected override void InitializeDataBindings()
    {
        BindingManager.AddBinding("tripMode.Value", "Trip.Mode");
        BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.AirportCode");
        BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.AirportCode");
        BindingManager.AddBinding("departureDate.SelectedDate", "Trip.StartingFrom.Date");
        BindingManager.AddBinding("returnDate.SelectedDate", "Trip.ReturningFrom.Date");
    }

    // event handler for findFlights button, uses injected 'bookingAgent' 
    // service and model 'trip' object to find flights
    private void SearchForFlights(object sender, EventArgs e)
    {
        FlightSuggestions suggestions = bookingAgent.SuggestFlights(trip);
        if (suggestions.HasOutboundFlights)
        {
            // redirect to SuggestedFlights page
        }
    }
}

这段看似简单的代码背后其实发生了很多事情,很值得我们逐一的讨论一下:
  • 在第一次请求页面时(IsPostback==false), InitializeModel方法被调用,在其中创建了一个Trip对象并初始化了它的属性。在页面显示之前,SaveModel方法会被调用,它的返回值会保存到HTTP Session中。最后,在每次回传时,LoadModal方法会被调用,传递给它的参数就是上次SaveModel方法保存到Session中的值。

对本例来说,这一过程非常简单:我们的模型只有一个trip对象,SaveModel方法也只返回trip对象,LoadModel只要把 savedModel参数转型为Trip,然后赋值给页面的trip字段。在稍微复杂的场合中,一般需要用SaveModel返回一个包含模型对象的字典,并且在LoadModel方法中读取字典的值。

  • 通过调用页面属性BindingManager的AddBinding方法,InitializeDataBindings方法为窗体上的五个输入控件分别定义了绑定规则。AddBinding方法有很多重载,除了上面代码中用到的源和目标的绑定表达式,还可以指定绑定方向和用于格式化绑定值的IFormatter对象。稍后我们会简单的讨论这两个可选参数,现在我们来看一下源和目标的绑定表达式:

数据绑定框架使用Spring.NET的表达式语言来定义绑定表达式。在大多数情况下,都会象上面的例子一样将源和目标表达式都解析为某个控件或数据模型的属性或字段名,尤其是在使用双向数据绑定的时候,因为此时这两个绑定表达式都是“可设置的”。关于InitializeDataBindings方法,有一点很重要(比应该在此方法中定义数据绑定规则这一点还要重要),就是该方法只会为每个页面类型执行一次。基本上,所有的绑定表达式都在页面第一次被初始化的时候解析并缓存,随后由该页面的所有实例共同使用。这样做主要是出于性能方面的考虑,因为在每次回传时都解析绑定表达式是没有必要的,反会增加页面的总体处理时间。

  • 注意SearchForFlights事件处理器并没有依赖界面元素,它只是使用注入进来的bookingAgent服务和之前绑定到UI控件的trip对象来获取一个推荐给客户的航班列表。另外,如果在事件处理器中对trip对象进行了修改,那么绑定的对象就会在页面显示前被更新。

这就实现了我们的首要目标:消除页面事件处理器对界面元素的依赖,将控制器与视图解耦。

  • (按:提示1、在定义页面的数据模型时,虽然从逻辑上一个私有的字段就足够实现所有功能,但请记住这个模型是要给Spring.NET的表达式语言访问的,所以请一定为数据模型定义一个公共属性,可以参考上例中TripForm页面的Trip属性;提示2、InitializeModel()方法的调用时机是当 IsPostBack为false时的Page_Init事件之前,请注意它和页面事件触发时间的先后。)

我们已经对使用Spring.NET在Web应用程序中进行数据绑定和模型管理有了一个总体的了解,下面,我们来深入讨论一些细节问题,比如数据绑定在后台究竟是怎样实现的?它都扩展了哪些部分?以及其它一些在实际开发很有用的功能。

顶部

编辑

19.5.1.数据绑定的后台实现

Spring.NET的数据绑定框架主要是围绕两个接口来实现的:IBinding和IBindingContainer。其中IBinding最为重要,所有的绑定类型都要实现它。该接口定义了几个方法,为方便使用,其中一些是有重载的:

public interface IBinding
{
    void BindSourceToTarget(object source, object target, ValidationErrors validationErrors);
    void BindSourceToTarget(object source, object target, ValidationErrors validationErrors, IDictionary variables);
    
    void BindTargetToSource(object source, object target, ValidationErrors validationErrors);
    void BindTargetToSource(object source, object target, ValidationErrors validationErrors, IDictionary variables);
    
    void SetErrorMessage(string messageId, params string[] errorProviders);
}

从这些方法的名称上我们大致可以看出它们的用途。BindSourceToTarget方法用于从源对象提取绑定的值并复制给目标对象; BindTargetToSource的功能则与之相反。这两个方法的命名方式很通用,连参数都是常用的类型——这是因为,数据绑定框架实际上可以将任意两个对象绑定起来,虽然最常见的情况是将Web窗体绑定到模型对象,而且Spring.NET已经将该功能紧密集成在Web框架中,但这只是数据绑定框架诸多应用中的一例而已。

我们需要单独说明一下validationErrors参数。虽然数据绑定框架没有和验证框架绑定在一起,但它们实际上还是有一定关系的。比如说,数据验证框架最适于根据业务规则验证赋值给模型的数据,但在绑定的过程中,数据绑定框架则更适合对数据的类型进行验证。虽然这两种验证是不同的,但都应该用统一的方式将错误信息显示给用户。为此,Spring.Web传递给绑定方法的参数和传递给验证对象的参数是同一个ValidationErrors实例。这就保证了所有的错误信息都能被统一管理,并通过Spring.Web的验证错误显示控件以统一的方式显示给最终用户。

IBinding接口的最后一个方法是SetErrorMessage,在绑定时如果发生错误,开发人员可以用这个方法指定一个错误信息的资源ID,以及用来显示错误信息的Provider列表。稍后我们会通过一个小例子来学习SetErrorMessage的用法。

IBindingContainer接口扩展了IBinding,添加了以下成员:

public interface IBindingContainer : IBinding
{
    bool HasBindings { get; }

    IBinding AddBinding(IBinding binding);
    IBinding AddBinding(string sourceExpression, string targetExpression);
    IBinding AddBinding(string sourceExpression, string targetExpression, BindingDirection direction);
    IBinding AddBinding(string sourceExpression, string targetExpression, IFormatter formatter);
    IBinding AddBinding(string sourceExpression, string targetExpression, BindingDirection direction, IFormatter formatter);
}

可以看到,这个接口定义了一组重载的AddBinding方法。其中AddBinding(IBinding binding)是最通用的一个,可以向IBindingContainer中添加任意的绑定类型。其余四个重载是为了使用方便,可以用它们来添加最常用的绑定类型:SimpleExpressionBinding。SimpleExpressionBinding就是我们在本节一开始的例子中用来将 Trip对象和Web窗体绑定在一起的类型。该类使用Spring.NET的表达式语言从源对象提取绑定值,并赋值给目标对象。之前我们已经讨论过 AddBinding方法的sourceExpression和targetExpression参数,现在来看另外两个。

顶部

编辑

19.5.1.1.绑定方向

参数direction用来确定数据绑定是单向还是双向。默认情况下,所有绑定都是双向的,除非将direction参数设为 BindingDirection.SourceToTarget或BindingDirection.TargetToSource,此时,绑定只在某一个方向上的方法被调用时进行,另一个方向上的方法调用会被忽略。在将模型绑定到非输入控件比如Label时,单向绑定比较适合。

如果Web窗体和表示层模型之间不是简单一对一关系,单向绑定也是很有用的。在前面的例子中,我们刻意将表示层模型(trip对象)设计为简单的一对一映射。为了方便讨论,现在我们添加一个Airport类,并修改原来的TripPoint类:

namespace SpringAir.Domain
{
    [Serializable]
    public class TripPoint
    {
        // fields
        private Airport airport;
        private DateTime date;

        // constructors
        public TripPoint()
        {}

        public TripPoint(Airport airport, DateTime date)
        {
            this.airport = airport;
            this.date = date;
        }

        // properties
        public Airport Airport
        {
            get { return this.airport; }
            set { this.airport = value; }
        }

        public DateTime Date
        {
            get { return this.date; }
            set { this.date = value; }
        }
    }

    [Serializable]
    public class Airport 
    {
        // fields
        private string code;
        private string name;
 
       // properties
        public string Code
        {
            get { return this.code; }
            set { this.code = value; }
        }

        public string Name
        {
            get { return this.name; }
            set { this.name = value; }
        }
    }
}

除了字符串属性AirportCode,TripPoint类又添加了一个Airport类型的属性:Airport。该类型的定义见上面的代码。现在我们有一个问题:先前AirportCode只是简单的字符串到字符串的绑定,窗体中下拉框的列表项和TripPoint.AirportCode之间可以直接赋值;现在变成了字符串到Airport类之间的绑定,所以我们需要解决这个类型不兼容的问题:

首先,从模型到控件的绑定还是很顺利的。我们只需要将绑定设置为从模型到控件的单向绑定:

protected override void InitializeDataBindings()
{
    BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.Airport.Code", BindingDirection.TargetToSource);
    BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.Airport.Code", BindingDirection.TargetToSource);
    ...
}

我们只需要将机场的代码从Trip.StartingFrom.Airport.Code中提取出来赋值给控件,注意这次并不是 Trip.StartingFrom.AirportCode。但是很遗憾,用同样的方式再从控件绑定到模型就不行了:虽然我们也可以把Airport的 Code属性直接(双向)绑定到控件,但是这会使Airport的Name属性无法被赋值。其实,我们要做的是根据机场的代码查找一个Airport对象,并把它赋值给TripPoint.Airport属性。好在Spring.NET的数据绑定能很容易的实现这一点:在Spring.Air中,我们已经在IoC容器内定义了一个airportDao对象,通过它的GetAirport(string airportCode)方法就可以查找机场数据。我们只要再定义一个从源到目标(模型,即Trip.StartingFrom.Airport属性)的单向绑定规则,并将源定义为表达式——一个调用airportDao对象的GetAirport(string airportCode)方法、从而根据控件值获取Airport对象的表达式。下面代码为两个下拉列表定义了完整的绑定规则:

protected override void InitializeDataBindings()
{
    BindingManager.AddBinding("@(airportDao).GetAirport(#root.leavingFromAirportCode.SelectedValue)", "Trip.StartingFrom.Airport", BindingDirection.SourceToTarget);
    BindingManager.AddBinding("leavingFromAirportCode.SelectedValue", "Trip.StartingFrom.Airport.Code", BindingDirection.TargetToSource);
    
    BindingManager.AddBinding("@(airportDao).GetAirport(#root.goingToAirportCode.SelectedValue)", "Trip.ReturningFrom.Airport", BindingDirection.SourceToTarget);
    BindingManager.AddBinding("goingToAirportCode.SelectedValue", "Trip.ReturningFrom.Airport.Code", BindingDirection.TargetToSource);
    ...
}

这就行了——定义两个使用不同表达式的单向绑定规则,并利用表达式从IoC容器中获取对象的功能,我们就可以解决这个很不一般的数据绑定问题。

按:注意上面的绑定表达式中的#root,此处必须在控件名称前添加#root来引用表达式所在的“根环境”,原文例子有错误。请参见第十章相关内容。

顶部

编辑

19.5.1.2. Formatters

AddBinding方法最后一个参数是formatter。我们可以通过这个参数指定一个格式,在将控件值绑定到模型前,用指定的格式来解析(控件的)字符串值;并在将模型值绑定到控件前,用来格式化(模型的)强类型值。

一般情况下,我们可以使用Spring.Globalization.Formatters命名空间中的Formatter类。如果标准的Formatter不能满足要求,也可以创建自己的Formatter类——只要实现IFormatter接口即可:

public interface IFormatter
{
    string Format(object value);
    object Parse(string value);
}

Spring.NET提供的标准Formatter包括:CurrencyFormatter、DateTimeFormatter、 FloatFormatter、IntegerFormatter、NumberFormatter和PercentFormatter,应该可以满足大多数需要了。

顶部

编辑

19.5.1.3.类型转换

因为数据绑定框架和IoC容器都使用相同的表达式求值引擎,所以能够利用所有已注册的类型转换器来执行数据绑定操作。Spring.NET本身已经自动注册了很多类型转换器(参见Spring.Objects.TypeConverters命名空间),不过如果需要,我们还可以实现自定义的类型转换器,并通过标准的Spring.NET类型转换器注册机制将它们注册到Spring.NET中。

顶部

编辑

19.5.1.4. 数据绑定事件

Spring.Web的Page类添加了两个与数据绑定相关的页面事件——DataBound和DataUnbound。

DataUnbound事件在数据模型被控件值更新后触发,且仅当PostBack时在Load事件之后触发,因为使用控件的初始值更新数据模型是没有意义的。

DataBound事件在控件值更新为数据模型值后触发。触发的时机是在PreRender事件之前。

数据模型的更新发生在Load事件之后,这样能确保DataUnbound事件处理器使用的是更新以后的数据模型;控件值的更新在PreRender事件之前,这样能确保数据模型的变化能够立即反映到控件中。

(按:请注意,双向数据绑定的主要目的是将MVC的各部分解耦,它管理的是UI控件值和模型对象属性之间(一对一)的映射关系,而不是控件状态的初始化。双向绑定无法完成诸如向列表控件绑定DataSource这样的功能,在页面类中,这种纯粹的表示层逻辑还是要通过DataSource属性和 DataBind()方法完成。读者可以参考SpringAir项目的相关代码。)

顶部

编辑

19.6. 本地化

虽然.NET框架对本地化的支持相对出色,但ASP.NET 1.x在这方面却存在缺憾。

在ASP.NET 1.1项目中,每个.aspx页面都有相关联的资源文件,但ASP.NET 1.1却没有去用这些资源文件。ASP.NET 2.0更正了这一问题,允许开发人员使用页面的本地资源。同时,Spring.Web对此也提供了支持。(按:原文撰写时2.0还是“将来”的事情,译文有些微改动。)

Spring.Web支持多种本地化方式,必要时可以混合使用。Spring.Web的本地化支持推模型和拉模型,并且在找不到本地资源时还可以退而查找全局资源。同时,Spring.Web也支持用户语言文化信息管理和图像本地化,稍后会讨论这些内容。(按:资源的“全局”和“本地”是指其在项目中的适用范围,本地资源仅和某个页面相关联。)

提示

请参考:Globalization Architecture for ASP.NETLocalization Practices for ASP.NET 2.0,这是两篇关于ASP.NET国际化和本地化技术的介绍性材料,作者:Michele Leroux Bustamante.

顶部

编辑

19.6.1.使用Localizer进行自动本地化(“推”模型的本地化)

“推”模型本地化的核心思想是:由开发人员在资源文件中为页面指定本地化资源,框架自动将这些资源应用到页面的控件中去。比如,开发人员创建了下面的页面UserRegistration.aspx...

<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %>

<%@ Page language="c#" Codebehind="UserRegistration.aspx.cs" 
    AutoEventWireup="false" Inherits="ArtFair.Web.UI.Forms.UserRegistration" %>
<html>
    <body>
        <spring:Content id="mainContent" contentPlaceholderId="main" runat="server">
            <div align="right">
                <asp:LinkButton ID="english" Runat="server" CommandArgument="en-US">English</asp:LinkButton> 
                <asp:LinkButton ID="serbian" Runat="server" CommandArgument="sr-SP-Latn">Srpski</asp:LinkButton>
            </div>
            <table>
                <tr>
                    <td><asp:Label id="emailLabel" Runat="server"/></td>
                    <td><asp:TextBox id="email" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="passwordLabel" Runat="server"/></td>
                    <td><asp:TextBox id="password" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="passwordConfirmationLabel" Runat="server"/></td>
                    <td><asp:TextBox id="passwordConfirmation" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="nameLabel" Runat="server"/></td>
                    <td><asp:TextBox id="name" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="street1Label" Runat="server"/></td>
                    <td><asp:TextBox id="street1" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="street2Label" Runat="server"/></td>
                    <td><asp:TextBox id="street2" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="cityLabel" Runat="server"/></td>
                    <td><asp:TextBox id="city" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="stateLabel" Runat="server"/></td>
                    <td><asp:TextBox id="state" Runat="server" Width="30px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="postalCodeLabel" Runat="server"/></td>
                    <td><asp:TextBox id="postalCode" Runat="server" Width="60px"/></td>
                </tr>
                <tr>
                    <td><asp:Label id="countryLabel" Runat="server"/></td>
                    <td><asp:TextBox id="country" Runat="server" Width="150px"/></td>
                </tr>
                <tr>
                    <td colspan="2">
                        <asp:Button id="saveButton" Runat="server"/> 
                        <asp:Button id="cancelButton" Runat="server"/>
                    </td>
                </tr>
            </table>
        </spring:Content>
    </body>
</html>

仔细看看上面的代码,我们发现其中所有的Label或Button控件都没有设置Text属性。这些控件的Text属性值都保存在页面的本地资源文件中,并且用下面这种格式的字符串来标识:

$this.controlId.propertyName

对应的本地资源文件UserRegistration.aspx.resx如下所示:

<root>
  <data name="$this.emailLabel.Text">
    <value>Email:</value>
  </data>
  <data name="$this.passwordLabel.Text">
    <value>Password:</value>
  </data>
  <data name="$this.passwordConfirmationLabel.Text">
    <value>Confirm password:</value>
  </data>
  <data name="$this.nameLabel.Text">
    <value>Full name:</value>
  </data>
  <data name="$this.street1Label.Text">
    <value>Street 1:</value>
  </data>
  <data name="$this.street2Label.Text">
    <value>Street 2:</value>
  </data>
  <data name="$this.cityLabel.Text">
    <value>City:</value>
  </data>
  <data name="$this.stateLabel.Text">
    <value>State:</value>
  </data>
  <data name="$this.postalCodeLabel.Text">
    <value>ZIP:</value>
  </data>
  <data name="$this.countryLabel.Text">
    <value>Country:</value>
  </data>
  <data name="$this.saveButton.Text">
    <value>$messageSource.save</value>
  </data>
  <data name="$this.cancelButton.Text">
    <value>$messageSource.cancel</value>
  </data>
</root>

其中最后两项需要特别解释一下。有时候需要将某些资源定义为全局资源。在本例中,我们会在整个应用程序中使用Save和Cancel按钮,所以将它们的资源定义为全局资源比较合适。

在本地资源文件中定义全局资源的方法,是将它们的属性设置为包含以下前缀的资源重定位表达式:

$messageSource.

Loclizer会使用value值中的“save”和“cancel”作为查询键值,从全局的消息源中获取最终的文本。有一点要记住的是,资源重定位的目标值只需要定义一次,一般来说要定义在固定语言文化(Invariant culture)资源文件中——所有重定位资源的查询操作都会以固定语言文化为依据,所以能够确保使用正确的语言文化进行全局消息源查询。

提示

要在VS.NET中查看页面的.resx文件,需要点击“Project/Show All Files”。该菜单项启用之后,就可以看到.resx文件象页面文件的一个“子节点”一样显示在解决方案浏览器中。

VS.NET 创建的.resx文件会包含一个xds:schemaElement和数个reshead节点。data节点出现在reshead节点之后。如果要修改. resx文件,可以在Windows资源管理器的右键菜单中选择"Open With",然后选择适当的源码编辑器。

顶部

编辑

19.6.2.使用Localizer

要想自动应用资源,需要向每个页面中注入一个Localizer对象(可以配置在一个抽象的页面对象定义中,然后让其它页面对象定义继承它)。注入给页面的Localizer会在页面第一次被请求时检查资源文件、将其中所有以“$this”开头的资源项的值加入缓存,并在页面显示前将这些值应用到页面的控件上。

Localizer是一个实现了Spring.Globalization.ILocalizer接口的对象。Spring.Net中有一个抽象基类以供继承,Spring.Globalization.AbstractLocalizer:该类包含一个抽象方法LoadResources。在实现类中,这个方法必须读取并返回所有需要自动应用的资源的列表。

Spring.Net还提供了一个具体的实现类, Spring.Globalization.Localizers.ResourceSetLocalizer,该类可以从本地资源文件中返回资源列表。将来Spring.NET还会提供其它实现类,以便从XML文件甚至是包含资源name-value对的文本文件中读取资源,这样开发人员就可以将资源保存到Web应用程序的文件中,而不需要嵌入到程序集内。当然,如果需要将资源保存在数据库中,也可以自己实现ILocalizer。

前面提到过,一般我们会在一个抽象页面对象定义中配置Localizer,再由需要它的页面继承,如下:

<object id="localizer" type="Spring.Globalization.Localizers.ResourceSetLocalizer, Spring.Core"/>
<object id="basePage" abstract="true">
    <description>
        Pages that reference this definition as their parent
        (see examples below) will automatically inherit following properties.
    </description>
    <property name="Localizer" ref="localizer"/>
</object>

当然,开发人员完全可以为每个页面单独配置Localizer;在任何情况下都可以用单独配置的Localizer覆盖抽象定义中的Localizer。另外,如果不需要自动应用资源,就可以不配置Localizer。

最后要注意,Spring.Web的UserControl会从它们所在的页面上继承Localizer和其它本地化的设置,但同样可以通过显式的依赖注入将其覆盖。

顶部

编辑

19.6.3.手动应用资源(“拉”模型的本地化)

虽然自动资源本地化在多数情况下都能满足要求,但对迭代控件中的内嵌控件却是无能为力,因为这些控件的ID是不确定的。同样,如果需要在同一页面中多次使用同一资源,自动方式也无法解决问题。例如,SpringAir示例程序中航班列表的标题列。(请参见:第二十九章,SpringAir - 参考程序)

在这些情况下,需要借助拉模型的本地化功能,方式很简单:调用GetMessage方法,如下:

<asp:Repeater id="outboundFlightList" Runat="server">
  <HeaderTemplate>
    <table border="0" width="90%" cellpadding="0" cellspacing="0" align="center" class="suggestedTable">
      <thead>
        <tr class="suggestedTableCaption">
          <th colspan="6">
            <%= GetMessage("outboundFlights") %>
          </th>
        </tr>
        <tr class="suggestedTableColnames">
          <th><%= GetMessage("flightNumber") %></th>
          <th><%= GetMessage("departureDate") %></th>
          <th><%= GetMessage("departureAirport") %></th>
          <th><%= GetMessage("destinationAirport") %></th>
          <th><%= GetMessage("aircraft") %></th>
          <th><%= GetMessage("seatPlan") %></th>
        </tr>
      </thead>
      <tbody>
  </HeaderTemplate>

Spring.Web.UI.Page和Spring.Web.UI.UserControls类中都实现了GetMessage方法,如果在本地资源中找不到所请求的项,该方法会自动去全局消息源中查找。

顶部

编辑

19.6.4.在Web应用程序中进行图像本地化

Spring.Web支持在Web应用程序中用简单(且统一)的方式进行图像本地化。在一般的Web应用程序中,图像资源与文本资源不同,文本资源可以保存在内嵌的资源文件、XML文件甚至数据库中,而图像资源一般保存在文件系统的文件中。在Spring.Web的支持下,通过一个自定义控件,并结合目录命名规范,本地化图像资源就象本地化文本资源一样简单。

Spring.Web的Page类有一个ImageRoot属性,用于定义存放图像文件的根目录。该属性的默认值是“Images”,也就是说Localizer会在应用程序根目录的Images文件夹中查找图像资源。不过在页面定义中可以将该属性改为任意路径名。

为进行图像本地化,需要在ImageRoot属性指向的目录下为每种要支持的语言文化定义一个子目录,比如:

/MyApp
   /Images
      /en
      /en-US
      /fr
      /fr-CA
      /sr-SP-Cyrl
      /sr-SP-Latn
      ...

目录层次建立之后,就只需将图像文件放置在各个子目录中了,同一图像在不同子目录中的文件名要相同。要在页面中进行图像本地化,需要定义一个<spring.LocalizedImage>控件:

<%@ Page language="c#" Codebehind="StandardTemplate.aspx.cs" 
                AutoEventWireup="false" Inherits="SpringAir.Web.StandardTemplate" %>
<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
  <body>
    <spring:LocalizedImage id="logoImage" imageName="spring-air-logo.jpg" borderWidth="0" runat="server" />
  </body>
</html>

该控件会使用标准的本地化规则和用户的语言文化信息在相应目录下查找指定的图片文件。例如,如果用户的语言文化是“en-US”, Localizer就会在Images/en-US目录中查找spring-air-logo.jpg文件,如果找不到,转而搜索Images/en目录,如果仍找不到,就会在Images目录下查找(该文件夹一般用于存放通用的文件)。

顶部

编辑

19.6.5.全局资源

全局资源是(以应用程序上下文为单位的、)定义在容器中的、以保留字“messageSource”命名的普通对象定义。开发人员可以在容器的配置文件中添加下面的对象定义。

<object id="messageSource" type="Spring.Context.Support.ResourceSetMessageSource, Spring.Core">
    <property name="ResourceManagers">
        <list>
            <value>MyApp.Web.Resources.Strings, MyApp.Web</value>
        </list>
    </property>    
</object>

全局资源会被IApplicationContext缓存,并可以通过IMessageSource接口访问。

Spring.Web的Page和UserControl类都定义了相应的属性来引用它们所在的应用程序上下文,以及与上下文关联的IMessageSource。这样以来,如果Page或UserControl在本地资源中找不到某个资源,就会自动去全局资源中查找。

目前,ResourceSetMessageSource是惟一随Spring.NET一同发布的IMessageSource实现类。

顶部

编辑

19.6.6.用户语言文化管理

除了全局和本地资源管理,Spring.Web也支持用户语言文化管理,在Spring.Web的Page和UserControl类中,都可用UserCulture属性访问的当前的CultureInfo值。

UserCulture属性将区域信息的解析工作代理给Spring.Globalization.ICultureResolver接口的实现类。开发人员可以在页面的对象定义中配置要使用的语言文化解析器类,如下:

<object id="BasePage" abstract="true">
    <property name="CultureResolver">
        <object type="Spring.Globalization.Resolvers.CookieCultureResolver, Spring.Web"/>
    </property>
</object>

Spring.Web提供了几个很有用的ICultureResolver实现类,开发人员不需要自行实现该接口。不过如果的确需要创建自己的实现类,过程也是很简单的,因为只有两个方法需要实现。下面几节讨论Spring.Web提供的几个ICultureResolver实现类。

顶部

编辑

19.6.6.1. DefaultWebCultureResolver

该类在Spring.Web中是默认的ICultureResolver实现类。如果没有为页面对象显式配置语言文化解析器,就会自动使用该类。有时候显式配置该类也是十分有用的,比如说,有时需要设置它的DefaultCulture属性来指定一个默认的语言文化。

DefaultWebCultureResolver会首先检查自己的DefaultCulture属性,如果不为空,就直接返回它的值。若 DefaultCulture属性值为空就会去检查HTTP请求的头信息,最后,如果头信息中没有包括“Accept-Lang”,就会返回当前线程的 UI语言文化。

顶部

编辑

19.6.6.2. RequestCultureResolver

该类的工作方法与DefaultWebCultureResolver类似,只是它会先检查HTTP请求的头信息,再检查DefaultCulture属性的值,最后使用与当前线程相关的语言文化。

顶部

编辑

19.6.6.3. SessionCultureResolver

该类会在用户会话中查找语言文化信息,如果找到就返回,如果找不到,其行为就和DefaultWebCultureResolver一样。

顶部

编辑

19.6.6.4. CookieCultureResolver

该类会在cookie中查找语言文化信息,如果找到就返回,如果找不到,其行为就和DefaultWebCultureResolver一样。

警告

如果使用localhost作为URL来请求页面,CookieCultureResolver就不起作用,因为一般情况下会认为这是在开发环境中发起的请求。

为克服这一局限,应该在开发时使用SessionCultureResolver,而在布署前改为CookieCultureResolver。在Spring.Web中这是很容易做到的(通过修改容器的配置),但还是请注意到这一点。

顶部

编辑

19.6.7.更改语言文化

要想更改语言文化,必须选用支持该功能的语言文化解析器(按:即选择的解析器必须能够保存新的CultureInfo值),例如 SessionCultureResolver或CookieCultureResolver。如果需要将语言文化信息保存到数据库中作为用户配置的一部分,则可以创建自己的ICultureResolver实现类。

只要选定了合适的语言文化解析器,剩下的就是在页面显示之前将UserCulture属性值改为新的CultureInfo对象了。在下面的例子中,页面上有两个LinkButton用于改变用户的语言,在事件处理器中,只需用下面的代码就可以完成语言文化的更改。这段代码摘自 UserRegistration.aspx.cs:

protected override void OnInit(EventArgs e)
{
    InitializeComponent();

    this.english.Command += new CommandEventHandler(this.SetLanguage);
    this.serbian.Command += new CommandEventHandler(this.SetLanguage);

    base.OnInit(e);
}

private void SetLanguage(object sender, CommandEventArgs e)
{
    this.UserCulture = new CultureInfo((string) e.CommandArgument);
}

(按:原文的这段描述不甚详细,在此请注意两点:1、只有当向页面注入了CultureResolver对象之后,才能对UserCulture属性赋值,另外,别忘了注入Localizer;2、UserCulture被改变之后,CultureResolver对象会将新的 UserCulture值保存到相应位置。若要对其它页面自动产生影响,需要向这些页面注入同一种类的CultureResovler,这样,当这些页面被请求时,CultureResolver会在页面显示前先读出已存的CultureInfo对象,并用它改变当前页面的UserCulture属性,随后,注入给页面的Localizer对象就可以利用该属性到正确的资源文件中读取资源。读者可以参考ICultureResolver接口实现类(如 SessionCultureResolver)的源码。

还有一个问题需要特别注意:Spring.Web.UI.MasterPage是没有CultureResolver属性的,如果不做其它处理,不可能在MasterPage中直接修改UserCulture属性!但是,除非有特殊要求,语言文化的修改选项必定设计在MasterPage上,这个问题该怎么解决?先提示一下读者——打开SpringAir参考项目,看一下Config/Web.xml文件中名为standardPage的对象定义。然后我们再来说明一下,若要在MasterPage中更改UserCulture,必须要向其内容页中:1、注入合适的ICultureResolver 对象。2、注入Localizer。3、也是关键所在,必须在内容页的对象定义中显式设置MasterPageFile属性值为MasterPage的页面名称。这样,在内容页被创建之后,MasterPage才能对语言文化进行设置。注意,如果有某个内容页没有显式设置MasterPageFile属性,那么再请求这个页面时,MasterPage中修改UserCulture的代码同样会出错,所以我们需要为每个内容页的对象定义都设置 MasterPageFile。最好的方式是将CultureResolver、Localizer和MasterPageFile集中定义在同一个父对象定义中,让其它页面对象定义去继承它,如同SpringAir中的standardPage对象定义一样。)

顶部

编辑

19.7. 结果映射

ASP.NET一个很显著的问题是没有任何手段将应用程序的流程控制移出代码之外。最常用的方式是在页面的事件处理器中硬编码,调用Response.Redirect和Server.Transer方法将页面重新定向。

这种方法是有问题的,程序流程上的任何变化都会导致代码的变化(以及随之而来的重新编译、测试、重新布署等等)。比较好的方法是将操作结果和目标页面的映射关系移到程序外部,这种方式已被很多基于MVC (Model-View-Controller)的Web框架证明是成功的。

Spring.Web支持这种功能,开发人员可以在页面的对象定义中配置结果映射集,然后在事件处理器中使用结果映射的逻辑名称来控制应用程序的流程。

Spring.Web使用Result类来封装并定义逻辑结果,通过Result类,可以象配置普通对象一样配置结果映射集:

<objects xmlns="http://www.springframework.net">

    <object id="homePageResult" type="Spring.Web.Support.Result, Spring.Web">
        <property name="TargetPage" value="~/Default.aspx"/>
        <property name="Mode" value="Transfer"/>
        <property name="Parameters">
            <dictionary>
                <entry key="literal" value="My Text"/>
                <entry key="name" value="${UserInfo.FullName}"/>
                <entry key="host" value="${Request.UserHostName}"/>
            </dictionary>
        </property>
    </object>

    <object id="loginPageResult" type="Spring.Web.Support.Result, Spring.Web">
        <property name="TargetPage" value="Login.aspx"/>
        <property name="Mode" value="Redirect"/>
    </object>

    <object type="UserRegistration.aspx" parent="basePage">
        <property name="UserManager" ref="userManager"/>
        <property name="Results">
            <dictionary>
                <entry key="userSaved" value-ref="homePageResult"/>
                <entry key="cancel" value-ref="loginPageResult"/>
            </dictionary>
        </property> 
    </object>

</objects>

在Result的对象定义中,必须配置TargetPage属性的值。Mode属性值可以是Transfer或Redirect,默认为Transfer。

如果目标页面需要参数,可以通过Result的Parameters属性定义。参数的值既可以是文本值,也可以是对象表达式;页面所在的容器会对表达式进行求值;在本例中,任何使用homePageResult的页面都必须定义一个UserInfo属性。

根据Result对象Mode属性值的不同,参数的处理方式也不同。对于Redirect模式的Result对象来说,所有参数都会先转换为字符串,然后将URL编码,最后附加在重定向的请求字符串上。而对于Transfer模式的Result对象,参数会在请求被转发给目标页面前被添加到 HttpContext.Items集合中。也就是说Transfer模式可以在页面间传递任何对象,所以更为灵活。同时Transfer模式也更为高效,因为此时不需要在服务器和客户端之间产生额外的往返过程,所以建议优先Transfer模式(这也是它是默认模式的原因)。

上例中,Result的对象定义是独立的,可以被所有页面引用,比如主页和登录页面。如果Result对象只被一个页面使用,应该将Result内嵌在页面对象定义的内部,此时可以用内嵌对象定义,也可以用专门的Result快捷语法(见下面代码中名为userSaved的Results值)。

<object type="~/UI/Forms/UserRegistration.aspx" parent="basePage">
        <property name="UserManager">
            <ref object="userManager"/>
        </property>
        <property name="Results">
            <dictionary>
                <entry key="userSaved" value="redirect:UserRegistered.aspx?status=Registration Successful,user=${UserInfo}"/>
                <entry key="cancel" value-ref="homePageResult"/>
            </dictionary>
        </property> 
</object>

如果使用快捷方式定义内嵌的Result,为页面Results属性添加的字典项值(即value属性的值)必须满足下面的格式...

[<mode>:]<targetPage>[?param1,param2,...,paramN]
其中<mode>的值前面讲过,就是...

redirect
transfer

要注意此处使用逗号来分隔参数,而不像URL中那样使用&符号,以免需要在XML中进行转义。如果一定要用&符号,应该使用&的实体引用(按:即“&”)。

定义了Result对象之后,就很容易在页面的事件处理器中使用了(下面代码取自UserRegistration.aspx.cs)...

private void SaveUser(object sender, EventArgs e)
{
    UserManager.SaveUser(UserInfo);
    SetResult("userSaved");
}

public void Cancel(object sender, EventArgs e)
{
    SetResult("cancel");
}

protected override void OnInit(EventArgs e)
{
    InitializeComponent();

    this.saveButton.Click += new EventHandler(this.SaveUser);
    this.cancelButton.Click += new EventHandler(this.Cancel);

    base.OnInit(e);
}

当然我们可以在上面的代码中使用常量来代替字符串。如果一个逻辑结果(比如“home”)会被多个页面引用,使用常量就比较合适。

顶部

编辑

19.8. 客户端脚本

通过Page类的RegisterClientScriptBlock和RegisterStartupScript方法,ASP.NET对客户端脚本的支持还算马马虎虎。

但是,这两个方法都不能向页面的节点中添加脚本,而很多情况下我们的确需要这么做。

顶部

编辑

19.8.1.在HTML的head节点中注册客户端脚本

Spring.Web为Spring.Web.UI.Page类增加了个方法,以增强对客户端脚本的支持,其中包括 RegisterHeadScriptBlock和RegisterHeadScriptFile,这两个方法都有自己的重载。在Page或用户控件中,可以用这两个方法将客户端脚本代码或脚本文件添加到HTML页面的<head>节点中。

为此,惟一要做的特殊工作就是需要用服务端控件<spring:Head>来定义页面的<head>节点,而不能用标准HTML的<head>节点。请看下面的例子:

<%@ Page language="c#" Codebehind="StandardTemplate.aspx.cs" 
                AutoEventWireup="false" Inherits="SpringAir.Web.StandardTemplate" %>

<%@ Register TagPrefix="spring" Namespace="Spring.Web.UI.Controls" Assembly="Spring.Web" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
  <spring:Head runat="server" id="Head1">
    <title>
      <spring:ContentPlaceHolder id="title" runat="server">
        <%= GetMessage("default.title") %>

      </spring:ContentPlaceHolder>
    </title>
    <LINK href="<%= CssRoot %>/default.css" type="text/css" rel="stylesheet">
    <spring:ContentPlaceHolder id="head" runat="server"></spring:ContentPlaceHolder>
  </spring:Head>

  <body>
  ...
  </body>
</html>

本例可以看到如何向Master页面添加<head>节点,以便子页面能用<spring: ContentPlaceholder>控件改变页面的标题,并向<head>节点中添加其它元素。其中,惟一与前文提到的 Register*方法有关的是<srping:Head>控件。

TODO : insert example

顶部

编辑

19.8.2.向<head>节点中添加CSS定义

使用前面提到的两个方法,我们也可以向页面中添加CSS文件。或者使用更为直接的方式,调用Spring.Web.UI.Page类的 RegisterStyle和RegisterStyleFile方法将CSS定义直接添加到<head>节点中。 RegisterStyleFile方法可以引用外部的CSS文件,RegisterStyle方法则可以向页面添加内嵌的样式表,在最终的HTML文档中,用RegisterStyle方法注册的样式表表现为一个内嵌的<style>节点。

TODO : insert example

顶部

编辑

19.8.3.全局目录(Well-Known Directories)

为简化客户端脚本文件、CSS文件和图像文件的引用过程,Spring.Web.UI.Page类提供了几个属性,使得开发人员能够通过绝对路径引用这些文件。如果开发人员喜欢常规的(目录式的)Web应用程序结构,这些属性会为他们提供很大的便利。

这几个属性是ScriptsRoot、CssRoot和ImagesRoot。它们的默认值分别为“Scripts”、“CSS”和 “Images”,如果在Web应用程序的根目录下创建了这几个目录,就可以直接使用默认值。但是,如果喜欢将这些文件放在其它位置,也可以随时在页面对象定义中修改这些默认值(同样,一般将这些值设置在抽象的页面定义中,再由其它页面定义继承)。下面是一个例子:

<object id="basePage" abstract="true">
    <description>
        Convenience base page definition for all the pages.        
        Pages that reference this definition as their parent (see the examples below)
        will automatically inherit following properties....        
    </description>
    <property name="CssRoot" value="Web/CSS"/>
    <property name="ImagesRoot" value="Web/Images"/>
</object>

顶部



.NET 藏经阁 | | 版权所有 ©2008 entlib.net.cn