Chapter 30. Extending the Web Part Framework

<feature><title>In this Chapter</title> <objective>

Creating Custom Web Part Zones

</objective>
<objective>

Creating a Multi-Column Web Part Zone

</objective>
<objective>

Creating Custom Catalog Zones

</objective>
<objective>

Creating Custom Editor Zones

</objective>
<objective>

Creating Custom Web Part Display Modes

</objective>
<objective>

Summary

</objective>
</feature>

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.

Creating Custom Web Part Zones

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.

How Web Part Zones Work

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.

Creating a Photo Web Part Zone

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).

The Photo Web Part Zone.

Figure 30.1. The Photo Web Part Zone.

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.

Creating a Multi-Column Web Part Zone

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).

Displaying Web Parts in multiple columns.

Figure 30.2. Displaying Web Parts in multiple columns.

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.

Web Standards Note

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.

Note

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.

Creating a Menu Web Part Zone

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).

Displaying fancy Web Part menus.

Figure 30.3. Displaying fancy Web Part menus.

To create fancy menus, three of the standard classes in the Web Part Framework must be modified:

  • WebPartVerbThis class represents a menu item. It needs to be extended to support nested menus and menu dividers.

  • WebPartChromeThis 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.

  • WebPartZoneThis 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.

Web Standards Note

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:

  • partverbThis string indicates that a Web Part verb has been clicked.

  • Verb ID—The ID of the Web Part verb that a user clicked.

  • Web Part ID—The ID of the Web Part that contains the server-side method to execute.

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.

Web Standards Note

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.

Creating Custom Catalog Zones

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.

How Catalog Zones Work

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:

  • GetAvailableWebPartDescriptionsReturns the list of descriptions of Web Parts contained in the catalog.

  • GetWebPartReturns 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:

  • CatalogPartsThis property automatically calls the CreateCatalogParts() method to create a list of catalog parts.

  • SelectedCatalogPartIDThis 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.

Creating a Reflection 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.

Note

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.

Automatically displaying available Web Parts.

Figure 30.4. Automatically displaying available Web Parts.

Creating a Drag-and-Drop Catalog Zone

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).

Dragging and dropping from a catalog.

Figure 30.5. Dragging and dropping from a catalog.

Web Standards Note

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.

Creating a Templated Catalog Zone

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).

Displaying a drop-down list of catalog parts.

Figure 30.6. Displaying a drop-down list of catalog parts.

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.

Note

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.

Creating Custom Editor Zones

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.

Note

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.

How Editor Zones Work

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.

Note

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.

Creating a Simple Custom Editor Part

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:

  • WebBrowsableObjectThis property represents the object that is edited. Normally, the property should just return a reference to the current control.

  • CreateEditorPartsThis 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.

Note

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.

Displaying a custom editor.

Figure 30.7. Displaying a custom editor.

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.

Creating 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).

Using the Templated Editor Part.

Figure 30.8. Using the Templated Editor Part.

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.

Creating Custom Web Part Display Modes

By default, the Web Part Framework supports the following Display Modes:

  • BrowseDisplayModeThe default mode.

  • DesignDisplayModeEnables you to drag and drop Web Parts between Web Part Zones.

  • EditDisplayModeEnables you to select a Web Part for editing. Associated with Editor Zones.

  • CatalogDisplayModeEnables you to add new Web Parts to a page. Associated with Catalog Zones.

  • ConnectDisplayModeEnables 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).

A page in Help Display Mode.

Figure 30.9. A page in Help Display Mode.

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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset