Web Part Fundamentals






Web Part Fundamentals

ASP.NET 2.0 introduces a collection of components and controls that provide the building blocks for constructing a customizable portal site. These components manage the details of storing the user customization data, providing the interface for customization, and managing the Web Parts you define as components for users to work with. You are left with the task of placing the various zones on your portal page (Web Part zones, editor zones, catalog zones, and connection zones), building customizable Web Parts, and instructing the WebPartManager when to transition among the various editing modes. These components make it easy to build a portal site in any format, layout, or design that you can imagine, taking care of the difficult elements of constructing a portal for you automatically. Figure shows a sample portal site in action, with the user dragging a Web Part from one zone on the page to another.

Sample portal page running


Portal Components

There are three core conceptual components to building portal pages in ASP.NET 2.0: Web Parts, zones, and the Web Part manager. Web Parts are the UI components you build as pieces of functionality for clients to use. Web Parts can be custom classes that inherit from the WebPart base class (which in turn inherits from the Panel control), or they can be any control-derived class in ASP.NET (controls are implicitly wrapped by a generic Web Part class). Zones are regions on a page, typically embedded in an HTML layout element like a table cell or DIV, that contain the Web Parts. When a client customizes the page, she can elect to move Web Parts from one zone to another, so the more zones you provide in your interface, the more flexibility the client will have with the page's layout. You will also typically include an editor zone and a catalog zone, which will display the properties of a Web Part that can be modified and a collection of available Web Parts, respectively. Finally, the Web Part manager is a nonvisual control that orchestrates the interaction of all of the zones and Web Parts on a page, providing methods for entering modes of customization and for exporting, importing, and saving Web Parts. Figure shows one possible layout of a portal page using Web Parts, zones, and the Web Part manager.

Portal components in ASP.NET 2.0


Building a Minimal Portal Page

The first task in putting together a portal page with ASP.NET 2.0 is to add a WebPartManager control, which orchestrates the interaction of Web Parts and Web Part zones on the page. This control must be placed before any other Web Part components on the page, so it is usually a good idea to place the WebPartManager control right at the top of the page (just inside the opening server-side form tag). Next you'll want to add a few WebPartZone controls so that there is a place to put Web Parts. Listing 6-1 shows a minimal portal page with two zones laid out in an HTML table and the accompanying WebPartManager.

-1. Minimal portal page

<%--File: MinimalPortalPage.aspx--%>
<%@ Page Language="C#" AutoEventWireup="true"
         CodeFile="MinimalPortalPage.aspx.cs"
         Inherits="MinimalPortalPage" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Minimal Portal Page</title>
</head>
<body>
  <form id="form1" runat="server">
  <asp:WebPartManager runat="server" ID="_webPartManager" />

  <div>
    <table width="100%">
    <tr><td>
        <asp:WebPartZone ID="_leftWebPartZone" runat="server"
                         HeaderText="Left Zone">
            <ZoneTemplate>
              <!-- Web Parts go here -->
            </ZoneTemplate>
      </asp:WebPartZone>
    </td><td>
        <asp:WebPartZone ID="_rightWebPartZone" runat="server"
                         HeaderText="Right Zone">
            <ZoneTemplate>
                <!-- Web Parts go here -->
            </ZoneTemplate>
        </asp:WebPartZone>
    </td></tr>
    </table>
  </div>

  </form>
</body>
</html>

The next task is to build some Web Parts to place in the Web Part zones. There are several ways of doing this, as you shall soon see, but we will start by writing a pair of Web Parts in code, much like you would write any custom control in ASP.NET. Instead of inheriting from Control or WebControl, however, you inherit from System.Web.UI.WebControls.WebParts.WebPart, and override the virtual RenderContents method to supply the user interface. Listing 6-2 shows a minimal pair of "Hello world" Web Parts.

-2. "Hello world" Web Parts

// File: HelloWorldWebParts.cs
using System;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;

namespace EssentialAspDotNet2.WebParts
{
    public class HelloWorldWebPart : WebPart
    {
        protected override void RenderContents(HtmlTextWriter writer)
        {
            writer.Write("Welcome to web parts!");

            base.RenderContents(writer);
        }
    }
    public class HelloWorldWebPart2 : WebPart
    {
        protected override void RenderContents(HtmlTextWriter writer)
        {
            writer.RenderBeginTag(HtmlTextWriterTag.H1);
            writer.Write("Hello world!");
            writer.RenderEndTag();

            base.RenderContents(writer);
        }
    }
}

To add our new Web Parts to our portal page, we need to register the namespace and assembly in the page, as you would with any custom control, and then declare instances of the Web Parts in the ZoneTemplate elements in each WebPartZone. Web Parts placed in this template will appear in that zone when the page is first accessed prior to any customization by the user (or administrator). If you would prefer not to have a particular Web Part displayed on a page when it is first shown, you could opt to include it in the declarative catalog for a page, as you will see. Listing 6-3 shows our updated portal page with an @Register directive referencing our Web Parts and an instance of each of our custom Web Parts placed in each of the page's zones. This example assumes that the HelloWorldWebParts.cs file is deployed in the App_Code directory of this site so that the Assembly attribute of the @Register directive can be omitted.

-3. Minimal portal page with Web Parts

<%--File: MinimalPortalPage.aspx--%>
<%@ Page Language="C#" AutoEventWireup="true"
         CodeFile="MinimalPortalPage.aspx.cs"
         Inherits="MinimalPortalPage" %>

<%@ Register Namespace="EssentialAspDotNet2.WebParts"
             TagPrefix="eadn2" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Minimal Portal Page</title>
</head>
<body>
  <form id="form1" runat="server">
  <asp:WebPartManager runat="server" ID="_webPartManager" />
  <div>
    <table width="100%">
    <tr><td>
        <asp:WebPartZone ID="_leftWebPartZone" runat="server"
                         HeaderText="Left Zone">
            <ZoneTemplate>
              <eadn2:HelloWorldWebPart runat="server" ID="_wp1" />
            </ZoneTemplate>
      </asp:WebPartZone>
    </td><td>
        <asp:WebPartZone ID="_rightWebPartZone" runat="server"
                         HeaderText="Right Zone">
            <ZoneTemplate>
                <eadn2:HelloWorldWebPart2 runat="server" ID="_wp2" />
            </ZoneTemplate>
        </asp:WebPartZone>
    </td></tr>
    </table>
  </div>

  </form>
</body>
</html>

At this point we can actually run our portal page, and the Web Parts will show up in their respective zones. It's not much to look at so far, and there is no customization available, but it will indeed run, as shown in Figure.

Minimal portal page running


The reason there is no customization yet in our site is that the site is running with anonymous access enabled, so there is no authentication for clients. The Web Part infrastructure depends on uniquely identifying clients by their user name and thus requires all clients to be authenticated to support customization. Note that by default if an anonymous client is accessing the site, she will see the Web Parts and be able to interact with them, but she will not be able to change their attributes or modify the page layout. You can use any of the supported authentication mechanisms in ASP.NET or IIS (Windows, Forms, Passport, etc.) as long as the user is forced to authenticate when she visits the page. In this example, we will enable Windows authentication and disallow anonymous users by modifying the web.config file, as shown in Listing 6-4.

-4. Forcing authentication for our portal site using web.config

<configuration>
  <system.web>
    <authentication mode="Windows" />
    <authorization>
      <deny users="?"/>
    </authorization>
  </system.web>
</configuration>

Once our users are authenticated, the Web Parts on the page take on a slightly different appearance by enabling a menu in the upper right corner. By default, this menu lets you minimize or close a Web Part, persisting the state of the Web Part on behalf of the current client (so when a new client visits the site, she will still see the Web Parts in their original state even though a different client may have minimized or closed them). Figure shows the default menu that appears once the page is displayed to an authenticated client. When you minimize a Web Part it displays only the title portion, but if you close it, it is removed from the form altogether. To give users the ability to add back closed Web Parts, we will need to add a catalog zone, which we will do shortly.

Web Parts with menu (displayed for authenticated clients)


Display Mode

The ability to minimize and close individual Web Parts is a good first step at providing customization for our site, but ideally we want to provide users with much more than that. Web Parts also support the ability to change the layout of a page (by letting users drag and drop parts between zones), modify properties of individual Web Parts, connect multiple Web Parts together (by sharing data), and displaying a catalog of all available Web Parts to add to a page. All of these features are enabled by changing the display mode of the Web Part manager; the only thing you have to do as the developer is to give users the ability to transition among the various modes. Figure shows the values the DisplayMode property of the WebPartManager class can take on, and what effect it has on the portal page.

Display modes

Display Mode

Purpose

BrowseDisplayMode

Standard view mode. No personalization or editing.

DesignDisplayMode

Permits drag-and-drop layout personalization/customization.

EditDisplayMode

Permits personalization/customization of Web Part properties to change appearance and behavior. Also permits users to delete Web Parts that have been added to the page dynamically.

ConnectDisplayMode

Permits users to connect Web Parts together at runtime.

CatalogDisplayMode

Permits users to add Web Parts into Web Part Zones at runtime.


One common approach to exposing the display mode to the client is to add one or more LinkButton controls to your portal page that indicate the customization modes. In the handler for each button, set the Web Part manager's DisplayMode property as appropriate. Of course, the interface is entirely up to you; all that matters is that you give users some way of transitioning among the modes you want to make available. It is also wise to disable links to modes that are not currently available, as the available modes may change based on whether a user is authenticated, whether there are Web Parts on the page that support links, and whether specific zone types are defined on the page.

You can query the Web Part manager to find out if a particular mode is supported by using the SupportedDisplayModes collection's Contains() method. This collection will always have the current set of modes that are supported based on client credentials and Web Part features. For our "minimal" portal site, we will provide just two LinkButtons for now. These let users select between Browse mode and Design mode, making sure to toggle the visibility of the Design mode button based on whether the mode is actually supported. Listings 6-5 and 6-6 show the additions to our page and codebehind file.

-5. LinkButtons added to portal page to change display mode

<asp:LinkButton runat="server" ID="_browseViewLinkButton"
        Text="Browse View" OnClick="_browseViewLinkButton_Click" />
    &nbsp;&nbsp;
<asp:LinkButton runat="server" ID="_designViewLinkButton"
        Text="Design View" OnClick="_designViewLinkButton_Click" />

-6. Codebehind logic for LinkButtons on portal page

public partial class MinimalPortalPage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        _designViewLinkButton.Visible =
            _webPartManager.SupportedDisplayModes.Contains(
                      WebPartManager.DesignDisplayMode);
    }
    protected void _browseViewLinkButton_Click(object src, EventArgs e)
    {
        _webPartManager.DisplayMode = WebPartManager.BrowseDisplayMode;
    }
    protected void _designViewLinkButton_Click(object src, EventArgs e)
    {
        _webPartManager.DisplayMode = WebPartManager.DesignDisplayMode;
    }
}

With our customization link in place, users can now click on the Design View link button and drag and drop the Web Parts on the page between zones,[1] as shown in Figure. Each time users move a Web Part from one zone to another, the customization data for that client is written to a data store, which we will describe in more detail shortly. Once a user has completed laying out the Web Parts on the page as she desires, she can then click the Browse View link button to transition back to normal viewing mode.

[1] The current release of ASP.NET 2.0 only supports visual drag and drop of Web Parts between zones using Internet Explorer 5.5 or higher. However, there is support in the Atlas framework (an add-on framework for ASP.NET 2.0) that provides drag and drop for the Mozilla Firefox browser as well.

Customizing Web Parts in design view


Catalog Parts and Zones

Currently the Web Parts available on our sample portal site are fixedthere is no way to add new Web Parts, and perhaps more importantly, there is no way to restore a Web Part a user closes using the Close item on the Web Part's menu. The solution to both of these problems is to include catalog parts on the page as well. Catalog parts provide the ability to select Web Parts for inclusion on the page, and they are contained in a catalog zone, much like Web Parts are contained in Web Part zones. Catalog parts become visible and active only when the page transitions into catalog display mode.

There are three types of catalog parts available: DeclarativeCatalogPart, PageCatalogPart, and ImportCatalogPart. The declarative catalog part contains a collection of Web Parts declared inside a WebPartsTemplate element, which will be made available to the user as available Web Parts when the page is in catalog display mode. The page catalog part is almost always one you want to include, as it will show all Web Parts that are no longer displayed (that is, have been closed) but are in the original page. This lets users recover parts that they have closed and want to reinstate in their page. Finally, the import catalog part provides the ability to import Web Part definitions complete with a set of property values and appearance attributes. We discuss the importing and exporting of Web Parts later in this section.

To add catalog parts to our sample portal site, we will begin by creating a new column in our table to house the catalog zone. Keep in mind that the catalog zone will not be visible until the user switches to the catalog display view, so it is common to place the catalog zone in a table's collapsible cell. Inside our catalog zone we will place an instance of the PageCatalogPart as well as a DeclarativeCatalogPart. Inside the WebPartsTemplate of the DeclarativeCatalogPart we add a Web Part declaration, in this case just our HelloWorldWebPart2 class. Finally, we need to add one more LinkButton to let users transition into catalog display mode. Listings 6-7 and 6-8 show the additions to our portal page and its codebehind file.

-7. Adding catalog support to a portal page (MinimalPortalpage.aspx)

<!-- This LinkButton is added immediately after the browse and design
     view buttons -->
<asp:LinkButton ID="_catalogViewLinkButton" runat="server"
        Text="Catalog View" OnClick="_catalogViewLinkButton_Click" />

<!-- This column is inserted immediately after the last
     WebPartZone column in our Figure>
<td>
  <asp:CatalogZone runat="server" ID="_catalogZone">
    <ZoneTemplate>
      <asp:PageCatalogPart ID="_pageCatalogPart" runat="server"
                           Title="Local Page Catalog" />
      <asp:DeclarativeCatalogPart ID="_declarativeCatalogPart"
           runat="server" Title="Minimal Portal Web Parts">
        <WebPartsTemplate>
          <eadn2:HelloWorldWebPart2 runat="server"
                                    ID="_helloWorldWebPart2" />
        </WebPartsTemplate>
      </asp:DeclarativeCatalogPart>
            </ZoneTemplate>
  </asp:CatalogZone>
</td>

-8. Adding catalog support to a portal page (MinimalPortalPage.aspx.cs)

protected void Page_Load(object sender, EventArgs e)
{
    //...
    _catalogViewLinkButton.Visible =
        _webPartManager.SupportedDisplayModes.Contains(
                  WebPartManager.CatalogDisplayMode);
}
protected void _catalogViewLinkButton_Click(object src, EventArgs e)
{
    _webPartManager.DisplayMode = WebPartManager.CatalogDisplayMode;
}

With our catalog parts in place and our new LinkButton wired up, the user can now switch the page into catalog view and select from among the various Web Parts stored in our declarative catalog. Also, if the user closes one of the existing Web Parts on the page, she can restore it by entering catalog view and adding it again from the local page catalog. Figure shows what the page looks like in catalog display mode assuming that the user has previously closed the Web Part in the left zone and has selected the Local Page Catalog link in the catalog zone.

Adding Web Parts using a catalog


Properties

In addition to adding, removing, and changing the layout of Web Parts on a page, users can also modify an individual Web Part's properties by placing the page into edit mode and using any edit parts available. By default, all Web Parts have a number of behavioral and appearance properties defined, which changes the features available for a given Web Part as well as how it looks on the page. Figure lists the set of behavioral and appearance properties available. In addition to all of these properties, the WebPart class inherits from WebControl, so all of the appearance properties defined there (like BackColor, BorderWidth, etc.) are also available to Web Parts.

WebPart properties

Behavioral Properties

Description

Default

AllowClose

Whether the client can close the Web Part.

True

AllowConnect

Whether the client can connect a Web Part to another (if supported).

True

AllowEdit

Whether the client can edit attributes of the Web Part.

True

AllowHide

Whether the client can hide a Web Part on a page.

True

AllowMinimize

Whether the client can minimize a Web Part on a page.

True

AllowZoneChange

Whether the client can move a Web Part from one zone to another.

True

AuthorizationFilter

String that can be used as a filter to determine whether a control can be added to a page.

ExportMode

Which properties (none, all, nonsensitive data) should be exported.

None

Appearance Properties

CatalogIconImageUrl

URL of image to use as icon in catalog.

ChromeState

Get or set whether a Web Part is in a minimized state.

Normal

ChromeType

Type of border framing a Web Part.

Default of zone (typically title and border)

Description

Text description for a Web Part shown in tooltips and catalog.

Height

Height of a control.

HelpUrl

URL to a help file for a control.

Hidden

Whether a Web Part is hidden or visible.

False

Title

Title text of a Web Part.

Untitled

TitleIconImageUrl

URL of image to use as icon in title bar of Web Part.

Width

Width of Web Part.


When you create Web Parts, it is common to initialize several of these inherited properties to customize your Web Part. At the very least, you should make the effort to set the Title and Description properties so that the control doesn't show up on the page as "Untitled." Listing 6-9 shows the HelloWorldWebPart control presented earlier with the addition of a constructor that initializes the Title, TitleIconImageUrl, BackColor, and BorderWidth properties to some meaningful defaults.

-9. Initializing Web Part properties

namespace EssentialAspDotNet2.WebParts
{
    public class HelloWorldWebPart : WebPart
    {
        public HelloWorldWebPart()
        {
            this.Title = "Hello World Web Part";
            this.TitleIconImageUrl = "~/images/star.gif";
            this.BackColor = Color.Beige;
            this.BorderWidth = 3;
        }
        //...
    }
}

Like all custom controls, Web Parts can expose their own properties as well. Unlike other controls, however, Web Parts have a built-in interface for letting clients modify their properties and the ability to store property values for a client between login sessions. In order to enable client-editing of properties, you must annotate your properties with the WebBrowsable and Personalizable attributes, indicating to the PropertyGridEditorPart that it is okay to let users modify and persist these properties respectively. Typically you set both of these attributes together, since if you don't enable the Personalizable attribute but do enable WebBrowsable, the client will be able to modify the property, but its value will not be persisted between sessions. There are two additional attributes that you will typically want to include as well on any exposed properties, WebDisplayName and WebDescription, which provide user-friendly strings describing the property.

Listing 6-10 shows our HelloWorldWebPart control with two new custom properties, each annotated with the four attributes to enable friendly editing, and an updated RenderContents method to reflect the value of those properties. Note that as with all control properties, the state is maintained in ViewState so that it persists across post-back requests.

-10. User-editable WebPart properties

namespace EssentialAspDotNet2.WebParts
{
  public class HelloWorldWebPart : WebPart
  {
    //...
    [WebBrowsable, Personalizable]
    [WebDisplayName("ShowTime")]
    [WebDescription("Display the current time")]
    public bool ShowTime
    {
      get { return (bool)(ViewState["showtime"] ?? false); }
      set { ViewState["showtime"] = value; }
    }

    public enum GreetingStyleEnum
           { Cordial, Informal, Friendly, Distant };

    [WebBrowsable, Personalizable]
    [WebDisplayName("GreetingStyle")]
    [WebDescription("Style for the greeting")]
    public GreetingStyleEnum GreetingStyle
    {
      get { return (GreetingStyleEnum)(ViewState["greetingstyle"] ??
                                       GreetingStyleEnum.Cordial); }
      set { ViewState["greetingstyle"] = value; }
    }

    protected override void RenderContents(HtmlTextWriter writer)
    {
      switch (GreetingStyle)
      {
        case GreetingStyleEnum.Cordial:
          writer.Write("Hello there, world.");
          break;
        case GreetingStyleEnum.Distant:
          writer.Write("hi");
          break;
        case GreetingStyleEnum.Friendly:
          writer.Write("Well hello there, world!");
          break;
        case GreetingStyleEnum.Informal:
          writer.Write("Whassup world??");
          break;
      }
      if (ShowTime)
        writer.Write("<br />{0}", DateTime.Now.ToShortTimeString());

      base.RenderContents(writer);
    }
  }
}

Editor Parts and Zones

Now that our Web Part has custom properties to expose, we need to give the user the ability to modify those properties using the EditorZone and one or more editor parts. Much like the catalog zone, the editor zone is displayed only when the page is transitioned into edit view mode. Also, the editor zone will never be displayed at the same time as the catalog mode, so it is common practice to place the catalog and editor zones at the same location on the page. Once you have the editor zone in place, there are four different editor parts available that can be placed into the zone, as described in Figure. Listings 6-11 and 6-12 show our minimal portal page augmented with an EditorZone containing an instance of each of the three core editor parts (we will discuss the BehaviorEditorPart shortly), along with a new LinkButton giving the user the ability to enter edit mode.

Editor parts available for placement in an EditorZone

Editor Parts

Description

AppearanceEditorPart

Editor for inherited properties of WebParts including Title, ChromeType, Direction, Height, Width, and Hidden.

BehaviorEditorPart

Editor for inherited properties of WebParts including Description, TitleUrl, TitleIconImageUrl, CatalogIconImageUrl, HelpUrl, HelpMode, ImportErrorMessage, ExportMode, AuthorizationFilter, AllowClose, AllowConnect, AllowEdit, AllowHide, AllowMinimize, AllowZoneChange. This part is only visible in shared scope.

LayoutEditorPart

Editor for inherited properties of WebParts including ChromeState, Zone, and ZoneIndex.

PropertyGridEditorPart

Editor for custom properties exposed by a WebPart that are annotated with the WebBrowsable attribute.


-11. Adding editing support to a portal page (MinimalPortalPage.aspx)

<asp:LinkButton ID="_editViewLinkButton" runat="server"
                Text="Edit View" OnClick="_editViewLinkButton_Click" />
<!--... -->

<asp:EditorZone ID="_editorZone" runat="server">
  <ZoneTemplate>
    <asp:AppearanceEditorPart ID="_appEditorPart" runat="server" />
    <asp:PropertyGridEditorPart ID="_propEditorPart" runat="server" />
    <asp:LayoutEditorPart ID="_layoutEditorPart" runat="server" />
  </ZoneTemplate>
</asp:EditorZone>

-12. Adding editing support to a portal page (MinimalPortalPage.aspx.cs)

protected void _editViewLinkButton_Click(object sender, EventArgs e)
{
    _webPartManager.DisplayMode = WebPartManager.EditDisplayMode;
}

With these additions in place, the user can now click on the Edit View link to enable the edit zone and its parts. Once in edit view, the Edit verb will appear on each Web Part's individual menu, which when selected will display the editor parts in the editor zone for that Web Part. All changes made to properties of Web Parts on the page are persisted to the client's repository and will be restored the next time he visits the portal site. Figure shows what the portal page looks like when in EditDisplayMode with all three core edit parts available. Note that the PropertyGridEditorPart is smart enough to render CheckBox controls to edit Boolean values and DropDownList controls to edit enumerations even for custom properties.

Editing Web Part properties using the editor zone


Verbs

In addition to adding custom properties to your Web Parts, you can also define custom "verbs." A Web Part verb is an action that a client can perform on the Web Part by selecting a menu item on the individual Web Part's drop-down verbs menu in its title bar. There are four standard verbsopen, close, minimize, and editthat show up on the menu in various modes of operation. To augment this standard set of verbs, you need to override the Verbs property collection and return a new WebPartVerbCollection class populated with the additional verbs you would like displayed. Each verb can have an associated string and image, and must be initialized with a delegate pointing to a method which will be invoked when the verb is selected from the menu. By default, all custom verbs that you add to your control are visible in all modes of editing. Listing 6-13 shows an example of implementing a custom verb in our HelloWorldWebPart for sending an e-mail, and Figure shows our new Web Part with its mail verb in place on our portal page.

-13. Adding a custom verb to a Web Part

namespace EssentialAspDotNet2.WebParts
{
  public class HelloWorldWebPart : WebPart
  {
    //...
    public override WebPartVerbCollection Verbs
    {
      get
      {
        WebPartVerb verbMail =
                 new WebPartVerb("verbMail", MailVerbHandler);
        verbMail.Text = "Email message";
        verbMail.Description = "Send message as email";
        verbMail.ImageUrl = "~/images/mail.gif";

        WebPartVerbCollection ret =
          new WebPartVerbCollection(new WebPartVerb[] {verbMail});
        return ret;
      }
    }

    public void MailVerbHandler(object src, WebPartEventArgs e)
    {
      // TODO - send hello world email message
    }
  }
}

Custom verb in a Web Part


Connections

Web Parts also support the concept of connections, which define a way of sharing information through a common interface between two or more Web Parts. There are several potential applications for defining connections between Web Parts, including the ability to share preference data among many controls, as well as creating master-detail relationships between Web Parts. Consider a portal page where one Web Part collects a user's zip code as one of its properties, for example. Other Web Parts on the page, like a weather forecaster, could tap into the first Web Part's data to have one central location to retrieve (and perhaps update) the information.

To create a connection between two Web Parts, you must first define a common interface that will determine what properties can be shared. For example, if we wanted to share a string message, we could define a simple interface IMessage with a single string property, as shown in Listing 6-14.

-14. IMessage interface for sharing a string through connections

namespace EssentialAspDotNet2.WebParts
{
    public interface IMessage
    {
        string Message { get; }
    }
}

Next, you add support to a Web Part for being a provider of data using this interface by adding a method to the Web Part that returns a reference to an object implementing the interface. This method must then be annotated with the ConnectionProvider attribute with a friendly name describing the provider. It is usually reasonable to implement the interface on the Web Part itself, and then just return the Web Part reference in the method implementation. You also must decide where the data for the connection comes from. For our example, we will create a TextBox as a child control to let the user input the message. Listing 6-15 shows a sample provider Web Part that implements IMessage and then exposes it as a connection using the ConnectionProvider attribute. It also provides the user with a TextBox to enter the message and a Button to set it.

-15. Provider Web Part implementing IMessage

namespace EssentialAspDotNet2.WebParts
{
  public class MessageProvider : WebPart, IMessage
  {
    private TextBox _messageTextBox;

    // IMessage property implementation
    public string Message
    {
      get { return _messageTextBox.Text;  }
    }

    [ConnectionProvider("MessageProvider")]
    public IMessage GetMessageProvider()
    {
      return this;
    }

    protected override void CreateChildControls()
    {
      _messageTextBox = new TextBox();

      Controls.Add(new LiteralControl("Enter text for message: "));
      Controls.Add(_messageTextBox);
      Button submitButton = new Button();
      submitButton.Text = "Submit message";
      Controls.Add(submitButton);

      base.CreateChildControls();
    }
    //...
  }
}

To make this provider Web Part useful, you must now define one or more Web Parts that are consumers of the IMessage interface. Consumers are a bit simpler, as they need only to define a single method that takes a reference to the interface that is annotated with the ConnectionConsumer attribute. Typically the Web Part will keep a reference to the interface so that it can pull data from the interface when the Web Part needs to for rendering. Listing 6-16 shows a sample consumer Web Part to the IMessage interface that displays the message sent by IMessage when it is connected to a producer, or a message indicating that it is not connected otherwise.

-16. Consumer Web Part consuming IMessage

namespace EssentialAspDotNet2.WebParts
{
  public class MessageConsumer : WebPart
  {
    private IMessage _provider;

    [ConnectionConsumer("MessageConsumer")]
    public void RegisterMessageProvider(IMessage provider)
    {
      _provider = provider;
    }

    protected override void RenderContents(HtmlTextWriter writer)
    {
      if (_provider != null)
        writer.Write("Message from provider: " +
                       _provider.Message);
      else
        writer.Write("No message provider attached");

        base.RenderContents(writer);
    }
  }
}

Now all that's left to do is to connect the two Web Parts. You can either connect the Web Parts dynamically using the connections zone or statically using the ProxyWebPartManager. Like the other modes of operation, connection mode is entered by setting the WebPartManager display mode and by defining a connection zone on the page. Like the catalog and editor zones, the connection zone only appears when activated, so it is often placed adjacent to these other two zones with the knowledge that only one will ever display at a time. Listings 6-17 and 6-18 show our MinimalPortalPage updated to support connection mode with a new LinkButton and a ConnectionZone.

-17. Adding dynamic connection mode support to a portal page (MinimalPortalPage.aspx)

<asp:LinkButton ID="_connectViewLinkButton" runat="server"
      Text="Connect View" OnClick="_connectViewLinkButton_Click" />
<!-- ... -->
<asp:ConnectionsZone ID="_connectionsZone" runat="server" />

-18. Adding dynamic connection mode support to a portal page (MinimalPortalPage.aspx.cs)

protected void _connectViewLinkButton_Click(object src, EventArgs e)
{
    _webPartManager.DisplayMode = WebPartManager.ConnectDisplayMode;
}

When the user now enters connect display mode, the connect verb appears on all Web Parts that have connections available. By selecting the connect verb of the producer Web Part, the client is presented with an interface for creating consumer connections for the Web Part, as shown in Figure. Alternatively, if the client selects the connect verb of a consumer Web Part, she is presented with an interface for creating producer connections.

Connecting producer Web Parts to consumer Web Parts


You can also set up connections between Web Parts declaratively using the ProxyWebPartManager control. This control has a StaticConnections subelement where you can list WebPartConnections to specify the association between Web Parts on your page. This is a top-level control, like the WebPartManager, and is typically placed near the top of the page. Listing 6-19 shows an example of specifying a connection between our consumer and provider Web Parts so that as soon as the page is run, the connection exists without any intervention by the client.

-19. Declaratively specifying static connections between Web Parts

<asp:ProxyWebPartManager runat="server" ID="_proxyWPM">
  <StaticConnections>
    <asp:WebPartConnection  ID="_wpc"
         ConsumerID="_messageConsumer"
         ProviderID="_messageProvider" />
  </StaticConnections>
</asp:ProxyWebPartManager>

Personalization Scope

All of the personalization we have seen so far has been at the "user" scope, meaning property values and layout information was saved on behalf of individual users. Web Parts also support the concept of "shared" scope, where any changes made to property values or layout information of a page is saved on behalf of all users. When changes are made to shared scope, it changes the default values that all users have for a page. If a user already has her own custom settings that conflict with a setting in shared scope, her value will still be used.

The idea behind shared scope is for some designated users to be given the authority to modify settings on behalf of all users, so you cannot enter shared scope unless you have explicitly granted permission to a user or a group to which a user belongs. To grant permission, you must add an allow element under the authorization element for personalization that grants access to the enterSharedScope verb to your configuration file. Listing 6-20 shows an example of granting permissions to enter shared scope to users who belong to the admin role and to the user named bob.

-20. Granting permission for users to enter shared scope (web.config)

<configuration>
  <system.web>
    <!-- ... -->
    <webParts>
      <personalization>
         <authorization>
            <allow roles="admin" users="bob" verbs="enterSharedScope" />
         </authorization>
      </personalization>
    </webParts>
  <system.web>
<configuration>

To actually enter shared scope, you call the ToggleScope() method on the Personalization property of the Web Part manager. You can also query to find out if the current user has the authority to enter shared scope by looking at the CanEnterSharedScope property of the Personalization property. Listings 6-21 and 6-22 show the addition of a LinkButton to our minimal portal page that allows the user to toggle the scope from user to shared if he has the permissions to do so. Note that if the user does not have permissions, we hide the button altogether.

-21. Adding support for toggling the scope(MinimalPortalPage.aspx)

<asp:LinkButton ID="_toggleScopeLinkButton" runat="server"
      Text="Toggle Scope" OnClick="_toggleScopeLinkButton_Click" />&nbsp;
<asp:Label runat="server" ID="_currentScopeLabel" Text="(scope=user)" />

-22. Adding support for toggling the scope(MinimalPortalPage.aspx.cs)

public partial class MinimalPortalPage : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        _currentScopeLabel.Text = "(scope=" +
                 _webPartManager.Personalization.Scope.ToString() + ")";

        if (!_webPartManager.Personalization.CanEnterSharedScope)
        {
            _currentScopeLabel.Visible = false;
            _toggleScopeLinkButton.Visible = false;
        }
  //...
}

When a page is running in shared scope, it also enables the BehaviorEditorPart if you have placed one in your editor zone. This allows an administrator to set not only the appearance and layout properties of Web Parts on the page, but also to set behavioral elements of each part, like whether it can be hidden, minimized, or closed. Figure shows the BehaviorEditorPart as it is displayed in the editor zone when a user in shared scope is editing the properties of a Web Part.

BehaviorEditorPart appearance for modifying behavioral properties of Web Parts when in shared scope


Exporting and Importing Web Parts

As clients interact with your portal site, they may find it useful to be able to export Web Parts that they have customized on your site for later importing. For example, a client may have customized a Web Part to her liking and want to share the customizations she has made with a colleague. By exporting the Web Part to a file, she could then share the customizations by handing the exported Web Part file to her colleague, who could then import the Web Part. The Web Part infrastructure in ASP.NET 2.0 supports the ability to export and import arbitrary Web Parts through an XML-formatted file with the extension .webpart. Note that for a Web Part to be imported, the Web Part definition must be available in the sitethis is not a mechanism for sharing Web Part implementations between sites.

To enable the ability to export and import Web Parts, you first need to enable the capability in your web.config file by setting the enableExport attribute of webParts to true, as shown in Listing 6-23.

-23. Enabling Web Parts exporting and importing in web.config

<configuration>
  <system.web>
    <webParts enableExport="true" />
    <!-- ... -->
  </system.web>
</configuration>

Next, you need to explicitly allow the importing and exporting of each Web Part in your site by setting the ExportMode property to All or NonSensitiveData (it defaults to None). This can be done programmatically or declaratively, but it typically will make sense for the builder of the Web Part to decide whether it should be exportable, and whether any of the data is sensitive so that it should never be exported. You indicate whether a particular property of your Web Part is sensitive by specifying a second parameter to the Personalizable attributetrue for sensitive and false for nonsensitive. The default is nonsensitive, so if your control uses the NonSensitiveData setting, you want to be sure to go through your control's properties and mark those with potentially sensitive data as such. Listing 6-24 shows our HelloWorldWebPart initializing its ExportMode to NonSensitiveData, and introducing a new property, EmailAddress, which is marked as sensitive. Note that our other properties are left with their default values, and so they will be assumed safe for export.

-24. Enabling exporting of Web Part properties

namespace EssentialAspDotNet2.WebParts
{
  public class HelloWorldWebPart : WebPart
  {
    public HelloWorldWebPart()
    {
      //...
      this.ExportMode = WebPartExportMode.NonSensitiveData;
    }
    [WebBrowsable, Personalizable(PersonalizationScope.User, true)]
    [WebDisplayName("EmailAddress")]
    [WebDescription("Email address to use for emailing message")]
    public string EmailAddress
    {
      get { return (string)(ViewState["emailaddress"] ?? ""); }
      set { ViewState["emailaddress"] = value; }
    }
    //...
  }
}

Once the Web Parts you want to be exportable are marked properly, and exporting is enabled in your web.config file, a new verb will appear on exportable Web Parts' menusExport. When the user selects the verb, she will be prompted to save a file with an extension of .webpart that contains an XML description of all the properties for that Web Part. Figure shows the interface presented to the user for exporting, and Listing 6-25 shows a sample .webpart file with exported Web Part property values.

Exporting a Web Part


-25. Sample .webpart file with exported Web Part property values

<?xml version="1.0" encoding="utf-8"?>
<webParts>
  <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
    <metaData>
      <type name="EssentialAspDotNet2.WebParts.HelloWorldWebPart" />
      <importErrorMessage>Cannot import this Web
                          Part.</importErrorMessage>
    </metaData>
    <data>
      <properties>
        <property name="AllowClose" type="bool">True</property>
        <property name="Width" type="unit" />
        <property name="AllowMinimize" type="bool">True</property>
        <property name="GreetingStyle" type="EssentialAspDotNet2.WebParts
.HelloWorldWebPart+GreetingStyleEnum, App_Code.11k7nmku, Version=0.0.0.0, Culture=neutral,
 PublicKeyToken=null">Informal</property>
        <property name="AllowConnect" type="bool">True</property>
        <property name="ChromeType" type="chrometype">Default</property>
        <property name="TitleIconImageUrl"
                  type="string">~/images/star.gif</property>
        <property name="Description" type="string" />
        <property name="Hidden" type="bool">False</property>
        <property name="TitleUrl" type="string" />
        <property name="AllowEdit" type="bool">True</property>
        <property name="AllowZoneChange" type="bool">True</property>
        <property name="Height" type="unit" />
        <property name="HelpUrl" type="string" />
        <property name="Title" type="string">
                  Hello World Web Part</property>
        <property name="CatalogIconImageUrl" type="string" />
        <property name="Direction" type="direction">NotSet</property>
        <property name="ChromeState"
                  type="chromestate">Normal</property>
        <property name="ShowTime" type="bool">True</property>
        <property name="AllowHide" type="bool">True</property>
        <property name="HelpMode" type="helpmode">Navigate</property>
        <property name="ExportMode"
                  type="exportmode">NonSensitiveData</property>
      </properties>
    </data>
  </webPart>
</webParts>

To enable the importing of Web Parts, you need to add an instance of the ImportCatalogPart to your catalog zone. This part provides a standard interface for uploading .webpart description files from the client's machine, and will take care of creating and initializing a new instance of the imported Web Part, setting all of the property values as specified in the import file. Listing 6-26 shows the CatalogZone on our minimal portal site augmented with an ImportCatalogPart. The user can access the import interface by switching the portal page into catalog view and selecting the import catalog link within the catalog zone, as shown in Figure.

Figure. Interface for importing Web Parts from .webpart files


-26. Adding an import catalog to a portal page

<!-- ... -->
<asp:CatalogZone runat="server" ID="_catalogZone">
  <ZoneTemplate>
    <!-- ... -->
    <asp:ImportCatalogPart ID="_importCatalogPart"
          runat="server" Title="Sample ImportCatalogPart"
          BrowseHelpText="Type a path or browse to find a control's
description file."
          UploadButtonText="Upload Description File"
          UploadHelpText="Click the button to upload the description
file."
           ImportedPartLabelText="My User Information WebPart"
           PartImportErrorLabelText="An error occurred while trying to
import a description file."  />
  </ZoneTemplate>
</asp:CatalogZone>

If you want even more control over adding Web Parts to your site, you can always resort to adding Web Parts programmatically to a zone. The Web Part manager supplies a method called AddWebPart, which takes the zone into which the Web Part is to be placed, the instance of the WebPart-derived class, and the index in the zone (its order is relative to other Web Parts). Listing 6-27 shows an example of adding an instance of our HelloWorldWebPart class to the top of the right zone in our portal page.

-27. Adding a Web Part programmatically

HelloWorldWebPart wp = new HelloWorldWebPart();
_webPartManager.AddWebPart(wp, _rightWebPartZone, 0);

Formatting Web Parts and Zones

All of the examples in this chapter have been intentionally devoid of styles in order to keep them short. It is important to be aware, however, that all of the Web Part components have a myriad of style attributes that can be altered to change the look and feel of your portal site. As an example, Listing 6-28 shows a .skin file containing a WebPartZone definition populated with a number of styles, and Figure shows the changes in appearance when this theme is applied. The .skin file and accompanying .css files are available in their entirety with the samples you can download with this book.

-28. Sample styles applied to a WebPartZone control in a .skin file

<asp:WebPartZone Runat="server" Height="100%" Width="100%"
                 PartChromeType="TitleAndBorder"
                 DragHighlightColor="255, 255, 128">
    <HeaderStyle CssClass="ZoneHeader" />
    <PartTitleStyle CssClass="WebPartTitle"  />
    <PartStyle CssClass="WebPart" CellSpacing=5  />
    <PartChromeStyle BorderWidth="1px" BackColor="#FFFFC0" />
    <EmptyZoneTextStyle CssClass="EmptyZone" />
    <MenuLabelHoverStyle BackColor="#EBFEFE" />
    <MenuLabelStyle BorderColor="Transparent" BackColor="#EBFEFE" />
    <MenuPopupStyle BackColor="#C4FAFB" BorderColor="#5072CB"
                    ShadowColor="#284286" BorderStyle="Solid"
                    BorderWidth="1px" GridLines="Horizontal"
                    Font-Names="Tahoma" Font-Size="9pt" />
    <MinimizeVerb ImageUrl="img/MinimizeVerb.gif"/>
    <RestoreVerb ImageUrl="img/RestoreVerb.GIF"/>
    <CloseVerb ImageUrl="img/PTCLOSE.GIF"/>
    <DeleteVerb ImageUrl="img/DELETE.GIF" />
    <EditVerb ImageUrl="img/EditVerb.gif"/>
</asp:WebPartZone>

Portal appearance with WebPartZone styles applied


User Controls as Web Parts

So far we have looked at creating Web Parts only as custom classes, but there are actually several other options that may often be a better choice. In general, any control can be used directly as a Web Part, without requiring modifications or wrapping by the developer. This means that you could do something as simple as drag an instance of the TextBox control onto a Web Part zone in your page, and it would be treated as a separate Web Part. Single-control Web Parts aren't generally that useful, but where this feature shines is when you take a User Control and use it directly as a Web Part.

The way this works internally is if a standard control (non-Web Part) is added to a Web Part Zone, an implicit call to WebPartManager.CreateWebPart is made, which allocates an instance of the GenericWebPart class and initializes it with the control that was added. The GenericWebPart class derives from the WebPart base class, providing implementations of the core Web Part properties, and when it is constructed, it adds the control it was initialized with as a child control. During rendering, the GenericWebPart renders nothing to the response buffer itself, and simply delegates rendering to its child control, as do most composite controls. The end result is that you can add any control you like to a Web Part Zone on a page and it "just works." For example, the page in Listing 6-29 defines a WebPartZone with a User Control and a standard Calendar control, both of which will be implicitly wrapped by the GenericWebPart class at creation time.

-29. Using controls as Web Parts

<%@ Register Src="webparts/CustomerList.ascx"
    TagName="CustomerList" TagPrefix="eadn" %>

<asp:WebPartManager ID=" _webPartManager" runat="server" />

<asp:WebPartZone ID="_webPartZone" runat="server" HeaderText="Zone 1">
  <ZoneTemplate>
    <eadn:CustomerList runat="server" id="_customerList"  />
    <asp:Calendar runat="server" id="_customerCalendar" />
  </ZoneTemplate>
</asp:WebPartZone>

As with standard Web Parts, it is possible to dynamically create controls wrapped by the GenericWebPart. If it is a User Control, you must first dynamically load and create the User Control instance with a call to Page.LoadControl. Second, you must explicitly assign a unique ID to the control. Third, you must call the WebPartManager object's CreateWebPart method to create an instance of the GenericWebPart class, which then acts as a wrapper around the User Control instance. Finally, you need to take the GenericWebPart reference returned from the call to CreateWebPart and pass it into a call to AddWebPart, specifying the Zone it should become a part of. These steps are shown in Listing 6-30.

-30. Adding a User Control-based Web Part programmatically

// create Web Part instance from User Control file
Control uc = this.LoadControl(@"webparts\CompanyNews.ascx");
uc.ID = "_wp2";
GenericWebPart wp2 = WebPartManager1.CreateWebPart(uc);
WebPartManager1.AddWebPart(wp2, WebPartZone1, 1);

The only disadvantage to this technique is that you lose the opportunity to control the Web Part-specific features of the control, because the GenericWebPart class is the one that inherits from WebPart, not your control. This will become obvious as soon as you run a page with controls wrapped by GenericWebPart, as they default to "Untitled" for their titles and have no icons or descriptions associated with them, which most Web Parts do.

One way to work around this problem is to add a handler for the Init event of your User Control, and if you are currently being wrapped by a GenericWebPart (which you can tell by querying the type of your Parent property), set the attributes of the GenericWebPart class, as shown in Listing 6-31.

-31. Setting attributes in the containing GenericWebPart of a UserControl

void Page_Init(object src, EventArgs e)
{
  GenericWebPart gwp = Parent as GenericWebPart;
  if (gwp != null)
  {
    gwp.Title = "My custom user control";
    gwp.TitleIconImageUrl = @"~\img\ALLUSR.GIF";
    gwp.CatalogIconImageUrl = @"~\img\ALLUSR.GIF";
  }
}

When you run the page again, as long as the User Control is being wrapped by GenericWebPart, the changes you made to the properties of the GenericWebPart parent will be reflected in the rendering of the Web Part containing your control.

There is one other solution that is even more compelling: to implement the IWebPart interface directly on your User Control class. This doesn't seem like it should help, since the User Control is never queried directly by the Web Part infrastructure for Web Part properties as those details are handled by the GenericWebPart class. Fortunately, the designers of the GenericWebPart class anticipated this need, and implemented the properties in the GenericWebPart class to automatically delegate to the wrapped control if that control implements the IWebPart interface.

So customizing the WebPart features of a User Control is just a matter of implementing the IWebPart interface and filling out the seven properties that it defines. Listing 6-32 shows an example of the codebehind class for a User Control that achieves the same results as we did before by dynamically altering the GenericWebPart properties.

-32. Implementing IWebPart

public partial class HelloWorld2 : UserControl, IWebPart
{
  protected string _title = "My custom user control";
  public string Title {
        get { return _title; }
        set { _title = value; }
  }

  private string _titleIconImageUrl = "~/img/ALLUSR.GIF";
  public string TitleIconImageUrl {
    get { return _titleIconImageUrl; }
    set { _titleIconImageUrl = value; }
  }

  private string _catalogIconImageUrl = "~/img/ALLUSR.GIF";
  public string CatalogIconImageUrl {
    get { return _catalogIconImageUrl; }
    set { _catalogIconImageUrl = value; }
  }

  // Remaining properties not shown...
}

You might even consider creating an alternative base class for your User Controls that implements IWebPart once and can then be inherited by all of the User Controls in your portal. With this solution, your User Controls can initialize the properties they care about in their constructors, and the rest takes care of itself. Listing 6-33 is a sample alternative base class for User Controls that implements IWebPart, and a corresponding codebehind class for a User Control that uses the class to set its title and icon properties.

-33. Creating a common UserControl base class that implements IWebPart

public class WebPartBase : UserControl, IWebPart
{

  protected string _title = "[Generic Title]";
  public string Title {
    get { return _title; }
    set { _title = value; }
  }

  private string _titleIconImageUrl = "~/img/star.GIF";
  public string TitleIconImageUrl {
    get { return _titleIconImageUrl; }
    set { _titleIconImageUrl = value; }
  }

  private string _catalogIconImageUrl = "~/img/star.GIF";
  public string CatalogIconImageUrl {
    get { return _catalogIconImageUrl; }
    set { _catalogIconImageUrl = value; }
  }

  // Remaining properties not shown...
}

// Codebehind class for CustomerList.ascx User Control
public partial class CustomerList : WebPartBase {
  public CustomerList() {
    this.Title = "Sample Customer List";
    this.TitleIconImageUrl = @"~\img\ALLUSR.GIF";
    this.CatalogIconImageUrl = @"~\img\ALLUSR.GIF";
  }
}

If you want your User Control-based Web Part to expose custom verbs as well, you can implement another interface: IWebActionable. This simple interface has just one read-only property, Verbs, which you can implement to return custom verbs as shown earlier in this chapter.

Now that we have so much flexibility with User Controls, you may very well ask the question, "Why would I ever want to create a custom control when I can have designer support with User Controls and still customize the WebPart features?" There are several reasons, actually, starting with the fact that User Controls are intrinsically scoped to the application directorythat is, it isn't possible to share a User Control implementation across multiple Web applications without physically copying the .ascx file from one project to another. Custom Web Parts that derive from the WebPart class, on the other hand, can be compiled into a reusable DLL and deployed globally in the global assembly cache (GAC). Also, with a custom WebPart you have the option of writing a custom designer for your control to change its default appearance in Visual Studio, and you can create an icon to associate with the WebPart when it is dropped onto the toolbox. Figure shows a comparison of features for help when deciding between custom WebParts and UserControls for your WebPart components.

Custom WebPart controls versus UserControls

Feature

WebPart-Inherited Class

User Control

Can be built in a separate DLL and installed in the GAC to reuse across applications

Yes

No

Can be added to Visual Studio toolbox with a fancy icon

Yes

No

Visual Designer support

No

Yes


Personalization Data and Providers

Like the Membership and Profile features of ASP.NET, the backend persistence mechanism for Web Parts is defined using a provider. By default, this provider maps onto a local SQL Server 2006 Express database in your /App_Data directory, but it can be changed to point to a completely different database, or even a completely different back-end medium with another provider.

The personalization provider is responsible for all of the data-related tasks dealing with Web Parts, including the following.

Responsibilities of the personalization provider

For

The Personalization Provider

A particular page and a particular user

Saves the Web Part properties and layout

A particular page and a particular user

Loads the Web Part properties and layout

A particular page

Saves the general Web Part properties and layout (for general customization)

A particular page

Loads the general Web Part properties and layout (for general customization)

A particular page and a particular user

Resets the Web Part properties and layout to their defaults

A particular page

Resets the Web Part properties and layout to their defaults (for general customization)


There are some other ancillary features that are part of the personalization infrastructure that need persistence capabilities too, but it basically boils down to these six capabilities. If we assume that there is a class capable of performing these six actions and successfully saving and restoring the data, then the WebPartManager on each page can use that class to save and restore all personalization and customization data as the site runs. The abstract class that defines these methods is called PersonalizationProvider, and the one concrete derivative of this class that is used by default is the SqlPersonalizationProvider. Listing 6-34 shows the three methods that represent these six pieces of functionality. Note that each method is capable of working with either user personalization or general customization based on whether the incoming userName parameter is null or not.

-34. PersonalizationProvider class

public abstract class PersonalizationProvider : ProviderBase {
    protected abstract void LoadPersonalizationBlobs(
                              WebPartManager webPartManager,
                              string path, string userName,
                              ref byte[] sharedDataBlob,
                              ref byte[] userDataBlob);
    protected abstract void ResetPersonalizationBlob(
                              WebPartManager webPartManager,
                              string path, string userName);
    protected abstract void SavePersonalizationBlob(
                              WebPartManager webPartManager,
                              string path, string userName,
                              byte[] dataBlob);

    // remaining methods not shown
}

Note that all of the personalization data is stored as straight binary data (byte[]), which in the default SqlPersonalizationProvider is written to an image field in the database. Since ASP.NET 2.0 knows that there is a class that provides these methods, it can build much more logic into its base set of controls than would otherwise be possible. In our case, the WebPartManager in each page that uses WebParts is responsible for making the right calls to the current PersonalizationProvider class to serialize and restore the personalization settings for each page. Figure shows the interaction between the EditorZone control and the default SqlPersonalizationProvider.

Interaction between the EditorZone control and the SqlPersonalizationProvider


Changing the Personalization Data Store

As with most of the providers in ASP.NET 2.0, the default provider for personalization is implemented to target a SQL backend. If you make no changes to the configuration files, the default SqlPersonalizationProvider uses a connection string for SQL Server 2005 Express, which supports a local file-based database. The connection string looks like this:

data source=.\SQLEXPRESS;Integrated Security=SSPI;
  AttachDBFilename=|DataDirectory|aspnetdb.mdf;
  User Instance=true

The advantage to using a SQL Server 2005 Express file-based database is that it can be created on the fly without any additional setup by the user. This means that you can create a brand new site, start using the personalization features without setting up any database, and it just works! When you first interact with the site, it will generate a new aspnetdb.mdf file in your site's App_Data directory, and initialize it with the tables and stored procedures necessary to support all of the default providers.

This is great for small sites that don't need to scale or support many concurrent users, but for enterprise systems it will be necessary to store the data in a fully administered, dedicated database server. Fortunately, changing the database used by the SqlPersonalizationProvider is quite straightforward. The SqlPersonalizationProvider configuration initializes the connection string to LocalSqlServer, which means that it looks for an entry in the <connectionStrings> section of the configuration file with that name, and uses the associated connection string to open a connection to the database. By default, this string is the connection string just shown, meaning that it will write to a local SQL Server 2005 express .mdf file. To change this, you must first clear the LocalSqlServer connection string collection and then reassign a new connection string value in your web.config file (alternatively, you could change this in your machine-wide machine.config file to affect all sites on that machine). Listing 6-35 shows a sample web.config file that will change the provider database to point to a local SQL Server 2000 instance.

-35. Changing the provider database

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
  <connectionStrings>
    <remove name="LocalSqlServer" />
    <add name="LocalSqlServer"
       connectionString="server=.;integrated security=sspi;database=aspnetdb"/>
  </connectionStrings>
  <!-- ... -->
</configuration>

Before this change will work, there of course must be a database named ASPNETDB on the local server with the necessary tables and stored procedures required by the SqlPersonalizationProvider. To create this database, there is a utility that ships with ASP.NET 2.0 called aspnet_regsql.exe. When run with the default settings, it will create a database locally called ASPNETDB with the necessary tables for all of the providers. Alternatively, you can choose to install the tables and stored procedures into an existing database. The table and stored procedure names are all prefixed with aspnet, so it is unlikely that they will clash with any existing tables.

As with all providers in ASP.NET 2.0, this level of indirection creates a very flexible architecture where the back-end data store can be completely changed without any modification to the pages or Web Parts contained within.

Creating Your Own Personalization Provider

The ability to change the connection string for the personalization provider gives you a certain amount of flexibility, but under the covers the SqlPersonalizationProvider uses the System.Data.Sql.Client namespace to perform its data retrieval. This means that it is limited to a SQL Server database. If you need to store your personalization in a different database, or perhaps in a completely different data store altogether, you'll have to take the next step and build your own custom personalization provider. Fortunately most of the hard work is already done for you and is easily leveraged. As an example of writing personalization data to a different data store, the samples available for this book have a custom personalization provider called FileBasedPersonalizationProvider that persists all personalization and customization data to local binary files in the application's App_Data directory. The binary file names are generated uniquely for each user and path, and there is one file per unique path for general user settings.

To build a custom personalization provider, you must first create a new class that inherits from the PersonalizationProvider base class and then override all of the abstract methods inherited from the base class. The class declaration in Listing 6-36 demonstrates how to do this.

-36. FileBasedPersonalizationProvider

namespace EssentialAspDotNet2.Providers {
  public class FileBasedPersonalizationProvider : PersonalizationProvider
  {
        public override string ApplicationName
        {
            get { /* todo */ }
            set { /* todo */ }
        }
        public override void Initialize(string name, NameValueCollection
configSettings)
        { /* todo */ }
        public override int GetCountOfState(PersonalizationScope scope,
                            PersonalizationStateQuery query)
        { /* todo */ }
        public override int ResetUserState(string path,
                             DateTime userInactiveSinceDate)
        { /* todo */ }
        protected override void LoadPersonalizationBlobs(
                       WebPartManager webPartManager, string path,
                       string userName, ref byte[] sharedDataBlob,
                       ref byte[] userDataBlob)
        { /* todo */ }
        protected override void ResetPersonalizationBlob(
                    WebPartManager webPartManager, string path,
                    string userName)
        { /* todo */ }
        public override int ResetState(PersonalizationScope scope,
                    string[] paths, string[] usernames)
        { /* todo */ }
        protected override void SavePersonalizationBlob(
                   WebPartManager webPartManager, string path,
                   string userName, byte[] dataBlob)
        { /* todo */ }

        public override PersonalizationStateInfoCollection FindState(
               PersonalizationScope scope,
               PersonalizationStateQuery query, int pageIndex,
               int pageSize, out int totalRecords)
        { /* todo */ }
   }
}

There are really only two significant methods that must be implemented for your personalization provider to begin workingLoadPersonalizationBlobs and SavePersonalizationBlob. These two methods represent the binary serialization of personalization data and are called by the personalization infrastructure to retrieve data when a page is loading, and to write the data back out (typically on behalf of a particular user) when data is changed in edit, catalog, or design view on a page with Web Parts. In the sample you can download that is associated with this book, the implementation of SavePersonalizationBlob writes the dataBlob parameter to a uniquely named file based on the userName and path that are passed in. Similarly, the LoadPersonalizationBlobs implementation looks for the file (using the same naming scheme) and returns either a user data blob or a shared data blob. Both of these two methods default to saving or loading shared data if the incoming userName parameter is null; otherwise, they save or load user data. The implementation of each of these methods in the sample FileBasedPersonalizationProvider is shown in Listing 6-37, along with a pair of helper methods to generate unique filenames based on user names and path information.

-37. Implementation of LoadPersonalizationBlobs and SavePersonalizationBlob

protected override void LoadPersonalizationBlobs(
                   WebPartManager webPartManager,
                   string path, string userName,
                   ref byte[] sharedDataBlob, ref byte[] userDataBlob)
{
  string fileName;
  if (string.IsNullOrEmpty(userName))
    fileName = HttpContext.Current.Server.MapPath(ConstructAllUsersDataFileName(path));
  else
    fileName = HttpContext.Current.Server.MapPath(
                           ConstructUserDataFileName(userName, path));

  if (!File.Exists(fileName))
    return;

  try
  {
    // lock on the filename in case two clients try accessing the
    // same file concurrently - note we lock on the interned filename
    // string, which will always return the same objref for identical
    // strings
    //
    if (Monitor.TryEnter(fileName, 5000))
    {
      if (string.IsNullOrEmpty(userName))
        sharedDataBlob = File.ReadAllBytes(string.Intern(fileName));
      else
        userDataBlob = File.ReadAllBytes(string.Intern(fileName));
    }
    else
      throw new ApplicationException("Monitor timed out");
  }
  finally
  {
    Monitor.Exit(string.Intern(fileName));
  }
}
protected override void SavePersonalizationBlob(WebPartManager webPartManager,
             string path, string userName, byte[] dataBlob)
{
  string fileName;
  if (string.IsNullOrEmpty(userName))
    fileName = ConstructAllUsersDataFileName(path);
  else
    fileName = ConstructUserDataFileName(userName, path);

  // lock on the filename in case two clients try accessing the same
  // file concurrently
  //
  try
  {
    if (Monitor.TryEnter(fileName, 5000))
      File.WriteAllBytes(HttpContext.Current.Server.MapPath(fileName),
dataBlob);
    else
      throw new ApplicationException("Failed to acquire lock on file to
write data");
  }
  finally
  {
    Monitor.Exit(fileName);
  }
}

// Helper function for creating a unique filename for all users based on
// a path
private string ConstructAllUsersDataFileName(string path)
{
  string pathConvertedToFileName = path.Replace('/', '_');
  pathConvertedToFileName = pathConvertedToFileName.Replace('~', '_');
  pathConvertedToFileName = pathConvertedToFileName.Replace('.', '_');

  return "~/App_Data/allusers" + pathConvertedToFileName + ".bin";
}

// Helper function for creating a unique filename for a particular user
// based on a path
private string ConstructUserDataFileName(string user, string path)
{
  string pathConvertedToFileName = path.Replace('/', '_');
  pathConvertedToFileName = pathConvertedToFileName.Replace('~', '_');
  pathConvertedToFileName = pathConvertedToFileName.Replace('.', '_');

  return "~/App_Data/" + user + pathConvertedToFileName + ".bin";
}

Once the provider is fully implemented, you use the providers section of the personalization configuration section to add it as a registered personalization provider. To actually begin using it, you must specify it as the default provider for personalization in your web.config file. Listing 6-38 shows an example of wiring up our custom file-based provider to be the default provider in our application.

-38. Configuration file wiring up of custom file-based provider

<webParts>
  <personalization defaultProvider="FileBasedPersonalizationProvider">
    <providers>
      <add name="FileBasedPersonalizationProvider"
           type="EssentialAspDotNet2.Providers.FileBasedPersonalizationProvider" />
    </providers>
  </personalization>
</webParts>

If we run our site again, all personalization data will now be stored in local binary files. Obviously this isn't the most scalable solution, but the sample should give you an idea of how to implement your own personalization provider on whatever backend you like.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows