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

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

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

268 C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

CREATE PROCEDURE

InsertEmployee

@EmployeeID

int OUTPUT

@FirstName

varchar(10),

@LastName

varchar(20),

@TitleOfCourtesy

varchar(25),

AS

 

INSERT INTO Employees

(TitleOfCourtesy, LastName, FirstName, HireDate)

VALUES (@TitleOfCourtesy, @LastName, @FirstName, GETDATE());

SET @EmployeeID = @@IDENTITY

GO

 

CREATE PROCEDURE

DeleteEmployee

@EmployeeID

int

AS

 

DELETE FROM Employees WHERE EmployeeID = @EmployeeID

GO

CREATE PROCEDURE UpdateEmployee @EmployeeID int, @TitleOfCourtesy varchar(25), @LastName varchar(20), @FirstName varchar(10) AS

UPDATE Employees

SET TitleOfCourtesy = @TitleOfCourtesy,

LastName = @LastName,

FirstName = @FirstName

WHERE EmployeeID = @EmployeeID

GO

CREATE PROCEDURE GetAllEmployees

AS

SELECT EmployeeID, FirstName, LastName, TitleOfCourtesy FROM Employees

GO

CREATE PROCEDURE CountEmployees

AS

SELECT COUNT(EmployeeID) FROM Employees

GO

CREATE PROCEDURE GetEmployee

@EmployeeID

int

 

AS

 

 

SELECT FirstName, LastName, TitleOfCourtesy FROM Employees

WHERE EmployeeID =

@EmployeeID

GO

 

 

The Data Utility Class

Finally, you need the utility class that performs the actual database operations. This class uses the stored procedures that were shown in the previous section.

In this example, the data utility class is named EmployeeDB. It encapsulates all the data access code and database-specific details. Here’s the basic outline:

C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

269

public class EmployeeDB

{

private string connectionString;

public EmployeeDB()

{

// Get default connection string.

connectionString = WebConfigurationManager.ConnectionStrings[ "Northwind"].ConnectionString;

}

public EmployeeDB(string connectionStringName)

{

// Get the specified connection string.

connectionString = WebConfigurationManager.ConnectionStrings[ "connectionStringName"].ConnectionString;

}

public int InsertEmployee(EmployeeDetails emp) { ... }

public void DeleteEmployee(int employeeID) { ... }

public void UpdateEmployee(EmployeeDetails emp) { ... }

public EmployeeDetails GetEmployee() { ... }

public EmployeeDetails[] GetEmployees() { ... }

public int CountEmployees() { ... }

}

Note You may have noticed that the EmployeeDB class uses instance methods, not static methods. That’s because even though the EmployeeDB class doesn’t store any state from the database, it does store the connection string as a private member variable. Because this is an instance class, the connection string can be retrieved every time the class is created, rather than every time a method is invoked. This approach makes the code a little clearer and allows it to be slightly faster (by avoiding the need to read the web.config file multiple times). However, the benefit is fairly small, so you can use static methods just as easily in your database components.

Each method uses the same careful approach, relying exclusively on a stored procedure to interact with the database. Here’s the code for inserting a record:

public int InsertEmployee(EmployeeDetails emp)

{

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

cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10)); cmd.Parameters["@FirstName"].Value = emp.FirstName;

cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20)); cmd.Parameters["@LastName"].Value = emp.LastName;

cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25));

cmd.Parameters["@TitleOfCourtesy"].Value = emp.TitleOfCourtesy;

270 C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Direction = ParameterDirection.Output;

try

{

con.Open();

cmd.ExecuteNonQuery();

return (int)cmd.Parameters["@EmployeeID"].Value;

}

catch (SqlException err)

{

//Replace the error with something less specific.

//You could also log the error now.

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

}

}

As you can see, the method accepts data using the EmployeeDetails package. Any errors are caught, and the sensitive internal details are not returned to the web-page code. This prevents the web page from providing information that could lead to possible exploits. This would also be an ideal place to call another method in a logging component to report the full information in an event log or another database.

The GetEmployee() and GetEmployees() methods return the data using the EmployeeDetails package:

public EmployeeDetails GetEmployee(int employeeID)

{

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

cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Value = employeeID;

try

{

con.Open();

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

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

EmployeeDetails emp = new EmployeeDetails( (int)reader["EmployeeID"], (string)reader["FirstName"], (string)reader["LastName"], (string)reader["TitleOfCourtesy"]); reader.Close();

return emp;

}

catch (SqlException err)

{

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

271

}

}

public List<EmployeeDetails> GetEmployees()

{

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

// Create a collection for all the employee records. List<EmployeeDetails> employees = new List<EmployeeDetails>();

try

{

con.Open();

SqlDataReader reader = cmd.ExecuteReader(); while (reader.Read())

{

EmployeeDetails emp = new EmployeeDetails( (int)reader["EmployeeID"], (string)reader["FirstName"], (string)reader["LastName"], (string)reader["TitleOfCourtesy"]); employees.Add(emp);

}

reader.Close(); return employees;

}

catch (SqlException err)

{

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

}

}

The UpdateEmployee() method plays a special role. It determines the concurrency strategy of your database component (see the next section, “Concurrency Strategies”).

Here’s the code:

public void UpdateEmployee(int EmployeeID, string firstName, string lastName, string titleOfCourtesy)

{

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

cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10)); cmd.Parameters["@FirstName"].Value = firstName;

cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20)); cmd.Parameters["@LastName"].Value = lastName;

cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25));

cmd.Parameters["@TitleOfCourtesy"].Value = titleOfCourtesy; cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Value = EmployeeID;

272 C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

try

{

con.Open();

cmd.ExecuteNonQuery();

}

catch (SqlException err)

{

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

}

}

Finally, the DeleteEmployee() and CountEmployees() methods fill in the last two ingredients:

public void DeleteEmployee(int employeeID)

{

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

cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Value = employeeID;

try

{

con.Open();

cmd.ExecuteNonQuery();

}

catch (SqlException err)

{

throw new ApplicationException("Data error.");

}

finally

{

con.Close();

}

}

public int CountEmployees()

{

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

try

{

con.Open();

return (int)cmd.ExecuteScalar();

}

catch (SqlException err)

{

throw new ApplicationException("Data error.");

}

finally

C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

273

{

con.Close();

}

}

Concurrency Strategies

In any multiuser application, including web applications, there’s the potential that more than one user will perform overlapping queries and updates. This can lead to a potentially confusing situation where two users, who are both in possession of the current state for a row, attempt to commit divergent updates. The first user’s update will always succeed. The success or failure of the second update is determined by your concurrency strategy.

There are several broad approaches to concurrency management. The most important thing to understand is that you determine your concurrency strategy by the way you write your UPDATE commands (particularly the way you shape the WHERE clause).

Here are the most common examples:

Last-in-wins updating: This is a less restrictive form of concurrency control that always commits the update (unless the original row has been deleted). Every time an update is committed, all the values are applied. Last-in-wins makes sense if data collisions are rare. For example, you can safely use this approach if there is only one person responsible for updating a given group of records. Usually, you implement a last-in-wins by writing a WHERE clause that matches the record to update based on its primary key. The UpdateEmployee() method in the previous example uses the last-in-wins approach.

UPDATE Employees SET ... WHERE EmployeeID=@ID

Match-all updating: To implement this strategy, you add a WHERE clause that tries to match the current values of every field in the record to your UPDATE statement. That way, if even a single field has been modified, the record won’t be matched and the change will not succeed. One problem with this approach is that compatible changes are not allowed. For example, if two users are attempting to modify different parts of the same record, the second user’s change will be rejected, even though it doesn’t conflict. Another, more significant, problem with the match-all updating strategy is that it leads to large, inefficient SQL statements. You can implement the same strategy more effectively with timestamps (see the next point).

UPDATE Employees SET ... WHERE EmployeeID=@ID AND FirstName=@FirstName

AND LastName=@LastName ...

Timestamp-based updating: Most database systems support a timestamp column, which the data source updates automatically every time a change is performed. You do not need to modify the timestamp column manually. However, you can examine it for changes and thereby determine if another user has recently applied an update. If you write an UPDATE statement with a WHERE clause that incorporates the primary key and the current timestamp, you’re guaranteed to update the record only if it hasn’t been modified, just like with match-all updating.

UPDATE Employees SET ... WHERE EmployeeID=@ID AND TimeStamp=@TimeStamp

Changed-value updating: This approach attempts to apply just the changed values in an UPDATE command, thereby allowing two users to make changes at the same time if these changes are to different fields. The problem with this approach is it can be complex, because you need to keep track of what values have changed (in which case they should be incorporated in the WHERE clause) and what values haven’t.

274 C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

To get a better understanding of how this plays out, consider what happens if two users attempt to commit different updates to an employee record using a method such as UpdateEmployee(), which implements last-in-wins concurrency. The first user updates the mailing address. The second user changes the employee name and inadvertently reapplies the old mailing address at the same time. The problem is that the UpdateEmployee() method doesn’t have any way to know what changes you are committing. This means that it pushes all the in-memory values back to the data source, even if these old values haven’t been changed (and wind up overwriting someone else’s update).

If you have large, complex records and you need to support different types of edits, the easiest way to solve a problem like this may be to create more-targeted methods. Instead of creating a generic UpdateEmployee() method, use more-targeted methods such as UpdateEmployeeAddress() or ChangeEmployeeStatus(). These methods can then execute more limited UPDATE statements that don’t risk reapplying old values.

Testing the Component

Now that you've created the data component, you just need a simple test page to try it out. As with any other component, you must begin by adding a reference to the component assembly. Then you can import the namespace it uses to make it easier to implement the EmployeeDetails and EmployeeDB classes. The only step that remains is to write the code that interacts with the classes. In this example, the code takes place in the Page.Load event handler.

First, the code retrieves and writes the number and the list of employees by using a private WriteEmployeesList() method that translates the details to HTML. Next, the code adds a record and lists the table content again. Finally, the code deletes the added record and shows the content of the Employees table one more time.

Here’s the complete page code:

public partial class ComponentTest : System.Web.UI.Page

{

// Create the database component so it's available anywhere on the page. private EmployeeDB db = new EmployeeDB();

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

{

WriteEmployeesList();

int empID = db.InsertEmployee(

new EmployeeDetails(0, "Mr.", "Bellinaso", "Marco")); HtmlContent.Text += "<br />Inserted 1 employee.<br />";

WriteEmployeesList();

db.DeleteEmployee(empID);

HtmlContent.Text += "<br />Deleted 1 employee.<br />";

WriteEmployeesList();

}

private void WriteEmployeesList()

{

StringBuilder htmlStr = new StringBuilder("");

int numEmployees = db.CountEmployees(); htmlStr.Append("<br />Total employees: <b>");

C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

275

htmlStr.Append(numEmployees.ToString()); htmlStr.Append("</b><br /><br />");

List<EmployeeDetails> employees = db.GetEmployees(); foreach (EmployeeDetails emp in employees)

{

htmlStr.Append("<li>");

htmlStr.Append(emp.EmployeeID); htmlStr.Append(" "); htmlStr.Append(emp.TitleOfCourtesy); htmlStr.Append(" <b>"); htmlStr.Append(emp.FirstName); htmlStr.Append("</b>, "); htmlStr.Append(emp.LastName); htmlStr.Append("</li>");

}

htmlStr.Append("<br />"); HtmlContent.Text += htmlStr.ToString();

}

}

Figure 8-2 shows the page output.

Figure 8-2. Using a database component

276 C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

Disconnected Data

So far, all the examples you’ve seen have used ADO.NET’s connection-based features. When using this approach, data ceases to have anything to do with the data source the moment it is retrieved. It’s up to your code to track user actions, store information, and determine when a new command should be generated and executed.

ADO.NET emphasizes an entirely different philosophy with the DataSet object. When you connect to a database, you fill the DataSet with a copy of the information drawn from the database. If you change the information in the DataSet, the information in the corresponding table in the database isn’t changed. That means you can easily process and manipulate the data without worry, because you aren’t using a valuable database connection. If necessary, you can reconnect to the original data source and apply all your DataSet changes in a single batch operation.

Of course, this convenience isn’t without drawbacks, such as concurrency issues. Depending on how your application is designed, an entire batch of changes may be submitted at once. A single error (such as trying to update a record that another user has updated in the meantime) can derail the entire update process. With studious coding you can protect your application from these prob- lems—but it requires additional effort.

On the other hand, sometimes you might want to use ADO.NET’s disconnected access model and the DataSet. Some of the scenarios in which a DataSet is easier to use than a DataReader include the following:

When you need a convenient package to send the data to another component (for example, if you’re sharing information with other components or distributing it to clients through a web service).

When you need a convenient file format to serialize the data to disk (the DataSet includes built-in functionality that allows you to save it to an XML file).

When you want to navigate backward and forward through a large amount of data. For example, you could use a DataSet to support a paged list control that shows a subset of information at a time. The DataReader, on the other hand, can move in only one direction: forward.

When you want to navigate among several different tables. The DataSet can store all these tables, and information about the relations between them, thereby allowing you to create easy master-detail pages without needing to query the database more than once.

When you want to use data binding with user interface controls. You can use a DataReader for data binding, but because the DataReader is a forward-only cursor, you can’t bind your data to multiple controls. You also won’t have the ability to apply custom sorting and filtering criteria, like you can with the DataSet.

When you want to manipulate the data as XML.

When you want to provide batch updates through a web service. For example, you might create a web service that allows a client to download a DataTable full of rows, make multiple changes, and then resubmit it later. At that point, the web service can apply all the changes in a single operation (assuming no conflicts occur).

In the remainder of this chapter, you’ll learn about how to retrieve data into a DataSet. You’ll also learn how to retrieve data from multiple tables, how to create relationships between these inmemory data tables, how to sort and filter data, and how to search for specific records. However, you won’t consider the task of using the DataSet to perform updates. That’s because the ASP.NET model lends itself more closely to direct commands, as discussed in the next section.

C H A P T E R 8 D ATA C O M P O N E N T S A N D T H E D ATA S E T

277

Web Applications and the DataSet

A common misconception is that the DataSet is required to ensure scalability in a web application. Now that you understand the ASP.NET request processing architecture, you can probably see that this isn’t the case. A web application runs only for a matter of seconds (if that long). This means that even if your web application uses direct cursor-based access, the lifetime of the connection

is so short that it won’t significantly reduce scalability, except in the mostly highly trafficked web applications.

In fact, the DataSet makes much more sense with distributed applications that use a rich Windows client. In this scenario, the clients can retrieve a DataSet from the server (perhaps using a web service), work with their DataSet objects for a long period of time, and reconnect to the system only when they need to update the data source with the batch of changes they’ve made. This allows the system to handle a much larger number of concurrent users than it would be able to if each client maintained a direct, long-lasting connection. It also allows you to efficiently share resources by caching data on the server and pooling connections between client requests.

The DataSet also acts as a neat package of information for rich client applications that are only intermittently connected to your system. For example, consider a traveling sales associate who needs to enter order information or review information about sales contacts on a laptop. Using the DataSet, an application on the user’s laptop can store disconnected data locally and serialize it to an XML file. This allows the sales associate to build new orders using the cached data, even when no Internet connection is available. The new data can be submitted later when the user reconnects to the system.

So, where does all this leave ASP.NET web applications? Essentially, you have two choices. You can use the DataSet, or you can use direct commands to bypass the DataSet altogether. Generally speaking, you’ll bypass the DataSet when adding, inserting, or updating records. However, you won’t avoid the DataSet completely. In fact, when you retrieve records, you’ll probably want to use the DataSet, because it supports a few indispensable features. In particular, the DataSet allows you easily pass a block of data from a database component to a web page. The DataSet also supports data binding, which allows you to display your information in advanced data controls such as the GridView. For that reason, most web applications retrieve data into the DataSet but perform direct updates using straightforward commands.

Note Web services represent the only real web application scenario in which you might decide to perform batch updating through a DataSet. In this case, a rich client application downloads the data as a DataSet, edits it, and resubmits the DataSet later to commit its changes.

XML Integration

The DataSet also provides native XML serialization. You don’t need to even be aware of this to enjoy its benefits, such as being able to easily serialize a DataSet to a file or transmit the DataSet to another application through a web service. At its best, this feature allows you to share your data with clients written in different programming languages and running on other operating systems.

The XML integration in the DataSet also allows you to access the information in the DataSet as an XML document at any time. You can even modify values, remove rows, and add new records by modifying the XML without losing any information. This deep XML integration isn’t required for a typical self-contained web application. In fact, if you modify relational data through an XML model, you can run into several types of problems that you won’t face using the DataSet object directly,