個人檔案乐土.Lab相片部落格清單 工具 說明

E=MC²

職業
居住地
blogIMG  
第 1 張 / 共 8 張

乐土.Lab

温故知新
2006/11/28

信道"tcp"已注册??

刚才在写一个remoting的程序,但在执行的时候会报异常:信道"tcp"已注册。

代码片断:

BinaryServerFormatterSinkProvider provider = new BinaryServerFormatterSinkProvider();
provider.TypeFilterLevel = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;

Hashtable props = new Hashtable();

props["port"] = 7654;
//props["name"] = "TcpChannel";


TcpChannel _chanl = new TcpChannel(props, null, provider);

//Console.WriteLine("Channel's Name:" _chanl);

Console.WriteLine("Register TcpChannel...");
ChannelServices.RegisterChannel(_chanl, false);

RemotingConfiguration.RegisterWellKnownServiceType(typeof(CRBTGateWay4Center), "CRBTGateway4Center",
WellKnownObjectMode.Singleton);

在各网站狂找原因和解决方法,最后才搞明白。

不是说注册信道的端口被占用了,而是这个信道的名字已经被占用了。即:异常中的"tcp"是要注册信道的名字。可以在控制台上打印信道名字看出来。其实这个"tcp"是.Net默认的名字,如果你没有指定的话。

那么我们为信道赋予其他的名字好了,TcpChannel 有一个ChannelName的属性,试了一下不行,这是个只读属性。那么怎么指定名字呢?

先从TcpChannel 的构造函数入手:

public TcpChannel (
IDictionary properties,
IClientChannelSinkProvider clientSinkProvider,
IServerChannelSinkProvider serverSinkProvider
)

参数

properties

一个 IDictionary 集合,它为客户端和服务器信道要使用的配置属性指定值。

clientSinkProvider

客户端信道要使用的 IClientChannelSinkProvider 实现。

serverSinkProvider

服务器信道要使用的 IServerChannelSinkProvider 实现。

看来我可以通过properties来干,我用的Hashtable类型的 props 就是第一个参数,而我用的信道类型是TcpChannel,它所支持的信道属性有:

TcpChannel

authorizationModule

bindTo

exclusiveAddressUse

impersonate

machineName

name

port

priority

rejectRemoteRequests

secure

servicePrincipalName

suppressChannelData

timeout

tokenImpersonationLevel

useIpAddress

参见:http://msdn2.microsoft.com/zh-cn/library/kw7c6kwc(VS.80).aspx

呵呵,看到黑体的port属性了吗,我在代码里设置了它props["port"] = 7654;

那个斜体的name 属性就可以指定信道的名字,那么可以用同样的方法props["name"] = "TcpChannel";

把这行代码加进去试一下(上面的代码中注释掉的),OK了!


Technorati : , ,

2006/7/19

中国的银行主页可用性调查报告

 

[From Uigarden.net]

 


 
中国的银行主页可用性调查报告

作者 赵明

1. 纲要

我们对中国的三家主要零售银行的主页的可用性进行了评估。

这些页面在提供了宝贵的信息资源同时,也具有以下问题:

  • 过多的闪动广告,滚动文字记及移动图片。
  • 没有提供明确的导航帮助和站内查询功能。
  • 不规律的分行业务导致不协调一致的分行页面。
  • 低劣的互动应用导致不良的用户体验。
  • 缺少相关内容协助潜在客户开展业务。

然而,有一个银行提供了一个及时有效地在线论坛提供在线的客户支持。

这篇研究提出以下几点建议来帮助改进中国的银行的主页:

  • 减少闪动的图片。
  • 加强查询和导航功能。
  • 提高互动应用。
  • 建立网页设计指导方针。
  • 增加商务银行内容。
  • 标准化分行业务。

2. 介绍

主页对于为已有的和潜在的客户,合作伙伴,提供商和大众提供信息和服务来说是非常重要的。网页的有效性对企业的成功,包括增加销售额,更好的客户服务以及降低运作成本有很大影响。

方法
这篇研究以用户的角度对中国三家零售银行网站的可用性进行了评估。对银行的选择主要建立在他们在中国银行市场上的受欢迎程度:

为了便于比较,三家国外银行也列入其中,分别来自澳大利亚,英国和美国,他们都是所在国家银行界的代表:

该评估参考了可用性准则,考虑到了一般的可用性问题和银行网站的特殊问题.然而该研究并没有考虑银行合作伙伴及供应商的特殊需要,也没有考虑为顾客提供的在线银行服务系统。

H3. 3. 主要发现

这三家中国银行的网站都提供了丰富的信息,基本良好的内容框架和页面设计。版面编排和颜色设置都令人满意。所有的页面都配有明确的网页地图。

然而和国外同行相比仍然存在违反可用性标准的可用性问题。

3.1. 过多使用图片和Flash
中国的网页,不止是银行方面的,都趋向使用大量的图片,flash或滚动文字。这是强加给用户的麻烦而不是文化差异。建行和农行相对来说较合理的控制了图片的使用,尽管与西方同类网站相比仍然过多。工行网站在使用图形和闪动元素方面是最差的。

最糟糕的是有一个移动的图形框跟随用户到屏幕的每一个角落,遮住了用户真正感兴趣的内容。

3.2. 薄弱的导航和站内查询功能
导航对于网络用户的重要性就像购物中心的商品分布示意牌对于购物者来说一样。在任何时候用户都要知道自己身在何处,怎样进入另一个页面以及毫不费力的回到主页。

并不是所有的银行网站都提供了便捷的导航。例如工行的主页提供了下拉菜单进入功能组(图—左部,如果一级菜单的一部分是个人服务)

然而,按着菜单结构,存款服务->产品介绍,到三级页面,既看不见任何功能组,也看不到当前页面在菜单结构中的位置(图1右部),要浏览另一个功能组,用户必须回到首级,再从下拉菜单中选择。

图一. 页面导航

图一. 页面导航:顶级菜单(左)和第三层菜单(右),建行。

这三家中国银行网站只提供了基本的站内关键字查询。如果要查询的词碰巧是比较通用的,用户必须通读所有采样页面。增加另一个关键词只会变得更糟,因为网站只能提供“或者”这种查询方式,从而会带来更多的样本页面。

图2显示了三家中国网站通过关键字“存款利率”的查询结果。与此相比,花旗银行和汇丰银行提供了高级查询功能,联邦银行则只有基本查询。

图二. “存款利率”的查询结果

图二. “存款利率”的查询结果:工行(左上),农行(左下)和汇丰银行的搜索技巧(右下)。

3.3有效的在线客户支持
工行提供了在线客户服务论坛。这个论坛很火已经有近100,000个会员,每日回答超过1000个提问。很多问题在几分钟内就会得到客服的回复。除了回答特殊的问题,论坛还在特定时间提供网上咨询服务。正因为保证了有问必答和回复速度,工行论坛在建立与客户的关系方面是很成功的。

此成功案例背后有两个因素:1)大量的习惯于在线论坛的客户群 2)相对低廉的客服支持人力成本。因此这是中国的独特的措施,西方银行很难实施。

图3是网上论坛的截图,照片上所显示的三位专家两天内将在网上召开咨询会议。

图三. 工行网上论坛

图三. 工行网上论坛

3.4. 不连贯的分行网页
国外银行的网站很少有分行的具体信息,除了地址电话等联系信息。而中国的银行却有很多分行的页面。

当访问一个分行页面时,客户很难辨认一个页面是分行的特有页面,还是总公司的复制或链接页面。

即使是同样的页面结构,不同的分行可能有非常不同的内容(图四)。

这种不连贯性不仅仅是一个网页设计或是可用性的问题,这是分行业务运行的差异的反映。

图四. “龙卡”页面

图四. 建行北京支行“龙卡”页面(左)和安徽支行“龙卡”页面(右)

这种不连续性给客户服务带来困难。例如,由于分行业务运行的多样性,所有的中国银行在提供常见问题页面时都出现困难。工行论坛的一个网管连如何接受银行汇票这样的简单问题都不能回答,而只能告诉客户去询问分行办公室。

3.5. 低劣的交互应用
使用得当的交互应用会提高用户体验。例如,交互式地图可以提供向通往分行办公室的公交路线这样的信息。

在三个中国银行和相对比的西方银行中,只有中国农行的上海分行有交互地图。然而这个交互地图连最基本的题图所应提供的解答都不能够提供(例如,查询明珠公园周围500米以内所有的自动提款机)。地图上似乎只能查询中间部分,根本没法将查询中心转移到像明珠公园这样地图的边缘。

另外一个低劣的交互应用——演示功能,也是来自于上海分行。例如,在银行转帐过程中,输入转帐金额和密码后(其它数据页面已提供),点击“确认”键出现了“页面无法显示”的错误。

两种应用都试图提升商务价值。可是以它们现在的可用性,它们只是产生了消极的用户体验也破坏了商务。

3.6. 缺少开始企业业务的相关内容
所有零售银行都提供个人和企业服务。中国银行和西方银行最大的不同来自于商务服务。中国的银行看上去更像一个“消极的金钱管理员”,网站上并没有表现他们对于初期商务开展的支持的相关内容。与此相反,从选择商业架构到制定商业计划等方面,西方银行在帮助建立企业方面发挥着非常积极的作用。

图5显示了建行(左)和联邦银行(右)的企业服务业务。建行根本就没有建立商业账户的信息。

图五 建行和联邦银行所提供的商业银行服务

Figure 5. 建行(左)和联邦银行(右)所提供的商业银行服务

4. 建议

基于以上可用性研究,以下的改进意见只考虑了一般用户群,忽略了特殊客户的需求。

4.1. 减少闪动的图片
尽管中国的用户能暂时忍受闪动的广告和移动的图片(就好像他们被迫要为用手机接听电话付费一样),他们绝对不喜欢到处都看到闪动的图片和广告。网页设计应该力求减少闪动图片的使用。

4.2. 增强导航和查询功能
我们建议所有的银行网站都应为用户提供高级查询功能更好的搜索站内内容。现在的技术已成熟到布尔查询都不算什么“先进”技术。

4.3. 提高交互应用
中国的文化鼓励勇敢的实验新技术,然而,像农行上海支行首页的可用性水准是绝对不能接受的。当一个新技术被开发出来的时候,必须至少要保证它能完成最基本的功能。

4.4. 建立网页设计准则
为网页设计建立设计指南是势在必行的[5-6],这样才能在中国的银行网站的主要项目内容,导航和内容开发方面保证连贯性。

4.5. 增加对公银行业务内容
总体上中国的银行的企业服务做得不如西方银行成熟,在这个领域,中国的银行可以有极大的提高以吸引新的业务,并与企业家们在新业务概念的初期建立合作伙伴关系。

4.6. Standardise Branch Operations
非标准化的支行运作给企业作为一个统一整体运转带来困难,也增加了商业成本。中国的银行迫切需要改进他们自身与国际公司标准看齐,才能面对更激烈的国际竞争。

5. 参考文献

Daniel Szuc and Whitney Quesenbery, Choosing the Right Channel for Communicating with Customers, October 2005

Daniel Szuc and Gerry Gaffney, Back to Basics, 4 December 2004

Jakob Nielsen, Top Ten Guidelines for Homepage Usability ,
12 May 2002

Jakob Nielsen, Top Ten Guidelines for Homepage Usability, 12 May 2002

Jakob Nielsen, The Ten Most Violated Homepage Design Guidelines, 10 November 2003

NSW Department of Commerce, Web Usability and Accessibility Guide , August 2002

Usability.gov – United States Department of Health and Human Services, http://www.usability.gov/guides/index.html Ron Rogowski, etc.

Ron Rogowski, etc. “What Do Consumers Expect From Corporate Home Pages?”, Forrester Report, 3 March 2006

6. 感谢

感谢 Webword 的John Rhodes。

关于Apogee
Apogee亚洲可用性公司(”Apogee”)位于中国香港,是亚洲领先的可用性研究和咨询业务提供商。Apogee帮助像雅虎中国,联邦快递,eBay,Cathay Pacific,,汇丰银行,中国人民保险公司等公司进行用户研究。Apogee同时在 www.usabilitythatworks.com提供可用性培训。

 

2006/7/18

PetShop的数据库访问(例子源码 III )

 

配置文件

[Web.config]

 

 

<?xml version="1.0"?>

 

<configuration>

 

    <connectionStrings>

        <!--连接Pub数据库的连接字符串-->

        <add

name="PubConnectionString"

connectionString=" Server=.;Integrated Security=SSPI;Database=pubs;"

providerName="System.Data.SqlClient"/>

    </connectionStrings>

 

    <appSettings/>

 

    <system.web>

 

        <compilation debug="true">

            <assemblies>

                <add assembly=" Accessibility,

Version=2.0.0.0,

Culture=neutral,

PublicKeyToken=B03F5F7F11D50A3A"/>

</assemblies>

</compilation>

 

        <authentication mode="Windows"/>

    </system.web>

 

</configuration>

 

PetShop的数据库访问(例子源码 II )

 

[SQLHelper.cs]

 

using System;

using System.Collections;

using System.Text;

using System.Data.SqlClient;

using System.Data;

using System.Configuration;

 

namespace PetShop.DBUtility {

    public class SqlHelper {

 

        // 数据库连接字符串

        public static readonly string PubConnectionString =

ConfigurationManager.

ConnectionStrings["PubConnectionString"].

ConnectionString;

 

        // 缓存参数数组

        private static Hashtable parmCache = Hashtable.Synchronized(new Hashtable());

 

        /// <summary>

        /// 根据提供的参数,执行一个SqlCommand以返回连接串所指定数据库的结果

        /// </summary>

        /// <remarks>

        /// 例如: 

        ///  SqlDataReader r = ExecuteReader(connString, CommandType.StoredProcedure, "PublishOrders", new SqlParameter("@prodid", 24));

        /// </remarks>

        /// <param name="connectionString">连接字符串</param>

        /// <param name="commandType">CommandType (存储过程, 文本, 等等...)</param>

        /// <param name="commandText">存储过程名或T-SQL </param>

        /// <param name="commandParameters">qlParamters数组,用于执行命令</param>

        /// <returns>包含结果的SqlDataReader对象</returns>

        public static SqlDataReader ExecuteReader(

string connectionString,

CommandType cmdType,

string cmdText,

params SqlParameter[] commandParameters) {

 

            SqlCommand cmd = new SqlCommand();

            SqlConnection conn = new SqlConnection(connectionString);

 

            try {

                PrepareCommand(cmd, conn, null, cmdType, cmdText, commandParameters);

                SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

                cmd.Parameters.Clear();

                return rdr;

            } catch {

                conn.Close();

                throw;

            }

        }

 

        /// <summary>

        /// Prepare a command for execution

        /// </summary>

        /// <param name="cmd">SqlCommand object</param>

        /// <param name="conn">SqlConnection object</param>

        /// <param name="trans">SqlTransaction object</param>

        /// <param name="cmdType">Cmd type e.g. stored procedure or text</param>

        /// <param name="cmdText">Command text, e.g. Select * from Products</param>

        /// <param name="cmdParms">SqlParameters to use in the command</param>

        private static void PrepareCommand(

SqlCommand cmd,

SqlConnection conn,

SqlTransaction trans,

CommandType cmdType,

string cmdText,

SqlParameter[] cmdParms) {

 

            if (conn.State != ConnectionState.Open)

                conn.Open();

 

            cmd.Connection = conn;

            cmd.CommandText = cmdText;

 

            if (trans != null)

                cmd.Transaction = trans;

 

            cmd.CommandType = cmdType;

 

            if (cmdParms != null) {

                foreach (SqlParameter parm in cmdParms)

                    cmd.Parameters.Add(parm);

            }

        }

    }

}

 

[Default.aspx]

 

<%@ Page

Language="C#"

AutoEventWireup="true" 

CodeFile="Default.aspx.cs"

Inherits="_Default" %>

<%@ import Namespace="PetShop.IDAL" %>

<%@ import Namespace="PetShop.Model" %>

<%@ import Namespace="PetShop.SQLServerDAL" %>

 

<!DOCTYPE

html

PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

 

<html xmlns="http://www.w3.org/1999/xhtml" >

<head runat="server">

    <title>Default</title>

</head>

 

<body>

    <form id="form1" runat="server">

    <div>

    <%

       

        IAuthors authors;

        System.Collections.Generic.IList<AuthorsInfo> authorsInfos;

        authorsInfos = new System.Collections.Generic.List<AuthorsInfo>();

        authors = new Authors();

        authorsInfos = authors.GetAllAuthors();

 

        foreach(AuthorsInfo item in authorsInfos){

            Response.Write("# " + item.Au_id + " | " + item.Au_lname + "<br/>");

        }

    %>

    </div>

    </form>

</body>

</html>

 

PetShop的数据库访问(例子源码 I )

 

[AuthorsInfo.cs]

 

using System;

using System.Collections.Generic;

using System.Text;

 

namespace PetShop.Model {

    public class AuthorsInfo {

 

        private string au_id;

        private string au_lname;

        private string au_fname;

        private string phone;

        private string address;

        private string city;

        private string state;

        private string zip;

        private bool contract;      //: SQL Server中的bit类型(0/1),对应c#bool类型(false/true)

 

        public AuthorsInfo() { }

 

        public AuthorsInfo(

            string au_id,

            string au_lname,

            string au_fname,

            string phone,

            string address,

            string city,

            string state,

            string zip,

            bool contract){

 

            this.au_id = au_id;

            this.au_lname = au_lname;

            this.au_fname = au_fname;

            this.phone = phone;

            this.address = address;

            this.city = city;

            this.state = state;

            this.zip = zip;

            this.contract = contract;

 

        }

 

        public string Au_id {

            get{ return au_id; }

            set{ au_id = value; }

        }

 

        public string Au_lname {

            get{ return au_lname; }

            set{ au_lname = value; }

        }

 

        public string Au_fname {

            get{ return au_fname; }

            set{ au_fname = value; }

        }

 

        public string Phone {

            get{ return phone; }

            set{ phone = value; }

        }

 

        public string Address {

            get{ return address; }

            set{ address = value; }

        }

 

        public string City {

            get{ return city; }

            set{ city = value; }

        }

 

        public string State {

            get{ return state; }

            set{ state = value; }

        }

 

        public string Zip {

            get{ return zip; }

            set{ zip = value; }

        }

 

        public bool Contract {

            get{ return contract; }

            set{ contract = value; }

        }

    }

}

 

 

[IAuthors.cs]

 

using System;

using System.Collections.Generic;

using System.Text;

using PetShop.Model;

 

namespace PetShop.IDAL {

    public interface IAuthors {

 

        /// <summary>

        /// 读取全部Author信息

        /// </summary>

        /// <returns>查询结果的实体集的泛型接口</returns>

        IList<AuthorsInfo> GetAllAuthors();

 

        /// <summary>

        /// 根据au_id读取author信息

        /// </summary>

        /// <param name="au_id">author的唯一标示</param>

        /// <returns>表示Author的业务实体</returns>

        AuthorsInfo GetAuthor(string au_id);

    }

}

 

 

[Authors.cs]

 

using System;

using System.Collections.Generic;

using System.Text;

using PetShop.IDAL;

using PetShop.Model;

using System.Data.SqlClient;

using PetShop.DBUtility;

using System.Data;

using System.Configuration;

 

namespace PetShop.SQLServerDAL {

    public class Authors : IAuthors {

 

        // 静态常量

        private const string SQL_SELECT_AUTHORS = "SELECT

authors.au_id,

authors.au_lname,

authors.au_fname,

authors.phone,

authors.address,

authors.city,

authors.state,

authors.zip,

authors.contract

FROM authors";

        private const string PARM_AU_ID = "@au_id";

 

        /// <summary>

        /// 读取全部Author信息

        /// </summary>

        /// <returns>查询结果的实体集的泛型接口</returns>

        public IList<AuthorsInfo> GetAllAuthors() {

 

            //设置返回值

            IList<AuthorsInfo> authorsInfos = new List<AuthorsInfo>();

 

            //执行查询

            using (

                SqlDataReader rdr = SqlHelper.ExecuteReader(

                                                SqlHelper.PubConnectionString,

                                                CommandType.Text,

                                                SQL_SELECT_AUTHORS,

                                                null)

            ) {

                AuthorsInfo item = null;

                while (rdr.Read()) {

                    item = new AuthorsInfo(

                                    rdr.GetString(0),

                                    rdr.GetString(1),

                                    rdr.GetString(2),

                                    rdr.GetString(3),

                                    rdr.GetString(4),

                                    rdr.GetString(5),

                                    rdr.GetString(6),

                                    rdr.GetString(7),

                                    rdr.GetBoolean(8));

 

                    authorsInfos.Add(item);

                }

            }

 

            return authorsInfos;

        }

 

        /// <summary>

        /// 根据au_id读取author信息

        /// </summary>

        /// <param name="au_id">author的唯一标示</param>

        /// <returns>表示Author的业务实体</returns>

        public AuthorsInfo GetAuthor(string au_id) {

 

            //设置返回值

            AuthorsInfo author = null;

 

            //创建参数

            SqlParameter parm = new SqlParameter(PARM_AU_ID, SqlDbType.VarChar, 10);

            //绑定参数

            parm.Value = au_id;

 

            //执行查询

            using (

                SqlDataReader rdr = SqlHelper.ExecuteReader(

                                                SqlHelper.PubConnectionString,

                                                CommandType.Text,

                                                SQL_SELECT_AUTHORS,

                                                parm)

            ) {

                if (rdr.Read())

                    author = new AuthorsInfo(

rdr.GetString(0),

rdr.GetString(1),

rdr.GetString(2),

rdr.GetString(3),

rdr.GetString(4),

rdr.GetString(5),

rdr.GetString(6),

rdr.GetString(7),

rdr.GetBoolean(8));

                else

                    author = new AuthorsInfo();

            }

            return author;

        }

    }

}

PetShop的数据库访问

 
  PetShop中,数据库房部分大概是这样:
 
  数据表现 - 数据库操作 - 数据库
           \   /
           实体模型
 
  在这个例子里,“数据表现”就简化到一个Web页面。
 
  “实体模型”与“数据库操作"、 "数据库"紧密联系。“实体模型”将数据库表映射到对象,在对业务的处理过程,对对象操作要比直接操作数据库的表等快捷方便干脆,模型中的成员通常与表的域一一对应,除非有表关联的情况(再议吧)。对应到例子,就是AuthorsInfo.cs
 
  “数据库操作"封装了对务需要的操作,在例子中对应的是IAuthors.csAuthors.cs,IAuthors.cs是个接口,Authors.cs是其实现,这样的好处是可以方便于针对不同的数据库。比如我用的是MS SQLServer,我就在写针对它的Authors类去实现IAuthors;用Oracle就写针对Oracle的类同样实现IAuthors。这样我调用的时候实例化一个IAuthors就好,具体用什么数据库都不用担心。
 
  “数据库”就是实际的数据库,例如:MS SQLServer、Oracle。
 
  另外还有一个数据访问的帮助类,封装了一些对数据库操作,它们是游离于业务之外的基础操作,诸如Select,Insert,Delete...这些操作是有数据库差异性的,所以针对每种数据库都要有不同的类。这个帮助类是服务于“数据库操作"的,“数据库操作"对不同的数据库有不同的实现,每种实现会相应的调用对应的帮助类。在例子里,对应的是SQLHelper.cs
 
  例子是针对MS SQLServer的。
 
  [Spaces不支持太长的文章,只好斩成几段来发。]
 
2006/7/14

确保使用数据源控件时连接字符串的安全

 [from MSDN]

在处理数据源控件时,建议您将连接字符串集中存储在应用程序的 Web.config 文件中。这会使连接字符串可供一个 Web 应用程序中的所有 ASP.NET 页使用,从而简化了连接字符串的管理。另外一个优势是,当连接字符串信息发生更改时,无需修改许多页。最后,您可以使用受保护配置对 Web.config 文件的连接字符串节进行加密,从而提高存储在连接字符串中的敏感信息(如数据库名称、用户名、密码等)的安全性。

本主题描述如何将连接字符串存储在 Web.config 文件的 connectionStrings 配置节,以及如何使用命令行 .NET Framework 工具来加密连接字符串以实现额外的安全性。

 

将连接字符串存储在 Web.config 文件中

  1. 打开应用程序的 Web.config 文件。如果 Web.config 文件尚不存在,请创建一个名为 Web.config 的文本文件并添加以下内容:

    <?xml version="1.0"?>
    <configuration 
            xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <appSettings/>
        <system.web>
        </system.web>
    </configuration>
  2. configuration 元素中,创建一个名为 connectionStrings 的新元素,如下面的示例所示:

    <?xml version="1.0"?>
    <configuration 
            xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
        <connectionStrings>    </connectionStrings>
        <appSettings/>
        <system.web>
        </system.web>
    </configuration>
  3. connectionStrings 元素中,针对将用在 Web 应用程序中的每个连接字符串创建一个 add 元素。将包括下表所示的属性。

     
    属性 说明
    • name

    此连接字符串配置对象的名称。此名称将由数据源控件和其他功能用来引用连接字符串信息。

    • connectionString

    数据源的连接字符串。

    • providerName

    要用于此连接的 NET Framework 数据提供程序的命名空间,如 System.Data.SqlClientSystem.Data.OleDbSystem.Data.Odbc

    一个完整的 connectionStrings 元素可能如下面的示例所示:

    <connectionStrings>
      <add 
        name="NorthwindConnection" 
        connectionString="Data Source=localhost;Integrated Security=SSPI;Initial Catalog=Northwind;" />
    </connectionStrings>
  4. 保存并关闭 Web.config 文件。

    现在,可以通过引用为 name 属性指定的名称来引用数据源控件的连接字符串。

  5. 在数据源控件的 ConnectionString 属性中,使用连接字符串表达式语法引用 Web.config 文件中的连接信息。

    下面的示例演示 SqlDataSource 控件,该控件读取 Web.config 文件中的连接字符串:

    <asp:SqlDataSource ID="ProductsDataSource" Runat="server" 
        SelectCommand="SELECT * from Products"
        ConnectionString="<%$ ConnectionStrings: NorthwindConnection %>"
    </asp:SqlDataSource>

对存储在 Web.config 文件中的连接字符串信息进行加密

  1. 在 Windows 命令行下运行带有下列选项的 ASP.NET IIS 注册工具 (aspnet_regiis.exe):

    • -pe 选项,向它传递字符串“connectionStrings”以加密 connectionStrings 元素。

    • -app 选项,向它传递应用程序的名称。

    aspnet_regiis.exe 工具位于 %systemroot%\Microsoft.NET\Framework\versionNumber 文件夹中。

    下面的示例演示如何对名为 SampleApplication 的应用程序的 Web.config 文件的 connectionStrings 节进行加密:

    aspnet_regiis -pe "connectionStrings" -app "/SampleApplication"

    在该命令完成之后,您可以查看 Web.config 文件的内容。connectionStrings 配置节将包含加密信息,而不是明文连接字符串,如下面的示例所示:

    <configuration>
       <connectionStrings configProtectionProvider="RsaProtectedConfigurationProvider">
          <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"
             xmlns="http://www.w3.org/2001/04/xmlenc#">
             <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
             <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
                   <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
                   <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                      <KeyName>RSA Key
                      </KeyName>
                   </KeyInfo>
                   <CipherData>
                      <CipherValue>WcFEbDX8VyLfAsVK8g6hZVAG1674ZFc1kWH0BoazgOwdBfinhcAmQmnIn0oHtZ5tO2EXGl+dyh10giEmO9NemH4YZk+iMIln+ItcEay9CGWMXSen9UQLpcQHQqMJErZiPK4qPZaRWwqckLqriCl9X8x9OE7jKIsO2Ibapwj+1Jo=
                      </CipherValue>
                   </CipherData>
                </EncryptedKey>
             </KeyInfo>
             <CipherData>
                <CipherValue>OpWQgQbq2wBZEGYAeV8WF82yz6q5WNFIj3rcuQ8gT0MP97aO9SHIZWwNggSEi2Ywi4oMaHX9p0NaJXG76aoMR9L/WasAxEwzQz3fexFgFSrGPful/5txSPTAGcqUb1PEBVlB9CA71UXIGVCPTiwF7zYDu8sSHhWa0fNXqVHHdLQYy1DfhXS3cO61vW5e/KYmKOGA4mjqT0VZaXgb9tVeGBDhjPh5ZlrLMNfYSozeJ+m2Lsm7hnF6VvFm3fFMXa6+h0JTHeCXBdmzg/vQb0u3oejSGzB4ly+V9O0T4Yxkwn9KVDW58PHOeRT2//3iZfJfWV2NZ4e6vj4Byjf81o3JVNgRjmm9hr9blVbbT3Q8/j5zJ+TElCn6zPHvnuB70iG2KPJXqAj2GBzBk6cHq+WNebOQNWIb7dTPumuZK0yW1XDZ5gkfBuqgn8hmosTE7mCvieP9rgATf6qgLgdA6zYyVV6WDjo1qbCV807lczxa3bF5KzKaVUSq5FS1SpdZKAE6/kkr0Ps++CE=
                </CipherValue>
             </CipherData>
          </EncryptedData>
       </connectionStrings>
    </configuration>

    使该命令提示符处于打开状态以供后面的步骤使用。

  2. 通过检索当前的 WindowsIdentity 名称来确定 ASP.NET 运行所使用的用户帐户或标识。

    下面的示例演示一种用来确定 WindowsIdentity 名称的方法:

    <%@ Page Language="C#" %>
    <%
    Response.Write(System.Security.Principal.WindowsIdentity.GetCurrent().Name);
    %>
  3. 注意

    默认情况下,在 Windows Server 2003 上,由于 Web.config 文件中禁用了对 ASP.NET 应用程序的模拟,应用程序运行所使用的标识是 NETWORK SERVICE 帐户。在其他版本的 Windows 上,ASP.NET 运行在本地 ASPNET 帐户下。

    ASP.NET 运行所使用的用户帐户或标识必须对用来加密和解密 Web.config 文件各节的加密密钥具有读取权限。此过程假定对网站配置了在 Machine.config 文件中指定的名为“RsaProtectedConfigurationProvider”的默认 RsaProtectedConfigurationProvider。默认 RsaProtectedConfigurationProvider 使用名为“NetFrameworkConfigurationKey”的 RSA 密钥容器。

  4. 在命令提示符下,运行带有下列选项的 aspnet_regiis.exe 工具:

    • -pa 选项,向它传递默认 RsaProtectedConfigurationProvider 的 RSA 密钥容器的名称。

    • 在前面的步骤中确定的 ASP.Net 应用程序的标识。

    下面的示例演示如何授予 NETWORK SERVICE 帐户对计算机级的“NetFrameworkConfigurationKey”RSA 密钥容器的访问权限:

    aspnet_regiis -pa "NetFrameworkConfigurationKey" "NT AUTHORITY\NETWORK SERVICE"
  5. 若要对加密的 Web.config 文件内容进行解密,请运行带有 -pd 选项的 aspnet_regiis.exe 工具。除了不指定受保护配置提供程序外,该语法与使用 -pe 选项加密 Web.config 文件内容的语法相同。相应的提供程序是在受保护节的 configProtectionProvider 属性中标识的。

    下面的示例演示如何对 ASP.NET 应用程序 SampleApplicationconnectionStrings 元素进行解密。

    aspnet_regiis -pd "connectionStrings" -app "/SampleApplication"
2006/6/23

.Net Remoting基础

 

什么是Remoting,简而言之,我们可以将其看作是一种分布式处理方式。从微软的产品角度来看,可以说Remoting就是DCOM的一种升级,它改善了很多功能,并极好的融合到.Net平台下。Microsoft® .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这也正是我们使用Remoting的原因。为什么呢?在Windows操作系统中,是将应用程序分离为单独的进程。这个进程形成了应用程序代码和数据周围的一道边界。如果不采用进程间通信(RPC)机制,则在一个进程中执行的代码就不能访问另一进程。这是一种操作系统对应用程序的保护机制。然而在某些情况下,我们需要跨过应用程序域,与另外的应用程序域进行通信,即穿越边界。

在Remoting中是通过通道(channel)来实现两个应用程序域之间对象的通信的。如图所示:

首先,客户端通过Remoting,访问通道以获得服务端对象,再通过代理解析为客户端对象。这就提供一种可能性,即以服务的方式来发布服务器对象。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),然后客户端再通过Remoting连接服务器,获得该服务对象并通过序列化在客户端运行。

在Remoting中,对于要传递的对象,设计者除了需要了解通道的类型和端口号之外,无需再了解数据包的格式。但必须注意的是,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。这既保证了客户端和服务器端有关对象的松散耦合,同时也优化了通信的性能。

1、Remoting的两种通道

Remoting的通道主要有两种:Tcp和Http。在.Net中,System.Runtime.Remoting.Channel中定义了IChannel接口。IChannel接口包括了TcpChannel通道类型和Http通道类型。它们分别对应Remoting通道的这两种类型。

TcpChannel类型放在名字空间System.Runtime.Remoting.Channel.Tcp中。Tcp通道提供了基于Socket的传输工具,使用Tcp协议来跨越Remoting边界传输序列化的消息流。TcpChannel类型默认使用二进制格式序列化消息对象,因此它具有更高的传输性能。HttpChannel类型放在名字空间System.Runtime.Remoting.Channel.Http中。它提供了一种使用Http协议,使其能在Internet上穿越防火墙传输序列化消息流。默认情况下,HttpChannel类型使用Soap格式序列化消息对象,因此它具有更好的互操作性。通常在局域网内,我们更多地使用TcpChannel;如果要穿越防火墙,则使用HttpChannel。

2、远程对象的激活方式

在访问远程类型的一个对象实例之前,必须通过一个名为Activation的进程创建它并进行初始化。这种客户端通过通道来创建远程对象,称为对象的激活。在Remoting中,远程对象的激活分为两大类:服务器端激活和客户端激活。

(1) 服务器端激活,又叫做WellKnow方式,很多又翻译为知名对象。为什么称为知名对象激活模式呢?是因为服务器应用程序在激活对象实例之前会在一个众所周知的统一资源标识符(URI)上来发布这个类型。然后该服务器进程会为此类型配置一个WellKnown对象,并根据指定的端口或地址来发布对象。.Net Remoting把服务器端激活又分为SingleTon模式和SingleCall模式两种。

SingleTon模式:此为有状态模式。如果设置为SingleTon激活方式,则Remoting将为所有客户端建立同一个对象实例。当对象处于活动状态时,SingleTon实例会处理所有后来的客户端访问请求,而不管它们是同一个客户端,还是其他客户端。SingleTon实例将在方法调用中一直维持其状态。举例来说,如果一个远程对象有一个累加方法(i=0;++i),被多个客户端(例如两个)调用。如果设置为SingleTon方式,则第一个客户获得值为1,第二个客户获得值为2,因为他们获得的对象实例是相同的。如果熟悉Asp.Net的状态管理,我们可以认为它是一种Application状态。

SingleCall模式:SingleCall是一种无状态模式。一旦设置为SingleCall模式,则当客户端调用远程对象的方法时,Remoting会为每一个客户端建立一个远程对象实例,至于对象实例的销毁则是由GC自动管理的。同上一个例子而言,则访问远程对象的两个客户获得的都是1。我们仍然可以借鉴Asp.Net的状态管理,认为它是一种Session状态。

(2) 客户端激活。与WellKnown模式不同,Remoting在激活每个对象实例的时候,会给每个客户端激活的类型指派一个URI。客户端激活模式一旦获得客户端的请求,将为每一个客户端都建立一个实例引用。SingleCall模式和客户端激活模式是有区别的:首先,对象实例创建的时间不一样。客户端激活方式是客户一旦发出调用的请求,就实例化;而SingleCall则是要等到调用对象方法时再创建。其次,SingleCall模式激活的对象是无状态的,对象生命期的管理是由GC管理的,而客户端激活的对象则有状态,其生命周期可自定义。其三,两种激活模式在服务器端和客户端实现的方法不一样。尤其是在客户端,SingleCall模式是由GetObject()来激活,它调用对象默认的构造函数。而客户端激活模式,则通过CreateInstance()来激活,它可以传递参数,所以可以调用自定义的构造函数来创建实例。

二、远程对象的定义

前面讲到,客户端在获取服务器端对象时,并不是获得实际的服务端对象,而是获得它的引用。因此在Remoting中,对于远程对象有一些必须的定义规范要遵循。

由于Remoting传递的对象是以引用的方式,因此所传递的远程对象类必须继承MarshalByRefObject。MSDN对MarshalByRefObject的说明是:MarshalByRefObject 是那些通过使用代理交换消息来跨越应用程序域边界进行通信的对象的基类。不是从 MarshalByRefObject 继承的对象会以隐式方式按值封送。当远程应用程序引用一个按值封送的对象时,将跨越远程处理边界传递该对象的副本。因为您希望使用代理方法而不是副本方法进行通信,因此需要继承MarshallByRefObject。

以下是一个远程对象类的定义:
public class ServerObject:MarshalByRefObject
{
        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }
}

这个类只实现了最简单的方法,就是设置一个人的基本信息,并返回一个Person类对象。注意这里返回的Person类。由于这里所传递的Person则是以传值的方式来完成的,而Remoting要求必须是引用的对象,所以必须将Person类序列化。

因此,在Remoting中的远程对象中,如果还要调用或传递某个对象,例如类,或者结构,则该类或结构则必须实现串行化Attribute[SerializableAttribute]:
[Serializable]
 public class Person
 {
        public Person()
        {
           
        }

        private string name;
        private string sex;
        private int age;

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

        public string Sex
        {
            get {return sex;}
            set {sex = value;}
        }

        public int Age
        {
            get {return age;}
            set {age = value;}
        }
  }
将该远程对象以类库的方式编译成Dll。这个Dll将分别放在服务器端和客户端,以添加引用。

在Remoting中能够传递的远程对象可以是各种类型,包括复杂的DataSet对象,只要它能够被序列化。远程对象也可以包含事件,但服务器端对于事件的处理比较特殊,我将在本系列之三中介绍。

三、服务器端

根据第一部分所述,根据激活模式的不同,通道类型的不同服务器端的实现方式也有所不同。大体上说,服务器端应分为三步:

1、注册通道

要跨越应用程序域进行通信,必须实现通道。如前所述,Remoting提供了IChannel接口,分别包含TcpChannel和HttpChannel两种类型的通道。这两种类型除了性能和序列化数据的格式不同外,实现的方式完全一致,因此下面我们就以TcpChannel为例。

注册TcpChannel,首先要在项目中添加引用“System.Runtime.Remoting”,然后using名字空间:System.Runtime.Remoting.Channel.Tcp。代码如下:
            TcpChannel channel = new TcpChannel(8080);
            ChannelServices.RegisterChannel(channel);

在实例化通道对象时,将端口号作为参数传递。然后再调用静态方法RegisterChannel()来注册该通道对象即可。

2、注册远程对象

注册了通道后,要能激活远程对象,必须在通道中注册该对象。根据激活模式的不同,注册对象的方法也不同。

(1) SingleTon模式

对于WellKnown对象,可以通过静态方法RemotingConfiguration.RegisterWellKnownServiceType()来实现:RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObject),
                “ServiceMessage”,WellKnownObjectMode.SingleTon);

(2)SingleCall模式

注册对象的方法基本上和SingleTon模式相同,只需要将枚举参数WellKnownObjectMode改为SingleCall就可以了。RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObject),
                “ServiceMessage”,WellKnownObjectMode.SingleCall);

(3)客户端激活模式

对于客户端激活模式,使用的方法又有不同,但区别不大,看了代码就一目了然。
RemotingConfiguration.ApplicationName = “ServiceMessage”;
RemotingConfiguration.RegisterActivatedServiceType(
                typeof(ServerRemoteObject.ServerObject));

为什么要在注册对象方法前设置ApplicationName属性呢?其实这个属性就是该对象的URI。对于WellKnown模式,URI是放在RegisterWellKnownServiceType()方法的参数中,当然也可以拿出来专门对ApplicationName属性赋值。而RegisterActivatedServiceType()方法的重载中,没有ApplicationName的参数,所以必须分开。

3、注销通道

如果要关闭Remoting的服务,则需要注销通道,也可以关闭对通道的监听。在Remoting中当我们注册通道的时候,就自动开启了通道的监听。而如果关闭了对通道的监听,则该通道就无法接受客户端的请求,但通道仍然存在,如果你想再一次注册该通道,会抛出异常。

           //获得当前已注册的通道;
            IChannel[] channels = ChannelServices.RegisteredChannels;

            //关闭指定名为MyTcp的通道;
            foreach (IChannel eachChannel in channels)
            {
                if (eachChannel.ChannelName == “MyTcp”)
                {
                    TcpChannel tcpChannel = (TcpChannel)eachChannel;

                    //关闭监听;
                    tcpChannel.StopListening(null);

                    //注销通道;
                    ChannelServices.UnregisterChannel(tcpChannel);
                }
            }
代码中,RegisterdChannel属性获得的是当前已注册的通道。在Remoting中,是允许同时注册多个通道的,这一点会在后面说明。

四、客户端

客户端主要做两件事,一是注册通道。这一点从图一就可以看出,Remoting中服务器端和客户端都必须通过通道来传递消息,以获得远程对象。第二步则是获得该远程对象。

1、注册通道:
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel);

注意在客户端实例化通道时,是调用的默认构造函数,即没有传递端口号。事实上,这个端口号是缺一不可的,只不过它的指定被放在后面作为了Uri的一部分。

2、获得远程对象。

与服务器端相同,不同的激活模式决定了客户端的实现方式也将不同。不过这个区别仅仅是WellKnown激活模式和客户端激活模式之间的区别,而对于SingleTon和SingleCall模式,客户端的实现完全相同。

(1) WellKnown激活模式

要获得服务器端的知名远程对象,可通过Activator进程的GetObject()方法来获得:
ServerRemoteObject.ServerObject serverObj = (ServerRemoteObject.ServerObject)Activator.GetObject(
              typeof(ServerRemoteObject.ServerObject), “tcp://localhost:8080/ServiceMessage”);

首先以WellKnown模式激活,客户端获得对象的方法是使用GetObject()。其中参数第一个是远程对象的类型。第二个参数就是服务器端的uri。如果是http通道,自然是用http://localhost:8080/ServiceMessage了。因为我是用本地机,所以这里是localhost,你可以用具体的服务器IP地址来代替它。端口必须和服务器端的端口一致。后面则是服务器定义的远程对象服务名,即ApplicationName属性的内容。

(2) 客户端激活模式

如前所述,WellKnown模式在客户端创建对象时,只能调用默认的构造函数,上面的代码就说明了这一点,因为GetObject()方法不能传递构造函数的参数。而客户端激活模式则可以通过自定义的构造函数来创建远程对象。

客户端激活模式有两种方法:
1) 调用RemotingConfiguration的静态方法RegisterActivatedClientType()。这个方法返回值为Void,它只是将远程对象注册在客户端而已。具体的实例化还需要调用对象类的构造函数。
 RemotingConfiguration.RegisterActivatedClientType(               
                typeof(ServerRemoteObject.ServerObject),
                “tcp://localhost:8080/ServiceMessage”);
 ServerRemoteObject.ServerObject serverObj = new ServerRemoteObject.ServerObject();

2) 调用进程Activator的CreateInstance()方法。这个方法将创建方法参数指定类型的类对象。它与前面的GetObject()不同的是,它要在客户端调用构造函数,而GetObject()只是获得对象,而创建实例是在服务器端完成的。CreateInstance()方法有很多个重载,我着重说一下其中常用的两个。
a、 public static object CreateInstance(Type type, object[] args, object[] activationAttributes);

参数说明:
type:要创建的对象的类型。
args :与要调用构造函数的参数数量、顺序和类型匹配的参数数组。如果 args 为空数组或空引用(Visual Basic 中为 Nothing),则调用不带任何参数的构造函数(默认构造函数)。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

这里的参数args是一个object[]数组类型。它可以传递要创建对象的构造函数中的参数。从这里其实可以得到一个结论:WellKnown激活模式所传递的远程对象类,只能使用默认的构造函数;而Activated模式则可以用户自定义构造函数。activationAttributes参数在这个方法中通常用来传递服务器的url。
假设我们的远程对象类ServerObject有个构造函数:
            ServerObject(string pName,string pSex,int pAge)
            {
                name = pName;
                sex = pSex;
                age = pAge;
            }

那么实现的代码是:
            object[] attrs = {new UrlAttribute(”tcp://localhost:8080/ServiceMessage”)};
            object[] objs = new object[3];
            objs[0] = “wayfarer”;
            objs[1] = “male”;
            objs[2] = 28;
            ServerRemoteObject.ServerObject = Activator.CreateInstance(
                typeof(ServerRemoteObject.ServerObject),objs,attrs);
可以看到,objs[]数组传递的就是构造函数的参数。

b、public static ObjectHandle CreateInstance(string assemblyName, string typeName, object[] activationAttribute);

参数说明:
assemblyName :将在其中查找名为 typeName 的类型的程序集的名称。如果 assemblyName 为空引用(Visual Basic 中为 Nothing),则搜索正在执行的程序集。
typeName:首选类型的名称。
activationAttributes :包含一个或多个可以参与激活的属性的数组。

参数说明一目了然。注意这个方法返回值为ObjectHandle类型,因此代码与前不同:
            object[] attrs = {new UrlAttribute(”tcp://localhost:8080/EchoMessage”)};           
            ObjectHandle handle = Activator.CreateInstance(”ServerRemoteObject”,
                                   “ServerRemoteObject.ServerObject”,attrs);
            ServerRemoteObject.ServerObject obj = (ServerRemoteObject.ServerObject)handle.Unwrap();

这个方法实际上是调用的默认构造函数。ObjectHandle.Unwrap()方法是返回被包装的对象。

说明:要使用UrlAttribute,还需要在命名空间中添加:using System.Runtime.Remoting.Activation;

五、Remoting基础的补充

通过上面的描述,基本上已经完成了一个最简单的Remoting程序。这是一个标准的创建Remoting程序的方法,但在实际开发过程中,我们遇到的情况也许千奇百怪,如果只掌握一种所谓的“标准”,就妄想可以“一招鲜、吃遍天”,是不可能的。

1、注册多个通道

在Remoting中,允许同时创建多个通道,即根据不同的端口创建不同的通道。但是,Remoting要求通道的名字必须不同,因为它要用来作为通道的唯一标识符。虽然IChannel有ChannelName属性,但这个属性是只读的。因此前面所述的创建通道的方法无法实现同时注册多个通道的要求。

这个时候,我们必须用到System.Collection中的IDictionary接口:

注册Tcp通道:
IDictionary tcpProp = new Hashtable();
tcpProp[”name”] = “tcp9090″;
tcpProp[”port”] = 9090;
IChannel channel = new TcpChannel(tcpProp,
 new BinaryClientFormatterSinkProvider(),
 new BinaryServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);

注册Http通道:
IDictionary httpProp = new Hashtable();
httpProp[”name”] = “http8080″;
httpProp[”port”] = 8080;
IChannel channel = new HttpChannel(httpProp,
 new SoapClientFormatterSinkProvider(),
 new SoapServerFormatterSinkProvider());
ChannelServices.RegisterChannel(channel);

在name属性中,定义不同的通道名称就可以了。

2、远程对象元数据相关性

由于服务器端和客户端都要用到远程对象,通常的方式是生成两份完全相同的对象Dll,分别添加引用。不过为了代码的安全性,且降低客户端对远程对象元数据的相关性,我们有必要对这种方式进行改动。即在服务器端实现远程对象,而在客户端则删除这些实现的元数据。

由于激活模式的不同,在客户端创建对象的方法也不同,所以要分离元数据的相关性,也应分为两种情况。

(1) WellKnown激活模式:

通过接口来实现。在服务器端,提供接口和具体类的实现,而在客户端仅提供接口:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

public class ServerObject:MarshalByRefObject,IServerObject
{ ……}
注意:两边生成该对象程序集的名字必须相同,严格地说,是命名空间的名字必须相同。
           
(2) 客户端激活模式:

如前所述,对于客户端激活模式,不管是使用静态方法,还是使用CreateInstance()方法,都必须在客户端调用构造函数实例化对象。所以,在客户端我们提供的远程对象,就不能只提供接口,而没有类的实现。实际上,要做到与远程对象元数据的分离,可以由两种方法供选择:

a、利用WellKnown激活模式模拟客户端激活模式:

方法是利用设计模式中的“抽象工厂”,下面的类图表描述了总体解决方案:

我们在服务器端的远程对象中加上抽象工厂的接口和实现类:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

    public interface IServerObjFactory
    {
        IServerObject CreateInstance();       
    }

    public class ServerObject:MarshalByRefObject,IServerObject
    {
        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }       
    }

    public class ServerObjFactory:MarshalByRefObject,IServerObjFactory
    {
        public IServerObject CreateInstance()
        {
            return new ServerObject();
        }
    }

然后再客户端的远程对象中只提供工厂接口和原来的对象接口:
    public interface IServerObject
    {
        Person GetPersonInfo(string name,string sex,int age);
    }

    public interface IServerObjFactory
    {
        IServerObject CreateInstance();       
    }
我们用WellKnown激活模式注册远程对象,在服务器端:
           //传递对象;
            RemotingConfiguration.RegisterWellKnownServiceType(
                typeof(ServerRemoteObject.ServerObjFactory),
                “ServiceMessage”,WellKnownObjectMode.SingleCall);

注意这里注册的不是ServerObject类对象,而是ServerObjFactory类对象。

客户端:
ServerRemoteObject.IServerObjFactory serverFactory =               
                (ServerRemoteObject.IServerObjFactory) Activator.GetObject(
                typeof(ServerRemoteObject.IServerObjFactory),
                “tcp://localhost:8080/ServiceMessage”);

ServerRemoteObject.IServerObject serverObj = serverFactory.CreateInstance();

为什么说这是一种客户端激活模式的模拟呢?从激活的方法来看,我们是使用了SingleCall模式来激活对象,但此时激活的并非我们要传递的远程对象,而是工厂对象。如果客户端要创建远程对象,还应该通过工厂对象的CreateInstance()方法来获得。而这个方法正是在客户端调用的。因此它的实现方式就等同于客户端激活模式。

b、利用替代类来取代远程对象的元数据

实际上,我们可以用一个trick,来欺骗Remoting。这里所说的替代类就是这个trick了。既然是提供服务,Remoting传递的远程对象其实现的细节当然是放在服务器端。而要在客户端放对象的副本,不过是因为客户端必须调用构造函数,而采取的无奈之举。既然具体的实现是在服务器端,又为了能在客户端实例化,那么在客户端就实现这些好了。至于实现的细节,就不用管了。

如果远程对象有方法,服务器端则提供方法实现,而客户端就提供这个方法就OK了,至于里面的实现,你可以是抛出一个异常,或者return 一个null值;如果方法返回void,那么里面可以是空。关键是这个客户端类对象要有这个方法。这个方法的实现,其实和方法的声明差不多,所以我说是一个trick。方法如是,构造函数也如此。

还是用代码来说明这种“阴谋”,更直观:

服务器端:
    public class ServerObject:MarshalByRefObject
    {
        public ServerObject()
        {
           
        }

        public Person GetPersonInfo(string name,string sex,int age)
        {
            Person person = new Person();
            person.Name = name;
            person.Sex = sex;
            person.Age = age;
            return person;
        }       
    }

客户端:
    public class ServerObject:MarshalByRefObject
    {
        public ServerObj()
        {
            throw new System.NotImplementedException();
        }

        public Person GetPersonInfo(string name,string sex,int age)
        {
            throw new System.NotImplementedException();
        }       
    }

比较客户端和服务器端,客户端的方法GetPersonInfo(),没有具体的实现细节,只是抛出了一个异常。或者直接写上语句return null,照样OK。我们称客户端的这个类为远程对象的替代类。

3、利用配置文件实现

前面所述的方法,于服务器uri、端口、以及激活模式的设置是用代码来完成的。其实我们也可以用配置文件来设置。这样做有个好处,因为这个配置文件是Xml文档。如果需要改变端口或其他,我们就不需要修改程序,并重新编译,而是只需要改变这个配置文件即可。

(1) 服务器端的配置文件:
<configuration>
  <system.runtime.remoting>
    <application name=”ServerRemoting”>
      <service>
        <wellknown mode=”Singleton” type=”ServerRemoteObject.ServerObject” objectUri=”ServiceMessage”/>
      </service>
      <channels>
         <channel ref=”tcp” port=”8080″/>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>

如果是客户端激活模式,则把wellknown改为activated,同时删除mode属性。

把该配置文件放到服务器程序的应用程序文件夹中,命名为ServerRemoting.config。那么前面的服务器端程序直接用这条语句即可:
RemotingConfiguration.Configure(”ServerRemoting.config”);

(2) 客户端配置文件

如果是客户端激活模式,修改和上面一样。调用也是使用RemotingConfiguration.Configure()方法来调用存储在客户端的配置文件。

配置文件还可以放在machine.config中。如果客户端程序是web应用程序,则可以放在web.config中。

4、启动/关闭指定远程对象

Remoting中没有提供类似UnregisterWellKnownServiceType()的方法,也即是说,一旦通过注册了远程对象,如果没有关闭通道的话,该对象就一直存在于通道中。只要客户端激活该对象,就会创建对象实例。如果Remoting传送的只有一个远程对象,这不存在问题,关闭通道就可以了。如果传送多个远程对象呢?要关闭指定的远程对象应该怎么做?关闭之后又需要启动又该如何?

我们注意到在Remoting中提供了Marshal()和Disconnect()方法,答案就在这里。Marshal()方法是将MarshalByRefObject类对象转化为ObjRef类对象,这个对象是存储生成代理以与远程对象通讯所需的所有相关信息。这样就可以将该实例序列化以便在应用程序域之间以及通过网络进行传输,客户端就可以调用了。而Disconnect()方法则将具体的实例对象从通道中断开。

方法如下:
首先注册通道:
TcpChannel channel = new TcpChannel(8080);
ChannelServices.RegisterChannel(channel);

接着启动服务:
先在服务器端实例化远程对象。
ServerObject obj = new ServerObject();

然后,注册该对象。注意这里不用RemotingConfiguration.RegisterWellKnownServiceType(),而是使用RemotingServices.Marshal():

ObjRef objrefWellKnown = RemotingServices.Marshal(obj, “ServiceMessage”);

如果要注销对象,则:
RemotingServices.Disconnect(obj);

要注意,这里Disconnect的类对象必须是前面实例化的对象。正因为此,我们可以根据需要创建指定的远程对象,而关闭时,则Disconnect之前实例化的对象。

至于客户端的调用,和前面WellKnown模式的方法相同,仍然是通过Activator.GetObject()来获得。但从实现代码来看,我们会注意到一个问题,由于服务器端是显式的实例化了远程对象,因此不管客户端有多少,是否相同,它们调用的都是同一个远程对象。因此我们将这个方法称为模拟的SingleTon模式。

客户端激活模式

我们也可以通过Marshal()和Disconnect()来模拟客户端激活模式。首先我们来回顾“远程对象元数据相关性”一节,在这一节中,我说到采用设计模式的“抽象工厂”来创建对象实例,以此用SingleCall模式来模拟客户端激活模式。在仔细想想前面的模拟的SingleTon模式。是不是答案就将呼之欲出呢?

在“模拟的SingleTon”模式中,我们是将具体的远程对象实例进行Marshal,以此让客户端获得该对象的引用信息。那么我们换一种思路,当我们用抽象工厂提供接口,工厂类实现创建远程对象的方法。然后我们在服务器端创建工厂类实例。再将这个工厂类实例进行Marshal。而客户端获取对象时,不是获取具体的远程对象,而是获取具体的工厂类对象。然后再调用CreateInstance()方法来创建具体的远程对象实例。此时,对于多个客户端而言,调用的是同一个工厂类对象;然而远程对象是在各个客户端自己创建的,因此对于远程对象而言,则是由客户端激活,创建的是不同对象了。

当我们要启动/关闭指定对象时,只需要用Disconnet()方法来注销工厂类对象就可以了。

六、小结

Microsoft.Net Remoting真可以说是博大精深。整个Remoting的内容不是我这一篇小文所能尽述的,更不是我这个Remoting的初学者所能掌握的。王国维在《人间词话》一书中写到:古今之成大事业大学问者,必经过三种境界。“昨夜西风凋碧树,独上高楼,望尽天涯路。”此第一境界也。“衣带渐宽终不悔,为伊消得人憔悴。”此第二境界也。“众里寻他千百度,蓦然回首,那人却在灯火阑珊处。”此第三境界也。如以此来形容我对Remoting的学习,还处于“独上高楼,望尽天涯路”的时候,真可以说还未曾登堂入室。

或许需得“衣带渐宽”,学得Remoting“终不悔”,方才可以“蓦然回首”吧。

 

2006/6/13

.Net的事件

 
  .Net的事件也是由委托(delegate)折腾出来的。似乎在.Net里,很多东西都直接或间接的和委托有着千丝万缕的练习。
 
  .Net用委托将事件和事件处理函数联系起来。

  定义一个事件类,由EventArgs继承得来。

  在一个对象A中有声明的事件(event),就是声明了一个委托(并加有关键字event)。有一个方法用来模拟触发事件。以及一个事件处理函数。

  首先,要将A中声明的事件和事件处理函数联系起来,可以用+=将实例化的委托(指向事件处理函数)联系起来。

  然后实例化定义的事件,并把它作为触发事件方法的参数。调用触发事件方法。

  触发事件方法里只有一个动作,即是调用委托。
 
  以下是示例:

using System;

using System.ComponentModel;

 

namespace EventTest {

 

    public class EventArgsTest : EventArgs {

        private readonly string testString;

 

        public EventArgsTest(string inputString) {

            this.testString = inputString;

        }

 

        public string TestString {

            get { return testString; }

        }

    }

 

    public delegate void EventHandlerTest(object sender, EventArgsTest e);

 

    public class EventSourceAndHandler {

       

    //声明事件

        public event EventHandlerTest Alarm;

 

    //事件处理函数

        public void eventHandler(object sender, EventArgsTest e) {

            Console.Out.WriteLine(e.TestString);

        }

 

    //触发事件

        public virtual void OnAlarm(EventArgsTest e) {

            if (Alarm != null) {

         // 调用委托

                Alarm(this, e);

            }

        }

    }

 

    public class eventTest {

        public static void Main(string[] args) {

 

            EventSourceAndHandler clock = new EventSourceAndHandler();

            clock.Alarm += new EventHandlerTest(clock.eventHandler);

            EventArgsTest e = new EventArgsTest("event test!");

            clock.OnAlarm(e);

        }

    }  

}

 


 
  执行结果:

event test!

 
2006/6/12

通过委托实现异步

  .NET Framework可以使用委托进行异步编程,亦可使用基于事件的异步模式进行多线程编程。


  琢磨了一下使用委托(delegate)进行异步编程。说白了,异步其实就是依靠多线程,用一个线程游离于主线程之外,以执行一个指定任务,而主线程继续做自己的事,任务完成时告诉主线程:“嘿,我完成任务了”。


  用到的类和接口也就是:


  System.IAsyncResult
  System.Runtime.Remoting.Messaging.AsyncResult
  System.AsyncCallback
(不会显式的运用)
  System.Threading.Thread(不会显式的运用)


  通过一个例子来说明:


  我们需要声明一个委托,届时它将指向一个具有相同签名(所谓签名,我的理解就是参数列表和返回类型)的方法(就是一会我们将要说到的目标方法)。


  当调用一个委托时,编译器会生成"Invoke"方法,它会在当前线程(即调用委托的线程)调用目标方法,.NET编译器是支持异步委托的,所以,它将生成"Invoke"方法以及"BeginInvoke"和"EndInvoke"方法。我们需要手动调用BeginInvoke方法。


  BeginInvoke的执行过程就是把调用目标方法的请求放入队列,然后立即返回。而目标方法在一个新线程中执行,与主线程并行。BeginInvoke的参数就是目标方法的参数加上另外两个参数,一个是委托类型的参数,指向我们要调用的回调函数(就是目标方法完成以后调用的函数),另一个是Object型参数,是回调方法将要使用的信息的对象。


  我们需要一个回调函数,这样目标方法完成时,回调函数将被唤起,起到主动通知的作用。比如我叫你做一件事,我就是主线程,你是新开的线程,我想知道你的事情做完了没有,因为我的一部分工作可能依靠你的工作完成以后才能做,有两个方式:1.我不时的去问你并等着你(我觉的这就不是异步的初衷了)。2.你做完了就告诉我。(2)的方式就是通过回调来让主线程得知。


  EndInvoke是在BeginInvoke执行完毕后调用,它是用来得到目标方法执行情况的。主线程可以调,回调函数也可以调。如果主线程调就是我们刚才说的(1),回调函数调就是(2)。我们当然选择(2)。


  回调函数怎么调用EndInvoke呢,我们为回调函数安插一个IAsyncResult型参数,通过它可以得到刚在指向目标函数的委托,这样就可以通过这个委托来调用EndInvoke了。BeginInvoke的最后一个参数我们赋予它指向目标函数的委托,这个委托便是刚才说的“回调方法将要使用的信息的对象”。
   


using System;
using System.Threading;

namespace AysncTest {

    public delegate void AsyncDelegate(int number);

    public class AsyncTest {

        //业务方法
       
public void businessMethod(int number) {
            Console.Out.WriteLine("businessMethod() beging...");
            int i;
            for (i = 0; i <= number/2; i++) {
                Console.Out.WriteLine("businessMethod's count : " + i + "  ..thread : " + Thread.CurrentThread.ManagedThreadId);
            }
            Console.Out.WriteLine("businessMethod() is complete! ");
        }

        // 回调方法
        public void callBackMethod(IAsyncResult ar) {
            try {
                Console.Out.WriteLine("callBackMethod() beging...");
                Console.Out.WriteLine("callBackMethod() @ Thread : " + Thread.CurrentThread.ManagedThreadId);
                AsyncDelegate caller = (AsyncDelegate)ar.AsyncState;
                for (int i = 0; i <= 100; i++) {
                    Console.Out.WriteLine("callBackMethod's count : " + i + "  ..thread : " + Thread.CurrentThread.ManagedThreadId);
                }
                caller.EndInvoke(ar);
                Console.Out.WriteLine("callBackMethod is complete! ");
            } catch (Exception e) {
                Console.WriteLine(e.StackTrace);
                Console.WriteLine(e.Message);
            }
        }

        //异步调用
        public void asyncTestMethod(int number) {

            Console.Out.WriteLine("asyncTestMethod() beging...");
            Console.Out.WriteLine("asyncTestMethod() @ Thread : " + Thread.CurrentThread.ManagedThreadId);
            AsyncDelegate asyncCaller = new AsyncDelegate(this.businessMethod);
            AsyncCallback callBack = new AsyncCallback(this.callBackMethod);
            IAsyncResult result = asyncCaller.BeginInvoke(number, callBack, asyncCaller);
            Console.Out.WriteLine("asyncTestMethod() is complete! ");

        }

        public static void Main() {
            Console.Out.WriteLine("Main() beging...");

            int number = 1000;
            AsyncTest test = new AsyncTest();
            test.asyncTestMethod(number);

            for (int i = 0; i <= number; i++) {
                Console.Out.WriteLine("Main's count : " + i + "  ..thread : " + Thread.CurrentThread.ManagedThreadId);
            }

            Console.Out.WriteLine("Main() is complete! ");
        }
    }


}


   执行结果:



Main() beging...
asyncTestMethod() beging...
asyncTestMethod() @ Thread : 1
asyncTestMethod() is complete!
Main's count : 0  ..thread : 1
Main's count : 1  ..thread : 1
Main's count : 2  ..thread : 1
businessMethod() beging...
businessMethod's count : 0  ..thread : 3
businessMethod's count : 1  ..thread : 3
   .                             .
   .                             .
   .                             .
businessMethod's count : 385  ..thread : 3
businessMethod's count : 386  ..thread : 3
Main's count : 3  ..thread : 1
Main's count : 4  ..thread : 1
   .                    .
   .                    .
   .                    .
Main's count : 393  ..thread : 1
Main's count : 394  ..thread : 1
businessMethod's count : 387  ..thread : 3
businessMethod's count : 388  ..thread : 3
   .                             .
   .                             .
   .                             .
businessMethod's count : 499  ..thread : 3
businessMethod's count : 500  ..thread : 3
businessMethod() is complete!
callBackMethod() beging...
callBackMethod() @ Thread : 3
callBackMethod's count : 0  ..thread : 3
callBackMethod's count : 1  ..thread : 3
   .                             .
   .                             .
   .                             .
callBackMethod's count : 99  ..thread : 3
callBackMethod's count : 100  ..thread : 3
callBackMethod is complete!
Main's count : 395  ..thread : 1
Main's count : 396  ..thread : 1
   .                    .
   .                    .
   .                    .
Main's count : 999  ..thread : 1
Main's count : 1000  ..thread : 1
Main() is complete!


  输出是在控制台上,可以看到主线程和目标方法的输出交替出现,说明是并行的,分别在两个线程上,还可以看到回调函数的执行也在目标方法的线程上。

  (黑色:主线程输出; 红色:目标方法输出; 绿色:回调方法输出)

 

2006/6/9

不知道这算不算IE的BUG

且看下面两段代码:
 
[test.aspx]

<%@ Page Language="C#" AutoEventWireup="true" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
    <title>无标题页</title>
    <SCRIPT src="test.js" type="text/javascript"></SCRIPT>
</head>
<body onload="hello()">
    <form id="form1" runat="server">
    <div>test</div>
    </form>
</body>
</html>

 
[test.js]

function hello(){
  alert('asdfasfsadfsadf');
}
// 当鼠标
function helloWorld()
{
    return;
}


 

  将这两个页面部署到IIS中。

  在IE中执行http://localhost/test.aspx

  将会报错(脚本方面的错误)。

  如果你把test.js中的“// 当鼠标”删掉,则不回报错,一切正常了。

  但在FireFox中,无论删不删那个注释都能正常运行。

 

  难道IE对脚本中的中文注释不能完全确认?试了时其它汉字好像没什么问题,就是这三个放在一起就不行。

  如果将test.aspx改成HTML格式的文件,在本地直接运行文件,则没事。

  例如改成test.html:


<html>
<head>
    <title>无标题页</title>
    <SCRIPT src="test.js" type="text/javascript"></SCRIPT>
</head>
<body onload="hello()">
    <form id="form1" runat="server">
    <div>test</div>
    </form>
</body>
</html>

2006/6/7

再谈:通过javascript取的浏览器客户区的尺寸

 
  上次说到“通过javascript取的浏览器客户区的尺寸”,是通过document.body.clientWidth 和 document.body.clientHeight来去客户区的高和宽,刚刚的实践中,发现在firefox下是无效的,几经试验发现了一个更好的方法,通过documentElement.clientWidth 和 document.documentElement.clientHeight来取客户区的高和宽。
 

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
      <title id="aaa">DIV布局</title>
      <meta http-equiv="Content-Type" content="text/html; charset=gb2312"/>
     
      <style type="text/css">
     
      body {
       BACKGROUND: #000000;
       MARGIN: 0px 0px 0px 0px;
       TEXT-ALIGN: center;
       width: 100%;
       height: 100%
      }
 
      #demo {
       BACKGROUND: #a5d854 ;
       MARGIN: 0px 0px 0px 0px;
       width: 2000px;
       height: 800px
      }
     
      </style>
      <script type="text/javascript">
       function sizeOfBody(x)
       {
        var ss = "";
        ss = "size of body(" + x + "): width:" + document.documentElement.clientWidth + " height:" + document.documentElement.clientHeight;
        alert(ss);
       }
      </script>
  </head>
  <body onload="sizeOfBody('onload')">
     <script type="text/javascript">
       sizeOfBody("begin of body");
     </script>
      <div id="demo">
         
      </div>
      <script type="text/javascript">
       sizeOfBody("end of body");
     </script>
  </body>
  </html>


执行的结果先后为:
    1. size of body(begin of body): width:1004 height:617
    2. size of body(end of body): width:1004 height:617
    3. size of body(onload): width:1004 height:617


  用这个有一个好处:无论在什么时机调用都是准确的客户区size,而且在firefox和IE下通吃。

 

通过javascript取的浏览器客户区的尺寸

 
  在开发基于WEB的管理系统或其它的应用,有时会需要把针对客户的视图固定为浏览的客户区一样的size,不用客户去拖拽浏览器的滚动条,这是我们需要在页面打开时取得浏览器的客户区size,然后根据取得的值用javaxcript调整页面的布局。

  可以通过body.clientWidth和bocy.clientHeight来取得我们需要的值。但是经过实际调试发现调用它们的时机的不同,得到的值是不一样的。这里做个分析。
 
  首先先写一段HTML代码作为用例:

  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
      <title id="aaa">DIV布局</title>
      <meta http-equiv="Content-Type" content="text/html; charset=gb2312"/>
     
      <style type="text/css">
     
      body {
       BACKGROUND: #000000;
       MARGIN: 0px 0px 0px 0px;
       TEXT-ALIGN: center;
       width: 100%;
       height: 100%
      }
  
      #demo {
       BACKGROUND: #a5d854 ;
       MARGIN: 0px 0px 0px 0px;
       width: 2000px;
       height: 800px
      }
     
      </style>
      <script type="text/javascript">
       function sizeOfBody(x)
       {
        var ss = "";
        ss = "size of body(" + x + "): width:" + document.body.clientWidth + " height:" + document.body.clientHeight;
        alert(ss);
       }
      </script>
  </head>
  <body onload="sizeOfBody('onload')">
     <script type="text/javascript">
       sizeOfBody("begin of body");
     </script>
      <div id="demo">
         
      </div>
      <script type="text/javascript">
       sizeOfBody("end of body");
     </script>
  </body>
  </html>

  这里body的样式中width,height一定要是100%,因为我们是通过boby的属性来取得浏览器的客户区。
 
  sizeOfBody函数是用来得到和显示客户区的size。
 
  我们分别在三个位置调用sizeOfBody函数。
    1. body的onload事件中;
    2. <body></body>内的开始,即:<div></div>之前;
    3. <body></body>内的结尾,即:<div></div>之后。
  
   执行的结果先后为:
    1. size of body(begin of body): width:1004 height:617
    2. size of body(end of body): width:2000 height:800
    3. size of body(onload): width:2000 height:800
  
  第一个结果是真正的客户区size(1004*617),即:浏览器的大小去掉了状态栏、地址栏、菜单栏、滚动条、边框等等剩下的专门显示网页的可见面积。后面的是已经加载了<div></div>后的size,body被“撑大了”,成了2000×800了。
 
2006/4/6

同时支持三种事件模型的javascript

 
  在web开发时,javascript是必然会用到的技术,包括Ajax也是以它为基础的。浸淫其中,最令我头疼的是其事件的处理。看到一篇好文,对事件部分讲得较为细致,转来一看。
 
转自:

  事件使得客户端的 JavaScript 有机会被激活,并得以运行。在一个 Web 页面装载之后,运行脚本的唯一方式,就是响应系统或者用户的动作。虽然从第一个支持脚本编程的浏览器面世以来,简单的事件被实现为 JavaScript 的一部分;但是大多数最近出现的浏览器都实现了强壮的事件模型,使脚本可以更加智能地处理事件。现在的问题在于:为了支持各种浏览器,您必须和多个先进的事件模型做斗争,准确地说,是三个。

  这三个事件模型分别和下面的文档对象模型(Document Object Model,即 DOM)三巨头结盟:Netscape Navigator 4 (NN4),Macintosh 和 Windows 系统的 Internet Explorer 4 及其更新版本(IE4+),以及在 Safari 中得到实现的 W3C DOM。尽管这些模型之间有些地方存在一些本质的差别,但是在一些简易的 JavaScript 的帮助下,它们都可以同时适用于同一个文档。本文主要着眼于相互冲突的事件模型中的两个关键方面:

  • 把一个事件和 HTML 元素绑定起来的方法。
  • 在事件被触发后如何对之进行处理。

事件绑定的方法

  事件绑定是指构造一个响应系统或者用户动作的 HTML 元素的过程。在不同的浏览器版本中,有不少于五种事件绑定技术。下面我们快速地介绍一下这些技术。

事件绑定方法I:绑定元素属性

  最简单和向后兼容性最好的事件绑定方法是把事件绑定到元素标识的属性。事件属性名称由事件类型外加一个“on”前缀构成。尽管HTML属性并不是大小写敏感的,人们还是定义了一个规则,规定事件类型的每一个“词”的首字母大写,比如 onClickonMouseOver。这些属性也被称为事件处理器,因为它们指示了元素如何“处理”特定的事件类型。

  正确的事件处理器属性的值在形式上是被引号包含的 JavaScript 语句。最常见的值是一条调用某个脚本函数的语句,而被调用的函数在位于文档前部的 <SCRIPT> 标识中定义--该标识通常位于 <HEAD> 部分。举例来说,下面的函数:

function myFunc() {
    // script statements here
}

  可以被定义为一个按键控件的事件处理器,按键的定义如下:

<INPUT TYPE="button" NAME="myButton" VALUE="Click Here" 
onClick="myFunc()">

  把事件绑定到元素属性上有一个优点,即可以支持开发者把参数传递给事件处理器函数。接收事件的元素的引用则由一个特殊的参数值--this 关键字来传递。下面的代码演示一个函数如何借助传入参数,把任意数目的文本框的内容转化为大写:

<SCRIPT LANGUAGE="JavaScript">
function convertToUpper(textbox) {
    textbox.value = textbox.value.toUpperCase();
}
</SCRIPT>
...
<FORM ....>
<INPUT TYPE="text" NAME="first_name" onChange="convertToUpper(this)"&gt
<INPUT TYPE="text" NAME="last_name" onChange="convertToUpper(this)"&gt
...
</FORM>

事件绑定方法II:绑定对象属性

  对于 NN3+ 和 IE4+ 这两类浏览器,脚本编程人员可以以脚本语句的方式把事件绑定到对象上,而不是绑定到元素标识的属性上。每一个负责事件响应的元素对象都为自己能够识别的事件设置了相应的属性。对象属性名称是元素标识属性的小写形式,比如 onmouseover。NN4 还接受 interCap(即首字小写,之后的每一个词的首字大写)版本的属性名,但是考虑到跨浏览器的兼容性,所有字母都是小写的名称会更安全一些。

  当您把一个函数的引用赋值给一个事件属性的时候,就发生了绑定。函数的引用是指函数的名称,但是不带函数定义中的括号。因此,如果要为一个名为 myButton 的按键的点击事件(click)进行绑定,使之激活一个定义为 myFunc() 的函数,则其赋值语句如下所示:

document.forms[0].myButton.onclick = myFunc;

  您应该注意一点:在事件触发的时候,没有办法向事件函数传递参数。本文在稍候对事件处理过程的讨论中还会回顾这个问题。

事件绑定方法III: 绑定 IE4+<SCRIPT FOR> 标识

  在 IE4+ 中,Microsoft 对 <SCRIPT> 标识实现了自己的扩展,可以将它包含的脚本语句和某个元素的一个事件类型进行绑定。支持这个绑定的标识属性(还没有被 W3C 批准为 HTML 的一部分)是 FOREVENT

  FOR 属性的值必须是您为元素的 ID 属性分配的唯一标识符。然后,您必须把事件的名称(onmouseover,onclick,等等)分配给 EVENT 属性。在上面的按键实例的基础上,我们必须对按键标识进行修改,使之包含一个 ID 属性:

<INPUT TYPE="button" NAME="myButton" ID="button1" VALUE="Click Here">

  脚本语句并不在函数中,而是在 <SCRIPT> 标识中,如下所示:

<SCRIPT FOR="button1" EVENT="onclick">
// script statements here
</SCRIPT>

  当然,标识中的语句可以调用页面上其它地方定义的任何函数(或者从.js文件中导入的函数)。然而,这种绑定方式意味着您必须为每一个元素和每一个事件创建一个 <SCRIPT FOR> 标识。

您还必须小心,只能把这种绑定方法部署在仅供 IE4+ 浏览器浏览的页面。其它任何支持脚本编程而又没有实现这个特殊的 <SCRIPT> 标识的浏览器(包括 IE3),都将把它作为常规的 <SCRIPT> 标识来处理,并试图在页面装载的时候执行这些脚本语句--这不可避免地引起脚本错误。

事件绑定方法IV:使用 IE5/Windows 的 attachEvent() 方法

  早在 W3C DOM 工作组磨砺出标准的事件模型之前,attachEvent() 方法已经被实现了,并且可被用于 Windows 版的 IE5 或更新版本的浏览器上的每一个 HTML 元素。

attachEvent() 方法的用法如下所示:

elemObject.attachEvent("eventName", functionReference);

  eventName 参数的值是表示事件名称的字符串,比如 onmousedownfunctionReference 参数是一个不带括号的函数引用,和早些时候描述的事件属性方法中一样。因此对于上面例子的按键对象,可以通过如下的脚本语句把函数绑定到按键的 click 事件:

document.getElementById("button1").attachEvent("onclick", myFunc);

  由于 attachEvent() 方法必须严格工作在 IE5+/Windows 的环境中,所以您既可以使用 W3C DOM 的元素引用方式(如上文所示),也可以使用 IE4+ 的引用方式:

document.all.button1.attachEvent("onclick", myFunc);

  这个方法有一个值得注意的地方:您不能在元素被载入浏览器之前执行这个语句。该对象的引用在相应的 HTML 按键元素被浏览器创建之前,都是无效的。因此,要让这样的绑定语句或者在页面的底部运行,或者在 BODY 元素的 onLoad 事件处理器调用的函数中运行。

事件绑定方法V:使用 W3C DOM 的 addEventListener() 方法

  Safari 使用的是 W3C DOM 级别2定义的事件绑定机制,这个机制和 IE5/Windows 的 attachEvent() 方法很类似,但是有自己的语法。W3C DOM 规范为 DOM 层次中的每一个结点都定义了一个 addEventListener() 方法。HTML 元素是 DOM 结点中的一类,在一对元素标识内部的文本结点也是一个结点,也能够接收事件。这一点在 NN6 事件处理过程中经常得到体现,在本文的后面部分您将会看到。

addEventListener() 方法的语法如下所示:

nodeReference.addEventListener("eventType", listenerReference, captureFlag);

  用 W3C DOM 规范中的行话来说,addEventListener() 方法为指定的结点注册了一个事件,表示该结点希望处理相应的事件。这个方法的第一个参数是一个声明事件类型的字符串(不带"on"前缀),比如 clickmousedown,和 keypressaddEventListener() 方法的第二个参数可以和早些时候描述过的函数引用同样对待。第三个参数则是一个 Boolean 值,指明该结点是否以DOM中所谓的捕捉模式来侦听事件。事件的捕捉和派发---综合起来称为事件的传播--最后由另一篇文章来描述。对于一个典型的事件侦听器来说,第三个参数应该为 false(假)

那种绑定方法最好?

  如果您足够幸运,只需要为某一个操作系统上特定版本的浏览器创建应用程序,则可以为选定的浏览器选择最现代的绑定方式。但是对于跨浏览器的网站作者来说,选择绑定方法则需要面对实质性的挑战。

  如果您只计划支持 IE5/Mac,则可以不考虑 attachEvent()addEventListener() 方法,因为 IE5/Mac 对这两种方法都不支持。这种情况下,比较实际的选择有两种,要么绑定标识属性,要么绑定对象属性。这时就需要费心思了。

  一方面,W3C DOM Level 2 承认基于标识属性的方法,并将它推荐为 addEventListener() 方法的可接受代替方法。为了和数以百万计的脚本相兼容,所有支持脚本编程的浏览器都支持基于标识属性的事件绑定方法。一些自动化的页面制作工具,比如 DreamWeaver,也把事件处理器的属性嵌入到 HTML 标识中。

  但是另一方面,在元素标识文件中嵌入面向脚本的信息,又不能将内容从风格及行为中分离开来,这和当前的流行趋势相违背。把事件绑定到对象属性上的方法听起来方向是对的,但是在 W3C 关于 HTML,XHTML,或者 DOM 的标准中,并没有对事件属性提供“官方”的支持。尽管如此,在实际生活中,除了第一代支持脚本编程的浏览器之外,其它浏览器都支持这种方法。

  一个纯标准论者会认为上述的两种方法都有缺点,但是对于讲究实际的开发者来说,即使考虑到未来主流浏览器的兼容性,这两种方法都是“安全”的。

事件的信息矿:事件对象

  所有这三种事件模型的核心都是一个事件对象--它是一个抽象的实体,其属性中包含很多对事件处理函数具有潜在价值的信息。从本文早些时候对事件绑定技术的讨论中,您可能可以推断出事件对象对脚本之所以至关重要,原因之一是除了基于标识属性的绑定方法以外,其它绑定方法都不支持将参数传递到事件处理函数中。

  事件对象通过提供足够的“挂钩”,使事件处理函数可以读取事件的特征,从而填补了这个缝隙。因此,事件处理函数可以得到接收事件的元素的引用,以及其它一些有用的信息,比如鼠标动作的坐标,鼠标使用的按键,键盘上被按压的键,以及在事件发生的过程中是否有修饰键被按下(比如检测 Shift-click 事件)。

访问事件对象

  虽然事件对象的精确构成因为本文讨论的三种 DOM(NN4,IE4+,以及 W3C/Safari)的不同而有所变化,但是,一个事件处理函数只能通过以下两种方式之一来访问事件对象:NN 方式和 IE 方式。W3C/Safari DOM 事件对象公布给脚本的接口方式和 NN4 的事件对象一样;而 IE4+ 则有自己的方法。

  IE4+ 的事件对象更加易于描述,因此我们首先对它进行讨论。简单地说,事件对象是 window 对象的一个属性。这意味着在所有的实例中只有一个事件对象。举例来说,在键盘上简单地按压和松开一个按键,会产生三个事件:onKeyDownonKeyPress,和 onKeyUp(事件的发生顺序和这里的列举顺序相同)。如果 onKeyDown事 件激活的函数花费很长的时间进行处理,则浏览器就会把其它两个事件保持在队列中,直到 onMouseDown 事件处理完成为止。

  而对于 NN4 和 W3C DOM 来说,事件对象看起来就更加抽象一些。除了基于标识属性风格的绑定方法之外,其它绑定方法都是把事件对象自动传递给与事件相绑定的函数。传递给函数的是一个单一的参数。开发者需要在函数中定义一个参数变量,来“接收”该参数的值。为了避免和IE中的 window.event 对象互相冲突,请不要把参数命名为 event。举例来说,把它命名为 evt 就相当好,相应的事件函数的定义大致如下:

function myFunc(evt) {
    // script statements here
}

  然而,如果您使用的是基于标识属性的事件绑定技术,就必须显式地把事件作为一个参数传递到您调用的函数。为了完成事件的传递,需要把 event 这个关键字作为参数进行传递:

onClick = "myFunc(event)"

  外部传入的参数是您的事件处理函数和 NN 的事件对象之间的唯一联系纽带。如果在主事件处理函数内部调用的其它函数需要该对象或者该对象的属性值,则您可以把该对象或其属性值作为参数中继给这些函数。

  如果您想知道 IE 是否把事件的引用保存在 window.event 属性中,那答案是“是”。使用这个语法交集是相当安全的,因为在 NN 和 IE 这两个浏览器,被传递到事件处理函数的事件对象都有您所期望的当前事件的属性值。

兼容两种事件对象引用

  设想在处理事件时,我们需要在一个事件函数中考察一个或者多个事件属性。这是一个简单的技术,可以使事件处理函数和作为参数传入的事件对象协同工作,或者从 window.event 属性中读取信息。而且,这个技术不必处理不同的浏览器版本之间的细微差别。

  在开始的时候,需要在您的事件处理函数中定义一个参数变量,准备接收可能传入的事件对象。然后,通过简单的条件表达式把浏览器的事件对象赋值给上述的参数变量:

function myFunc(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    // process event here
}

  如果事件对象真的以参数的形式传进来了,则在函数内部,事件对象就被保留在 evt 这个局部变量中。如果这个参数是 null,而且浏览器的 window 对象包含有一个 event 属性,则 window.event 对象就会把自己赋值给 evt 变量。

  然而,为了完成这个工作,还应该再包含一层或者多层条件控制,以便优雅地适应那些在事件模型中没有定义事件对象的的早期浏览器:

function myFunc(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    if (evt) {
        // process event here
    }
}

  为了把同样的方式应用到所有事件处理函数的构建中,您可以定义一个函数来兼容两种事件,即由绑定的标识属性显式传入的事件对象,以及由绑定的事件属性隐式传入的事件对象。这样即使您在开发过程中改变了事件绑定的风格,这个函数也不必改变。

瑞典自助餐式地选择事件对象

  然而,建立一个指向事件对象的引用只是战斗的一部分。来自不同事件模型的每一个事件对象都拥有自己的一套属性,以容纳事件的细节。下面的表格列出了最常用的属性,以及这些属性在上述三种事件对象类型中的名称。

表格 1. 流行的事件对象属性

描述 NN4 IE4+ W3C/Safari
Event target target srcElement target
Event type type type type
X coordinate on page pageX * pageX
Y coordinate on page pageY * pageY
Mouse button which button button
Keyboard key which keyCode keyCode

标注*的属性值可以通过对 event.clientX + document.body.scrollTop 或者 event.clientY + document.body.scrollTop 进行求值来得到。

  Macintosh 版本的IE5在通常情况下都遵循 IE4+ 的事件对象模型,但是有一个例外,即 IE5/Mac 的事件对象既定义了 srcElement 属性,也定义了 target 属性,这两个属性都指向接收事件的元素。

  需要抽象的最重要的事件对象属性可能得算指向接收事件的 HTML 元素的引用。NN4 和 W3C 的事件对象采用相同的属性名(target),而 IE4+ 的事件对象则使用 srcElement 属性。这时候,对象检测技术(而不是费力劳神而又具有危险倾向的浏览器版本识别方法)再次拯救了我们。对于那些非文本容器的元素,一个简单的条件表达式就可以轻松处理脚本语法上的差别:

var elem = (evt.target) ? evt.target : evt.srcElement

  从现在开始,您的脚本就可以读写任何浏览器对象模型公布出来的元素对象属性了。

W3C DOM结点的事件目标

  W3C DOM 的结点架构使得文档中的每一个结点都可以接收事件。在支持这一架构的浏览器中,发生在嵌套文本顶上的事件并不调用分配给文本容器的事件处理器,相应的文本结点才是该事件的目标结点。考虑如下场景:

  在事件实例,当鼠标的指针在一个 SPAN 元素包含的文本顶上滚动时,该文本就会被高亮显示。 事件绑定的过程通过对象属性在 init() 函数中进行。从表面上看,当用户在 SPAN 元素顶上滚动鼠标时,onMouseOver 事件动作函数就为该元素指派一个与风格表单规则相关联的类名(highlight),该风格规则把文本的显示风格定义为粗体,黄色背景;而在 onMouseOut 函数中,则把风格恢复为原始的版本(类 normal)。请注意一个 toggleHighlight() 函数是如何在事件对象的 type 属性的帮助下,执行两个动作的(该属性在所有事件模型对象中的名称是相同的)。请试一下这个事件实例

  但是如果您把例子装载到 NN6,则鼠标事件的真正目标就是 SPAN 元素中的文本结点了。本文并不讨论事件的传播机制,但是请相信,W3C DOM 事件模型的缺省行为会使事件沿着结点的包含层次向上传播(和 IE4+ 中事件通过元素容器向上传播的机制很类似)。因此,在这个事件实例中。鼠标事件会从其真正的目标向上传递到文本结点的容器(也就是 SPAN 元素)。这些事件触发了 SPAN 元素中相应的事件处理器。

  虽然事件处理器属于 SPAN 元素,事件对象还是保留文本对象的引用,并将它作为事件的原始目标。然而,只有对文本结点的容器进行动作,才能修改它的风格。为了实现 toggleHighlight() 函数的等价操作,使之可以修改SPAN容器的 className 属性,该函数需要派生出一个指向文本结点容器的引用。

  一个策略是使用 W3C DOM 事件对象的 currentTarget 属性,该属性返回一个处理事件的结点的引用。脚本中的决策树需要考虑这个属性,增加代码之后的 toggleHighlight() 函数如下所示:

function toggleHighlight(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    if (evt) {
        var elem
        if (evt.target) {
            if (evt.currentTarget && (evt.currentTarget != evt.target)) {
                elem = evt.currentTarget
            } else {
                elem = evt.target
            } 
        } else {
            elem = evt.srcElement
        }
        elem.className = (evt.type == "mouseover") ? "highlight" : "normal"
    }
}

  另一个可选的方法是考察由 target 属性返回的对象的 ronodeType 属性。一个能够把事件定向给文本结点的浏览器,也可以把一个文本结点的 nodeType 属性值报告为3,而不是报告为元素结点的类型(其值为1)。如果事件的目标是一个文本结点,则脚本程序就可以通过该文本结点的 parentNode 属性来得到其上级元素结点的引用。这种方法的决策树在某种程度上得到更多的改进:

function toggleHighlight(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    if (evt) {
        var elem
        if (evt.target) {
            elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
        } else {
            elem = evt.srcElement
        }
        elem.className = (evt.type == "mouseover") ? "highlight" : "normal"
    }
}

  如果您正在用遵循 W3 的浏览器阅读本文,则请尝试这个修改过的版本,看看鼠标滚动时的风格变化。

  这个页面使用了嵌入到事件实例中的最新版本的 toggleHighlight() 函数,展示了如何使用 JavaScript 为那些能够显示期望效果的浏览器增加额外的价值,同时也可以那些基本的内容提供给仍然使用着较老版本或者不支持脚本编程的浏览器的用户,只不过在模式上不那么动人和便于交互。

一个事件处理函数的模板

  并不是每个事件处理函数都处理页面元素对象中同样的属性或者行为,但是,从上文的讨论可以派生出来的一个模板,您可以在这个模板的帮助下开始编码。模板如下:

function functionName(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    if (evt) {
        var elem
        if (evt.target) {
            elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
        } else {
            elem = evt.srcElement
        }
        if (elem) {
            // process event here
        }
    }
}

  请把第一行的函数名替换为您希望的函数名,并在注视指示的地方开始书写具体事件的代码。这个格式应该可以为您提供一个起点,适合于您采用的任何跨浏览器的事件绑定风格。如果您需要在一个页面中多次使用这个格式,则可以进一步精简代码,即把读取目标的代码抽象成一个可重用的工具函数,然后在每一个事件处理函数中进行调用:

// shared function
function getTargetElement(evt) {
    var elem
    if (evt.target) {
        elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
    } else {
        elem = evt.srcElement
    }
    return elem

}

function functionName(evt) {
    evt = (evt) ? evt : ((window.event) ? window.event : "")
    if (evt) {
        var elem = getTargetElement(evt)
        if (elem) {
            // process event here
        }
    }
}

  有了这类框架,您现在应该可以把更多的注意力集中在各个事件处理函数要求的具体动作中了。

查看实例:

下载脚本