探讨微软ASP.NET AJAX控件开发技术(客户端)

一、简介

如今,ASP.NET AJAX框架以其与ASP.NET 2.0系统的有机整合与完全面向对象的客户端JavaScript组件模型正在吸引着越来越多的Web开发人员。此外,这个框架还为基于ASP.NET 2.0平台的AJAX Web开发提供了一揽子方案(尽管尚嫌稚气)。

首先,我们来回忆ASP.NET AJAX框架设计的主要目标:其一,扩展现有的ASP.NET服务器端模型,让其能够生成支持富客户端的JavaScript代码;其二,为ASP.NET应用增加客户端编程模型,让纯粹的客户端编程变得更为容易。

基于此,我们至少可以从如下两个方面来扩展ASP.NET AJAX框架。第一,创建更为丰富的ASP.NET AJAX服务器端扩展器控件,以便为这些控件提供丰富的客户端Ajax行为。此部分的极好例证就是AJAX Control Toolkit中的大部分控件。第二,创建纯粹的客户端组件/控件。这部分的例证就是框架已经实现的对于常规HTML元素的封装、高级客户端控件ListView及客户端离线数据源DataSource等。为此,控件开发者可以创建新的客户端组件/控件来封装典型的客户端功能,或者基于现有客户端组件/控件加以扩展。

【说明】第一,事实上,我们可以进一步沿着ASP.NET AJAX客户端与服务器端架构层次关系图进一步扩展其底层。第二,要开发ASP.NET AJAX控件要求开发人员首先具备关于ASP.NET AJAX框架的客户端和服务器器知识(相比而言,在具体编程的过程中要求掌握更多的客户端相关知识)。

在本文中,我们将从客户端和服务端两个角度全面探讨ASP.NET AJAX框架中的控件开发相关技术。首先,让我们从客户端开始。

二、控件开发客户端相关技术

微软Ajax库的最伟大之处在于,它扩展了JavaScript的面向对象编程模型并且提供一个增强的类型系统(其中包含命名空间,类,接口,枚举,异常处理,反射及其它为.NET开发者所熟悉的结构)。

尽管Ajax控件都是继承自Sys.UI.Control,但是,整个Ajax控件的核心却是Sys.Comoponent类。这个类模拟了.NET框架的System.ComponentModel.Comoponent类。图1展示了在Ajax控件开发中继承类之间关系的示意图。

图1:Ajax控件开发中所涉及的类关系图

(一)Sys.IDisposable

这个接口类似于.NET框架的IDisposable接口。组件利用的所有资源都应该在dispose方法中释放。具体地说,这些资源包括事件处理器,大型数组,等等。

(二)Sys.INotifyDisposing

这个接口允许组件用户通过disposing事件检测该组件的释放情况。

(三)Sys.INotifyPropertyChange

这个接口允许组件用户通过PropertyChanged事件检测有关属性的改变情况。在本文后面,我们还要对此展开详细的讨论。

(四)Sys.Component

该Sys.Component类要实现上面所有的接口。封装了复杂的逻辑或包含子DOM元素的组件一般都要求使用一个中央位置用于初始化和清除类的实例。这通常是通过重载Sys.Component类的initialize和dispose方法实现的。下列图2展示了Sys.Component类的主要功能。

图2:Sys.Component类主要功能展示

进一步归纳来看,共存在两种类型的组件:

◆Nonvisual(非可视化的)
◆Visual(可视化的)

一个非可视化的组件没有任何用户接口。一个典型的例子是ASP.NETAJAX框架内置的定时器控件或一个定制的对web服务进行队列调用的组件。非可视化组件类似于ASP.NET中的ObjectDataSource和TableAdapter控件,但是没有任何用户接口。

另一方面,可视化的组件则提供了一个用户接口。一个典型的例子是Ajax Control Toolkit中的控件,UpdateProgress,以及AjaxGrid控件等等。其实,我们还可以把可视化组件进一步划分为:

◆Action(Sys.UI.Action)
◆Control(Sys.UI.Control)
五)Sys.UI.Action

一个行为(Action)的目的是用来扩展一个DOM元素而不改变它的核心功能。Ajax Control Toolkit中的大多数控件就属于行为,例如该AutoCompleteTextBox,MaskEdit,DragPanel等。一个行为必须拥有一个相关联的DOM元素。注意,单个DOM元素可以拥有多个与之相关联的行为。既然本文的重点在讨论控件的开发技术,所以,我们不再进一步讨论行为的概念。如果你对此感兴趣的话,请访问Ajax Control Toolkit站点。

(六)Sys.UI.Control

相对于上面的Action而言,一个Control(控件)本身就是一个DOM元素。创建一个控件的主要目的是通过对一个现有控件加以包装进而提供新的功能。其典型的示例就是UpdatePanel,UpdateProgress或Ajax Control Toolkit中新加入的Tab控件。下列的代码向你展示了创建一个控件所要求的最少代码。

列表1:构建一个客户端控件至少需要实现的JavaScript框架代码

Type.registerNamespace('DummyNamespace');
DummyNamespace.DummyControl = function(element)
{
DummyNamespace.DummyControl.initializeBase(this, [element]);
}
DummyNamespace.DummyControl.prototype =
{
initialize : function()
{
DummyNamespace.DummyControl.callBaseMethod(this, 'initialize');
},
dispose : function()
{
DummyNamespace.DummyControl.callBaseMethod(this, 'dispose');
}
}
DummyNamespace.DummyControl.registerClass('DummyNamespace.DummyControl',
Sys.UI.Control);
if (typeof (Sys) != 'undefined')
{
Sys.Application.notifyScriptLoaded();
}

上面所列出的仅是一个控件的框架代码。如你所见,与之相关联的DOM元素作为参数传入该控件的构造器函数中。我们还要重载initialize和dispose方法以便执行初始化和最后释放控件时的清理工作。因为在此列出的仅是一个虚构的控件,所以,除了调用基类的一些方法外我们什么也没有做。下列图3展示了Sys.UI.Control类提供的主要功能。

图3:Sys.UI.Control类提供的方法

一个控件和一个行为之间的另一个区别在于,一个行为允许你设置一个id而一个控件却不允许。事实上,一个控件的id与其相关联的DOM元素是一致的。下面是这个类中常用方法(有些是父类提供)的详细解释。

◆get_element():返回该控件描述的DOM元素。
◆get_id():返回该控件的id,为$find语句所用以引用此控件。
◆set_id():如果你试图设置id,那么,你将得到一个Error.invalidOperation异常,因为控件是不允许设置id的。
◆get_parent():返回父级控件。
◆set_parent():设置父控件。
◆get_visibilityMode():visibilityMode是一个枚举,其取值是hide或collapse。
◆set_visibilityMode():设置visibilityMode的值。
◆get_visibile():返回相应DOM元素的可见性。
◆set_visibile():设置DOM元素风格可见性,取值为hidden或visible。
◆addCssClass():把指定的CSS类添加到DOM元素的className中。
◆dispose():继承自Sys.Component。
◆initialize():继承自Sys.Component。
◆onBubbleEvent():处理由raiseBubbleEvent激发的事件。如果这个方法返回true,则该事件将不会被上传到其父元素中进行处理。注意,如果你不处理该事件的话,你应该把此事件交由父级来作默认处理。
◆raiseBubbleEvent():引发一个事件并交由父控件处理。总的来看,当你创建复杂的控件(经常是包含一个或多个子控件并且想把一个事件从子控件上交由其父控件来处理时)时往往要使用onBubbleEvent与这个方法。
◆removeCssClass():从DOM元素的className中删除指定的CSS类。
◆toggleCssClass():把指定的CSS类添加到DOM元素的className中(如果以前没有设置的话)。注意,如果已经指定了这个CSS类,那么,将从className中移除该类。

三、实例分析

在本示例中,我们将创建一个增强的客户端ImageButton控件。这个控件在鼠标移过时将显示一幅不同的图像。

(一)创建示例AJAX网站

启动Visual Studio 2005,选择“文件→新建网站…”,然后选择“ASP.NET AJAX-Enabled Web Site”模板,命名工程为“AjaxClientCtrlTest”,并选择C#作为内置支持语言,最后点击OK。

注意,此后系统将自动加入对于程序集System.Web.Extensions.dll的引用(因其被自动加入到GAC中,所以默认情况下无法直接看到)。当然,你还会注意到,作为ASP.NET AJAX控制中心的服务器控件ScriptManager被自动地添加到默认网页Default.aspx中。然后,把一个HTML 元素拖动到此页面中,并稍加修改,得到如下图4所示布局。

图4:示例网站屏幕快照

(二)使用面向对象JavaScript设计控件类

以鼠标右击工程添加一个JavaScript脚本文件ImageButton.js,内容如下列表2所示。

列表2:脚本控件MyCliImageButton完整源码

Type.registerNamespace('AjaxImageButtonNamespace');
AjaxImageButtonNamespace.MyCliImageButton = function(element)
{
this._hoverImageUrl = '';
this._originalImageUrl = '';
this._mouseOverHandler = null;
this._mouseOutHandler = null;
this._clickHandler = null;
AjaxImageButtonNamespace.MyCliImageButton.initializeBase(this, [element]);
}
AjaxImageButtonNamespace.MyCliImageButton.prototype =
{
get_hoverImageUrl : function(){
return this._hoverImageUrl;
},
set_hoverImageUrl : function(value) {
var e = Function._validateParams(arguments, [{name: 'value', type: String}]);
if (e) throw e;
if (this._hoverImageUrl != value)
{
this._hoverImageUrl = value;
this.raisePropertyChanged('hoverImageUrl');
}
},
initialize : function(){
AjaxImageButtonNamespace.MyCliImageButton.callBaseMethod(this, 'initialize');
var target = this.get_element();
this._originalImageUrl = target.src;
this._mouseOverHandler = Function.createDelegate(this, this._onMouseOver);
this._mouseOutHandler = Function.createDelegate(this, this._onMouseOut);
this._clickHandler = Function.createDelegate(this, this._onClick)
$addHandlers(target, {'mouseover':this._mouseOverHandler,

'mouseout':this._mouseOutHandler, 'click': this._clickHandler}, this);
},
dispose : function(){
$clearHandlers(this.get_element());
delete this._mouseOverHandler;
delete this._mouseOutHandler;
delete this._clickHandler;
AjaxImageButtonNamespace.MyCliImageButton.callBaseMethod(this, 'dispose');
},
add_click : function(handler) {
this.get_events().addHandler('click', handler);
},
remove_click : function(handler) {
this.get_events().removeHandler('click', handler);
},
_onMouseOver : function(e) {
e.target.src = this._hoverImageUrl;
},
_onMouseOut : function(e) {
e.target.src = this._originalImageUrl;
},
_onClick : function(e) {
e.preventDefault();
var handler = this.get_events().getHandler('click');
if (handler != null) {
handler(this, Sys.EventArgs.Empty);
}
}
}
AjaxImageButtonNamespace.MyCliImageButton.registerClass

('AjaxImageButtonNamespace.MyCliImageButton', Sys.UI.Control);
if (typeof(Sys) != 'undefined')
Sys.Application.notifyScriptLoaded();
}

如你所见,我们重载了initialize方法来“钩住”对应DOM元素的mouseover,mouseout和click事件,以便我们可以在这些事件中设置合适的图像并激发click事件。我们还重载了dispose方法以分离我们在initialize方法中设置的事件处理器。后面,我们还要细致讨论。

(三)使用控件

有了上面的控件类,至于使用就很简单了。请切换到页面default.aspx的源码视图,然后在区创建如下列表3所示内容。

列表3

    

在此,我们使用ASP.NET AJAX客户端全局方法$create创建控件AjaxImageButtonNamespace.MyCliImageButton的一个实例,指定其属性hoverImageUrl取值、click事件的处理器函数,并把它与当前页面中的HTML 元素关联起来。当然,为了使得框架在运行时能够找到我们刚才建立的脚本文件,还要对ScriptManager的属性稍微设置一下,如下列表4所示。

列表4:在ScriptManager控件内配置脚本文件





现在,请按F5键运行此页面观察结果即可。下图5相应于当鼠标移动到图像按钮上时发生的变化(你还可以点击之自行观察效果)。

图5:当鼠标移动到图像按钮上时图像切换成另一幅

下面,我们针对使用ASP.NET AJAX框架提供的面向对象JavaScript进行客户端控件编程时有关概念作全面分析。

四、使用JavaScript进行客户端控件编程相关概念

(七)属性

把属性添加到一个控件中是很简单的。在此,推荐使用的方法是在控件构造器中声明一个私有变量,并在prototype部分添加相应的getter及setter属性方法。当访问该属性时,ASP.NET AJAX框架总是添加get_和set_前缀。因此,当添加getter/setter时,我们应该遵循get_propertyName和set_propertyName命名惯例。正如在上面的示例中相应于hoverImageUrl属性所实现的,我们使用get_hoverImage()作为getter,set_hoverImage()作为setter。但是在上面的例子中当我们使用一个$create()语句创建该控件时,我们仅传递了hoverImageUrl,因为ASP.NET AJAX框架将会自动添加set_前缀。与属性有关的另外一个重要问题是,当它的属性改变时如何通知该控件的客户端。下列的代码片断展示了如何激发propertyChanged事件。

set_hoverImageUrl : function(value){
if (this._hoverImageUrl != value)
{
//如果不同于当前值则仅设置新值
this._hoverImageUrl = value;
//激发propertyChanged事件
//注意这是一个位于基类Sys.Component中的方法
this.raisePropertyChanged('hoverImageUrl');
}
}

(八)方法

使用JavaScript编写一个类的方法时,一般不作特殊考虑;但是,我们还是推荐在控件的原型(prototype)对象中添加它们。

【注】就JavaScript本身编程而言,其表达形式多样,但建议你遵循这里推荐的格式(也正是框架相应js源码中所使用的格式)。

(九)事件

在控件类中添加事件类似添加属性。首先,在控件构造器中把事件处理器声明为一个私有变量并在原型对象中创建相应于该事件的一个add/delete方法对。尽管并不要求在模块级声明事件处理器变量,但是,这样做的确存在一些优点,我将在Ajax框架的Function.createDelegate()节讨论这个问题。当添加/删除事件时,Ajax框架总是自动地添加上add_和remove_前缀。因此,当添加事件订阅代码时,该方法应该以add_eventName方式命名;反之,以remove_eventName方式命名。例如,在上面的click事件示例中,我们以add_click和remove_click形式添加事件订阅方法,但是在$create语句中,我们以click方式对它命名,由Ajax框架负责添加add_前缀。如你在上面的示例所见,add_click和remove_click都引用了this.get_event()(继承自Sys.Component)。这个Sys.Component类维持着一个订阅事件的内部列表,存储于一个Sys.EventHandlerList类的实例(以及各自它们的处理器)中。

除了这个get_event()方法对外,还推荐使用raiseEventName方法来激发事件。由于上面所举的仅是一个很基本的例子,所以,我没有在其中使用它。但是,当你想从多处激发同一个事件时你必须添加它。当激发一个事件时,我们可以传递一些称为事件参数的上下文数据。

在事件中传递参数的推荐的方法是,创建一个继承自Sys.EventArgs的新类并构造器中传递要求的参数。针对每一个参数,它将在原型对象部分暴露getter和setter。因为本例非常简单,所以,没有使用这种更一般的方式。
当前在该微软Ajax库中相应于事件参数存在两个内置类—Sys.EventArgs和Sys.CancelEventArgs。即使你有一个不需要任何事件参数的事件,那么,你也应该传递Sys.EventArgs.Empty(例如在上面的ImageButton控件示例中),以便事件订阅者可以遵循相同的模式来处理所有的事件。

如果你细致研究框架源码,你会注意到,其中绝大部分的控件都遵循了相同的模式—使用函数HandleEvent(sender, e)来处理所有事件。注意,这与.NET框架处理事件所实现的模式是相同的。而且,遵循这种方式给人的初始感觉过于复杂,但是却更遵循了一般性规律,便于以后的更复杂设计。

五、其它几个重要的全局客户端方法

下列,我们列出在开发客户端控件时,一些常用(有些是必用的)的重要方法。

$get()

这个方法是我们以前常用的Document.getElementById方法的一个快捷方式。此方法使用一个可选的父级DOM元素;如果没有指定这个父元素,那么,搜索操作将从Document对象开始执行。

$create()

这个方法是Sys.Component.Create方法的一个快捷方式,用于实例化一个客户端组件,行为或控件。这个方法的完整定义如下所示:

$create(type, properties, events, references, element);

举例:

$create(AjaxImageButtonNamespace.MyCliImageButton, 
{'hoverImageUrl':'Images/updateh.gif'},
{'click':buttonClicked}, null, $get('cliBtn'));

在此,第一个参数使用了对象的完整类型名。例如,当创建一个MyCliImageButton控件时,我们需要传递AjaxImageButtonNamespace.MyCliImageButton。这个参数必须存在。

第二个参数对应一个键/值对(每一个一个键/值对对应一个特定属性及其该属性的取值)数组。例如,当创建MyCliImageButton控件时,我们需要要传递hoverImageUrl并给它赋值为{'hoverImageUrl':' Images/update-h.gif'}。如果我们想设置更多的属性,我们需要使用一个逗号来隔开这些键/值对。例如:

{'aProperty':value1, 'bProperty':value2}.

第三个参数与第二个参数几乎相同。这个参数用于定义事件,也使用一个键/值对数组的表示方式。在上面的例子中,我们使用onButtonClick处理器订阅了click事件。

第四个参数使用一个键/值对数组,用于引用那些在创建此组件时所需要使用的组件。既然本例中的图像按钮并没有请求任何对外部组件的引用;所以,我们简单地传递null为这个参数值。

第五个也即是最后一个参数是一个DOM元素。在创建行为和控件时,这是一个必需的参数;但是,对于一个非可视化组件而言,这个参数是可选的。

$find()

这个方法是Sys.Application.findComponent静态方法的快捷方式。其使用语法为:

var o = $find(id, [parent]);

因此,它类似于$get方法;但是,它返回的是组件或其子类而不是DOM元素。可以给它指定一个可选的IContainer类型的父类,以便从它开始搜索。如果没有指定父类,那么,它从Sys._Application开始进行搜索。

【注】这里的parent既可以是Component也可以是Sys.UI.DomElement。当其类型为Component时,搜索对象被限定为它的子组件;当其类型为DOM元素时,搜索对象被限定为其下的子元素。

Function._validateParams

我们知道,JavaScript并不是一个强类型化的语言;所以,为了充分保证方法中的参数符合既定的类型,必须进行大量编码,例如使用typeOf,parseInt,parseFloat,(parameters[0]!=null)等语句对参数进行测试。很可能是基于这样的原因,Ajax开发小组包括了这样一个功能极为强大的函数。现在,仅凭一个语句,这个函数就可以实现校验所有的方法参数。

【注】虽然是一个函数,但它实际上是Function._validateParameterCount、Function._validateParameter及Function._validateParameterType等多个子函数的功能组合。读者可以参考下载源码中的MicrosoftAjax.js文件自行分析。

让我们来看一个简短的示例:

set_hoverImageUrl : function(value)
{
var e = Function._validateParams(arguments, [{name: 'value',
type: String, mayBeNull: false}]);
if (e) throw e;
//……
}

在上面的示例中,我们要保证参数值是一个字符串数据类型并且非空。所以,这个函数的正确形式应该是:

Function._validateParams(params, expectedParams)

第一个参数是一个参数数组,通常是参数变量;第二个参数是一个定义了每一个参数信息的数组。每一个定义可以包含:

◆Name:参数名;
◆Type:参数的数据类型,例如String,Number,Boolean,Array或任何其它你自己创建的定制类型;
◆mayBeNull:取值为true或false;缺省为false;
◆optional:取值为true或false;缺省为false;
◆parameterArray:取值为true或false;缺省为false;
◆integer:取值为true或false;缺省为false;
◆elementType:如果参数是一个数组,那么该项用于指定此数组包含的数据类型;
◆elementInteger:如果参数是一个数组,那么该项用于确定此数组包含的是integer;
◆elementDomElement:如果参数是一个数组,那么该项用于确定该数组包含的是DOM元素;
◆elementMayBeNull:取值为true或false。如果参数是一个数组,那么该项用于确定是否该数组可以拥有任何的空项。

如果校验失败,Function._validateParams将返回一个应该由此函数的调用者抛出的异常。

$addHandler

这是Sys.UI.DomEvent.addHandler的一个快捷方式。它提供了一种一致的方式用于“钩住”DOM元素的事件;同时,隐藏了不同浏览器下事件操作的复杂性。该方法的完整形式如下所示:

$addHandler(element, eventName, handler);

第一个参数是一个DOM元素的引用。第二个参数相应于事件名(这里不必提供“on”前缀,ASP.NET Ajax系统为自动地添加这个前缀)。最后一个参数用于指定该事件的处理器函数。例如,在下列代码中,我为按钮“btnSelect”绑定了click事件(也即是为此按钮添加了一个事件代理,这与C#中的事件机制是基本一致的)。

$addHandler($get('btnSelect'), 'click', selectRowHandler);
//更多代码……
function selectRowHandler()
{
//处理click事件的代码
}
//更多代码……

$removeHandler

删除一个之前使用$addHandler创建的事件处理器。这个方法的语法形式与$addHandler完全一致。实际使用中,一般建议在pageUnload事件中删除所有的事件处理器。

$addHandlers

这个方法类似于$addHandler,其语法如下所示。

$addHandlers(element, events, [handlerOwner]);

因此,它允许使用单个语句指定相同DOM元素的多个事件代理。例如:

$addHandlers(this.get_element(), {'mouseover':this._mouseOverHandler, 
'mouseout':this._mouseOutHandler, 'click': this._clickHandler}, this);

在上面的代码中,我们使用一个语句添加了mouseover,mouseout和click事件。第一个参数是对DOM元素的引用;第二个参数是一个“事件名/处理器”对数组。最后一个参数是一个可选的参数(通常是this)。

$clearHandlers

删除所有之前使用$addHandler或$addHandlers创建的事件处理器。它仅有一个参数(引用一个DOM元素)。例如,下列语句会清除所有添加到一个DOM元素上的事件处理器函数:

$clearHandlers(this.get_element());

Function.createDelegate

这是一个既能够“包装”一个现有函数,又能够返回一个新函数的函数。创建此函数的主要目的是解决this关键字的问题。在一个由一个DOM元素引发的事件处理器中,this关键字总是引用此DOM元素而不是类本身。这一点很重要,我们不妨来考虑下列的示例。如果我们如下面这样对图像按钮控件编程,那么,它将总是引发一个异常:

//…………
initialize : function()
{
//调用基类方法
AjaxImageButtonNamespace.MyCliImageButton.
callBaseMethod(this, 'initialize');
var target = this.get_element();
//添加需要使用的事件处理器
$addHandler(target, 'mouseover', this._onMouseOver);
},
//…………
_onMouseOver : function()
{
//请注意下面的this关键字,它不是MyCliImageButton控件本身,

而是指相应的DOM元素,所以它肯定会引发一个异常
this.get_element().src = this._hoverImageUrl;
},
//……

接下来,让我们看一下真正有趣的Function.createDelegate函数。现在,我们按如下方式来重写前面的代码:

……
initialize : function()
{
//调用基类方法
AjaxImageButtonNamespace.MyCliImageButton.
callBaseMethod(this, 'initialize');
var target = this.get_element();
//请注意下面两句的联合使用
this._mouseOverHandler = Function.createDelegate(this, this._onMouseOver);
//添加需要的事件处理器
$addHandler(target, 'mouseover', this._mouseOverHandler);
},
……………
_onMouseOver : function(e) {
//现在的this关键字就变成了MyCliImageButton控件本身而不再是前面的DOM元素了。
//注意:现在e.target对应于原来的DOM元素
e.target.src = this._hoverImageUrl;
},
…………

总之,Function.createDelegate方法要求使用指定两个参数。第一个参数是在该该事件处理器中的this所指向的对象。第二个参数是将作为事件处理器执行的函数。

Function.createCallback

这个函数也有些特别;因为它也能够“包装”一个现有函数,同时返回一个新的函数。但是,创建这个函数的真正目的是为创建的回调函数提供相应的上下文数据(与Win32中的回调函数意义完全可比)。

Function.createCallback方法也要求使用两个参数。第一个参数是将被执行的函数,第二个参数指定相应的上下文数据。不同于Function.createDelegete,这个函数并没有解析this关键字。

最后,除了上面的几个方法外,我还强力推荐你认真研究一下ASP.NETAJAX文档中提供的Sys.UI.DomEvent,Sys.UI.MouseButton和Sys.UI.Key等内容。

六、总结

在本篇中,我们讨论了开发Ajax控件所涉及的客户端相关技术并给出相应的示例。在下篇中,我们将着重讨论开发基于ASP.NET Ajax框架的控件所涉及的服务器端主要技术,也将一并开发出相应的示例程序

转载于:https://www.cnblogs.com/xujiaci/archive/2007/09/13/891405.html

你可能感兴趣的:(javascript,ui,c#)