Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
104
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

818 C H A P T E R 2 4 P R O F I L E S

e.ContinueWithProfileAutoSave = false;

}

}

Remember, the Profile.AutoSaving event fires for any change. If you have more than one page that modifies different profile details, you might need to write conditional code that checks which page was requested and restricts or permits the save accordingly. In this situation, it’s usually easier to turn off automatic saving altogether and force the page to use the Profile.Save() method.

The Profiles API

Although your page automatically gets the profile information for the current user, that doesn’t prevent you from retrieving and modifying the profiles of other users. In fact, you have two tools to help you—the ProfileBase class and the ProfileManager class.

The ProfileBase object (provided by the Page.Profile property) includes a useful GetProfile() function that retrieves, by user name, the profile information for a specific user. Figure 24-4 shows an example with a Windows authenticated user.

Figure 24-4. Retrieving a profile manually

Here’s the code that gets the profile:

protected void cmdGet_Click(object sender, EventArgs e)

{

ProfileCommon profile = Profile.GetProfile(txtUserName.Text); lbl.Text = "This user lives in " + profile.Address.Country;

}

Notice that once you have a Profile object, you can interact with it in the same way as you interact with the profile for the current user. You can even make changes. The only difference is that changes aren’t saved automatically. If you want to save a change, you need to call the Save() method of the Profile object.

Note If you try to retrieve a profile that doesn’t exist, you won’t get an error. Instead, you’ll simply end up with blank data. If you change and save the profile, a new profile record will be created.

C H A P T E R 2 4 P R O F I L E S

819

If you need to perform other tasks with profiles, you can use the ProfileManager class in the System.Web.Profile namespace, which exposes the useful static methods described in Table 24-5. Many of these methods work with a ProfileInfo class, which provides information about a profile.

The ProfileInfo includes the user name (UserName), last update and last activity dates (LastActivityDate and LastUpdateDate), the size of the profile in bytes (Size), and whether the profile is for an anonymous user (IsAnonymous). It doesn’t provide the actual profile values.

Table 24-5. ProfileManager Methods

Method

Description

DeleteProfile()

Deletes the profile for the user you specify.

DeleteProfiles()

Deletes multiple profiles at once. You supply an array of

 

user names.

DeleteInactiveProfiles()

Deletes profiles that haven’t been used since a time you

 

specify. You also must supply a value from the Profile-

 

AuthenticationOption enumeration to indicate what type

 

of profiles you want to remove (All, Anonymous, or

 

Authenticated).

GetNumberOfProfiles()

Returns the number of profile records in the data source.

GetNumberOfInactiveProfiles()

Returns the number of profiles that haven’t been used since

 

the time you specify.

GetAllInactiveProfiles()

Retrieves profile information for profiles that haven’t been

 

used since the time you specify. The profiles are returned as

 

ProfileInfo objects.

GetAllProfiles()

Retrieves all the profile data from the data source as a

 

collection of ProfileInfo objects. You can choose what

 

type of profiles you want to retrieve (All, Anonymous, or

 

Authenticated). You can also use an overloaded version of

 

this method that uses paging and retrieves only a portion

 

of the full set of records based on the starting index and

 

page size you request.

FindProfilesByUserName()

Retrieves a collection of ProfileInfo objects that match a

 

specific user name. The SqlProfileProvider uses a LIKE

 

clause when it attempts to match user names. That means

 

you can use wildcards such as the % symbol. For example,

 

if you search for the user name user%, you’ll return values

 

like user1, user2, user_guest, and so on. You can use an

 

overloaded version of this method that uses paging.

FindInactiveProfilesByUserName()

Retrieves profile information for profiles that haven’t been

 

used since the time you specify. You can also filter out

 

certain types of profiles (All, Anonymous, or Authenticated)

 

or look for a specific user name (with wildcard matching).

 

The return value is a collection of ProfileInfo objects.

 

 

For example, if you want to remove the profile for the current user, you need only a single line of code:

ProfileManager.DeleteProfile(User.Identity.Name);

And if you want to display the full list of users in a web page (not including anonymous users), just add a GridView with AutoGenerateColumns set to true and use this code:

820 C H A P T E R 2 4 P R O F I L E S

protected void Page_Load(object sender, EventArgs e)

{

GridView1.DataSource = ProfileManager.GetAllProfiles( ProfileAuthenticationOption.Authenticated);

GridView1.DataBind();

}

Figure 24-5 shows the result.

Figure 24-5. Retrieving information about all the profiles in the data source

Anonymous Profiles

So far, all the examples have assumed that the user is authenticated before any profile information is accessed or stored. Usually, this is the case. However, sometimes it’s useful to create a temporary profile for a new, unknown user. For example, most e-commerce websites allow new users to begin adding items to a shopping cart before registering. If you want to provide this type of behavior and you choose to store shopping cart items in a profile, you’ll need some way to uniquely identify anonymous users.

ASP.NET provides an anonymous identification feature that fills this gap. The basic idea is that the anonymous identification feature automatically generates a random identifier for any anonymous user. This random identifier stores the profile information in the database, even though no user ID is available. The user ID is tracked on the client side using a cookie (or in the URL, if you’ve enable cookieless mode). Once this cookie disappears (for example, if the anonymous user closes and reopens the browser), the anonymous session is lost and a new anonymous session is created.

Anonymous identification has the potential to leave a lot of abandoned profiles, which wastes space in the database. For that reason, anonymous identification is disabled by default. However, you can enable it using the <anonymousIdentification> element in the web.config file, as shown here:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

...

<system.web>

<anonymousIdentification enabled="true" />

...

</system.web>

</configuration>

C H A P T E R 2 4 P R O F I L E S

821

You also need to flag each profile property that will be retained for anonymous users by adding the allowAnonymous attribute and setting it to true. This allows you to store just some basic information and restrict larger objects to authenticated users.

<properties>

<add name="Address" type="Address" allowAnonymous="true" />

...

</properties>

The <anonymousIdentification> element also supports numerous optional attributes that let you set the cookie name and timeout, specify whether the cookie will be issued only over an SSL connection, control whether cookie protection (validation and encryption) is used to prevent tampering and eavesdropping, and configure support for cookieless ID tracking. Here’s an example:

<anonymousIdentification enabled="true" cookieName=".ASPXANONYMOUS" cookieTimeout="43200" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All" cookieless="UseCookies"/>

For more information, refer to the configuration settings for forms authentication (Chapter 20) and role management (Chapter 23), which use the same settings.

Tip If you use anonymous identification, it’s a good idea to delete old anonymous sessions regularly using the aspnet_Profile_DeleteInactiveProfiles stored procedure, which you can run at scheduled intervals using the SQL Server Agent. You can also delete old profiles using the ProfileManager class, as described in the previous section.

Migrating Anonymous Profiles

A challenge that occurs with anonymous profiles is what to do with the profile information when a previously anonymous user logs in. For example, in an e-commerce website a user might select several items and then register or log in to complete the transaction. At this point, you need to make sure the shopping cart information is copied from the anonymous user’s profile to the appropriate authenticated (user) profile.

Fortunately, ASP.NET provides a solution through the ProfileModule.MigrateAnonymous event. This event (which can be handled in the global.asax file) fires whenever an anonymous identifier is available (either as a cookie or in the URL if you’re using cookieless mode) and the current user is authenticated.

The basic technique when handling the MigrateAnonymous event is to load the profile for the anonymous user by calling Profile.GetProfile() and passing in the anonymous ID, which is provided to your event handler through the ProfileMigrateEventArgs.

Once you’ve loaded this data, you can then transfer the settings to the new profile manually. You can choose to transfer as few or as many settings as you want, and you can perform any other processing that’s required. Finally, your code should remove the anonymous profile data from the database and clear the anonymous identifier so the MigrateAnonymous event won’t fire again.

void Profile_MigrateAnonymous(Object sender, ProfileMigrateEventArgs pe)

{

// Get the anonymous profile.

ProfileCommon anonProfile = Profile.GetProfile(pe.AnonymousID);

//Copy information to the authenticated profile

//(but only if there's information there).

if (anonProfile.Address.Name != null || anonProfile.Address.Name != "")

{

822 C H A P T E R 2 4 P R O F I L E S

Profile.Address = anonProfile.Address;

}

//Delete the anonymous profile from the database.

//(You could decide to skip this step to increase performance

//if you have a dedicated job scheduled on the database server

//to remove old anonymous profiles.) System.Web.Profile.ProfileManager.DeleteProfile(pe.AnonymousID);

//Remove the anonymous identifier. AnonymousIdentificationModule.ClearAnonymousIdentifier();

}

You need to handle this task with some caution. If you’ve enabled anonymous identification, every time a user logs in, the MigrateAnonymous event fires, even if the user hasn’t entered any information into the anonymous profile. That’s a problem, because if you’re not careful, you could easily overwrite the real (saved) profile for the user with the blank anonymous profile. The problem is further complicated because complex types (such as the Address object) are created automatically by the ProfileModule, so you can’t just check for a null reference to determine whether the user has anonymous address information.

In the previous example, the code tests for a missing Name property in the Address object. If this information isn’t a part of the anonymous profile, no information is migrated. A more sophisticated example might test for individual properties separately or might migrate an anonymous profile only if the information in the user profile is missing or out of date.

Building a Shopping Cart

Now that you’ve learned how to use profiles, it’s worth considering an end-to-end example that uses the GridView and the profiles feature to build a basic shopping cart.

Shopping carts are a hallmark of e-commerce websites. They allow users to select a batch of items for an upcoming purchase. Using profiles, you can store shopping cart information in a user’s profile so that it’s stored permanently for repeat visits. When the user makes the purchase, you can remove the profile information and use a database component to commit the corresponding order records.

In the following sections, you’ll go through the steps needed to build a complete shopping cart framework that revolves around a single test page (see Figure 24-6). This test pages uses two databound controls—one to show the product catalog and one to show the shopping cart items. To add items from the catalog, the user must click a link in the product catalog.

Note You can also use the following model if you want to store shopping cart information in session state. You still set up the grid controls in the same way and use the same shopping cart classes. The only difference is that the shopping cart is stored temporarily in the Session collection instead of in the database through the Page.Profile property.

C H A P T E R 2 4 P R O F I L E S

823

Figure 24-6. A shopping cart

The Shopping Cart Classes

In theory, you could use the DataRow and DataTable objects to represent a shopping cart. However, because the shopping cart information doesn’t directly correspond to a table in the database, the process would be awkward and counterintuitive. A better approach is to design your own classes to represent the shopping cart and its items.

The first ingredient you need is a class to represent individual shopping cart items. This class needs to track the product information, along with the number of units the user wants to order. Here’s a ShoppingCartItem class that fills this role:

[Serializable()]

public class ShoppingCartItem

{

private int productID; public int ProductID

{

824 C H A P T E R 2 4 P R O F I L E S

get {return productID;}

}

private string productName; public string ProductName

{

get {return productName;}

}

private decimal unitPrice; public decimal UnitPrice

{

get {return unitPrice;}

}

private int units; public int Units

{

get {return units;} set {units = value;}

}

public decimal Total

{

get {return Units * UnitPrice;}

}

public ShoppingCartItem(int productID,

string productName, decimal unitPrice, int units)

{

this.productID = productID; this.productName = productName; this.unitPrice = unitPrice; this.units = units;

}

}

You’ll notice a few interesting details about this class. First, its properties are almost all readonly. None of the shopping cart item information can be changed once the item is created, with the exception of the number of desired units. The second interesting detail is the Total property. This property doesn’t map to a private member variable—instead it’s calculated based on the unit price and the number of desired units. It’s the class equivalent of a calculated column. This property is a great help when you bind a ShoppingCartItem to a control, because it allows you to easily show the total price of each line.

Note When designing a class that you intend to use with a data-bound control, you must use property procedures rather than public member variables. For example, if you implemented the UnitPrice information using a public member variable instead of a property procedure, you wouldn’t be able to bind to it and display that information in a data-bound control.

Finally, note that the ShoppingCartItem class is decorated with the Serializable attribute but doesn’t include a default parameterless constructor. This is because it’s intended for use with binary serialization, as discussed earlier.

C H A P T E R 2 4 P R O F I L E S

825

Of course, a shopping cart is a collection of zero or more shopping cart items. To create the shopping cart, you can use a standard .NET collection class. However, it’s often useful to create your own strongly typed collection class. This way, you can add your own helper methods and control the serialization process.

Creating a strongly typed collection is easy because you can derive from the System.Collections.CollectionBase class to acquire the basic functionality you need. Essentially, the CollectionBase wraps an ordinary ArrayList, which is exposed through the protected variable List. However, this ArrayList isn’t directly accessible to other classes. Instead, your custom class must add methods such as Add(), Remove(), Insert(), and so on, which allow other classes to use the collection. Here’s the trick—even though the internal ArrayList isn’t typesafe, the collection methods that you create are, which prevents errors and ensures that the collection contains the correct type of object.

Here’s a strongly typed ShoppingCart collection that accepts only ShoppingCartItem instances:

[Serializable()]

public class ShoppingCart : CollectionBase

{

public ShoppingCartItem this[int index]

{

get {return((ShoppingCartItem)List[index]);} set {List[index] = value;}

}

public int Add(ShoppingCartItem value)

{

return(List.Add(value));

}

public int IndexOf(ShoppingCartItem value)

{

return(List.IndexOf(value));

}

public void Insert(int index, ShoppingCartItem value)

{

List.Insert(index, value);

}

public void Remove(ShoppingCartItem value)

{

List.Remove(value);

}

public bool Contains(ShoppingCartItem value)

{

return(List.Contains(value));

}

}

Notice that the ShoppingCart doesn’t implement ICollection, which is a requirement for data binding. It doesn’t need to, because the CollectionBase class it inherits from already does.

At this point, you’re ready to use the ShoppingCart and ShoppingCartItem classes in an ASP.NET web page. To make them available, simply add the following profile property:

<add name="Cart" type="ShoppingCart" serializeAs="Binary"/>

826 C H A P T E R 2 4 P R O F I L E S

The Test Page

The next step is to create and configure the GridView controls for showing the product and shopping cart information. This example has two separate GridView controls—one for showing the product catalog and another for showing the current contents of the shopping cart. The GridView for the product information has a fairly straightforward structure. It uses several BoundField tags that display fields from the bound table (with the correct numeric formatting) and one ButtonField that allows the user to select the row. The ButtonField is displayed as a hyperlink with the text Add.

Here are the definitions for all the GridView columns used to display the product catalog:

<Columns>

<asp:BoundField DataField="ProductID" HeaderText="ID"></asp:BoundField> <asp:BoundField DataField="ProductName"

HeaderText="Product Name"></asp:BoundField>

<asp:BoundField DataField="UnitPrice" HeaderText="Unit Price" DataFormatString="{0:C}"></asp:BoundField>

<asp:CommandField ShowSelectButton="True" ButtonType="Link" SelectText="Add..." /> </Columns>

When this page first loads, it queries the database component to get the full list of products. Then it binds the product list to the GridView. The code that performs this work is as follows:

private NorthwindDB db = new NorthwindDB(); private DataSet ds;

protected void Page_Load(object sender, System.EventArgs e)

{

// Update the product list.

ds = db.GetCategoriesProductsDataSet(); gridProducts.DataSource = ds.Tables["Products"]; gridProducts.DataBind();

}

No matter what other events happen, the shopping cart is bound just before the page is rendered. That’s because the shopping cart may be modified as a result of other event handlers. By binding it at the end of the page life cycle, you ensure that the GridView shows the most up-to-date information.

protected void Page_PreRender(object sender, System.EventArgs e)

{

// Show the shopping cart in the grid. gridCart.DataSource = Profile.Cart; gridCart.DataBind();

}

So, what can happen in the meantime between the Page.Load and Page.PreRender events? One possibility is that the user clicks one of the Add links in the GridView of the product catalog. In this case, the SelectedIndexChanged event fires, and a series of steps take place.

First, the code retrieves the DataRow for the selected product using the in-memory copy of the DataSet:

protected void gridProducts_SelectedIndexChanged(object sender, System.EventArgs e)

{

// Get the full record for the one selected row. DataRow[] rows = ds.Tables["Products"].Select(

"ProductID=" + gridProducts.SelectedDataKey.Values["ProductID"].ToString()); DataRow row = rows[0];

...

C H A P T E R 2 4 P R O F I L E S

827

Next, the code searches to see if this product is already in the cart. If it is, the Units property of the corresponding ShoppingCartItem is incremented by 1, as shown here:

...

// Search to see if an item of this type is already in the cart. Boolean inCart = false;

foreach (ShoppingCartItem item in Profile.Cart)

{

// Increment the number count.

if (item.ProductID == (int)row["ProductID"])

{

item.Units += 1; inCart = true; break;

}

}

...

If the item isn’t in the cart, a new ShoppingCartItem object is created and added to the collection, as follows:

...

// If the item isn’t in the cart, add it. if (!inCart)

{

ShoppingCartItem item = new ShoppingCartItem( (int)row["ProductID"], (string)row["ProductName"], (decimal)row["UnitPrice"], 1);

Profile.Cart.Add(item);

}

...

Finally, the selected index is cleared so that the product row doesn’t become highlighted. (You could also set the selection style so that the selected row doesn’t appear to the user.) The act of selection is now complete.

...

// Don't keep the item selected in the product list. gridProducts.SelectedIndex = -1;

}

Notice that the GridView that displays the shopping cart information binds directly to the ShoppingCart collection. Creating this GridView is fairly straightforward. You can use BoundField tags in the same way that you would with a table, except now the DataField identifies the name of one of the properties in the ShoppingCartItem class. Here are the bound columns used in the GridView for the shopping cart details:

<asp:BoundField DataField="ProductID" HeaderText="ID"></asp:BoundField> <asp:BoundField DataField="ProductName" HeaderText="Product Name">

</asp:BoundField>

<asp:BoundField DataField="UnitPrice" HeaderText="Unit Price" DataFormatString="{0:C}"></asp:BoundField>

<asp:BoundField DataField="Total" HeaderText="Total" DataFormatString="{0:C}"></asp:BoundField>

The column for displaying the Units property is slightly different. It uses a TemplateField. The template uses a text box, which displays the number of desired units, and allows the user to edit this number.