Personalization with Profiles and Web Parts in ASP.NET 2.0

Personalization information often needs to be more persistent than session state. Prior to ASP.NET 2.0, developers needed to create this functionality for from scratch, typically using tables relationally linked to a user table. ASP.NET 2.0 introduced two new features that help developers manage and implement personalization in their sites, which are the subject of this chapter.

This chapter is from the book

This chapter is from the book

Web applications often need to track user information over time. Sometimes, this information is mission critical, such as customer orders, shipping addresses, and so on. A site may also want to track information that is not absolutely essential to the application, but is helpful in terms of the overall user experience with the site. This type of information is often referred to as personalization information. Examples of personalization information can include user preference information such as the language or theme to use in the site, the size of the font, shopping interests, and so on. Personalization can also include information that is visit oriented, but which needs to be more persistent than session state. Some examples of this type of information are a shopping cart, a wish list, or a list of the last N products viewed by the user.

Prior to ASP.NET 2.0, developers needed to create this functionality from scratch, typically using tables relationally linked to a user table. ASP.NET 2.0 introduced two new features that help developers manage and implement personalization in their sites. These features are the profile system and the Web parts framework and are the subject of this chapter.

ASP.NET Profiles

The profile feature is a way of easily associating and persisting application information with users. You can use profiles to track any user-related information, from personalization data to more mission-critical information such as customer names, addresses, financial information, and so on.

The profile feature is yet another ASP.NET subsystem that is built using the provider model (see Chapter 13 for more information on the provider model). As such, the actual data storage mechanism for the profile information is independent of its usage. By default, ASP.NET uses the SqlProfileProvider, which uses the same SQL Server database as the membership and role management features examined in the previous chapter (i.e., ASPNETDB.MDF in the App_Data folder). However, if you have an existing database of user or personalization data, you might decide instead to create your own custom profile provider.

Profile data can be easily defined within the Web.config file and is consumed using the familiar properties programming model. It can be used to store data of any type, as long as it is serializable. Unlike session state data, which must be cast to the appropriate type, profile data is made available via strongly typed properties.

Defining Profiles

Perhaps the best thing about the profile feature is how easy it is to set up. You define the data that is to be associated with each user profile via the profile element in the Web.config file. Within this element, you simply define the properties whose values you want to persist. Each property consists of a name, a type, and perhaps a default value as well as a flag indicating whether this property is available to anonymous users. For instance, Listing 14.1 illustrates the configuration of three string properties.

Table 14.1. Configuration for the Profile add Element

Indicates whether the property can be set by anonymous users. The default is false.

customProviderData

A string of custom data to be used by the profile provider. Individual providers can implement custom logic for using this data.

defaultValue

The initial default value used for the property.

name

The name of the property.

provider

The profile provider to use (if not using the default profile provider). By default, all properties are managed using the default provider specified for profile properties, but individual properties can also use different providers.

readOnly

Indicates whether the property is read-only. The default is false.

serializeAs

Specifies the serialization method to use when persisting this value. Possible values are described by the SerializationMode enumeration: Binary, ProviderSpecific (the default), String, and Xml.

type

The data type of the property. You can specify any .NET class or any custom type that you have defined. If you use a custom type, you must also specify how the provider should serialize it via the serializeAs property.

Using Profile Data

After the profile properties are configured, you can store and retrieve this data within your pages via the Profile property (which is an object of type ProfileCommon), of the Page class. Each profile defined via the add element is available as a property of this Profile property. For instance, to retrieve the Region value, you could use the following.

string sRegion = Profile.Region;

Notice that you do not have to do any typecasting, because the profile properties are typecast using the value specified by the type attribute for each property in the Web.config file.

You can save this profile value in a similar manner as shown in the following.

Profile.Region = txtRegion.Text;

This line (eventually) results in the profile value being saved by the profile provider for the current user. Where might you place these two lines of code? A common pattern is to retrieve the profile values in the Page_Load handler for the page and then set the profile value in some type of event handler as a result of user action.

Listings 14.2 and 14.3 illustrate a sample page that makes use of the three profile properties defined in Listing 14.1. You may recall that the Web.config file in Listing 14.1 requires that the user must first log in. After authentication, the page uses the default region, background color, and text color defined in Listing 14.1. The user can then change these profile values via three drop-down lists (see Figure 14.1). When the user clicks the Save Settings button, the event handler for the button changes the page's colors as well as saves the values in the profile object for the page.

Listing 14.2. ProfileTest.aspx

