Web Parts Personalization Providers

 

Introduction to the Provider Model
Membership Providers
Role Providers
Site Map Providers
Session State Providers
Profile Providers
Web Event Providers
Web Parts Personalization Providers
Custom Provider-Based Services
Hands-on Custom Providers: The Contoso Times

Web Parts personalization providers provide the interface between ASP.NET's Web Parts personalization service and personalization data sources. The two most common reasons for writing a custom Web Parts personalization provider are:

  • You wish to store personalization data in a data source that is not supported by the Web Parts personalization providers included with the .NET Framework, such as an Oracle database.
  • You wish to store personalization data in a SQL Server database whose schema differs from that of the database used by System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider.

The fundamental job of a Web Parts personalization provider is to provide persistent storage for personalization state-state regarding the content and layout of Web Parts pages-generated by the Web Parts personalization service. Personalization state is represented by instances of System.Web.UI.WebControls.WebParts.PersonalizationState. The personalization service serializes and deserializes personalization state and presents it to the provider as opaque byte arrays. The heart of a personalization provider is a set of methods that transfer these byte arrays to and from persistent storage.

The PersonalizationProvider Base Class

Developers writing custom personalization providers begin by deriving from System.Web.UI.WebControls.WebParts.PersonalizationProvider, which derives from ProviderBase and adds several methods and properties defining the characteristics of a Web Parts personalization provider. Because many PersonalizationProvider methods come with default implementations (that is, are overridable rather than mustoverride), a functional provider can be written by overriding as little as three or four members in a derived class. PersonalizationProvider is prototyped as follows:

Public MustInherit Class PersonalizationProvider 
   Inherits ProviderBase
  ' Properties
  Public MustOverride Property ApplicationName() As String

  ' overridable methods
  Protected Overridable Function _
CreateSupportedUserCapabilities() As IList

  Public Overridable Function DetermineInitialScope( _
   ByVal webPartManager As WebPartManager, _
   ByVal loadedState As PersonalizationState) As PersonalizationScope

  Public Overridable Function DetermineUserCapabilities( _
   ByVal webPartManager As WebPartManager) As IDictionary

  Public Overridable Function LoadPersonalizationState( _
   ByVal webPartManager As WebPartManager, _
   ByVal ignoreCurrentUser As Boolean) As PersonalizationState

  Public Overridable Sub ResetPersonalizationState( _
   ByVal webPartManager As WebPartManager)

  Public Overridable Sub SavePersonalizationState( _
   ByVal state As PersonalizationState)


  ' MustOverride methods
  Public MustOverride Function FindState( _
   ByVal scope As PersonalizationScope, _
   ByVal query As PersonalizationStateQuery, _
   ByVal pageIndex As Integer, _
   ByVal pageSize As Integer, _
   ByRef totalRecords As Integer _
   ) As PersonalizationStateInfoCollection

  Public MustOverride Function GetCountOfState( _
   ByVal scope As PersonalizationScope, _
   ByVal query As PersonalizationStateQuery) As Integer

  Protected MustOverride Sub LoadPersonalizationBlobs( _
   ByVal webPartManager As WebPartManager, _
   ByVal path As String, _
   ByVal userName As String, _
   ByRef sharedDataBlob As Byte(), _
   ByRef userDataBlob As Byte())

  Protected MustOverride Sub ResetPersonalizationBlob( _
   ByVal webPartManager As WebPartManager, _
   ByVal path As String, ByVal userName As String)

  Public MustOverride Function ResetState( _
   ByVal scope As PersonalizationScope, _
   ByVal paths As String(), _
   ByVal usernames As String()) As Integer

  Public MustOverride Function ResetUserState( _
   ByVal path As String, _
   ByVal userInactiveSinceDate As DateTime) As Integer
 
  Protected MustOverride Sub SavePersonalizationBlob( _
   ByVal webPartManager As WebPartManager, _
   ByVal path As String, _
   ByVal userName As String, ByVal dataBlob As Byte())

End Class

The following table describes PersonalizationProvider's members and provides helpful notes regarding their implementation:

Method or PropertyDescription
ApplicationNameThe name of the application using the personalization provider. ApplicationName is used to scope personalization state so that applications can choose whether to share personalization state with other applications. This property can be read and written.
CreateSupportedUserCapabilitiesReturns a list of WebPartUserCapability objects indicating which users can access shared personalization state and which are permitted to save personalization state. User capabilities can be specified in an <authorization> element in the <personalization> section of the <webParts> configuration section. By default, all users can read shared personalization data, and all users can read and write user-scoped personalization data.
DetermineInitialScopeUsed by WebPartPersonalization to determine whether the initial scope of previously loaded personalization state is shared or per-user.
DetermineUserCapabilitiesReturns a dictionary of WebPartUserCapability objects indicating whether the current user can access shared personalization state and save personalization state. User capabilities can be specified in an <authorization> element in the <personalization> section of the <webParts> configuration section. By default, all users can access shared state and all can save personalization settings.
LoadPersonalizationStateRetrieves raw personalization data from the data source and converts it into a PersonalizationState object. The default implementation retrieves the current user name (unless instructed not to with the ignoreCurrentUser parameter) and path from the specified WebPartManager. Then it calls LoadPersonalizationBlobs to get the raw data as a byte array and deserializes the byte array into a PersonalizationState object.
ResetPersonalizationStateDeletes personalization state from the data source. The default implementation retrieves the current user name and path from the specified WebPartManager and calls ResetPersonalizationBlob to delete the corresponding data.
SavePersonalizationStateWrites a PersonalizationState object to the data source. The default implementation serializes the PersonalizationState object into a byte array and calls SavePersonalizationBlob to write the byte array to the data source.
FindStateReturns a collection of PersonalizationStateInfo objects containing administrative information regarding records in the data source that match the specified criteria-for example, records corresponding to users named Jeff* that have been modified since January 1, 2005. Wildcard support is provider-dependent.
GetCountOfStateReturns the number of records in the data source that match the specified criteria-for example, records corresponding to users named Jeff* that haven't been modified since January 1, 2005. Wildcard support is provider-dependent.
LoadPersonalizationBlobsRetrieves personalization state as opaque blobs from the data source. Retrieves both shared and user personalization state corresponding to a specified user and a specified page.
ResetPersonalizationBlobDeletes personalization state corresponding to a specified user and a specified page from the data source.
ResetStateDeletes personalization state corresponding to the specified users and specified pages from the data source.
ResetUserStateDeletes user personalization state corresponding to the specified pages and that hasn't been updated since a specified date from the data source.
SavePersonalizationBlobWrites personalization state corresponding to a specified user and a specified page as an opaque blob to the data source. If userName is Nothing, then the personalization state is shared state and is not keyed by user name.

Your job in implementing a custom Web Parts personalization provider is to provide implementations of PersonalizationProvider's mustoverride methods, and optionally to override key overridables such as Initialize. In most cases, the default implementations of PersonalizationProvider's LoadPersonalizationState, ResetPersonalizationState, and SavePersonalizationState methods will suffice. However, you may override these methods if you wish to modify the binary format in which personalization state is stored-though doing so also requires deriving from PersonalizationState to support custom serialization and deserialization.

You can build a provider that's capable of persisting the personalization state generated as users modify the content and layout of Web Parts pages by implementing three key PersonalizationProvider methods: LoadPersonalizationBlobs, ResetPersonalizationBlob, and SavePersonalizationBlob. It is highly recommended that you implement GetCountOfState, too, because that method is called by WebPartPersonalization.HasPersonalizationState, which in turn is called by the WebPartPageMenu control (which was present in beta 1 and is still available as a sample). Other mustoverride methods inherited from PersonalizationProvider are administrative in nature and should be implemented by a fully featured provider but are not strictly required.

Scoping of Personalization Data

Web Parts personalization state is inherently scoped by user name and request path. Scoping by user name allows personalization state to be maintained independently for each user. Scoping by path ensures that personalization settings for one page don't affect personalization settings for others. The Web Parts personalization service also supports shared state, which is scoped by request path but not by user name. (When the service passes shared state to a provider, it passes in a null user name.) When storing personalization state, a provider must take care to key the data by user name and request path so it can be retrieved using the same keys later.

In addition, all Web Parts personalization providers inherit from PersonalizationProvider a property named ApplicationName whose purpose it to scope the data managed by the provider. Applications that specify the same ApplicationName when configuring the Web Parts personalization service share personalization state; applications that specify unique ApplicationNames do not. Web Parts personalization providers that support ApplicationName must associate personalization state with application names so operations performed on personalization data sources can be scoped accordingly.

As an example, a provider that stores Web Parts personalization data in a SQL database might use a command similar to the following to retrieve personalization state for the user named "Jeff" and the application named "Contoso:"

SELECT * FROM PersonalizationState
WHERE UserName='Jeff' AND Path='~/Default.aspx'
AND ApplicationName='Contoso'

The final AND in the WHERE clause ensures that other applications containing personalization state keyed by the same user name and path don't conflict with the "Contoso" application.

TextFilePersonalizationProvider

Listing 1 contains the source code for a PersonalizationProvider-derivative named TextFilePersonalizationProvider that demonstrates the minimum functionality required of a Web Parts personalization provider. It implements the three key mustoverride PersonalizationProvider methods-LoadPersonalizationBlobs, ResetPersonalizationBlob, and SavePersonalizationBlob-but provides trivial implementations of the others. Despite its simplicity, TextFilePersonalizationProvider is fully capable of reading and writing the personalization state generated as users customize the layout and content of Web Parts pages.

TextFilePersonalizationProvider stores personalization state in text files in the application's ~/App_Data/Personalization_Data directory. Each file contains the personalization state for a specific user and a specific page and consists of a single base-64 string generated from the personalization blob (byte array) passed to SavePersonalizationBlob. The file name, which is generated by combining the user name and a hash of the request path, indicates which user and which path the state corresponds to and is the key used to perform lookups. (Shared personalization state is stored in a file whose name contains a hash of the request path but no user name.) You must create the ~/App_Data/Personalization_Data directory before using the provider; the provider doesn't attempt to create the directory if it doesn't exist. In addition, the provider must have read/write access to the ~/App_Data/Personalization_Data directory.

Listing 1. TextFilePersonalizationProvider

Imports System
Imports System.Configuration.Provider
Imports System.Security.Permissions
Imports System.Web
Imports System.Web.UI.WebControls.WebParts
Imports System.Collections.Specialized
Imports System.Security.Cryptography
Imports System.Text
Imports System.IO

Public Class TextFilePersonalizationProvider
    Inherits PersonalizationProvider
    Public Overrides Property ApplicationName() As String
        Get
            Throw New NotSupportedException()
        End Get
        Set(ByVal value As String)
            Throw New NotSupportedException()
        End Set
    End Property

    Public Overrides Sub Initialize(ByVal name As String, _
        ByVal config As NameValueCollection)
        ' Verify that config isn't null
        If config Is Nothing Then
            Throw New ArgumentNullException("config")
        End If

        ' Assign the provider a default name if it doesn't have one
        If String.IsNullOrEmpty(name) Then
            name = "TextFilePersonalizationProvider"
        End If

        ' Add a default "description" attribute to config if the
        ' attribute doesn't exist or is empty
        If String.IsNullOrEmpty(config("description")) Then
            config.Remove("description")
            config.Add("description", _
            "Text file personalization provider")
        End If

        ' Call the base class's Initialize method
        MyBase.Initialize(name, config)

        ' Throw an exception if unrecognized attributes remain
        If config.Count > 0 Then
            Dim attr As String = config.GetKey(0)
            If (Not String.IsNullOrEmpty(attr)) Then
                Throw New ProviderException( _
                "Unrecognized attribute: " & attr)
            End If
        End If

        ' Make sure we can read and write files in the
        ' ~/App_Data/Personalization_Data directory
        Dim permission As FileIOPermission = New FileIOPermission( _
            FileIOPermissionAccess.AllAccess, _
            HttpContext.Current.Server.MapPath( _
            "~/App_Data/Personalization_Data"))
        permission.Demand()
    End Sub

    Protected Overrides Sub LoadPersonalizationBlobs( _
        ByVal webPartManager As WebPartManager, _
        ByVal path As String, ByVal userName As String, _
        ByRef sharedDataBlob As Byte(), _
        ByRef userDataBlob As Byte())
        ' Load shared state
        Dim reader1 As StreamReader = Nothing
        sharedDataBlob = Nothing

        Try
            reader1 = New StreamReader(GetPath(Nothing, path))
            sharedDataBlob = Convert.FromBase64String( _
                reader1.ReadLine())
        Catch e1 As FileNotFoundException
            ' Not an error if file doesn't exist
        Finally
            If Not reader1 Is Nothing Then
                reader1.Close()
            End If
        End Try

        ' Load private state if userName holds a user name
        If (Not String.IsNullOrEmpty(userName)) Then
            Dim reader2 As StreamReader = Nothing
            userDataBlob = Nothing

            Try
                reader2 = New StreamReader(GetPath(userName, path))
                userDataBlob = Convert.FromBase64String( _
                    reader2.ReadLine())
            Catch e2 As FileNotFoundException
                ' Not an error if file doesn't exist
            Finally
                If Not reader2 Is Nothing Then
                    reader2.Close()
                End If
            End Try
        End If
    End Sub

    Protected Overrides Sub ResetPersonalizationBlob( _
        ByVal webPartManager As WebPartManager, _
        ByVal path As String, ByVal userName As String)
        ' Delete the specified personalization file
        Try
            File.Delete(GetPath(userName, path))
        Catch e1 As FileNotFoundException
        End Try
    End Sub

    Protected Overrides Sub SavePersonalizationBlob( _
        ByVal webPartManager As WebPartManager, _
        ByVal path As String, ByVal userName As String, _
        ByVal dataBlob As Byte())
        Dim writer As StreamWriter = Nothing

        Try
            writer = New StreamWriter(GetPath(userName, path), False)
            writer.WriteLine(Convert.ToBase64String(dataBlob))
        Finally
            If Not writer Is Nothing Then
                writer.Close()
            End If
        End Try
    End Sub

    Public Overrides Function FindState( _
        ByVal scope As PersonalizationScope, _
        ByVal query As PersonalizationStateQuery, _
        ByVal pageIndex As Integer, _
        ByVal pageSize As Integer, _
        ByRef totalRecords As Integer _
        ) As PersonalizationStateInfoCollection
        Throw New NotSupportedException()
    End Function

    Public Overrides Function GetCountOfState( _
        ByVal scope As PersonalizationScope, _
        ByVal query As PersonalizationStateQuery) As Integer
        Throw New NotSupportedException()
    End Function

    Public Overrides Function ResetState( _
        ByVal scope As PersonalizationScope, _
        ByVal paths As String(), _
        ByVal usernames As String()) As Integer
        Throw New NotSupportedException()
    End Function

    Public Overrides Function ResetUserState( _
        ByVal path As String, _
        ByVal userInactiveSinceDate As DateTime) As Integer
        Throw New NotSupportedException()
    End Function

    Private Function GetPath(ByVal userName As String, _
        ByVal path As String) As String
        Dim sha As SHA1CryptoServiceProvider = _
            New SHA1CryptoServiceProvider()
        Dim encoding As UnicodeEncoding = New UnicodeEncoding()
        Dim hash As String = Convert.ToBase64String( _
        sha.ComputeHash(encoding.GetBytes(path))).Replace( _
            "/"c, "_"c)

        If String.IsNullOrEmpty(userName) Then
            Return HttpContext.Current.Server.MapPath( _
                String.Format( _
        "~/App_Data/Personalization_Data/{0}_Personalization.txt", _
        hash))
        Else
            ' NOTE: Consider validating the user name here to prevent
            ' malicious user names such as "../Foo" from targeting
            ' directories other than ~/App_Data/Personalization_Data

            Return HttpContext.Current.Server.MapPath( _
            String.Format( _
         "~/App_Data/Personalization_Data/{0}_{1}_Personalization.txt", _
              userName.Replace("\"c, "_"c), hash))
        End If
    End Function
End Class

Listing 2 demonstrates how to make TextFilePersonalizationProvider the default Web Parts personalization provider. It assumes that TextFilePersonalizationProvider is implemented in an assembly named CustomProviders.

Listing 2. Web.config file making TextFilePersonalizationProvider the default Web Parts personalization provider

<configuration>
  <system.web>
    <webParts>
      <personalization
        defaultProvider="AspNetTextFilePersonalizationProvider">
        <providers>
          <add name="AspNetTextFilePersonalizationProvider"
            type="TextFilePersonalizationProvider, CustomProviders"/>
        </providers>
      </personalization>
    </webParts>
</configuration>

For simplicity, TextFilePersonalizationProvider does not honor the ApplicationName property. Because TextFilePersonalizationProvider stores personalization state in text files in a subdirectory of the application root, all data that it manages is inherently application-scoped. A full-featured profile provider must support ApplicationName so Web Parts consumers can choose whether to keep personalization data private or share it with other applications.

Click here to continue on to part 8, Custom Provider-Based Services.

Show: