Concurrent coding in ADO (msdn)

Source: Internet
Author: User
Tags case statement net command
Concurrent coding release date in ADO: 12/23/2004 | updated on: 12/23/2004

Rick Dobson

What happens if two users try to update the same row at the same time? Since the advent of shared databases, similar problems have been plagued by developers. Currently, ADO. net uses a method named "Open concurrency" to flexibly solve this problem. Rick Dobson explains how this method works and how to make applications more robust in highly scalable environments.

Content on this page
Concurrency Overview
Data Update concurrency
Refresh dataset and resubmit updates
Resubmit, refresh, or restore
Insert and delete concurrency
Form2 load event Process
Multi-User insertion
Multi-User deletion Problems
Summary

ADO. NET is specially designed to improve scalability-especially for data manipulation when multiple users need to manipulate the same rows in database tables. An important feature of ADO. Net that helps improve scalability is that it relies on open concurrency for data manipulation.

With open concurrency, You do not lock a row from the moment it is prepared to update it. Row locking applies only when changes are submitted in ADO. net. Therefore, another user can change the same row before the first user submits changes to the row. In this case, the first user updates the dbconcurrencyexception object. ADO. NET provides a function that helps solve such problems. Multi-user problems may also affect deletion and insertion using ADO. net.

This article describes the coding principles for processing concurrency issues during data manipulation using ADO. net. You will understand the context that causes the concurrency issue and how to handle the issue for update, insert, and delete, respectively. By commenting on this topic, You will be provided with a set of core skills to handle basic concurrency issues, as well as a foundation to increase your skills to handle more complex concurrency situations.

Concurrency Overview

The cause of concurrency issues with ADO. NET is that users usually make indirect changes to the database. Ado. Net connects users to the database through the dataadapter object, and this object depends on the connection object. Instead of submitting changes directly to the database through the dataadapter, the user changes the local DataSet object. Dataset may contain one or more able objects, and each datatable may contain multiple datarow objects.

The dataadapter object acts as a two-direction pump between the dataset and the database. The ADO. Net solution can initially fill the dataset with the dataadapter fill method. After the user modifies one or more datarow objects in the datatable of the dataset, the application can call the update method of the dataadapter to transmit these changes to the database. The update method is operated by the updatecommand, deletecommand, and insertcommand attributes of dataadapter. These attributes represent command objects that wrap SQL update, delete, and insert statements (or stored procedure calls.

When an ADO. NET application has multiple users, each user usually has access to a separate dataset (and usually has access to a separate dataadapter ). When multiple users can change datasets and call the update method of dataadapter at the same time, at least one dataset may become out of sync with the database. In this case, the dataadapter of the unsynchronized dataset does not know which rows to apply a group of changes. When you call the update method of the dataadapter that is not synchronized with dataset, ADO. Net triggers the dbconcurrencyexception object for the first row with the unsynchronized value between the dataset and the database.

There are four possible datarowversion enumeration values, but the relationship between two datarowversion enumeration values and basic concurrency problems is particularly close: original and current. Original enumeration applies to datarow after the fill method but before making any changes to its column value. Current applies to datarow after one or more column values are changed but before these values are submitted to the database. After the data adapter of datarow in datatable successfully calls the update method, ADO. Net assigns the current value of datarow to the original value of datarow.

The original enumeration and current enumeration references of datarow are critical to the design of SQL update, delete, and insert statements for transmitting changes from dataset to the database. For example, the update statement uses the current value in its set clause, but the WHERE clause of the statement must contain reference to the original value. By referencing the original value, dataadapter can evaluate whether the dataset is still synchronized with the database.

Back to Top

Data Update concurrency

The module behind form1 in the hcvsconcurrency project (which is included in the accompanying download file) simulates two users who can change the tables in the database, describes several coding principles that are useful when processing data modifications. This form uses the shippers table in the SQL Server northwind database to enable the change of the companyName column value of the row containing the shipperid value 1.

To simplify the example, I created a dataadapter (sqldataadapter1) graphically, which automatically fills in the updatecommand, deletecommand, and insertcommand attributes to achieve open concurrency. Then, I used the context menu of sqldataadpapter1 to generate three dataset whose name attributes are dataset1user11, dataset1user21, and dataset1fromdb1. Dataset1user11 and dataset1user21 dataset are used for user 1 and user 2.Figure 1The form1 design is displayed, and the ADO. NET component is displayed in the component bar under the form.

Back to Top

Refresh dataset and resubmit updates

The click event process of button1 tries to update the database with the text attribute value of textbox1. This process Implements user 1's changes. The click event process of button2 (used by user 2) executes the same task for textbox2. The second process demonstrates more mature technologies used to handle open concurrency conflicts. When any user tries to modify the value of the companyName column, the code behind form1 begins to modify dataset1user11 or dataset1user21 and calls the update method of sqldataadapter1. If the modification is successful, the code will clear checkbox1, which indicates an open concurrency conflict. If the modification fails, ADO. NET will cause dbconcurrencyexception and the exception object will be processed by a try... catch... finally statement.

The next code snippet (the load event process from form1) initializes the dataset of user 1 and user 2. The two fill methods call the selectcommand Attribute Based on sqldataadapter1 and fill the two local dataset with the value in the northwind database. The selectcommand SQL statement returns all three columns of the shippers table for all rows. The code snippet also calls refreshcontrols.

SqlDataAdapter1.Fill(DataSet1User11)SqlDataAdapter1.Fill(DataSet1User21)RefreshControls()

The refreshcontrols process (whose content is displayed in the next code block) is called from the form1 load event process and the button1 and button2 click events. The application fills in dataset1fromdb1 dataset through the refreshcontrols process. Then, the Code uses the companyName column value in the first datarow of shippers datatable in dataset1fromdb1 in the allocation Statement of label1, textbox1, and textbox2. The expression of the text attribute of each Textbox Control adds 1 or 2 to the value of the companyName column. Note that in the following syntax, you can use the name or index number starting from scratch to reference the datatable and datatable columns in the dataset.

SqlDataAdapter1.Fill(DataSet1FromDB1)Label1.Text = "Current DB value: " & _ DataSet1FromDB1.Tables("Shippers"). _ Rows(0)("CompanyName")TextBox1.Text = DataSet1User11.Tables(0). _ Rows(0)(1) & "1"TextBox2.Text = DataSet1User21.Tables(0). _ Rows(0)(1) & "2"

The following code snippet is selected from the Click Event process of button1, which illustrates a simple method to coordinate open concurrency conflicts. As you can see, this statement actually contains two catch clauses. One is used for the dbconcurrencyexception object, and the other is used for the exception object of any other type. Good programming rules require that the final catch clause be used to capture any exception objects not captured by the previous more specific catch clause.

Try DataSet1User11.Shippers.Rows(0)(1) = _  DataSet1User11.Shippers.Rows(0)(1) + "1" SqlDataAdapter1.Update(DataSet1User11) CheckBox1.Checked = FalseCatch ex As DBConcurrencyException SqlDataAdapter1.Fill(DataSet1User11) DataSet1User11.Shippers.Rows(0)(1) = _  DataSet1User11.Shippers.Rows(0)(1) + "1" SqlDataAdapter1.Update(DataSet1User11) CheckBox1.Checked = TrueCatch ex As Exception str1 = ex.GetType.ToString & _  ControlChars.CrLf & ex.Message MessageBox.Show(str1, "Error form", _  MessageBoxButtons.OK, MessageBoxIcon.Error)Finally RefreshControls()End Try

The try clause of the try... catch... finally statement contains three statements. First, the Code appends "1" to the value of the second column in the first row of shippers able of dataset1user11. Because dataset is typed for all three applications, the code can use. tablename instead of. Tables (tablename) as needed ). The second statement calls the update method of sqldataadapter1. If the update method is successful, the new value assigned to dataset will update the shippers table in the northwind database. Control is then passed to the third statement in the try clause. This statement clears checkbox1.

If user 2 updates the first row of the northwind shippers table since dataset1user11 last refreshed with the database value, the dbconcurrencyexception object is thrown when trying to call the update in the try clause, and pass the control to the first catch clause. The code in this clause first uses the fill method of sqldataadapter1 to refresh dataset1user11. Next, the example executes the same update as in the try clause, but this time the dataset value is known to be synchronized with the database. The first catch clause ends by assigning a check mark to checkbox1.

The finally Clause runs no matter whether the update method is successful or fails. The unique statement of the finally clause calls the refreshcontrols discussed earlier.

Back to Top

Resubmit, refresh, or restore

After you click button1, immediately clicking button2 may fail to generate open concurrency, which will cause dbconcurrencyexception object. This is because the value of the companyName column in the first line of dataset1user21 reflects the database that was changed by clicking button1. Unlike the method demonstrated by the code snippet above to forcibly resolve the open concurrency conflict, the Click Event process of button2 allows you to choose from three possible solutions. You can use the refreshed dataset to submit the changes again, or discard the changes but still refresh dataset1user21, or restore the database to the original value of the first datarow In the shippers datatable.

The application presents the three options to the user through inputbox.Figure 2The format of form1 after the initial click of button1 is displayed. Note that the form reports the current database value as speedy express1. The inputbox under form1 provides three options for resolving concurrency conflicts. The options are simplified in the title of inputbox. These options are numbered 1, 2, and 3. The inputbox body displays three values:

The original attribute value in the second column of the shippers datatable in dataset1user21.

The current value of the second column in The datatable.

The current northwind database value of the companyName column in the first row of the shippers table.

The try... catch... finally statement in the button2 Click Event process has the same overall design as the try... catch... finally statement in the button1 Click Event process. However, the catch clause of the dbconcurrencyexception object is different. The following code snippet shows the dbconcurrencyexception catch clause that uses three different technologies to handle concurrency conflicts. The code in the catch clause is divided into three parts. The first part sets and displays inputbox. The second part is a select case statement with a separate case clause corresponding to each of the three different concurrency solutions. The last part consists of a single statement that assigns a check mark to checkbox1. Each of the following project symbols summarizes the role of the Case clause in the select case statement.

The case clause corresponding to inputbox function value 1 refreshes dataset1user21 and changes it again. Then, the changes are submitted to the database by calling the update method of sqldataadapter1.

When the inputbox function returns 2, the Select case statement only refreshes dataset1user21, instead of resubmitting the changes to the database. This option synchronizes dataset1user21 with the database.

The inputbox function value 3 is the companyName value in the first datarow of the shippers datatable. The original value in dataset1user21 is restored, and the value is allocated to the corresponding column value in the northwind shippers table. The syntax of this method encapsulates the SQL update statement in the ADO. Net command object. Sqldataadapter1 is not used when the change is submitted to the database.

The final case else clause captures any value except 1, 2, and 3 provided to the inputbox function.

Catch ex As DBConcurrencyException str1 = "Value summary:" & ControlChars.CrLf str1 &= "Original value: " & _  DataSet1User21.Shippers.Rows(0) _  ("CompanyName", DataRowVersion.Original). _  ToString & ControlChars.CrLf str1 &= "Current value: " & _  DataSet1User21.Shippers.Rows(0) _  ("CompanyName", DataRowVersion.Current). _  ToString & ControlChars.CrLf SqlDataAdapter1.Fill(DataSet1FromDB1) str1 &= "Database value: " & _  DataSet1FromDB1.Shippers.Rows(0) _  ("CompanyName", DataRowVersion.Current) str1Return = InputBox(str1, _  "1 re-submit change, 2 abort change, " & _  "3 restore original", "2") Select Case str1Return  Case "1"   SqlDataAdapter1.Fill(DataSet1User21)   DataSet1User21.Shippers.Rows(0)(1) = _    Mid(DataSet1User21.Shippers.Rows(0)(1), 1, _    Len(DataSet1User21.Shippers.Rows(0)(1)) _    - 1) + "2"   SqlDataAdapter1.Update(DataSet1User21)  Case "2"   SqlDataAdapter1.Fill(DataSet1User21)  Case "3"   Dim cmd1 As New SqlClient.SqlCommand   cmd1.Connection = SqlConnection1   cmd1.CommandText = _    "Update Shippers SET CompanyName = '" & _    DataSet1User21.Shippers.Rows(0) _    ("CompanyName", DataRowVersion.Original). _    ToString & "' WHERE ShipperID = " & _    DataSet1User21.Shippers.Rows(0) _    ("ShipperID", DataRowVersion.Current). _    ToString   cmd1.Connection.Open()   cmd1.ExecuteNonQuery()   cmd1.Connection.Close()  Case Else   MessageBox.Show("1,2, or 3 only", _   "Warning message", MessageBoxButtons.OK, _   MessageBoxIcon.Information) End Select CheckBox1.Checked = True
Back to Top

Insert and delete concurrency

In addition to updates, concurrency issues or related categories also apply to insertion and deletion. However, the expressions and solutions for these problems are different (insertion and deletion are different, and they are different from updates ).Figure 3Shows the design view of form2 in the hcvsconcurrency project. This project contains three textbox controls, which are used to specify the column values to be added to a new row or the primary key of the row to be deleted. Each of these two users has a row of buttons in the form. The first row is used for user 1 to insert and delete rows, and the database value is used to refresh dataset1user11. The button on the second line enables the same feature for user 2 and dataset1user21. The DataGrid Control under these buttons displays the current value in the shippers table of the northwind database.

You can copy the ADO. NET Component created in graphical form from form1 to form2 so that these components can be used for form2. A custom dataadapter (dap1) is designed during the load event process of form2 to facilitate insertion of new rows into the northwind shippers table. The custom dataadapter code is interesting for two reasons. First, it illustrates the general design principles for creating a dataadapter that is used together with dataset. Next, it explains how to assign a value to a column with the identity attribute settings, for example, the shipperid column in the shippers table.

Back to Top

Form2 load event Process

The code behind form2 declares dap1 at the module level. A common case is the variable that corresponds to the ADO. Net class instance declared at the module level, because these instances are often used by two or more processes. The code used to specify dap1 dataadapter has two parts. The core attribute that the value is allocated to the dataadapter at the beginning. The second part demonstrates how to specify the dataadapter parameters.

The following code snippet assigns a value to the dap1 attribute so that it can be used in form2. The instantiation Statement of dap1 reuse the sqlconnection1 object created graphically to direct the dataadapter to the northwind database. The SQL string specifies the base table to which the dataadapter is connected and the columns in the table. The allocation statement of the insertcommand attribute of dap1 contains two SQL statements. The Set identity_insert statement can be used to allocate values to the shipperid column (which has the identity attribute setting. The insert into statement specifies how to transmit the column values in the rows in the shippers datatable in the dataset to the shippers table in the northwind database. Although the selectcommand attribute assigns sqlconnection1 to dap1, sqlconnection1 must be allocated to the connection Member of insertcommand in dap1.

The three group allocation statements (corresponding to each parameter) instantiate the parameter processing parameters in the insert into SQL statement. You need to add a parameter with an appropriate name and data type for each parameter in the SQL statement. Depending on the application design, you can use the sourceversion attribute of the parameter object to specify whether the current value or original value of the datarow column is used as the parameter value. @ Shipperid: The allocation statement of the parameter demonstrates the syntax for allocating the current value. However, this statement is not absolutely required in the context of this example, because the current value of the column is the default parameter value.

dap1 = New SqlClient.SqlDataAdapter _ ("SELECT ShipperID, CompanyName, Phone " & _ "FROM Shippers", SqlConnection1)dap1.InsertCommand = New SqlClient.SqlCommand _ ("SET IDENTITY_INSERT Shippers ON ")dap1.InsertCommand.CommandText &= _ "INSERT INTO Shippers " & _ "(ShipperID, CompanyName, Phone) " & _ "VALUES (@ShipperID, @CompanyName, @Phone)"dap1.InsertCommand.Connection = SqlConnection1Dim prm1 As SqlClient.SqlParameter = _ dap1.InsertCommand.Parameters.Add _ ("@ShipperID", SqlDbType.Int)prm1.SourceColumn = "ShipperID"prm1.SourceVersion = DataRowVersion.CurrentDim prm2 As SqlClient.SqlParameter = _ dap1.InsertCommand.Parameters.Add _ ("@CompanyName", SqlDbType.NVarChar, 40)prm2.SourceColumn = "CompanyName"Dim prm3 As SqlClient.SqlParameter = _ dap1.InsertCommand.Parameters.Add _ ("@Phone", SqlDbType.NVarChar, 24)prm3.SourceColumn = "Phone"

The shippers datatable in dataset1user11 and dataset1user21 is initialized by other form2 load event process statements in the following code snippet. The datatable stores the northwind shippers table values of user 1 and user 2. Dataset1fromdb1.

dap1.Fill(DataSet1User11, "Shippers")dap1.Fill(DataSet1User21, "Shippers")PopulateGridFromDB()

The following code appears in the populategridfromdb process. It refreshes a dataset and assigns the dataset to the datasource attribute of the DataGrid Control. In addition to updates, data manipulation tasks include insertion and deletion, but calling the fill method for dataset may not be able to get all the changes made by other users. Although the fill method can obtain updates by itself, it cannot restore the insert and delete operations performed by other users. Clearing the able and refilling the dataset that retains the datatable can indeed obtain a complete new copy of the table from the database to reflect any new row or discarded row since the last refresh of the datatable.

DataSet1FromDB1.Tables("Shippers").Clear()dap1.Fill(DataSet1FromDB1, "Shippers")DataGrid1.DataSource = _ DataSet1FromDB1.Tables("Shippers")

The final code snippet of the load event process of form2 assigns the default value to the text attributes of textbox1, textbox2, and textbox3. These distributions make the form ready for insertion and deletion immediately without changing the original column values in the northwind shippers table. The first statement reminds you to allocate the shipperid column. For example, you can assign a value of 4 to it.

TextBox1.Text = "4"TextBox2.Text = "CAB, Inc."TextBox3.Text = "(123) 456-7890"
Back to Top

Multi-User insertion

As long as you can ensure that all new row inserts are unique, no concurrency issues will occur when the application tries to Insert a new row into the database. However, not every application that allows insertion can make this guarantee. For example, the sample insert code in this section allows you to enter the identity attribute value. Because the application simulates two users, each user may try to enter a row with the same identity attribute value. Even if two users do not specify duplicate identity attribute values, a single user may enter duplicate values for columns with unique constraints in the datatable. When you try to add a datarow to a able with a column value that matches the value of another datarow column, if the column has a unique constraint, A constraintexception object may be thrown. When designing code to allow insertion of new rows, consider these two types of errors.

The click event process of button1 contains two sequential try... catch statements-each type of error corresponds to a try... catch statement. Before error capture is started, the Code creates a new row of column values, which have the same design as the columns in the shippers datatable of dataset1user11. Then, the Code fills in the column of the split row with the value of the Textbox Control at the top of form2.

Dim drw1 As DataRow = _ DataSet1User11.Tables("Shippers").NewRowdrw1("ShipperID") = Integer.Parse(TextBox1.Text)drw1("CompanyName") = TextBox2.Textdrw1("Phone") = TextBox3.Text

The first try... catch statement contains a call to the Add method of the datarowcollection member of shippers datatable in dataset1user11. This add method tries to add new rows that have been previously specified and filled. The datarow column in The datatable has a type and may have constraints. If these conditions are not met, an exception (for example, constraintexception) may be thrown ). The first try... catch statement will detect this class and other exception objects.

Try DataSet1User11.Tables("Shippers"). _  Rows.Add(drw1)Catch ex As Exception str1 = ex.GetType.ToString & _  ControlChars.CrLf & ex.Message MessageBox.Show(str1, "Error form", _  MessageBoxButtons.OK, MessageBoxIcon.Error)End Try

The second try... catch statement detects the sqlexception object generated by the primary key conflict. For the shippers table, two different users allocate duplicate values to the shipperid column (for example, specify shipperid value 4), which will lead to ADO. Net triggering this type of sqlexception object. You can use the when clause of the catch statement to evaluate the SQL Server Error indicated by sqlexception. The parameter of the when clause uses the instr function to search for text that indicates a primary key conflict in the message attribute value returned by the sqlexception class.

Try dap1.Update(DataSet1User11, "Shippers") PopulateGridFromDB()Catch ex As SqlClient.SqlException When InStr _ (ex.Message, "Violation of PRIMARY KEY") > 0 HandlePKViolation _    (DataSet1User11, drw1("ShipperID"))Catch ex As Exception str1 = ex.GetType.ToString & _  ControlChars.CrLf & ex.Message MessageBox.Show(str1, "Error form", _  MessageBoxButtons.OK, MessageBoxIcon.Error)End Try

The preceding code snippet calls the handlepkviolation process when detecting the sqlexception object of a message with a primary key conflict. As you can see in the following code snippets, The handlepkviolation process executes two tasks. First, it displays a message box to identify the problem and recommends one of the two solutions. Second, it operates the shippers datatable in the local dataset (which is represented by a variable named das) to find and remove rows with duplicate primary key values.

str1 = _ "Primary key already in database table.  " & _ "Modify primary key and re-submit or " & _ "abort attempt to insert."MessageBox.Show(str1, "Error form", _ MessageBoxButtons.OK, MessageBoxIcon.Error)drw1 = das.Tables("Shippers"). _ Rows.Find(PKValue)das.Tables("Shippers").Rows.Remove(drw1)

Figure 4 shows form2 and error messages about primary key conflicts. This form is displayed in its DataGrid Control. The shippers table contains a row with the shipperid value 4. This row is input by clicking button1 (user 1 insert). The Textbox Control content is displayed in the figure. Click button2 (user 2 insert) to trigger the sqlexception object caused by primary key conflict. The above code snippet captures the error message and displays the message box (it appears at the bottom of Figure 4 ).

The code of the button2 Click Event process is the same as that of the button1 Click Event process. The most significant difference is that the Code operates dataset1user21 instead of dataset1user11. The handlepkviolation process is used to adapt to one of two dataset with different names. The more general design of this process can adapt to a variable able name and a dataset name at the same time.

Back to Top

Multi-User deletion Problems

Dbconcurrencyexception may occur when you try to delete a row from a table in the database using datarow in the dataset able. This happens when another user deletes the same row in advance since your local dataset was last refreshed. A particularly simple solution to concurrency errors in this context is to clear the dataset and repopulate it with the database. This operation can synchronize the local dataset with the database.

As with the insert task, another local error may be generated when you try to delete datarow from the able. This second error occurs when you try to delete a row that no longer exists. A common practice is to search for the target datarow in the datatable to avoid this type of error. If a match is found in the search, the application can safely delete datarow. Otherwise, the application can bypass the call to the delete method because the target datarow does not exist.

The following code snippet in the button4 click event shows the logic for calling the delete method only when datarow exists. This code snippet is used for user 2; The Click Event process of button3 has similar code for user 1. The Return Value of the find method in the first row is allocated to the text attribute of textbox1 in datarowcollection of shippers datatable in dataset1user21. If the find method finds that a datarow has a primary key equivalent to the value of the text property value, the drw1 variable is not empty. Otherwise, drw1 (which has the datarow type) is nothing. If drw1 is not a nothing, the IF block calls the delete method of datarow. If drw1 is a nothing, the else block only exits the process.

drw1 = DataSet1User21.Tables("Shippers"). _ Rows.Find(Integer.Parse(TextBox1.Text))If Not (drw1 Is Nothing) Then drw1.Delete()Else Exit SubEnd If

When the code calls the delete method of datarow, ADO. Net does not remove datarow from the able. Instead, datarow is only marked for deletion. When the code calls the update method of the dataset container of the datatable and the dataadapter that is marked with datarow to be deleted next time, the dataadapter will try to remove the corresponding row from the database. If the attempt is successful, the dataadapter accepts the changes in the dataset. If the attempt fails for some reason (for example, the dbconcurrencyexception object), datarow will be retained in the able. The following code snippet during the button4 click event shows the syntax used to test the dbconcurrencyexception object after calling the update method of sqldataadapter1. If an exception occurs, the code first clears dataset and then fills dataset1user21 with the fill method.

Try SqlDataAdapter1.Update(DataSet1User21, _  "Shippers") PopulateGridFromDB()Catch ex As DBConcurrencyException DataSet1User21.Clear() dap1.Fill(DataSet1User21, "Shippers")

Before finishing this section, I want to make a simple comparison between the delete and remove methods. The delete method only marks datarow so that dataadapter can remove it. The Remove Method immediately removes datarow from the datarowcollection of the datatable.

Back to Top

Summary

Ado. net loose connects the local dataset with the database through open concurrency. This design feature provides a huge scalability advantage that surpasses ADO (which typically uses parallel concurrent operations. Although ADO. Net contains a rich set of features to simplify the handling of errors that may be caused by open concurrency, you still need to master some basic technologies. This article describes a series of code examples that demonstrate the basic knowledge of the exclusive technology that you may find useful when handling concurrent errors (also known as dbconcurrencyexception objects. The hcvsconcurrency project provided in the download file contains all the technologies described in this article and a valid version of other technologies not described Due to space limitations.

Download409dobson. Zip

For more information about hardcore Visual Studio and pinnacle Hing, visit their http://www.pinpub.com/web site.

Note: This is not a web site of Microsoft Corporation. Microsoft is not liable for its content.

This article was reproduced from the September 2004 release of hardcore Visual Studio. Copyright: 2004, Pinnacle Publishing, Inc. (unless otherwise stated ). All rights reserved. Hardcore Visual Studio is an independently released product of pinnacle Hing, Inc. No part of this article shall be used or reproduced in any form without the prior consent of pinnacle Publishing, Inc. (except for short references in comments ). To contact pinnacle Publishing, inc., call 1-800-788-1900.

Go to the original English page

Http://www.microsoft.com/china/msdn/library/langtool/vsdotnet/usvs04i1.mspx

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.