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

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

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

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

Note Even if you don’t use the default database name (aspnetdb), you should use a new, blank database that doesn’t include any other custom tables. That’s because aspnet_regsql.exe creates several tables for profiles (see Table 24-1), and you shouldn’t risk confusing them with business data. The examples in the rest of this chapter assume you’re using aspnetdb.

Figure 24-1 shows the relationships between the most important profile tables.

Figure 24-1. The profile tables

ASP.NET also creates several stored procedures that allow it to manage the information in these tables more easily. Table 24-2 lists the most noteworthy stored procedures.

Table 24-2. Database Stored Procedures Used for Profiles

Stored Procedure

Description

aspnet_Applications_CreateApplications

Checks whether a specific application name

 

exists in the aspnet_Applications table and

 

creates the record if needed.

aspnet_CheckSchemaVersion

Checks for support of a specific schema version

 

for a specific feature (such as profiles) using the

 

aspnet_SchemaVersions table.

aspnet_Profile_GetProfiles

Retrieves the user name and update times for all

 

the profile records in the aspnet_Profile table for

 

a specific web application. Doesn’t return the

 

actual profile data.

aspnet_Profile_GetProperties

Retrieves the profile information for a specific

 

user (which you specify by user name). The

 

information is not parsed in any way—instead,

 

this stored procedure simply returns the

 

underlying fields (PropertyNames,

 

PropertyValuesString, PropertyValuesBinary).

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

809

Stored Procedure

Description

aspnet_Profile_SetProperties

Sets the profile information for a specific user

 

(which you specify by user name). This stored

 

procedure requires values for the Property-

 

Names, PropertyValuesStrings, and

 

PropertyValuesBinary fields. There’s no way to

 

update just a single property in a profile.

aspnet_Profile_GetNumberOfInactiveProfiles

Returns profile records that haven’t been used

 

within a time window you specify.

|aspnet_Profile_DeleteInactiveProfiles

Removes profile records that haven’t been used

 

within a time window you specify.

aspnet_Users_CreateUser

Creates a new record in the aspnet_Users table

 

for a specific user. Checks whether the user

 

exists (in which case no action is taken) and

 

creates a GUID to use for the UserID field if

 

none is specified.

aspnet_Users_DeleteUser

Removes a specific user record from the

 

aspnet_Users table.

 

 

Configuring the Provider

Now that you have the database in place, you can register the SqlProfileProvider using the web.config file. First, define a connection string for the profile database. Then, use the <profile> section to remove any existing providers (with the <clear> element), and add a new instance of the System.Web.Profile.SqlProfileProvider class (with the <add> element). Here are the configuration settings you need:

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

<add name="SqlServices" connectionString=

"Data Source=localhost;Integrated Security=SSPI;Initial Catalog=aspnetdb;" /> </connectionStrings>

<system.web>

<profile defaultProvider="SqlProvider"> <providers>

<clear />

<add name="SqlProvider" type="System.Web.Profile.SqlProfileProvider" connectionStringName="SqlServices" applicationName="TestApplication" />

</providers>

</profile>

...

</system.web>

</configuration>

When you define a Profiles provider, you need to supply a name (which the <profile> element can then reference as the default provider), the exact type name, a connection string, and a web application name. Use different application names to separate the profile information between web applications (or use the same application name to share it).

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

Defining Profile Properties

Before you can store anything in the aspnet_Profile table, you need to define it specifically. You do this by adding the <properties> element inside the <profile> section of the web.config file. Inside the <properties> element, you place one <add> tag for each user-specific piece of information you want to store. At a minimum, the <add> element supplies the name for the property, like this:

<profile defaultProvider="SqlProvider"> <providers>

...

</providers>

<properties>

<add name="FirstName"/> <add name="LastName"/>

</properties>

</profile>

Usually, you’ll also supply the data type. (If you don’t, the property is treated as a string.) You can specify any serializable .NET class as the type, as shown here:

<add name="FirstName" type="String"/> <add name="LastName" type="String"/> <add name="DateOfBirth" type="DateTime"/>

You can set a few more property attributes to create the more advanced properties shown in Table 24-3.

Table 24-3. Profile Property Attributes

Attribute (for the <add> Element)

Description

name

The name of the property.

type

The fully qualified class name that represents the data type for

 

this property. By default, this is String.

serializeAs

Indicates the format to use when serializing this value (String,

 

Binary, Xml, or ProviderSpecific). You’ll look more closely at

 

the serialization model in the section “Profile Serialization.”

readOnly

Add this attribute with a value of true to create a property

 

that can be read but not changed. (Attempting to change the

 

property will cause a compile-time error.) By default, this is

 

false.

defaultValue

A default value that will be used if the profile doesn’t exist

 

or doesn’t include this particular piece of information. The

 

default value has no effect on serialization—if you set a profile

 

property, the ProfileModule will commit the current values to

 

the database, even if they match the default values.

allowAnonymous

A Boolean value that indicates whether this property can be

 

used with the anonymous profiles feature discussed later in

 

this chapter. By default, this is false.

provider

The Profiles provider that should be used to manage just this

 

property. By default, all properties are managed using the

 

provider specified in the <profile> element, but you can assign

 

different properties to different providers.

 

 

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

811

Using Profile Properties

Because profiles are stored in a user-specific record, you need to authenticate the current user before you can read or write profile information. You can use any type of authentication system (Windows, forms, or custom). You simply need to add an authorization rule to prevent anonymous access for the page or folder where you plan to use the profile. Here’s an example:

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

...

<system.web>

<authentication mode="Windows"/> <authorization>

<deny users="?"/> </authorization>

...

</system.web>

</configuration>

Chapter 23 has much more information about authorization rules.

With these details in place, you’re ready to access the profile information using the Profile property of the current page. When you run your application, ASP.NET creates a new class to represent the profile by deriving from System.Web.Profile.ProfileBase, which wraps a collection of profile settings. ASP.NET adds a strongly typed property to this class for each profile property you’ve defined in the web.config file. These strongly typed properties simply call the GetPropertyValue() and SetPropertyValue() methods of the ProfileBase base class to retrieve and set the corresponding profile values.

For example, if you’ve defined a string property named FirstName, you can set it in your page like this:

Profile.FirstName = "...";

Figure 24-2 presents a complete test page that allows the user to display the profile information for the current user or set new profile information.

Figure 24-2. Testing profiles

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

The first time this page runs, no profile information is retrieved, and no database connection is used. However, if you click the Show Profile Data button, the profile information is retrieved and displayed on the page:

protected void cmdShow_Click(object sender, EventArgs e)

{

lbl.Text = "First Name: " + Profile.FirstName + "<br />" + "Last Name: " + Profile.LastName + "<br />" +

"Date of Birth: " + Profile.DateOfBirth.ToString();

}

At this point, an error will occur if the profile database is missing or the connection can’t be opened. Otherwise, your page will run without a hitch, and you’ll see the newly retrieved profile information. Technically, the complete profile is retrieved when your code accesses the Profile.FirstName property in the first line and is used for the subsequent code statements.

Note Profile properties behave like any other class member variable. That means if you read a profile value that hasn’t been set, you’ll get a default initialized value (like an empty string or the number 0).

If you click the Set Profile Data button, the profile information is set based on the current control values:

protected void cmdSet_Click(object sender, EventArgs e)

{

Profile.FirstName = txtFirst.Text; Profile.LastName = txtLast.Text; Profile.DateOfBirth = Calendar1.SelectedDate;

}

Now the profile information is committed to the database when the page request finishes. If you want to commit some or all of the information earlier (and possibly incur multiple database trips), just call the Profile.Save() method. As you can see, the profiles feature is unmatched for simplicity.

Tip The Profile object doesn’t include just the properties you’ve defined. It also provides LastActivityDate and LastUpdatedDate properties with information drawn from the database.

Profile Serialization

Earlier, you learned how properties are serialized into a single string. For example, if you save a FirstName of Harriet and a LastName of Smythe, both values are crowded together in the PropertyValuesString field, saving space:

HarrietSmythe

The PropertyNames field gives the information you need to parse each value from the PropertyValuesString field. Here’s what you’ll see in the PropertyNames field in this example:

FirstName:S:0:7:LastName:S:7:6:

The colons (:) are used as delimiters. The basic format is as follows:

PropertyName:StringOrBinarySerialization:StartingCharacterIndex:Length:

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

813

Something interesting happens if you create a profile with a DateTime data type. When you look at the PropertyValuesString field, you’ll see something like this:

<?xml version="1.0" encoding="utf-16"?><dateTime>2005-07-12T00:00:00-04:00 </dateTime>HarrietSmythe

Initially, it looks like the profile data is serialized as XML, but the PropertyValuesString clearly doesn’t contain a valid XML document (because of the text at the end). What has actually happened is that the first piece of information, the DateTime, is serialized (by default) as XML. The following two profile properties are serialized as ordinary strings.

The ProperyNames field makes it slightly clearer:

DateOfBirth:S:0:87:FirstName:S:87:7:LastName:S:94:6:

Interestingly, you have the ability to change the serialization format of any profile property by adding the serializeAs attribute to its declaration in the web.config file. Table 24-4 lists your choices.

Table 24-4. Serialization Options

SerializeAs

Description

String

Converts the type to a string representation. Requires a type converter

 

that can handle the job. (See Chapter 28 for more information about type

 

converters.)

Xml

Converts the type to an XML representation, which is stored in a string, using

 

the System.Xml.XmlSerialization.XmlSerializer (the same class that’s used

 

with web services).

Binary

Converts the type to a proprietary binary representation that only .NET

 

understands using the

 

System.Runtime.Serialization.Formatters.Binary.BinaryFormatter. This is the

 

most compact option but the least flexible. Binary data is stored in the

 

PropertyValuesBinary field instead of the PropertyValues.

ProviderSpecific

Performs customized serialization that’s implement in a custom provider.

 

 

For example, here’s how you can change the serialization for the profile settings:

<add name="FirstName" type="String" serializeAs="Xml"/> <add name="LastName" type="String" serializeAs="Xml"/>

<add name="DateOfBirth" type="DateTime" serializeAs="String"/>

Now the next time you set the profile, the serialized representation in the PropertyValuesString field will take this form:

7/12/2005<?xml version="1.0" encoding="utf-16"?><string>Harriet</string> <?xml version="1.0" encoding="utf-16"?><string>Smythe</string>

If you use the binary serialization mode, the property value will be placed in the PropertyValuesBinary field instead of the PropertyValuesString field. The only indication of this shift is the use of the letter B instead of S in the PropertyNames field. Here’s an example where the FirstName property is serialized in the PropertyValuesBinary field:

DateOfBirth:S:0:9:FirstName:B:0:31:LastName:S:9:64:

All of these serialization details raise an important question—what happens when you change profile properties or the way they are serialized? Profile properties don’t have any support for versioning. However, you can add or remove properties with relatively minor consequences. For example, the ProfileModule will ignore properties that are present in the aspnet_Profile table but not defined in the web.config file. The next time you modify part of the profile, these properties

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

will be replaced with the new profile information. Similarly, if you define a profile in the web.config file that doesn’t exist in the serialized profile information, the ProfileModule will just use the default value. However, more dramatic changes—such as renaming a property, changing its data type, and so on, are likely to cause an exception when you attempt to read the profile information. Even worse, because the serialized format of the profile information is proprietary, you have no easy way to migrate existing profile data to a new profile structure.

Tip Not all types are serializable in all ways. For example, classes that don’t provide a parameterless constructor can’t be serialized in Xml mode. Classes that don’t have the Serializable attribute can’t be serialized in Binary mode. You’ll consider this distinction when you learn how to use custom types with profiles, but for now just keep in mind that you may run across types that can be serialized only if you choose a different serialization mode.

Profile Groups

If you have a large number of profile settings, and some settings are logically related to each other, you may want to use profile groups to achieve better organization.

For example, you may have some properties that deal with user preferences and others that deal with shipping information. Here’s how you could organize these profile properties using the <group> element:

<profile defaultProvider="SqlProvider"> <properties>

<group name="Preferences">

<add name="LongDisplayMode" defaultValue="true" type="Boolean" /> <add name="ShowSummary" defaultValue="true" type="Boolean" />

</group>

<group name="Address">

<add name="Name" type="String" /> <add name="Street" type="String" /> <add name="City" type="String" /> <add name="ZipCode" type="String" /> <add name="State" type="String" /> <add name="Country" type="String" />

</group>

</properties>

</profile>

Now you can access the properties through the group name in your code. For example, here’s how you retrieve the country information:

lblCountry.Text = Profile.Address.Country;

Groups are really just a poor man’s substitute for a full-fledged custom structure or class. For example, you could achieve the same effect as in the previous example by declaring a custom Address class. You’d also have the ability to add other features (such as validation in the property procedures). The next section shows how.

Profiles and Custom Data Types

Using a custom class with profiles is easy. You need to begin by creating the class that wraps the information you need. In your class, you can use public member variables or full-fledged property procedures. The latter choice, though longer, is the preferred option because it ensures your class will support data binding and gives you the flexibility to add property procedure code later.

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

815

Here’s a slightly abbreviated Address class that ties together the same information you saw in the previous example:

[Serializable()] public class Address

{

private string name; public string Name {...}

private string street; public string Street {...}

private string city; public string City {...}

private string zipCode; public string ZipCode {...}

private string state; public string State {...}

private string country; public string Country {...}

public Address(string name, string street, string city, string zipCode, string state, string country)

{

Name = name; Street = street; City = city; ZipCode = zipCode; State = state; Country = country;

}

public Address() { }

}

You can place this class in the App_Code directory (or compile it and place the DLL assembly in the Bin directory). The final step is to add a property that uses it:

<properties>

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

...

</properties>

Now you can manipulate it in your code like this:

Profile.Address = new Address("Name", "Street", "City", "Zip", "State", "Country"); lbl.Text = "You are in " + Profile.Address.Country;

Custom Type Serialization

You need to keep in mind a few points, depending on how you decide to serialize your custom class. By default, all custom data types use XML serialization with the XmlSerializer. This class is relatively limited in its serialization ability. It simply copies the value from every public property or member variable into a straightforward XML format like this:

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

<Address> <Name>...</Name> <Street>...</Street> <City>...</City> <ZipCode>...</ZipCode> <State>...</State> <Country>...</Country>

</Address>

You do have the ability to shape this XML representation by adding attributes to your class. For example, you can rename elements or tell .NET to serialize a property as an attribute instead of an element. The XML elements are described with web services in Chapter 33, because web services use the same XmlSerializer.

When deserializing your class, the XmlSerializer needs to be able to find a parameterless public constructor. In addition, none of your properties can be read-only. If you violate either of these rules, the deserialization process will fail.

If you decide to use binary serialization instead of XmlSerialization, .NET uses a completely different approach.

<add name="Address" type="Address" serializeAs="Binary"/>

In this case, the ProfileModule enlists the help of the BinaryFormatter. The BinaryFormatter can serialize the full public and private contents of any class, provided the class is decorated with the Serializable attributes. (Additionally, any class it derives from or references must also be serializable.) You can learn much more about the binary formatter in Chapter 13.

Finally, you can decide to use string serialization:

<add name="Address" type="Address" serializeAs="String"/>

In this case, you need a type converter that can translate between an instance of your class and its string representation. Chapter 28 shows you how to create type converters.

Automatic Saves

The ProfileModule that saves profile information isn’t able to detect changes in complex data types (anything other than strings, simple numeric types, Boolean values, and so on). This means if your profile includes complex data types, the ProfileModule saves the profile information at the end of every request that accesses the Profile object.

This behavior obviously adds unnecessary overhead. To optimize performance when working with complex types, you have several choices. One option is to set the corresponding profile property to be read-only (if you know it never changes). Another approach is to disable the autosave behavior completely by adding the automaticSaveEnabled attribute on the <profile> element and setting it to false, as shown here:

<profile defaultProvider="SqlProvider" automaticSaveEnabled="false">...</profile>

If you choose this approach, it’s up to you to call Profile.Save() to explicitly commit changes. Generally, this approach is the most convenient, because it’s easy to spot the places in your code where you modify the profile. Just add the Profile.Save() call at the end:

Profile.Address = new Address(txtName.Text, txtStreet.Text, txtCity.Text, txtZip.Text, txtState.Text, txtCountry.Text);

Profile.Save();

One final option is to handle the ProfileModule.ProfileAutoSaving event in the global.asax file. At this point, you can check to see if a save is really necessary and cancel the save if it isn’t.

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

817

With this technique, the obvious problem is determining whether the automatic save should be cancelled. You could store the original profile data in memory and then compare these objects with the current objects when the ProfileAutoSaving event fires. However, this approach would be awkward and slow. A better option is to make the page keep track of whether a change has been made. If a change has been made, your code can then set a flag to indicate that the update should go ahead.

For example, consider the test page shown in Figure 24-3 that allows you to retrieve and modify address information.

Figure 24-3. Modifying a complex type in a profile

All the text boxes on this page use the same event handler for their TextChanged event. This event handler indicates that a change has been made by storing a Boolean value in the context for the current request:

protected void txt_TextChanged(object sender, EventArgs e)

{

Context.Items["AddressDirtyFlag"] = true;

}

Keep in mind that a value stored in this way lasts only for the duration of the current request. In this example, that’s not a problem because the user has only two options after making a change— rejecting the change (by clicking Get) or applying the change (by clicking Save). However, if you create a page where the user can make changes over several steps and then apply them later, you would need to go to more work to maintain the flag. Storing the flag in other locations such as session state or view state won’t work, because they aren’t available when the AutoSaving event fires in the global.asax file.

Finally, here’s the event handler you need that allows the autosave to carry on only if a change has been made:

void Profile_ProfileAutoSaving(Object sender, ProfileAutoSaveEventArgs e)

{

if ((e.Context.Items["AddressDirtyFlag"] == null) || ((bool)e.Context.Items["AddressDirtyFlag"] == false))

{