<h1>Profile Test</h1>
<p>Welcome
<asp:LoginName ID="logName" runat="server" Font-Bold="true" /> from
<asp:Label ID="labWhere" runat="server" Font-Bold="true" />.
This page demonstrates the new profile system in ASP.NET 2.0.
</p>
<asp:Panel ID="panForm" runat="server" GroupingText="Profile Data">
<asp:label ID="labBack" runat="server"
AssociatedControlID="drpBackColor"
Text="Background Color:" />
<asp:DropDownList ID="drpBackColor" runat="server" >
<asp:ListItem>Black</asp:ListItem>
<asp:ListItem>Green</asp:ListItem>
<asp:ListItem>Red</asp:ListItem>
<asp:ListItem>Yellow</asp:ListItem>
<asp:ListItem>White</asp:ListItem>
</asp:DropDownList><br />
<asp:label ID="labText" runat="server"
AssociatedControlID="drpTextColor" Text="Text Color:" />
<asp:DropDownList ID="drpTextColor" runat="server" >
<asp:ListItem>Black</asp:ListItem>
<asp:ListItem>Green</asp:ListItem>
<asp:ListItem>Red</asp:ListItem>
<asp:ListItem>Yellow</asp:ListItem>
<asp:ListItem>White</asp:ListItem>
</asp:DropDownList><br />
<asp:label ID="labRegion" runat="server"
AssociatedControlID="drpRegion" Text="Region:" />
<asp:DropDownList ID="drpRegion" runat="server" >
<asp:ListItem>Africa</asp:ListItem>
<asp:ListItem>Asia</asp:ListItem>
<asp:ListItem>Europe</asp:ListItem>
<asp:ListItem>North America</asp:ListItem>
<asp:ListItem>Oceania</asp:ListItem>
<asp:ListItem>South America</asp:ListItem>
</asp:DropDownList>
<p>
<asp:Button ID="btnSave" runat="server" Text="Save Settings"
OnClick="btnSave_Click" />
</p>
</asp:Panel>
<asp:Panel ID="panSample" runat="server"
GroupingText="Sample Text">
<p>
Web applications often need to track user information over time.
Sometimes this information is mission critical, such as customer
orders, shipping addresses, and so on. A site may also wish to track
information that is not absolutely essential to the application, but
is helpful in terms of the overall user experience with the site.
This type of information is often referred to as personalization
information. Examples of personalization information can include
user preference information such as the language or theme to use in
the site, the size of the font, shopping interests, and so on.
Personalization can also include information that is more "visit-
oriented," but needs to be more persistent than session state, such
as a shopping cart, a wish list, or a list of the last N products
viewed by the user
</p>
</asp:Panel>

The code-behind for this page is reasonably straightforward. The first time the page is requested, it sets the page's colors based on the current profile values, and sets the default values of the page's controls to these same profile values. The event handler for the Save Settings button simply sets the profile properties to the new user-chosen values. Everything else is handled by the profile system and its provider, so that the next time this user requests this page, whether in five minutes or five years, it uses these saved profile values.

How Do Profiles Work?

The profile system uses the provider model introduced in Chapter 13 so that some type of profile provider handles the actual responsibility of saving and retrieving items from the data store. But how does the Profile property of the page "know" about the profile properties defined in the Web.config file?

When a page is requested from a Web application that has profile properties defined in its Web.config file, ASP.NET automatically generates a class named ProfileCommon in the Temporary ASP.NET Files directory. In the following example, you can see the ProfileCommon class that was generated from the profiles in Listing 14.1.

As you can see, this generated class is a subclass of the ProfileBase class and contains strongly typed properties for each profile property defined in the Web.config file. This class is available to the page via a Profile property that is added to the Web Form by the page parser.

Saving and Retrieving Profile Data

One of the real benefits of using the profile system is that the values for each user are automatically saved and retrieved by the system. The profile system works via an HttpModule named ProfileModule. You may recall from Chapter 2, all incoming requests pass through the events in the HttpApplication pipeline and, as part of this process, different HTTP modules can register their own event handlers with the events in the pipeline.

The ProfileModule registers its event handlers for the AcquireRequestState and EndRequest events of the pipeline, as well as registers its own Personalization event (which is called before the profile data is loaded and which can be handled in the global.asax file). The ProfileModule does not populate the profile object with data during the AcquireRequestState event, because this necessitates retrieving data from the profile provider for every request, regardless of whether the page used profile data. Instead, an instance of ProfileCommon is created and populated with data (via the ProfileBase.Create method) from the provider by HttpContext during the page's first request for the Profile property.

The data within the ProfileCommon object is potentially saved during the EndRequest event of the request pipeline. The data in this object is serialized and then saved by the provider if the data has been changed by the page. For the default SQL Server profile provider, simple primitive data is serialized into a string; for more complex data types, such as arrays and custom types, the data is serialized into XML. For instance, the sample profile properties from Listing 14.1 are serialized into the following string and then saved into the PropertyValuesString field of the aspnet_Profile table.

YellowRedEurope

As well, there is a PropertyNames field in this table that specifies how to deserialize this string data (it indicates the starting position for each property and its length).

BackgroundColor:S:0:6:TextColor:S:6:3:Region:S:9:6:

In summary, using profiles in your page potentially adds two requests to the data source. A page that uses profile data always reads the profile data at the beginning of the request. Because the profile system uses the lazy initialization pattern (see Chapter 12), if your page does not access the Profile object, it does not read the profile data from the data source. When the page has finished the request processing, it checks to see if the profile data needs to be saved. If all the profile data consists of strings or simple primitive data types such as numbers or Booleans, the profile system checks if any of the properties of the page's ProfileCommon object are dirty (that is, have been modified) and only saves the data if any of them has been modified. If your profile object does contain any complex type, the page always saves the profile data, regardless of whether it has been changed.

Controlling Profile Save Behavior

If your profile data includes nonprimitive data types, the profile system assumes the profile data is dirty and thus saves it, even if it hasn't been modified. If your site includes many pages that use complex profile data, but few that actually modify it, your site will do a lot of unnecessary profile saving. In such a case, you may want to disable the automatic saving of profile data and then explicitly save the profile data only when you need to.

To disable autosaving, simply make the following change to the profile element in your application's Web.config file.

<profile automaticSaveEnabled="false" >

After the automatic profile saving is disabled, it is up to the developer to save the profile data by explicitly calling the Profile.Save method. For instance, you could change the code-behind in your example in Listing 14.3 to the following.

Using Custom Types

Profile properties can be any .NET type or any custom type that you have defined. The only requirement is that the type must be serializable. For instance, imagine that you have a custom class named WishListCollection that can contain multiple WishListItem objects. If you want to store this custom class in profile storage, both it and its contents must be serializable. You can do so by marking both classes with the Serializable attribute, as shown here.

Along with allowing the use of custom types for profile properties, the profile system allows you to define the profile structure programmatically using a custom type rather than declaratively in the Web.config file. You can do this by defining a class that inherits from the ProfileBase class and then referencing it via the inherits attribute of the profile element in the Web.config file. For instance, Listing 14.4 illustrates a sample derived profile class. Notice that it inherits from the ProfileBase class. Notice as well that each property is marked with the SettingsAllowAnonymous attribute; this is equivalent to the AllowAnonymous attribute that was used in the Web.config example in Listing 14.1.

With this custom profile class defined, you can use it by changing your Web.config file as shown in the following example. Notice that you can still supply additional declarative properties to the profile if you want.

The main advantage of using a custom provider class is that the developer can better control what happens when the profile's accessors (i.e., the getters and setters) are used. For instance, in Listing 14.4, the setter for the Theme property first verifies that the specified theme actually exists as a real theme folder before setting the value. Just like we saw back in Chapter 11 with custom business objects and entities, a custom profile class allows you to implement some type of business logic with your profile data. As well, another advantage of using a custom provider class is that you can define a profile property that uses a generic collection, as illustrated in Listing 14.4.

Working with Anonymous Users

As you have seen, by default, the profile properties in ASP.NET are only available to authenticated users. However, you can make profiles available to anonymous users. This involves two steps. The first step is to mark individual properties as allowing anonymous access. You can do this declaratively using the allowAnonymous attribute.

By default, anonymous identification is disabled. By enabling it, ASP.NET ensures that the AnonymousIdentificationModule is involved in the request pipeline. This module creates and manages anonymous identifiers for an ASP.NET application. When an anonymous user makes a request for a resource in an application that has anonymous identification enabled, the AnonymousIdentificationModule generates a globally unique identifier (this is universally referred to as a GUID). A GUID is a 128-bit integer (16 bytes) that can be used across all computers and networks wherever a unique identifier is required. Such an identifier has a very low probability of being duplicated. At any rate, after generating the GUID, the module writes it to a persistent cookie and this GUID becomes the anonymous user's name.

Core Note

Although each new anonymous user session is assigned a new GUID (because the GUID is written to a cookie on the computer), if a different person accesses the site using this same computer, the site uses the same anonymous GUID issued to the first user.

Deleting Anonymous Profiles

Because every new anonymous user session generates a new GUID, a site that receives many anonymous requests may end up with many profile records for all these anonymous requests, most of which are inactive. In this case, you may need to run some type of scheduled task that deletes these inactive profiles because there is no automatic built-in mechanism for doing so. You can do this via the ProfileManager class. This class can be used to manage profile settings, search for user profiles, and delete user profiles that are no longer in use. Table 14.2 lists the methods of the ProfileManager class.

Table 14.2. Methods of the ProfileManager Class

Method

Description

DeleteInactiveProfiles

Deletes user profiles for which the last activity date occurred before the specified date.

DeleteProfile

Deletes the profile for the specified user name.

DeleteProfiles

Deletes the specified list of profiles.

FindInactiveProfilesByUserName

Retrieves a collection of ProfileInfo objects in which the last activity date occurred on or before the specified date and the user name for the profile matches the specified user name.

FindProfilesByUserName

Retrieves a collection of ProfileInfo objects that matches the specified user name.

GetAllInactiveProfiles

Retrieves a collection of ProfileInfo objects in which the last activity date occurred on or before the specified date.

GetAllProfiles

Retrieves a collection of ProfileInfo objects that represents all the profiles in the profile data source.

GetNumberOfInactiveProfiles

Gets the number of profiles in which the last activity date occurred on or before the specified date.

GetNumberOfProfiles

Gets the total number of profiles in the profile data source.

For instance, imagine that you have some type of administrative page in which you want to display all the authenticated profiles and all the anonymous profiles in two separate GridView controls. The code for this might look like the following.

Notice that the ProfileManager.GetAllProfiles method uses the ProfileAuthenticationOption enumeration to specify which profiles to retrieve. If you want to delete the anonymous profiles in which there has been no activity for the last day, you could use the following.

You can also delete just a specific profile as well. For instance, if the GridView of authenticated users has a delete command column, you could allow the user to delete a specific authenticated user using the following.

In the code samples that can be downloaded for this book, there is a completed profile manager page that allows an administrator to view and delete authenticated and anonymous user profiles. Figure 14.2 illustrates how this sample profile manager appears in the browser.

Migrating Anonymous Profiles

If your site supports profiles for both authenticated and anonymous users, you might want the ability to migrate a user's anonymous profile to his authenticated profile. That is, if an anonymous user sets some profile values, then registers and logs on, the profiles the user set while anonymous should be copied to the new profile the user has as an authenticated user.

The profile system provides a mechanism for this scenario. You can handle this situation by creating a MigrateAnonymous event handler for the ProfileModule in the global.asax file. This handler copies the profile properties from the anonymous profile into the profile for the just-logged-in user. However, there is a potential problem with this mechanism. Recall that before the user can log in, the system considers the user to be an anonymous user. Unfortunately, the MigrateAnonymous event is triggered every time the user logs in. This means you would likely overwrite the user's actual profile with the anonymous profile each time the user logs in. One way to handle this situation is to add some type of flag to the profile to indicate whether the profile has already been migrated. The custom profile class in Listing 14.4 has such a flag property, named MigratedAlready.

You can set up the MigrateAnonymous event handler in the global.asax file as shown in the following example. Notice that it also deletes the anonymous profile and its cookie.

When to Use Profiles

The profile system is ideal for user-related data in which you do not have an already existing database schema. If you already have an existing database of user or personalization data, you may prefer instead to use a similar approach to that covered in Chapter 11—namely, create entity or business object classes for this data along with necessary data access classes, rather than design a custom profile provider for this existing data. Even if you do not already have an existing database schema for user information, you might want to avoid using the default profile provider for mission-critical or large amounts of data, because the profile system is not exactly the most optimized way to store data. Although the profile system can serialize its data into string or XML or binary format, serialization is never as efficient as the pure binary approach used by a database.

In my opinion, the profile system is best used for tracking user information that is generated by the application itself, and not for data that is actually entered by the user. For instance, in a site that contains forms for the user to enter her mailing addresses, credit card information, shoe size, and so on, it is best to design database tables for containing this information, and then use business objects/entities and data access classes for working with this data. This way, you can add any validation, application-logic processing, or other behaviors to these classes.

The profile system is ideal for customizing the site based on the user's preferences and the user's behavior within the site, because this type of data either necessitates little validation or logic (e.g., use preferences) or can be generated by the system itself without the user's knowledge (e.g., tracking user behaviors).

In the extended example that you can download for this chapter, you can see both types of customization demonstrated. This sample site (see Figures 14.3, 14.4, and 14.5) uses roughly the same master page-based theme selection mechanism used in the listings at the close of Chapter 6, except here it uses the profile mechanism rather than session state to preserve the user's theme choice.

The site also keeps track of (and displays) the last five books browsed by the user in the single book display page (see Figure 14.4). Both this tracking and the user's theme selection are enabled for both authenticated and unauthenticated users.

Finally, if the user is authenticated, the profile system is used to maintain the user's wish list of books (shown in Figure 14.5).