通过用户控件实现 Web 部件相当容易,但是也有一些弊端:
- 受限的重用:如果不手动复制 .ascx 文件到其他 Web 应用程序的目录下,就不能动态添加这些控件到其他 Web 应用程序的页面中。
- 受限的个性化:用户控件的个性化仅限于常用的属性,如标题、标题 URL 等。你不能获取用户控件的自定义属性,该属性保存在个性化存储里,只有从 Web 部件派生而来的类才能有这类功能。
- 更好的呈现和行为控制:当使用自定义服务器控件时,你对呈现过程有更好的控制,而且可以更加动态的生成用户界面。
因此,有时候需要将高级 Web 部件实现为从 System.Web.UI.WebControls.WebParts.WebPart 派生而来的服务器控件。
创建一个 Web 部件的步骤如下:
- 从 WebPart 派生
- 添加自定义属性,通过特性指定哪些属性可以被用户编辑,以及在个性化存储里哪些需要按照每个用户存储,哪些又被所有用户共享。
- 编写初始化和加载代码,覆盖你需要的任何初始化过程。通常,会覆盖 OnInit()和 CreateChildrenControls()。
- 加载阶段完成后,控件将触发它们的事件,你可以在自定义 Web 部件里为子控件添加事件处理程序。
- 在呈现阶段开始前,要完成最后的任务,如设置控件的属性和基于其所绑定到的数据源构建控件的结构。
- 最后,必须编写代码来呈现 Web 部件。必须覆盖 RenderContents()方法,它被基类在呈现边框、标题栏和带有动词的标题菜单的操作之间调用。
1. 开始之前,创建强类型的 DataSet
在开始深入到开发 Web 部件的细节之前,必须为方便的访问存在数据库中的数据添加几个特别的组件,这些组件将用来完成本文的代码示例。右击项目添加一个 DataSet,命名为 CustomerSet,拖曳 Customer 和 CustomerNotes 表至 DataSet 的设计界面上。如下图:
两个强类型的 DataSet 扩展了 DataSet 类并提供了强类型的表适配器。右击 CustomerNotes 表,添加一个查询用于获取某位客户的注解,该查询有一个参数 CustomerId。
总之,应该总是在开始创建实际的用户界面组件之前创建业务层和数据访问层,当然,业务层和数据访问层是可以跨不同应用程序的,就像这个强类型的 DataSet 一样。
2. 自定义 WebPart 的骨架
创建一个继承自 WebPart 的自定义类。引入命名空间 System.Web.UI.WebControls.WebParts 可以方便的访问 Web Parts Framework。给 Web 部件添加一些属性。对类里的每一个属性过程,都可以指定这个属性是对每个用户个性化还是被所有用户共享,以及这个属性能否被用户访问。
namespace Apress.WebParts.Samples
{
public class CustomerNotesPart : WebPart
{
public CustomerNotesPart() { }
[WebBrowsable(true)]
[Personalizable(PersonalizationScope.User)]
public string Customer { get; set; }
}
}
WebBrowsable 特性指定了这个属性对终端用户可见。Personalizable 特性指定了该属性的个性范围是基于每个用户的。
3. 初始化 Web 部件
你可以选择性的创建子控件,就像创建一个组合 Web 部件时所做的那样。如果不想在 RenderContents()方法内使用预置的控件,你可以自己呈现 Web 部件。但是,使用组合控件可以让事情变得简单很多,因为你不必担心 HTML 细节。
要创建控件,必须覆盖 CreateChildControls()方法,如下所示:
private TextBox NewNoteText;
private Button InsertNewNote;
private GridView CustomerNotesGrid;
protected override void CreateChildControls()
{
NewNoteText = new TextBox();
InsertNewNote = new Button();
InsertNewNote.Text = "Insert...";
InsertNewNote.Click += InsertNewNote_Click;
CustomerNotesGrid = new GridView();
CustomerNotesGrid.HeaderStyle.BackColor = System.Drawing.Color.LightBlue;
CustomerNotesGrid.RowStyle.BackColor = System.Drawing.Color.LightGreen;
CustomerNotesGrid.AlternatingRowStyle.BackColor =
System.Drawing.Color.LightGray;
CustomerNotesGrid.AllowPaging = true;
CustomerNotesGrid.PageSize = 5;
CustomerNotesGrid.PageIndexChanging += CustomerNotesGrid_PageIndexChanging;
Controls.Add(NewNoteText);
Controls.Add(InsertNewNote);
Controls.Add(CustomerNotesGrid);
}
void CustomerNotesGrid_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
// Insert Page Change Logic
// ...
}
void InsertNewNote_Click(object sender, EventArgs e)
{
// Insert new note here
// ...
}
4. 加载数据和处理事件
这个 Web 部件的生命周期里的下一个阶段是加载阶段。此时,可以连接到数据库加载数据到控件里。要完成这些,必须覆盖 OnInit()和 OnLoad()方法,或者捕获 Web 部件的 Init 和 Load 事件。两种做法效果相同。但是当覆盖 OnLoad() 时,需要调用 Base.OnLoad()以便基类的加载功能也被执行。因此,有必要建立一次事件处理程序并捕获自定义控件的事件,以便你不会忘记这点:
// Add some method into the control's constructor
public CustomerNotesPart()
{
this.Init += new EventHandler(CustomerNotesPart_Init);
this.Load += new EventHandler(CustomerNotesPart_Load);
this.PreRender += new EventHandler(CustomerNotesPart_PreRender);
}
现在,先编写从数据库中加载数据的功能,然后在事件里调用它。引入 CustomerSetTableAdapters 命名空间编码会简短一些,这个命名空间是用原来设计的强类型的 DataSet 创建的。
void BindGrid()
{
// 确定服务器控件是否包含子控件。如果不包含,则创建子控件。
EnsureChildControls();
CustomerNotesTableAdapter adapter = new CustomerNotesTableAdapter();
if (Customer.Equals(string.Empty))
{
CustomerNotesGrid.DataSource = adapter.GetData();
}
else
{
CustomerNotesGrid.DataSource = adapter.GetDataByCustomer(Customer);
}
}
void CustomerNotesPart_Load(object sender, EventArgs e)
{
// Initialize web part properties
this.Title = "Customer Notes";
this.TitleIconImageUrl = "NotesImage.jpg";
}
void CustomerNotesPart_Init(object sender, EventArgs e)
{
// Don't try to load data in design mode
if (!this.DesignMode)
{
BindGrid();
}
}
一定要记得调用 EnsureChildControls()方法,因为你并不确定 ASP.NET 什么时候实际调用 CreateChildControls()来创建子控件,ASP.NET 会在需要的时候创建这些控件。因此,你需要确保在这个方法里的控件可用。
现在,数据已经加载到网格了。在生命周期的下一阶段,ASP.NET 运行库处理事件。你的自定义 Web 部件必须捕获之前添加的提交新注解到数据库的 InertNewNote 按钮的事件和改变页面的 CustomerNotesGrid 的事件:
void CustomerNotesGrid_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
CustomerNotesGrid.PageIndex = e.NewPageIndex;
}
void InsertNewNote_Click(object sender, EventArgs e)
{
CustomerNotesTableAdapter adapter = new CustomerNotesTableAdapter();
// if NoteID is an identity column, you needn't insert it.
adapter.Insert(Customer, DateTime.Now, NewNoteText.Text);
// Refresh the grid with the new row as well
BindGrid();
}
最后,必须在代码的另一个地方也加载数据到 GridView。一旦有人修改了 Customer 属性的值,Web 部件就要显示与这个新选择的客户相关的信息:
private string _Customer;
[WebBrowsable(true)]
[Personalizable(PersonalizationScope.User)]
public string Customer
{
get
{
return _Customer;
}
set
{
_Customer = value;
if (!this.DesignMode)
{
EnsureChildControls();
CustomerNotesGrid.PageIndex = 0;
CustomerNotesGrid.SelectedIndex = -1;
BindGrid();
}
}
}
5. 最后的呈现
到此为止,已经初始化了 Web 部件,创建了控件,编写了加载数据的代码,并且捕获了控件事件。那么,该呈现 Web 部件了。在呈现之前,你可以在控件上设置影响最后呈现的最后的属性。比如,如果用户还没有初始化 Customer 属性,你应该禁用 InsertNewNote 按钮。
GridView 现在可以创建必需的 HTML 控件来显示其所绑定的数据了(数据的加载之前的事件中已经完成了)。要完成这些,需要调用 DataBind()方法:
void CustomerNotesPart_PreRender(object sender, EventArgs e)
{
if (Customer.Equals(string.Empty))
{
InsertNewNote.Enabled = false;
}
else
{
InsertNewNote.Enabled = true;
}
CustomerNotesGrid.DataBind();
}
在 RenderContents()里,可以创建 HTML 代码来设置 Web 部件的布局。如果不覆盖这个方法,Web 部件将按照控件在 CreateChildControls()方法里被插入到 Web 部件的 Controls 集合中的顺序来自动呈现。所以,你要覆盖这个方法来创建一个更好的,美观的,基于表的布局:
protected override void RenderContents(System.Web.UI.HtmlTextWriter writer)
{
writer.Write("<table>");
writer.Write("<tr>");
writer.Write("<td>");
NewNoteText.RenderControl(writer);
InsertNewNote.RenderControl(writer);
writer.Write("");
writer.Write("</tr>");
writer.Write("<tr>");
writer.Write("<td>");
CustomerNotesGrid.RenderControl(writer);
writer.Write("");
writer.Write("</tr>");
writer.Write("");
}
6. 更多定制步骤
如前所述,使用 IWebPart 接口,实现的自定义 Web 部件可以覆盖属性,如标题和描述。此外,你还可以通过为其赋值(最好在 Load 里)来为 Web 部件的其他属性指定默认值。甚至可以覆盖来自 Web 部件的默认属性和方法的实现:
public override bool AllowClose
{
get
{
return false; ;
}
set
{
// Don't want this to be set.
}
}
这样,已经创建了一个 Web 部件,其中调用者不能通过外部的属性设置来覆盖其行为。你已经完成了对 Web 部件哪些能做、哪些不能做的定制。
7. 使用 Web 部件
在 Web 部件页面的顶部使用 <%@ Register%> 指令注册 Web 部件:
<%@ Register TagPrefix="apress" Namespace="Apress.WebParts.Samples" %>
接着,在 WebPartZone 控件里使用该 Web 部件:
<asp:WebPartZone runat="server" ID="MainZone" >
<zonetemplate>
<uc1:Customers ID="MyCustomers" runat="server" OnLoad="MyCustomers_Load" />
<apress:CustomerNotesPart ID="MyCustomerNotes" runat="server" />
</zonetemplate>
</asp:WebPartZone>
现在调试前一篇的示例,效果如下: