Personalization

This chapter is excerpted from Programming ASP.NET 3.5, Fourth Edition by Jesse Liberty, Dan Maharry, Dan Hurwitz, published by O'Reilly Media

Programming ASP.NET 3.5, Fourth Edition

Logo

Buy Now

Most modern websites are designed for users to visit repeatedly, and therefore, they support some level of personalization: the ability to remember a user's preferences and, if appropriate, previous user choices. For example, in what color did the user prefer to see the site? In what language did he view the site? What are his name and email address?

In this chapter, you'll see how ASP.NET enables the creation of user profiles and how they are used to enable the personalization of a site. In particular, you'll see how to store different types of values in a user's profile and then how to use them to affect the design and layout of the site.

Creating Personalized Websites

The simplest form of personalization is to record information about the user and then to make that information available whenever the user logs on. This requires a kind of persistence that goes beyond session state. To create true personalization, you'll want to store the user's choices and information in a database that associates the saved information with a particular user, and persists indefinitely. Fortunately, ASP.NET provides all of the plumbing required. You do not need to design, edit, or manage the database tables; all of that is done for you.

You saw in Chapter 12, Forms-Based Security that by default, ASP.NET relies on a database to store the information for a site's users and the roles to which they belong. That same database is also used in part to support the personalization of a website in conjunction with some alterations to the site's web.config file. In this chapter, you'll use a copy of the site created in Chapter 12, Forms-Based Security as a basis for working with ASP.NET's profile provider.

Tip

You can find instructions on how to generate the ASP.NET membership database for your website in "Setting Up Forms-Based Authentication" in Chapter 12, Forms-Based Security.

So, then, to get started, copy the C12_Security website folder and all its contents to a new folder called C14_Personalization, and then open that website in Visual Studio 2008 (VS2008). Set Welcome.aspx as the start page and run the program to make sure you have a working duplicate.

Tip

Profiles are related to specific users and not to the roles to which they are assigned. This site has three users, called Dan, Jane, and Jesse, with passwords Dan@123, Jane@123, and Jesse@123, respectively.

Configuring the Site for Profiles

Profiles must be set up in web.config. Unlike membership and roles, VS2008's Web Site Administration Tool (WAT) will not help you. (You can also use the ASP.NET section of Internet Information Services [IIS] 7.0 to make changes to web.config through dialogs if you prefer, but we'll leave that until Chapter 18, Application Logic and Configuration when we look at website configuration in more detail.)

If you created the tables in the default database (ASPNETDB) on .\SQLEXPRESS, as shown in Chapter 12, Forms-Based Security, all you need to do in web.config is add the highlighted code in Example 14.1, "Enabling the default profile provider in web.config" to the <system.web> element.

Example 14.1. Enabling the default profile provider in web.config

<configuration>
   <system.web>
   ...
      <profile enabled="true">
         <properties>
            <add name="lastName" />
            <add name="firstName" />
            <add name="phoneNumber" />
            <add name="birthDate" type="System.DateTime" />
         </properties>
      </profile>
   ...
   </system.web>
</configuration>

This will enable the profile service and cause the Profile API to create storage for four pieces of information: lastName, firstName, phoneNumber, and birthDate. The default storage type is string. Notice, however, that you are storing the birthdate as a DateTime object.

Tip

The <properties> element is mandatory for the <profile> element. It can be empty, but you have to include it.

This default configuration, like that for membership and roles, uses a provider defined in .NET's main config file, machine.config, which your website's web.config file may override if you so choose. You can find it in the %windows%\Microsoft.NET\Framework \v2.0.50727/CONFIG folder. The relevant pieces of code are shown in Example 14.2, "The default profile provider configuration in .NET".

Example 14.2. The default profile provider configuration in .NET

<configuration>
  <connectionStrings>
    <add name="LocalSqlServer"
      connectionString="data source=.\SQLEXPRESS; 
        Integrated Security=SSPI;
        AttachDBFilename=|DataDirectory|aspnetdb.mdf;
        User Instance=true" providerName="System.Data.SqlClient"/>
  </connectionStrings>

  <system.web>
    <profile>
      <providers>
        <add name="AspNetSqlProfileProvider"
          connectionStringName="LocalSqlServer"
          applicationName="/"
          type="System.Web.Profile.SqlProfileProvider, System.Web,
            Version=2.0.0.0, Culture=neutral, 
            PublicKeyToken=b03f5f7f11d50a3a"/>
      </providers>
    </profile>
  </system.web>
</configuration>

Because you did not specify which provider to use in Example 14.1, "Enabling the default profile provider in web.config", ASP.NET defaults to using AspNetSqlProfileProvider. If you're using a database other than the default one to store your membership and profile data, you'll need to override these defaults in your web.config file. Example 14.3, "Overriding web.config with your profile database" shows the code to override these defaults using a connection string called Membership running on an instance of SQL Server called sql2k5 on the web server.

Example 14.3. Overriding web.config with your profile database

<configuration>
  <connectionStrings>
    <clear />

    <add name="MembershipDB"
      connectionString="Data Source=(local)\sql2k5;
      Initial Catalog=Membership;Integrated Security=True;
      Application Name=C14"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

  <system.web>
    <profile enabled="true">
      <providers>
        <remove name="AspNetSqlProfileProvider"/>
        <add name="AspNetSqlProfileProvider"
          connectionStringName="MembershipDB"
          applicationName="C14" 
          type="System.Web.Profile.SqlProfileProvider, System.Web,
            Version=2.0.0.0, Culture=neutral,
            PublicKeyToken=b03f5f7f11d50a3a"/>
      </providers>
      <properties>
         <add name="lastName" />
         <add name="firstName" />
         <add name="phoneNumber" />
         <add name="birthDate" type="System.DateTime" />
      </properties>
    </profile>
  </system.web>
</configuration>

Alternatively, you could redefine the LocalSqlServer connection string or use a different name for your ProfileProvider. In the latter case, you'd also need to add a defaultProvider attribute to <profile>:

<profile enabled="true" defaultProvider="myProfileProvider">
   <properties>
      ...
   </properties>
   <providers>
      <remove name="AspNetSqlProfileProvider"/>
      <add name="myProfileProvider" ... />
   </providers>
</profile>

If you do use your own configuration, you can use the attributes listed in Table 14.1, "Properties for the <profile> element" to tweak the profile provider's properties as required.

Table 14.1. Properties for the <profile> element

Property

Default value

Description

enabled

true

Sets whether profiles are enabled.

defaultProvider

AspNetSql-ProfileProvider

Sets the name of the profile provider to be used.

inherits

Rather than specifying the properties for the profile in the config file, you can create a class inherited from ProfileBase which contains properties for the profile, and set inherits to the name of the class.

automaticSaveEnabled

true

Specifies whether the user profile is automatically saved at the end of the execution of an ASP.NET page.

Working with Profile Data

With your website configured to support the gathering of user profile data, a database to store that data ready and waiting, and the information to store in a user profile specified, all you need to do now is gather it. To keep the example simple, just add a new hyperlink to the LoggedInTemplate in Welcome.aspx to point to the page where it can be entered:

<asp:LoginView ID="LoginView1" runat="server">
   <LoggedInTemplate>
      Welcome to this fab new site
      <asp:LoginName ID="LoginName1" runat="server" />
      <br />
      <asp:HyperLink ID="HyperLink2" runat="server" 
         NavigateUrl="~/ChangePW.aspx">
         Change your password</asp:HyperLink><br />
      <asp:HyperLink ID="HyperLink3" runat="server" 
         NavigateUrl="~/ManageRoles.aspx">
         Manage Roles</asp:HyperLink><br />
      <asp:HyperLink ID="HyperLink4" runat="server"
         NavigateUrl="~/ProfileInfo.aspx">
         Add Profile Information</asp:HyperLink>
   </LoggedInTemplate>
   <AnonymousTemplate>
      You aren&#39;t logged in yet.<br />
      You&#39;ll need to log in to access the system
   </AnonymousTemplate>
</asp:LoginView>

As you can see, the link brings you to ProfileInfo.aspx, a page you should now add to the website. Add to the page an HTML table with two columns and five rows, and within it four TextBoxes, some descriptive text, and a Save button, as shown in Figure 14.1, "ProfileInfo.aspx in Design view".

Figure 14.1. ProfileInfo.aspx in Design view

ProfileInfo.aspx in Design view

From top to bottom, the controls have the IDs txtFirstName, txtLastName, txtPhone, txtBirthDate, and btnSave. The full source for ProfileInfo.aspx is in Example 14.4, "ProfileInfo.aspx in full".

Example 14.4. ProfileInfo.aspx in full

<%@ Page Language="C#" AutoEventWireup="true" 
   CodeFile="ProfileInfo.aspx.cs" Inherits="ProfileInfo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Profile Info Demo</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
      <table>
         <tr>
            <td>First Name:</td>
            <td><asp:TextBox ID="txtFirstName" runat="server" /></td>
         </tr>
         <tr>
            <td>Last Name:</td>
            <td><asp:TextBox ID="txtLastName" runat="server" /></td>
         </tr>
         <tr>
            <td>Phone Number:</td>
            <td><asp:TextBox ID="txtPhone" runat="server" /></td>
         </tr>
         <tr>
            <td>Birth Date:</td>
            <td><asp:TextBox ID="txtBirthDate" runat="server" /></td>
         </tr>
         <tr>
            <td>
               <asp:Button ID="btnSave" Text="Save" runat="server"
                  onClick="btnSave_Click" />
            </td>
            <td>
            </td>
         </tr>
      </table>

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

The eagle-eyed among you will note that the Click event for btnSave is handled. Double-click btnSave in Design view to generate the handler for its Click event if you haven't done so already, and add the code following this paragraph. You can also add some code to the Page_Load event to get initial values for the TextBoxes from the Profile object. By doing so, you make any profile information previously entered by a user available for editing rather than having the user enter everything from scratch again.

using System;
using System.Web.UI;

public partial class ProfileInfo : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      if (!IsPostBack)
      {
         if (Profile.IsAnonymous == false)
         {
            txtLastName.Text = Profile.lastName;
            txtFirstName.Text = Profile.firstName;
            txtPhone.Text = Profile.phoneNumber;
            txtBirthDate.Text = Profile.birthDate.ToShortDateString();
         }
      }
   }

   protected void btnSave_Click(object sender, EventArgs e)
   {
      if (Profile.IsAnonymous == false)
      {
         Profile.lastName = txtLastName.Text;
         Profile.firstName = txtFirstName.Text;
         Profile.phoneNumber = txtPhone.Text;
         DateTime birthDate = DateTime.Parse(txtBirthDate.Text);
         Profile.birthDate = birthDate;
      }
      Response.Redirect("Welcome.aspx");
   }
}

Tip

If IntelliSense does not recognize the Profile.lastName field, leave the if statement empty, build the application, and try again. This will force VS2008 to reread the configuration file and generate a class that implements this field and the others listed in web.config. We'll discuss this class in more detail in "Inheriting Profile Properties," later in this chapter.

When you start the application, you are asked to log in. Once you're logged in, a new hyperlink appears: Add Profile Info. This was created by the hyperlink you added to the LoggedInTemplate (earlier). Clicking that link brings you to your new profile page.

The Profile object has properties that correspond to the properties you added in web.config. To test that the Profile object has stored this data, you want to add a panel to the bottom of the Welcome page just before the closing </div> tag:

<asp:Panel ID="pnlInfo" runat="server" Visible="False">
   <br />
   <table>
      <tr>
         <td>
            <asp:Label ID="lblFullName" runat="server"
               Text="Full name unknown" /></td>
      </tr>
      <tr>
         <td>
            <asp:Label ID="lblPhone" runat="server" 
               Text="Phone number unknown" /></td>
      </tr>
      <tr>
         <td>
            <asp:Label ID="lblBirthDate" runat="server" 
               Text="Birthdate unknown" /></td>
      </tr>
   </table>

</asp:Panel>

The panel has a table with three rows, and each row has a Label initialized indicating the value is unknown (this is not normally needed, but it is included here to ensure the data you see was in fact retrieved from the Profile object). When the page is loaded, you check to see whether you have Profile data for this user and, if so, you assign that data to the appropriate controls.

To do so, implement Page_Load in Welcome.aspx.cs:

protected void Page_Load(object sender, EventArgs e)
{
   if (Profile.UserName != null & Profile.IsAnonymous == false)
   {
      pnlInfo.Visible = true;
      lblFullName.Text = Profile.firstName + " " + Profile.lastName;
      lblPhone.Text = Profile.phoneNumber;
      lblBirthDate.Text = Profile.birthDate.ToShortDateString();
   }
   else
   {
      pnlInfo.Visible = false;
   }
}

Run the application, log in, and click Add Profile Information. You will be brought to the Profile Information form, as shown in Figure 14.2, "ProfileInfo.aspx in action".

When you click Save and return to the Welcome page, the Page_Load event fires, both parts of the if statement return true, that is, the UserName value is in the profile (and is not null), and the user is logged in and is not anonymous.

if (Profile.UserName != null & Profile.IsAnonymous == false)

Figure 14.2. ProfileInfo.aspx in action

ProfileInfo.aspx in action

Your profile information is displayed, as shown in Figure 14.3, "Saved profile information displayed in Welcome.aspx".

Figure 14.3. Saved profile information displayed in Welcome.aspx

Saved profile information displayed in Welcome.aspx

Returning to the profile information page, you'll see that all the profile data is already in the text boxes ready for editing right where you left it in Figure 14.2, "ProfileInfo.aspx in action".

Warning

The Page_Load handler in ProfileInfo.aspx.cs adds profile information to the TextBox controls only if the page has not posted back. This is because when the Save button is clicked and the page posts back, Page_Load runs first, overwriting any changes made on the screen before the Click handler saves the now overwritten changes back to the Profile. Try removing the check for IsPostBack from the Page_Load event and changing some profile data. You'll find that you can't do it. It's a subtle bug, but with an obvious effect.

So, how is a user's profile data stored in the database?

Exploring the Profile Tables

Open the Server Explorer window in VS2008 (or the Database Explorer if you're using Visual Web Developer) and look at the tables in your membership database. In turn, right-click on the following two tables and select Show Table Data from the menu:

  • aspnet_Users (which lists all the users your security system knows about)

  • aspnet_Profile (which lists the profile information for those users)

Right-click the tab for the window showing the aspnet_Users table and click New Horizontal Tab Group so you can compare both tables at the same time, as shown in Figure 14.4, "Comparing the Users and Profile tables". The Users table shows you that each user has a unique UserID. The Profile table has a foreign key into that table (UserID) and lists the PropertyNames and PropertyValues, also in Figure 14.4, "Comparing the Users and Profile tables".

Figure 14.4. Comparing the Users and Profile tables

Comparing the Users and Profile tables

PropertyNames matches up with the entries you created in the <profile> section of web.config:

<profile>
   <properties>
      <add name="lastName" />
      <add name="firstName" />
      <add name="phoneNumber" />
      <add name="birthDate" type="System.DateTime"/>
   </properties>
</profile>

Each property is named (such as phoneNumber), and is given a type (S for string), a starting offset (phoneNumber begins at offset 3), and a length (phoneNumber's value has a length of 8). The offset and value are used to find the value within the PropertyValueString field; hence, the following value in the table:

firstName:S:0:3:phoneNumber:S:3:8:birthDate:S:11:81:lastName:S:92:7:

Note that birthDate is listed as a string that begins at offset 11 and is 81 characters long, but if you look at the propertyValuesString column, you'll find that the birth date is encoded as XML:

<?xml version="1.0" encoding="utf-16"?><dateTime>
2008-02-29T00:00:00</dateTime>

Profile Property Groups

If a user profile is going to contain many different properties, you might consider grouping them in the same way that a server control's properties are grouped in VS2008's Property dialog. To do this, add a <group> element under <properties> in web.config. For example, you could change web.config in C14_Personalization to read as follows:

<profile>
   <properties>
      <group name="PersonalInfo">
         <add name="lastName" />
         <add name="firstName" />
         <add name="phoneNumber" />
         <add name="birthDate" type="System.DateTime" />
      </group>
   </properties>
</profile>

Each of the four properties in the group can now be accessed as Profile.PersonalInfo.* rather than Profile.* to keep things clear-a trick that IntelliSense makes doubly useful.

Personalizing with Complex Types

Although personalizing a site for your users is terrific, you often need to store complex user-defined types (classes) or collections.

In the next exercise, you'll edit the web.config file to add a collection of strings called favoriteBooks. Doing so will allow the user to choose one or more books and have those choices stored in the user's profile.

Add a line to web.config for your new property:

<profile>
   <properties>
      <add name="lastName" />
      <add name="firstName" />
      <add name="phoneNumber" />
      <add name="birthDate" type="System.DateTime" />
      <add name="favoriteBooks"
         type="System.Collections.Specialized.StringCollection" />
   </properties>
</profile>

To see this collection at work, edit the page ProfileInfo.aspx and insert a row with a checkbox list just above the row with the Save button, as shown here:

... top of profileinfo.aspx
<tr>
   <td>
      Birth Date:
   </td>
   <td>
      <asp:TextBox ID="txtBirthDate" runat="server" />
   </td>
</tr>
<tr>
   <td>Books</td>
   <td>
      <asp:CheckBoxList ID="cblFavoriteBooks" runat="server">
         <asp:ListItem>Programming C# 3.5</asp:ListItem>
         <asp:ListItem>Programming ASP.NET</asp:ListItem>
         <asp:ListItem>Programming .NET Apps</asp:ListItem>
         <asp:ListItem>Programming Silverlight</asp:ListItem>
         <asp:ListItem>Object Oriented Design Heuristics
            </asp:ListItem>
         <asp:ListItem>Design Patterns</asp:ListItem>
      </asp:CheckBoxList>
   </td>
</tr>
<tr>
   <td>
      <asp:Button ID="btnSave" Text="Save" runat="server" 
                  OnClick="btnSave_Click" />
   </td>
   <td>
   </td>
</tr>
... bottom of ProfileInfo.aspx

You'll also need to modify both the Page_Load and btnSave_Click handlers in ProfileInfo.aspx.cs to add the selected books to the profile and show them preselected in the list. The changes are shown in Example 14.5, "ProfileInfo.aspx.cs modified to add a StringCollection value".

Example 14.5. ProfileInfo.aspx.cs modified to add a StringCollection value

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Specialized;

public partial class ProfileInfo : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      if (!IsPostBack)
      {
         if (Profile.IsAnonymous == false)
         {
            txtLastName.Text = Profile.lastName;
            txtFirstName.Text = Profile.firstName;
            txtPhone.Text = Profile.phoneNumber;
            txtBirthDate.Text = Profile.birthDate.ToShortDateString();

            if (Profile.favoriteBooks != null)
            {
               foreach (ListItem li in cblFavoriteBooks.Items)
               {
                  foreach (string profileString
                             in Profile.favoriteBooks)
                  {
                     if (li.Text == profileString)
                     {
                        li.Selected = true;
                     }
                  }
               }
            }
         }
      }
   }
   protected void btnSave_Click(object sender, EventArgs e)
   {
      if (Profile.IsAnonymous == false)
      {
         Profile.lastName = txtLastName.Text;
         Profile.firstName = txtFirstName.Text;
         Profile.phoneNumber = txtPhone.Text;
         DateTime birthDate = DateTime.Parse(txtBirthDate.Text);
         Profile.birthDate = birthDate;
         Profile.favoriteBooks = new StringCollection();
         foreach (ListItem li in cblFavoriteBooks.Items)
         {
            if (li.Selected)
            {
               Profile.favoriteBooks.Add(li.Value.ToString());
            }
         }
      }
      Response.Redirect("Welcome.aspx");
   }
}

Tip

Each time you save the books, you create an instance of the string collection and then iterate through the checked listboxes, looking for the selected items. Each selected item is added to the StringCollection within the profile (the favoriteBooks property).

To confirm that this data has been stored, add a listbox (lbBooks) into the table displaying profile data in Welcome.aspx:

<tr>
   <td>
      <asp:ListBox ID="lbBooks" runat="server" />
   </td>
</tr>

Then update the Page_Load handler in Welcome.aspx.cs to reflect the chosen books in the profile:

protected void Page_Load(object sender, EventArgs e)
{
   if (Profile.UserName != null & Profile.IsAnonymous == false)
   {
      pnlInfo.Visible = true;
      lblFullName.Text = Profile.firstName + " " + Profile.lastName;
      lblPhone.Text = Profile.phoneNumber;
      lblBirthDate.Text = Profile.birthDate.ToShortDateString(  );

      lbBooks.Items.Clear();
      if (Profile.favoriteBooks != null)
      {
         foreach (string bookName in Profile.favoriteBooks)
         {
            lbBooks.Items.Add(bookName);
         }
      }
   }
   else
   {
      pnlInfo.Visible = false;
   }
}

Save all the changes and run the site. Once you've logged on, click Add Profile Information and you'll now be able to add books to your user's profile, as shown in Figure 14.5, "Choosing books for a user profile".

Figure 14.5. Choosing books for a user profile

Choosing books for a user profile

Once you've chosen a couple, click Save and your choices will be reflected on the Welcome page through your profile, as shown in Figure 14.6, "Showing the books saved to your profile".

Figure 14.6. Showing the books saved to your profile

Showing the books saved to your profile

Anonymous Personalization

It is common to allow your users to personalize your site before identifying themselves. A classic example of this is Amazon.com, which lets you add books to your shopping cart before you log in (you need to log in only when you are ready to purchase what is in your cart).

ASP.NET supports the ability to link anonymous personalized data with a specific user's personalized data, once that user logs in (you don't want to lose what's in your cart when you do log in).

To demonstrate, you'll enable anonymous personalization in the C14_Personalization website and allow browsers to select their favorite books before copying them to their profile when they log in.

A little more configuration

To enable anonymous personalization, you must update your web.config file, adding the following in the <system.web> section:

<anonymousIdentification enabled="true" />

You'll also need to identify which profile elements can be set by the anonymous user by adding the attribute-value pair allowAnonymous="true" to them:

<profile>
   <properties>
      <add name="lastName" />
      <add name="firstName" />
      <add name="phoneNumber" />
      <add name="birthDate" type="System.DateTime" />
      <add name="favoriteBooks" 
           type="System.Collections.Specialized.StringCollection"
           allowAnonymous="true" />
   </properties>
</profile>

A little more code

You'll need to make changes to:

  • Welcome.aspx to show the chosen books of the anonymous user

  • ProfileInfo.aspx to allow the choice of books for the anonymous user

  • Global.asax to migrate the choice of books from the anonymous profile into a user's profile when the user logs in

Redesign your Welcome.aspx page so the hyperlink that links to the profile information page and the lbBooks list is between the LoginView and the Panel control so that you can see the hyperlink and the list even if you are not logged in. While you are at it, rename Add Profile Info to Edit Profile Info, because you will be using this link to add, and edit, the profile info.

...
</asp:LoginView>
<asp:HyperLink ID="HyperLink1" runat="server" 
   NavigateUrl="~/CreateAccount.aspx">
   Create a new account
</asp:HyperLink>
<br />
<asp:HyperLink ID="HyperLink4"
   runat="server" NavigateUrl="~/ProfileInfo.aspx">
   Edit Profile Information</asp:HyperLink>
<br />
<asp:ListBox ID="lbBooks" runat="server" />
<br />
<asp:HyperLink ID="HyperLink1" runat="server" 
   NavigateUrl="~/CreateAccount.aspx">  
   Create a new account</asp:HyperLink>
<asp:Panel ID="pnlInfo" runat="server" Visible="False">
...

You'll also need to change the Page_Load handler in Welcome.aspx.cs to display the list of books for the anonymous user:

protected void Page_Load(object sender, EventArgs e)
{
   if (Profile.UserName != null & Profile.Is Anonymous == false)
   {
      pnlInfo.Visible = true;
      lblFullName.Text = Profile.firstName + " " + Profile.lastName;
      lblPhone.Text = Profile.phoneNumber;
      lblBirthDate.Text = Profile.birthDate.ToShortDateString();

      lbBooks.Items.Clear();
   }
   else
   {
      pnlInfo.Visible = false;
   }

   if (Profile.favoriteBooks != null)
   {
      foreach (string bookName in Profile.favoriteBooks)
      {
         lbBooks.Items.Add(bookName);
      }
   }
}

Now to the profile information page; ProfileInfo.aspx currently assumes that a user has logged in, so all five pieces of information are available to edit. However, only favoriteBooks is set to be stored for an anonymous user, so you'll wrap the rest of the controls in a Panel control, as shown in Example 14.6, "Modifying ProfileInfo.aspx for anonymous profile data", and use the Page_Load handler to hide it if the user isn't logged in.

Example 14.6. Modifying ProfileInfo.aspx for anonymous profile data

<%@ Page Language="C#" AutoEventWireup="true" 
   CodeFile="ProfileInfo.aspx.cs" Inherits="ProfileInfo" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Profile Info Demo</title>
</head>

<body>
   <form id="form1" runat="server">
   <div>
     <asp:Panel ID="pnlNonAnonymousInfo" runat="server">
      <table>
         <tr>
            <td>First Name:</td>
            <td><asp:TextBox ID="txtFirstName" runat="server" />
            </td>
         </tr>
         <tr>
            <td>Last Name:</td>
            <td><asp:TextBox ID="txtLastName" runat="server" /></td>
         </tr>
         <tr>
            <td>Phone Number:</td>
            <td><asp:TextBox ID="txtPhone" runat="server" /></td>
         </tr>
         <tr>
            <td>Birth Date:</td>
            <td><asp:TextBox ID="txtBirthDate" runat="server" />
            </td>
         </tr>
      </table>

     </asp:Panel>
      <table>
         <tr>
            <td>Books</td>
            <td>
               <asp:CheckBoxList ID="cblFavoriteBooks"
                  runat="server" >
                  <asp:ListItem>Programming C# 3.5</asp:ListItem>
                  <asp:ListItem>Programming ASP.NET</asp:ListItem>
                  <asp:ListItem>Programming .NET Apps</asp:ListItem>
                  <asp:ListItem>Programming Silverlight</asp:ListItem>
                  <asp:ListItem>
                     Object Oriented Design Heuristics</asp:ListItem>
                  <asp:ListItem>Design Patterns</asp:ListItem>
               </asp:CheckBoxList>
            </td>
         </tr>
         <tr>
            <td>
               <asp:Button ID="btnSave" Text="Save" runat="server" 
                  OnClick="btnSave_Click" />
            </td>
         </tr>
      </table>

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

When an anonymous user chooses books, that user will automatically be assigned a Globally Unique Identifier (GUID), and an entry will be made in the database. However, only those properties marked with allowAnonymous will be stored, so you must modify the btnSave_Click event handler in ProfileInfo.aspx.cs as well as Page_Load. In both cases, the alterations are mostly to ensure that the favoriteBooks property is saved and shown whether the user is anonymous or not, as shown in Example 14.7, "Modifying ProfileInfo.aspx.cs to work with the anonymous user profile".

Example 14.7. Modifying ProfileInfo.aspx.cs to work with the anonymous user profile

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Specialized;

public partial class ProfileInfo : Page
{
   protected void Page_Load(object sender, EventArgs e)
   {
      if (!IsPostBack)
      {
         if (Profile.Is Anonymous == true)
         {
            pnlNon AnonymousInfo.Visible = false;
         }
         else
         {
            txtLastName.Text = Profile.lastName;
            txtFirstName.Text = Profile.firstName;
            txtPhone.Text = Profile.phoneNumber;
            txtBirthDate.Text = Profile.birthDate.ToShortDateString();
         }

         if (Profile.favoriteBooks != null)
         {
            foreach (string bookName in Profile.favoriteBooks)
            {
               cblFavoriteBooks.Items.FindByText(bookName).Selected
                  = true;
            }
         }
      }
   }

   protected void btnSave_Click(object sender, EventArgs e)
   {
      if (Profile.IsAnonymous == false)
      {
         Profile.lastName = txtLastName.Text;
         Profile.firstName = txtFirstName.Text;
         Profile.phoneNumber = txtPhone.Text;
         DateTime birthDate = DateTime.Parse(txtBirthDate.Text);
         Profile.birthDate = birthDate;
      }

      Profile.favoriteBooks = new StringCollection();
      foreach (ListItem li in cblFavoriteBooks.Items)
      {
         if (li.Selected)
         {
            Profile.favoriteBooks.Add(li.Value.ToString());
         }
      }
      Response.Redirect("Welcome.aspx");
   }
}

Run the application. Do not log in; click the Profile Info link. Select a few books and click Save. When you return to the Welcome page, you will still not be logged in, but your selected books will be displayed, as shown in Figure 14.7, "Anonymous user information".

Figure 14.7. Anonymous user information

Anonymous user information

Stop the application and reopen the database. Open the aspnet_Users table and the aspnet_Profile tables. You'll see that an ID has been created for the anonymous user (and the UserName has been set to the GUID generated). In addition, the chosen books list has been stored in the corresponding record, as shown in Figure 14.8, "The anonymous user profile in the database".

The GUID is also stored in a cookie on the user's machine so if the user leaves before logging in, his profile will be retained in the database for his return as long as he returns to the site without clearing all his cookies from the browser or before that particular cookie expires. By default, this cookie is named .ASPXANONYMOUS, but you can change this in your web.config file by adding to the anonymousIdentification element:

<anonymousIdentification enabled="true"
   cookieName=".C14_AnonymousCookieName"/>

Figure 14.8. The anonymous user profile in the database

The anonymous user profile in the database

When the user returns to the site, the GUID in the cookie is sent on each request to the server so that the server can retrieve that user's anonymous profile. If the user logs in, you must migrate the profile data accumulated for the anonymous user to the appropriate authenticated user's record (so, for example, the user's favorite books are not lost). You do this by writing a handler for the global MigrateAnonymous event that is fired when a user logs in. This handler must be named Profile_MigrateAnonymous (case-sensitive) and is created within the script tags in Global.asax:

void Profile_MigrateAnonymous(object sender, 
                              ProfileMigrateEventArgs e)
{
   ProfileCommon anonymousProfile = Profile.GetProfile(e.AnonymousID);

   if (anonymousProfile != null
                        & anonymousProfile.favoriteBooks != null)
   {
      // add anonymous user choices to named user profile
      foreach (string s in anonymousProfile. favoriteBooks)
      {
         Profile.favoriteBooks.Remove(s);  // Remove duplicates
         Profile.favoriteBooks.Add(s);
      }

      // Clear anonymous profile
      anonymousProfile.favoriteBooks.Clear();
   }
}

Tip

If your project does not have a Global.asax file, right-click the project and choose Add New Item. One of your choices will be Global Application Class, and it will default to the name Global.asax. Click Add.

The first step in the event handler is to get a reference to the profile that matches the AnonymousID passed in as a property of the ProfileMigrateEventArgs structure (shown in bold).

If the reference is not null, you will know there is a matching anonymous profile, and that you may pick up whatever data you need from that profile. In this case, you copy over the favoriteBooks collection.

The user's profile is updated, and the books chosen as an anonymous user are now part of that user's profile. To finish, the anonymous user profile is then cleared of chosen books as they have been migrated successfully to the user's own profile.

Inheriting Profile Properties

So, how does a list of profile properties in web.config get turned into a strongly typed set of properties? When you press F5, the ASP.NET compiler parses the <profile> element in web.config and auto-generates a class derived from System.Web.Profile.ProfileBase, which it saves in the Temporary ASP.NET Files directory. Each profile property has a corresponding C# property generated for it that looks like this:

public virtual string lastName {
   get {
      return ((string)(this.GetPropertyValue("lastName")));
   }
   set {
      this.SetPropertyValue("lastName", value);
   }
}

To see this for yourself, you can either seek out the generated file in C:\Users\<YourUserName>\AppData\Local\Temp\Temporary ASP.NET Files\c14_personalization or set a breakpoint on any code-behind line involving a profile property and step into it (more on debugging in Chapter 19, Tracing, Debugging, and Error Handling).

This is all well and good, but it would be nicer if you could influence how the values are stored in the properties. Indeed, you can by creating your own C# class inheriting from the ProfileBase class and tying that into your website's profile provider. For instance, Example 14.8, "CustomProfile.cs" shows how to implement the five properties in the profile used in this chapter as a CustomProfile class. You'll need to save it in your website's App_Code directory.

Example 14.8. CustomProfile.cs

using System;
using System.Collections.Specialized;
using System.Web.Profile;

/// <summary>
/// Summary description for CustomProfile
/// </summary>

public class CustomProfile : ProfileBase
{
   public string lastName
   {
      get { return base["LastName"] as string; }
      set { base["LastName"] = value; }
   }

   public string firstName
   {
      get { return base["FirstName"] as string; }
      set { base["FirstName"] = value; }
   }

   public string phoneNumber
   {
      get { return base["PhoneNumber"] as string; }
      set { base["PhoneNumber"] = value; }
   }

   public DateTime birthDate
   {
      get { return (DateTime)(base["BirthDate"]); }
      set { base["BirthDate"] = value; }
   }

   [SettingsAllowAnonymous(true)]
   public StringCollection favoriteBooks
   {
      get { return base["FavoriteBooks"] as StringCollection; }
      set { base["FavoriteBooks"] = value; }
   }
}

To get your website to use this class rather than the class generated by the ASP.NET class, replace the <profile> element and its contents in web.config with this:

<profile inherits="CustomProfile" />

You'll now be able to run the website again using your own class as the backbone of the profile. This is useful if you want to add extra logic to the Profile properties. For example, you could make sure all first and last names have their first letter capitalized before saving them, or that a birth date is given a sensible value rather than 10,000 BC.

Note that if you've created your custom profile class outside the App_Code folder, you'll need to add its fully qualified name to the inherits property:

<profile inherits="Full.Namespace.CustomProfile" />

Saving Profile Properties

Whether you are working with the profile capabilities provided by ASP.NET, or a custom profile class such as the one in Example 14.8, "CustomProfile.cs", the page a user is visiting will save all the profile values to the data store specified by the Profile provider at the end of its execution unless you tell it otherwise. For example, if most of the pages in your site don't use any profile information, you might consider the extra trips to the database as each page closes to be a waste of resources and prefer to save it to the database only when there's a chance of it having been changed.

If you do wish to save profile values manually, you'll need to set the automaticSaveEnabled property of the <profile> element in web.config to false:

<profile enabled="true"
   automaticSaveEnabled="false">

Once you've done this, you'll need to make sure to call Profile.Save() whenever you need to update profile properties. For example, in ProfileInfo.aspx.cs, you would add the call to the btnSave_Click method just before the redirect back to the Welcome page:

...
   foreach (ListItem li in cblFavoriteBooks.Items)
   {
      if (li.Selected)
      {
         Profile.favoriteBooks.Add(li.Value.ToString());
      }
   }
   Profile.Save();
   Response.Redirect("Welcome.aspx");
}

Themes and Skins

Many users like to personalize their favorite websites by setting the look and feel of the site's controls to meet their own personal aesthetic preferences. ASP.NET 3.5 provides support for themes that enable you to offer that level of personalization to your users.

A theme is a collection of skins. A skin describes how a control should look. A skin can define stylesheet attributes, images, colors, and so forth.

Having multiple themes allows your users to choose how they want your site to look by switching from one set of skins to another at the touch of a button. Combined with personalization, your site can remember the look and feel your user prefers.

There are two types of themes:

  • Stylesheet themes define styles that may be overridden by the page or control. These are, essentially, equivalent to CSS stylesheets.

  • Customization themes define styles that cannot be overridden.

You set a stylesheet theme by adding the StyleSheetTheme attribute to the Page directive, and similarly, you set a customization theme by setting the Theme attribute in the Page directive.

Tip

You can set the default theme for the entire website in web.config by adding the pages element to the system.web element within the configuration element, as follows:

<configuration>    <system.web>       <pages theme="Psychedelic" />    </system.web> </configuration>
Settings in the page will override those in web.config.

In any given page, the properties for the controls are set in this order:

  1. Properties are applied first from a stylesheet theme.

  2. Properties are then overridden based on properties set in the control.

  3. Properties are then overridden based on a customization theme.

The customization theme is guaranteed to have the final word in determining the look and feel of the control.

Skins come in two flavors: default skins and explicitly named skins. Thus, you might create a Labels skin file with this declaration:

<asp:Label runat="server"
   ForeColor="Blue" Font-Size="Large"
   Font-Bold="True" Font-Italic="True" />

Or, more preferably and in line with web standards:

<asp:Label runat="server" CssClass="BlueLabel" />

This is a default skin for all Label controls. It looks like the definition of an asp:Label server control, but it is housed in a skin file and is used to define the look and feel of all Label objects.

In addition, however, you might decide that some labels must be red. To accomplish this, you create a second skin, but you assign this skin a SkinID property:

<asp:Label runat="server"
   SkinID="RedLabel"
/>

Any Label that does not have a SkinID attribute will receive the default skin; any Label that sets SkinID="RedLabel" will receive your named skin.

The steps to providing a personalized website are as follows:

  1. Create the test site.

  2. Organize your themes and skins.

  3. Enable themes and skins for your site.

  4. Specify themes declaratively if you wish.

Creating the Test Site

To demonstrate the use of themes and skins, you'll need some controls whose look and feel you can set. Open Welcome.aspx and add the following controls to the page, using the code shown in Example 14.9, "Extra controls for Welcome.aspx to demonstrate skins" (add this new code after the panel but before the closing <div>).

Example 14.9. Extra controls for Welcome.aspx to demonstrate skins

<table>
   <tr>
      <td>
         <asp:Label ID="lblListBox" runat="server" Text="ListBox" />
      </td>
      <td>
         <asp:ListBox ID="lbItems" runat="server">
            <asp:ListItem>First Item</asp:ListItem>
            <asp:ListItem>Second Item</asp:ListItem>
            <asp:ListItem>Third Item</asp:ListItem>
            <asp:ListItem>Fourth Item</asp:ListItem>
         </asp:ListBox>
      </td>
      <td>
         <asp:Label ID="lblRadioButtonList" runat="server" 
            Text="Radio Button List" />
      </td>
      <td>
         <asp:RadioButtonList ID="RadioButtonList1" runat="server">
            <asp:ListItem>Radio Button 1</asp:ListItem>
            <asp:ListItem>Radio Button 2</asp:ListItem>
            <asp:ListItem>Radio Button 3</asp:ListItem>
            <asp:ListItem>Radio Button 4</asp:ListItem>
            <asp:ListItem>Radio Button 5</asp:ListItem>
            <asp:ListItem>Radio Button 6</asp:ListItem>
         </asp:RadioButtonList>
         <br />
      </td>
   </tr>
   <tr>
      <td>
         <asp:Label ID="lblCalendar" runat="server" Text="Calendar" />
      </td>
      <td>
         <asp:Calendar ID="Calendar1" runat="server" />
      </td>
      <td>
         <asp:Label ID="lblTextBox" runat="server" Text="TextBox" />
      </td>
      <td>
         <asp:TextBox ID="TextBox1" runat="server" />
      </td>
   </tr>
</table>

You will use skins to change the look and feel of these controls, and you will organize sets of skins into themes.

Organizing Site Themes and Skins

Themes are stored in your project in a folder named App_Themes. To create this folder, go to the Solution Explorer, right-click the project folder, and choose Add ASP.NET Folder → Theme. The folder App_Themes will be created automatically, with a theme folder under it. Name the new folder DarkBlue. Create a second theme folder by right-clicking the App_Themes folder and choosing Add ASP.NET Folder → Theme. Name it Psychedelic.

Right-click the DarkBlue theme folder and choose Add New Item. From the Templates list, choose Skin File and name it Button.skin (to hold all the button skins for your DarkBlue theme), as shown in Figure 14.9, "Creating the skin file".

Each skin file is a text file that contains a definition for the control type but with no ID. For example, your Button.skin file might look like this (for the DarkBlue theme):

<asp:Button Runat="server"
   ForeColor="Blue" Font-Size="Large"
   Font-Bold="True" Font-Italic="True" />

Create skin files for each of the following types in both themes:

  • Button.skin

  • Calendar.skin

  • Label.skin

  • ListBox.skin

  • RadioButton.skin

  • Text.skin

At this point, your solution should look more or less like Figure 14.10, "Themes and skins in your project".

Figure 14.9. Creating the skin file

Creating the skin file

Figure 14.10. Themes and skins in your project

Themes and skins in your project

Enabling Themes and Skins

To let your users choose the theme they like and have it stored in their profile, you need to add a single line to the <properties> element in the <profile> element of web.config:

<add name="theme" />

Save and rebuild your application to make sure the class implementing the profile is set properly.

Specifying Themes for Your Page

You can set the themes on your page declaratively or programmatically. To set a theme declaratively, add the Theme attribute to the Page directive. For example:

<%@ Page Language="C#" AutoEventWireup="true"
   CodeFile="Welcome.aspx.cs" Inherits="Welcome"
   Theme="DarkBlue"%>

This will set the theme for Welcome.aspx to the DarkBlue theme you've created.

You can set the theme programmatically by hardcoding it, or (even better) by setting it from the user's profile.

Setting Stylesheet Themes

You set stylesheet themes by overriding the StyleSheetTheme property for the page. IntelliSense will help you with this. Open Welcome.aspx.cs, and scroll to the bottom of the class. Type the words public override and all the members you can override are shown. Start typing sty and IntelliSense will scroll to the property you want, StyleSheetTheme, as shown in Figure 14.11, "Overriding the stylesheet theme".

Figure 14.11. Overriding the stylesheet theme

Overriding the stylesheet theme

Once IntelliSense finds the method you want, press the Tab key to accept that property. Fill in the accessors as follows:

public override string StyleSheetTheme
{
   get
   {
      if ((!Profile.IsAnonymous) & Profile.Theme != null)
      {
         return Profile.Theme;
      }
      else
      {
         return "DarkBlue";
      }
   }
   set
   {
      Profile.Theme = value;
   }
}

Setting Customization Themes

If you are going to set a customization theme programmatically, you must do so from the PreInit event handler for the page because the theme must be set before the controls are created.

protected void Page_PreInit(object sender, EventArgs e)
{
   if (!Profile.IsAnonymous)
   {
      Page.Theme = Profile.Theme;
   }
}

Setting the theme in PreInit creates a bit of a difficulty when you want to allow the user to change the theme at runtime. If you create a control that posts the page back with a new theme, the PreInit code runs before the event handler for the button that changes the theme, so by the time the theme is changed, the buttons have already been drawn.

To overcome this you must either refresh the page or, as in this example, set the theme on a different page (ProfileInfo.aspx) before returning to the page (Welcome.aspx) with the call to Page_PreInit that sets its theme. Add two buttons to the ProfileInfo.aspx page at the bottom of the table at the bottom of the page:

<tr>
    <td>
        <asp:Button ID="ThemeBlue" Text="DarkBlue"
          Runat="server" OnClick="Set_Theme" />
    </td>
    <td>
        <asp:Button ID="ThemePsych" Text="Psychedelic"
         Runat="server" OnClick="Set_Theme" />
    </td>
</tr>

The two buttons share a single Click event handler. An easy way to have VS2008 set up that event handler for you is to switch to Design view and click one of the buttons. Click the lightning bolt in the Properties window to go to the events, and double-click the Set_Theme event handler. You are ready to implement the event handler. You'll cast the sender to Button and check its text, setting the theme appropriately:

protected void Set_Theme(object sender, EventArgs e)
{
   Button btn = (Button)sender;
   if (btn.Text == "Psychedelic")
   {
      Profile.Theme = "Psychedelic";
   }
   else
   {
      Profile.Theme = "DarkBlue";
   }
}

When the user is not logged on, the page's default theme will be used. Once the user sets a theme in the profile, that theme will be used. Create skins for your two themes and run the application to see the effect of applying the themes.

Alternatively, you can use the skins included with the copy of this chapter's website in the code download for this book.

Using Named Skins

You can override the theme for particular controls by using named skins. For example, you can set the lblRadioButtonList label in Welcome.aspx to be red even in the DarkBlue theme, by using a named skin. To accomplish this, create two Label skins in the Label.skin file within the DarkBlue folder:

<asp:Label Runat="server"
    ForeColor="Blue" Font-Size="Large"
    Font-Bold="True" Font-Italic="True" />

<asp:Label Runat="server" 
    SkinID="Red"
    ForeColor="Red" Font-Size="Large"
    Font-Bold="True" Font-Italic="True" />

The first skin is the default, and the second is a named skin because it has a SkinID property set to Red. Open the source for Welcome.aspx, find the label you want to make red, and add the attribute SkinID="Red", as shown in the following code snippet:

<asp:Label ID="lblRadioButtonList" Runat="server"
   Text="Radio Button List"
   SkinID="Red"/>

When you log in and set your theme to DarkBlue, you'll find that the label for the RadioButtonList is Red (honest!), as shown in Figure 14.12, "The RadioButtonList's label is red (honest)".

Figure 14.12. The RadioButtonList's label is red (honest)

The RadioButtonList's label is red (honest)

Web Parts

Web parts allow your users to reconfigure sections of your site to meet their own needs and preferences. Many information providers allow users to pick which content they want displayed and in which column to display it. Web parts allow you to provide that functionality with drag and drop "parts" of your page.

Web Parts Architecture

Web parts are created and managed on top of personalization using a structural component, called the WebPartManager control, to manage the interaction of web parts and UI controls to create user-managed interfaces.

Every web part page has a WebPartManager control. This invisible control tracks all the individual web part controls and manages the web part zones (described shortly). It also tracks the different display modes of the page, and whether personalization of your web part page applies to a particular user or to all users.

You'll need to make sure the WebPartManager is placed at the top of the page. ASP.NET will throw an error if you run this page and the WebPartManager is declared lower in the source than any of the web parts that it is managing: between the form and div tags at the top of the page should be fine. For example:

<body>
   <form id="form1" runat="server">
      <asp:WebPartManager ID="WebPartManager1" runat="server" />
      <div>
         ... rest of page ...

The WebPartManager uses another ASP.NET provider, this time the AspNetSqlPersonalizationProvider, to store the current location of the contents in the web parts in your database. Like the providers for users, roles, and profiles, this one also has a default configuration defined, although this time, you'll find it in the global web.config file rather than in machine.config in %windows%\Microsoft.NET\Framework\v2.0.50727/CONFIG. The relevant pieces of code are shown in Example 14.10, "The default personalization provider configuration".

Example 14.10. The default personalization provider configuration

<configuration>
  <connectionStrings>
    <add name="LocalSqlServer"
      connectionString="data source=.\SQLEXPRESS;
        Integrated Security=SSPI;
        AttachDBFilename=|DataDirectory|aspnetdb.mdf;
        User Instance=true" providerName="System.Data.SqlClient"/>
  </connectionStrings>

  ...
  <system.web>
    <webParts>
      <personalization>
        <providers>
          <add connectionStringName="LocalSqlServer"
            name="AspNetSqlPersonalizationProvider"
            type="System.Web.UI.WebControls.WebParts.
                     SqlPersonalizationProvider,
                  System.Web, Version=2.0.0.0, Culture=neutral,
                  PublicKeyToken=b03f5f7f11d50a3a"/>
        </providers>
       </personalization>
    </webParts>
  </system.web>
</configuration>

Because you did not specify which provider to use in your site's web.config file, ASP.NET uses the defaults. If you're using something other than the default SQL Express-based database to store your membership and profile data, you'll need to override these defaults in your site's web.config file. Example 14.11, "Overriding web.config with your personalization database" shows the code to override these defaults using a connection string called MembershipDB.

Example 14.11. Overriding web.config with your personalization database

<configuration>
  ...
  <webParts>
    <personalization defaultProvider="LocalPersonalizationProvider">
      <providers>
        <clear />
        <add name="LocalPersonalizationProvider"
          connectionStringName="MembershipDB" applicationName="C14"
            type="System.Web.UI.WebControls.WebParts.
                      SqlPersonalizationProvider,
                  System.Web, Version=2.0.0.0, Culture=neutral,
                  PublicKeyToken=b03f5f7f11d50a3a"/>
      </providers>
    </personalization>
  </webParts>
  ...
</configuration>

Or, as noted before, simply define your own connection string called LocalSqlServer and this will have the same effect:

<connectionStrings>
  <clear/>
  <add name="LocalSqlServer" 
     connectionString="<your_connection_string>"
     providerName="System.Data.SqlClient"/>
</connectionStrings>

With your changes to web.config in place, you can start building a page that uses web parts.

Creating Zones

A page that uses web parts is divided into zones: areas of the page that can contain content and controls that derive from the Part class (Part controls). They can contain consistent UI elements (header and footer styles, border styles, etc.) known as the chrome of the control.

To see a simple example of web parts at work, follow these steps:

  1. Create a new page called WebParts.aspx.

  2. Open the WebParts section of your Toolbox and drag a WebPartManager onto your page. The job of the WebPartManager is to track and coordinate all the web part controls on the page. It will not be visible when the page is running.

  3. Add a new table, made up of two rows and three columns. Rearrange the columns so that they are not of even size.

  4. Drag a WebPartZone into each of the six table cells. Each WebPartZone will have a default name (such as WebPartZone6) and a default heading. You can modify either or both of these properties in the Properties window, as shown in Figure 14.13, "Web part zones".

  5. Set the HeaderText property for WebPartZone1 to News.

Adding Controls to Zones

Drag a Label control into WebPartZone1. The Label is wrapped in a GenericWebPart control, and its title is set to Untitled, as shown in Figure 14.14, "A Label wrapped in a GenericWebPart control".

Switch to Source view and change the Title property of the label to "Today's News" and the text to the following:

<br/>
Penguin Classics releases new translation of "In Search of Lost Time".

Tip

Title is not normally a property of the Label control and will not show up in the Properties window or IntelliSense. However, when you add it to a WebPartZone, it is wrapped, at runtime, in a GenericWebPart control that does recognize this property.

Switch back to Design view and drag a ListBox control into the top-right WebPartZone control. Set the header text for the WebPartZone to "Our Sponsors". Click the ListBox and then its smart tag and Edit Items to open the ListItems Collection Editor. Add a few items to the listbox. Back in Source view set the ListBox's Title property to "Our Sponsors". (This control, like the Label control, does not inherently have a Title property, so IntelliSense will complain; as the earlier note explains, all will be well.)

Figure 14.13. Web part zones

Web part zones

Figure 14.14. A Label wrapped in a GenericWebPart control

A Label wrapped in a GenericWebPart control

Finally, add a hyperlink to Welcome.aspx pointing to WebParts.aspx so that you can access the page. Now run the site and log in. Click the link to the web part page which you added to Welcome.aspx. You should see two web parts, complete with Minimize and Close commands, as shown in Figure 14.15, "Two web parts visible in their zones".

If you don't log in, the web parts will not have the Minimize and Close commands.

Figure 14.15. Two web parts visible in their zones

Two web parts visible in their zones

Minimizing and Restoring

Click the Minimize tag and a menu appears allowing you to minimize or close the web part, as just shown in Figure 14.15, "Two web parts visible in their zones".

If you choose Minimize, the web part will be minimized to its title, and the minimize tag will offer a choice of Restore or Close, as shown in Figure 14.16, "Restore or close".

Figure 14.16. Restore or close

Restore or close

Exit the application. Restart, sign back in, and navigate to these pages. Aha! The minimized zone remains minimized. The individual's personalized web parts are automatically persisted through the personalization database.

Web part controls derive from the Part class and are the essential UI of a web part page. You can create custom web part controls, or you can use existing ASP.NET server controls, user controls, and custom controls.

Enabling Editing and Layout Changes

Web parts provide users with the ability to change the layout of the web part controls by dragging them from zone to zone. You may also allow your users to modify the appearance of the controls, their layout, and their behavior.

The built-in web part control set provides basic editing of any web part control on the page. You can create custom editor controls that let users do more extensive editing.

Creating a user control to enable changing page layout

To edit the contents of zones or to move controls from one zone to another, you need to be able to enter Design and Edit mode. To do this, you will create a new user control called DisplayModeMenu.ascx (see Chapter 15, Custom and User Controls for information on creating user controls), which will allow the user to change modes among Browse, Design, and Edit, as shown in Figure 14.17, "The DisplayMode user control".

Figure 14.17. The DisplayMode user control

The DisplayMode user control

Right-click the web project in the Solution Explorer and choose Add New Item. Select Web User Control; name the new user control DisplayModeMenu, as Figure 14.18, "Adding a user control" shows.

Add the code in Example 14.12, "DisplayMode.aspx in full" to the content file of your new user control.

Example 14.12. DisplayMode.aspx in full

<%@ Control Language="C#" AutoEventWireup="true" 
   CodeFile="DisplayModeMenu.ascx.cs" Inherits="DisplayModeMenu" %>

<div>
   <asp:Panel ID="Panel1" runat="server" BorderWidth="1"
      Width="230" BackColor="lightgray"
      Font-Names="Verdana, Arial, Sans Serif">
      <asp:Label ID="Label1" runat="server" Text="Display Mode"
         Font-Bold="true" Font-Size="8" Width="120" />
      <asp:DropDownList ID="ddlDisplayMode" runat="server"
         AutoPostBack="true" EnableViewState="false" Width="120"
         OnSelectedIndexChanged="ddlDisplayMode_SelectedIndexChanged" />
   </asp:Panel>
</div>

Figure 14.18. Adding a user control

Adding a user control

This code creates a panel, and within that panel it adds a single drop-down list (ddlDisplayMode). It sets the event handler for when the Selected item changes in the drop-down list. To support this page, open the code-behind file (DisplayModeMenu.ascx.cs) and add the code shown in Example 14.13, "DisplayModeMenu.aspx.cs".

Example 14.13. DisplayModeMenu.aspx.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

public partial class DisplayModeMenu : UserControl
{
   // will reference the current WebPartManager control.
   WebPartManager webPartManager;

   public void Page_Init(object sender, EventArgs e)
   {
      Page.InitComplete += new EventHandler(InitComplete);
   }

   // when the page is fully initialized
   public void InitComplete(object sender, EventArgs e)
   {
      webPartManager = WebPartManager.GetCurrentWebPartManager(Page);
      String browseModeName = WebPartManager.BrowseDisplayMode.Name;
      foreach (WebPartDisplayMode mode in 
                 webPartManager.SupportedDisplayModes)
      {
         String modeName = mode.Name;
         if (mode.IsEnabled(webPartManager))
         {
            ListItem listItem = new ListItem(modeName, modeName);
            ddlDisplayMode.Items.Add(listItem);
         }
      }
   }

   // Change the page to the selected display mode.
   public void ddlDisplayMode_SelectedIndexChanged
      (object sender, EventArgs e)
   {
      String selectedMode = ddlDisplayMode.SelectedValue;
      WebPartDisplayMode mode = 
         webPartManager.SupportedDisplayModes[selectedMode];
      if (mode != null)
      {
         webPartManager.DisplayMode = mode;
      }
   }

   // Set the selected item equal to the current display mode.
   public void Page_PreRender(object sender, EventArgs e)
   {
      ListItemCollection items = ddlDisplayMode.Items;
      int selectedIndex = 
         items.IndexOf(items.FindByText(
                              webPartManager.DisplayMode.Name));
      ddlDisplayMode.SelectedIndex = selectedIndex;
   }
}

Now open WebParts.aspx, and in Design mode make a space between the WebPartManager and the table of zones. Drag the DisplayModeMenu.ascx file from the Solution Explorer into that space. Change to Source view. VS2008 has done two things for you. First, it has registered the new control:

<%@ Register src="DisplayModeMenu.ascx"
   tagname="DisplayModeMenu" tagprefix="uc1" %>

Second, it has placed the control into the form:

<form id="form1" runat="server">
   <asp:WebPartManager ID="WebPartManager1" runat="server" />
   <uc1:DisplayModeMenu ID="DisplayModeMenu1" runat="server" />
   <div>
      ...

Before testing this, drag an EditorZone control onto the page underneath the table (currently unoccupied) and drag an AppearanceEditorPart and a LayoutEditorPart onto the EditorZone: you'll need to drop it onto the TextBox-like area above the OK, Cancel, and Apply zones, as shown in Figure 14.19, "An empty EditorZone control".

Figure 14.19. An empty EditorZone control

An empty EditorZone control

To make the EditorZone stand out, click its smart tag and choose AutoFormat and then Professional. Your EditorZone control should now look more or less like Figure 14.20, "A formatted EditorZone control".

Figure 14.20. A formatted EditorZone control

A formatted EditorZone control

Tip

WebPartZones may not be nested inside a WebPart.

Moving a part

Run the application. When you log in and go to the web part page, you are in Browse mode. Use the Display mode drop down to switch to Design mode and all the zones (except the editing zone) appear. You can click any web part (e.g., Our Sponsors) and drag it to any zone. Pressing the Alt key while dragging will animate this dragging, as shown in Figure 14.21, "Moving a web part between zones".

Figure 14.21. Moving a web part between zones

Moving a web part between zones

Next, change the drop down to Edit mode. Nothing much happens, but click the drop-down tag on one of the web part controls. A menu appears that now includes Edit, as shown in Figure 14.22, "A new edit option in web part Edit mode".

Click Edit and the EditorZone appears, allowing you to edit the current web part, as shown in Figure 14.23, "The EditorZone control in action".

Figure 14.22. A new edit option in web part Edit mode

A new edit option in web part Edit mode

The Appearance Editor lets you change the title and look of the web part, and the Layout lets you change, among other things, in which zone the WebPart will appear.

Adding Parts from a Catalog

You may want to provide a catalog of web parts from which your users can add to the various zones. To do so, open WebParts.aspx in Source view, and delete the bottom-left WebPartZone control. The table cell that contained it should now be empty. Switch to Design view and drag a CatalogZone control into the newly empty cell. Click the zone, and in the Properties window set the HeaderText property to CatalogZone if it isn't already. Drag a DeclarativeCatalogPart control into the zone, as shown in Figure 14.24, "Adding a DeclarativeCatalogPart control".

Click the smart tag on the DeclarativeCatalogPart and select Edit Templates. From the Standard tab of the Toolbox drag on a Calendar and a FileUpload control into the WebPartsTemplate, as shown in Figure 14.25, "Dragging controls into the WebPartsTemplate". This is the only template for the DeclarativeCatalogPart.

Before you run your program, switch to Source view and find the CatalogZone you added. Within the <WebPartsTemplate> element, add a Title attribute to both the Calendar and the FileUpload controls, as shown in Example 14.14, "Adding titles in the CatalogZone". (Again, IntelliSense will not like this attribute, but be strong and do it anyway.)

Figure 14.23. The EditorZone control in action

The EditorZone control in action

Figure 14.24. Adding a DeclarativeCatalogPart control

Adding a DeclarativeCatalogPart control

Figure 14.25. Dragging controls into the WebPartsTemplate

Dragging controls into the WebPartsTemplate

Example 14.14. Adding titles in the CatalogZone

<asp:CatalogZone ID="CatalogZone1" runat="server">
 <ZoneTemplate>
    <asp:DeclarativeCatalogPart 
       ID="DeclarativeCatalogPart1" runat="server">
       <Web PartsTemplate>
          <asp:Calendar ID="Calendar1" runat="server"
             Title="Calendar" />
          <asp:FileUpload ID="FileUpload1" runat="server"
             Title="Upload Files" />
       </WebPartsTemplate>
    </asp:DeclarativeCatalogPart>
 </ZoneTemplate>
</asp:CatalogZone>

Save and run the application. Log in and switch to WebParts.aspx. You'll see that the Catalog mode has been added automatically to the Display Mode drop-down menu, as shown in Figure 14.26, "Catalog mode now available in the menu".

Figure 14.26. Catalog mode now available in the menu

Catalog mode now available in the menu

When you select Catalog, the catalog zone will be exposed. You may select one of the controls and decide which zone to place it in, as shown in Figure 14.27, "Adding a control from the catalog".

Figure 14.27. Adding a control from the catalog

Adding a control from the catalog

Once you've picked your control and the zone to which to add it, click Add and the control instantly appears in the designated zone.

Web parts are the building blocks (along with personalization, themes, and skins) for the next generation of web applications, in which the user (rather than the designer) decides how the site appears and which information is given prominence.