复合控件开发系列之一

本文从Community里提取出来,使用效果如图,但是这里仅仅是一个Sorter控件,并不是两个DropDownList组成


点击此处,下载本文源代码,编译后就可以直接在您的项目里使用  ,下面解释仅供参考
1开发复合控件

每个控件都具有从 System.Web.UI.Control 继承的 Controls 属性。这是表示控件的子控件(如果有)的集合属性。如果控件未用 ParseChildrenAttribute 标记,或是标记为 ParseChildrenAttribute(ChildrenAsProperties = false),则当控件在页上以声明方式使用时,ASP.NET 页框架将应用以下分析逻辑:

 如果分析器在控件的标记内遇到嵌套控件,它将创建嵌套控件的实例并将它们添加到控件的 Controls 属性。标记之间的文本添加为 LiteralControl。任何其他嵌套元素都生成分析器错误。
 
 如果自定义控件是从 WebControl 派生的,它将不具有示例中描述的分析逻辑,因为 WebControl 是用 ParseChildrenAttribute(ChildrenAsProperties = true) 标记的,用它可以指示当在页上以声明方式使用控件时,嵌套在服务器控件标记内的 XML 元素是应视为属性还是应视为子控件。无法继承此类。
 
以常见的DropDownList为例,下面是DropDownList控件基本使用模式:

<asp:DropDownList id=”drop” ruant=”server”>

<asp:ListItem>item1<asp:ListItem>

… …

<asp:ListItem>itemn<asp:ListItem>

</asp:DropDownList>

 我们说过web服务器控件都是从WebControl类派生,这样.NET框架解析该控件时,ASP.NET自动将DropDownListChildrenAsProperties 属性设置为true,告诉框架在建立DropDownList时,ListItem应该看成是子控件而不是DropDownList的属性。这样框架就会建立子控件ListItem对象。

一个控件到底是属性还是子控件有什么区别吗?其实属性和自控件主要区别是包容容器的不同。当一个字段被当作属性时,这个属性其实是该类的一个成员,例如DropDownListID,它是DropDownList的属性,因此DropDownList的定义应该类似如下:

class DropDownList:WebControl

{int ID;

… …}

ListItem是子控件使得ListItemDropDownList是逻辑上分离的,也就是说ListItem提供一种接口,这样复合这种接口的控件,都可以作为它的父控件(或者说容器),

正式因为这个原因,所以对于ListBox控件可能有如下使用方式

<asp:ListBox id=”list” ruant=”server”>

<asp:ListItem>item1<asp:ListItem>

… …

<asp:ListItem>itemn<asp:ListItem>

</asp:ListBox>

 读者可以看到,作为子控件的ListItem很容易“融合”在ListBox里,把ListBox当作其容器。

 接下来我们要开发功能和DropDownList控件类似的Sorter控件,所以读者页可能已经感觉到,将要建立的控件使用应该类似如下:

<Community:Sorter id=”sorter” ruant=”server”>

<asp:ListItem>item1<asp:ListItem>

… …

<asp:ListItem>itemn<asp:ListItem>

</Community:Sorter>

 注意:可以通过使用类撰写组合现有控件来创作新控件。复合控件等效于使用 ASP.NET 页语法创作的用户控件。用户控件和复合控件之间的主要差异是用户控件保持为 .ascx 文本文件,而复合控件则经过编译并保持在程序集中。

 在开发Sorter自定义复合控件前,先看一下常规开发的两个主要步骤:

 A)重写从 Control 继承的受保护的 CreateChildControls 方法,以创建子控件的实例并将它们添加到控件集合。前面说过既然ListItem是子控件就需要将它添加到父容器里,例如如果是DropDownList控件其父容器是DropDownList,如果是ListBox控件,则父容器是ListBox。下面显示了如何重新CreateChildControls

public class Composition1 : Control, INamingContainer {

       ... ...

        protected override void CreateChildControls() {

            //加入第一个控件

            this.Controls.Add(new LiteralControl("<h3>" + "值:"));

           //加入第二个控件

            TextBox box = new TextBox();

            box.Text = "0";

            this.Controls.Add(box);

           //加入第三个控件

            this.Controls.Add(new LiteralControl("</h3>"));

}

 

这样在控件树里加入了三个控件:两个LiteralControl和一个TextBox控件。其中第一个LiteralControl的值为“<h3>值”,第二个LiteralControl的值为“</h3>”,TextBox的值为0。这样如果在服务器发送到浏览器后,生成的HTML代码就是“<h3>0</h3>”。

当将子控件加入控件树以后,就可以从索引为零的编号获取控件树里的控件,加入后页面控件树示意图如下:

Page

|

|--LiteralControl (Controls[0])

|

|--TextBox (Controls[1])

|

|--LiteralControl (Controls[2])

  其中Page是整个控件树的树根,每一个子控件根据加入顺序的位置的不同依次加入控件树。当需要获取控件树时,可以使用索引获取,例如读写TextBox的代码如下:

//读取TextBox控件的值,并赋给value变量

string value((TextBox)Controls[1]).Text;

//value值写入TextBox控件里

          ((TextBox)Controls[1]).Text = value.ToString();

这里Controls[1]获取的就是第二个控件,也就是TextBox,然后使用TextBox类进行强制转换,获取Text值后赋值给value。写如的方法和此类似。

 

B)如果复合控件的新实例将在页上重复创建,请实现 System.Web.UI.INamingContainer 接口。这是不具有方法的标记接口。当用控件实现时,ASP.NET 页框架将在此控件下创建新的命名范围。这确保了子控件在控件层次结构树中具有唯一的 ID 因为子控件会提供呈现逻辑,所以不必重写 Render 方法。可以公开合成子控件属性的属性。

