The standard parts included in the Web Part Framework are fine for building simple Web Part applications, but you’ll quickly run into some of their limitations when your needs become more complex.
For example, the Web Part Framework includes the DeclarativeCatalogPart
control, which you can use to add new Web Parts to a page. However, this control is quite limited. Because it does not support paging or sorting, you cannot use the DeclarativeCatalogPart
control to display a catalog that contains more than a small number of Web Parts. It also does not support drag-and-drop functionality.
The PropertyGridEditorPart
included with the framework enables you to edit custom Web Part properties easily. However, when you use this control, you cannot customize the appearance of the form displayed by the editor. In particular, the form generated by this control does not support validation.
Don’t worry. The Web Part Framework can be extended easily. In this chapter, you learn how to create custom Web Part Zones, custom Catalog Zones, custom Editor Zones, and custom Display Modes. By the end of this chapter, you will be able to extend the Web Part Framework so it does just about anything you want.
The WebPartZone control is responsible for rendering all the Web Parts contained in a zone. Therefore, if you want to modify the appearance of a Web Part Zone, or modify the appearance of the Web Parts that appear in a Web Part Zone, then you need to create a custom Web Part Zone.
In this section, we create three custom Web Part Zones: a Photo Web Part Zone, a Multi-Column Web Part Zone, and a Menu Web Part Zone.
Three types of parts included in the Web Part Framework are related to Web Part Zones: the WebPartZone
, WebPartChrome
, and WebPart
controls.
The WebPartZone
control is derived from the WebPartZoneBase
class. This class includes one important MustOverride
(abstract) method named GetInitialWebParts()
. The GetInitialWebParts()
method returns the Web Parts that the Web Part Zone initially displays before a user personalizes the zone.
For example, the standard WebPartZone
control overrides the GetInitialWebParts()
method and returns the collection of Web Parts contained in its ZoneTemplate
. In other words, the WebPartZone
control overrides the WebPartZoneBase
control to support declaratively listing a set of initial Web Parts in a ZoneTemplate
.
You can override the GetInitialWebParts()
method and return any set of Web Parts that you please. For example, you could get the initial list of Web Parts from a database table, a Web service, or generate a random collection of Web Parts.
Before a Web Part Zone renders its Web Parts, the Web Part Zone creates an instance of the WebPartChrome
class. The Web Part Chrome contains the standard elements which appear around each Web Part in a Web Part Zone. The default chrome includes the Web Part title bar and menu.
If you want to modify the Web Part Chrome, then you need to derive a new class from the base WebPartChrome
class and associate your new chrome with a Web Part Zone. You can associate a custom WebPartChrome
class with a Web Part Zone by overriding the WebPartZoneBase
class’s CreateWebPartChrome()
method.
After the Web Part Zone gets an instance of the WebPartChrome
class from its CreateWebPartChrome()
method, the Web Part Zone uses the class to render each of its Web Parts. The WebPartChrome
class includes a method named RenderWebPart()
, which renders a particular Web Part that uses the chrome.
When all is said and done, you must interact with three classes to modify the appearance of a Web Part Zone. The WebPartZone
control uses the WebPartChrome
class to render individual WebPart controls.
In this section, we create a custom Web Part Zone, which automatically displays a list of photos from a folder. Each photo is converted automatically into a Web Part so that you can re-arrange the photos on a page (see Figure 30.1).
To create a Photo Web Part Zone, you need to override the GetInitialWebParts()
method. By default, this method retrieves a list of initial Web Parts from the ZoneTemplate contained in a Web Part Zone. In the modified version of this method, the list of Web Parts is obtained from a Photo folder.
The PhotoWebPartZone
control is contained in Listing 30.1.
Example 30.1. PhotoWebPartZone.vb
Imports System Imports System.IO Imports System.Collections.Generic Imports System.Web.UI.WebControls Imports System.Web.UI.WebControls.WebParts Namespace myControls Public Class PhotoWebPartZone Inherits WebPartZoneBase Dim _photoFolderUrl As String = "~/Photos" ''' <summary> ''' Represents the URL for the folder that ''' contains the photos. ''' </summary> Public Property PhotoFolderUrl() As String Get Return _photoFolderUrl End Get Set(ByVal Value As String) _photoFolderUrl = Value End Set End Property ''' <summary> ''' Get the initial Web Parts from the Photo Folder ''' </summary> Protected Overrides Function GetInitialWebParts() As WebPartCollection ' Don't do anything when displayed in Designer If (Me.DesignMode) Then Return New WebPartCollection() End If ' Get the WebPartManager control Dim wpm As WebPartManager = WebPartManager.GetCurrentWebPartManager(Page) ' Create a WebPart collection Dim photos As New List(Of WebPart)() ' Get the list of photos Dim photoDir As DirectoryInfo = New DirectoryInfo(Page.MapPath(_photoFolderUrl)) Dim files() As FileInfo = photoDir.GetFiles() Dim file As FileInfo For Each file In files Dim photo As Image = New Image() photo.ID = Path.GetFileNameWithoutExtension(file.Name) photo.ImageUrl = Path.Combine(_photoFolderUrl, file.Name) photo.Width = Unit.Pixel(200) photo.Attributes.Add("Title", file.Name) photo.AlternateText = file.Name + " Photo" photos.Add(wpm.CreateWebPart(photo)) Next ' Return WebPartCollection Return New WebPartCollection(photos) End Function End Class End Namespace
The bulk of the code in Listing 30.1 is contained in the GetInitialWebParts()
method. This method grabs a list of all the files located in the Photo folder. Next, the method creates a new ASP.NET Image
control for each of the photos. Each Image
control is wrapped in a Generic Web Part with the help of the WebPartManager
control’s CreateWebPart()
method. Finally, the collection of Web Parts is returned.
The page in Listing 30.2 includes the PhotoWebPartZone control.
Example 30.2. PhotoGallery.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Sub Page_Load() WebPartManager1.DisplayMode = WebPartManager.DesignDisplayMode End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Photo Gallery</title> </head> <body> <form id="form1" runat="server"> <div> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <custom:PhotoWebPartZone id="WebPartZone1" PhotoFolderUrl="~/Photos" LayoutOrientation="Horizontal" runat="server" /> <asp:WebPartZone id="WebPartZone2" LayoutOrientation="Horizontal" runat="server" /> </div> </form> </body> </html>
The page in Listing 30.2 contains two Web Part Zones. The first Web Part Zone is the custom Photo Web Part Zone. The second zone is a standard Web Part Zone.
When you first open the page, all the photos contained in the Photos folder are displayed in the Photo Web Part Zone. Because the page is set to be in Design Display Mode by default in the Page_Load()
method, you can rearrange the photos immediately after opening the page.
The GetInitialWebParts()
method returns the list of Web Parts that a zone displays before the page has been personalized. You can re-arrange the photos in any way that you please and the Web Part Framework automatically saves your personalization data.
Notice that you can move photos between the custom Photo Web Part Zone and the standard Web Part Zone. If you close the page and return in the future, all the photos will retain their positions.
By default, a Web Part Zone displays the Web Parts that it contains in only one of two orientations: Horizontal or Vertical. You can select a particular orientation with the WebPartZone control’s LayoutOrientation
property.
However, there are situations in which you might want to display Web Parts with a more complicated layout. For example, in the previous section, we created a Photo Web Part Zone. It would be nice if we could display the photos in a certain number of repeating columns.
In this section, we build a Multi-Column Web Part Zone. This Web Part Zone includes a RepeatColumns
property. When you declare the Web Part Zone, you can use this property to set the number of columns of Web Parts to display (see Figure 30.2).
To create this custom Web Part Zone, you need to override the default rendering behavior of the WebPartZone
class. The WebPartZone
class includes several methods related to rendering its content including:
RenderContents()
—. Renders the entire contents of the Web Part Zone.
RenderHeader()
—. Renders the header of the Web Part Zone.
RenderBody()
—. Renders the body of the Web Part Zone.
RenderDropCue()
—. Renders the drop cues that appear when you move Web Parts.
RenderFooter()
—. Renders the footer of the Web Part Zone.
In an ideal world, Web Part Zones and Web Parts would not use HTML tables for layout. We would use Cascading Style Sheets for layout and use HTML tables only for their intended purpose: displaying tabular information. Unfortunately, we do not live in an ideal world. Microsoft is committed to supporting older browsers and older browsers do not provide good support for Cascading Style Sheets. So we’re stuck with HTML tables.
To create a Multi-Column Web Part Zone, you need to override the RenderBody()
method. The code for the MultiColumnWebPartZone
control is contained in Listing 30.3.
Example 30.3. MultiColumnWebPartZone.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Displays Web Parts in multiple columns ''' </summary> Public Class MultiColumnWebPartZone Inherits WebPartZone Private _repeatColumns As Integer = 2 ''' <summary> ''' The number of columns to display ''' </summary> Public Property RepeatColumns() As Integer Get Return _repeatColumns End Get Set(ByVal Value As Integer) _repeatColumns = Value End Set End Property ''' <summary> ''' Overrides default Web Part Zone rendering ''' in Browse Display Mode ''' </summary> Protected Overrides Sub RenderBody(ByVal writer As HtmlTextWriter) If Me.DesignMode Then MyBase.RenderBody(writer) ElseIf Me.WebPartManager.DisplayMode Is WebPartManager.BrowseDisplayMode Then RenderMultiColumnBody(writer) Else MyBase.RenderBody(writer) End If End Sub ''' <summary> ''' Renders Web Parts in multiple columns by iterating ''' through the Web Parts collection ''' </summary> Private Sub RenderMultiColumnBody(ByVal writer As HtmlTextWriter) ' Create the Web Part Chrome Dim chrome As WebPartChrome = Me.CreateWebPartChrome() ' Create the opening Table Tag writer.AddAttribute("border", "1") writer.RenderBeginTag(HtmlTextWriterTag.Table) writer.RenderBeginTag(HtmlTextWriterTag.Tr) ' Render each Web Part Dim counter As Integer = 1 Dim part As WebPart For Each part In Me.WebParts writer.RenderBeginTag(HtmlTextWriterTag.Td) chrome.RenderWebPart(writer, part) writer.RenderEndTag() ' Add a Tr when counter = RepeatColumns If counter = _repeatColumns Then writer.RenderEndTag() ' Close Tr writer.RenderBeginTag(HtmlTextWriterTag.Tr) counter = 0 End If counter = counter + 1 Next ' Close Table Tag writer.RenderEndTag() writer.RenderEndTag() End Sub End Class End Namespace
The class contained in Listing 30.3 inherits from the base WebPartZone
class. The MultiColumnWebPartZone
control overrides the base class’s RenderBody()
method to render the zone’s Web Parts in a multi-column table.
Notice that a multi-column table is rendered only when the page is in Browse Display mode. When the page is in any other display mode or the control is displayed in a designer, the base RenderBody()
method is called and the Web Parts are rendered the normal way.
It would be nice if the Web Parts could be rendered in a multi-column table in Design Mode as well as in Browse Mode. Unfortunately, the JavaScript that renders the drop cues for moving Web Parts assumes that a Web Part Zone is rendered either vertically or horizontally. To fix this problem, you would have to rewrite the JavaScript library used by the Web Part Framework.
The RenderMultiColumnBody()
method performs all the work of rendering the multi-column table. First, the method grabs the Web Part Chrome that is used when rendering each Web Part. Each of the Web Parts contained in the zone are rendered by calling the RenderWebPart()
method of the WebPartChrome
class (otherwise, the Web Parts would appear without their title bars and menus).
The actual list of Web Parts that the zone renders is retrieved from the base WebPartZone
control’s WebParts
property. Be aware that this property, unlike the GetInitialWebParts()
method, returns the list of Web Parts that the Web Part Zone displays after personalization.
You can experiment with the MultiPartWebPartZone control with the page in Listing 30.4.
Example 30.4. ShowMultiColumnWebPartZone.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" Assembly="__code" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show MultiColumn Web Part Zone</title> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> </Items> </asp:Menu> <custom:MultiColumnWebPartZone id="WebPartZone1" RepeatColumns="2" runat="server"> <ZoneTemplate> <asp:Label id="Label1" Text="hello 1" runat="server" /> <asp:Label id="Label2" Text="hello 2" runat="server" /> <asp:Label id="Label3" Text="hello 3" runat="server" /> <asp:Label id="Label4" Text="hello 4" runat="server" /> <asp:Label id="Label5" Text="hello 5" runat="server" /> </ZoneTemplate> </custom:MultiColumnWebPartZone> </form> </body> </html>
The page in Listing 30.4 uses the MultiColumnWebPartZone
control to render its one and only Web Part Zone. The MultiColumnWebPartZone
control’s RepeatColumns
property is set to display a two-column table. When you open the page in your browser, you will see the page in Figure 30.2.
By default, Web Part controls display very simple menus. You can use the WebPartVerbRenderMode
property of the WebPartZone
class to display one of two types of menus: a menu that appears in a single drop-down list or a menu that appears as a list of static links in the title bar.
In this section, we create fancier menus for Web Parts. We add the necessary functionality to the Web Part Framework to make it possible to create mulltiple drop-down menus for a single Web Part. We also add support for creating dividers between menu items (see Figure 30.3).
To create fancy menus, three of the standard classes in the Web Part Framework must be modified:
WebPartVerb
—. This class represents a menu item. It needs to be extended to support nested menus and menu dividers.
WebPartChrome
—. This class represents the chrome that is rendered around a Web Part. To create fancy menus, you have to completely override the default rendering behavior of this class.
WebPartZone
—. This class represents the Web Part Zone which hosts a set of Web Parts. You need to modify this class so you can display a custom Web Part Chrome.
Let’s start by modifying the WebPartVerb
class. The modified version of this class, named MenuWebPartVerb
, is contained in Listing 30.5.
Example 30.5. MenuWebPartVerb.vb
Imports System Imports System.Web.UI.WebControls.WebParts ''' <summary> ''' Extends the base WebPartVerb class ''' with support for nested menus and menu ''' dividers. ''' </summary> Public Class MenuWebPartVerb Inherits WebPartVerb Private _parentVerbId As String = String.Empty Private _hasDivider As Boolean = False ''' <summary> ''' Enables you to nest one menu beneath another ''' </summary> Public Property parentVerbId() As String Get Return _parentVerbId End Get Set(ByVal Value As String) _parentVerbId = Value End Set End Property ''' <summary> ''' This property enables you to render a divider ''' above the menu item. ''' </summary> Public Property hasDivider() As Boolean Get Return _hasDivider End Get Set(ByVal Value As Boolean) _hasDivider = Value End Set End Property ''' <summary> ''' We need to call the base class constructors ''' in our derived class ''' </summary> Public Sub New(ByVal id As String, ByVal clientClickHandler As String) MyBase.New(id, clientClickHandler) End Sub Public Sub New(ByVal id As String, ByVal serverClickHandler As WebPartEventHandler) MyBase.New(id, serverClickHandler) End Sub Public Sub New(ByVal id As String, ByVal serverClickHandler As WebPartEventHandler, ByVal clientClickHandler As String) MyBase.New(id, serverClickHandler, clientClickHandler) End Sub End Class
In Listing 30.5, the base WebPartVerb
class is extended with two new properties: ParentId
and HasDivider
. The parentId
property enables you to nest menu items. For example, all the menu items that appear beneath the File menu will have the ID of the File menu as their ParentId
.
The HasDivider
property enables you to display a menu divider above a menu item. When you set this property to the value True
, an HTML <hr>
tag is rendered above the current menu item.
The bulk of the code for the fancy menus is contained in Listing 30.6, which contains the custom Web Part Chrome that renders the fancy menus.
Example 30.6. MenuWebPartChrome.vb
Imports System Imports System.Collections.Generic Imports System.Web.UI Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' WebPartChrome, which includes multiple drop-down ''' menus and menu dividers ''' </summary> Public Class MenuWebPartChrome Inherits WebPartChrome ''' <summary> ''' Required Constructor ''' </summary> Public Sub New(ByVal zone As WebPartZone, ByVal manager As WebPartManager) MyBase.New(zone, manager) End Sub ''' <summary> ''' The main method for rendering a Web Part. ''' Here, we take over responsibility for rendering ''' the title and menu ''' </summary> Public Overrides Sub RenderWebPart(ByVal writer As HtmlTextWriter, ByVal webPart As WebPart) ' Render an enclosing Div writer.AddAttribute(HtmlTextWriterAttribute.Id, Me.GetWebPartChromeClientID(webPart)) writer.AddAttribute(HtmlTextWriterAttribute.Class, "menuWebPartZone_chrome") writer.RenderBeginTag(HtmlTextWriterTag.Div) ' Render the title bar RenderTitleBar(writer, webPart) ' Render the Web Part MyBase.RenderPartContents(writer, webPart) ' Close the enclosing Div writer.RenderEndTag() ' Close main DIV End Sub ''' <summary> ''' Renders the title bar area of the chrome. ''' This is the part that a user can drag ''' </summary> Private Sub RenderTitleBar(ByVal writer As HtmlTextWriter, ByVal webPart As WebPart) ' Render the menu RenderMenu(writer, webPart) ' Create a break writer.AddStyleAttribute("clear", "all") writer.RenderBeginTag(HtmlTextWriterTag.Br) ' Render the title bar writer.AddAttribute(HtmlTextWriterAttribute.Id, Me.GetWebPartTitleClientID(webPart)) writer.AddAttribute(HtmlTextWriterAttribute.Class, "menuWebPartZone_chromeTitle") writer.RenderBeginTag(HtmlTextWriterTag.Div) writer.Write(webPart.DisplayTitle) writer.RenderEndTag() ' Close title DIV End Sub ''' <summary> ''' Renders the menus (possibly nested) ''' </summary> Private Sub RenderMenu(ByVal writer As HtmlTextWriter, ByVal webPart As WebPart) writer.AddStyleAttribute("display", "inline") writer.AddAttribute(HtmlTextWriterAttribute.Class, "menuWebPartZone_menu") writer.RenderBeginTag(HtmlTextWriterTag.Ul) ' Get the top-level menu items that are not hidden Dim topLevelVerbs As WebPartVerbCollection = GetChildVerbs(webPart.Verbs, String.Empty) For Each verb As MenuWebPartVerb In topLevelVerbs writer.AddStyleAttribute("float", "left") writer.AddAttribute("onmouseover", "menuWebPartZone.showMenu(this)") writer.RenderBeginTag(HtmlTextWriterTag.Li) RenderMenuRecurse(writer, verb, webPart) writer.RenderEndTag() Next writer.RenderEndTag() ' Close Ul End Sub ''' <summary> ''' The main method for rendering the subMenus. ''' This method is called recursively so you ''' can show infinitely nested menus. ''' </summary> Private Sub RenderMenuRecurse(ByVal writer As HtmlTextWriter, ByVal verb As MenuWebPartVerb, ByVal webPart As WebPart) Dim childVerbs As WebPartVerbCollection = GetChildVerbs(WebPart.Verbs, verb.ID) If (childVerbs.Count > 0) Then ' Renders a menu item that is not a link RenderHeaderMenuItem(writer, verb) writer.AddAttribute(HtmlTextWriterAttribute.Class, "menuWebPartZone_popupMenu") writer.AddStyleAttribute("position", "absolute") writer.AddStyleAttribute(HtmlTextWriterStyle.Display, "none") writer.RenderBeginTag(HtmlTextWriterTag.Ul) For Each childVerb As MenuWebPartVerb In childVerbs writer.AddAttribute("onmouseover", "menuWebPartZone.showMenu(this)") writer.RenderBeginTag(HtmlTextWriterTag.Li) RenderMenuRecurse(writer, childVerb, webPart) writer.RenderEndTag() Next writer.RenderEndTag() ' Close UL Else ' Renders a link menu item RenderLinkMenuItem(writer, verb, webPart) End If End Sub ''' <summary> ''' Renders a menu item that is not a link. ''' When a user clicks this menu item, it ''' expands sub-menu items. ''' </summary> Private Sub RenderHeaderMenuItem(ByVal writer As HtmlTextWriter, ByVal verb As MenuWebPartVerb) ' Render divider RenderMenuDivider(writer, verb) ' Render encloding Div writer.AddAttribute(HtmlTextWriterAttribute.Class, "menuWebPartZone_menuItem") writer.RenderBeginTag(HtmlTextWriterTag.Div) ' Render verb icon RenderMenuIcon(writer, verb) ' Render the verb text writer.Write(verb.Text) writer.RenderEndTag() ' Close Div End Sub ''' <summary> ''' Renders a menu item that causes a postback. ''' </summary> Private Sub RenderLinkMenuItem(ByVal writer As HtmlTextWriter, ByVal verb As MenuWebPartVerb, ByVal webPart As WebPart) ' Render divider RenderMenuDivider(writer, verb) ' Render Enclosing Div writer.AddAttribute(HtmlTextWriterAttribute.Class, "menuWebPartZone_menuItem") writer.RenderBeginTag(HtmlTextWriterTag.Div) ' Render verb icon RenderMenuIcon(writer, verb) ' Render the verb link Dim eventArg As String = String.Format("partverb:{0}:{1}", verb.ID, WebPart.ID) Dim eventRef As String = Me.Zone.Page.ClientScript.GetPostBackClientHyperlink(Me.Zone, eventArg) writer.AddAttribute(HtmlTextWriterAttribute.Href, eventRef) writer.RenderBeginTag(HtmlTextWriterTag.A) writer.Write(verb.Text) writer.RenderEndTag() writer.RenderEndTag() ' Close Div End Sub ''' <summary> ''' If a menu item has an icon then show it. ''' </summary> ''' <remarks> ''' Notice that we set empty ALT text for ''' accessibility reasons. ''' </remarks> Private Sub RenderMenuIcon(ByVal writer As HtmlTextWriter, ByVal verb As WebPartVerb) If verb.ImageUrl <> String.Empty Then writer.AddAttribute(HtmlTextWriterAttribute.Src, Me.Zone.Page.ResolveUrl(verb.ImageUrl)) writer.AddAttribute(HtmlTextWriterAttribute.Alt, String.Empty) writer.AddAttribute(HtmlTextWriterAttribute.Align, "middle") writer.RenderBeginTag(HtmlTextWriterTag.Img) writer.RenderEndTag() End If End Sub ''' <summary> ''' If a menu should display a divider above it, ''' then show it with an hr tag. ''' </summary> Private Sub RenderMenuDivider(ByVal writer As HtmlTextWriter, ByVal verb As MenuWebPartVerb) If verb.hasDivider Then writer.RenderBeginTag(HtmlTextWriterTag.Hr) writer.RenderEndTag() End If End Sub ''' <summary> ''' Returns all the verbs that have a certain ''' parent verb ''' </summary> Private Function GetChildVerbs(ByVal verbs As WebPartVerbCollection, ByVal parentId As String) As WebPartVerbCollection Dim children As New List(Of WebPartVerb)() For Each verb As MenuWebPartVerb In verbs If verb.parentVerbId = parentId Then children.Add(verb) End If Next Return New WebPartVerbCollection(children) End Function End Class End Namespace
The WebPartChrome
class is responsible for rendering the outer chrome displayed around each Web Part in a Web Part Zone. This outer chrome includes the title bar and menu rendered for each Web Part.
In Listing 30.6, we override the RenderWebPart()
method. When you override this method, you must take complete responsibility for rendering the entire contents of the Web Part Chrome.
The RenderWebPart()
method does two things. First, it renders the chrome’s title bar. The title bar contains the area that a user selects when dragging a Web Part from one zone to another when a page is in Design Display mode. The custom Web Part Chrome gets this functionality for free because the base WebPartChrome
class’s GetWebPartTitleClientID()
is used to render the right ID for the title bar. The JavaScript library used by the Web Part Framework automatically detects this ID and enables drag-and-drop support.
The default WebPartChrome
class renders an HTML table to create the chrome around a Web Part. In the custom MenuWebPartChrome
class, we use a <div>
tag instead. This choice makes sense from a standards perspective and requires less code. Unfortunately, using a div
element instead of a table element breaks all the default formatting properties included with the WebPartZone
control. If you want to format the custom MenuWebPartChrome
class, you need to use Cascading Style Sheets.
Next, in the RenderWebPart()
method, the Web Part contained within the Web Part Chrome is rendered. The Web Part is rendered with the help of the base WebPartChrome
class’s RenderPartContents()
method.
The bulk of the code in Listing 30.6 is devoted to rendering the fancy menus. The RenderTitleBar()
method calls the RenderMenu()
method to build the custom menu.
The fancy menus are created with a set of nested unordered lists (HTML <ul>
tags). The second-level menus are hidden by default with the display:none
Cascading Style Sheet rule. A client-side onmouseover
event handler is added to each list item so that a submenu is displayed when you hover your mouse over a list item.
The links rendered in the menu require additional explanation. The menu links are rendered by the RenderLinkMenuItem()
method. When you click a menu item link, the server-side method that corresponds to the menu item is executed by the Web Part.
The GetPostBackClientHyperlink()
method is called to retrieve the necessary JavaScript for invoking the server-side menu event. This method returns a string that contains JavaScript code for posting an argument back to the server. The argument must be in a special format to invoke the correct server-side menu click handler in the Web Part.
The argument passed back to the server must have three parts, separated by colons:
By following this special format, you can leverage the existing Web Part Framework support for firing off the correct server-side method when you click a particular menu item.
To use a custom Web Part Chrome, you need to create a custom Web Part Zone. The MenuWebPartZone
is contained in Listing 30.7.
Example 30.7. MenuWebPartZone.vb
Imports System Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Web Part Zone that displays fancy nested menus ''' </summary> Public Class MenuWebPartZone Inherits WebPartZone ''' <summary> ''' Register the client-script for the menus. ''' </summary> Protected Overrides Sub OnPreRender(ByVal e As EventArgs) If Not Page.ClientScript.IsClientScriptIncludeRegistered("MenuWebPartZone") Then Page.ClientScript.RegisterClientScriptInclude("MenuWebPartZone", Page.ResolveUrl("~/ClientScripts/MenuWebPartZone.js")) End If MyBase.OnPreRender(e) End Sub ''' <summary> ''' Create special Web Part chrome that contains the menus ''' </summary> Protected Overrides Function CreateWebPartChrome() As WebPartChrome Return New MenuWebPartChrome(Me, Me.WebPartManager) End Function End Class End Namespace
The custom Menu Web Part Zone overrides two methods of the base WebPartZone
class.
First, it overrides the CreateWebPartChrome()
method to substitute the custom Web Part Chrome. In this method, what’s returned is simply an instance of the MenuWebPartChrome
class.
Second, the OnPreRender()
method is overridden so that you can include a link to a JavaScript library, named MenuWebPartZone.js
, which contains the client-side code for the fancy menus. The MenuWebPartZone.js
library is contained in Listing 30.8.
Example 30.8. MenuWebPartZone.js
var menuWebPartZone = new function() { this.showMenu = menuWebPartZone_showMenu; } function menuWebPartZone_showMenu(el) { // Get ul elements var subMenus = el.getElementsByTagName('UL'), // If there are ul elements, show the first one if (subMenus.length > 0) { subMenus[0].style.display = ''; // Set up function to hide ul element again el.onmouseout = function(e) { subMenus[0].style.display = 'none'; } } }
When you hover your mouse over a menu item, the menuWebPartZone_showMenu()
method is called. This JavaScript method finds the first HTML <ul>
tag contained under the current list item and displays it. Next, the method adds a onmouseout
handler to hide the submenu when the user moves the mouse away from the menu item.
The JavaScript menu library works well in the case of Internet Explorer 6 and Opera 8. Unfortunately, it doesn’t work so well with Firefox 1. Firefox does not reliably fire the onmouseout
handler, so open menus tend to get stuck.
We are finally in a position to try out the fancy menus. First, a new Web Part needs to be created that takes advantage of them. The TextEditorPart
in Listing 30.9 displays both a File and Edit menu.
Example 30.9. TextEditorPart.ascx
<%@ Control Language="VB" ClassName="TextEditorPart" %> <%@ Implements Interface="System.Web.UI.WebControls.WebParts.IWebActionable" %> <%@ Import Namespace="System.Collections.Generic" %> <script runat="server"> ''' <summary> ''' Create the menu ''' </summary> Public ReadOnly Property Verbs() As WebPartVerbCollection Implements IWebActionable.Verbs Get Dim myVerbs As New List(Of WebPartVerb)() ' Create File menu Dim fileVerb As New MenuWebPartVerb("file", AddressOf doMenuAction) fileVerb.Text = "File" myVerbs.Add(fileVerb) Dim NewVerb As New MenuWebPartVerb("new", AddressOf doMenuAction) NewVerb.Text = "New" NewVerb.parentVerbId = fileVerb.ID myVerbs.Add(NewVerb) ' Create Edit menu Dim editVerb As New MenuWebPartVerb("edit", AddressOf doMenuAction) editVerb.Text = "Edit" myVerbs.Add(editVerb) Dim copyVerb As New MenuWebPartVerb("copy", AddressOf doMenuAction) copyVerb.Text = "Copy" copyVerb.parentVerbId = editVerb.ID myVerbs.Add(copyVerb) Dim pasteVerb As New MenuWebPartVerb("pasted", AddressOf doMenuAction) pasteVerb.Text = "Paste" pasteVerb.parentVerbId = editVerb.ID myVerbs.Add(pasteVerb) Dim boldVerb As New MenuWebPartVerb("bold", AddressOf doMenuAction) boldVerb.Text = "Bold" boldVerb.ImageUrl = "~/Icons/Bold.gif" boldVerb.hasDivider = True boldVerb.parentVerbId = editVerb.ID myVerbs.Add(boldVerb) Dim italicVerb As New MenuWebPartVerb("italic", AddressOf doMenuAction) italicVerb.Text = "Italic" italicVerb.ImageUrl = "~/Icons/Italic.gif" italicVerb.parentVerbId = editVerb.ID myVerbs.Add(italicVerb) ' Return the menu Return New WebPartVerbCollection(myVerbs) End Get End Property ''' <summary> ''' The server-side method that is invoked when you ''' click a menu item ''' </summary> Public Sub doMenuAction(ByVal s As Object, ByVal e As WebPartEventArgs) Dim verb As MenuWebPartVerb = CType(s, MenuWebPartVerb) lblAction.Text = String.Format("{0} clicked!", verb.Text) End Sub </script> <div style="padding:10px"> Select a menu item from the menu above the title bar. <br /> <asp:Label id="lblAction" EnableViewState="false" Runat="server" /> </div>
The menu is created by the TextEditorPart
control’s Verbs
property. This property is a member of the IWebActionable
interface (notice that the user control implements this interface with the directive at the top of the file).
All the menu items are wired to the same server-side method. If you click any of the menu items, the doMenuAction()
method executes and reports the ID of the menu item clicked. You could, of course, wire each menu item to a different server-side method.
You can use the TextEditorPart
in the page in Listing 30.10.
Example 30.10. ShowMenuWebPartZone.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" Assembly="__code" %> <%@ Register TagPrefix="user" TagName="TextEditorPart" Src="~/TextEditorPart.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <title>Show Menu Web Part Zone</title> <style type="text/css"> .menuWebPartZone_chrome { border:solid 2px black; width:300px; height:200px; } .menuWebPartZone_chromeTitle { background-color:silver; padding:3px; font:bold 16px Arial,sans-serif; } .menuWebPartZone_menuItem { cursor:hand; } .menuWebPartZone_menu { font:12px Arial, sans-serif; } .menuWebPartZone_menu li { margin-left:4px; margin-right:10px; margin-top:3px; list-style-type:none; } .menuWebPartZone_menu ul { padding:0px; margin:0px; background-color:#eeeeee; border:solid 1px black; width:100px; } .menuWebPartZone_menu a { color:blue; text-decoration:none; } </style> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> </Items> </asp:Menu> <custom:MenuWebPartZone id="WebPartZone1" Runat="server"> <ZoneTemplate> <user:TextEditorPart id="TextEditorPart1" Title="Text Editor Part" Description="Enables you to edit text" runat="server" /> </ZoneTemplate> </custom:MenuWebPartZone> <custom:MenuWebPartZone id="WebPartZone2" Runat="server" /> </form> </body> </html>
After you open the page in Listing 30.10 in your Web browser, you can hover your mouse over the File and Edit menu items displayed by the TextEditorWebPart
and see the sub-menus. If you select a menu option, the page posts back to the server and executes the doMenuAction()
method. This method simply reports the name of the menu item clicked in a Label control.
Notice that the ShowMenuWebPartZone.aspx
page contains several style sheet rules. These rules are used to define the background color, size, and general appearance of the menus. The page also includes style sheet rules that determine several aspects of the appearance of the Web Part Chrome, such as the style of the chrome’s title bar and border.
As previously mentioned, the default functionality of a Web Part Catalog is pretty limited. By default, Web Part Catalogs do not support paging or sorting. You also cannot drag and drop new Web Parts from a catalog into a Web Part Zone.
This section explores several methods of creating fancier catalogs. We’ll create a catalog that automatically displays all the Web Parts available in the current application, a catalog that supports drag-and-drop, and a catalog that supports templates.
Three types of parts in the Web Part Framework are related to catalogs: the CatalogZone
class, the CatalogPartChrome
class, and the CatalogPart
class. All three parts participate in the rendering of a catalog. The CatalogZone
class creates an instance of the CatalogPartChrome
class, and the CatalogPartChrome
class renders each CatalogPart
control contained in the Catalog Zone.
It is important to understand that individual CatalogPart
controls, such as the DeclarativeCatalogPart
or PageCatalogPart
control, don’t render anything. They merely act as databases of available catalog items.
The CatalogPart
class has two important methods:
GetAvailableWebPartDescriptions
—. Returns the list of descriptions of Web Parts contained in the catalog.
GetWebPart
—. Returns an actual Web Part.
For example, the DeclarativeCatalogPart
control derives from the CatalogPart class. This control overrides the GetAvailableWebPartDescriptions()
method and returns the list of catalog parts that have been listed in its template. The DeclarativeCatalogPart
also overrides the GetWebPart()
method to return one of the Web Parts declared in its template.
The individual Catalog Part controls contained in a Catalog Zone do not render anything. The CatalogZone
control creates an instance of the CatalogPartChrome
control and uses this instance to render each of the CatalogPart
controls.
The CatalogPartChrome
control has four important methods:
CreateCatalogPartChromeStyle()
—. If you override this method, you can modify the properties of the Style
object used when rendering the chrome and catalog part.
PerformPreRender()
—. If you override this method, you can execute code before a CatalogPart
control is rendered.
RenderCatalogPart()
—. If you override this method, then you modify the entire rendering behavior of the chrome.
RenderPartContents()
—. If you override this method, then you can render additional content inside the chrome.
The CatalogZone
class creates an instance of the CatalogPartChrome
class and calls its RenderCatalogPart()
method to render the selected CatalogPart
control. The CatalogZone
control itself includes several valuable methods:
CreateCatalogParts()
—. Returns the list of catalog parts that the Catalog Zone displays.
InvalidateCatalogParts()
—. Resets the collection of catalog parts associated with the Catalog Zone.
RenderContents()
—. Renders the entire Catalog Zone.
RenderHeader()
—. Renders the header area of a catalog zone.
RenderBody()
—. Renders the body area of a catalog zone.
RenderFooter()
—. Renders the footer area of a catalog zone.
RenderCatalogPartLinks()
—. Renders the list of links to the particular Catalog Parts contained in the Catalog Zone.
RenderVerbs()
—. Renders the verbs that appear at the bottom of a catalog zone.
The CatalogZoneBase
class also includes the following properties:
CatalogParts
—. This property automatically calls the CreateCatalogParts()
method to create a list of catalog parts.
SelectedCatalogPartID
—. This property enables you to get or set the current Catalog Part.
If you want to modify the appearance of any Catalog Part, then you must modify the rendering behavior of the Catalog Zone or Catalog Part Chrome that contains the Catalog Part.
Let’s start by creating a new Catalog Part that automatically lists all the Web Part controls contained in the App_Code folder. Because the custom Catalog Part will take advantage of reflection to determine the list of available Web Parts, we’ll name this custom Catalog Part the Reflect Catalog Part.
Reflection refers to the process of retrieving information about .NET Framework types—such as classes, methods, properties, and attributes—at runtime. Two namespaces in the .NET Framework are devoted to reflection: System.Reflection
and System.Reflection.Emit
.
The ReflectCatalogPart control is contained in Listing 30.11.
Example 30.11. ReflectCatalogPart.cs
Imports System Imports System.Reflection Imports System.Collections.Generic Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Catalog Part that automatically displays all ''' Web Parts defined in the App_Code folder ''' </summary> Public Class ReflectCatalogPart Inherits CatalogPart Private _catalog As New Dictionary(Of String, WebPart) ''' <summary> ''' We create the list of available Web Parts ''' during the Init event since we use this list ''' with both the GetAvailableWebPartDescriptions() ''' and GetWebPart() methods ''' </summary> Protected Overrides Sub OnInit(ByVal e As EventArgs) ' Get the list of Web Parts through reflection Dim moduleArray As Reflection.Module() = Assembly.GetExecutingAssembly().GetModules(False) Dim webPartTypes As Type() = moduleArray(0).FindTypes(AddressOf WebPartFilter, Nothing) ' Create and instance of each Web Part and add to catalog For i As Integer = 0 To webPartTypes.Length - 1 Dim newPart As WebPart = CType(Activator.CreateInstance(webPartTypes(i)), WebPart) newPart.ID = "part" + i.ToString() _catalog.Add(newPart.ID, newPart) Next MyBase.OnInit(e) End Sub ''' <summary> ''' Returns a collection of descriptions of Web Parts ''' in the App_Code folder ''' </summary> Public Overrides Function GetAvailableWebPartDescriptions() As WebPartDescriptionCollection Dim descriptions As New List(Of WebPartDescription)() For Each NewPart As WebPart In _catalog.Values descriptions.Add(New WebPartDescription(NewPart)) Next Return New WebPartDescriptionCollection(descriptions) End Function ''' <summary> ''' Because you already instantiated all the Web Parts ''' in the OnInit() method, you simply return a ''' Web Part from the catalog collection ''' </summary> Public Overrides Function GetWebPart(ByVal description As WebPartDescription) As WebPart Return _catalog(description.ID) End Function ''' <summary> ''' You don't want a list of all classes in the App_Code folder, ''' only those classes that derive from the WebPart class ''' </summary> Private Function WebPartFilter(ByVal t As Type, ByVal s As Object) As Boolean Return GetType(WebPart).IsAssignableFrom(t) End Function End Class End Namespace
The list of Web Parts defined in the App_Code folder is retrieved in the OnInit()
method. The current module is retrieved by grabbing the first module in the currently executing assembly. Next, the FindTypes()
method is called with a filter that returns only classes derived from the Web Part class (the filter used by FindTypes()
is created with the WebPartFilter()
method).
The GetAvailableWebPartDescriptions()
method returns a collection of WebPartDescription
classes. The Catalog Zone calls this method to get the list of Web Parts that it displays. In our implementation of this method, we simply copy the list of Web Parts that we retrieved in the OnInit()
method into a WebPartDescriptionCollection
class.
Finally, the GetWebPart()
method returns a Web Part that matches a description. This method is called by the Catalog Zone when a Web Part is added to a Web Part Zone. Again, we take advantage of the collection built in the OnInit()
method to return a Web Part that matches the description parameter.
You can use the custom ReflectCatalogPart
in the page in Listing 30.12. You’ll need to switch to Catalog Display Mode by clicking the Catalog link to see the Reflect Catalog Part.
Example 30.12. ShowReflectCatalogPart.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Reflect Catalog Part</title> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Catalog" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server" /> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server" /> <asp:CatalogZone id="CatalogZone1" CssClass="column" Runat="server"> <ZoneTemplate> <custom:ReflectCatalogPart id="ReflectCatalogPart1" Runat="server" /> </ZoneTemplate> </asp:CatalogZone> </form> </body> </html>
When you open the page in Listing 30.12 and click the Catalog link, you’ll see all of the Web Parts defined in your App_Code folder listed (see Figure 30.4). The Reflect Catalog Part doesn’t display anything unless you add some Web Parts to your App_Code folder. In particular, the control doesn’t show Web Parts that you have created with User Controls because these Web Parts are contained in a different assembly.
Although the Web Part Framework supports drag-and-drop functionality when you move Web Parts between Web Part Zones, the Web Part Framework does not support drag-and-drop when you want to add new Web Parts to a Web Part Zone from a Catalog Zone. In this section, we’ll fix this limitation of the Web Part Framework by creating a custom DragDropCatalogZone
control.
To create the custom Catalog Zone, you won’t have to do as much work as you might expect. You can leverage the existing JavaScript library included with the Web Part Framework. We’ll tweak this library so it will work the same way with Catalog Zones as it works with Web Part Zones.
First, we need to create a custom Catalog Zone. Listing 30.13 contains the code for the custom DragDropCatalogZone
class.
Example 30.13. DragDropCatalogZone.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' CatalogZone that supports drag-and-drop ''' adding of Web Parts ''' </summary> Public Class DragDropCatalogZone Inherits CatalogZone ''' <summary> ''' Adds the client-side script for drag-and-drop ''' </summary> Protected Overrides Sub OnPreRender(ByVal e As EventArgs) EnsureID() Dim startupScript As String = String.Format("dragDropCatalogZone.start('{0}'),", Me.ClientID) If Not Page.ClientScript.IsClientScriptIncludeRegistered("DragDropCatalogZone") Then Page.ClientScript.RegisterClientScriptInclude("DragDropCatalogZone", Page.ResolveUrl("~/ClientScripts/DragDropCatalogZone.js")) Page.ClientScript.RegisterStartupScript(GetType(DragDropCatalogZone), "DragDropCatalogZone", startupScript, True) End If MyBase.OnPreRender(e) End Sub ''' <summary> ''' Utility method to return currently ''' selected Catalog Part ''' </summary> Public ReadOnly Property SelectedCatalogPart() As CatalogPart Get Return Me.CatalogParts(Me.SelectedCatalogPartID) End Get End Property ''' <summary> ''' Override the default postback handler to ''' add support for adding a Web Part to a ''' Web Part Zone ''' </summary> Protected Overrides Sub RaisePostBackEvent(ByVal eventArgument As String) If eventArgument.StartsWith("Add:") Then Dim args() As String = eventArgument.Split(":"c) AddWebPart(args(1), args(2), Int32.Parse(args(3))) End If MyBase.RaisePostBackEvent(eventArgument) End Sub ''' <summary> ''' Use the WebPartManager control to ''' actually add the Web Part ''' </summary> Private Sub AddWebPart(ByVal webPartId As String, ByVal zoneId As String, ByVal zoneIndex As Integer) ' Get Web Part Dim desc As WebPartDescription = SelectedCatalogPart.GetAvailableWebPartDescriptions()(webPartId) Dim webPart As WebPart = SelectedCatalogPart.GetWebPart(desc) ' Get Zone Dim zone As WebPartZoneBase = Me.WebPartManager.Zones(zoneId) ' Add Web Part Me.WebPartManager.AddWebPart(webPart, zone, zoneIndex) End Sub ''' <summary> ''' This Web Part Zone uses the specialized ''' DragDropWebPartChrome ''' </summary> Protected Overrides Function CreateCatalogPartChrome() As CatalogPartChrome Return New DragDropCatalogPartChrome(Me) End Function End Class ''' <summary> ''' This class extends the base CatalogPartChrome ''' class by adding a div element to mark the ''' area of draggable images. ''' </summary> Public Class DragDropCatalogPartChrome Inherits CatalogPartChrome ''' <summary> ''' Add the DIV tag ''' </summary> Public Overrides Sub RenderCatalogPart(ByVal writer As HtmlTextWriter, ByVal catalogPart As CatalogPart) writer.AddAttribute(HtmlTextWriterAttribute.Id, GenerateId()) writer.RenderBeginTag(HtmlTextWriterTag.Div) MyBase.RenderCatalogPart(writer, catalogPart) writer.RenderEndTag() End Sub ''' <summary> ''' Create a unique ID for the DIV tag ''' so we can grab the DIV tag in our ''' client script ''' </summary> Private Function GenerateId() As String Return String.Format("{0}_draggable", Me.Zone.ID) End Function Public Sub New(ByVal zone As CatalogZoneBase) MyBase.New(zone) End Sub End Class End Namespace
The custom Catalog Zone does three things. First, the OnPreRender()
method adds a reference to an external JavaScript file named DragDropCatalogZone.js
. This file contains the JavaScript code used to support the client-side drag-and-drop functionality. This file is discussed in a moment.
The custom Catalog Zone also overrides the base CatalogZone
control’s RaisePostBackEvent()
method. This method is called after a user drops a catalog item into a Web Part Zone. The method adds the new Web Part to the zone with the help of the WebPartManager
control’s AddWebPart()
method.
Finally, the custom Catalog Zone overrides the base CreateCatalogPartChrome()
method. Notice that the file in Listing 30.13 actually defines two classes: DragDropCatalogZone
and DragDropCatalogPartChrome
. The DragDropCatalogZone
control renders a catalog part by using the custom chrome defined by the DragDropCatalogPartChrome
class. This class simply adds a <div>
tag around the contents of the CatalogPart. This <div>
tag is used in the JavaScript code to identify the draggable catalog items.
The JavaScript code used by the custom DragDropCatalogZone control is contained in Listing 30.14.
Example 30.14. DragDropCatalogZone.js
var dragDropCatalogZone = new function() { this.start = dragDropCatalogZone_start; this.addWebPart = dragDropCatalogZone_addWebPart; } function dragDropCatalogZone_start(catalogZoneId) { // Get Catalog Zone var catalogZone = document.getElementById(catalogZoneId); // Find Element with Draggable Class var draggable = document.getElementById(catalogZoneId + '_draggable'), // Get Images contained in Draggable var images = draggable.getElementsByTagName('img'), // Get Inputs contained in Draggable var inputs = draggable.getElementsByTagName('input'), // Check that Images == Inputs if (images.length != inputs.length) { alert('DragDropCatalogZone:Each catalog item must have a catalog icon'), return; } // Convert images into Web Parts for (var i=0;i<images.length;i++) { var catItem = new WebPart(images[i],images[i]); images[i].webPartId = inputs[i].value; images[i].detachEvent("ondragend", WebPart_OnDragEnd); images[i].attachEvent("ondragend", dragDropCatalogZone.addWebPart); } // Add Drop Handler to WebPartManager __wpm.xCatalogZoneId = catalogZoneId; __wpm.xCompleteAddWebPartDragDrop = dragDropCatalogZone_CompleteAddWebPartDragDrop; } function dragDropCatalogZone_addWebPart() { __wpm.xCompleteAddWebPartDragDrop(); } function dragDropCatalogZone_CompleteAddWebPartDragDrop() { var dragState = this.dragState; this.dragState = null; if (dragState.dropZoneElement != null) { dragState.dropZoneElement.__zone.ToggleDropCues(false, dragState.dropIndex, false); } document.body.detachEvent("ondragover", Zone_OnDragOver); for (var i = 0; i < __wpm.zones.length; i++) { __wpm.zones[i].allowDrop = false; } this.overlayContainerElement.removeChild(this.overlayContainerElement.firstChild); this.overlayContainerElement.style.display = "none"; if ((dragState != null) && (dragState.dropped == true)) { var currentZone = dragState.webPartElement.__webPart.zone; var currentZoneIndex = dragState.webPartElement.__webPart.zoneIndex; if ((currentZone != dragState.dropZoneElement.__zone) || ((currentZoneIndex != dragState.dropIndex) && (currentZoneIndex != (dragState.dropIndex - 1)))) { var eventTarget = this.xCatalogZoneId; var eventArgument = 'Add:' + dragState.webPartElement.webPartId + ':' + dragState.dropZoneElement.__zone.uniqueID + ':' + dragState.dropIndex; this.SubmitPage(eventTarget, eventArgument); } } }
The dragDropCatalogZone_start()
method is called immediately after the DragDropCatalogZone
control is rendered. This method takes advantage of the <div>
tag added to the Catalog Zone by the DragDropCatalogPartChrome
class to identify the images and input elements associated with the catalog items.
The method converts each of the images in the catalog zone into a Web Part by calling the WebPart()
constructor defined in the Web Part Framework JavaScript library. Next, the method overrides the default JavaScript function that is called when a Web Part is dropped into a Web Part Zone. When a user drops a catalog item into a Web Part Zone, the dragDropCatalogZone_CompleteAddWebPartDragDrop()
method is called.
The dragDropCatalogZone_CompleteAddWebPartDragDrop()
method posts the page back to the server by calling the SubmitPage()
method. An eventArgument
that represents the Web Part ID, Web Part Zone ID, and Web Part Zone index is posted back to the server and processed by the RaisePostBackEventHandler()
method. This server method actually adds the Web Part to the selected zone.
Listing 30.15 illustrates how you can use the DragDropCatalogZone
control in a page.
Example 30.15. ShowDragDropCatalogZone.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Drag-and-Drop Catalog Zone</title> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Catalog" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server" /> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server" /> <custom:DragDropCatalogZone id="CatalogZone1" CssClass="column" Runat="server"> <ZoneTemplate> <asp:DeclarativeCatalogPart id="DeclarativeCatalogPart1" Runat="server"> <WebPartsTemplate> <asp:Label id="Label1" CatalogIconImageUrl="~/Images/FirstSimplePart.gif" Title="First Simple Part" Description="The first Web Part" Runat="server" /> <asp:Label id="Label2" CatalogIconImageUrl="~/Images/SecondSimplePart.gif" Title="Second Simple Part" Description="The second Web Part" Runat="server" /> </WebPartsTemplate> </asp:DeclarativeCatalogPart> <asp:PageCatalogPart id="PageCatalogPart1" Runat="server" /> </ZoneTemplate> </custom:DragDropCatalogZone> </form> </body> </html>
After you open the page in Listing 30.15, you can view the custom Catalog Zone by clicking the Catalog link. Notice that you can add items from the catalog to the page by dragging the catalog icons onto particular Web Part Zones (see Figure 30.5).
The DragDropCatalogZone
control still renders the normal check boxes for adding items. This is good from an accessibility standpoint. When you implement fancy JavaScript drag-and-drop functionality, you should always implement an equivalent way of doing the same thing from the keyboard.
The default CatalogZone
control has a certain appearance and there is nothing you can do about it. The CatalogZone
control does not support paging or sorting. It doesn’t even display the descriptions with Web Parts.
In this section, we’ll fix this limitation of the Web Part Framework by building a custom Templated Catalog Zone. The custom CatalogZone
control supports an ItemTemplate
. You can take advantage of the ItemTemplate
to display catalog items in any way that you please.
For example, you can use a GridView
control in the ItemTemplate
to enable users to sort and page through catalog items. You can also take advantage of the ItemTemplate
to modify the default appearance of the Catalog Part menu. Rather than display a list of links to individual Catalog Parts, you can display a drop-down list of Catalog Parts (see Figure 30.6).
The custom Templated Catalog Zone control is contained in Listing 30.16.
Example 30.16. TemplatedCatalogZone.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' This control enables you to lay out a ''' Catalog Zone with a template ''' </summary> Public Class TemplatedCatalogZone Inherits CatalogZone Private _itemTemplate As ITemplate Private _item As ItemContainer ''' <summary> ''' Represents the Item Template ''' </summary> <TemplateContainer(GetType(TemplatedCatalogZone))> _ Public Property ItemTemplate() As ITemplate Get Return _itemTemplate End Get Set(ByVal Value As ITemplate) _itemTemplate = Value End Set End Property ''' <summary> ''' Utility method that returns the currently ''' selected Catalog Part ''' </summary> Public ReadOnly Property SelectedCatalogPart() As CatalogPart Get Return CatalogParts(SelectedCatalogPartID) End Get End Property ''' <summary> ''' Returns list of Web Web Part descriptions ''' so that you can use Container.Descriptions ''' in the ItemTemplate ''' </summary> Public ReadOnly Property Descriptions() As WebPartDescriptionCollection Get Return SelectedCatalogPart.GetAvailableWebPartDescriptions() End Get End Property ''' <summary> ''' Returns list of Web Part Zones so that you ''' can use Container.Zones in the ItemTemplate ''' </summary> Public ReadOnly Property Zones() As WebPartZoneCollection Get Return Me.WebPartManager.Zones End Get End Property ''' <summary> ''' Adds a new Web Part to a Web Part Zone ''' </summary> Public Sub AddWebPart(ByVal webPartID As String, ByVal zoneID As String) ' Get Web Part Dim descs As WebPartDescriptionCollection = SelectedCatalogPart.GetAvailableWebPartDescriptions() Dim NewWebPart As WebPart = SelectedCatalogPart.GetWebPart(descs(webPartID)) ' Get Zone Dim zones As WebPartZoneCollection = Me.WebPartManager.Zones Dim selectedZone As WebPartZoneBase = zones(zoneID) ' Add the Web Part Me.WebPartManager.AddWebPart(NewWebPart, selectedZone, 0) End Sub ''' <summary> ''' Extends base method to support the ItemTemplate ''' </summary> Protected Overrides Sub CreateChildControls() MyBase.CreateChildControls() _item = New ItemContainer() ItemTemplate.InstantiateIn(_item) Controls.Add(_item) End Sub ''' <summary> ''' Render the contents of the ItemTemplate ''' </summary> Protected Overrides Sub RenderBody(ByVal writer As HtmlTextWriter) _item.RenderControl(writer) End Sub ''' <summary> ''' Suppress the footer since the same content ''' is contained in our ItemTemplate ''' </summary> Protected Overrides Sub RenderFooter(ByVal writer As HtmlTextWriter) End Sub End Class ''' <summary> ''' This class does nothing but hold the contents ''' of the ItemTemplate so we can render it. ''' </summary> Public Class ItemContainer Inherits Control End Class End Namespace
The TemplatedCatalogZone
control in Listing 30.16 inherits from the base CatalogZone
class. The derived class exposes an ItemTemplate
property that you can use to customize the control’s appearance. The ItemTemplate
is created in the CreateChildControls()
method, and the contents of the ItemTemplate
are rendered in the RenderBody()
method.
Notice that the TemplatedCatalogZone
class includes a public AddWebPart()
method. This method adds a Web Part with a particular ID to a particular Web Part Zone. The Web Part is added with the help of the WebPartManager
class’s AddWebPart()
method.
The page in Listing 30.17 contains the TemplatedCatalogZone
control.
Example 30.17. ShowTemplatedCatalogZone.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> ''' <summary> ''' Whenever the CatalogZone is opened, we need to ''' rebind to the datasource ''' </summary> Protected Sub WebPartManager1_DisplayModeChanged(ByVal sender As Object, ByVal e As WebPartDisplayModeEventArgs) If WebPartManager1.DisplayMode Is WebPartManager.CatalogDisplayMode Then CatalogZone1.SelectedCatalogPartID = CatalogZone1.CatalogParts(0).ID CatalogZone1.DataBind() End If End Sub ''' <summary> ''' When a user selects a new Catalog Part, ''' we need to rebind the GridView ''' </summary> Protected Sub btnSelectCatalog_Click(ByVal sender As Object, ByVal e As EventArgs) ' Update selected catalog Dim dropCatalogs As DropDownList = CType(CatalogZone1.FindControl("dropCatalogs"), DropDownList) CatalogZone1.SelectedCatalogPartID = dropCatalogs.SelectedValue End Sub ''' <summary> ''' When a user selects a new page, we ''' must rebind the GridView ''' </summary> Protected Sub grdDesc_PageIndexChanging(ByVal sender As Object, ByVal e As GridViewPageEventArgs) Dim grdDesc As GridView = CType(sender, GridView) grdDesc.PageIndex = e.NewPageIndex End Sub ''' <summary> ''' When a user selects a Web Part from a Catalog, we ''' add the new Web Part to the page ''' </summary> Protected Sub grdDesc_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) ' Rebind the GridView to reload the DataKeys Dim grdDesc As GridView = CType(sender, GridView) grdDesc.DataBind() ' Get Selected Web Part Dim webPartID As String = CType(grdDesc.SelectedValue, String) ' Get Selected Zone Dim dropZones As DropDownList = CType(CatalogZone1.FindControl("dropZones"), DropDownList) Dim zoneID As String = dropZones.SelectedValue ' Add the Web Part to the Page CatalogZone1.AddWebPart(webPartID, zoneID) End Sub ''' <summary> ''' If in Catalog Display Mode, bind GridView ''' </summary> Protected Overrides Sub OnPreRenderComplete(ByVal e As EventArgs) If WebPartManager1.DisplayMode Is WebPartManager.CatalogDisplayMode Then Dim grdDesc As GridView = CType(CatalogZone1.FindControl("grdDesc"), GridView) grdDesc.DataBind() End If MyBase.OnPreRenderComplete(e) End Sub Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .catalogPartStyle { padding:10px; font:14px Georgia,serif; } .catalogPartStyle fieldset { padding-top:30px; padding-bottom:10px; margin-bottom:10px; } .catalogPartStyle label { font-weight:bold; } .catalogGrid { margin:10px; } .catalogRow td { padding:5px; border-bottom:solid 1px black; } .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Templated Catalog Zone</title> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" Runat="server" OnDisplayModeChanged="WebPartManager1_DisplayModeChanged" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Catalog" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" HeaderText="Zone 1" CssClass="column" Runat="server" /> <asp:WebPartZone id="WebPartZone2" HeaderText="Zone 2" CssClass="column" Runat="server"/> <custom:TemplatedCatalogZone id="CatalogZone1" CssClass="column catalogPartStyle" Runat="server"> <ItemTemplate> <fieldset> <legend>Select Catalog</legend> <asp:DropDownList id="dropCatalogs" DataTextField="Title" DataValueField="ID" DataSource='<%# Container.CatalogParts %>' runat="server"/> <asp:Button id="btnSelectCatalog" Text="select" Tooltip="Select Catalog" OnClick="btnSelectCatalog_Click" runat="server"/> </fieldset> <asp:Label id="lblZone" Text="Add to Zone:" AssociatedControlID="dropZones" Runat="server"/> <asp:DropDownList id="dropZones" DataTextField="HeaderText" DataValueField="ID" DataSource='<%# Container.Zones %>' runat="server" /> <asp:GridView id="grdDesc" GridLines="none" CssClass="catalogGrid" RowStyle-CssClass="catalogRow" DataKeyNames="ID" AllowPaging="true" PageSize="2" ShowHeader="false" EmptyDataText="This catalog is empty" AutoGenerateColumns="false" AutoGenerateSelectButton="true" DataSource='<%# Container.Descriptions %>' OnPageIndexChanging="grdDesc_PageIndexChanging" OnSelectedIndexChanged="grdDesc_SelectedIndexChanged" runat="server"> <Columns> <asp:BoundField DataField="Title" /> <asp:BoundField DataField="Description" /> </Columns> </asp:GridView> </ItemTemplate> <ZoneTemplate> <asp:DeclarativeCatalogPart id="DeclarativeCatalogPart1" Title="Declarative Catalog" Runat="server"> <WebPartsTemplate> <asp:Label id="Label1" Title="Label 1" Description="The very first Label" Runat="server" /> <asp:Label id="Label2" Title="Label 2" Description="The second Label" Runat="server" /> <asp:Label id="Label3" Title="Label 3" Description="The very last Label" Runat="server" /> </WebPartsTemplate> </asp:DeclarativeCatalogPart> <asp:PageCatalogPart id="PageCatalogPart1" Title="Page Catalog" Runat="server"/> </ZoneTemplate> </custom:TemplatedCatalogZone> </form> </body> </html>
If you open the page in Listing 30.17 in your browser and click the Catalog link, you can see the custom Templated Catalog Zone. Notice that the custom Catalog Zone supports paging. Furthermore, notice that unlike the default CatalogZone control, the list of Catalog Parts is displayed in a drop-down list.
In Listing 30.17, the TemplatedCatalogZone
control contains an ItemTemplate
that includes two DropDownList
controls and a GridView
control. The DropDownList
controls are used to display a list of Catalog Parts and a list of Web Part Zones. The GridView
control is used to display the list of catalog items (the Web Part descriptions).
Each of these three controls is bound to a datasource with a databinding expression. For example, the DropDownList
control that displays the list of Catalog Parts is declared like this:
<asp:DropDownList id="dropCatalogs" DataTextField="Title" DataValueField="ID" DataSource='<%# Container.CatalogParts %>' runat="server"/>
In this control declaration, Container.CatalogParts
refers to the CatalogParts
property exposed by the TemplateCatalogControl
class. Because this class also exposes a Descriptions and Zones property, you can also use Container.Descriptions
and Container.Zones
in a databinding expression within the TemplateCatalogZone
control’s ItemTemplate
.
Because the controls in the ItemTemplate
do not take advantage of declarative databinding, you must undertake the responsibility of binding the controls to their respective data sources. Notice, for instance, that the GridView control is bound to its data source in the OnPreRenderComplete()
method defined near the top of the file in Listing 30.17.
Notice that the GridView is bound to its data source during the PreRenderComplete
event. This is the last event that happens before the page is rendered to the browser. You must perform the databinding at this point in the page execution lifecycle because certain Catalog Parts, such as the PageCatalogPart
control, don’t update their catalog of Web Parts until the PreRender
event.
The advantage of using the TemplatedCatalogZone
control is that you can make a Catalog Zone look like anything you want. However, as Spiderman says, “With great power comes great responsibility.” The disadvantage is that you are forced to write all sorts of additional code to bind the data controls to their data sources.
Editor Zones contain the Editor Parts you can use to edit the properties of your Web Parts. The Web Part Framework contains a standard set of Editor Part controls that can be used to edit the standard properties of a Web Part: the AppearanceEditorPart
, the BehaviorEditorPart
, and the LayoutEditorPart
controls.
If your Web Part has custom properties, and you want to edit these properties with an Editor Part, then you have two choices. You can use the PropertyGridEditorPart
control or build your own custom Editor Part. In most cases, you won’t want to use the PropertyGridEditorPart
because it does not provide any support for form validation or advanced layout. When using the PropertyGridEditorPart
, for any property other than a Boolean or Enum property, you get a single-line text box and there isn’t anything you can do about it.
Another option is to edit a Web Part’s properties inline. In other words, you can add an edit form to the Web Part itself and display the edit form only when the page is in Edit Display Mode. You can detect whether a page is in Edit Display Mode within a Web Part by retrieving the current instance of the WebPartManager
control with the WebPartManager.GetCurrentWebPartManager(Page)
method and checking the value of the DisplayMode
property.
This section explores two methods of creating custom Editor Parts. First, to clarify the concepts involved, we’ll create a simple Editor Part designed to edit the properties of a particular Web Part. Next, you’ll learn how to create a templated Editor Part you can use when editing any Web Part control regardless of the types of properties that it contains.
As in other types of Web Zones, three types of objects are involved in rendering an Editor Zone: the EditorZone
control, the EditorPartChrome
class, and the EditorPart
control.
The EditorZone
control appears only when a page is in Edit Display Mode. When the page is in Edit Display Mode, the Editor Zone still does not display any Editor Parts until you select the Edit menu option on a particular Web Part.
When you select a Web Part to edit, two types of Editor Parts are displayed. First, any Editor Parts declared in the Editor Zone are displayed. Second, if the Web Part implements the IWebEditable
interface or derives from the base WebPart
class, then any Editor Parts returned from the Web Part’s CreateEditorParts()
method are displayed.
The BehaviorEditorPart
appears only when a page is in Shared Personalization scope and the Web Part being edited is shared among all users.
Finally, the EditorPartChrome
class is actually responsible for rendering each Editor Part. An instance of the EditorPartChrome
class is created by an Editor Zone, and then the Editor Zone calls the EditorPartChrome
control’s RenderEditorPart()
method to render each Editor Part.
If you want to modify the appearance of an Editor Zone or the chrome that appears around each Editor Part, then you need to modify the EditorZone
and EditorPartChrome
classes. If you want to modify the appearance of the Editor Parts themselves, then you need to modify the properties of a particular Editor Part control.
Let’s start by creating the Web Part that we will edit. The FeaturedBookPart
is contained in Listing 30.18.
Example 30.18. FeaturedBookPart.ascx
<%@ Control Language="VB" ClassName="FeaturedBookPart" %> <%@ Implements Interface="System.Web.UI.WebControls.WebParts.IWebEditable" %> <%@ Implements Interface="myControls.IFeaturedBook" %> <%@ Import Namespace="System.Collections.Generic" %> <%@ Import Namespace="myControls" %> <script runat="server"> Private _bookTitle As String Private _datePublished As DateTime Private _price As Decimal Public ReadOnly Property WebBrowsableObject() As Object Implements IWebEditable.WebBrowsableObject Get Return Me End Get End Property Public Function CreateEditorParts() As EditorPartCollection Implements IWebEditable.CreateEditorParts Dim editorParts As New List(Of EditorPart)() Dim editor As New FeaturedBookEditorPart() editor.ID = "FeaturedEditor1" editorParts.Add(editor) Return New EditorPartCollection(editorParts) End Function <Personalizable()> _ Public Property BookTitle() As String Implements IFeaturedBook.BookTitle Get Return _bookTitle End Get Set(ByVal Value As String) _bookTitle = Value End Set End Property <Personalizable()> _ Public Property DatePublished() As DateTime Implements IFeaturedBook.DatePublished Get Return _datePublished End Get Set(ByVal Value As DateTime) _datePublished = Value End Set End Property <Personalizable()> _ Public Property Price() As Decimal Implements IFeaturedBook.Price Get Return _price End Get Set(ByVal Value As Decimal) _price = Value End Set End Property Private Sub Page_PreRender() lblBookTitle.Text = _bookTitle lblDatePublished.Text = _datePublished.ToString("D") lblPrice.Text = _price.ToString("c") End Sub </script> Title: <asp:Label id="lblBookTitle" Runat="server" /> <br /> Published: <asp:Label id="lblDatePublished" Runat="server" /> <br /> Price: <asp:Label id="lblPrice" Runat="server" />
Notice that the FeaturedBookPart
control implements two interfaces: the IWebEditable
and the IFeaturedBook
interfaces. Implementing the first interface enables you to associate the FeaturedBookPart
with a custom EditorPart
control. The IWebEditable
interface has two members:
WebBrowsableObject
—. This property represents the object that is edited. Normally, the property should just return a reference to the current control.
CreateEditorParts
—. This method returns the collection of EditorPart
controls associated with the current Web Part.
The FeaturedBookPart
also implements the IFeaturedBook
interface that we create in a moment. The custom EditorPart
control needs some method of identifying the properties exposed by the FeaturedBookPart
. The IFeaturedBook
interface provides the EditorPart
with this information. (You don’t need to create an interface when creating a Web Part with a custom control rather than a User Control.)
The custom Editor Part is contained in Listing 30.19.
Example 30.19. FeaturedBookEditorPart.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Describes the properties of the ''' FeaturedWebPart control ''' </summary> Public Interface IFeaturedBook Property BookTitle() As String Property DatePublished() As DateTime Property Price() As Decimal End Interface ''' <summary> ''' Custom Editor for FeaturedBookPart ''' </summary> Public Class FeaturedBookEditorPart Inherits EditorPart Private _txtBookTitle As TextBox Private _calDatePublished As Calendar Private _txtPrice As TextBox Private _valPrice As CompareValidator Private _title As String = "Featured Book Editor" ''' <summary> ''' Create standard title ''' </summary> Public Overrides Property Title() As String Get Return _title End Get Set(ByVal Value As String) _title = Value End Set End Property ''' <summary> ''' Utility method that returns the child control ''' in the case of a GenericWebPart ''' </summary> Private ReadOnly Property ControlToEdit() As Control Get If TypeOf (Me.WebPartToEdit) Is GenericWebPart Then Return (CType(WebPartToEdit, GenericWebPart)).ChildControl Else Return Me.WebPartToEdit End If End Get End Property ''' <summary> ''' Called when you click OK or Apply to ''' apply the Web Part property changes ''' </summary> Public Overrides Function ApplyChanges() As Boolean Dim success As Boolean = False EnsureChildControls() Page.Validate() If Page.IsValid Then CType(ControlToEdit, IFeaturedBook).BookTitle = _txtBookTitle.Text CType(ControlToEdit, IFeaturedBook).DatePublished = calDatePublished.SelectedDate CType(ControlToEdit, IFeaturedBook).Price = Decimal.Parse(_txtPrice.Text) success = True End If Return success End Function ''' <summary> ''' Called when the Web Part Framework ''' has initialized the Web Part being edited ''' </summary> Public Overrides Sub SyncChanges() EnsureChildControls() _txtBookTitle.Text = (CType(ControlToEdit, IFeaturedBook)).BookTitle _calDatePublished.SelectedDate = (CType(ControlToEdit, IFeaturedBook)).DatePublished _txtPrice.Text = (CType(ControlToEdit, IFeaturedBook)).Price.ToString() End Sub ''' <summary> ''' Adds the controls rendered by this Editor Part ''' to the controls collection. ''' </summary> Protected Overrides Sub CreateChildControls() ' Add Book Title _txtBookTitle = New TextBox() _txtBookTitle.ID = "txtBookTitle" Controls.Add(_txtBookTitle) ' Add Date Published _calDatePublished = New Calendar() _calDatePublished.ID = "calDatePublished" Controls.Add(_calDatePublished) ' Add Price _txtPrice = New TextBox() _txtPrice.ID = "txtPrice" _txtPrice.Columns = 5 Controls.Add(_txtPrice) ' Add Price Validator _valPrice = New CompareValidator() _valPrice.ID = "valPrice" _valPrice.ControlToValidate = _txtPrice.ID _valPrice.Operator = ValidationCompareOperator.DataTypeCheck _valPrice.Type = ValidationDataType.Currency _valPrice.Text = "(Must be currency)" Controls.Add(_valPrice) End Sub ''' <summary> ''' Renders the User Interface for the Editor Part ''' </summary> ''' <param name="writer"></param> Protected Overrides Sub RenderContents(ByVal writer As HtmlTextWriter) ' Render Book Title RenderLabel(writer, "Book Title:", _txtBookTitle.ClientID) writer.WriteBreak() _txtBookTitle.RenderControl(writer) writer.WriteBreak() writer.WriteBreak() ' Render Date Published RenderLabel(writer, "Date Published:", _calDatePublished.ClientID) writer.WriteBreak() _calDatePublished.RenderControl(writer) writer.WriteBreak() writer.WriteBreak() ' Render Price RenderLabel(writer, "Price:", _txtPrice.ClientID) _valPrice.RenderControl(writer) writer.WriteBreak() _txtPrice.RenderControl(writer) writer.WriteBreak() End Sub ''' <summary> ''' Renders an accessible Label for the ''' form fields ''' </summary> Private Sub RenderLabel(ByVal writer As HtmlTextWriter, ByVal labelText As String, ByVal associatedControlId As String) writer.AddAttribute(HtmlTextWriterAttribute.For, associatedControlId) writer.RenderBeginTag(HtmlTextWriterTag.Label) writer.Write(labelText) writer.RenderEndTag() End Sub End Class End Namespace
The class in Listing 30.19 inherits from the base EditorPart
class. It overrides two methods from the base class: SyncChanges()
and ApplyChanges()
.
The SyncChanges()
method is called automatically when the EditorPart
is displayed and the Web Part being edited has been initialized. Listing 30.19 takes advantage of this method to synchronize the editor form with the current property values of the Web Part being edited.
The ApplyChanges()
method is automatically called when the user clicks the Apply or OK button in the Editor Zone. This method updates the properties of the Web Part being edited with the values from the editor form.
The ApplyChanges()
and SyncChanges()
methods are executed during the processing of postback data after the Page Load
event and before the PreRender
event.
Notice that both the SyncChanges()
and ApplyChanges()
methods take advantage of a property named ControlToEdit
, which also is defined in the class in Listing 30.19. This property returns a different control depending on whether the Web Part being edited is a GenericWebPart
or a “true” Web Part. When you create a Web Part by using a User Control or any control that does not derive from the WebPart class, the Web Part Framework automatically wraps the control in a GenericWebPart
control. In the case of a GenericWebPart
control, you want to edit the properties of the first child control of the Web Part and not the properties of the Web Part itself.
The bulk of the work in Listing 30.19 is devoted to building the custom editor form. Two TextBox
controls and one Calendar
control are created in the CreateChildControls()
method. These controls are actually rendered in the RenderContents()
method.
Finally, Listing 30.20 contains a page that displays the FeaturedBookPart
and FeaturedBookEditorPart
controls.
Example 30.20. ShowFeaturedBookEditorPart.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <%@ Register TagPrefix="user" TagName="FeaturedBookPart" Src="~/FeaturedBookPart.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Help Display Mode</title> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Edit" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server"> <ZoneTemplate> <user:FeaturedBookPart id="FeaturedBookPart1" Title="Featured Book" runat="Server" /> </ZoneTemplate> </asp:WebPartZone> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server"/> <asp:EditorZone id="EditorZone1" CssClass="column" runat="server"> <ZoneTemplate> <asp:LayoutEditorPart id="LayoutEditorPart1" runat="server" /> </ZoneTemplate> </asp:EditorZone> </form> </body> </html>
After you open the page in Listing 30.20, you can click the Edit link and then select the Edit menu option on the FeaturedBookPart
. When you edit the FeaturedBookPart
, both our custom FeaturedBookEditorPart
and the standard LayoutEditorPart
will appear in the Editor Zone (see Figure 30.7). Notice that an instance of the LayoutEditorPart
control was declared in the Editor Zone contained in the page.
It took a lot of work to create the custom Editor Part control. Too much work to simply display an editor form. In the next section, you’ll learn how to avoid doing any of this work in the future. In the next section, we create a templated Editor Part.
In this section, we create the last Editor Part that you’ll ever need to make. We will create a Templated Editor Part, which will enable you to easily build any type of editor form that you need. You can create the custom form for the Templated Editor Part with a form defined in a user control file.
The Templated Editor Part is contained in Listing 30.21.
Example 30.21. TemplatedEditorPart.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Enables you to use templates when ''' editing Web Parts ''' </summary> Public Class TemplatedEditorPart Inherits EditorPart Private _editTemplateUrl As String Private _editTemplate As ITemplatedEditorPart ''' <summary> ''' Loads the user control that contains ''' the Edit Template ''' </summary> Protected Overrides Sub CreateChildControls() _editTemplate = CType(Page.LoadControl(_editTemplateUrl), ITemplatedEditorPart) Controls.Add(CType(_editTemplate, Control)) End Sub ''' <summary> ''' Utility method that returns the child control ''' in the case of a GenericWebPart ''' </summary> Private ReadOnly Property ControlToEdit() As Control Get If TypeOf Me.WebPartToEdit Is GenericWebPart Then Return (CType(WebPartToEdit, GenericWebPart)).ChildControl Else Return Me.WebPartToEdit End If End Get End Property ''' <summary> ''' Called when the user clicks Apply or OK ''' in the Editor Zone ''' </summary> ''' <returns></returns> Public Overrides Function ApplyChanges() As Boolean EnsureChildControls() Return _editTemplate.ApplyChanges(ControlToEdit) End Function ''' <summary> ''' Called when the Editor Template ''' displays current Web Part property ''' values. ''' </summary> Public Overrides Sub SyncChanges() EnsureChildControls() _editTemplate.SyncChanges(ControlToEdit) End Sub ''' <summary> ''' Pass the Editor Part Title and Template ''' URL to the constructor ''' </summary> Public Sub New(ByVal title As String, ByVal editTemplateUrl As String) Me.Title = title _editTemplateUrl = editTemplateUrl End Sub End Class ''' <summary> ''' Defines the contract that any Edit Template ''' must satisfy ''' </summary> Public Interface ITemplatedEditorPart Function ApplyChanges(ByVal controlToEdit As Control) As Boolean Sub SyncChanges(ByVal controlToEdit As Control) End Interface End Namespace
The constructor for the TemplatedEditorPart
class takes a title and editTemplateUrl
parameter. The editTemplateUrl
parameter specifies the location of a user control that contains the edit form used by the TemplatedEditorPart
control. The user control is loaded in the CreateChildControls()
method.
Notice that the TemplatedEditorPart
control derives from the base EditorPart
class. It implements the ApplyChanges()
and SyncChanges()
from the base class. In this case, however, the control simply calls methods with the same name in the user control that it loads.
The Web Part in Listing 30.22 uses the TemplatedEditorPart
control.
Example 30.22. FeaturedVideoPart.ascx
<%@ Control Language="VB" ClassName="FeaturedVideoPart" %> <%@ Implements Interface="System.Web.UI.WebControls.WebParts.IWebEditable" %> <%@ Import Namespace="System.Collections.Generic" %> <%@ Import Namespace="myControls" %> <script runat="server"> Private _videoTitle As String Private _director As String Private _price As Decimal Public ReadOnly Property WebBrowsableObject() As Object Implements IWebEditable.WebBrowsableObject Get Return Me End Get End Property Public Function CreateEditorParts() As EditorPartCollection Implements IWebEditable.CreateEditorParts Dim editorParts As New List(Of EditorPart)() Dim editor As New TemplatedEditorPart("Featured Video Editor", "~/FeaturedVideoEditTemplate.ascx") editor.ID = "Editor1" editorParts.Add(editor) Return New EditorPartCollection(editorParts) End Function <Personalizable> _ Public Property VideoTitle() As String Get Return _videoTitle End Get Set (ByVal Value As String) _videoTitle = value End Set End Property <Personalizable> _ Public Property Director() As String Get Return _director End Get Set (ByVal Value As String) _director = value End Set End Property <Personalizable> _ Public Property Price() As Decimal Get Return _price End Get Set (ByVal Value As Decimal) _price = value End Set End Property Private Sub Page_PreRender() lblVideoTitle.Text = _videoTitle lblDirector.Text = _director lblPrice.Text = _price.ToString("c") End Sub </script> Title: <asp:Label id="lblVideoTitle" Runat="server" /> <br /> Director: <asp:Label id="lblDirector" Runat="server" /> <br /> Price: <asp:Label id="lblPrice" Runat="server" />
The Web Part in Listing 30.22 displays a featured video. Notice that the Web Part implements the IWebActionable
interface with its CreateEditorParts()
method and WebBrowsableObject
property.
The CreateEditorParts()
method returns an instance of the TemplatedEditorPart
control. The TemplatedEditorPart
is initialized with the path to a user control named FeaturedVideoEditTemplate
. This user control contains the template for editing the Web Part, and it is contained in Listing 30.23.
Example 30.23. FeaturedVideoEditTemplate.ascx
<%@ Control Language="VB" ClassName="FeaturedVideoEditTemplate" %> <%@ Reference Control="~/FeaturedVideoPart.ascx" %> <%@ Implements Interface="myControls.ITemplatedEditorPart" %> <%@ Import Namespace="myControls" %> <script runat="server"> Public Sub SyncChanges(ByVal controlToEdit As Control) Implements ITemplatedEditorPart.SyncChanges Dim part As ASP.FeaturedVideoPart = CType(controlToEdit, ASP.FeaturedVideoPart) txtVideoTitle.Text = part.VideoTitle txtDirector.Text = part.Director txtPrice.Text = part.Price.ToString() End Sub Public Function ApplyChanges(ByVal controlToEdit As Control) As Boolean Implements ITemplatedEditorPart.ApplyChanges Dim success As Boolean = False Page.Validate() If Page.IsValid Then Dim part As ASP.FeaturedVideoPart = CType(controlToEdit, ASP.FeaturedVideoPart) part.VideoTitle = txtVideoTitle.Text part.Director = txtDirector.Text part.Price = Decimal.Parse(txtPrice.Text) success = True End If Return success End Function </script> <asp:Label id="lblVideoTitle" Text="Video Title:" AssociatedControlID="txtVideoTitle" Runat="server" /> <br /> <asp:TextBox id="txtVideoTitle" Runat="server" /> <br /><br /> <asp:Label id="lblDirector" Text="Director:" AssociatedControlID="txtDirector" Runat="server" /> <br /> <asp:TextBox id="txtDirector" Runat="server" /> <br /><br /> <asp:Label id="lblPrice" Text="Price:" AssociatedControlID="txtPrice" Runat="server" /> <br /> <asp:TextBox id="txtPrice" Runat="server" /> <asp:CompareValidator id="valprice" ControlToValidate="txtPrice" Display="dynamic" Text="(Must be Currency)" Type="Currency" Operator="DataTypeCheck" Runat="server" /> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="txtPrice" Display="dynamic" Text="(Required)" Runat="server" />
Notice that the user control in Listing 30.23 implements the ITemplatedEditorPart
interface with its SyncChanges()
and ApplyChanges()
methods. The SyncChanges()
method initializes the form fields in the user control with the current values of the properties of the Web Part being edited. The ApplyChanges()
method updates the Web Part being edited with changes made in the edit form.
The page in Listing 30.24 uses the TemplatedEditorPart
control to edit the FeaturedVideoPart
control.
Example 30.24. ShowTemplatedEditorPart.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <%@ Register TagPrefix="user" TagName="FeaturedVideoPart" Src="~/FeaturedVideoPart.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Templated Editor Part</title> </head> <body> <form id="form1" runat="server"> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Edit" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server"> <ZoneTemplate> <user:FeaturedVideoPart id="FeaturedVideoPart1" Title="Featured Video" runat="server" /> </ZoneTemplate> </asp:WebPartZone> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server" /> <asp:EditorZone id="EditorZone1" CssClass="column" Runat="server" /> </form> </body> </html>
After you open the page in Listing 30.24, you can view the TemplatedEditorPart
by clicking the Edit link and selecting a Web Part to edit (see Figure 30.8).
The nice thing about the TemplatedEditorPart
control is that you can associate a different edit template with each of the different types of Web Parts in a page. Just pass the path to a different template when initializing the TemplatedEditorPart
in each Web Part’s CreateEditorParts()
method.
By default, the Web Part Framework supports the following Display Modes:
BrowseDisplayMode
—. The default mode.
DesignDisplayMode
—. Enables you to drag and drop Web Parts between Web Part Zones.
EditDisplayMode
—. Enables you to select a Web Part for editing. Associated with Editor Zones.
CatalogDisplayMode
—. Enables you to add new Web Parts to a page. Associated with Catalog Zones.
ConnectDisplayMode
—. Enables you to connect Web Parts. Associated with Connections Zones.
Notice that the last three display modes are associated with particular tool zones. For example, when you select a Web Part for editing, the contents of the Editor Zone are displayed.
Like most other aspects of the Web Part Framework, this list of Display Modes is not written in stone. You can extend the Web Part Framework with your own custom display modes.
In this section, we’ll create a custom Display Mode and an associated custom tool zone. We’ll create a Help Display Mode. When a Web Parts page is in Help Display Mode, a help box appears above each Web Part. Furthermore, detailed help can be accessed from a HelpZone (see Figure 30.9).
Let’s start by creating the Web Part Help Display Mode itself. The custom HelpDisplayMode
class is contained in Listing 30.25.
Example 30.25. HelpDisplayMode.vb
Imports System Imports System.Web.UI.WebControls.WebParts ''' <summary> ''' Defines custom Help Display Mode ''' </summary> Public Class HelpDisplayMode Inherits WebPartDisplayMode Public Sub New(ByVal name As String) MyBase.New(name) End Sub ''' <summary> ''' When true, users can move Web Parts ''' </summary> Public Overrides ReadOnly Property AllowPageDesign() As Boolean Get Return False End Get End Property ''' <summary> ''' When true, a HelpZone must be added ''' to the page. ''' </summary> Public Overrides ReadOnly Property AssociatedWithToolZone() As Boolean Get Return False End Get End Property ''' <summary> ''' When true, an error is raised when ''' personalization is disabled. ''' </summary> Public Overrides ReadOnly Property RequiresPersonalization() As Boolean Get Return False End Get End Property ''' <summary> ''' When true, hidden Web Parts ''' are displayed. ''' </summary> Public Overrides ReadOnly Property ShowHiddenWebParts() As Boolean Get Return True End Get End Property End Class
The HelpDisplayMode
class overrides a number of properties from its base WebPartDisplayMode
class. For example, you prevent users from dragging Web Parts between zones when Help Display Mode is enabled by setting the AllowPageDesign
property to the value False
.
To use the custom Web Part Display Mode, you must modify the WebPartManager
control. The modified WebPartManager
control, named CustomWebPartManager
, is contained in Listing 30.26.
Example 30.26. CustomWebPartManager.vb
Imports System Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Extends WebPartManager control with support for ''' HelpDisplayMode ''' </summary> Public Class CustomWebPartManager Inherits WebPartManager Public Shared ReadOnly HelpDisplayMode As HelpDisplayMode = New HelpDisplayMode("Help") Protected Overrides Function CreateDisplayModes() As WebPartDisplayModeCollection Dim modes As WebPartDisplayModeCollection = MyBase.CreateDisplayModes() modes.Add(HelpDisplayMode) Return modes End Function End Class End Namespace
In Listing 30.26, the CreateDisplayModes()
method is overridden and the HelpDisplayMode
class is added. Notice that the HelpDisplayMode
class is exposed as a public field of the CustomWebPartManager
. That way, you can use CustomWebPartManager.HelpDisplayMode
in your code to refer to the custom Display Mode.
Next, we need to create the Web Parts that displays in the page. These Web Parts will display a floating help box. They are contained in Listing 30.27 and Listing 30.28.
Example 30.27. FirstHelpPart.ascx
<%@ Control Language="VB" ClassName="FirstHelpPart" %> <%@ Import Namespace="myControls" %> <script runat="server"> Sub Page_PreRender() Dim wpm As CustomWebPartManager = CType(WebPartManager.GetCurrentWebPartManager(Page), CustomWebPartManager) divHelp.Visible = (wpm.DisplayMode Is CustomWebPartManager.HelpDisplayMode) End Sub </script> <div id="divHelp" class="divHelp" runat="server"> Here is the help for the FirstHelpPart control! </div> <h1>First Help Part</h1>
Example 30.28. SecondHelpPart.ascx
<%@ Control Language="VB" ClassName="SecondHelpPart" %> <%@ Import Namespace="myControls" %> <script runat="server"> Sub Page_PreRender() Dim wpm As CustomWebPartManager = CType(WebPartManager.GetCurrentWebPartManager(Page), CustomWebPartManager) divHelp.Visible = (wpm.DisplayMode Is CustomWebPartManager.HelpDisplayMode) End Sub </script> <div id="divHelp" class="divHelp" runat="server"> Here is the help for the SecondHelpPart control! </div> <h1>Second Help Part</h1>
Both Web Parts contain a <div>
tag that has a brief help message. The contents of the <div>
tag are hidden or displayed in the Page_PreRender()
method, depending on the current Web Part Display Mode.
Next, we need to create the custom HelpZone
control. This control can be used to display extended help for the page. The HelpZone
control is contained in Listing 30.29.
Example 30.29. HelpZone.vb
Imports System Imports System.Web.UI Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Displays extended page help when a page ''' is in Help Display Mode ''' </summary> Public Class HelpZone Inherits ToolZone Private _contents As String = "Help Contents" Private _headerText As String = "Help" Public Sub New() MyBase.New(CustomWebPartManager.HelpDisplayMode) End Sub ''' <summary> ''' Text displayed in title bar ''' </summary> Public Overrides Property HeaderText() As String Get Return _headerText End Get Set(ByVal Value As String) _headerText = Value End Set End Property ''' <summary> ''' Represents the help text displayed in the ''' Help Zone ''' </summary> <PersistenceMode(PersistenceMode.InnerProperty)> _ Public Property Contents() As String Get Return _contents End Get Set(ByVal Value As String) _contents = Value End Set End Property ''' <summary> ''' Renders the help text ''' </summary> Protected Overrides Sub RenderBody(ByVal writer As HtmlTextWriter) writer.Write(_contents) End Sub ''' <summary> ''' When the user clicks Close, switch ''' back to Browse Display Mode ''' </summary> Protected Overrides Sub Close() Me.WebPartManager.DisplayMode = WebPartManager.BrowseDisplayMode End Sub End Class End Namespace
The HelpZone
control takes whatever text is contained in its <Contents>
tag and renders it. The text appears only when the page is in Help Display Mode.
Finally, we can create the page that hosts all of the custom controls. The page in Listing 30.30 contains the CustomWebPartManager
control, the custom HelpZone
control, and the two custom Web Parts.
Example 30.30. ShowHelpDisplayMode.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <%@ Register TagPrefix="user" TagName="FirstHelpPart" Src="~/FirstHelpPart.ascx" %> <%@ Register TagPrefix="user" TagName="SecondHelpPart" Src="~/SecondHelpPart.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) CustomWebPartManager1.DisplayMode = CustomWebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .divHelp { position:absolute; width:200px; top:10px; left:20px; border:solid 2px orange; background-color:#FFFFE0; padding:5px; font:12px Arial,sans-serif; filter:progid:DXImageTransform.Microsoft.dropshadow(OffX=-5, OffY=5, Color=#cccccc) } .helpZone { border:solid 2px orange; background-color:#FFFFE0; padding:5px; } .column table { position:relative; } .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Help Display Mode</title> </head> <body> <form id="form1" runat="server"> <custom:CustomWebPartManager id="CustomWebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Help" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server"> <ZoneTemplate> <user:FirstHelpPart id="FirstHelpPart1" Title="First Web Part" runat="Server" /> </ZoneTemplate> </asp:WebPartZone> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server"> <ZoneTemplate> <user:SecondHelpPart id="SecondHelpPart1" Title="Second Web Part" runat="Server" /> </ZoneTemplate> </asp:WebPartZone> <custom:HelpZone id="HelpZone1" CssClass="helpZone" runat="Server"> <Contents> This is the extended help for this page. </Contents> </custom:HelpZone> </form> </body> </html>
After you open the page in Listing 30.30, you can click the Help link to switch the page into Help Display Mode. When you switch to Help Display Mode, you should see help messages pop up above each Web Part. Furthermore, the contents of the HelpZone
are displayed.
The page in Listing 30.30 takes advantage of its style sheet to perform most of the formatting. For example, the floating help boxes are created with the help of the divHelp
CSS class.
In this chapter, you learned how you easily can extend the power of the Web Part Framework. The first section explored different types of Web Part Zones that you can create. For example, we created a custom Photo Web Part Zone that automatically displays the photos contained in a folder. We also created a multi-column Web Part Zone that displays Web Parts in a configurable number of repeating columns. Finally, we created a Web Part Zone that supports fancy drop-down menus.
Next, you learned about several methods of extending Catalog Zones. We created a custom Catalog Part that automatically displays all the Web Part controls contained in an application’s App_Code
folder. We also created a custom Catalog Zone that supports drag-and-drop functionality. Finally, you explored a method for creating a templated Catalog Zone that enables you to customize the appearance of a catalog in any way that you please.
Next, we tackled the subject of Editor Zones. First, we created a simple custom Editor Part that renders a custom form for editing the properties of a Web Part. Next, we created a templated Editor Zone that enables you to easily associate custom editor forms with any Web Part.
Finally, we built a custom Web Part Display Mode and Tool Zone. By taking advantage of a custom Help Display Mode, you can display help messages easily to the users of a Web Part page.