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 virtual rather than abstract), 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 abstract class PersonalizationProvider : ProviderBase
{
    // Properties
    public abstract string ApplicationName { get; set; }

    // Virtual methods
    protected virtual IList CreateSupportedUserCapabilities() {}
    public virtual PersonalizationScope DetermineInitialScope
        (WebPartManager webPartManager,
        PersonalizationState loadedState) {}
    public virtual IDictionary DetermineUserCapabilities
        (WebPartManager webPartManager) {}
    public virtual PersonalizationState LoadPersonalizationState
        (WebPartManager webPartManager, bool ignoreCurrentUser) {}
    public virtual void ResetPersonalizationState
        (WebPartManager webPartManager) {}
    public virtual void SavePersonalizationState
       (PersonalizationState state) {}

    // Abstract methods
    public abstract PersonalizationStateInfoCollection FindState
        (PersonalizationScope scope, PersonalizationStateQuery query,
        int pageIndex, int pageSize, out int totalRecords);
    public abstract int GetCountOfState(PersonalizationScope scope,
        PersonalizationStateQuery query);
    protected abstract void LoadPersonalizationBlobs
        (WebPartManager webPartManager, string path, string userName,
        ref byte[] sharedDataBlob, ref byte[] userDataBlob);
    protected abstract void ResetPersonalizationBlob
        (WebPartManager webPartManager, string path, string userName);
    public abstract int ResetState(PersonalizationScope scope,
        string[] paths, string[] usernames);
    public abstract int ResetUserState(string path,
        DateTime userInactiveSinceDate);
    protected abstract void SavePersonalizationBlob
        (WebPartManager webPartManager, string path, string userName,
        byte[] dataBlob);
}

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

Method or Property Description
ApplicationName The 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.
CreateSupportedUserCapabilities Returns 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.
DetermineInitialScope Used by WebPartPersonalization to determine whether the initial scope of previously loaded personalization state is shared or per-user.
DetermineUserCapabilities Returns 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.
LoadPersonalizationState Retrieves 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.
ResetPersonalizationState Deletes 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.
SavePersonalizationState Writes 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.
FindState Returns 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.
GetCountOfState Returns 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.
LoadPersonalizationBlobs Retrieves 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.
ResetPersonalizationBlob Deletes personalization state corresponding to a specified user and a specified page from the data source.
ResetState Deletes personalization state corresponding to the specified users and specified pages from the data source.
ResetUserState Deletes user personalization state corresponding to the specified pages and that hasn't been updated since a specified date from the data source.
SavePersonalizationBlob Writes personalization state corresponding to a specified user and a specified page as an opaque blob to the data source. If userName is null (Nothing in Visual Basic), 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 abstract methods, and optionally to override key virtuals 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 abstract 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 abstract 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

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

public class TextFilePersonalizationProvider : PersonalizationProvider
{
    public override string ApplicationName
    {
        get { throw new NotSupportedException(); }
        set { throw new NotSupportedException(); }
    }

    public override void Initialize(string name,
        NameValueCollection config)
    {
        // Verify that config isn't null
        if (config == null)
            throw new ArgumentNullException("config");

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

        // Add a default "description" attribute to config if the
        // attribute doesn't exist or is empty
        if (string.IsNullOrEmpty(config["description"]))
        {
            config.Remove("description");
            config.Add("description",
                "Text file personalization provider");
        }

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

        // Throw an exception if unrecognized attributes remain
        if (config.Count > 0)
        {
            string attr = config.GetKey(0);
            if (!String.IsNullOrEmpty(attr))
                throw new ProviderException
                    ("Unrecognized attribute: " + attr);
        }

        // Make sure we can read and write files in the
        // ~/App_Data/Personalization_Data directory
        FileIOPermission permission = new FileIOPermission
            (FileIOPermissionAccess.AllAccess,
            HttpContext.Current.Server.MapPath
            ("~/App_Data/Personalization_Data"));
        permission.Demand();
    }

    protected override void LoadPersonalizationBlobs
        (WebPartManager webPartManager, string path, string userName,
        ref byte[] sharedDataBlob, ref byte[] userDataBlob)
    {
        // Load shared state
        StreamReader reader1 = null;
        sharedDataBlob = null;

        try
        {
            reader1 = new StreamReader(GetPath(null, path));
            sharedDataBlob =
                Convert.FromBase64String(reader1.ReadLine());
        }
        catch (FileNotFoundException)
        {
            // Not an error if file doesn't exist
        }
        finally
        {
            if (reader1 != null)
                reader1.Close();
        }

        // Load private state if userName holds a user name
        if (!String.IsNullOrEmpty (userName))
        {
            StreamReader reader2 = null;
            userDataBlob = null;

            try
            {
                reader2 = new StreamReader(GetPath(userName, path));
                userDataBlob =
                    Convert.FromBase64String(reader2.ReadLine());
            }
            catch (FileNotFoundException)
            {
                // Not an error if file doesn't exist
            }
            finally
            {
                if (reader2 != null)
                    reader2.Close();
            }
        }
    }

    protected override void ResetPersonalizationBlob
        (WebPartManager webPartManager, string path, string userName)
    {
        // Delete the specified personalization file
        try
        {
            File.Delete(GetPath(userName, path));
        }
        catch (FileNotFoundException) {}
    }

    protected override void SavePersonalizationBlob
        (WebPartManager webPartManager, string path, string userName,
        byte[] dataBlob)
    {
        StreamWriter writer = null;

        try
        {
            writer = new StreamWriter(GetPath (userName, path), false);
            writer.WriteLine(Convert.ToBase64String(dataBlob));
        }
        finally
        {
            if (writer != null)
                writer.Close();
        }
    }

    public override PersonalizationStateInfoCollection FindState
        (PersonalizationScope scope, PersonalizationStateQuery query,
        int pageIndex, int pageSize, out int totalRecords)
    {
        throw new NotSupportedException();
    }

    public override int GetCountOfState(PersonalizationScope scope,
        PersonalizationStateQuery query)
    {
        throw new NotSupportedException();
    }

    public override int ResetState(PersonalizationScope scope,
        string[] paths, string[] usernames)
    {
        throw new NotSupportedException();
    }

    public override int ResetUserState(string path,
        DateTime userInactiveSinceDate)
    {
        throw new NotSupportedException();
    }

    private string GetPath(string userName, string path)
    {
        SHA1CryptoServiceProvider sha =
            new SHA1CryptoServiceProvider(); 
        UnicodeEncoding encoding = new UnicodeEncoding ();
        string hash = Convert.ToBase64String(sha.ComputeHash
            (encoding.GetBytes (path))).Replace ('/', '_');
        
        if (String.IsNullOrEmpty(userName))
            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('\\', '_'), hash));
        }
    }
}

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

© Microsoft Corporation. All rights reserved.