21.5 Web部件的高级应用

现在已经了解了如何创建一个简单的Web部件页面。为了加深对Web部件的了解,接下来继续阐述Web部件的一些高级应用。

21.5.1 自定义Web部件

虽然通过用户控件实现Web部件是一个很不错的办法,但它也存在着许多不足之处,如在重用与个性化等方面都受到一定的限制。有时候,为了能够以编程的方式最大限度地控制控件的行为和Web部件功能,通常需要通过继承自WebPart类来创建自定义Web部件。

其中,WebPart抽象类用做自定义ASP.NET Web部件控件的基类,为Part基类功能添加了一些附加用户界面属性、创建连接的能力和个性化行为。它的常用方法如表21-4所示。

figure_0775_0581

figure_0776_0582

与此同时,在属性方面,该类还包括一组影响用户界面外观的常用属性,如表21-5所示。其中,AllowClose、AllowConnect、AllowEdit、AllowHide、AllowMinimize和AllowZoneChange属性分别指定是否允许We b应用程序的用户以给定属性名所指示的方式与部件控件交互;而CatalogIconImageUrl、ChromeState、ChromeType、Description、Height、HelpUrl、Hidden、Title、TitleIconImageUrl、TitleUrl和Width属性确定WebPart控件的大小、可见性、外观和支持内容(如标题和说明)等。

figure_0776_0583

figure_0777_0584

和所有ASP.NET自定义控件一样,除了表21-5中的这些属性之外,自定义Web部件还可以暴露自己的属性。但与其他控件不同的是,Web部件有一个内置的接口让用户能够修改其属性,还能够在两次登录之间存储属性值。要让用户能够编辑属性,必须使用属性((atribute)WebBrowsable和Personalizable来标记它们,以告诉PropertyGridEditorPart可以让用户修改和持久化这些属性。通常,应同时设置这两个属性((atribute),因为如果不启用Personalizable而只启用WebBrowsable,用户将能够修改属性((poperty),但其值将不会在两次登录之间持久化。对于暴露的属性((poperty),通常还要启用其他两个属性((atribute):WebDisplayName和WebDescription,它们提供了用户友好的字符串对属性((poperty)进行描述。

下面的HelloWorldWebPart示例演示了如何创建自定义Web部件,如代码清单21-1所示。

代码清单21-1 HelloWorldWebPart.cs


using System;

using System.Web.UI;

using System.Web.UI.WebControls.WebParts;

namespace_21_1.WebParts

{

public class HelloWorldWebPart:WebPart

{

public HelloWorldWebPart()

{

this.Title="Hello World";

this.TitleIconImageUrl=@"Images\p.gif";

}

[WebBrowsable, Personalizable]

[WebDisplayName("ShowTime")]

[WebDescription("Display the current time")]

public bool ShowTime

{

get{return(bool)((VewState["showtime"]??false);}

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

}

public enum Language

{

English, Chinese

};

[WebBrowsable, Personalizable]

[WebDisplayName("GreetingStyle")]

[WebDescription("Style for the greeting")]

public Language GreetingStyle

{

get

{

return(Language)((VewState["greetingstyle"]??

Language.English);

}

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

}

protected override void RenderContents(

HtmlTextWriter writer)

{

switch(GreetingStyle)

{

case Language.English:

writer.Write("Hello!");

break;

case Language.Chinese:

writer.Write("你好!");

break;

}

if(ShowTime)

{

writer.Write("<br/>{0}",

DateTime.Now.ToShortTimeString());

}

base.RenderContents(writer);

}

}

}


在代码清单21-1中,添加了两个自定义属性ShowTime与GreetingStyle(其中,ShowTime用于设置是否显示时间;GreetingStyle用于设置获取问候的语言类型),它们都使用WebBrowsable、Personalizable、WebDisplayName、WebDescription这4个属性进行标记,以实现用户友好的编辑。同时,还在该类里更新了方法RenderContents,以反映这些属性的值。并且,和所有自定义控件属性一样,状态也被存储在ViewState中,使其能够跨回传请求持久化。

创建好HelloWorldWebPart之后,就可以在页面里来使用该部件了,如代码清单21-2所示。

代码清单21-2 HelloWorld.aspx


<%@Page Language="C#"AutoEventWireup="true"

CodeBehind="HelloWorld.aspx.cs"Inherits="_21_1.HelloWorld"

Theme="MyTheme"%>

<%@Register Assembly="21-1"Namespace="_21_1.WebParts"

TagPrefix="cc1"%>

<!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:WebPartManager ID="WebPartManager1"runat="server">

</asp:WebPartManager>

<div>

<asp:WebPartZone ID="WebPartZone1"runat="server"

Width="200px">

<ZoneTemplate>

<cc1:HelloWorldWebPart ID="HelloWorldWebPart1"

runat="server"GreetingStyle="Chinese"/>

<cc1:HelloWorldWebPart ID="HelloWorldWebPart2"

runat="server"GreetingStyle="English"

ShowTime="True"/>

</ZoneTemplate>

</asp:WebPartZone>

</div>

</form>

</body>

</html>


示例运行结果如图21-20所示。

因为在HelloWorldWebPart里添加了两个自定义属性ShowTime与GreetingStyle,并且它们都使用了WebBrowsable、Personalizable、WebDisplay Name与WebDescription属性进行标记,以实现用户友好的编辑。因此,现在就可以在HelloWorld.aspx页面上再添加一个EditorZone区域,在该区域里添加一个PropertyGridEditorPart,就可以编辑HelloWorld WebPart里的两个自定义属性ShowTime与GreetingStyle。如下面的代码所示:


<asp:EditorZone ID="EditorZone1"runat="server"

HeaderText="编辑区域"InstructionText="">

<ZoneTemplate>

<%——<asp:AppearanceEditorPart runat="server"

ID="Appearance1"></asp:AppearanceEditorPart>

<asp:LayoutEditorPart runat="server"

ID="Layout1"></asp:LayoutEditorPart>——%>

<asp:PropertyGridEditorPart runat="server"

ID="PropertyGridEditorPart1">

</asp:PropertyGridEditorPart>

</ZoneTemplate>

<HeaderCloseVerb Text="关闭"/>

</asp:EditorZone>


运行结果如图21-21所示,现在就可以在编辑模式下设置自定义部件的自定义属性。

figure_0780_0585

图 21-20 示例运行结果

figure_0780_0586

图 21-21 示例运行结果

21.5.2 自定义谓词

其实,除了能够在自定义Web部件中添加自定义属性外,还可以添加自定义的谓词。Web部件谓词是对Web部件可执行的操作,用户可以通过从Web部件的标题栏中的下拉谓词菜单中选择相应的菜单项来执行。如在各种编辑模式下显示在菜单中的标准谓词:打开、关闭、最小化和编辑。

如果要添加自定义的谓词,需要重写属性集合Verbs,使其返回一个WebPartVerbCollection对象,其中包含要显示的其他谓词。每个谓词都有一个字符串和图像,且必须用一个委托进行初始化,该委托指向用户从菜单中选择该谓词时将调用的方法。默认情况下,添加的自定义谓词在所有显示模式下都可见。

如下面的示例代码在HelloWorldWebPart中实现了两个自定义谓词:“切换语言”与“时间显示/隐藏”。其中,“切换语言”用于实现部件问候信息在中文/英文之间进行切换;而“时间显示/隐藏”用于实现部件中时间的显示与隐藏。如下面的代码所示:


public override WebPartVerbCollection Verbs

{

get

{

WebPartVerb verbLanguage=new WebPartVerb("verbLanguage",

new WebPartEventHandler(LanguageHandler));

verbLanguage.Text="切换语言";

verbLanguage.Description="切换语言类型";

verbLanguage.ImageUrl=@"~\Images\language.gif";

WebPartVerb verbShowTime=new WebPartVerb("verbShowTime",

new WebPartEventHandler(ShowTimeHandler));

verbShowTime.Text="时间显示/隐藏";

verbShowTime.Description="时间显示与隐藏";

verbShowTime.ImageUrl=@"~\Images\time.gif";

WebPartVerb[]verbs=new WebPartVerb[]{

verbLanguage, verbShowTime};

return new WebPartVerbCollection(verbs);

}

}

public void LanguageHandler(Object s, WebPartEventArgs e)

{

if(GreetingStyle==Language.Chinese)

{

GreetingStyle=Language.English;

}

else

{

GreetingStyle=Language.Chinese;

}

}

public void ShowTimeHandler(Object s, WebPartEventArgs e)

{

if(ShowTime==false)

{

ShowTime=true;

}

else

{

ShowTime=false;

}

}


示例运行结果如图21-22所示。

figure_0781_0587

图 21-22 示例运行结果

21.5.3 自定义编辑器

除了可以自定义Web部件之外,还可以通过继承EditorPart类来自定义编辑器。

其中,EditorPart类提供一组基属性和基方法,由Web部件控件集提供的派生EditorPart控件(如AppearanceEditorPart、BehaviorEditorPart、LayoutEditorPart与PropertyGridEditorPart)和自定义EditorPart控件使用。它允许用户通过修改关联WebPart控件的布局、外观、属性、行为或其他特性对其进行编辑。

需要特别注意的是,如果要创建自定义EditorPart控件,则必须重写如下两个重要的方法:

1)ApplyChanges方法是EditorPart控件的关键抽象方法,必须由继承的控件实现。它旨在将用户输入到EditorPart控件中的值保存到WebPartToEdit属性中引用的WebPart控件的相应属性中。其实现方法是使用WebPartToEdit属性获取对关联控件的引用,然后用EditorPart控件中的当前值更新该控件的属性。它在用户单击编辑用户界面中表示“OK”的按钮或应用谓词时调用。

2)SyncChanges方法与ApplyChanges方法一样,SyncChanges方法也是EditorPart控件的关键抽象方法,必须由继承的控件实现。它旨在从WebPartToEdit属性中引用的WebPart控件检索当前值,并用这些值更新EditorPart控件中的字段,以便用户能够进行编辑。其实现的方法是使用WebPartToEdit属性获取对关联控件的引用,然后用关联WebPart控件的属性值更新EditorPart控件。

每当关联WebPart控件中的值可能发生更改时,就会调用SyncChanges方法。对于每个EditorPart控件,包含该控件的EditorZoneBase区域在调用ApplyChanges方法后立即调用SyncChanges方法,以便EditorPart控件中的值始终与关联WebPart控件中的值保持同步。另一种调用SyncChanges方法的情况是在WebPart控件进入编辑模式时。

值得注意的是,如果ApplyChanges方法返回false,则不调用SyncChanges方法,因为这种情况下已发生错误。

接下来,继续为HelloWorldWebPart部件创建一个自定义编辑器HelloWorldEditWebPart。因为这里的HelloWorldEditWebPart是一个组合控件,所以,必须通过覆盖CreateChildControls方法来添加子控件。这样,需要创建一个ListBox控件列表来显示可用的语言类型,然后供用户进行设置。详细示例如代码清单21-3所示。

代码清单21-3 HelloWorldEditWebPart.cs


using System;

using System.Collections;

using System.ComponentModel;

using System.Drawing;

using System.Security.Permissions;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

namespace_21_1.WebParts

{

[AspNetHostingPermission(SecurityAction.Demand,

Level=AspNetHostingPermissionLevel.Minimal)]

public class HelloWorldEditWebPart:EditorPart

{

public HelloWorldEditWebPart()

{

this.Title="自定Web部件编辑器";

}

ListBox_GreetingStyle;

public override bool ApplyChanges()

{

HelloWorldWebPart part=

((HlloWorldWebPart)WebPartToEdit;

if(_GreetingStyle.SelectedValue=="Chinese")

{

part.GreetingStyle=

HelloWorldWebPart.Language.Chinese;

}

else

{

part.GreetingStyle=

HelloWorldWebPart.Language.English;

}

return true;

}

public override void SyncChanges()

{

HelloWorldWebPart part=

((HlloWorldWebPart)WebPartToEdit;

string currentStyle=part.GreetingStyle.ToString();

foreach(ListItem item in GreetingStyle.Items)

{

if(item.Value==currentStyle)

{

item.Selected=true;

break;

}

}

}

protected override void CreateChildControls()

{

Controls.Clear();

_GreetingStyle=new ListBox();

_GreetingStyle.Items.Add("English");

_GreetingStyle.Items.Add("Chinese");

Controls.Add(_GreetingStyle);

}

protected override void RenderContents(

HtmlTextWriter writer)

{

writer.Write("选择一个语言类型");

writer.WriteBreak();

_GreetingStyle.RenderControl(writer);

writer.WriteBreak();

}

private ListBox GreetingStyle

{

get

{

EnsureChildControls();

return_GreetingStyle;

}

}

}

}


定义好HelloWorldEditWebPart编辑器之后,还需要在HelloWorldWebPart部件中重写WebPart类的CreateEditorParts方法,在该方法的实现中创建关联EditorPart控件的集合(在本示例中,集合中仅有一个名为HelloWorldEditWebPart的EditorPart控件)。

最后,CreateEditorParts方法在HelloWorldWebPart部件进入编辑模式时执行。详细代码如下所示:


public override EditorPartCollection CreateEditorParts()

{

ArrayList editorArray=new ArrayList();

HelloWorldEditWebPart edPart=new HelloWorldEditWebPart();

edPart.ID=this.ID+"_editorPart1";

editorArray.Add(edPart);

EditorPartCollection editorParts=

new EditorPartCollection(editorArray);

return editorParts;

}

public override object WebBrowsableObject

{

get{return this;}

}


示例运行结果如图21-23所示。现在,就可以通过选择列表里的语言类型来设置HelloWorld的WebPart部件中语言的显示。

21.5.4 连接Web部件

Web部件连接是两个服务器控件之间的链接或关联,使二者可以共享数据。一个连接始终涉及两个控件:一个控件是数据提供者,另一个控件是提供者所提供数据的使用者。其中,一个控件既可以是使用者,也可以是提供者,并且无论WebPart控件、自定义控件还是用户控件,任意类型的服务器控件都可以设计为参与连接。

如图21-24所示,在默认情况下,提供者部件可以同时与多个使用者部件建立连接;使用者部件一次只能连接到一个提供者部件。

figure_0784_0588

图 21-23 示例运行结果

figure_0784_0589

图 21-24 连接Web部件

通过使用连接,开发人员可以发现重用代码和组合独立控件功能的新机会。例如,假定开发人员创建一个保存用户地址信息(包括邮政编码)的控件,并且在用户定购商品时始终可以使用此信息填写发货地址单。然后开发人员添加其他依赖于特定邮政编码的控件,如显示用户所在区域的天气信息和新闻标题的控件以及在给定邮政编码内按类别查找公司的控件。开发人员可以将每个新控件都设计为需要输入邮政编码,而不是将它们设计为具有保存邮政编码的相同功能。然后,开发人员只需将保存邮政编码的控件连接到将邮政编码作为输入项的天气、新闻和公司列表控件。每个连接都扩展了原始控件的作用,同时消除了新控件中的冗余代码。

1.定义通信约定

如图21-24所示,由于Web部件连接基于连接的“拉”模型,其中使用者从提供者那里获取数据。若要创建连接,作为数据提供者的控件将定义通信约定(即要交换的那些信息),表明该控件可提供的数据。作为使用者且知道该通信约定的另一个控件将检索这一数据。

通信约定表现为接口形式,如果要共享一条字符串消息,可以定义一个简单的IMessage接口,该接口包含单个字符串属性Message。其中,Message属性用来表示共享的字符串消息。如代码清单21-4所示。

代码清单21-4 IMessage.cs


using System;

using System.Web;

using System.Security.Permissions;

namespace_21_1.WebParts

{

[AspNetHostingPermission(SecurityAction.Demand,

Level=AspNetHostingPermissionLevel.Minimal)]

[AspNetHostingPermission(SecurityAction.InheritanceDemand,

Level=AspNetHostingPermissionLevel.Minimal)]

public interface IMessage

{

string Message{get;set;}

}

}


2.实现提供者Web部件

定义好通信约定IMessage之后,就需要创建一个提供者Web部件MessageProvider。该部件的主要任务是实现通信约定接口,并返回一个实现了该接口的对象的引用,从而让该Web部件充当使用该接口提供数据的提供者。如代码清单21-5所示。

代码清单21-5 MessageProvider.cs


using System;

using System.Web;

using System.Web.Security;

using System.Security.Permissions;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

namespace_21_1.WebParts

{

[AspNetHostingPermission(SecurityAction.Demand,

Level=AspNetHostingPermissionLevel.Minimal)]

[AspNetHostingPermission(SecurityAction.InheritanceDemand,

Level=AspNetHostingPermissionLevel.Minimal)]

public class MessageProvider:WebPart, IMessage

{

string messageText=String.Empty;

TextBox input;

Button send;

public MessageProvider()

{

}

[Personalizable()]

public virtual string Message

{

get{return messageText;}

set{messageText=value;}

}

[ConnectionProvider("Message Provider",

"MessageProvider")]

public IMessage ProvideMessage()

{

return this;

}

protected override void CreateChildControls()

{

Controls.Clear();

input=new TextBox();

this.Controls.Add(input);

send=new Button();

send.Text="发送信息";

send.Click+=new EventHandler(this.submit_Click);

this.Controls.Add(send);

}

private void submit Click(object sender, EventArgs e)

{

if(input.Text!=String.Empty)

{

messageText=Page.Server.HtmlEncode(input.Text);

input.Text=String.Empty;

}

}

}

}


在MessageProvider中,ProvideMessage方法是一个回调方法,它实现了IMessage接口的唯一成员。该方法只是返回该接口的一个实例,并在其元数据中用ConnectionProvider特性进行标记,从而用来将该方法标识为提供者连接点的回调方法的机制。

3.实现使用者Web部件

要让上面的提供者Web部件MessageProvider发挥作用,还需要定义一个或多个充当IMessage接口使用者((cnsumer)的Web部件。这里定义一个使用者Web部件MessageConsumer,它有一个名为GetMessage的方法,该方法用于从提供者控件获取IMessage接口的实例。需要特别注意的是,该方法必须在其元数据中由ConnectionConsumer特性标记为使用者的连接点。如代码清单21-6所示。

代码清单21-6 MessageConsumer.cs


using System;

using System.Web;

using System.Web.Security;

using System.Security.Permissions;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

namespace_21_1.WebParts

{

[AspNetHostingPermission(SecurityAction.Demand,

Level=AspNetHostingPermissionLevel.Minimal)]

[AspNetHostingPermission(SecurityAction.InheritanceDemand,

Level=AspNetHostingPermissionLevel.Minimal)]

public class MessageConsumer:WebPart

{

private IMessage_provider;

Label DisplayContent;

[ConnectionConsumer("Message Consumer",

"MessageConsumer")]

public void GetMessage(IMessage Provider)

{

_provider=Provider;

}

protected override void OnPreRender(EventArgs e)

{

EnsureChildControls();

if(this._provider!=null)

{

DisplayContent.Text="接受的信息为:"

+_provider.Message.Trim();

}

}

protected override void CreateChildControls()

{

Controls.Clear();

DisplayContent=new Label();

this.Controls.Add(DisplayContent);

}

}

}


4.连接Web部件

定义好MessageProvider与MessageConsumer部件之后,还需要定义一个测试页面Message.aspx。在这里,为了能够更好地理解Web部件之间的连接,Message.aspx页面提供了三种连接方式。

第一种方式是声明性的静态连接。其中,在页的标记中声明了一个<StaticConnections>元素,该元素中有一个<asp:WebPartConnections>子元素,其中包含了连接的各种使用者和提供者的详细信息,它们是作为特性指定的。因为这是静态连接,所以两个自定义控件之间的连接在页第一次加载时立即创建。

第二种方式是通过页面中的<asp:connectionszone>元素来进行连接。如果用户在运行时将页显示模式切换到连接显示模式,并且单击了其中一个控件上的连接谓词,则<asp:connectionszone>元素将自动呈现用来创建连接的用户界面。

第三种方式是以编程方式进行连接。在Button1_Click方法中,代码为提供者控件创建了一个ProviderConnectionPoint对象,并通过调用GetProviderConnectionPoints方法检索其连接点详细信息。代码对于使用者控件执行类似的任务,调用GetConsumerConnectionPoints方法。最后,代码通过调用WebPartManager控件上的ConnectWebParts方法来创建新的WebPartConnection对象。详细情况如代码清单21-7所示。

代码清单21-7 Message.aspx


<%@Page Language="C#"AutoEventWireup="true"

CodeBehind="Message.aspx.cs"Inherits="_21_1.Message"

Theme="MyTheme"%>

<%@Register Assembly="21-1"Namespace="_21_1.WebParts"

TagPrefix="cc1"%>

<!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 id="Head1"runat="server">

<title></title>

</head>

<body>

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

<div>

<asp:WebPartManager ID="WebPartManager1"runat="server"

OnDisplayModeChanged="

WebPartManager1_DisplayModeChanged">

<StaticConnections>

<asp:WebPartConnection ID="conn1"

ConsumerConnectionPointID="MessageConsumer"

ConsumerID="mc"

ProviderConnectionPointID="MessageProvider"

ProviderID="mp"/>

</StaticConnections>

</asp:WebPartManager>

<div>

设置显示模式:

<asp:DropDownList ID="dl_SelectMode"runat="server"

Height="20px"AutoPostBack="true"

OnSelectedIndexChanged="

dl_SelectMode_SelectedIndexChanged"Width="169px">

</asp:DropDownList>

</div>

<asp:WebPartZone ID="WebPartZone1"runat="server">

<ZoneTemplate>

<cc1:MessageProvider ID="mp"runat="server"

Title="Message Provider"/>

<cc1:MessageConsumer ID="mc"runat="server"

Title="Message Consumer"/>

</ZoneTemplate>

</asp:WebPartZone>

<asp:ConnectionsZone ID="ConnectionsZone1"

runat="server">

</asp:ConnectionsZone>

<asp:Button ID="Button1"runat="server"

Text="连接WebPart部件"OnClick="Button1_Click"

Visible="false"/>

</div>

</form>

</body>

</html>


页面的后台代码如下所示:


protected void Page_Load(object sender, EventArgs e)

{

if(!IsPostBack)

{

foreach(WebPartDisplayMode mode in

WebPartManager1.SupportedDisplayModes)

{

string modeName=mode.Name;

if(mode.IsEnabled(WebPartManager1))

{

ListItem item=new ListItem(modeName, modeName);

dl_SelectMode.Items.Add(item);

}

}

}

}

protected void dl_SelectMode_SelectedIndexChanged(

object sender, EventArgs e)

{

string selectedMode=dl_SelectMode.SelectedValue;

WebPartDisplayMode mode=

WebPartManager1.SupportedDisplayModes[selectedMode];

if(mode!=null)

{

WebPartManager1.DisplayMode=mode;

}

}

protected void WebPartManager1_DisplayModeChanged(object sender,

WebPartDisplayModeEventArgs e)

{

if(WebPartManager1.DisplayMode==

WebPartManager.ConnectDisplayMode)

Button1.Visible=true;

else

Button1.Visible=false;

}

protected void Button1_Click(object sender, EventArgs e)

{

ProviderConnectionPoint provPoint=

WebPartManager1.GetProviderConnectionPoints

((m)["MessageProvider"];

ConsumerConnectionPoint connPoint=

WebPartManager1.GetConsumerConnectionPoints

((m)["MessageConsumer"];

WebPartConnection conn1=

WebPartManager1.ConnectWebParts(

mc, provPoint, mp, connPoint);

}


运行结果如图21-25所示,在浏览器中加载该页面之后,第一个连接已经存在,因为它是在<StaticConnections>元素中声明的。在提供者的文本控件中输入文本“我是ASP.NET”,单击“发送信息”按钮,这些文本将显示在使用者控件中。

figure_0789_0590

图 21-25 连接Web部件的运行示例

然后,断开两个控件的连接。使用“设置显示模式”下拉列表控件,将该页显示模式更改为连接显示模式。单击其中任意一个控件的谓词菜单,这时可以看到每个菜单都有一个“连接”选项。需要说明的是,该“连接”选项仅在该页处于连接模式时才会出现在谓词菜单中。单击其中一个控件上的连接谓词,ConnectionsZone控件提供的连接用户界面将出现,如图21-26所示。单击“断开连接((Dsconnect)”按钮终止控件间的静态连接。然后,继续使用“显示模式”控件将该页显示模式返回到浏览模式。尝试在提供者中再次输入一些新的文本,可以看到,由于控件已经断开连接,这些文本未能在使用者控件中更新。

接下来,使用与前面相同的方法将该页再次切换到连接显示模式。单击其中一个控件上的连接谓词。单击“创建连接”链接,并使用ConnectionsZone控件提供的用户界面创建控件之间的连接,如图21-27所示。需要说明的是,一旦连接形成,上一次在提供者控件中输入的字符串(因为控件断开连接而未能显示)就会立即出现在使用者中,因为该连接已经重新创建。

figure_0790_0591

图 21-26 断开“连接”

figure_0790_0592

图 21-27 ConnectionsZone控件提供的用户界面创建控件之间的连接

最后,如果不通过单击连接谓词来建立连接,还可以通过单击“连接WebPart控件”按钮来建立连接。

21.5.5 导出导入Web部件

到目前为止,在页面上使用的所有Web部件都是静态的,这样的Web部件存在着一个缺陷,那就是不方便在多个应用程序中进行共享。因此,对于某些复用度较高的Web部件,可以封装成单独的类库组件,然后使用动态上传部件说明文件的办法将部件上传到Web部件页面进行使用。下面仍然以HelloWorld部件为例,来阐述如何动态地导出导入Web部件。

首先来创建一个MyWebPart类库,如图21-28所示,并在该类库里来定义一个自定义We b部件ExportHelloWorld,如代码清单21-8所示。

figure_0790_0593

图 21-28 MyWebPart类库

代码清单21-8 ExportHelloWorld.cs


using System;

using System.Web.UI;

using System.Web.UI.WebControls.WebParts;

using System.Collections;

namespace MyWebPart

{

public class ExportHelloWorld:WebPart

{

public ExportHelloWorld()

{

this.Title="Hello World";

}

[WebBrowsable, Personalizable]

[WebDisplayName("ShowTime")]

[WebDescription("Display the current time")]

public bool ShowTime

{

get{return(bool)((VewState["showtime"]??false);}

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

}

public enum Language

{

English, Chinese

};

[WebBrowsable, Personalizable]

[WebDisplayName("GreetingStyle")]

[WebDescription("Style for the greeting")]

public Language GreetingStyle

{

get

{

return(Language)((VewState["greetingstyle"]??

Language.English);

}

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

}

protected override void RenderContents(

HtmlTextWriter writer)

{

switch(GreetingStyle)

{

case Language.English:

writer.Write("Hello!");

break;

case Language.Chinese:

writer.Write("你好!");

break;

}

if(ShowTime)

{

writer.Write("<br/>{0}",

DateTime.Now.ToShortTimeString());

}

base.RenderContents(writer);

}

}

}


其次,在WebPart控件的说明文件可以导入之前,必须首先基于已存在的WebPart控件((EportHelloWorld)导出说明文件。如果要为一个WebPart控件导出说明文件,需要做如下工作:

1)该WebPart控件具有用Personalizable特性标记的属性。

2)在Web.config文件中,将<webParts>元素的enableExport特性值设置为true。如下面的代码所示:


<configuration>

<system.web>

<webParts enableExport="true"/>

</system.web>

</configuration>


3)将Web部件的ExportMode属性设置为All或NonSensitiveData(它的默认设置为None),显式地允许导入和导出它。当然,这可以以编程或声明方式来完成,但通常由Web部件的创建者来决定它是否可以导出以及是否有敏感信息(因此不应将其导出)更合理。可以在Personalizable属性中使用第二个参数来指出Web部件的特定属性是否是敏感的:True表示敏感;False表示不敏感。默认为不敏感,因此如果Web部件使用的是设置NonSensitiveData,应仔细检查其每个属性,并对可能包含敏感数据的属性进行标记。

下面就来创建一个ExportHelloWorldWebForm.aspx页面将ExportHelloWorld控件的说明文件导出。如代码清单21-9所示。

代码清单21-9 ExportHelloWorldWebForm.aspx


<%@Page Language="C#"AutoEventWireup="true"

CodeBehind="ExportHelloWorldWebForm.aspx.cs"

Inherits="_21_1.ExportHelloWorldWebForm"Theme="MyTheme"%>

<%@Register Assembly="MyWebPart"Namespace="MyWebPart"

TagPrefix="cc1"%>

<!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 id="Head1"runat="server">

<title></title>

</head>

<body>

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

<asp:WebPartManager ID="WebPartManager1"runat="server">

</asp:WebPartManager>

<div>

<div>

设置显示模式:

<asp:DropDownList ID="dl_SelectMode"runat="server"

Height="20px"AutoPostBack="true"

OnSelectedIndexChanged="

dl_SelectMode_SelectedIndexChanged"Width="169px">

</asp:DropDownList>

</div>

<table width="100%"border="0"cellspacing="0"

cellpadding="0">

<tr>

<td valign="top"width="100%"height="100%"

align="left">

<asp:WebPartZone ID="WebPartZone1"

runat="server"Width="200px">

<ZoneTemplate>

<cc1:ExportHelloWorld

ID="ExportHelloWorld1"runat="server"

GreetingStyle="Chinese"

ExportMode="All"/>

</ZoneTemplate>

<ExportVerb Text="导出"/>

</asp:WebPartZone>

</td>

<td>

</td>

</tr>

</table>

</div>

</form>

</body>

</html>


运行ExportHelloWorldWebForm.aspx页面,结果如图21-29所示。

figure_0793_0594

图 21-29 导出说明文件

选择菜单的“导出”选项之后,将下载一个扩展名为.WebPart(HelloWorld.WebPart)的说明文件,如代码清单21-10所示。该说明文件将包含HelloWorld部件所有属性的XML描述。

代码清单21-10 HelloWorld.WebPart


<?xml version="1.0"encoding="utf-8"?>

<webParts>

<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">

<metaData>

<type name="MyWebPart.ExportHelloWorld"/>

<importErrorMessage>

Cannot import this Web Part.

</importErrorMessage>

</metaData>

<data>

<properties>

<property name="AllowClose"type="bool">True</property>

<property name="Width"type="unit"/>

<property name="AllowMinimize"type="bool">True

</property>

<property name="GreetingStyle"

type="MyWebPart.ExportHelloWorld+Language, MyWebPart,

Version=1.0.0.0,Culture=neutral,

PublicKeyToken=null">Chinese</property>

<property name="AllowConnect"type="bool">True</property>

<property name="ChromeType"type="chrometype">Default

</property>

<property name="TitleIconImageUrl"type="string"/>

<property name="Description"type="string"/>

<property name="Hidden"type="bool">False</property>

<property name="TitleUrl"type="string"/>

<property name="AllowEdit"type="bool">True</property>

<property name="AllowZoneChange"type="bool">True

</property>

<property name="Height"type="unit"/>

<property name="HelpUrl"type="string"/>

<property name="Title"type="string">Hello World

</property>

<property name="CatalogIconImageUrl"type="string"/>

<property name="Direction"type="direction">NotSet

</property>

<property name="ChromeState"type="chromestate">Normal

</property>

<property name="ShowTime"type="bool">False</property>

<property name="AllowHide"type="bool">True</property>

<property name="HelpMode"type="helpmode">Navigate

</property>

<property name="ExportMode"type="exportmode">All

</property>

</properties>

</data>

</webPart>

</webParts>


有了这个HelloWorld.WebPart说明文件之后,就可以利用该说明文件来在Web部件页面动态地导入HelloWorld部件了。

要导入Web部件,首先需要在目录区域CatalogZone中添加一个ImportCatalogPart控件。该控件提供了一个用于上传客户端计算机中的.WebPart描述文件的标准接口,并负责创建和初始化导入的Web部件实例,按导入文件指定的方式设置其所有属性值。

ImportCatalogPart控件提供了许多有用的属性。其中,BrowseHelpText属性包含当用户浏览到说明文件时提供给用户的说明文本;ImportedPartLabelText属性包含当导入控件出现在ImportCatalogPart控件中时,用做该控件标签的文本;PartImportErrorLabelText包含当导入控件说明发生错误时显示的文本;如果开发人员没有为ImportCatalogPart控件指定标题,则Title属性重写基属性来为该控件指定一个默认标题;UploadButtonText属性包含用户单击以上传说明文件的按钮的文本;UploadHelpText属性包含上传过程的说明。

接下来,为了演示如何导入Web部件,继续来在ExportHelloWorldWebForm.aspx页面添加一个目录区域CatalogZone,并在该区域里添加一个ImportCatalogPart控件。如下面的代码所示:


<asp:CatalogZone runat="server"ID="_catalogZone">

<ZoneTemplate>

<asp:ImportCatalogPart ID="_importCatalogPart"

runat="server"Title="导入部件"BrowseHelpText=""

UploadButtonText="上传.WebPart文件"UploadHelpText=""

ImportedPartLabelText=""PartImportErrorLabelText=""/>

</ZoneTemplate>

</asp:CatalogZone>


要访问导入界面,可以将页显示模式切换到目录显示模式,然后在目录区域中上传相关的说明文件((HlloWorld.WebPart),如图21-30所示。

在目录区域中上传完相关的说明文件之后,就可以将它们添加到页面相关的区域进行显示,如图21-31所示。

figure_0795_0595

图 21-30 上传说明文件

figure_0795_0596

图 21-31 向页面导入部件

21.5.6 自定义个性化数据提供程序

在ASP.NET Web部件中,为个性化数据提供程序修改连接字符串的能力赋予了一定程度的灵活性,但是在SqlPersonalizationProvider内部还是使用命名空间System.Data.Sql.Client的功能来存取数据。这意味着必须使用SqlServer数据库,如果需要将个性化数据保存到另外一种数据库中,或者可能是另外一种完全不同的数据存储中,那么将不得不更进一步创建自己定制的个性化数据提供程序。

如果创建一个自定义的个性化数据提供程序,则必须首先建立一个从PersonalizationProvider基类派生一个新的类,然后重写所有从PersonalizationProvider基类继承的抽象方法。这些抽象方法专门处理在物理数据存储区中的数据保存、数据加载和数据存储区管理。自定义提供程序必须能够以可将Shared数据和User数据区分开来的方式操作个性化设置信息,并且提供程序必须按页和按应用程序对个性化设置数据进行分段。

其中,PersonalizationProvider的实现与PersonalizationState的实现是紧耦合的,原因是部分个性化设置提供程序方法会返回PersonalizationState派生类的实例。为了简化自定义提供程序的开发,PersonalizationProvider基类提供了个性化设置逻辑和序列化/反序列化逻辑的默认实现,该实现可由WebPartPersonalization类直接使用。因此,一般情况下,如果专门为使用不同数据存储区而创建自定义提供程序,只需下列抽象方法的实现:

1)GetCountOfState方法需要能够为所提供的查询参数对数据库中个性化设置数据的行进行计数。

2)LoadPersonalizationBlobs方法在网页加载时被个性化基础设施调用以检索数据。即在给定路径和用户名的情况下,该方法从数据库中加载两个二进制大对象((BOB):一个BLOB用于共享数据,另一个用于用户数据。如果提供了用户名和路径,就不需要WebPartManager控件访问可提供用户名/路径信息的页信息。

3)ResetPersonalizationBlob方法在给定路径和用户名的情况下,删除数据库中相应的行。如果提供了用户名和路径,就不需要WebPartManager控件访问可提供用户名/路径信息的页信息。

4)SavePersonalizationBlob方法在用户在网页的编辑、目录和设计模式下修改了个性化数据时被调用,以便将数据写回到数据库(通常为特定用户)。即在给定路径和用户名的情况下,该方法把所提供的BLOB保存到数据库。如果提供了用户名和路径,就不需要WebPartManager控件访问可提供用户名/路径信息的页信息。

在上面的所有这些方法中,如果只提供了一个路径,则指示正在操作页面的共享个性化设置数据。如果将用户名和路径都传递给了方法,那么则应当对页面的用户个性化设置数据执行操作。对于LoadPersonalizationBlobs,应始终加载指定路径的共享数据,如果用户名不是null,还可选择加载该路径的用户个性化设置数据。

下面的自定义个性化数据提供程序TextFilePersonalizationProvider演示了如何将个性化数据保存到应用程序App_Data\Personalization_Data目录下的一个本地txt文件中,如代码清单21-11所示。

代码清单21-11 TextFilePersonalizationProvider.cs


using System;

using System.Configuration.Provider;

using System.Security.Permissions;

using System.Web;

using System.Web.UI.WebControls.WebParts;

using System.Collections.Specialized;

using System.Security.Cryptography;

using System.Text;

using System.IO;

namespace_21_1

{

public class TextFilePersonalizationProvider:

PersonalizationProvider

{

public override string ApplicationName

{

get{throw new NotSupportedException();}

set{throw new NotSupportedException();}

}

public override void Initialize(string name,

NameValueCollection config)

{

if(config==null)

{

throw new ArgumentNullException("config");

}

if(String.IsNullOrEmpty(name))

{

name="TextFilePersonalizationProvider";

}

if(string.IsNullOrEmpty(config["description"]))

{

config.Remove("description");

config.Add("description",

"Text file personalization provider");

}

base.Initialize(name, config);

if(config.Count>0)

{

string attr=config.GetKey(0);

if(!String.IsNullOrEmpty(attr))

throw new ProviderException

("Unrecognized attribute:"+attr);

}

FileIOPermission permission=new FileIOPermission(

FileIOPermissionAccess.AllAccess,

HttpContext.Current.Server.MapPath

("~/App_Data/Personalization_Data"));

permission.Demand();

}

protected override void LoadPersonalizationBlobs(

WebPartManager webPartManager, string path,

string userName, ref byte[]sharedDataBlob,

ref byte[]userDataBlob)

{

StreamReader reader1=null;

sharedDataBlob=null;

try

{

reader1=new StreamReader(GetPath(null, path));

sharedDataBlob=

Convert.FromBase64String(reader1.ReadLine());

}

catch(FileNotFoundException)

{

}

finally

{

if(reader1!=null)

{

reader1.Close();

}

}

if(!String.IsNullOrEmpty(userName))

{

StreamReader reader2=null;

userDataBlob=null;

try

{

reader2=new StreamReader(

GetPath(userName, path));

userDataBlob=Convert.FromBase64String(

reader2.ReadLine());

}

catch(FileNotFoundException)

{

}

finally

{

if(reader2!=null)

{

reader2.Close();

}

}

}

}

protected override void ResetPersonalizationBlob(

WebPartManager webPartManager,

string path, string userName)

{

try

{

File.Delete(GetPath(userName, path));

}

catch(FileNotFoundException)

{

}

}

protected override void SavePersonalizationBlob(

WebPartManager webPartManager, string path,

string userName, byte[]dataBlob)

{

StreamWriter writer=null;

try

{

writer=new StreamWriter(

GetPath(userName, path),false);

writer.WriteLine(Convert.ToBase64String(dataBlob));

}

finally

{

if(writer!=null)

{

writer.Close();

}

}

}

public override PersonalizationStateInfoCollection

FindState(PersonalizationScope scope,

PersonalizationStateQuery query, int pageIndex,

int pageSize, out int totalRecords)

{

throw new NotSupportedException();

}

public override int GetCountOfState(

PersonalizationScope scope,

PersonalizationStateQuery query)

{

throw new NotSupportedException();

}

public override int ResetState(

PersonalizationScope scope, string[]paths,

string[]usernames)

{

throw new NotSupportedException();

}

public override int ResetUserState(

string path, DateTime userInactiveSinceDate)

{

throw new NotSupportedException();

}

private string GetPath(string userName, string path)

{

SHA1CryptoServiceProvider sha=

new SHA1CryptoServiceProvider();

UnicodeEncoding encoding=new UnicodeEncoding();

string hash=Convert.ToBase64String(sha.ComputeHash(

encoding.GetBytes(path))).Replace('/','_');

if(String.IsNullOrEmpty(userName))

{

return HttpContext.Current.Server.MapPath(

String.Format("~/App_Data/Personalization_Data

/{0}_Personalization.txt",hash));

}

else

{

return HttpContext.Current.Server.MapPath(

String.Format("~/App_Data/Personalization_Data

/{0}_{1}_Personalization.txt",

userName.Replace('\','_'),hash));

}

}

}

}


实现好个性化数据提供程序之后,还需要在Web.config文件中做如下配置,如下面的代码所示:


<webParts>

<personalization

defaultProvider="AspNetTextFilePersonalizationProvider">

<providers>

<add name="AspNetTextFilePersonalizationProvider"

type="_21_1.TextFilePersonalizationProvider,21-1"/>

</providers>

</personalization>

</webParts>


现在,如果重新运行上面的Web部件页面,所有的个性化数据现在都会被存储到一个本地的txt文件中。图21-32展示了新的个性化数据提供程序TextFilePersonalizationProvider是如何插入到整个Web部件体系结构中去的。

figure_0799_0597

图 21-32 使用TextFilePersonali-zationProvider

21.5.7 配置文件中的webParts元素

在ASP.NET中,webParts元素允许指定Web部件个性化设置提供程序、设置个性化设置授权以及添加自定义类(用于扩展WebPartTransformer类供Web部件连接使用)。它位于配置文件的<configuration>的<system.web>元素中。

webParts元素有一个enableExport特性和两个子元素personalization与transformers。其中,enableExport特性允许将控件数据导出到XML说明文件,默认值为false;personalization元素在Web部件的个性化设置中非常重要,它用于指定Web部件个性化设置提供程序,并设置Web部件个性化设置授权;transformers元素用语定义TransformerInfo对象的集合。

如果要设置默认的自定义个性化数据提供程序(如AspNetTextFilePersonalizationProvider),则可以将personalization元素的defaultProvider特性(其中,defaultProvider特性的默认值为AspNetSqlPersonalizationProvider)设置为要默认的Web部件个性化数据提供程序的名称,然后将其作为注册的个性化提供程序加入到<providers>子节中。如下面的代码所示:


<webParts>

<personalization

defaultProvider="AspNetTextFilePersonalizationProvider">

<providers>

<add name="AspNetTextFilePersonalizationProvider"

type="_21_1.TextFilePersonalizationProvider,21-1"/>

</providers>

</personalization>

</webParts>


如果要设置当前Web应用程序的Web部件个性化设置授权,则就必须设置personalization元素的authorization子元素。其中,authorization元素也提供了两个可选的子元素,如下所示:

1)allow元素向授权规则映射添加一条允许Web部件控件访问的授权规则。它只允许设置这两个值:enterSharedScope与modifyState。其中,enterSharedScope指示用户或角色是否可以进入共享范围;modifyState指示用户或角色是否可以修改当前活动范围的个性化设置数据。

2)deny元素向授权规则映射添加一条拒绝Web部件控件访问的授权规则。它也只允许设置这两个值:enterSharedScope与modifyState。其中,enterSharedScope指示禁止用户或角色进入共享范围;modifyState指示禁止用户或角色修改当前活动范围的个性化设置数据。

如下面的示例说明了如何配置Web部件控件的授权设置。


<authorization>

<deny users="*"verbs="enterSharedScope"/>

<allow users="*"verbs="modifyState"/>

</authorization>