User Profile Data

User Profile Data

ASP.NET has a feature that allows you to easily track various kinds of data for each user. This is basically a persistent version of the ASP.NET session state storage mechanism. It is often used to remember per-user defaults between sessions, such as the particular skin a user likes, or even default values for forms. This can be enabled to store data only for authenticated users or to support anonymous users as well. You define the name and types of the data that you want to store in Profile data using the web.config file for the application. You can then set and retrieve the specific data values from your server-side code. Although the Profile feature is similar to the Session object, it goes beyond it in several key ways. By default, the data is stored in the built-in database available to ASP.NET applications, so it is automatically persisted between user sessions. Session data is not - it is stored in memory by default, but even if you choose to utilize SQL Server to hold session data, it will always be lost at the end of the user’s current session. Also, Profile data is strongly typed, making it easier to code against and avoiding some common developer errors. Session data is only treated as the basic object type and must be converted by you to the original type, leading to possible runtime exceptions if the wrong type is used by mistake. ASP.NET provides the aspnet_regsql.exe tool that can be used to configure a backend SQL Server with the tables necessary to store the profile information. The Profile information can also be made available in a server farm deployment scenario, since the database is always used as the persistence medium.

Defining Profile Data

The first step in using the Profile feature is to establish the names and types of the data that you want to track. The following section from a web.config file declares five properties to be tracked as Profile data for each user. Note that in addition to the name and type, you can control whether or not the data is tracked for anonymous users. This is useful when a subset of users may authenticate in parts of the application and you want to store information for only that group, so you may not want to hold data for anonymous users. Conversely, maybe you have some personalization features that you specifically want to open up to anonymous users. Therefore, if the same anonymous user visits your site again, he will see that you were able to remember some facts about him, even though he didn’t create a user account. Also, notice that you can define what type of serialization to utilize for storing the data as is done with the Artists.


<profile enabled="true" defaultProvider="SampleProvider">
  <providers>
       <add name="SampleProvider" type="Wrox.Samples.WroxProfileProvider" />
     </providers>
     <properties>
       <add name="Genre" type="System.String" allowAnonymous="true" />
       <add name="Artists" type="System.Collections.ArrayList"
allowAnonymous="true" serializeAs="Binary" />
       <add name="LastAlbumPurchased" type="Album" allowAnonymous="true"/>

       <group name="FavoriteAlbum">
         <add name="Artist" type="System.String" allowAnonymous="true" />
         <add name="Title" type="System.String" allowAnonymous="true"/>
       </group>
     </properties>
   </profile>

At the top level, you can have either items or groups. Groups are just a way to organize related items. The Title and Artist are in the FavoriteAlbum group. By grouping items together, you get nesting semantics, which can be especially helpful if there are a lot of Profile properties because Intellisense can help you find the item you’re looking for. If you don’t use groups and the list of Profile items is long, you may have to scroll through a lot of items to find what you want. Figure 7-2 shows that the Profile items become available in the code editor and Intellisense helps you select the desired items and groups.

Image from book
Figure 7-2

The type specified for the LastAlbumPurchased is just listed as Album, which does not exist in the .NET Framework. It is defined in a class located in the App_Code directory of the application. It is a simple class that tracks the artist, title, and year of release, as seen in Listing 7-5 (Album.cs). Because the default serialization will work fine for this class and its basic types, nothing special is required to configure or use it.

Listing 7-5
Image from book

using System;

public class Album
{
    private String _artist;
    private String _title;
    private int _releaseYear;

  public Album() {
  }

    public String Artist {
        get {
            return _artist;
        }
        set {
            _artist = value;
        }
    }

    public String Title {
        get {
            return _title;
        }
        set {
            _title = value;
        }
    }

    public int ReleaseYear {
        get {
            return _releaseYear;
        }
        set {
            _releaseYear = value;
        }
    }
} 
Image from book

Accessing Profile Properties

Once the properties are defined, you can access them directly from the Profile intrinsic on any page. The Profile intrinsic is the object available in any ASP.NET page for retrieving and setting the properties for the current user that have been defined as part of the profile in the configuration. Another nice aspect of the Profile feature, as compared to using session state storage, is the strongly typed nature of the data.

In this example, an integer and string are stored in Session state. Because Session state treats everything as an object, they have to be explicitly cast to the desired type when they are read.

Session["count"] = 96;
Session["where"] = "anywhere";
count = (int)Session["count"];
where = (string)Session["where"];

If you were to declare the count and where variables are as Profile properties, the code becomes much easier to read and write.

Session["count"] = 96;
Session["where"] = "anywhere";
count = (int)Session["count"];
where = (string)Session["where"];

Listing 7-6 (Profile.aspx) is a sample .aspx page showing Profile storage in action. It links to other pages that show the current values of the Profile properties by loading them from the Profile storage and displaying them in the browser. There is also a page that makes updates to the Profile.

Listing 7-6
Image from book

<%@ Page Language="C#"%>
<script language="C#" runat="server">
protected override void OnLoad(EventArgs e) {
    Profile.FavoriteAlbum.Artist = "Phish";
    Profile.FavoriteAlbum.Title = "Rift";
    Profile.Genre = "Other";
    Profile.Artists.Clear();
    Profile.Artists.Add("Phish");
    Profile.Artists.Add("The Chemical Brothers");
    Profile.Artists.Add("Bryan Ferry");
    Profile.LastAlbumPurchased.Artist = "The Chemical Brothers";
    Profile.LastAlbumPurchased.Title = "Push The Button";
    Profile.LastAlbumPurchased.ReleaseYear = 2006;
}
</script>
<!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>Untitled Page</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <asp:HyperLink runat="server" NavigateUrl="~/ShowProfile.aspx">
Show Profile</asp:HyperLink>    
    <asp:HyperLink runat="server" NavigateUrl="~/PreLoadedProfileItems.aspx">
PreLoaded Profile Items</asp:HyperLink><br />
    <asp:HyperLink runat="server" NavigateUrl="~/LoadProfileItems.aspx">
Load Profile Items</asp:HyperLink><br />
    <asp:HyperLink runat="server" NavigateUrl="~/SaveProfileItems.aspx">
Save Profile Items</asp:HyperLink><br />
    </div>
    </form>
</body>
</html>
Image from book

When this page is requested, the Profile is populated with some music information, which is just hard-coded in the page’s Page_Load event handler. Clicking the Show Profile link takes you to a page (ShowProfile.aspx) that retrieves these same property values from the Profile and renders them to the browser. The result is shown in Figure 7-3.

Image from book
Figure 7-3

Accessing Profile Data from the Browser

So far, the examples have shown how to set up and use the Profile feature on the server. These are all tasks that can be done with ASP.NET 2.0. With ASP.NET AJAX, however, you can also access the Profile information directly from JavaScript code running in the browser! The Profile properties and groups have already been defined in the application’s configuration file. But this alone does not make the Profile data available remotely. You also have to explicitly opt in to the ability to use the Profile feature from the browser. This is done in the system.web.extensions section of the web.config file. In addition to setting enabled to true for the profileService element, you need to elaborate which properties should be made available. The read and write access permissions are controlled independently so that you can further control how the Profile data is used. If you know that you won’t need to modify Profile values on the client, you should omit the writeAccessProperties.

<system.web.extensions>
   <scripting>
     <webServices>
     <profileService enabled="true"
         readAccessProperties="Genre, Artists, FavoriteAlbum.Artist,
FavoriteAlbum.Title, LastAlbumPurchased"
         writeAccessProperties="Genre, Artists, FavoriteAlbum.Artist,
FavoriteAlbum.Title, LastAlbumPurchased" />    
     </webServices>
   </scripting>
 </system.web.extensions>

ASP.NET AJAX adds a new handler for access to the Membership and Profile services. For IIS versions earlier than 7, or IIS7 running in the Classic mode, this setting is in the httpHandlers configuration section under system.web.

      <system.web>
       <httpHandlers>
       <remove verb="*" path="*.asmx"/>
       <add verb="*" path="*_AppService.axd" validate="false" 
 type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions,
 Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
       </httpHandlers>
       </system.web>

And for the IIS7 Integrated mode, the setting is in the system.webserver section in the handlers element.

    <system.webserver>
    <handlers>
      <remove name="WebServiceHandlerFactory-ISAPI-2.0"/>
      <add name="ScriptHandlerFactoryAppServices" verb="*" path="*_AppService.axd"
preCondition="integratedMode"
           type="System.Web.Script.Services.ScriptHandlerFactory, 
System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, 
PublicKeyToken=31bf3856ad364e35"/>
    </handlers>
    </system.webserver> 

The configuration is now in place. You have defined a set of Profile properties, enabled the service for remote access, and declared the properties to be both readable and writeable. Now you need a page that accesses the Profile store from JavaScript.

Preload Profile Properties

The set of data being stored in the Profile can be quite extensive. It would be wasted overhead to send all of the Profile information down to the browser in the original page rendering if there’s only a small chance that you’ll need it all. The default is not to preload any Profile properties, but this isn’t ideal for the pages where you know you will need at least some Profile data. The ScriptManager’s ProfileService element has a LoadProperties attribute that allows you to specify what properties should be made available initially and sent down with the first page rendering.

In this example, the Genre and Artists would be available in the initial page rendering. Client script could make use of them without making any calls to the server to get more data. However, access to any other Profile data will still require going back to the server.

<asp:ScriptManager runat="server" ID="scriptManager">
    <ProfileService LoadProperties="Genre, Artists" />
</asp:ScriptManager>

The preloaded properties can be accessed directly from the properties object of the ProfileService object, which is part of the Microsoft AJAX Library.

var artists = Sys.Services.ProfileService.properties.Artists;

Thankfully, there is no requirement to provide callback functions or to pause and wait for an asynchronous call to the server to return the data for the preloaded properties. Listing 7-7 (PreLoadedProfileItems.aspx) is a page that adds a button and binds a click handler to it. The han-dler then shows the Genre and Artists Profile properties declared for preloading.

Listing 7-7
Image from book

<!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>PreLoaded Profile Items</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="scriptManager">
        <ProfileService LoadProperties="Genre, Artists" />
    </asp:ScriptManager>
    <div>
    <input type="button" id="showProfile" value="Show Profile" />
    </div>
    </form>
</body>
<script type="text/javascript">
function pageLoad() {
    $addHandler($get('showProfile'), 
}

function showProfile() {    
    var genre = "Favorite Genre : " + Sys.Services.ProfileService.properties.Genre;
    alert(genre);
    var artists = Sys.Services.ProfileService.properties.Artists;
    var message = "Favorita Artists:\r\n";
    for(var index = 0; index < artists.length; index++) {
        message += artists[index] + "\r\n";
    }
    alert(message);
}
</script>
</html>
Image from book

Figure 7-4 shows the Artists displayed by the browser. The ArrayList on the server is translated into a JavaScript object, where the individual elements of the ArrayList are accessed through the object’s indexer.

Image from book
Figure 7-4

Load Profile Properties

All of the Profile properties you might want to access while the page is running do not have to be preloaded. As long as they are declared in the web.config file as accessible remotely, Profile properties can be loaded on demand at any time. However, when making a call to retrieve those properties that were not preloaded, you need to provide a callback function, since there’s no way to predict how long that data-fetching operation will take. You can also make a call back to the server to refetch preloaded values, in case their value may have been changed since the page was loaded.

There are four arguments to the load method of the ProfileService. The first argument is an array of the names of the properties to be retrieved. The second is the callback function for completing the call. The third is the error callback. The last argument is for any user context data that should be passed to the completion events.

Sys.Services.ProfileService.load(
    ["FavoriteAlbum.Artist", "FavoriteAlbum.Title"],       loadCompletedCallback, 
      failedCallback, 
      null );

It is not necessary to pass anything for the userContext parameter unless you want to tell your callback something extra about this particular call. In this example, I include the fourth parameter with a null that is actually redundant. JavaScript implies a null value for any parameter that you don’t explicitly provide. I like to be explicit about it, as doing so helps me make fewer mistakes by thinking through what is needed and what is not. It’s also easier to come back to this code and add some context data in place of the null because I can see where that value should go.

The arguments to your completion callback function are the result of the call to the server method, the userContext that was passed to the load function, and the name of the method that was called. In this case, the result is the count of properties that were loaded.

function loadCompletedCallback(result, context, methodName) {
    alert("Retrieved " + result + " profile properties.");
}

The AJAX error object passed to the failed callback provides access to the type of exception that occurred and its error message. You can also get the stack trace in effect at the time the exception occurred.

function failedCallback(error, context, methodName) {
    var message = error.get_exceptionType() + "\r\n" +
        error.get_message() + "\r\n" +
        error.get_stackTrace();
    alert(message);
}
Image from book
Figure 7-5

The page in Listing 7-8 (LoadProfileItems.aspx) has two buttons: one for loading Profile data and the other for displaying it. It binds the event handlers to the button click events in the pageLoad method. It also adds an error handler to display more detailed information if something goes wrong. Displaying a stack trace may be helpful during development, but when the application is deployed, the end user won’t be able to do anything useful with the information, so more robust and user-friendly error recovery should be added before final production deployment.

Listing 7-8
Image from book
 <!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>Load Profile Items</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="scriptManager">
        <ProfileService/>
    </asp:ScriptManager>
    <div>
    <input type="button" id="loadFavorite" value="Load Favorite" /><br />
    <input type="button" id="showFavorite" value="Show Favorite" />
    </div>
    </form>
</body>
<script type="text/javascript">
function pageLoad() {
    $addHandler($get('[showFavorite'), 'click ', showFavorite);
    $addHandler($get('loadFavorite'), 'click ', loadFavorite);

}
function showFavorite() {
    alert("Favorite Album\r\n" + 
        Sys.Services.ProfileService.properties.FavoriteAlbum.Title + 
        " by " +
        Sys.Services.ProfileService.properties.FavoriteAlbum.Artist);
}

function loadFavorite() {
    Sys.Services.ProfileService.load(["FavoriteAlbum.Artist", 
"FavoriteAlbum.Title"], loadCompletedCallback, failedCallback, null);
}

function loadCompletedCallback(result, context, methodName) {
    alert("Retrieved " + result + " profile properties.");
}

function failedCallback(error, context, methodName) {
    var message = error.get_exceptionType() + "\r\n" +
        error.get_message() + "\r\n" +
        error.get_stackTrace();
    alert(message);
}
</script>
</html>
Image from book

Saving Profile Data

When Profile data is updated in the browser, it needs to be saved back to the server to be persisted for later use. The signature of the save function is the same as that of the load function. The first argument now indicates what set of Profile properties should be saved. If you don’t include a set of properties, all of the properties on the client will be sent. This can be overkill if only a few are actually in need of being updated.

function saveFavorite() {
    var profile = Sys.Services.ProfileService;
    profile.properties.Artists[0] = 
    profile.save(["Artists"], saveCompletedCallback, failedCallback, null);
}

When working with custom types, like the Album example, there are a few additional requirements to be aware of. The Profile Service counts on the custom type having a parameterless constructor and property setters for each custom type that it creates. This is a standard requirement in order to deserialize data. The Profile Service doesn’t make other assumptions about how to deserialize data beyond requiring a list of name-value pairs for creating the type. To create an Album object to be saved in the Profile, the Artist, Title, and ReleaseYear are specified along with the values. The syntax can seem a little odd at first, but the simplicity avoids the need for any custom serialization.

function addLastPurchased() {
    var profile = Sys.Services.ProfileService;
    profile.properties.LastAlbumPurchased = 
       { Artist: "Irina", Title: "ala riko kaavaa", ReleaseYear: "2001" };
    profile.save(
        ["LastAlbumPurchased"], 
        saveCompletedCallback, 
        failedCallback, 
        null );
}

When updating the properties of a custom type that was already loaded, the name-value pair syntax is not necessary. The properties of custom objects are accessed in the same way as common .NET Framework types. You can access the property directly on the Profile property.

var lastArtistPurchased = 
Sys.Services.ProfileService.properties.LastAlbumPurchased.Artist;

Or you can access the Profile’s version of the custom type and pass the name of the property as an indexer argument. This would be most useful if the name of the property were being extracted from a JavaScript variable.

var albumTitle = 
Sys.Services.ProfileService.properties.LastAlbumPurchased["Title"];

The key thing to remember when working with custom types that are part of the Profile is that in the JavaScript representation, no custom serialization or advanced type conversion occurs. Instead, the properties are reflected as simple types when sent down to the browser. And when they are sent to the server, they are treated as name-value pairs, where the name indicates the property accessor to invoke, and the value is the argument to pass to the property setter. Listing 7-9 (SaveProfileItems.aspx) is a page that displays Profile information, allows the user to modify the first element of the Artists array, and updates the last album purchased in a button’s click event handler.

Listing 7-9
Image from book

<!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>Save Profile Items</title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager runat="server" ID="scriptManager">
        <ProfileService LoadProperties="Artists, LastAlbumPurchased" />
    </asp:ScriptManager>
    <div><b>Favorite Artist : </b><input type="text" id="favoriteArtist" /><br />
    <input type="button" id="saveFavorite" value="Save" /><br /><br />
    <asp:Label runat="server" ID="artists" /><br /><br />
    <input type="button" id="addLast" value="Add Album"/>
    <asp:Label runat="server" ID="lastArtistPurchased" />
    </div>
    </form>
</body>
<script type="text/javascript">
var favoriteArtist = "";

function pageLoad() {
    $get('favoriteArtist').value = 
Sys.Services.ProfileService.properties.Artists[0];
    $addHandler($get('saveFavorite'), 
    $addHandler($get('addLast'), 
    updateLabel();
}

function updateLabel() {
    var theArtists = Sys.Services.ProfileService.properties.Artists;
    var theLabel = "";
    for(var i = 0; i < theArtists.length; i++) {
        theLabel += theArtists[i] + "<br />";
    }
    $get('artists').innerHTML = theLabel;
    $get('lastArtistPurchased').innerHTML
Sys.Services.ProfileService.properties.LastAlbumPurchased["Artist"];    
}

function saveFavorite() {
    var profile = Sys.Services.ProfileService;
    profile.properties.Artists[0] = 
    profile.save(["Artists"], saveCompletedCallback, failedCallback, null); 
}

function addLastPurchased() {
    var profile = Sys.Services.ProfileService;
    profile.properties.LastAlbumPurchased = 
       { Artist: "Irina", Title: "ala riko kaavaa", ReleaseYear: "2001" };
    profile.save(
        ["LastAlbumPurchased"], 
        saveCompletedCallback, 
        failedCallback, 
        null );
}

function saveCompletedCallback(result, context, methodName) {
    updateLabel();   
    alert("Saved " + result + " profile properties.");
}

function failedCallback(error, context, methodName) {
    var message = error.get_exceptionType() + "\r\n" +
        error.get_message() + "\r\n" +
        error.get_stackTrace();
    alert(message);
}

</script>
</html>
Image from book

Profile data, whether they are simple types or complex custom types, can now be accessed easily from JavaScript code in the browser. You can retrieve and update data on the fly without a full postback of the page.