脚本控件的作用
ASP.NET AJAX的脚本控件,连接了服务器端和客户端,因为我们(可以)只在服务器端编程,而效果产生在客户端,这就需要我们首先在服务器端编写一个控件类,然后包含一个或几个脚本文件,其中定义了客户端组件,可以让开发人员只在服务端操作控件,而在页面上添加客户端行为
一个典型的脚本控件就是UpdateProgress,我们来看一下它的实现方式
一个UpdateProgress的简单示例
创建一个aspx页面
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="UpdateProgress.aspx.cs" Inherits="Demo13_UpdateProgress" %> <!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></title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager> <asp:UpdatePanel ID="UpdatePanel1" runat="server"> <ContentTemplate> <%= DateTime.Now %><br /> <asp:Button ID="Button1" runat="server" Text="Update" onclick="Button1_Click" /> </ContentTemplate> </asp:UpdatePanel> <asp:UpdateProgress ID="UpdateProgress1" runat="server" DisplayAfter="1000"> <ProgressTemplate> Loading...... </ProgressTemplate> </asp:UpdateProgress> </form> </body> </html>
在Button1的点击事件里,让线程等待三秒钟
protected void Button1_Click(object sender, EventArgs e) { System.Threading.Thread.Sleep(3000); }
开发页面,我们点击按钮Button1,可以看到,等待一秒钟后,出现“Loading…”字样,因为我们设置了UpdateProfress的DisplayAfter为1000,这里代码1000毫秒,而我们让控件的点击事件触发,引发异步回送后,在服务器端停留了三秒钟,所以三秒后,时间更新,同时“Loading…”字样消失
我们打开在网页中右键选择打开源文件,可在页面的form结束之前找到如下代码
Sys.Application.add_init(function() { $create(Sys.UI._UpdateProgress, {"associatedUpdatePanelId":null,"displayAfter":1000,"dynamicLayout":true}, null, null, $get("UpdateProgress1")); });
这段代码,是不是很熟悉呢?没错,如果看过我上一节的文章的,就会很熟悉这种代码格式,它响应了Application的init事件,然后创建一个Sys.UI._UpdateProgress类型的组件,然后设置它绑定的ID,这里是Null,和displayAfter,停留多少毫秒后显示,和UpdateProgress的占位方式,最后,设置的是它要修饰的element
脚本控件的指责
- 在页面上引入客户端组件所需要的脚本文件
- 在页面上生成使用客户端组件的脚本代码
于是出现了IScriptControl接口
- IEnumerable<ScriptReference> GetScriptReferences()方法:描述页面中需要加载在页面中的脚本文件
- IEnumerable<ScriptDescriptor> GetScriptDescriptors()方法:告诉页面需要输出的脚本内容
如果我们要开发一个脚本控件,除了实现以上的两个方法以外,还需要重写Control类的两个方法
- OnPreRender
- OnRender
由于大部分的脚本控件对于以上两个方法实现相同,因此在开发时候,也可以直接继承ScriptControl类,它已经实现了IScriptControl接口
一个脚本控件的示例:StyledTextBox
创建一个名为StyledTextBox.js的文件
/// <reference name="MicrosoftAjax.js"/> Type.registerNamespace('Demo');//注册命名控件 Demo.StyledTextBox = function(element) { Demo.StyledTextBox.initializeBase(this, [element]);//调用父类构造函数 this._highlightCssClass = null; this._nohighlightCssClass = null; this._onfocusHandler = null; this._onblurHandler = null; } Demo.StyledTextBox.prototype = { //highlightCssClass属性 get_highlightCssClass: function() { return this._highlightCssClass; }, set_highlightCssClass: function(value) { if (this._highlightCssClass !== value) { this._highlightCssClass = value; this.raisePropertyChanged('highlightCssClass'); } }, //nohighlightCssClass属性 get_nohighlightCssClass: function() { return this._nohighlightCssClass; }, set_nohighlightCssClass: function(value) { if (this._nohighlightCssClass !== value) { this._nohighlightCssClass = value; this.raisePropertyChanged('nohighlightCssClass'); } }, initialize: function() { Demo.StyledTextBox.callBaseMethod(this, 'initialize'); //创建两个EventHandler this._onfocusHandler = Function.createDelegate(this, this._onFocus); this._onblurHandler = Function.createDelegate(this, this._onBlur); //把这两个EventHandler加到我们要修饰的控件上 $addHandlers(this.get_element(), { 'focus': this._onFocus, 'blur': this._onBlur }, this); this.get_element().className = this._nohighlightCssClass; }, dispose: function() { $clearHandlers(this.get_element()); Demo.StyledTextBox.callBaseMethod(this, 'dispose'); }, //如果获得焦点,把highlightCssClass给到这个element的class上 _onFocus: function(e) { if (this.get_element() && !this.get_element().disabled) { this.get_element().className = this._highlightCssClass; } }, //如果失去焦点,把nohighlightCssClass给到这个element的class上 _onBlur: function(e) { if (this.get_element() && !this.get_element().disabled) { this.get_element().className = this._nohighlightCssClass; } } } Demo.StyledTextBox.registerClass('Demo.StyledTextBox', Sys.UI.Control);
然后创建一个aspx页面
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ScriptControl.aspx.cs" Inherits="Demo13_ScriptControl" %> <!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></title> <style type="text/css"> .NoHighLight { border:solid 1px gray; background-color:#EEEEEE; } .HighLight { border:solid 1px gray; background-color:Ivory; } </style> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager ID="ScriptManager1" runat="server"> <Scripts> <asp:ScriptReference Path="~/Demo13/StyledTextBox.js" /> </Scripts> </asp:ScriptManager> <input type="text" id="textBox" /> <script language="javascript" type="text/javascript"> Sys.Application.add_init(function() { $create( Demo.StyledTextBox, { highlightCssClass: "HighLight", nohighlightCssClass: "NoHighLight" }, null, null, $get("textBox")); }); </script> </form> </body> </html>
代码很简单,和上一讲的如出一辙。。。什么如出一辙,本来就是一回事,文本框获得焦点,样式设置为HighLight,失去焦点,样式设置为NoHighLight。
这里,我们还是在客户端进行编程的,还没有做到在服务端编写在客户端生效的这样一个效果
我们开始做一个服务端控件
创建一个名为StyledTextBox.cs的类
using System; using System.Data; using System.Configuration; using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Xml.Linq; using System.Collections.Generic; namespace Demo { /// <summary> ///StyledTextBox 的摘要说明 /// </summary> //继承自TextBox,实现接口IScriptControl public class StyledTextBox : TextBox, IScriptControl { //两个属性,分别是控件或者焦点和失去焦点时候要设置的样式 public string HighlightCssClass { get; set; } public string NoHighlightCssClass { get; set; } #region IScriptControl 成员 //告诉ScriptManager将如何生成脚本代码 public IEnumerable<ScriptDescriptor> GetScriptDescriptors() { ScriptControlDescriptor descriptor = new ScriptControlDescriptor("Demo.StyledTextBox", this.ClientID);//参数1:创建的组件类型,参数2:返回此控件在客户端生成的ID //添加两个属性 descriptor.AddProperty("highlightCssClass", this.HighlightCssClass); descriptor.AddProperty("nohighlightCssClass", this.NoHighlightCssClass); yield return descriptor; } //告诉页面我们要引入的脚本文件 public IEnumerable<ScriptReference> GetScriptReferences() { ScriptReference reference = new ScriptReference(); reference.Path = this.ResolveClientUrl("~/Demo13/StyledTextBox.js");//ResolveClientUrl方法用于浏览器的指定资源的完全限定 URL yield return reference; } #endregion //以下是开发一个脚本控件,需要重写Control的两个方法 protected override void OnPreRender(EventArgs e) { //如果不在设计期间 if (!this.DesignMode) { //把自身注册给ScriptManager的ScriptControl ScriptManager.GetCurrent(this.Page).RegisterScriptControl<StyledTextBox>(this); } base.OnPreRender(e); } protected override void Render(HtmlTextWriter writer) { if (!this.DesignMode) { //把自身注册给ScriptManager的ScriptDescriptors ScriptManager.GetCurrent(this.Page).RegisterScriptDescriptors(this); } base.Render(writer); } } }
然后修改刚才的aspx页面
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ScriptControl.aspx.cs" Inherits="Demo13_ScriptControl" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <%@ Register Namespace="Demo" TagPrefix="demo" %> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <style type="text/css"> .NoHighLight { border:solid 1px gray; background-color:#EEEEEE; } .HighLight { border:solid 1px gray; background-color:Ivory; } </style> </head> <body> <form id="form1" runat="server"> <input type="text" id="textBox" /> <asp:ScriptManager runat="server" ID="s"></asp:ScriptManager> <demo:StyledTextBox ID="StyledTextBox1" runat="server" HighlightCssClass="HighLight" NoHighlightCssClass="NoHighLight" /> </form> </body> </html>
注意,这里我们不需要javascript代码,也不需要在页面中引入我们刚才的js文件,只需要在页面中注册这个脚本控件,然后在页面中当作服务端控件那样直接使用,设置属性就可以啦
我们看到StyledTextBox继承了TextBox,同时扩展了TextBox,这个概念和客户端组件的Control模型很相似,事实上普通的脚本控件包含的脚本中大多数都是定义了客户端的Control模型的组件
Extender模型
和客户端的Behavior模型概念类似的服务端模型是Extender模型,可以为一个服务器端控件附加多个Extender,Extender模型理论上继承自IExtenderControl即可,实际上开发时候议案继承自ExtenderControl类,免去一些额外的工作
开发ExtenderControl需要覆盖一下两个方法
- IEnumerable<ScriptReference> GetScriptReferences()方法:描述页面中需要加载在页面中的脚本文件
- IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl)方法:需要在目标控件的执行的脚本代码
一个扩展控件的示例:FocusExtender
新建一个类库项目,添加引用System.Web和System.Web.Extensions
创建一个名为FocusBehavior.js的文件
Type.registerNamespace('Demo'); Demo.FocusBehavior = function(element) { Demo.FocusBehavior.initializeBase(this, [element]); this._highlightCssClass = null; this._nohighlightCssClass = null; } Demo.FocusBehavior.prototype = { get_highlightCssClass: function() { return this._highlightCssClass; }, set_highlightCssClass: function(value) { if (this._highlightCssClass !== value) { this._highlightCssClass = value; this.raisePropertyChanged('highlightCssClass'); } }, get_nohighlightCssClass: function() { return this._nohighlightCssClass; }, set_nohighlightCssClass: function(value) { if (this._nohighlightCssClass !== value) { this._nohighlightCssClass = value; this.raisePropertyChanged('nohighlightCssClass'); } }, initialize: function() { Demo.FocusBehavior.callBaseMethod(this, 'initialize'); $addHandlers(this.get_element(), { 'focus': this._onFocus, 'blur': this._onBlur }, this); this.get_element().className = this._nohighlightCssClass; }, dispose: function() { $clearHandlers(this.get_element()); Demo.FocusBehavior.callBaseMethod(this, 'dispose'); }, _onFocus: function(e) { if (this.get_element() && !this.get_element().disabled) { this.get_element().className = this._highlightCssClass; } }, _onBlur: function(e) { if (this.get_element() && !this.get_element().disabled) { this.get_element().className = this._nohighlightCssClass; } } } Demo.FocusBehavior.registerClass('Demo.FocusBehavior', Sys.UI.Behavior);
代码和之前的一样,我就没添加注释,看过我之前的文章,看这段代码不是问题
然后创建一个名为FocusExtender.cs的类文件
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.UI; //描述我们需要引用的资源 [assembly: WebResource("FocusExtender.FocusBehavior.js", "application/x-javascript")] namespace FocusExtender { [TargetControlType(typeof(Control))] public class FocusExtender : ExtenderControl { public string HighlightCssClass { get; set; } public string NoHighlightCssClass { get; set; } protected override IEnumerable<ScriptDescriptor> GetScriptDescriptors(Control targetControl) { ScriptBehaviorDescriptor descriptor = new ScriptBehaviorDescriptor("Demo.FocusBehavior", targetControl.ClientID); descriptor.AddProperty("highlightCssClass", this.HighlightCssClass); descriptor.AddProperty("nohighlightCssClass", this.NoHighlightCssClass); yield return descriptor; } protected override IEnumerable<ScriptReference> GetScriptReferences() { ScriptReference reference = new ScriptReference(); reference.Assembly = "FocusExtender"; reference.Name = "FocusExtender.FocusBehavior.js"; yield return reference; } } }
在这里描述应用资源的时候应该注意,这里不是文件名,也不是这个类库的名称加点然后加文件名
我们点击项目右键属性,打开属性页面
我们的资源名称,是默认命名控件.文件名称
这里的代码,与前面的示例唯一不同的是,多了一个targetControl,在类名前加一个标识,表示我们这个控件作用到那种类型的控件上,我们这里设置为“Control”,表示所有控件
还应该注意一点,我们应该在项目生成操作的时候,把js文件作为嵌入的资源,点击js文件属性,然后在属性对话框里做相应修改
然后我们就可以在我们的网站里使用它啦
在网站中点击右键添加引用,选择我们创建的FocusExtender项目,会在bin目录下出现一个FocusExtender.dll,注意要先生成一下
创建aspx页面
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="FocusExtender.aspx.cs" Inherits="Demo13_FocusExtender" %> <%@ Register Assembly="FocusExtender" Namespace="FocusExtender" TagPrefix="demo" %> <!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></title> <style type="text/css"> .NoHighLight { border:solid 1px gray; background-color:#EEEEEE; } .HighLight { border:solid 1px gray; background-color:Ivory; } </style> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager> <asp:TextBox runat="server" ID="textBox" /> <demo:FocusExtender ID="FocusExtender1" runat="server" TargetControlID="textBox" HighlightCssClass="HighLight" NoHighlightCssClass="NoHighLight" /> <asp:Panel Width="200" Height="200" ID="panel" runat="server">Hello World!</asp:Panel> <demo:FocusExtender ID="FocusExtender2" runat="server" TargetControlID="panel" HighlightCssClass="HighLight" NoHighlightCssClass="NoHighLight" /> </form> </body> </html>
这样,我们把我们创建的控件“附加”到了一个文本框和一个Panel上,在同时我们提供了三个属性,作用的控件,和两个样式属性,运行页面,得到与前面我们的脚本控件相同的效果
脚本控件和Extender模型
- IScriptControl:对应Sys.Component__ScriptComponentDescriptor
- ScriptComtrol:对应Sys.UI.Control__ScriptControlDescript
- ExtenderControl:对应Sys.UI.Behavior__ScriptBehaviroDescriptor
在PostBack中保持状态
- 与普通服务器控件不同,ScriptControl的精髓在客户端,在普通的服务端控件中使用ViewSate并,它不能保持客户端状态
- 组件状态可能在客户端被改变
- 需要在PostBack前后保持客户端状态
在异步刷新中,由于不刷新整个页面,因此可以保存在页面变量中,但是完整的PostBack需要将状态从客户端提交到服务器端,然后再写回给客户端,客户端向服务器端提交信息的方法有以下三种
- Query String(改变URL)
- Cookie(作用域太大)
- Input+Post
那么,如果我们要保存页面的某个状态,就分两种情况啦
一种是异步刷新,因为异步刷新的时候,页面并没有销毁,所以,我们可以把保存这种状态的键值放在window对象或者一个HiddenField中,但是如果是传统的更新,页面是会被销毁的,则只能保存在HiddenField中啦
在UpdatePanel中使用内联脚本
- UpdatePanel在更新时使用的是设置innerHTML的做法
- 设置innerHTML并不会执行其中的内联脚本
- 需要把内联脚本提出来,然后eval
为了让UpdatePanle可以使用内联脚本,就需要使用一个内联脚本控件
内联脚本
- 要子啊异步更新后执行脚本,唯一的方法就是调用ScriptManager的脚本注册方法
- 开发一个控件,在普通加载时简单输出内联脚本,在异步更新时调用脚本注册方法
一个内联脚本的示例
创建一个aspx页面
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="InlineScripts.aspx.cs" Inherits="Demo13_InlineScripts" %> <!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></title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager runat="server" ID="sm" /> <asp:UpdatePanel runat="server" ID="update"> <ContentTemplate> <%= DateTime.Now %> <asp:Button runat="server" ID="btnRefresh" Text="Refresh" /> <script language="javascript" type="text/javascript"> alert("Xiaoyaojian"); </script> </ContentTemplate> </asp:UpdatePanel> </form> </body> </html>
打开页面,刷新页面,都会弹出提示框,而在我们点击Refresh后,脚本却并没有被执行,这不是我们想要的效果,但是这里的脚本在异步回送的时候确实是被加载啦,那要怎么做呢 。。。。。
我们创建一个名为InlineScript的类库项目,添加一个类
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.UI; using System.IO; namespace InlineScript { public class InlineScript : Control { //重写Render方法,每次UpdatePanle更新,这个方法都会被调用 protected override void Render(HtmlTextWriter writer) { ScriptManager sm = ScriptManager.GetCurrent(this.Page); if (sm.IsInAsyncPostBack)//如果页面是异步更新的情况下 { StringBuilder sb = new StringBuilder(); base.Render(new HtmlTextWriter(new StringWriter(sb))); //得到的UpdatePanel中的script标签的所有内容 string script = sb.ToString(); ScriptManager.RegisterStartupScript(this, typeof(InlineScript), this.UniqueID, script, false);//把这段脚本注册到页面上 } else { base.Render(writer); } } } }
生成项目,然后和上面一样,在网站项目中添加对这个项目的引用,然后修改上面的页面
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="InlineScripts.aspx.cs" Inherits="Demo13_InlineScripts" %> <%@ Register Assembly="InlineScript" Namespace="InlineScript" TagPrefix="demo" %><%--注册这个控件--%> <!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></title> </head> <body> <form id="form1" runat="server"> <asp:ScriptManager runat="server" ID="sm" /> <asp:UpdatePanel runat="server" ID="update"> <ContentTemplate> <%= DateTime.Now %> <asp:Button runat="server" ID="btnRefresh" Text="Refresh" /> <%--使用注册的控件--%> <demo:InlineScript runat="server"> <script language="javascript" type="text/javascript"> alert("Xiaoyaojian"); </script> </demo:InlineScript> </ContentTemplate> </asp:UpdatePanel> </form> </body> </html>
打开页面,刷新,点击按钮,都会弹出提示框,对嘛 这才是我们要的效果