ASP.NET提供的服务器控件里都实现了InamingContainer接口,所以您可以在一个页面生多次使用buttonTextBox等这样的控件。但是在下面介绍的Sorter自定义控件里,根据实际需求,一个页面只要一个Sorter自定义控件就可以了,所以Sorter自定义控件没有实现InamingContainer接口。

 由于将要开发的复合控件还需要实现数据回发、事件处理等,下面再介绍这些基础的内容。

 2)开发处理回发数据的自定义(非复合)控件 Sorter

检查回发(输入)数据的控件必须实现 System.Web.UI.IPostBackDataHandler 接口。这将向 ASP.NET 页框架发出信号,指出控件应参与回发数据处理。页框架将输入数据作为键/值对传递给此接口的 LoadPostData 方法。请看下面代码:

using System;

using System.Web;

using System.Web.UI;

using System.Collections.Specialized;

 

namespace CustomControls{

      public class MyTextBox: Control, IPostBackDataHandler {

           

public String Text {

       get {   return (String) ViewState["Text"]; }

                  set { ViewState["Text"] = value;   }                 

            }

           

            public event EventHandler TextChanged;

           

            public virtual bool LoadPostData(string postDataKey,        NameValueCollection values)

{

                  String presentValue = Text;

                  String postedValue = values[postDataKey];

                  if (!presentValue.Equals(postedValue)){

                        Text = postedValue;

                        return true;

                  }

                  return false;

            }

           

            public virtual void RaisePostDataChangedEvent() {

                  OnTextChanged(EventArgs.Empty);    

            }

           

            protected virtual void OnTextChanged(EventArgs e){

                  if (TextChanged != null)

                        TextChanged(this,e);

            }

           

            protected override void Render(HtmlTextWriter output) {

                output.AddAttribute(HtmlTextWriterAttribute.Type, "text");

                output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);

                output.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);

                output.RenderBeginTag(HtmlTextWriterTag.Input);

                output.RenderEndTag();

            }

      }   

}

  若要使控件能够检查客户端发回的窗体数据,控件必须实现 System.Web.UI.IPostBackDataHandler 接口。此接口的协定允许控件确定是否在回发后改变其状态以及引发相应的事件。IPostBackDataHandler 接口包含两个方法。

public interface IPostBackDataHandler{

   public bool LoadPostData(string postDataKey,           NameValueCollection postCollection);

   public void RaisePostDataChangedEvent();

}

  回发后,页框架在发送的内容中搜索与实现 IPostBackDataHandler 的服务器控件的 UniqueID 匹配的值。然后,页框架按顺序在每个实现该接口的控件上调用 LoadPostDataLoadPostData 的两个参数是:标识控件的关键字以及包含发送数据的集合 NameValueCollection。通常实现 LoadPostData,以便在回发后更新控件的状态。以下示例说明用于自定义文本框 (TextBox) 控件的 LoadPostData 实现。

public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {

    string presentValue = Text;

    string postedValue = postCollection[postDataKey];

   

    if (!presentValue.Equals(postedValue)){

          Text = postedValue;

          return true;

    }

    return false;

}

 

如果控件状态因回发而更改,则 LoadPostData 返回 true;否则返回 false。页框架跟踪所有返回 true 的控件并在这些控件上调用 RaisePostDataChangedEvent。更改事件(如果有)就是从该方法引发的。因此,回发数据处理分两个阶段进行,即更新状态和引发更改通知。这可防止在加载回发数据过程中引发更改通知,在该过程中,更改通知可能在各控件加载回发数据之前错误地修改状态。以下代码片段显示了用于自定义文本框 (TextBox) 控件的 RaisePostDataChanged 实现。

public virtual void RaisePostDataChangedEvent() {

            OnTextChanged(EventArgs.Empty);      

      }

 

呈现逻辑必须为控件的名称特性分配 UniqueID。否则,页框架就无法将回发数据传送给控件。如果控件发出多个窗体元素,则至少有一个元素必须具有与控件 UniqueID 对应的名称特性。以下代码片段将 UniqueID 分配给名称特性。

protected override void Render(HtmlTextWriter output)

{

    output.AddAttribute(HtmlTextWriterAttribute.Type, "text");

    output.AddAttribute(HtmlTextWriterAttribute.Value, this.Text);

    output.AddAttribute(HtmlTextWriterAttribute.Name, this.UniqueID);

    output.RenderBeginTag(HtmlTextWriterTag.Input);

    output.RenderEndTag();

}

 

<%@ Register TagPrefix="Custom" Namespace="CustomControls" Assembly = "CustomControls" %>

<html>

<script language="C#" runat=server>

  private StringBuilder message = new StringBuilder("");

 

      private void Text_Changed(Object sender,EventArgs e){

            message.Append("The text in" + sender.ToString()+ " was changed.");

            message.Append("<br>You entered " + Server.HtmlEncode(Box.Text) +".");

      }

           

      protected override void Render(HtmlTextWriter output) {

            base.Render(output);

            output.Write(message.ToString());

      }    

</script>

           

<body>

                 

<form method="POST" action="MyTextBox.aspx" runat=server>

   

Enter your name:   <Custom:MyTextBox Text=" " OnTextChanged = "Text_Changed" id = "Box"  runat=server/>                                

<br><br>

<asp:Button Text = "Submit" runat = server/>                                                                             

</form>                                              

</body>                                          

</html>

这里再次强调一下UniqueID,上面我们使用了一个IDBoxMyTextBox控件,介绍我们在服务器控件使用了两个MyTextBox控件如下

<Custom:MyTextBox Text="this is a mytextbox1 "  id = "Box1"  runat=server/>

<Custom:MyTextBox Text="this is amytextbox2"    id = "Box1"  runat=server/>

 那么当页面回发时如何区分获取这两个控件值呢?

 这里首先说明,在上面MyTextBox自定义控件里并没有实现InamingContainer接口,所以不能够再页面实现两个MyTextBox控件,现在是假设实现了InamingContainer接口,如何分别获取Box1Box2的值。

首先可以更改LoadPostData方法如下如下:

public virtual bool LoadPostData(string postDataKey, NameValueCollection postCollection) {

    string presentValue = Text;

    string postedValue = postCollection[UniqueID];

   

    if (!presentValue.Equals(postedValue)){

          Text = postedValue;

          return true;

    }

    return false;

}

LoadPostData的第二个参数是NameValueCollection类型,可以使用名值对的方式获取数据。

对于第一个MyTextBox来说,其IDBox1,所以此时UniqueID就是Box1,通过postCollection[UniqueID]就可以获取传递的文本值“this is a mytextbox1”。

对于第二个MyTextBox来说,其IDBox2,所以此时UniqueID就是Box2,通过postCollection[UniqueID]就可以获取传递的文本值“this is a mytextbox2”。

那么数据又如何改变呢?

对于第一个MyTextBox,它首先的文本是“this is a mytextbox1”,假设我们在*.aspx里改变其文本为“mytextbox1 values”,那么数据回发到服务器时,会将现在的文本和原来的文本进行毕竟,那么原来的文本是怎么保存的呢?答案是通过ViewState,读者应该看到在MyTextBox里有如下代码

public String Text {

       get {   return (String) ViewState["Text"]; }

                  set { ViewState["Text"] = value;   }                  

            }

它会保存上一次的数据,也就是“this is a mytextbox1”,然后和本次提交的数据“mytextbox1 values”进行毕竟,比较是通过是否相等进行的(换句话说比较文本有没有改变),如下

if (!presentValue.Equals(postedValue)){

          Text = postedValue;

          return true;

    }

如果当前的数据presentValue(它的现在的值是“this is a textbox1”)和postedValue(它的现在的值是“mytextbox1 values”)不等则将postedValue赋值给Text,以便回发到客户端进行更新TextBox的值。然后再返回true

 在返回true非常重要,因此系统再保存了当前值mytextbox1 values以后,并不意味用户的处理已经结束了,可能用户还定义了事件,此时就可以使用RaisePostDataChangedEvent进一步处理事件,如下

public virtual void RaisePostDataChangedEvent() {

            OnTextChanged(EventArgs.Empty);     

      }

我们看到再该实现里调用了OnTextChanged方法,而在OnTextChanged方法里调用了TextChanged事件,所以用户就可以在*.aspx页面里使用如下方式触发自定义事件处理内容:

   private void Text_Changed(Object sender,EventArgs e){}

    

  <Custom:MyTextBox Text=" " OnTextChanged = "Text_Changed" id = "Box"  runat=server/> 

 

 3)自定义复合控件处理事件

下面代码定义了一个复合控件 Composition2,该控件将两个按钮控件(名为 Add Subtract)添加到复合控件里,并为按钮的 Click 事件提供事件处理方法。这些方法增加和减少 Composition2 Value 属性。Composition2 CreateChildControls 方法创建引用这些方法的事件处理程序(委托)的实例,并将委托附加到 Button 实例的 Click 事件。最后得到一个进行自己的事件处理的控件——单击 Add 按钮时,文本框中的值增加;单击 Subtract 按钮时,值减少。

using System;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace CompositionSampleControls {

 

    public class Composition2 : Control, INamingContainer {

 

        public int Value {

           get {

               this.EnsureChildControls();

               return Int32.Parse(((TextBox)Controls[1]).Text);

           }

           set {

               this.EnsureChildControls();

               ((TextBox)Controls[1]).Text = value.ToString();

           }

        }

 

        protected override void CreateChildControls() {

          

// 添加文本控件

           this.Controls.Add(new LiteralControl("<h3>" + "值:"));

          

 

// 添加文本框

           TextBox box = new TextBox();

           box.Text = "0";

           this.Controls.Add(box);

 

           // 添加文本控件

           this.Controls.Add(new LiteralControl("</h3>"));

 

           // 添加按钮

           Button addButton = new Button();

           addButton.Text = "";

           addButton.Click += new EventHandler(this.AddBtn_Click);

           this.Controls.Add(addButton);

 

           // 添加文本控件

           this.Controls.Add(new LiteralControl(" | "));

 

           // 添加按钮

           Button subtractButton = new Button();

           subtractButton.Text = "";

           subtractButton.Click += new EventHandler(this.SubtractBtn_Click);

           this.Controls.Add(subtractButton);

 

        }

 

        private void AddBtn_Click(Object sender, EventArgs e) {

           this.Value++;

        }

 

        private void SubtractBtn_Click(Object sender, EventArgs e) {

           this.Value--;

        }

    }

}

 

 在这种复合控件里,事件的处理由控件内部进行处理,所以如果我们在页面*.aspx使用该控件可能类似的代码如下:

<CompositionSampleControls:Composition2 id="MyControl" runat=server/>

读者看到,这里仅仅引用了复合控件,而对于数据添加/删除处理都是由控件内部来实现。

 

4)复合控件可以定义自定义事件,通过引发该事件来响应其子控件引发的事件。

 上面介绍的复合控件将事件的具体处理都在其内部实现了,所以使用面叫窄,例如如果用户每一次单击Add添加2,每一次单击Subtract减少2,那么我们还需要到自定义控件内部更改代码类似如下:

private void AddBtn_Click(Object sender, EventArgs e) {

           this.Value +=2;

        }

 

        private void SubtractBtn_Click(Object sender, EventArgs e) {

           this.Value-=2;

        }

可以该控件使用人员还必须了解控件内部的运行机制,这非常不方便使用,所以我们应该自定义带有事件的复合控件。

下面的示例显示复合控件 Composition3,该控件引发自定义事件 Change 以响应 TextBox 子控件的 TextChanged 事件。

using System;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

 

namespace CompositionSampleControls {

 

    public class Composition3 : Control, INamingContainer {

 

        public event EventHandler Change;

 

        public int Value {

           get {

               this.EnsureChildControls();

               return Int32.Parse(((TextBox)Controls[1]).Text);

           }

           set {

               this.EnsureChildControls();

               ((TextBox)Controls[1]).Text = value.ToString();

           }

        }

 

        protected void OnChange(EventArgs e) {

              Change(this, e);

        }

 

        protected override void CreateChildControls() {

 

           // 添加文本控件

           this.Controls.Add(new LiteralControl("<h3>" + "值:"));

 

           // 添加文本框

           TextBox box = new TextBox();

           box.Text = "0";

           box.TextChanged += new EventHandler(this.TextBox_Change);

           this.Controls.Add(box);

 

           // 添加文本控件

           this.Controls.Add(new LiteralControl("</h3>"));

 

           // 添加按钮

 

           Button addButton = new Button();

           addButton.Text = "";

           addButton.Click += new EventHandler(this.AddBtn_Click);

           this.Controls.Add(addButton);

 

           // 添加文本控件

 

           this.Controls.Add(new LiteralControl(" | "));

 

           // 添加按钮

 

           Button subtractButton = new Button();

           subtractButton.Text = "";

           subtractButton.Click += new EventHandler(this.SubtractBtn_Click);

           this.Controls.Add(subtractButton);

 

        }

 

        private void TextBox_Change(Object sender, EventArgs e) {

           OnChange(EventArgs.Empty);

        }

 

        private void AddBtn_Click(Object sender, EventArgs e) {

           this.Value++;

           OnChange(EventArgs.Empty);

        }

 

        private void SubtractBtn_Click(Object sender, EventArgs e) {

           this.Value--;

           OnChange(EventArgs.Empty);

        }

    }

}

 

实现方法如下:

 

a)自定义 Change 事件通过标准事件模式定义。(该模式包括受保护的 OnChange 方法的定义,该方法将引发 Change 事件。)

public event EventHandler Change;

protected void OnChange(EventArgs e) {

    Change(this, e);}

 前面也介绍过了,OnChange这种命名方式是为了便于和微软的资料相互一致,我们定义了OnChange方法后,就可以在*.aspx里使用类似如下代码定义事件具体的处理:

 <CompositionSampleControls:Composition3 id="MyControl"  OnChange="Composition3_Change" runat=server/>

 

<script language="C#" runat=server>

 

      private void Composition3_Change(Object sender, EventArgs e) {

 

         if (MyControl.Value < 0) {

            MyControl.Value = 0;

         }

      }

   </script>

 

  读者此时应该明白一直使用类似OnClickOnLoad的意义:通过代理进行链接,所以上面代码也可以写成更为明了的方式

 Composition3.Change += new EventHander(Composition3_Change)

 

b TextBox TextChanged 事件定义了一个事件处理方法。该方法通过调用 OnChange 方法来引发 Change 事件。

private void TextBox_Change(Object sender, EventArgs e) {

    OnChange(EventArgs.Empty);

}

  cCreateChildControls 方法创建一个事件处理程序的实例,该实例引用上述方法并将事件处理程序附加到 TextBox 实例的 TextChanged 事件。

protected override void CreateChildControls() {

  ..

  TextBox box = new TextBox();

  box.TextChanged += new EventHandler(this.TextBox_Change);

  ...

}

 

 Change 事件可以由承载控件的页来处理,如下面的示例所示。在此示例中,页为 Change 事件提供事件处理方法。如果用户输入的数字为负,该事件将 Value 属性设置为零。

 定义好的自定义控件,就可以按照如下代码引用

<%@ Register TagPrefix="CompositionSampleControls" Namespace="CompositionSampleControls" Assembly="CompositionSampleControls" %>

 

<html>

    <script language="C#" runat=server>

 

      private void Composition3_Change(Object sender, EventArgs e) {

 

         if (MyControl.Value < 0) {

            MyControl.Value = 0;

         }

      }

 

   </script>

 

   <body>

 

      <form method="POST" action="Composition3.aspx" runat=server>

 

        <CompositionSampleControls:Composition3 id="MyControl"

 OnChange="Composition3_Change" runat=server/>

 

      </form>

 

   </body>

 

</html>

 

 

5)维护状态

 每个 Web 窗体控件都有一个 State 属性(从 Control 继承),该属性使 Web 窗体控件能够参与 State 管理。State 的类型为 Sytem.Web.UI.StateBag,这是等效于哈希表的数据结构。控件可以将数据作为键/值对保存在 State 中。State 通过 ASP.NET 页框架保持为字符串变量,并以隐藏变量的形式与客户端之间往返。回发时,页框架分析来自隐藏变量的输入字符串,并在页的控件层次结构中填充每个控件的 State 属性。通过使用 State 属性,控件可以还原其状态(将属性和字段设置为它们回发前的值)。

 

 

 5.9.3 Sorter自定义控件

 

下面介绍Sorter自定义控件,首先导入命名空间

 

namespace ASPNET.StarterKit.Communities {

    using System;

    using System.Web;

    using System.Web.UI;

    using System.Web.UI.WebControls;

    using System.Collections;

    using System.ComponentModel;

    using System.Collections.Specialized;

 

Sorter类文件用于生成Sorter控件,
 [ParseChildren(false), ControlBuilder(typeof(SorterControlBuilder)),

    Designer(typeof(ASPNET.StarterKit.Communities.CommunityDesigner))     ]

    public class Sorter : WebControl, IPostBackDataHandler  {

 在这段代码里,ParseChildren设置为false,这是因为Sorter控件从WebControl派生,默认ParseChildren将被设置为trueParseChildren用于指示页面分析器如何分析Sorter直接的XML标记,当将ParseChildren显式设置为falsie,页框架将认为Sorter直接是属性而非子控件,下面是Sorter在页面里的典型应用

<community:Sorter id="Sorter" align="right" runat="Server">

      <ListItem Text="Default Order" value="Default" />

      <ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text="Title" value="Title"/>

      <ListItem Text="Popularity" value="ViewCount"/>

      <ListItem Text="Topic" value="Topic" />

      <ListItem Text="Author" value="Author" />

      <ListItem Text="Rating" value="Rating" />

    </community:Sorter>

 

通过将ParseChildren设置为false告诉系统,<community:Sorter></community:Sorter>之间的<ListItem>

</ListItem>标记应该看生是Sorter属性,这和常规使用的DropDownList默认将ListItem当作子控件并不一样。那么为什么这里设置为false呢?

  笔者认为,将ParseChildren设置为false能够提供更为广阔的自定义控件的灵活性,以常规的DropDownList为例,下面的使用是错误的:

<asp:DropDownList id="Drop" align="right" runat="Server">

      <asp:ListItem Text="Default Order" value="Default" />

      <asp:ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text="Title" value="Title"/>

      this is a dropdown

    </asp:DropDownList>

就是因为DropDownListParseChildren设置为true,它只能够解析内部的子控件,而对于<ListItem Text="Title" value="Title"/>this is a dropdown这样的文本则不能够解析,提示发生错误。

但是如果使用Sorter自定义控件,则如下写法是可以的

<Community:Sorter id="Drop" align="right" runat="Server">

      <asp:ListItem Text="Default Order" value="Default" />

      <asp:ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text="Title" value="Title"/>

      this is a dropdown

    </Community:Sorter>

因此Sorter将其内部数据并不看成子控件,后来还可以看到,通过派生的SorterControlBuilder类(下叙述),使得代码对于类似this is a dropdown这样的文本进行了过滤。

 

ASP.NET 页框架使用称为控件生成器的类来处理页上控件标记中的声明。每个 Web 窗体控件都与默认的控件生成器类 System.Web.UI.ControlBuilder 关联。默认的控件生成器为它在控件标记中遇到的每个嵌套控件将子控件添加到 Controls 集合。另外,它为嵌套控件标记之间的文本添加 Literal 控件。通过将自定义控件生成器类与控件关联,可以重写此默认行为。这通过对控件应用控件生成器属性来实现,Sorter使用的重新代码如下:

[ControlBuilder(typeof(SorterControlBuilder))]

public class Sorter : WebControl, IPostBackDataHandler  {...}

 以上方括号里的元素为公共语言运行库属性,该属性将 SorterControlBuilder类与 Sorter控件关联。通过从 ControlBuilder 派生并重写SorterControlBuilder方法,自定义了Sorter的生成器。

在后面代码我们可以看到,Sorter定义一个自定义控件生成器,它重写从 ControlBuilder 继承的 GetChildControlType 方法。此方法返回要添加的控件类型,并可用来决定将要添加哪些控件。在Sorter中,控件生成器仅在标记名称为“listItem”或者“asp:listItem”时才添加子控件。

 

Designer可以扩展自定义web服务器控件的模式行为。在Sorter的使用类似如下

[Designer(typeof(ASPNET.StarterKit.Communities.CommunityDesigner))   ]

    public class Sorter : WebControl, IPostBackDataHandler  {…}

表示具体的扩展类由ASPNET.StarterKit.Communities.CommunityDesigner类实现,后面会由介绍

   接下来定义一个OrderChanged事件,并定义了升序或者降序时显示的文本属性。

        public event EventHandler OrderChanged

        ListItemCollection _items = new ListItemCollection();

 

        string _ascendingText = "Ascending";

         public string AscendingText

{ get { return _ascendingText; } set { _ascendingText = value; }    }

       

string _descendingText = "Descending";

        public string DescendingText

 {    get { return _descendingText; }       set { _descendingText = value; }   }

     请读者明白一个Sorter控件其实包含了两个DropDownList控件,,但是对于有便那个下拉框它的文本显示是固定的,要么是升序(Ascending)要么是降序(Descending),所以在用户使用该控件值,这个DropDownList的值并不需要用户维护,而是由该Sorter控件内部自己维护,所以这里定义了AscendingTextDescendingText属性。

 

 如果当前是升序用户选择了降序或者当前是降序用户选择了升序时,都意味这用户选择的改变,所以还需要一种机制来记录用户的选择,这个功能由SortOrderValue 实现,但是这里还容易忽略一个问题就是“默认”的排序方式,如果用户在使用该控件时没有在布局代码里明确指出是升序还是降序,那就需要在Sorter里给出一种默认的排序方式,这个功能有FlipSortOrder属性完成。

FlipSortOrder属性主要用于默认排序,如下请看其代码:

  bool _flipSortOrder = false;       

    public bool FlipSortOrder {   get { return _flipSortOrder; }  set { _flipSortOrder = value; }      }

  从这里似乎还可不到它是怎么实现的,在后面介绍SortOrderValue时读者可以看到它用了“?”运算符进行比较来实现的。

 

     SelectedSortOrder属性用于生成排序方式(包括按照哪一例),

  public string SelectedSortOrder

{     get { return SortColumnValue + " " + SortOrderValue.Trim();}     }

 

例如我们给SortColumnValue传递Author,给SortOrderValue传递AscSelectedSortOrder的值相当于 Select * from Community_ContentPage Order By Author Asc

在这里,需要在SortColumnValueSortOrderValue之间加入空格,这就是SortColumnValueSortOrderValue直接由一个“+ " " +”的原因。

 

 

      SortColumnValue属性设置为列的值,它的值就是前面说的Date CreatedView CountRatingTitleDate CommentedDate UpdatedAuthorDefaultTopic的任意一个。

        public string SortColumnValue {

            get {

  if (ViewState["SortColumn"] == null)

                    return _items[0].Value;

                else

                    return (string)ViewState["SortColumn"];

            }

            set { ViewState["SortColumn"] = value; }

        }

  读者可以看到,对于SortColumnValue它的取值为_items[0].Value,这里的items[0]和你布局Sorter的使用有关,例如按照如下的使用方式:

<community:Sorter id="Sorter" align="right" runat="Server">

      <ListItem Text="Default Order" value="Default" />

      <ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text="Title" value="Title"/>

      <ListItem Text="Popularity" value="ViewCount"/>

   </community:Sorter>

那么_items[0].Value就是“Default Order”‘如果使用方式如下

<community:Sorter id="Sorter" align="right" runat="Server">

      <ListItem Text=" Title " value="Default" />

      <ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text=" Default Order " value="Title"/>

      <ListItem Text="Popularity" value="ViewCount"/>

   </community:Sorter>

那么_items[0].Value就是“Title”。当页面回传时使用ViewState记住用户的选择。这里Sorter并没有类似DropDownListSelected属性,所以不能够直接设置被选择的选项。

 

 

SortOrderValue属性设置为排序的值,它的值是asc或者desc之一。       

        public string SortOrderValue {

            get {

                if (ViewState["SortOrder"] == null)

                    return _flipSortOrder ? "asc" : "desc";

                else

                    return (string)ViewState["SortOrder"];

            }

            set { ViewState["SortOrder"] = value; }

        }

  请看这里的“默认”设置,页面在加载时,SortOrder将为空,前面可以看到_ flipSortOrder的值是false,所以return _flipSortOrder ? "asc" : "desc"返回的是降序,这就是我们为什么浏览页面时页面降序显示的原因。

  读者可以将_ flipSortOrder的值是true,那么当你浏览所有区域时,默认将是按照升序进行排序。

,如果SortOrder不为空,SortOrderValue是怎么知道回传改变的呢?在LoadPostData里有如下代码:

public bool LoadPostData(String postDataKey, NameValueCollection values)

              {                   string newSortOrderValue = values[SortOrderHelperID];

                if (newSortColumnValue != SortColumnValue || newSortOrderValue != SortOrderValue)

                     {…          SortOrderValue = newSortOrderValue;           }

正如你所看到的,当用户选择不同的排序时,LoadPostData会将新值赋值给SortOrderValue,这降导致SortOrderValue的值的改变,然后将用新值生成SQL预计。

 

    SortColumnOptionHelperSortOrderOptionHelper都是用于检索选项ListItem的值,它们的区别仅仅是值的不同,SortColumnOptionHelper值是Date CreatedView CountRating等不固定的,而SortOrderOptionHelper则是Asc或者Desc,但是本质上处理是一样的,代码如下:

         private string SortColumnOptionHelper(ListItem item) {

            if (String.Compare(item.Value, SortColumnValue) == 0)

                return String.Format("<option value=\"{0}\" selected=\"selected\">{1}</option>", item.Value, item.Text);

            else

                return String.Format("<option value=\"{0}\">{1}</option>", item.Value, item.Text);

        }           

        

  这里请注意如下事项:

1Format用于格式化数据,在上面代码里Format需要格式化两个变量:item.Valueitem.Text,这样在使用Format格式化时,使用{0}表示第一个参数item.Value,用{0}表示第二个参数item.Text

2)对于转移符号需要使用“\”,例如"<option value=\"{0}\" selected=\"selected\">{1}</option>",我们希望它的输出类似如下:<option value=”myitemvalue”  selected=”selected">myitemText</option>,但是对于引号如果直接写会被系统直接使用不会输出,所以使用“\””就可以输出引号。

 

 

private string SortOrderOptionHelper(string itemText, string itemValue) {

            if (String.Compare(itemValue, SortOrderValue) == 0)

                return String.Format("<option value=\"{0}\" selected=\"selected\">{1}</option>", itemValue, itemText);

            else

                return String.Format("<option value=\"{0}\">{1}</option>", itemValue, itemText);

        }

SortOrderOptionHelperSortColumnOptionHelper功能类似,后面会介绍。

 

SortColumnHelperID属性和SortOrderHelperID属性用于返回SortColumn/SortOrder下拉框的值,这里scSortColumn的缩写,soSortOrder的缩写,如下:

        private string SortColumnHelperID

{     get { return UniqueID + "_sc"; }        }

         

        private string SortOrderHelperID

{    get { return UniqueID + "_so"; }       }

 

 

OnChangeHelper用于获取对客户端脚本函数的引用,调用该函数将使服务器发送回该页。该方法还将一个参数传递到在服务器上执行回发处理的服务器控件。这里的参数this表示返回到原控件。

        private string OnChangeHelper {

            get { return "javascript:" + Page.GetPostBackEventReference(this); }        }

 

在后面代码里可以看到对如下一句代码

   writer.AddAttribute(HtmlTextWriterAttribute.Onchange, OnChangeHelper);

 

这就告诉系统,当选项发生改变触发OnChange时,就调用OnChangerHelper脚本,系统通过在页面生成类似如下脚本

<input type="hidden" name="__EVENTTARGET" value="" />

<input type="hidden" name="__EVENTARGUMENT" value="" />

<script language="javascript">

<!--

     function __doPostBack(eventTarget, eventArgument) {

         var theform;

         if (window.navigator.appName.toLowerCase().indexOf("netscape") > -1) {

              theform = document.forms["Form1"];

         }

         else {

              theform = document.Form1;

         }

         theform.__EVENTTARGET.value = eventTarget.split("$").join(":");

         theform.__EVENTARGUMENT.value = eventArgument;

         theform.submit();

     }

// -->

     而现在要控件当用户选择不同选项时触发回发就要调用该教本,通过OnChangeHelper返回给客户端的HTML代码类似如下:

<select name="sorts_sc" onchange="javascript:__doPostBack('sorts','')">

<select name="sorts_so" onchange="javascript:__doPostBack('sorts','')">

 

  那么如何理解GetPostBackEventReference(this)里面的this参数呢?

This参数指出具体处理返回到该控件本身。例如我在使用该控件的代码类似如下:

<sort:Sorter runat="server" id="mysorts">

                                                               <ListItem Text="Default Order" value="Default" />

      <ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text="Title" value="Title"/>

      <ListItem Text="Popularity" value="ViewCount"/>

      <ListItem Text="Topic" value="Topic" />

                                                              </sort:Sorter>

那么它生成的HTML代码就类似为:

<select name="sorts_sc" onchange="javascript:__doPostBack('mysorts','')">

<select name="sorts_so" onchange="javascript:__doPostBack('mysorts','')">

具体有控件本身处理。

 

    Sorter里用LoadPostData验证用户的选择由没有更改,如果更则返回true,否则返回false

          public bool LoadPostData(String postDataKey, NameValueCollection values) {

           string newSortColumnValue = values[SortColumnHelperID];

           string newSortOrderValue = values[SortOrderHelperID];

          

           if (newSortColumnValue != SortColumnValue || newSortOrderValue != SortOrderValue) {

                SortColumnValue = newSortColumnValue;

                SortOrderValue = newSortOrderValue;

                return true;

           } else

                return false;

        }

 

请看下面示意图5-56

 

我在选择排序时,开始使用Title进行排序,当我再次选择按照Date排序时,此时数据回发到服务器,原来的SortColumnValue的值为Title,而newSortColumnValue的值为Date,这样

  if (newSortColumnValue != SortColumnValue || newSortOrderValue != SortOrderValue) {...}

将返回trueASP.NET页框架将自动跟踪LoadPostDate返回值的控件,对于返回值为true的,则调用RaisePostDataChangedEvent,在Sorter类里就通过在RaisePostDataChangedEvent里调用OnOrderChanged函数实现页面更新排序。 代码如下:

 

    public void RaisePostDataChangedEvent() {

            OnOrderChanged(EventArgs.Empty);

        }

 

OnOrderChanged函数里调用orderChanged事件,如下:

   public virtual void OnOrderChanged(EventArgs e) {

            if (OrderChanged != null)

                OrderChanged(this, e);

        }

 

  这样,我们就可以实现Order改变时的排序。例如在Photo模块里使用OrderChanged代码如下:

if (objSorter != null)

                objSorter.OrderChanged += new EventHandler(ContentList_OrderChanged);

 可以看到,具体的排序由ContentList_OrderChanged完成,后面我们会介绍ContentList_OrderChanged的实现。

 

      将控件注册为需要回发处理的控件。请注意这里选择的是OnPreRender

        protected override void OnPreRender(EventArgs e) {

            Page.RegisterRequiresPostBack(this);

        }

       

    Render判断有没有选现,如果没有选项则项目不显示该控件。请注意所谓的不显示该控件就是不调用基类的base.Render() 

        protected override void Render(HtmlTextWriter writer) {

            if (_items.Count > 0)

                base.Render(writer);      }

 

 

       RenderContents方法将呈现SortColumnSortOrder这两个下拉框控件。但是具体则是由RenderSortColumnRenderSortOrder实现。

        protected override void RenderContents(HtmlTextWriter writer) {

            // 打开tr标记

            writer.RenderBeginTag(HtmlTextWriterTag.Tr);

            RenderSortColumn(writer);

            RenderSortOrder(writer);

            writer.RenderEndTag();

        }

  

上面调用的RenderSortColumnRenderSortOrder方法代码如下:

          private void RenderSortColumn(HtmlTextWriter writer) {

            //获取SectionInfo信息

            SectionInfo objSectionInfo = (SectionInfo)Context.Items["SectionInfo"];

           

            //如果Topic不可用,则从下拉框里移除该选项

            ListItem deleteItem;

            if (!objSectionInfo.EnableTopics) {

                deleteItem = _items.FindByValue( "Topic" );

                if (deleteItem != null)

                    _items.Remove(deleteItem);

            }

 

            //如果Rating不可用,则从下拉框里移除该选项

            if (!objSectionInfo.EnableRatings) {

                deleteItem = _items.FindByValue( "Rating" );

                if (deleteItem != null)

                    _items.Remove(deleteItem);

            }

 

 

            //打开单元格

            writer.RenderBeginTag(HtmlTextWriterTag.Td);

           

            //打开select

           //这里就使用了SortColumnHelperID以便name的唯一性

            writer.AddAttribute(HtmlTextWriterAttribute.Name, SortColumnHelperID);

            writer.AddAttribute(HtmlTextWriterAttribute.Onchange, OnChangeHelper);

            writer.RenderBeginTag(HtmlTextWriterTag.Select);

           

            // 显示每一个Item

           //读者可以看到,对于每一个option,分别输出

            foreach (ListItem item in _items) {

                writer.Write(SortColumnOptionHelper(item));          }

       

            //关闭Select

            writer.RenderEndTag();

       

            //关闭单元个

            writer.RenderEndTag();

        }

 

  

        private void RenderSortOrder(HtmlTextWriter writer) {

          //打开单元格

            writer.RenderBeginTag(HtmlTextWriterTag.Td);

           

            // 打开select

           //同样这里使用了SortOrderHelperID以保证唯一性

            writer.AddAttribute(HtmlTextWriterAttribute.Name, SortOrderHelperID);

            writer.AddAttribute(HtmlTextWriterAttribute.Onchange, OnChangeHelper);

            writer.RenderBeginTag(HtmlTextWriterTag.Select);

       

            //呈现Ascending/Descending

            

            if (_flipSortOrder) {

                writer.WriteLine(SortOrderOptionHelper(_ascendingText, "asc"));

                writer.WriteLine(SortOrderOptionHelper(_descendingText, "desc"));

            } else {

                writer.WriteLine(SortOrderOptionHelper(_descendingText, "desc"));

                writer.WriteLine(SortOrderOptionHelper(_ascendingText, "asc"));

            }

           

            // 关闭 select

            writer.RenderEndTag();

       

            // 关闭单元格

            writer.RenderEndTag();

        }

       

     如果控件 A 在页上的其控件标记中有嵌套控件,页分析器会将那些控件的实例添加到 A Controls 集合。这通过调用 A AddSubParsedObject 方法来实现。每个控件从 Control 继承此方法,默认实现只不过将子控件插入到控件层次结构树中。通过重写 AddSubParsedObject 方法,控件可以重写默认的分析逻辑

  Sorter里当分析特定类型的子控件时,它只会将类型为ListItem的对象添加到集合,而忽略其它对象。

        protected override void AddParsedSubObject(Object obj) {

           if (obj is ListItem) {

              _items.Add((ListItem)obj);

           }

        }

 

 Sort的构造函数调用基类,并生成table标记,因为他是基于表的

        public Sorter() : base(HtmlTextWriterTag.Table) {     }

 

 

   

  Sorter类里的SorterControlBuilderControlBuilder派生,它重写GetChildControlType方法,使得Sorter标记之间只有为ListItem或者是asp:ListItem时才添加子控件。

  还要注意一下,在代码里不管是ListItem还是asp:ListItem,它返回的都是ListItem,这样可以放置asp:但被忽略被解析。

    public class SorterControlBuilder : ControlBuilder {

       public override Type GetChildControlType(String tagName, IDictionary attributes)

{

          if (String.Compare(tagName, "ListItem", true) == 0 || String.Compare(tagName, "asp:ListItem", true) == 0)

 {       return typeof(ListItem);    }

          return null;

       }

    }  

}

 

 

下面我们给出该控件的基本使用模式

<sort:Sorter runat="server" id="sorts">

                                                               <ListItem Text="Default Order" value="Default" />

      <ListItem Text="Date Posted" value="DateCreated"/>

      <ListItem Text="Title" value="Title"/>

      <ListItem Text="Popularity" value="ViewCount"/>

      <ListItem Text="Topic" value="Topic" />

                                                              </sort:Sorter>

 

同时给出了由基本使用模式生成的HTML代码,在HTML里,读者必须明白它不仅仅生成了HTMLselect元素,还包括tabletrtdjavascript脚本等

 

 

       <table id="sorts">

      <tr>

             <td><select name="sorts_sc" onchange="javascript:__doPostBack('sorts','')">

                    <option value="Default" selected="selected">Default Order</option><option value="DateCreated">Date Posted</option><option value="Title">Title</option><option value="ViewCount">Popularity</option><option value="Topic">Topic</option>

             </select></td><td><select name="sorts_so" onchange="javascript:__doPostBack('sorts','')">

                    <option value="desc" selected="selected">Descending</option>

                    <option value="asc">Ascending</option>

 

             </select></td>

      </tr>

</table></TD>

                                        

                                 

<input type="hidden" name="__EVENTTARGET" value="" />

<input type="hidden" name="__EVENTARGUMENT" value="" />

<script language="javascript">

<!--

      function __doPostBack(eventTarget, eventArgument) {

             var theform;

             if (window.navigator.appName.toLowerCase().indexOf("netscape") > -1) {

                    theform = document.forms["Form1"];

             }

             else {

                    theform = document.Form1;

             }

             theform.__EVENTTARGET.value = eventTarget.split("$").join(":");

             theform.__EVENTARGUMENT.value = eventArgument;

             theform.submit();

      }

// -->

</script>



在后面我们将进一步介绍使用客户端脚本的控件

posted @ 2005-11-20 17:34  启明星工作室  阅读(4971)  评论(4编辑  收藏  举报