ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Liberty on Whidbey

Personalization in ASP.NET 2.0

by Jesse Liberty
10/25/2004

In previous articles, I discussed security and managing users' roles. This article will pick up from where those articles left off, and show you how to provide personalized web pages for your users. Personalization allows your web site to welcome the user ("Welcome back Jesse") and to persist the user's state ("You have three items in your shopping cart").

Getting Started

There have been some small changes since the previous articles were published, and so I suggest downloading the updated starter code. Unzip the starter project to any directory you like. It will create a subdirectory called personalization. Open IIS Manager and create a virtual directory named Personalization that points to your (physical) personalization sub-directory. Right-click on the virtual directory, and choose Properties. Click on the ASP.NET tab, and then click on Edit Configuration. Click on the Authentication tab, set the Authentication Mode drop-down menu to Forms, and make sure the Role Management Enabled checkbox is checked, as shown in Figure 1.

 

Figure 1

Figure 1. Setting up personalization

Start Visual Studio 2005 Beta 1 and choose File -> Open -> Open Web site. Navigate to the Personalization directory and click Open. Click Debug (and save the sln file) and you should be brought to the default page. Log in with the username Jesse (the starter kit includes this user) with the password Jesse. Click on Manage Roles and then within the ManageRoles.aspx page, click on Administrator. You should see something very much like Figure 2.

Figure 2

Figure 2. User and role management

Assuming this all worked as expected, you're ready to add personalization. (If it did not work, make sure you are using Beta Refresh 1 of Visual Studio 2005 and that IIS is properly installed. If all else fails, start over. If that fails, give up; it is beta, after all!)

Related Reading

Programming .NET Windows Applications
By Jesse Liberty, Dan Hurwitz

Personalization

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 create a database that associates the saved information with a particular user, and that persists indefinitely.

To do this, you need to store the data in a database. ASP.NET 2.0 provides all of the plumbing required. You do not have to design, edit, or manage the tables; all of that is done for you.

In fact, ASP.NET 2.0 has decoupled the Profile API (how you programmatically interact with profile data) from the underlying data provider (how you store the data). This allows you to use the default provider (Access), or one of the other providers supplied (SQL server) or even write your own provider (e.g., for an existing customer relationship management system) without changing the way you interact with the profile in the rest of your code.

To add data to the user's profile, you must first alert the system about the data you wish to store. You do so in Web.config, by adding a profile section to the system.web element:


<profile>
  <properties>

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

This causes the Profile API to create storage for (in this case) four pieces of information (first and last name, phone number, and birthdate). The default storage type is string, but here we are storing the birthdate as a datetime object. You can gather this information any way you like. To keep the example simple, we'll remove the role groups section from Default.aspx, and add a hyperlink to the LoggedInTemplate:


<LoggedInTemplate>
    Welcome
     <asp:LoginName ID="LoginName1" Runat="server" />
  <asp:HyperLink ID="linkProfile" Runat="server" 
   NavigateUrl="~/ProfileInfo.aspx">Add Profile Info</asp:HyperLink>
</LoggedInTemplate>

This link brings you to the ProfileInfo.aspx page, where you are asked to fill in your profile information, as shown in Figure 3.

Figure 3

Figure 3. Profile info page

When you click Save, the event handler is fired and you may store the data to the Profile object. The Profile object was automatically instantiated for the current user, and so the data you assign here is properly associated with the current user ID.


void save_Click(object sender, EventArgs e)
{
	Profile.lastName = this.lastName.Text;
	Profile.firstName = this.firstName.Text;
	Profile.phoneNumber = this.phone.Text;
	Profile.birthDate = Convert.ToDateTime(this.birthDate.Text);
	Response.Redirect("Default.aspx");
}

The Profile object has properties that correspond to the properties you added in Web.config.

To test that the Profile object has in fact stored this date, you'll add a panel to the default page:

 
<asp:Panel ID="pnlInfo" Runat="server" Visible="False" Width="422px" Height="63px">
  <br />
  <table width="100%">
    <tr>
      <td>
        <asp:Label ID="lblFullName" Runat="server"  Text="Full name unknown">
        </asp:Label></td>
      </tr>
    <tr>
      <td>
        <asp:Label ID="lblPhone" Runat="server" Text="Phone number unknown">
        </asp:Label>
      </td>
    </tr>
    <tr>
      <td>
        <asp:Label ID="lblBirthDate" Runat="server"  Text="Birthdate  unknown">
        </asp:Label>
      </td>
    </tr>
  </table>
 </asp:Panel>
 
 

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


if (Profile.UserName != null && Profile.IsAnonymous == false )
{
  this.lblFullName.Text = "Full name: " + Profile.firstName + " " + Profile.lastName;
  this.lblPhone.Text = "Phone: " + Profile.PhoneNumber;
  this.lblBirthDate.Text = "Born: " + Profile.birthDate.ToShortDateString();
  this.pnlInfo.Visible = true;
}
else
{
  this.pnlInfo.Visible = false;
}

Notice that you convert the datetime to a string for easy display in the label. The result is shown in Figure 4.

Figure 4

Figure 4. Using personalization

Before going on, let's look at how that data is stored. In your Project, you'll find a Data folder, and within that Data folder, you'll find an .mdb file (assuming you are using the default Access profile database). Open that database and examine two tables: aspnet_Users (which lists all of the users your security system knows about) and aspnet_Profile, as shown in Figure 5.

Figure 5

Figure 5. The database

The profile table is tied to the users table by the UserID (red arrows), much as you might see in any relational database. Because the property types and values cannot be predicted, they are encoded. Each property is listed in a string in the PropertyNames field, and the values are aggregated in a second string in the PropertyValuesString column.

You can decode this fairly easily, at least for simple types. The lastName is a string (S) that begins at offset 0 and is seven characters long. The first name is a string that begins at offset 7 and is five characters long (highlighted in Figure 5). Notice that birthDate is listed as a string, that begins at offset 24 and is 95 characters long, but if you look at the propertyValuesString column, you'll find that the birthdate is encoded as XML.

Saving Complex Types

There are times when you'll want to save more than just strings or integers to the Profile. It is convenient to be able to save collections or user-defined types. To demonstrate how to add more complex items to the Profile, you'll allow the user to select a series of book titles to store (as if added to a shopping cart). To begin, open the ASP.NET Web Site Administration Tool. In Visual Studio, click on WebSite -> ASP.NET Configuration, as shown in Figure 6.

Figure 6

Figure 6. ASP.NET configuration

When the Web Site Administrator opens, click on the Profile tab, and then click on Create Profile Properties, as shown in Figure 7.

Figure 7

Figure 7. Site administration

On the Profiles Property page, set the property name, be sure to check the "Make this property available for anonymous users" check box, and fill in the fully qualified type name, in this case: System.Collections.Specialized.StringCollection (these areas are highlighted in Figure 8). Click Save and OK to save your profile data.

Figure 8

Figure 8. Setting properties

To see this collection at work, you'll add a CheckBoxList to the ProfileInfo page, which you will populate with the name of four books. Hand-populate this list by clicking on the Items property, and filling in the ListItems Collection Editor, as shown in Figure 9.

Figure 9

Figure 9. Adding personalization values

When the user's profile is updated, the user may select one or more books. When the Save button is clicked, the click handler will call the private helper method AddBooks:


private void AddBooks()
{
  Profile.ChosenBooks = new System.Collections.Specialized.StringCollection();
  foreach (ListItem item in this.cblBooks.Items)
  {
    if (item.Selected)
    {
      Profile.ChosenBooks.Add(item.Value.ToString());
    }
  }
}

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

To confirm that this data has been stored, you'll add a list box to the Default.aspx page, and you'll bind that list box to the collection in the profile:


if (Profile.ChosenBooks != null)
{
  this.lbBooks.DataSource = Profile.ChosenBooks;
  this.lbBooks.DataBind();
  this.lbBooks.Visible = true;
}

To make your code a bit easier to maintain, you want to have the selected values (name, phone, selected books, etc.) pre-filled when you return to the profile editing page, so you'll implement a bit of code on Page_Load to get the initial values from the Profile object:

 
public void Page_Load(object sender, EventArgs e)
{
  if (!IsPostBack && Profile.UserName != null)
  {
    this.lastName.Text = Profile.lastName;
    this.firstName.Text = Profile.firstName;
    this.phone.Text = Profile.phoneNumber;
    this.birthDate.Text = Profile.birthDate.ToShortDateString();
    if (Profile.ChosenBooks != null)
    {
      foreach (ListItem li in this.cblBooks.Items)
      {
        foreach (string s in Profile.ChosenBooks)
        {
          if (li.Text == s)
          {
            li.Selected = true;
          } // end if thext is the same
        } // end foreach string in saved isbns
      } // end foreach item in the list box
    } // end if savedisbns not null
  } // end if not postback
} // end Page_Load

Each time you navigate to this page, the values are updated from the profile, and you are free to change them, as shown in Figure 10.

Figure 10

Figure 10

When you click Save, the values are stored in the Profile object, and displayed in the default page, as shown in Figure 11.

Figure 11

Figure 11

Anonymous Personalization

It is possible that you'll want to allow your user to fill up a shopping cart (or otherwise create data to store in a Profile) before logging in. To allow for this, you may enable anonymous personalization. Return to Web.config, and mark a few of the properties allowAnonymous="true". Also add an element enabling anonymousIdentification, as shown highlighted in Figure 12.

Figure 12

Figure 12. Web.config

Redesign your Default.aspx page so that the hyperlink that links to the profile information page, and the lbBooks list box, are both outside of the LoginView 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 Profile Info, since you will be using this link to add, and edit, the profile info.

When an anonymous user chooses books, the user will be automatically be assigned a Globally Unique Identifier (GUID), and an entry will be made in the database for that ID. However, note that only those properties marked with allowAnonymous may be stored, so you must modify your save_Click event handler in ProfileInfo.aspx.cs accordingly.

void save_Click(object sender, EventArgs e)
{
  if (Profile.IsAnonymous == false)
  {
    Profile.lastName = this.lastName.Text;
    Profile.firstName = this.firstName.Text;
    Profile.phoneNumber = this.phone.Text;
    Profile.birthDate = Convert.ToDateTime(this.birthDate.Text);
  }
  AddBooks();
  Response.Redirect("Default.aspx");
}

When saving your Profile data, you check whether the IsAnonymous property is false. If it is false, then you have a logged-in user, and you may get all of the properties; otherwise, you may get only those that are allowed for anonymous users. Similarly, so as not to display default (and possibly bizarre) data, you will want to check the IsAnonymous flag when you pre-fill the fields on page load:

public void Page_Load(object sender, EventArgs e)
{
  if (!IsPostBack && Profile.UserName != null )
  {
    if (Profile.IsAnonymous == false)
    {
      this.lastName.Text = Profile.lastName;
      this.firstName.Text = Profile.firstName;
      this.phone.Text = Profile.phoneNumber;
      this.birthDate.Text = Profile.birthDate.ToShortDateString();
    }
  
    if (Profile.ChosenBooks != null)
      {
        foreach (ListItem li in this.cblBooks.Items)
        {
          foreach (string s in Profile.ChosenBooks)
          {
            if (li.Text == s)
            {
              li.Selected = true;
            } // end if thext is the same
          } // end foreach string in saved isbns
        } // end foreach item in the list box
      } // end if savedisbns not null
  } // end if not postback
} // end Page_Load

 

Notice that if IsAnonymous is true, only the book list will be initialized from the Profile.

Run the application, do not log in, but do click the Profile Info link. Select a few books and click Save. When you return to the default page, you are still not logged in, but your selected books are displayed, as shown in Figure 13.

Figure 13

Figure 13: Using personalization

A quick look at the database shows that an ID has been created for this anonymous user (and the UserName has been set to the GUID generated). In addition, the shopping cart has been stored in the corresponding record, as shown in Figure 14.

Figure 14

Figure 14. The data

Notice in Figure 14 that the UserName is the GUID, that the IsAnonymous checkbox is checked, and that in the Profile, this user has a record of which books where chosen.

Migrating from Anonymous to Authenticated

A typical scenario in an online store is that your anonymous user will, eventually, be identified (perhaps at checkout). It is important that you be able to migrate the Profile data you've accumulated for the anonymous user to the appropriate authenticated user so that, for example, the shopping cart items are not lost. You do this by writing a global handler in global.asax.

If your project does not yet have a global.asax file, right-click on the project and choose Add New Item. One of your choices will be Global Application Class, and it will automatically be named global.asax. Within that class, add a method to handle the MigrateAnonymous event that is fired when a user logs in:

void Profile_MigrateAnonymous(object sender, ProfileMigrateEventArgs e)
{
  ASP.HttpProfile anonymousProfile = Profile.GetProfile(e.AnonymousId);
  if (anonymousProfile != null && anonymousProfile.ChosenBooks != null)
  {
    foreach (string s in anonymousProfile.ChosenBooks)
    {
      Profile.ChosenBooks.Remove(s);  // avoid duplicates
      Profile.ChosenBooks.Add(s);
    }
  }
}

The first step is to get a reference to the profile that matches the AnonymousID that is passed in as a property of the ProfileMigrateEventArgs structure. If the reference is not null, then you know that there is a matching anonymous profile, and that you may pick up whatever data you need from that profile. In this case, we copy over the ChosenBooks collection.

The user's profile is updated, and the books chosen as an anonymous user are now part of that user's profile.

Jesse Liberty is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.


Read more Liberty on Whidbey columns.

Return to ONDotnet.com.

Copyright © 2009 O'Reilly Media, Inc.