The heart of the Web Part Framework is personalization. When users interact with a website built with Web Parts, individual users can customize the website according to their personal preferences. They can pick and choose the Web Parts that they want to display in their pages, rearrange the layout of Web Parts to their hearts’ desires, and they can customize the properties of particular Web Parts.
This chapter leaps into the details of personalization. You learn how to enable users to personalize both Web Part pages and individual Web Parts. You also learn how to administer a Web Parts application by pruning stale personalization data. Finally, you learn how to create custom personalization providers. At the end of this chapter, we create both a custom query string personalization provider and an anonymous personalization provider.
When a user modifies the layout or content of a page that contains Web Parts, the personalization changes are scoped to two things: the page and the user. Each page in an application can have different personalization data associated with it. If you change the name of a page, all personalization data that was associated with the page is lost and you must personalize the page again from scratch.
Personalization data is scoped to a page’s application relative path. If you move a Web Part application from one server to another, personalization data is not lost. However, if you rename an application, the personalization data is lost.
Personalization is also scoped to the user. The Web Part Framework supports two types of personalization: User and Shared. By default, when a user customizes a Web Part page, the changes have User scope. Each user of a Web Part application can personalize the same application in different ways.
You can also make Shared scoped hanges to a Web Part application. A Shared scope change, unlike a User scope change, has an effect on all users of a Web Part application. Typically, you want to enable only a select group of administrators to make Shared scope changes to an application.
Shared and User scoped personalization data is merged when a user requests a page. For example, an administrator can add a standard set of Web Parts to a page. A particular user can add an additional set of Web Parts. When the page is displayed, both sets of Web Parts are displayed.
So how is all this personalization data stored? The Web Part framework uses the provider model, so it is really up to you. Later in this chapter, we’ll extend the ASP.NET Framework with our own custom personalization providers.
The default and only personalization provider included in the ASP.NET framework is the SqlPersonalizationProvider. This provider stores personalization data in two database tables: aspnet_PersonalizationAllUsers and aspnet_PersonalizationPerUser. The first table contains the personalization data that is scoped to all users, and the second table contains the personalization data that is scoped to a particular user. By default, these database tables are located in the ASPNETDB.mdf
SQL Server 2005 Express database, located in your application’s APP_Data folder.
Under the covers, the WebPartPersonalization
class is responsible for all of the low-level operations related to personalization. This class acts as a bridge between the WebPartManager
control and a particular personalization provider.
The WebPartPersonalization
class is exposed by the WebPartManager
control’s Personalization
property. You access the properties and methods of the WebPartPersonalization
class through the WebPartManager
control.
The WebPartPersonalization
class has a number of useful properties:
CanEnterSharedScope
—. Use this property to determine whether the current user can make changes to the page that have Shared personalization scope (has an effect on all users).
Enabled
—. Use this property to disable personalization for the current page.
HasPersonalizationState
—. Use this property to determine whether any personalization data is associated with the current page and the current user, given the current personalization scope.
InitialScope
—. Use this property to place a page in either Shared or User personalization scope.
IsEnabled
—. Use this property to determine whether personalization is currently enabled for this page.
IsModified
—. Use this property to determine whether the current user can make changes to personalization data.
ProviderName
—. Use this property to retrieve or set the name of the personalization provider.
Scope
—. Use this property to determine the current personalization scope (User or Shared).
Furthermore, the WebPartPersonalization
class has two important methods:
ResetPersonalizationState
—. Deletes personalization data associated with the current page and the current user, given the current personalization scope.
ToggleScope
—. Toggles the current personalization scope between User personalization scope and Shared personalization scope (or vice versa).
When working with personalization, it helps to see exactly how personalization data is being stored. In the examples in this section, we take advantage of the properties and methods of the WebPartPersonalization
class to create a Personalization Manager. The Personalization Manager is contained in Listing 29.1.
Example 29.1. PersonalizationManager.ascx
<%@ Control Language="VB" ClassName="PersonalizationManager" %> <script runat="server"> ''' <summary> ''' Display Personalization Information ''' </summary> Private Sub Page_PreRender() Dim wpm As WebPartManager = WebPartManager.GetCurrentWebPartManager(Page) lblCurrentScope.Text = wpm.Personalization.Scope.ToString() lblIsModifiable.Text = wpm.Personalization.IsModifiable.ToString() lblCanEnterSharedScope.Text = wpm.Personalization.CanEnterSharedScope.ToString() lblHasPersonalizationState.Text = wpm.Personalization.HasPersonalizationState.ToString() lnkToggleScope.Visible = wpm.Personalization.CanEnterSharedScope End Sub ''' <summary> ''' Switches to Shared Scope ''' </summary> Protected Sub lnkToggleScope_Click(ByVal sender As Object, ByVal e As EventArgs) Dim wpm As WebPartManager = WebPartManager.GetCurrentWebPartManager(Page) wpm.Personalization.ToggleScope() End Sub ''' <summary> ''' Deletes Personalization data ''' </summary> Protected Sub lnkReset_Click(ByVal sender As Object, ByVal e As EventArgs) Dim wpm As WebPartManager = WebPartManager.GetCurrentWebPartManager(Page) wpm.Personalization.ResetPersonalizationState() End Sub </script> <div class="personalizationManager"> Current Scope: <asp:Label id="lblCurrentScope" Runat="server" /> Can Modify State: <asp:Label id="lblIsModifiable" Runat="server" /> Can Enter Shared Scope: <asp:Label id="lblCanEnterSharedScope" Runat="server" /> Has Personalization State: <asp:Label id="lblHasPersonalizationState" Runat="server" /> <span> <asp:LinkButton id="lnkToggleScope" Text="Toggle Scope" OnClick="lnkToggleScope_Click" Runat="server" /> </span> <asp:LinkButton id="lnkReset" Text="Reset Personalization" Runat="server" OnClick="lnkReset_Click" /> </div>
The Personalization Manager displays an information bar across the top of a page (see Figure 29.1). The bar displays the values of the following properties:
Current Scope
—. Displays whether the page is in User or Shared scope personalization mode.
Can Modify State
—. Displays whether the user can modify the personalization state associated with the page.
Can Enter Shared Scope
—. Displays whether the user can enter Shared Personalization scope.
Has Personalization State
—. Displays whether the current page, given the current personalization scope, has personalization data associated with it.
The Personalization Manager includes links to invoke the following methods:
Toggle Scope
—. Clicking this link switches the page from User to Shared personalization scope (or back again). This link appears only when the user can enter Shared personalization scope.
Reset Personalization
—. Clicking this link removes the personalization data associated with the current personalization scope. Clicking this link in User scope personalization mode removes all User scoped personalization data, and clicking the link in Shared scope personalization mode removes all Shared scoped personalization data.
You can use the Personalization Manager in any Web Part page. For example, you can experiment with the Personalization Manager with the page in Listing 29.2.
Example 29.2. ShowPersonalizationManager.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="PersonalizationManager" Src="~/PersonalizationManager.ascx" %> <%@ Register TagPrefix="user" TagName="FirstSimplePart" Src="~/FirstSimplePart.ascx" %> <%@ Register TagPrefix="user" TagName="SecondSimplePart" Src="~/SecondSimplePart.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .personalizationManager { border:dotted 2px orange; padding:5px; background-color:White; font:12px Arial, Sans-Serif; } .personalizationManager span { padding-right:10px; margin-right:10px; border-right:solid 1px black; } .personalizationManager a { text-decoration:none; } .column { float:left; width:40%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Personalization Manager</title> </head> <body> <form id="form1" runat="server"> <user:PersonalizationManager id="PersonalizationManager1" Runat="Server" /> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server"> <ZoneTemplate> <user:FirstSimplePart id="FirstSimplePart1" Title="First Web Part" Description="Our first simple Web Part" Runat="server" /> <user:SecondSimplePart id="SecondSimplePart1" Title="Second Web Part" Description="Our second simple Web Part" Runat="server" /> </ZoneTemplate> </asp:WebPartZone> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server" /> </form> </body> </html>
When you first open the page in Listing 29.2, the Personalization Manager displays the fact that no personalization data is associated with the page. If you click the Design link, and re-arrange the Web Parts in the page, you create new personalization data. You can click the Reset Personalization link at any time to clear away any personalization data associated with the page.
To this point, we have relied on User scoped personalization exclusively. All personalization data has been scoped to a particular user. In this section, you’ll learn how to enable Shared personalization scope so that you can enable administrators to make changes to an application for everyone.
You’ll also learn how to configure the SqlPersonalization provider to store personalization data in a particular database in your computer network.
To enable User and Shared scope personalization, you must authorize the group of users allowed to make personalization changes. By default, all users are allowed to make User personalization changes and no users are allowed to make Shared personalization changes.
The default personalization configuration settings are contained in the root Web.Config
file located at the following path:
WindowsMicrosoft.NETFrameworkv2.0.xxxxxconfig
The default authorization section is contained in Listing 29.3.
The first authorization rule prevents any user from entering Shared authorization scope. The second authorization rule enables any user to personalize Web Part pages.
To authorize an administrator to make Shared personalization changes, you need to override these settings in the default Web configuration file. If you add the configuration file in Listing 29.4 to the root of your application, then anyone who is a member of the Administrators role can make Shared personalization changes.
Example 29.4. Web.Config
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <webParts> <personalization> <authorization> <allow users="*" verbs="modifyState" /> <allow roles="Administrators" verbs="enterSharedScope" /> </authorization> </personalization> </webParts> </system.web> </configuration>
The Web.Config
file in Listing 29.4 is saved with the name Web.Config_listing4
on the CD so that it does not interfere with the other code samples in this chapter.
After you authorize a user or role to enter Shared personalization scope, you can place a page into Shared personalization mode in either of two ways. First, you can take advantage of the InitialScope
property of the WebPartPersonalization
class. If you want a page to open in Shared personalization mode by default, then you can declare the WebPartManager
control in the page like this:
<asp:WebPartManager id="WebPartManager1" Personalization-InitialScope="Shared" Runat="server" />
This WebPartManager control declaration causes the page to enter Shared scope personalization mode by default. If a user requests the page and the user is authorized to enter Shared scope, then changes made to the page have Shared scope. If the user is not authorized to make Shared scope changes, then the page remains in User scope personalization mode (no exception is thrown).
You cannot modify the IntialScope
property after the Page PreInit
event. What this means, in practice, is that you need to set this property declaratively. The ToggleScope()
method gets around this limitation by automatically performing a Server.Transfer()
back to the same page.
An alternate and in many situations better way to change personalization scope is to take advantage of the ToggleScope()
method. Calling this method switches the page between User and Shared personalization scope.
The Personalization Manager created in Listing 29.1 includes a link that invokes the ToggleScope()
method. If you want to experiment with Shared personalization scope, you can add the Personalization Manager to a Web Part Page and toggle between User and Shared scope.
By default, all personalization data is saved in a SQL Server 2005 Express database named ASPNETDB.mdf
, located in your application’s APP_Data
folder. If you want to store personalization data in another SQL Server database on your network (for example, a SQL Server 2000 or SQL Server 2005 database) then you need to do two things:
You need to set up the new database.
You need to modify your application’s web configuration file.
First, you need to add the necessary database objects for personalization to the new database. The ASP.NET Framework includes a command-line tool named aspnet_regsql
, which automatically installs the necessary objects. This tool is located at the following path:
WindowsMicrosoft.NETFrameworkv2.0.xxxxxaspnet_regsql.exe
If you run the tool without any parameters, the tool displays a wizard that walks you through the steps required for configuring a database for personalization (see Figure 29.2).
Alternatively, you can execute a set of SQL Server batch files directly against a database. This is useful when you don’t want to set up the .NET Framework on the server hosting your database. The same folder that contains the aspnet_regsql.exe
tool contains the following four scripts:
InstallCommon.sql
—. Installs the database objects that are used by several ASP.NET services such as the aspnet_Users table. Run this script first.
InstallPersonalization.sql
—. Installs the database objects particular to Web Part personalization. Run this script after executing InstallCommon.sql
.
UninstallCommon.sql
—. Removes the objects added by InstallCommon.sql
.
UninstallPersonalization.sql
—. Removes the objects added by InstallPersonalization.sql
.
After you add the necessary database objects for personalization, you can point any of your ASP.NET applications at the new database. For example, the Web configuration file in Listing 29.5 causes an application to store personalization data in a SQL Server database named AppData located on a server named DataServer.
Example 29.5. Web.Config
<?xml version="1.0" encoding="utf-8"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <connectionStrings> <add name="DataServer" connectionString="Server=DataServer;Trusted_Connection=true; Database=AppData"/> </connectionStrings> <system.web> <webParts> <personalization defaultProvider="MyPersonalizationProvider"> <providers> <add name="MyPersonalizationProvider" type="System.Web.UI.WebControls.WebParts.SqlPersonalizationProvider" connectionStringName="DataServer" /> </providers> <authorization> <allow users="*" verbs="modifyState" /> <allow roles="Administrators" verbs="enterSharedScope" /> </authorization> </personalization> </webParts> </system.web> </configuration>
The Web.Config
file in Listing 29.5 is saved with the name Web.Config_listing5
on the CD so that it does not interfere with the other code samples in this chapter.
The web configuration file in Listing 29.5 contains a <webParts>
section that contains a <personalization>
sub-section. The defaultProvider
attribute points to a provider named MyPersonalizationProvider
defined in the <providers>
section. This provider uses the SqlPersonalization
provider and uses a database connection string named DataServer
.
The DataServer
connection string is defined in the <connectionStrings>
configuration section at the top of the configuration file. This connection string points to a server named DataServer
and a database named AppData
. You can, of course, provide a connection string for any database in your network.
Users can edit Web Part properties at runtime. For example, you might want to create a Web Part that displays database data. You can enable users to modify the SQL select query that returns the database records.
The Web Part Framework automatically saves the value of any property marked with the Personalizable attribute. By default, a personalizable property is scoped to a particular user. In other words, each user can personalize a property in different ways. However, you also have the option of creating a personalizable property that can be modified only in Shared user scope.
The Web Part in Listing 29.6, the DataPart Web Part, enables you to display records from any database table. The Web Part includes one personalizable property that has User scope—the SelectCommand
property—and one property that has Shared scope—the ConnectionString
property.
Example 29.6. DataPart.ascx
<%@ Control Language="VB" ClassName="DataPart" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <script runat="server"> Private _connectionString As String = String.Empty Private _selectCommand As String = String.Empty <Personalizable(PersonalizationScope.Shared)> _ <WebBrowsable> _ Public Property ConnectionString() As String Get Return _connectionString End Get Set (ByVal Value As String) _connectionString = value End Set End Property <Personalizable> _ <WebBrowsable> _ Public Property SelectCommand() As String Get Return _selectCommand End Get Set (ByVal Value As String) _selectCommand = value End Set End Property Private Sub Page_PreRender() If _connectionString <> String.Empty And_selectCommand <> String.Empty Then Try Dim dad As SqlDataAdapter = New SqlDataAdapter(_selectCommand,_connectionString) Dim dst As DataSet = New DataSet() dad.Fill(dst) grdData.DataSource = dst grdData.DataBind() Catch e As Exception lblError.Text = e.Message End Try End If End Sub </script> <asp:Label id="lblError" EnableViewState="false" Runat="server" /> <asp:GridView id="grdData" Runat="server" />
Notice that the first property in Listing 29.6, the ConnectionString
property, is decorated with a Personalizable attribute that includes a PersonalizationScope.Shared
parameter. This property can be edited only by an administrator who can enter Shared personalization mode.
The second property, SelectCommand
, is also decorated with the Personalizable
attribute. This property can be modified in either Shared or User personalization mode. Modifying the property in Shared personalization mode provides the property with a default value for everyone. (Individual users can override the default value.)
You can experiment with the DataPart Web Part with the page in Listing 29.7. This page includes the Personalization Manager so you can toggle between User and Shared scope.
Example 29.7. ShowDataPart.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="user" TagName="DataPart" Src="~/DataPart.ascx" %> <%@ Register TagPrefix="user" TagName="PersonalizationManager" Src="~/PersonalizationManager.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .personalizationManager { border:dotted 2px orange; padding:5px; background-color:White; font:12px Arial, Sans-Serif; } .personalizationManager span { padding-right:10px; margin-right:10px; border-right:solid 1px black; } .personalizationManager a { text-decoration:none; } .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Show Data Part</title> </head> <body> <form id="form1" runat="server"> <user:PersonalizationManager id="PersonalizationManager1" Runat="Server" /> <asp:WebPartManager id="WebPartManager1" Runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> <asp:MenuItem Text="Edit" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server"> <ZoneTemplate> <user:DataPart id="DataPart1" Title="Data Part" Description="Displays database records" Runat="server" /> </ZoneTemplate> </asp:WebPartZone> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server" /> <asp:EditorZone id="EditorZone1" CssClass="column" Runat="server"> <ZoneTemplate> <asp:PropertyGridEditorPart id="PropertyGridEditorPart1" Runat="server" /> </ZoneTemplate> </asp:EditorZone> </form> </body> </html>
After you open the page in Listing 29.7, you can edit the properties of the DataPart Web Part by clicking the Edit link. The PropertyGridEditorPart
control automatically renders a property sheet, which enables you to modify the values of the ConnectionString
and SelectCommand
properties (see Figure 29.3).
The ConnectionString
property appears only when you enter Shared personalization scope by clicking the Toggle Scope link in the Personalization Manager control’s information bar. The SelectCommand
property, on the other hand, can be altered in either Shared or User personalization mode.
The CD includes a sample database in the App_Data folder named MyDatabase.mdf
, which includes a Movie database table. You can connect to this database with the following connection string:
Server=.SQLExpress;Trusted_Connection=true; AttachDbFileName=|DataDirectory|MyDatabase.mdf;User Instance=true
And execute the following SQL SELECT statement:
SELECT * FROM Movies
In the previous section, you learned how to use the Personalizable
attribute with properties that represent simple types such as Strings and Integers. You also can use the Web Part Framework to automatically persist the values of properties that represent more complex types such as ArrayLists or custom classes.
Behind the scenes, the Web Part Framework uses the ObjectStateFormatter
class to serialize and deserialize the values of Web Part properties. This is a powerful class. It can serialize the state of any class that can be represented statically.
You should not use personalization with a property that returns an instance of a class defined in the App_Code folder. If you make changes to the App_Code folder, the contents of the folder are automatically recompiled into a new assembly. Because the assembly name changes with each recompilation, the Web Part Framework cannot automatically serialize and deserialize classes defined in the assembly.
However, one important limitation of the Web Part Framework relates to complex properties. The Web Part Framework can detect changes to simple properties automatically, but the framework cannot detect changes made to more complex properties automatically. In general, the Web Part Framework can detect changes to immutable properties, but not changes made to mutable properties.
A mutable type is a type that has properties or fields that can change after it is instantiated. Most reference types are mutable. Most value types are immutable.
For example, if you attempt to use the Personalizable
attribute with a property that returns an ArrayList, you don’t get an exception, but the state of the ArrayList is not saved. The Web Part Framework fails to save the state of the property because the Web Part Framework cannot detect when the ArrayList has changed.
There are two ways around this limitation. The next section in this chapter discusses how you can take advantage of the IPersonalizable
interface to take a more hands-on approach to personalization state management. When you implement the IPersonalizable
interface, you can indicate exactly when you want the Web Part Framework to save changes to a Web Part’s properties.
This section, however, explores a simpler option. The WebPart
class includes a method named the SetPersonalizationDirty()
method. There is both a shared (static) and an instance version of this method, so you can use it either when working with a Web Part created from a User Control or when working with a “True” Web Part derived from the base WebPart
class.
For example, the Web Part in Listing 29.8—the FirstTaskListPart
Web Part—enables you to create and save a task list (see Figure 29.4). The list of tasks is represented by an ArrayList.
You can view the FirstTaskListPart
by opening the ShowFirstTaskListPart.aspx
page included on the CD that accompanies this book.
Example 29.8. FirstTaskListPart.ascx
<%@ Control Language="VB" ClassName="FirstTaskListPart" %> <script runat="server"> Private _tasks As ArrayList = Nothing <Personalizable()> _ Public Property Tasks() As ArrayList Get Return _tasks End Get Set(ByVal Value As ArrayList) _tasks = value End Set End Property Private Sub Page_PreRender() grdTasks.DataSource = _tasks grdTasks.DataBind() End Sub Protected Sub btnAdd_Click(ByVal sender As Object, ByVal e As EventArgs) If _tasks Is Nothing Then _tasks = New ArrayList() End If _tasks.Add(txtNewTask.Text) WebPart.SetPersonalizationDirty(Me) End Sub </script> <asp:GridView id="grdTasks" Runat="server" /> <hr> <b>New Task:</b> <asp:TextBox id="txtNewTask" Runat="server" /> <asp:Button id="btnAdd" Text="Add" OnClick="btnAdd_Click" Runat="server" />
Notice that the WebPart.SetPersonalizationDirty()
method is called after a new item is added to the ArrayList. If you neglected to call this method, then you would never be able to add more than a single item to the Task List.
Also, it is important to notice that the _tasks
variable used to represent the tasks is initially set to Nothing
(null). The variable is initialized like this:
Private _tasks As ArrayList = Nothing
It is important that you initialize a personalizable property that represents a reference type with the value Nothing
. The very first value assigned to a personalizable property is considered the default value. The Web Part Framework compares the current value against the default value and if there are no changes, the framework does not update the saved personalization data.
Imagine that you had initialized the property like this:
Private _tasks As ArrayList = New ArrayList()
In that case, because an ArrayList is a reference type, the Web Part Framework would never detect a change in the property even when new items have been added to the ArrayList. (This is true even when you use the WebPart.SetPersonalizationDirty()
method to warn the framework that there have been changes.)
In most cases, using the Personalizable
attribute to mark the Web Part properties that you want to save automatically works fine. However, the Personalizable attribute does have some limitations:
A Personalizable property must be public.
A Personalizable property must have both a public get and set accessor.
A Personalizable property cannot have an indexer or parameter.
A Personalizable property is ignored in a nested control.
If you encounter one of these limitations, then you have no choice but to implement the IPersonalizable
interface. When you implement this interface, you are responsible for selecting the data that you want to save.
The IPersonalizable
interface includes one property and two methods that you must implement:
IsDirty
—. Return the value True
from this property when the Web Part Framework calls the Save()
method.
Load()
—. This method loads personalization state information.
Save()
—. This method saves personalization state information.
For example, Listing 29.9 contains a task list Web Part that implements the IPersonalizable
interface named SecondTaskListPart.ascx
.
Example 29.9. SecondTaskListPart.ascx
<%@ Control Language="VB" ClassName="SecondTaskListPart" %> <%@ Implements Interface="System.Web.UI.WebControls.WebParts.IPersonalizable" %> <script runat="server"> Private _tasks As ArrayList = New ArrayList() Private _isDirty As Boolean = False Public Property Tasks() As ArrayList Get Return _tasks End Get Set(ByVal Value As ArrayList) _tasks = value End Set End Property Public ReadOnly Property IsDirty() As Boolean Implements IPersonalizable.IsDirty Get Return _isDirty End Get End Property Public Sub Save(ByVal state As PersonalizationDictionary) Implements IPersonalizable.Save Dim wpm As WebPartManager = WebPartManager.GetCurrentWebPartManager(Page) state.Add("tasks", New PersonalizationEntry(_tasks, wpm.Personalization.Scope)) End Sub Public Sub Load(ByVal state As PersonalizationDictionary) Implements IPersonalizable.Load _tasks = CType(state("tasks").Value, ArrayList) End Sub Private Sub Page_PreRender() grdTasks.DataSource = _tasks grdTasks.DataBind() End Sub Protected Sub btnAdd_Click(ByVal sender As Object, ByVal e As EventArgs) _tasks.Add(txtNewTask.Text) _isDirty = True End Sub </script> <asp:GridView id="grdTasks" Runat="server" /> <hr> <b>New Task:</b> <asp:TextBox id="txtNewTask" Runat="server" /> <asp:Button id="btnAdd" Text="Add" OnClick="btnAdd_Click" Runat="server" />
You can view the SecondTaskListPart
Web Part with the ShowSecondTaskList.aspx
page on the CD that accompanies this book.
Notice that the Web Part in Listing 29.9 includes an <%@ Implements %>
directive and that the Web Part implements the IPersonalizable
interface.
The body of the Web Part contains a GridView
control, a TextBox
control, and a Button
control. When a user enters a new task description in the TextBox
and clicks the button, the btnAdd_Click()
method executes. This method adds the new task to the ArrayList and marks the Web Part as dirty.
Each time the Web Part is loaded in the page, the Load()
method is called and the task list is returned from the underlying personalization data store. Whenever the Web Part is marked as dirty—after a user enters a new task—the Save()
method is called. The Web Part framework calls this method before saving the personalization data.
Notice that both the Load()
and Save()
methods use a PersonalizationDictionary, which contains instances of the PersonalizationEntry
class. Each PersonalizationEntry represents the information being saved and the personalization scope associated with the information being saved (User or Shared). In the Save()
method, the current WebPartManager
control is used to determine the page’s current personalization scope.
A public website that uses Web Parts might have thousands of members. Each member, potentially, could personalize multiple pages in the website.
Most public websites experience significant churn. A person registers at the website, plays around with it for a few minutes, and then leaves without ever being seen again. Saving personalization data for inactive users could be a huge waste of resources.
The Web Part Framework includes a PersonalizationAdministration
class. This class includes several valuable methods for identifying and pruning inactive personalization data. Here is a list of the methods supported by this class:
FindInactiveUserState
—. Returns a collection of User personalization state information when supplied with path, username, and date parameters. This method supports wildcards in its parameters.
FindSharedState
—. Returns a collection of Shared personalization state information when supplied with a path. This method supports wildcards in its path parameter.
FindUserState
—. Returns a collection of User personalization state information when supplied with a path and username. This method supports wildcards in its parameters.
GetAllInactiveUserState
—. Returns a collection of User personalization state information when supplied with a date.
GetAllState
—. Returns a collection of personalization state information when supplied with a personalization scope.
GetCountOfInactiveUserState
—. Returns the number of user personalization items older than the supplied date parameter.
GetCountOfState
—. Returns the number of user personalization items matching a particular personalization scope.
GetCountOfUserState
—. Returns the number of user personalization items matching a certain username. This method supports wildcards in the username parameter.
ResetAllState
—. Deletes personalization information matching a particular personalization scope.
ResetInactiveUserState
—. Deletes User personalization data older than the supplied date parameter.
ResetSharedState
—. Deletes Shared state information that matches the supplied path parameter.
ResetState
—. Deletes state information that matches the contents of the supplied parameter, which represents a collection of state information.
ResetUserState
—. Deletes state information that matches either the supplied username or path parameters.
The ResetAllState()
method is the nuclear bomb of personalization administration methods. You can use this method to blow away all User and Shared state information for all users.
The other methods are useful for pruning stale state information. The page in Listing 29.10 illustrates how you can retrieve personalization state information for a particular user.
Example 29.10. AdministerPersonalization.aspx
<%@ Page Language="VB" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As EventArgs) GridView1.DataSource = PersonalizationAdministration.FindUserState(Nothing, txtUsername.Text) GridView1.DataBind() End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .grid { font: 14px Arial, Sans-Serif; } .grid td { padding:10px; } .grid th { padding:10px; background-color:orange; text-align:left; } </style> <title>Administer Personalization</title> </head> <body> <form id="form1" runat="server"> <div> <asp:Label id="lblUsername" AssociatedControlID="txtUsername" Text="Username:" Runat="server" /> <asp:TextBox id="txtUsername" Runat="server" /> <asp:Button id="btnSubmit" Text="Submit" OnClick="btnSubmit_Click" Runat="server" /> <hr /> <asp:GridView id="GridView1" CssClass="grid" Runat="server" /> </div> </form> </body> </html>
If you enter a username in the form contained in Listing 29.10 and click the Submit button, matching personalization items are displayed by the GridView
control (see Figure 29.5). The FindUserState()
method is used to retrieve the matching personalization items.
The Web Part Framework uses the Provider Model to save personalization information. The default and only Personalization Provider included with the framework is the SqlPersonalizationProvider. The beautiful thing about the Provider Model is that if you don’t like anything about the default provider, then you can easily create your own custom provider.
In this section, we create two custom personalization providers: a Query String Personalization Provider and a Anonymous Personalization Provider.
Personalization information, by default, is scoped against a particular page path. A user can personalize two different pages in two different ways. However, a user cannot personalize the same page in two different ways.
For some applications, this limitation might present a problem. For example, imagine that you are creating an online store that includes a Product.aspx
page. This page displays information on different products, depending on the value of a Query String passed to the page.
Another approach to solving the problem discussed in this section is to create a VirtualPathProvider. This option is discussed in Chapter 19, “Advanced Navigation.”
The SqlPersonalization provider does not enable you to customize the page differently, depending on the product ID passed to the page. To enable users to personalize different versions of the same product page, you need to create a custom Query String Personalization Provider.
This project contains the following two pages:
MovieList.aspx
—. This page displays a list of movies. Clicking a movie links to the MovieDetails.aspx
page, with the movie ID passed as a query string parameter.
MovieDetails.aspx
—. This Web Part page displays information on a particular movie.
The project also includes the following two Web Parts:
MoviePart.ascx
—. Displays details for a particular movie.
TextPart.ascx
—. Displays a block of text.
Finally, the project contains the following two support files:
QueryStringPersonalizationProvider
—. This class implements the custom personalization provider.
Web.Config
—. This configuration file configures the custom personalization provider.
All the files listed here are contained on the CD that accompanies the book. In this section, we concentrate on the QueryStringPersonalizationProvider
class, which is contained in Listing 29.11.
Example 29.11. QueryStringPersonalizationProvider.vb
Imports System Imports System.Web Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Custom personalization provider which takes into account the ''' id query string parameter. ''' </summary> Public Class QueryStringPersonalizationProvider Inherits SqlPersonalizationProvider ''' <summary> ''' Called when data is saved to the database ''' </summary> Protected Overrides Sub SavePersonalizationBlob(ByVal webPartManager As WebPartManager, ByVal path As String, ByVal userName As String, ByVal dataBlob() As Byte) Dim queryStringId As String = HttpContext.Current.Request("id") If Not queryStringId Is Nothing Then path += "?id=" + queryStringId End If MyBase.SavePersonalizationBlob(webPartManager, path, userName, dataBlob) End Sub ''' <summary> ''' Called when data is loaded from the database ''' </summary> Protected Overrides Sub LoadPersonalizationBlobs(ByVal webPartManager As WebPartManager, ByVal path As String, ByVal userName As String, ByRef sharedDataBlob() As Byte, ByRef userDataBlob() As Byte) Dim queryStringId As String = HttpContext.Current.Request("id") If Not queryStringId Is Nothing Then path += "?id=" + queryStringId End If MyBase.LoadPersonalizationBlobs(webPartManager, path, userName, sharedDataBlob, userDataBlob) End Sub ''' <summary> ''' Called when a user's personalization data is reset ''' </summary> Protected Overrides Sub ResetPersonalizationBlob(ByVal webPartManager As WebPartManager, ByVal path As String, ByVal userName As String) Dim queryStringId As String = HttpContext.Current.Request("id") If Not queryStringId Is Nothing Then path += "?id=" + queryStringId End If MyBase.ResetPersonalizationBlob(webPartManager, path, userName) End Sub End Class End Namespace
The class in Listing 29.11 overrides three methods of the base SqlPersonalizationProvider
class: the SavePersonalizationBlob()
, LoadPersonalizationBlob()
, and ResetPersonalizationBlob()
methods. The Blob
, in this context, refers to the serialized blob of personalization data.
In each of these methods, the value of the query string parameter named ID is added to the path associated with the state data being saved. In other words, the state data is scoped to the path and ID query string parameter.
After you add the class in Listing 29.11 to your application’s App_Code folder, you need to configure the custom personalization provider. The Web configuration file in Listing 29.12 configures the QueryStringPersonalizationProvider as the application’s default personalization provider.
Example 29.12. Web.Config
<?xml version="1.0"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <connectionStrings> <add name="Northwind" connectionString="Server=localhost;Trusted_Connection=true; Database=Northwind"/> </connectionStrings> <system.web> <webParts> <personalization defaultProvider="QueryStringPersonalizationProvider"> <providers> <add name="QueryStringPersonalizationProvider" type="myControls.QueryStringPersonalizationProvider" connectionStringName="localSQLServer" /> </providers> </personalization> </webParts> </system.web> </configuration>
The Web.Config
file in this section is named Web.Config_listing12
on the CD so that it does not interfere with the other code samples in this chapter.
You can test the QueryStringPersonalizatonProvider by opening the MovieList.aspx
page on the CD that accompanies this book in your browser. When you click on a movie title, you are linked to the MovieDetails.aspx
page, which displays a particular movie. Notice that you can personalize each version of the MovieDetails.aspx
page differently. For example, you can enter different text in the TextPart
control for each movie (see Figure 29.6 and Figure 29.7), even though the different movies are displayed by the same page.
The default SqlPersonalizationProvider stores personalization data for a user only when the user is authenticated. This requirement makes sense. Typically, you do not want to store personalization data for every random stranger who visits your website.
However, there are situations in which you might want to enable anonymous users to personalize a Web Part application. For example, you might want to create a customizable portal page, such as those used for My MSN or My Yahoo, and not require users to register at your website prior to performing the customization.
Fortunately, modifying the existing SqlPersonalizationProvider to support anonymous users is not that difficult because another part of the ASP.NET Framework already includes the infrastructure for identifying anonymous users. The ASP.NET Framework supports a feature called Anonymous Identification, which is used to support anonymous ASP.NET Profiles.
The Profile class is discussed in Chapter 22, “Maintaining Application State.”
In this section, you learn how to create an Anonymous Personalization Provider. To create this custom personalization provider, you need to modify three of the standard classes used by the Web Part Framework.
First, you need to create the Anonymous Personalization Provider class itself. This class is contained in Listing 29.13.
Example 29.13. AnonSqlPersonalizationProvider.vb
Imports System Imports System.Web Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Custom Personalizaton Provider which enables ''' anonymous personalization ''' </summary> Public Class AnonSqlPersonalizationProvider Inherits SqlPersonalizationProvider ''' <summary> ''' Saves personalization data to the database ''' </summary> Protected Overrides Sub SavePersonalizationBlob(ByVal webPartManager As WebPartManager, ByVal path As String, ByVal userName As String, ByVal dataBlob() As Byte) If Not HttpContext.Current.Request.IsAuthenticated Then userName = HttpContext.Current.Request.AnonymousID End If MyBase.SavePersonalizationBlob(webPartManager, path, userName, dataBlob) End Sub ''' <summary> ''' Loads personalization data from the database ''' </summary> Protected Overrides Sub LoadPersonalizationBlobs(ByVal webPartManager As WebPartManager, ByVal path As String, ByVal userName As String, ByRef sharedDataBlob() As Byte, ByRef userDataBlob() As Byte) If Not HttpContext.Current.Request.IsAuthenticated Then userName = HttpContext.Current.Request.AnonymousID End If MyBase.LoadPersonalizationBlobs(webPartManager, path, userName, sharedDataBlob, userDataBlob) End Sub ''' <summary> ''' Deletes personalization data from the database ''' </summary> Protected Overrides Sub ResetPersonalizationBlob(ByVal webPartManager As WebPartManager, ByVal path As String, ByVal userName As String) If Not HttpContext.Current.Request.IsAuthenticated Then userName = HttpContext.Current.Request.AnonymousID End If MyBase.ResetPersonalizationBlob(webPartManager, path, userName) End Sub ''' <summary> ''' Determines whether the page opens in User or Shared ''' personalization scope ''' </summary> Public Overrides Function DetermineInitialScope(ByVal webPartManager As WebPartManager, ByVal loadedState As PersonalizationState) As PersonalizationScope Return webPartManager.Personalization.InitialScope End Function End Class End Namespace
The AnonPersonalizationProvider overrides four methods of the base SqlPersonalizationProvider
class. If a user is anonymous, then the LoadPersonalizationBlob()
, SavePersonalizationBlob()
, and ResetPersonalizationBlob()
methods use the anonymous ID associated with the user rather than the normal username. The DetermineInitialScope()
method is also overridden because the default implementation of this method automatically puts anonymous users into Shared personalization scope.
Next, you need to modify the standard WebPartPersonalization
class. The standard version of this class prevents anonymous users from modifying state information. The updated AnonWebPartPersonalization
class is contained in Listing 29.14.
Example 29.14. AnonWebPartPersonalization.vb
Imports System Imports System.Collections Imports System.Web Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Overrides the standard WebPartPersonalization class ''' to enable anonymous users to modify state. ''' </summary> Public Class AnonWebPartPersonalization Inherits WebPartPersonalization Public Sub New(ByVal webPartManager As WebPartManager) MyBase.New(webPartManager) End Sub Protected Overrides ReadOnly Property UserCapabilities() As IDictionary Get If HttpContext.Current.Request.IsAuthenticated = True Then Return MyBase.UserCapabilities Else Dim capabilities As Hashtable = New Hashtable() capabilities.Add(WebPartPersonalization.ModifyStateUserCapability, WebPartPersonalization.ModifyStateUserCapability) Return capabilities End If End Get End Property End Class End Namespace
In Listing 29.14, the UserCapabilities
property is overridden. The new version of this property ensures that all users, even anonymous users, have the capability to modify state information.
Next, because a custom WebPartPersonalization
class has been created, the standard WebPartManager
control has to be modified before you can use it. The updated WebPartManager
control is contained in Listing 29.15.
Example 29.15. AnonWebPartManager.vb
Imports System Imports System.Web.UI.WebControls.WebParts Namespace myControls ''' <summary> ''' Modifies the base WebPartManager control ''' to use the AnonWebPartPersonalization class ''' </summary> Public Class AnonWebPartManager Inherits WebPartManager Protected Overrides Function CreatePersonalization() As WebPartPersonalization Return New AnonWebPartPersonalization(Me) End Function End Class End Namespace
In Listing 29.15, the base CreatePersonalization()
method of the WebPartManager
control is overridden to use the custom AnonWebPartPersonalization
class.
Next, you are ready to enable the Anonymous Personalization Provider. The web configuration file in Listing 29.16 contains the necessary configuration settings.
Example 29.16. Web.Config
<?xml version="1.0"?> <configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web> <anonymousIdentification enabled="True"/> <authentication mode="None" /> <webParts> <personalization defaultProvider="AnonProvider"> <providers> <add name="AnonProvider" type="myControls.AnonSqlPersonalizationProvider" connectionStringName="localSQLServer"/> </providers> <authorization> <allow users="Administrators" verbs="enterSharedScope"/> </authorization> </personalization> </webParts> </system.web> </configuration>
The configuration file in Listing 29.16 is saved with the name Web.Config_listing16
on the CD so that it does not interfere with the other code samples in this chapter.
The Web configuration file in Listing 29.16 does three things. First, it enables Anonymous Identification. When this feature is enabled, a GUID is generated automatically for each user and stored in a browser cookie. Second, the configuration file disables authentication by setting the Authentication Mode to the value None
. Finally, the configuration file configures the AnonPersonalizationProvider as the default personalization provider for the application.
At this point, it’s finally time to try out the Anonymous Personalization Provider. The Web Form page in Listing 29.17 can be customized by strangers.
Example 29.17. TestAnonymous.aspx
<%@ Page Language="VB" %> <%@ Register TagPrefix="custom" Namespace="myControls" %> <%@ Register TagPrefix="user" TagName="PersonalizationManager" Src="~/PersonalizationManager.ascx" %> <%@ Register TagPrefix="user" TagName="TextPart" Src="~/TextPart.ascx" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <script runat="server"> Protected Sub Menu1_MenuItemClick(ByVal sender As Object, ByVal e As MenuEventArgs) WebPartManager1.DisplayMode = WebPartManager1.DisplayModes(e.Item.Text) End Sub </script> <html xmlns="http://www.w3.org/1999/xhtml" > <head id="Head1" runat="server"> <style type="text/css"> .personalizationManager { border:dotted 2px orange; padding:5px; background-color:White; font:12px Arial, Sans-Serif; } .personalizationManager span { padding-right:10px; margin-right:10px; border-right:solid 1px black; } .personalizationManager a { text-decoration:none; } .column { float:left; width:30%; height:200px; margin-right:10px; border:solid 1px black; background-color: white; } .menu { margin:5px 0px; } html { background-color:#eeeeee; } </style> <title>Anonymous Personalization</title> </head> <body> <form id="form1" runat="server"> <custom:AnonWebPartManager id="WebPartManager1" Runat="server" /> <user:PersonalizationManager id="PersonalizationManager1" runat="server" /> <asp:Menu id="Menu1" OnMenuItemClick="Menu1_MenuItemClick" Orientation="Horizontal" CssClass="menu" Runat="server"> <Items> <asp:MenuItem Text="Browse" /> <asp:MenuItem Text="Design" /> </Items> </asp:Menu> <asp:WebPartZone id="WebPartZone1" CssClass="column" Runat="server"> <ZoneTemplate> <user:TextPart id="TextPart1" Title="Text Part" Description="Displays block of text" Runat="server" /> </ZoneTemplate> </asp:WebPartZone> <asp:WebPartZone id="WebPartZone2" CssClass="column" Runat="server" /> </form> </body> </html>
After you open the page in Listing 29.17, you can update the text displayed by the TextPart Web Part (see Figure 29.8). If you close your browser and return to the website in two years, the same text will appear.
If you want to simulate two anonymous users, then you need to open two different types of browsers (opening multiple instances of Internet Explorer doesn’t work because they all share the same browser cookies). For example, you can open the Default.aspx
page in both Internet Explorer and Mozilla Firefox and make different changes to the same page.
There is one undesirable consequence of the way that the Anonymous Personalization Provider was implemented in this chapter. Anonymous users are added to the aspnet_Users database table without being marked as anonymous. To fix this problem you need to modify the following line in the aspnet_PersonalizationPerUser_SetPageSettings
stored procedure:
EXEC dbo.aspnet_Users_CreateUser @ApplicationId, @UserName, 0, @CurrentTimeUtc, @UserId OUTPUT
The 0 parameter hard codes all users as authenticated.
This chapter leapt into the details of Web Part personalization. First, you learned how to enable a user to make both User and Shared scoped personalization changes to a page. Next, you learned how to mark different properties of a Web Part as personalizable. You learned how to use the IPersonalizable
interface for advanced personalization scenarios.
You also learned how to administer a Web Part application. In particular, you learned methods of pruning stale personalization data from a Web Part application. You also learned to generate a report of how much personalization data is being stored for each application user.
Finally, you studied the advanced topic of creating custom personalization providers. You learned how to create both a custom Query String Personalization Provider and a custom Anonymous Personalization Provider.