Capturing User Settings using Serialization and Isolated Storage...

When using retail- or news-based web sites, you probably noticed that certain information entered into the site was retained in the system. This is user profiling in action, where an application retains settings about an individual user, or a population group as a whole.


By: Brian Mains Date: July 7, 2004 Download the code.

When using retail- or news-based web sites, you probably noticed that certain information entered into the site was retained in the system. This information is then populated automatically for you, such as zip codes, previous search settings, etc. One example of this is Amazon.com; based on books that you view or buy, Amazon.com compiles a list of other related books, which is then available on the home page (instead of a generic list of books). Not every book in this list is an interest to the user; however, this process tailors more of the results to that user’s preferences. This is user profiling in action, where an application retains settings about an individual user, or a population group as a whole. Web sites do this for many reasons: to make it easier for the user to browse the site, to bring up products and services that you might be interested in, to collect demographics information, etc.

Collecting information about a population group for a region can provide greater marketing capabilities. For businesses that store merchandise in a warehouse, knowing which regions purchase more products of one type than another region can be very helpful. It can also make the organization more profitable by reducing the total cost of shipping freight between stores. If a company knows that a particular product sells better in Pennsylvania instead of Ohio, by sending more of that product to Pennsylvania reduces any amounts of freight that would have to be transferred between stores later.

This scenario doesn't necessarily need to be for a retail business; it may be desirable to store previous searches to allow the user to remember what they've searched on in the past. You may have seen searches involving zip codes or area codes may retain that information for you to search on again. An example would be movie listings, where the content will change over time.

Be careful what information you capture, as some information captured may not be allowed by law. In addition, a privacy policy exists for companies collecting information about users, and your site may need one too.

Introduction

Some of the technologies that will be used to store user information in applications will be: isolated storage, XML, and XML Serialization. Isolated storage is a private file system to the assembly; this storage area is unknown to the user; however, through the objects defined in the System.IO.IsolatedStorage namespace, folders, and files can be created, deleted, or modified through those objects. To understand more about Isolated Storage, please read my article on DotNetJohn.com.

Isolated storage uses the IsolatedStorageFile object to connect to the isolated store. Typically a store is isolated by user and identity, although domain and roaming isolation can also be specified. A file in isolated storage is accessed by the IsolatedStorageFileStream object, which is then passed to a stream reader/writer object to perform the IO operations.

Dim objStore As IsolatedStorageFile = IsolatedStorageFile.GetUserStoreForAssembly()
Dim objFile As New IsolatedStorageFileStream("IsoStoreFolder/MyFile.txt", _
  FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read, objStore)

Dim objReader As New StreamReader(objFile)
Response.Write(objReader.ReadToEnd())

Storing information at the database level requires more server involvement, but is a more secure way of protecting more sensitive data. The key to securing a database within your application is to protect the user ID/password or the connection string as a whole. If it is in clear text format, or in a place that is potentially accessible, your database could then vulnerable. Storing the data in the machine.config or registry will offer more security over storing it in the web.config, which could be vulnerable to a hacking attack. This type of example will not be shown in the code attached; however, the example was added because databases can be an important part of the application.

Dim objConnection As New SqlConnection(strConnectionString)
Dim objAdapter As New SqlDataAdapter("sp_selectUsers", objConnection)
objAdapter.SelectCommand.CommandType = CommandType.StoredProcedure

objAdapter.SelectCommand.Parameters.Add("@UserID", SqlDbType.VarChar, 32).Value = User.Identity.Name
Dim tblUser As New DataTable
objAdapter.Fill(tblUser)

'Custom profile class that stores information retrieved
'from database
Dim objUser As New UserProfile(User.Identity)
With objUser
Dim objRow As DataRow = tblUser.Rows(0)
  .FullName = objRow("FullName")
  .EmailAddress = objRow("EmailAddress")
  .PhoneNumber = objRow("PhoneNumber")
  .IsAdministrator = objRow("IsAdministrator")
  .EmailPreferences = objRow("EmailPreferences")
  '... and more ...
End With

Serialization is the process of converting a .NET object to another representation (such as XML), and vice versa. During serialization, the object is converted into an XML file, which copies both the object structure and the data into an XML structure. Only public properties/variables are retained in serialization. In addition, any objects that are public children to the serialized object are also converted to XML. De-serialization is the process of converting the XML back into the .NET object. This object is then accessible in the application.

Another possible alternative would be cookies. Cookies are client-based but are accessible at the server-level. Cookies will not be discussed in this article.

Terminology

You will see the terms categories and subcategories throughout the article. These terms are used to note areas of news that are covered. For example, the categories would be all of the areas covered by the News Service. In the example code attached, the categories are News, Sports, and Health. Subcategories are categories beneath News, Sports, and Health. For the News category example, subcategories would be Business, Country, International, Local, etc. Only a few categories were created for simplicity, and the News category will be the category used as an example throughout the article.

In addition, the term site layout is the custom page class that each web page will inherit. The component used to generate the site is called the WebSkeleton, created by Peter Lanoie. More information will be provided at the end of the article.

This article will take advantage of some of these capabilities of storage to retain user settings within the application. In addition, the objects capturing these settings will be serialized and stored via isolated storage. The code featured in this article and in the project attached will be a news site; it won't be a complete example, as that would be a large effort for an example, but minimally it will illustrate the use of serialization and isolated storage to store preferences. To start, let’s create the class that will store the counters for each class. For simplicity, each class implements public integers that can be incremented or decremented as needed. This approach is not advisable, because the values could be accidentally altered.

Public NotInheritable Class HealthStats
  Public Count As Integer
  Public EatingRightCount As Integer
  Public WorkingOutCount As Integer
  Public PreventingDiseaseCount As Integer
End Class

Public NotInheritable Class NewsStats
  Public Count As Integer
  Public BusinessCount As Integer
  Public CountryCount As Integer
  Public InternationalCount As Integer
  Public LocalCount As Integer
  Public OddStoriesCount As Integer
  Public PoliticsCount As Integer
  Public TechnicalCount As Integer
End Class

Public NotInheritable Class SportsStats
  Public Count As Integer
  Public BaseballCount As Integer
  Public BasketballCount As Integer
  Public CollegeSportsCount As Integer
  Public FootballCount As Integer
  Public HockeyCount As Integer
  Public SoccerCount As Integer
End Class

The statistics classes shown above are accessible as properties through the UserProfile class. In the constructor of the class, each of these objects is instantiated.

Public Class UserProfile
  Public Health As HealthStats
  Public News As NewsStats
  Public Sports As SportsStats

  Public Sub New()
    'Do nothing
    Health = New HealthStats
    News = New NewsStats
    Sports = New SportsStats
  End Sub
End Class

Isolated storage stores the serialized version of the UserProfile class, which is retrieved in the GetProfile method. The GetProfile method is responsible for retrieving the XML file and converting it back into the object, which is assigned to the Profile property of the UserProfile class. This Profile property is then accessible to every form that inherits it.

Protected Sub GetProfile()
  Dim objStore As IsolatedStorageFile
  Dim objFile As IsolatedStorageFileStream
  Dim objXML As XmlSerializer

  Try
    objStore = IsolatedStorageFile.GetUserStoreForAssembly()
    objFile = New IsolatedStorageFileStream("MNSUserProfile.xml", _
      IO.FileMode.OpenOrCreate, IO.FileAccess.ReadWrite, objStore)

    objXML = New XmlSerializer(GetType(UserProfile))
    Profile = objXML.Deserialize(objFile)

    'If the object doesn't exist, create it
    If (IsNothing(Profile)) Then
      Profile = New UserProfile
    End If

    objFile.Close()
    objStore.Dispose()
    objStore.Close()

  Catch objEx As System.Exception
    Throw New System.Exception("The user profile could not be retrieved.")
  End Try
End Sub

After the profile is altered, it must be saved, or all changes are lost; this is due to the nature of the web environment. The SaveProfile method is responsible for converting the UserProfile instanced (stored in the Profile property) back into an XML file with the updates intact.

Protected Sub SaveProfile()
  Dim objStore As IsolatedStorageFile
  Dim objFile As IsolatedStorageFileStream
  Dim objXML As XmlSerializer

  Try
    objStore = IsolatedStorageFile.GetUserStoreForAssembly()
    objFile = New IsolatedStorageFileStream("MNSUserProfile.xml", _
      IO.FileMode.Create, IO.FileAccess.Write, objStore)

    objXML = New XmlSerializer(GetType(UserProfile))
    objXML.Serialize(objFile, Profile)
    objFile.Close()
    objStore.Dispose()
    objStore.Close()

  Catch ex As Exception
    Throw New System.Exception("Cannot store user profile object")
  End Try
End Sub

The GetProfile and SaveProfile methods come in to play below. Each category link is created in the site layout's constructor, which only a part of the code is shown below. The site layout's constructor is responsible for the creation of the template for the site. The links to choose the desired news category are created dynamically in the site layout, and appended to the form control. An event handler is configured that, upon clicking the link, the profile is retrieved, the appropriate counter is updated, and the profile is saved.

When the link is clicked (also shown below), the event handler for the link is responsible to get the serialized object and update the changes (through GetProfile and SaveProfile). This method also updates the appropriate counter, and redirects to the appropriate news page.

'Link is created in the Site Layout’s new constructor
Dim objLink As New LinkButton
With objLink
  .ID = "lnkNews"
  .Text = "News"
  AddHandler .Click, AddressOf lnkNews_Click
End With
.Controls.Add(objLink)

'Event handler for the news click event outside new constructor
Private Sub lnkNews_Click(ByVal sender As Object, ByVal e As EventArgs)
  'If the profile object doesn't exist, get it
  If (IsNothing(Profile)) Then
    'Get the XML file and convert it to an object
    GetProfile()
  End If

  'Increment the news count
  Profile.News.Count += 1
  'Save the object; convert back into an XML file
  SaveProfile()
  'Redirect to the news page
  Page.Response.Redirect("news.aspx", False)
End Sub

The subcategory links for the News category work in the same manner. In the news.aspx page which has all of the news stories for the day, the subcategory links are created here in the same manner that the category links were created. The creation statements will not be reiterated here; however, to show only the desired subcategory links, a LoadData method filters out the stories based on the selected category type (Business, Country, Local, etc.). When the subcategory link is clicked, the profile is updated and the selected stories are shown. Both the LoadData and the business (for this example) click events are shown below for subcategories:

Private Function LoadData(ByVal objType As CurrentLinkType) As DataView
  Dim objDS As New DataSet
  objDS.ReadXml(Server.MapPath(Request.ApplicationPath & "/xml/news.xml"))

  Dim objView As DataView = objDS.Tables(0).DefaultView
  Select Case (objType)
    Case CurrentLinkType.Business
      objView.RowFilter = "Category = 'Business'"
    Case CurrentLinkType.Country
      objView.RowFilter = "Category = 'Country'"
    Case CurrentLinkType.International
      objView.RowFilter = "Category = 'International'"
    Case CurrentLinkType.Local
      objView.RowFilter = "Category = 'Local'"
    Case CurrentLinkType.OddStories
    objView.RowFilter = "Category = 'OddStories'"
    Case CurrentLinkType.Politics
      objView.RowFilter = "Category = 'Politics'"
    Case CurrentLinkType.Technology
      objView.RowFilter = "Category = 'Technology'"
  End Select

  rptDocuments.DataSource = objView
  rptDocuments.DataBind()
End Function

Private Sub lnkBusiness_Click(ByVal sender As Object, ByVal e As EventArgs)
  LoadData(CurrentLinkType.Business)

  If (IsNothing(Me.Profile)) Then
    Me.GetProfile()
  End If

  Me.Profile.News.BusinessCount += 1
  Me.SaveProfile()
End Sub

A look at the XML version of the UserProfile class stored in isolated storage is shown below. All of the public properties of the statistics classes are shown in the XML content. The data for these entries is carried over among many for the user.

<?xml version="1.0"?>
<UserProfile xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Health>
    <Count>11</Count>
    <EatingRightCount>1</EatingRightCount>
    <WorkingOutCount>1</WorkingOutCount>
    <PreventingDiseaseCount>1</PreventingDiseaseCount>
  </Health>
  <News>
    <Count>28</Count>
    <BusinessCount>0</BusinessCount>
    <CountryCount>0</CountryCount>
    <InternationalCount>1</InternationalCount>
    <LocalCount>1</LocalCount>
    <OddStoriesCount>0</OddStoriesCount>
    <PoliticsCount>1</PoliticsCount>
    <TechnicalCount>0</TechnicalCount>
  </News>
  <Sports>
    <Count>19</Count>
    <BaseballCount>1</BaseballCount>
    <BasketballCount>1</BasketballCount>
    <CollegeSportsCount>1</CollegeSportsCount>
    <FootballCount>2</FootballCount>
    <HockeyCount>5</HockeyCount>
    <SoccerCount>1</SoccerCount>
  </Sports>
</UserProfile>

Why is this important or needed? Well, it depends on your application. Continuing with the Amazon.com example, the site tracks your preferences and creates what it thinks is a list of books that it believes you may like. Every time you click on an individual book to look at, the site can track that information, which can be used to compile book listings of other related books. It often does that by subjects. If you choose a book in Nonfiction, it will compile other related Nonfiction books that it thinks you may like. How that will help is by customizing the home page to provide information that the user may want to see, instead of one common page.

About the Code

The code attached includes an assembly created by Peter Lanoie, named WebSkeleton. WebSkeleton is a .Net class used for building web application infrastructure and simplifying the common problem of site "templating" in ASP.net. More information can be found at http://www.geekdork.com/WebSkeleton/.

The example provided contains the profile object at the page level. This was done to make the examples easier to follow (I hope). An alternative approach to serializing/deserializing the object is at the Session_Begin and Session_End methods in the global.asax file. Additionally, using a database to host the files over XML would have many advantages, including:

XML was used to be a common data source for the example. Dynamic link generation is also another alternative, as each link would be dynamically based on data from the database or other source, reducing the impact of code changes. Secondly, the gray bar in the header has many alternative uses, such as:

In addition, using a hash table or a collection object to store the counters would be more advisable, instead of using variables. More code maintenance would be involved, in addition to more repetition that isn't needed.

You may download the code here.