Web部件目录
我们已经见过了如何在 WebPartZones 控件中事先放入Web部件。你还可以用另外一种方法完成这个功能,那就是允许用户在运行时添加新的Web部件。通过使用 CatalogZone 控件和 CatalogParts 类型的部件,比如 :PageCatalogPart 和 DeclarativeCatalogPart 来达到这个目的。(参见图8)
当你用这个方法添加某个CatalogZone和CatalogPart之后,用户就可以在运行时动态添加Web部件了,界面就像如图9中所示的那样。
图9:CatalogZones允许用户动态添加Web部件
首先,你需要理解 PageCatalogPart 的用途。在设计显示模式或者编辑显示模式下,用户能调用Web部件的“关闭”命令。当用户关闭 某个Web部件时,Web部件和相对应的个人化或者自定义配置被保留下来,以便用户可以在以后再次添加该Web部件。因此,PageCatalogPart 控件显示所有已经被关闭的Web部件的列表,用户可以再次加到页面上去。
“关闭”命令与“删除”命令是不同的。“删除”命令在出现在编辑显示模式。当用户删除一个Web部件时,有关这个部件的所有相关信息,包括自定义和个人化数据,全部会被删除掉。
DeclarativeCatalogPart 部件能够以声明的方式添加Web部件。图8中的代码说明了怎样定义这个目录,其中使用了一个自定义名字的部件 WeatherWebPart。采用这种方法,你就能够给用户提供各种各样的Web部件了。
作为本文的补充,同时为了给你提供更多的有关建立Web部件页面的详细信息,我们建议大家去阅读 Stephen Walther 所写的“Introducing the 2.0 Web Parts Framework”一文 。文中提供了更详细的信息,以及使用 EditorZones 和 CatalogZones 建立Web控件页面的完整例子。Stephen 还讲了一些更高级的话题,包括 Verbs、Connections 和 Web 部件的导入导出等内容。
2.0 门户应用开发
除了Web部件自身体系结构之外,在 2.0里面还有一些新的特性使得内部门户网站的开发更加有吸引力。正如文章前面所说,主题和换肤的引入使得风格属性可以方便直接地独立于门户页面,不用修改每个页面,就可以做 统一的风格改变。当然,更令人兴奋的是母版页(Master Pages)的引入。使用母版页,就可以将总体的 WebPartManager 控件和所有 WebPartZones 控件放在一个单独的模版页面,其它页面都可以继承这些基础外观和功能。在示例门户应用中,我们用到的一个有趣的技术就是在 母版页每个 WebPartZone 控件的ZoneTemplate 模版中都添加一个 ContentPlaceholder 控件。这样的话,使用这个 母版页的内容页面可以使用这个内容控件来添加其自己的Web部件,并且映射到相应的 ContentPlaceholder 控件中。
为一个门户网站设计母版页时,你必须考虑的一件事是如果给用户提供自定义特性。正如你已经看到的,根据页面中包含的不同类型的区控件,有几种不同的修改页面的自定义模式 供让用户选择。
其中一种给用户显示自定义选项的方法是将 WebPartManager 控件和一系列按钮(典型的是LinkButton)封装为一个用户控件,然后将这个控件放在 母版页中,就可以为网站中所有页面提供自定义选项。如果你的网站有多于一个的主控页面,用来给不同的页面提供不同的布局,封装为一个用户控件(我们可以叫 它WebPartManagerPanel)也是很有用的。之前图1中给出的那个门户应用的菜单条就给出了一个示例的用户控件,可以显示出当前 WebPartManager 的显示模式和范围,还提供了一些 LinkButton 将页面改变 为 WebPartManager 控件支持的其中一种编辑模式。(这就是我们示例门户应用使用的用户控件)。
在你的 WebPartManagerPanel 控件中可以提供的另外一个有用的特性是可以根据当前用户和当前页面显示或者隐藏相应的显示模式菜单项。通过查看 WebPartManager 的 SupportedDisplayModes 这个collection属性中包含哪些支持的显示模式,就可以显示或隐藏相应的显示模式菜单项。例如,要找出当前页面是否支持CatalogDisplayMode,你应该写如下的代码:
if (WebPartManager1.SupportedDisplayModes.Contains(
WebPartManager.CatalogDisplayMode)) {
//enable catalog display mode LinkButton here...
}
还应该注意,如果当前用户没有相应的权限,调用 ToggleScope 将会失败。所以通过代码判断一下是否显示或隐藏让用户进入共享范围的界面元素是个好主意,查询 WebPartManager 控件的 Personalization 属性的 CanEnterSharedScope 属性可以做到这件事。代码如下:
if (WebPartManager1.Personalization.CanEnterSharedScope) {
// display UI element that allows user to enter shared scope
}
本文附带的示例应用中的 WebPartManagerPanel 用户控件包含了一个完整的实现,它可以根据当前用户和当前页面的能力动态地调整 窗格的显示。
将用户控件作为Web部件
在使用 Windows SharePoint 服务创建Web部件的时候,最令人沮丧的一件事情就是你必须用代码去创建控件的整个界面,设计器一点帮不了忙。因为 许多Web部件都是一系列互相交互的服务器端控件组成的,在创建Web部件时,不能使用 Visual Studio 的设计器是一件很不幸的事情。一个显而易见的解决方法就是允许开发人员创建用户控件,并且可以作为Web部件使用。(一个叫 SmartPart 的第三方工具提供了在 Windows SharePoint服务中可以将用户控件作为Web部件使用)。
2.0 Web部件解决了这个问题,它可以允许任何控件直接作为Web部件使用,不用修改或者包装这些控件。这不仅可以将用户控件 结合到Web部件集合中,而且还可以轻易地将现有 页面中使用的那些自定义控件集成起来。
这种方法内部的工作原理是,如果一个标准控件(不是Web部件)被加入到 WebPartZone 控件中,系统会隐含地调用 WebPartManager.CreateWebPart 方法,这个方法会创建一个 GenericWebPart 类的实例,并且用 添加的那个控件去初始化这个实例。GenericWebPart 从基类 WebPart 中继承,提供了核心Web部件属性实现。当构建 GenericWebPart 控件的时候,它会将初始化的那个控件作为子控件加入。在页面呈现过程中,就像大多数复合控件那样,GenericWebPart自身不会在 响应缓存中输出任何内容,只是作为输出子控件内容的一个代理。最终结果是你可以在页面中的 WebPartZone 控件里面加入任何控件,不用担心它不会运行。例如,下面的页面定义了一个 WebPartZone 控件,里面包括一个用户控件和一个标准日历控件,在创建的时候,这两个控件都会被隐含地包装成为一个 GenericWebPart 类的控件。代码如下:
<%@ Register Src="webparts/CustomerList.ascx"
TagName="CustomerList" TagPrefix="Wingtip" %>
<asp:WebPartManager ID=" WebPartManager1" runat="server" />
<asp:WebPartZone ID="WebPartZone1" runat="server" HeaderText="Zone 1">
<ZoneTemplate>
<Wingtip:CustomerList runat="server" id="CustomerList" />
<asp:Calendar runat="server" id="CustomerCalendar" />
</ZoneTemplate>
</asp:WebPartZone>
和标准的Web部件一样,动态创建被 GenericWebPart 包装的控件也是可以的。如果是用户控件,首先,你必须调用 Page.LoadControl 来动态地载入和创建用户控件实例。其次,还必须显式地给这个控件设置一个唯一的ID。再者,你还必须调用 WebPartManager 对象的 CreateWebPart 方法去创建一个 GenericWebPart 类的实例来作为用户控件实例的包装。最后,将获得的 GenericWebPart 实例的引用作为参数传给 AddWebPart 方法,并且指定要加入的WebPartZone。代码如下:
// create Web Part instance from User Control file
Control uc = this.LoadControl(@"webparts\CompanyNews.ascx");
uc.ID = "wp2";
GenericWebPart wp2 = WebPartManager1.CreateWebPart(uc);
WebPartManager1.AddWebPart(wp2, WebPartZone1, 1);
这种技术的唯一缺点就是你无法控制Web部件的一些专用特性,因为你的控件不是从 WebPart 类继承的,而只有 GenericWebPart 是从 WebPart 类继承的。一旦你运行拥有由 GenericWebPart 包装的控件的页面,你马上就会很明显地发现一个现象,不想大多数Web部件,这些Web部件默认是无标题的,而且也没有相关的图标和描述信息。图10给出了一个 由 GenericeWebPart 控件包装的带有默认标题(无标题)和图标的示例用户控件。
图10 GenericWebPart
其中一种解决方法是,在你的用户控件中,增加一个Init事件处理例程。如果你的控件由 GenericWebPart包装(通过查询 Parent 属性的类型可以判断),你就应该在程序中设置 GenericWebPart 类的一些属性,代码如下:
void Page_Init(object src, EventArgs e) {
GenericWebPart gwp = Parent as GenericWebPart;
if (gwp != null) {
gwp.Title = "My custom user control";
gwp.TitleIconImageUrl = @"~\img\ALLUSR.GIF";
gwp.CatalogIconImageUrl = @"~\img\ALLUSR.GIF";
}
}
当你再次运行此页面时,一旦用户控件被 GenericWebPart 包装,对 GenericeWebPart 父控件的属性的修改会反映在包含你的控件的Web部件上。图11给出了新的设置过属性的用户控件,请注意标题和图标。
图11 标题和图标
另外一个更有吸引力的解决方案是直接在你的用户控件类里实现 IWebPart 接口。既然用户控件从来不直接查询Web部件的属性,因为那些信息是由 GenericWebPart 类处理的,这样做初看起来好像没什么帮助。幸运的是,GenericWebPart 类的设计者意识到这种需求,如果控件实现了 IWebPart 接口, 那么在 GenericeWebPart 类中实现属性就会自动委托所包装的控件。
所以定制某个用户控件的Web部件特性仅仅是实现 IWebPart 接口,并填充接口中定义的七个属性就可以了。图12中的代码给出了用户控件的 后台代码类的一个例子,实现了和之前我们动态修改 GenericWebPart 属性一样的结果。
你可能还会考虑给你的用户控件建立一个另外的基类,这个基类从 UserControl 继承,并且实现了IWebPart接口,然后就可以被你的门户应用中所有的用户控件所继承。我们在这篇文章的示例应用中就是这么做的。采用这种方法,你的用户控件就能在 它们的构造函数中初始化其所需的属性,其它的就由基类去控制了。图13给出了一个实现 IWebPart 接口的用户控件基类以及一个与之相对应的后台类的代码, 该用户控件使用这个基类设置标题和图标属性。
现在你拥有了创建用户控件的这么多的灵活性,你可能会问:当你拥有设计器支持的用户控件,同时还可以定制Web部件特性,那为什么还要创建自己自定义 的控件呢?实际上,有几个原因需要你这样做。其中一个原因是你不能给用户控件添加定义的动作(verbs)。如果需要那样做,你必须直接从 WebPart 继承,然后重写Verbs属性。当然,你也可以考虑在你的控件中实现 IWebEditable 接口。
另外一个原因是用户控件局限于应用程序的目录,除非你将.ascx文件从一个项目复制到另外一个项目的目录下,否则你不可能在多个Web应用程序中共享用户控件。另一方面,自定义Web部件类继承自 WebPart 类,能够被编译到一个可重用的dll里面, 并且部署到全局程序集缓存(GAC)。还有一点,通过自定义Web部件类,你还可以给你的控件写一个自定义的设计器,以改变在 Visual Studio 中默认的外观,而且你还可以在这个Web部件类被放在工具箱的时候,创建一个图标。图14提供了一个特性对比表,让你决定是选择自定义Web部件还是用户控件。
Web部件和个人化特性提供者程序
提供者程序是 2.0的一个新特性,这也是你能在这个版本中看到如此之多内置的功能完整的控件只需要很少的甚至不需要任何代码就能运行 的一个主要原因。提供者程序背后的基本思路是为某个特定的特性定义一套公共的与数据相关的任务,将那些任务聚集到一个抽象类声明中,该抽象类从公共的 ProviderBase 类继承。在本文探讨的个人化特性中,必须明确提供的数据相关任务包括:
为某个特定页面和用户保存Web部件的属性和布局;
为某个特定页面和用户装载Web部件的属性和布局;
保存常规Web部件属性和特定的页面布局( 用于常规定制);
加载常规Web部件属性和特定的页面布局(用于常规定制);
将某个特定页面和用户的Web部件属性和布局重置为其默认值 ;
将某个特定页面的Web部件属性和布局重置为其默认值(用于常规定制);
还有其它的一些属于个人化体系结构的附属特性也需要持久化存储的能力,但是基本上可以归结为以上六种需求。如果我们假设有一个类可以完成这六个动作,而且能够成功 地保存和恢复数据,那么当站点运行的时候,每个页面上的 WebPartManager 控件就能够使用那个类保存和恢复所有的个人化和自定义数据。定义这些方法的抽象类的名字叫 PersonalizationProvider 类, 默认情况下使用的一个具体的派生类是 SqlPersonalizationProvider 类。图15显示了代表我们定义的 六个功能的那三个方法。请注意,不论输入的 userName 参数是否为空,每个方法都能够完成用户个人化或者共享自定义数据的功能。
所有的个人化数据都保存为普通的二进制数据(byte[]),默认的 SqlPersonalizationProvider 类会将这些数据写入数据库中的一个image类型的字段。既然 2.0 知道有一个类可以提供这些方法,它就能够在基础的控件集里面建立比以前更加多的逻辑。在我们的 案例中,每个使用Web部件的页面上的 WebPartManager类负责正确地调用当前的 PersonalizationProvider 类来序列化和恢复每个页面的个人化设置。图16展示了 EditorZone 控件与默认的 SqlPersonalizationProvider 类是如何交互的。
图16 交互
你使用 2.0 越多,对该提供者架构的例子了解就会越多。比如其中有成员提供者、角色管理提供者、站点地图提供者、站点监控提供者等等很多的提供者,所有的提供者程序都定义了一个相似的核心方法集与控件交互。
修改个人化数据存储
和大多数 2.0中的提供者程序一样,默认的个人化提供者程序是面向 SQL Server 后台存储而实现的。如果不修改配置文件,默认的 SqlPersonalizationProvider 采用 SQL Server Express Edition 连接字符串,支持基于本地文件的数据库。这个连接字符串就像下面这样:
data source=.\SQLEXPRESS; Integrated Security=SSPI;
AttachDBFilename=|DataDirectory|aspnetdb.mdf; User Instance=true
使用 SQL Server Express Edition 基于文件的数据库的一个优势就是它可以被动态创建,不需要用户任何附加的设置。这意味着你可以建立一个全新的站点,不用设置数据库就能启用个人化特性,也能够运行!当你最初与网站交互时,系统会在站点的 App_Data 目录中生成一个新的 aspnetdb.mdf 文件,并且用支持所有默认提供者程序所需的表和存储过程来初始化该数据库。
对于不需要扩展规模或者支持很多并发用户的小站点来说,这简直太好了。但是对于企业系统来说,需要将数据存储到某个被全面管理的、专用的数据库服务器上。幸运的是,修改 SqlPersonalizationProvider 使用的数据库是非常简单直接的。SqlPersonalizationProvider 的配置将连接字符串初始化为 LocalSqlServer,这意味着它会在配置文件的<connectionStrings>节中寻找名字为 LocalSqlServer 的配置项,使用相关的连接字符串去打开到数据库的连接。默认情况下,这个字符串就是你在前文所看到的,意味着它会写入一个本地的 SQL Server Express Edition .mdf 文件。要修改它,你必须首先清除掉 LocalSqlServer 连接字符串集合,在你的 Web.config 文件中重新设置一个新的连接字符串值。(或者你也可以修改机器范围的 Machine.config文件,去影响这台机器上的所有站点)以下是一个 Web.config 文件的例子,它将提供者数据库的值修改为指向一个本地的 SQL Server 2000 的实例:
<configuration xmlns="/.NetConfiguration/
V.2.0">
<connectionStrings>
<clear />
<add name="LocalSqlServer" connectionString=
"server=.;integrated security=sspi;database=aspnetdb"/>
</connectionStrings>
...
</configuration>
在修改生效之前,本地 SQL 服务器上必须有一个名为 aspnetdb 的数据库,里面有SqlPersonalizationProvider 所需的表和存储过程。随 2.0 发行了一个名为 aspnet_regsql.exe 的工具,用它可以建立这个数据库。当以默认设置运行该程序时,它将创建一个名为 aspnetdb 的本地数据库,其中有所有提供者程序所必需的表和存储过程,或者你可以选择将这些表和存储过程安装到一个已有的数据库中。所有的表和存储过程的名字都会以“aspnet”开头,所以不太可能会与任何现有的表重复。
和所有 2.0 的提供者程序一样,这种间接的模式提供了一种非常灵活的架构,使得不用修改任何页面或者Web部件,就可以将后端的数据存储完全替换掉。
创建自己的个人化提供者程序
为个人化提供者程序修改连接字符串的能力赋予了你一定程度的灵活性,但是在 SqlPersonalizationProvider 内部还是使用命名空间 System.Data.Sql.Client 的功能来存取数据。这意味着你必须使用 SqlServer 数据库。如果你需要将个人化数据保存到另外一种数据库中,或者可能是另外一种完全不同的数据存储中,你将不得不更进一步,创建你自己定制的个人化数据提供 者。幸运的是,大多数困难的工作已经为你做好了,而且也易于使用。作为个人化数据存储到另外一种数据存储的例子,本文的示例门户网站有一个自定义提供者程 序的完整实现,名为 FileBasedPersonalizationProvider 类,它将所有的个人化和自定义数据保存到应用程序 App_Data 目录下的一个本地二进制文件中。这个二进制文件的名称为每个用户和路径唯一生成,每个路径下还有一个唯一的通用的用户配置文件。
创建一个自定义的个人化数据提供者,你必须首先建立一个从 PersonalizationProvider 基类继承的新的类,然后重写所有从基类继承的抽象方法。图17中给出的类定义演示了如何做到这一点。
为了使你的提供者程序能够运作,其实只有两个重要的方法必须要实现:LoadPersonalizationBlobs 和 SavePersonalizationBlob。这两个方法完成了个人化数据的二进制序列化功能,当加载页面时,个人化架构会调用它们。当Web部件页 面处于编辑、目录或者设计模式时,如果数据被修改了,个人化架构还会调用它们将数据写回。(典型情况下是基于一个特定的用户)。
在下载的示例代码中,SavePersonalizationBlob 的实现代码将 dataBlob 参数写入一个基于传入的用户名称和路径唯一命名的文件。相似地,LoadPersonalizationBlobs 的实现代码会查找这个文件(使用相同的命名方法),并返回一个用户个人化的或者共享的blob数据。如果传入的userName参数为空,这两个方法默认都会保存或者装载共享数据,如果不为空,就会保存或者装载用户个人化数据。图18给出了示例 FileBasedPersonalizationProvider 中这两个方法的实现代码,以及一对用来根据用户名和路径信息生成唯一文件名的 helper 方法。
一旦提供者程序完全实现了,通过个人化配置节中的提供者节,你就可以将这个程序注册为一个提供者程序。要想实际使用它,你必须在 Web.config 文件中定义它为默认的个人化提供者。下面是一个将我们自定义的基于文件的提供者程序作为默认提供者的例子:
<webParts>
<personalization defaultProvider="FileBasedPersonalizationProvider">
<providers>
<add name="FileBasedPersonalizationProvider"
type="Wingtip.Providers.FileBasedPersonalizationProvider" />
</providers>
</personalization>
</webParts>
如果我们重新运行我们的网站,所有的个人化数据现在都会被存储到一个本地的二进制文件。显然这不是最好的解决方法,但是示例代码提供了一些思路,让你 了解如何实现你自己的个人化提供者,不论你想基于什么样的后端存储都可以。图19给出了我们新的提供者是如何插入到整个的Web部件体系结构中去的。
图19 使用 FileBasedPersonalizationProvider
更进一步
到现在为止,你已经看到了如果使用 2.0 和其新的Web部件控件来创建具有丰富特性的支持自定义和个人化的门户应用程序,而 2.0 使得这一工作变得相当简单。也许这个架构的最重要的特性是可插件的能力。提供者架构使得将符合你的站点特性的个人化数据写入后端数据存储变得相对简单,而且也不会被绑定到一个特定的序列化实现和数据存储上。所以,现在就前进,大胆地使用 2.0 Web部件去创建可以自定义的站点吧!
如果你喜欢这篇文章,在 2.0门户应用中创建Web部件还有大量的东西需要学习。记得从MSDN杂志网站下载本文的示例代码。在 2.0 QuickStart tutorials 中也有一些例子可以拿来研究。我们还推荐你去看看 Fredrik Normén的网志,里面有一些有关 2.0 Web 部件的有趣的例子。