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

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

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

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

<asp:TemplateField HeaderText="Units"> <HeaderStyle Width="5px"></HeaderStyle> <ItemTemplate>

<asp:TextBox runat="server" Font-Size="XX-Small" Width="31px" Text='<%# DataBinder.Eval(Container, "DataItem.Units") %>'>

</asp:TextBox>

</ItemTemplate>

</asp:TemplateField>

If a user does decide to change the number of units for an item, the change must be committed by clicking an Update link in another column:

<asp:CommandField ShowSelectButton="True" SelectText="Update" />

For simplicity, this link is also treated as a select command. However, with slightly more code you could use the RowCommand event instead (as discussed in Chapter 10).

When the user clicks the Update link, the GridView.RowCommand event fires. At this point, the code finds the corresponding ShoppingCartItem instance and updates the Units property (or removes the item entirely if the count has reached 0). Here’s the code that performs this task:

protected void gridCart_SelectedIndexChanged(object sender, EventArgs e)

{

//The first control in a template column is always a blank LiteralControl.

//The text box is the second control.

TextBox txt = (TextBox)gridCart.Rows[gridCart.SelectedIndex].Cells[3].Controls[1];

try

{

// Update the appropriate cart item. int newCount = int.Parse(txt.Text); if (newCount > 0)

{

Profile.Cart[gridCart.SelectedIndex].Units = newCount;

}

else if (newCount == 0)

{

cart.RemoveAt(gridCart.SelectedIndex);

}

}

catch

{

// Ignore invalid (nonnumeric) entries.

}

gridCart.SelectedIndex = -1;

}

Invalid entries are simply ignored using an exception handler. Another option is to use a CompareValidator validation control to prevent the user from entering negative numbers or text.

As written, this example is fairly powerful, and it’s a reasonable starting point for creating a shopping cart for a highly professional application. All you need to do is implement the checkout logic, which would commit the order to the database and clear the Profile.Cart by replacing it with a new, empty shopping cart.

Multiple Selection

Another refinement you might want to add to this example is to allow the product list to support multiple selection (similar to the way files are selected in a Hotmail inbox). The basic approach is to

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

829

create a template column that includes a CheckBox control. When the user clicks another button (such as submit), you can loop over all the items in the GridView and check the state of the check box in each item. If the state is checked, you would then add that item to the shopping cart.

Custom Profiles Providers

The profile model plugs neatly into ASP.NET web pages. However, it isn’t very configurable. You might decide you need to create a custom Profiles provider for a number of reasons:

You need to store profile information in a data source other than a SQL Server database, such as an Oracle database.

You need your profile data to be available to other applications. Parsing the information in the PropertyValuesString and PropertyValuesBinary fields is tedious, error-prone, and inflexible. If you need to use this information in other queries or applications, you need to store your profile information in a database table that’s split into distinct fields.

You need to implement additional logic when storing or retrieving profile data. For example, you could apply validation, caching, logging, encryption, or compression. (In some cases, you can get these features by simply extending the ProfileBase class that wraps profile settings, rather than creating an entirely new ProfileProvider.)

In the following sections, you’ll focus on the second scenario. You’ll see how to build a custom provider that keeps its property values in separate fields and can be adapted to fit any existing database.

The Custom Profiles Provider Classes

To implement a Profiles provider, you need to create a class that derives from the ProfileProvider abstract class from the System.Web.Profile namespace. The ProfileProvider abstract class itself inherits the SettingsProvider abstract class from the System.Configuration namespace, which inherits from the ProviderBase abstract class from the System.Configuration.Provider namespace. As a result, you also need to implement members from the SettingsProvider and ProviderBase classes. Altogether, more than a dozen members must be implemented before you can compile your custom Profiles provider.

However, these methods aren’t all of equal importance. For example, you can create a basic provider that saves and retrieves profile information by implementing two or three of these methods. Many of the other methods support functionality that’s exposed by the ProfileManager class, such as the ability to delete profiles or find inactive profiles.

In the following example, you’ll consider a simple Profiles provider that includes the core logic that’s needed to plug into a page but doesn’t support most other parts of the Profiles API. Methods that aren’t supported simply throw a NotImplementedException, like this:

public override int DeleteProfiles(string[] usernames)

{

throw new Exception("The method or operation is not implemented.");

}

All of these methods are conceptually easy to implement (all you need is some basic ADO.NET code). However, properly coding each method requires a fairly substantial amount of code.

Table 24-6 lists all the overridable properties and methods and indicates which class defines them. Those that are implemented in the following example are marked with an asterisk. To be considered truly complete, a provider must implement all of these members.

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

Table 24-6. Abstract Methods for Profiles Providers

Class

Member

Description

*ProviderBase

Name

A read-only property that returns the name

 

 

(set in the web.config file) for the current

 

 

provider.

*ProviderBase

Initialize()

Gets the configuration element from the

 

 

web.config file that initializes this provider.

 

 

Gives you the chance to read custom

 

 

settings and store the information in

 

 

member variables.

SettingsProvider

ApplicationName

A name (set in the web.config file) that

 

 

allows you to separate the users of different

 

 

applications that are stored in the same

 

 

database.

*SettingsProvider

GetPropertyValues()

Retrieves the profile information for

 

 

a single user. This method is called

 

 

automatically when a web page accesses

 

 

the Page.Profile property. This method

 

 

is provided with a list of all the profile

 

 

properties that are defined in the

 

 

application. You must return a value

 

 

for each of these properties.

*SettingsProvider

SetPropertyValues()

Updates the profile information for a single

 

 

user. This method is called automatically

 

 

at the end of a request when profile

 

 

information is changed. This method

 

 

is provided with a list of all the profile

 

 

properties that are defined in the

 

 

application and their current values.

ProfileProvider

DeleteProfiles()

Deletes one or more user profile records

 

 

from the database. This method has two

 

 

overloads, which take different parameters.

ProfileProvider

DeleteInactiveProfiles()

Similar to DeleteProfiles() but looks for

 

 

profiles that haven’t been accessed since

 

 

a specific time. To support this method,

 

 

you must keep track of when profiles are

 

 

accessed or updated in your database.

ProfileProvider

GetAllProfiles()

Returns information about a group of

 

 

profile records. This method must support

 

 

paging so that it returns only a subset of

 

 

the total records. Refer to the aspnet_

 

 

Profile_GetProfiles stored procedure that

 

 

aspnet_regsql creates for a sample paging

 

 

implementation.

ProfileProvider

GetAllInactiveProfiles()

Similar to GetAllProfiles() but looks for

 

 

profiles that haven’t been accessed since

 

 

a specific time. To support this method,

 

 

you must keep track of when profiles are

 

 

accessed or updated in your database.

ProfileProvider

FindProfilesByUserName()

Retrieves profile information based on the

 

 

user name of one or more (if you support

 

 

wildcard matching) users. The actual

 

 

profile information isn’t returned—only

 

 

some standard information such as the last

 

 

activity date is returned.

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

831

Class

Member

Description

ProfileProvider

FindInactiveProfilesByUser-

Similar to FindProfilesByUserName ()

 

Name()

but looks for profiles that haven’t been

 

 

accessed since a specific time.

ProfileProvider

GetNumberOfInactive-

Counts the number of profiles that haven’t

 

Profiles()

been accessed since a specific time.

 

 

 

* Implemented in the following example

Designing the FactoredProfileProvider

The FactoredProfileProvider stores property values in a series of fields in a database table, rather than in a single block. This makes the values easier to use in different applications and with different queries. Essentially, the FactoredProfileProvider unlocks the profiles table so that it’s no longer using a proprietary schema. The only disadvantage to this approach is that it’s no longer possible to change the profile or add information to it without modifying the schema of your database.

When implementing a custom Profiles provider, you need to determine how generic you want your solution to be. For example, if you decide to implement compression using the classes in the System.IO.Compression namespace (see Chapter 13) or encryption with the classes in the System.Security.Cryptography namespace (see Chapter 25), you’ll also need to decide whether you want to create an all-purpose solution or a more limited provider that’s fine-tuned for your specific scenario.

Similarly, the FactoredProfileProvider has two possible designs:

You can create a provider that’s designed specifically for your database schema.

You can create a generic provider that can work with any database table by making certain assumptions. For example, you can simply assume that profile properties match field names.

The first approach is the most straightforward and in some cases will be the easiest to secure and optimize. However, it also limits your ability to reuse your provider or change your database schema later. The second approach is the one you’ll see in the following example.

The basic idea behind the FactoredProfileProvider is that it will perform its two key tasks (retrieving and updating profile information) through two stored procedures. That gives you a powerful layer of flexibility, because you can modify the stored procedures at any time to use different tables, field names, data types, and even serialization choices.

The critical detail in this example is that the web application chooses which stored procedures to use by using the provider declaration in the web.config file. Here’s an example of how you might use the FactoredProfileProvider in an application:

<profile defaultProvider="FactoredProfileProvider"> <providers >

<clear />

<add name="FactoredProfileProvider" type="FactoredProfileProvider" connectionStringName="SqlServices" updateUserProcedure="Users_Update" getUserProcedure="Users_GetByUserName"/>

</providers> <properties>...</properties>

</profile>

Along with the expected attributes (name, type, and connectionStringName), the <add> tag includes two new attributes: updateUserProcedure and getUserProcedure. The updateUserProcedure

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

indicates the name of the stored procedure that’s used to insert and update profile information. The getUserProcedure indicates the name of the stored procedure that’s used to retrieve profile information.

This design allows you to use the FactoredProfileProvider with any database table. But what about mapping the properties to the appropriate columns? You could take a variety of approaches to make this possible, but the FactoredProfileProvider takes a convenient shortcut. When updating, it simply assumes that every profile property you define corresponds to the name of a stored procedure parameter. So, if you define the following properties:

<properties>

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

</properties>

the FactoredProfileProvider will call the update stored procedure you’ve specified and pass the value in for parameters named @FirstName and @LastName. When querying profile information, the FactoredProfileProvider will look for the field names FirstName and LastName.

This is similar to the design used by the SqlDataSource and ObjectDataSource controls. Although it forces you to follow certain conventions in your two stored procedures, it imposes no other restrictions on the rest of your database. For example, the update stored procedure can insert the information into any series of fields in any table, and the stored procedure used to query profile information can use aliases or joins to construct the expected table.

Coding the FactoredProfileProvider

The first step of creating the FactoredProfileProvider is to derive the class from ProfileProvider:

public class FactoredProfileProvider : ProfileProvider { ... }

All the methods that aren't implemented in this example (see Table 24-6) are simply filled with a single line of code that throws an exception.

Tip One quick way to fill all the methods with exception-throwing logic is to right-click ProfileProvider in the class declaration and choose Refactor Implement Abstract Class.

Initialization

The FactoredProfileProvider needs to keep track of a few basic details, such as the provider name, the connection string, and the two stored procedures. These details are all exposed through readonly properties, as shown here:

private string name; public override string Name

{

get { return name; }

}

private string connectionString; public string ConnectionString

{

get { return connectionString; }

}

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

833

private string updateProcedure; public string UpdateUserProcedure

{

get { return updateProcedure; }

}

private string getProcedure; public string GetUserProcedure

{

get { return getProcedure; }

}

To set these details, you need to override the Initialize() method. At this point, you receive a collection that contains all the attributes of the <add> element that registered the provider. If any of the necessary details are absent, you should raise an exception.

public override void Initialize(string name, NameValueCollection config)

{

this.name = name;

// Initialize values from web.config. ConnectionStringSettings connectionStringSettings =

ConfigurationManager.ConnectionStrings[config["connectionStringName"]]; if (connectionStringSettings == null ||

connectionStringSettings.ConnectionString.Trim() == "")

{

throw new HttpException("You must supply a connection string.");

}

else

{

connectionString = connectionStringSettings.ConnectionString;

}

updateProcedure = config["updateUserProcedure"]; if (updateProcedure.Trim() == "")

{

throw new HttpException(

"You must specify a stored procedure to use for updates.");

}

getProcedure = config["getUserProcedure"]; if (getProcedure.Trim() == "")

{

throw new HttpException(

"You must specify a stored procedure to use for retrieving user records.");

}

}

Reading Profile Information

When the web page accesses any profile information, ASP.NET calls the GetPropertyValues() method. It passes in two parameters—a SettingsContext object that includes information such as the current user name and a SettingsPropertyCollection object that contains a collection of all the profile properties that the application has defined (and expects to be able to access). You need to return a SettingsPropertyValueCollection with the corresponding values.

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

Before doing anything, you should create a new SettingsPropertyValueCollection:

public override SettingsPropertyValueCollection GetPropertyValues( SettingsContext context, SettingsPropertyCollection properties)

{

// This collection will store the retrieved values. SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();

...

Now create the ADO.NET objects that you need in order to execute the stored procedure that retrieves the profile information. The connection string and stored procedure name are specified through the configuration attributes that were retrieved in the Initialize() method.

...

SqlConnection con = new SqlConnection(connectionString);

SqlCommand cmd = new SqlCommand(getProcedure, con);

cmd.CommandType = CommandType.StoredProcedure;

...

The only nonconfigurable assumption in this code is that the stored procedure accepts a parameter named @UserName. You could add other configuration attributes to make this parameter name configurable.

...

cmd.Parameters.Add(new SqlParameter("@UserName", (string)context["UserName"]));

...

Now you’re ready to execute the command and retrieve the matching record. Depending on the design of the database, this record may actually represent the joining of two tables (one with a list of users and one with profile information), or all the information may come from a single table.

...

try

{

con.Open();

SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);

// Get the first row. reader.Read();

...

Once you have the row, the next task is to loop through the SettingsPropertyCollection. For each defined property, you should retrieve the value from the corresponding field. However, it’s perfectly valid for a user to exist without any profile information. In this case (when reader.HasRows is false), you should still create the SettingsPropertyValue objects for each requested property, but don’t bother setting the property values. They’ll simply keep their defaults.

...

foreach (SettingsProperty property in properties)

{

SettingsPropertyValue value = new SettingsPropertyValue(property);

if (reader.HasRows)

{

value.PropertyValue = reader[property.Name];

}

values.Add(value);

}

...

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

835

The final step is to close the reader and connection and to return the collection of values.

...

reader.Close();

}

finally

{

con.Close();

}

return values;

}

Note If you want to mimic the behavior of the SqlProfileProvider, you should also update the database with the last activity time whenever the GetPropertyValues() method is called.

Updating Profile Information

The job of updating profile properties in the SetPropertyValues() is just as straightforward as reading property values. This time, the update stored procedure is used, and every supplied value is translated into a parameter with the same name.

Here’s the complete code:

public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection values)

{

// Prepare the command.

SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand(updateProcedure, con); cmd.CommandType = CommandType.StoredProcedure;

//Add the parameters.

//The assumption is that every property maps exactly

//to a single stored procedure parameter name. foreach (SettingsPropertyValue value in values)

{

cmd.Parameters.Add(new SqlParameter(value.Name, value.PropertyValue));

}

//Again, this provider assumes the stored procedure accepts a parameter named

//@UserName.

cmd.Parameters.Add(new SqlParameter("@UserName", (string)context["UserName"]));

// Execute the command. try

{

con.Open();

cmd.ExecuteNonQuery();

}

finally

{

con.Close();

}

}

This completes the code you need for the simple implementation of the FactoredProfileProvider.

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

Note If you want to mimic the behavior of the SqlProfileProvider, you should also update the database with the last update time whenever the SetPropertyValues() method is called.

Testing the FactoredProfileProvider

To try this example, you need to create, at a bare minimum, a database with a Users table and the two stored procedures. The following example demonstrates an example with a Users table that provides address information (see Figure 24-7)

Figure 24-7. A custom Users table

A straightforward procedure named Users_GetByUserName queries the profile information from the table:

CREATE PROCEDURE Users_GetByUserName @UserName varchar(50)

AS

SELECT * FROM Users WHERE UserName = @UserName

GO

The Users_Update stored procedure is a little more interesting. It begins by checking for the existence of the specified user. If the user doesn’t exist, a record is created with the profile information. If the user does exist, that record is updated. This design meshes with the behavior of the SqlProfileProvider.

Note Remember, all Profile providers assume the user has already been authenticated. If you’re using the same table to store user authentication information and profile information, an unauthenticated user must have a record in this table. However, this isn’t the case if you use separate tables or Windows authentication.

Here’s the complete code for the Users_Update stored procedure:

CREATE PROCEDURE [Users_Update] (@UserName [varchar](50), @AddressName [varchar](50), @AddressStreet [varchar](50), @AddressCity [varchar](50), @AddressState [varchar](50), @AddressZipCode [varchar](50),

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

837

@AddressCountry [varchar](50))

AS

DECLARE @Match int

SELECT @Match = COUNT(*) FROM Users WHERE UserName = @UserName

IF (@Match = 0) INSERT INTO Users

(UserName, AddressName, AddressStreet, AddressCity, AddressState, AddressZipCode, AddressCountry)

VALUES

(@UserName, @AddressName, @AddressStreet, @AddressCity, @AddressState, @AddressZipCode, @AddressCountry)

IF (@Match = 1) UPDATE Users SET

[UserName] = @UserName, [AddressName] = @AddressName, [AddressStreet] = @AddressStreet, [AddressCity] = @AddressCity, [AddressState] = @AddressState, [AddressZipCode] = @AddressZipCode, [AddressCountry] = @AddressCountry

WHERE

(UserName = @UserName)

GO

Note You can download a script to create this table and the corresponding stored procedures with the sample code for this chapter.

To use this table, you simply need to configure the FactoredProfileProvider, identify the stored procedures you’re using, and define all the fields of the Users table that you need to access. Here are the complete web.config configuration details:

<profile defaultProvider="FactoredProfileProvider"> <providers >

<clear />

<add name="FactoredProfileProvider" type="FactoredProfileProvider" connectionStringName="SqlServices" updateUserProcedure="Users_Update" getUserProcedure="Users_GetByUserName"/>

</providers>

<properties>

<add name="AddressName"/> <add name="AddressStreet"/> <add name="AddressCity"/> <add name="AddressState"/> <add name="AddressZipCode"/> <add name="AddressCountry"/>

</properties>

</profile>