* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Download ADO .NET Introduction - Professor Jerry Post
Microsoft Jet Database Engine wikipedia , lookup
Open Database Connectivity wikipedia , lookup
Microsoft SQL Server wikipedia , lookup
Entity–attribute–value model wikipedia , lookup
Extensible Storage Engine wikipedia , lookup
Relational model wikipedia , lookup
Functional Database Model wikipedia , lookup
.NET Web Forms ADO .NET Introduction © 2002 by Jerry Post 1 ADO .NET Overview Major question: What is missing? Hint: it is missing in ADO .NET, not just the diagram. ASP .NET Code Data Connector (Establishes connection and opens the database.) Answer: save data Data Adapter Web Form Data Controls Database (Translates commands and exchanges data.) Fill Data Set (Collection of tables and relationships.) Data Bind Data Table *CustomerID LastName FirstName Data View (Based on 1 table.) ORDER BY LastName WHERE … Data Relationship (Associations among tables.) 2 Sample Project: Simple2 You should create a new sample project to test your skills. The completed version should look something like: Simple2 3 Data Connector The Data Connector is usually straightforward. The wizard starts automatically when you add a Data Adapter. Choose the type of database (that picks the provider and the specific connector). Enter the connection information—usually location. Access: Specific physical path. Oracle: Oracle connection information Registered name User name User password SQL Server connection information (but use the SQL Adapter) Server User name User password SQL Server has many shortcuts and tricks within Visual Studio The connector simply establishes the communication links to the database. 4 Data Connector Challenges ADO .NET difficulties Access problem is that it is hard to change the location later. It is also difficult to change databases later (SQL Server to Oracle). Partial Solution (but critical) Store all DB specific code in one place. Do not scatter database code across your application and forms. Before building any database forms, create a new blank Component for the sole purpose of holding all primary database definitions. Solution Explorer + Add + Add New Item + Component Class: DBMain To create a database connection, simply drag one onto the design screen of the new component, and fill in the wizard properties. Select a provider Access: Jet 4.0 Oracle: Oracle Provider for OLE DB (maybe Microsoft version) SQL Server: Use the SQL Adapter instead! Rename the connector: cnnOle 5 TMF: Things Microsoft Forgot I developed some standard routines to handle several common issues. They are stored in a class called TMF. You will eventually end up adding this class to most of your forms. First, add the file to your project, right-click + Add + Add Existing: TMF.vb (you only have to do this once). Second, for any form that you need the routines, open the Code View (View + Code, or right-click); add one line of code, often at the top, along with the control definitions: Dim tmf As TMF = New TMF() Details of the routines will be explained in future slides as they are needed. Notes and some documentation are written as comments in the file. 6 Data Connector: Access Flexibility One problem with the Access connector is that it relies on a fixed pathname, making it difficult to move the database. You need to add the special TMF (Things Microsoft Forgot) component to your project (Add + Existing File). In the DBMain component, expand the Component Designer Generated region, in the Public Sub New(), after InitializeComponent, add Dim tmf As New TMF() Dim strCS As String strCS = tmf.GetCnnString("cnnOleCareer") If (strCS <> "") Then Me.cnnOleCareer.ConnectionString = strCS In the Web.config file, right before the end </configuration>, add/modify <appSettings> <!-- User application and configured property settings go here.--> <!-- Example: <add key="settingName" value="settingValue"/> --> <add key="cnnOleCareer.ConnectionString" value="" /> <add key="cnnOleCareer.DataSource" value="../Database/career.mdb" /> </appSettings> This approach uses a relative path, so you can move the entire project. 7 Data Adapter Comments Data Adapter Is responsible for translating SQL requests to the chosen type of database and returning data rows and messages. Every command or table that you want to access must be associated with a data adapter. Technically, a data adapter can handle several different tables. But, unless the tables will be related (parent/child), it seems easier to create a separate data adapter for each table. Biggest drawback is that SQL Server uses a different data adapter than Access/Oracle. So, if you want to change DBMSs later, you have to replace all of the adapters! Critical: Keep all data adapters in one (maybe two) locations: the DBMain component. Creating a data adapter From the Data toolbox, drag an adapter onto the main DB component. (Ole for Access or Oracle, Sql for SQL Server). Answer the wizard questions. To change later: Configure Data Adapter 8 Create Data Adapter: Query Type First, choose your database connection (not shown here). Second, choose the type of query to use. Access only supports text SQL. SQL Server and Oracle will generally run faster if you use a stored procedure, which is a parameterized view stored and precompiled on the database server. If you let the wizard build the procedure, you should go back to the database and double-check it. 9 Create Data Adapter: Query Type Create the SQL statement to retrieve the data from tables or existing views. Use the Query Builder to minimize typing errors. Warning: if you pull data from more than one table (or from a view that uses more than one table), .NET will not be able to edit that data. Query Builder (next slide) Advanced Options (slide) 10 Data Adapter: Query Builder Add tables Click columns to add: Warning, they are displayed in the order in which you add them. You can move them later, but it is easier to do it right now. Note that columns that are reserved words should use brackets. When in doubt, use brackets. If you use more than one table, .NET will not be able to edit the data. If you JOIN tables, do it in the database itself and use a simple query here to pull from that view. Portability! 11 Data Adapter: Advanced Options If you only need to retrieve data (not change it), uncheck all of these options. In some situations, you will want to skip the optimistic concurrency. If you uncheck this box, all changes will be written to the database, even if someone else changed the data immediately before you. The third option is only available with SQL Server and Oracle (databases that support query languages that can do more than one thing). Useful if you insist on using Identity (AutoNumber) columns and need to insert rows. 12 Data Set Overview A Data Set is roughly a miniature in-memory copy of part of your database. You specify which tables you want to include. You can build relationships (usually parent/child). You can create Data Views, but data views are not queries. They cannot perform JOINs. They can only sort and filter a single data table. Technically, a data set can contain multiple tables, but that feature only seems to be useful if you have related tables. Even then, it is not as useful as you might think. Major issue: the data set is an in-memory copy of your database. The server has to hold the data in memory. Once you send your page to the user, the data set is discarded by the server. If you want to keep it for postback/roundtrips, you have to store the dataset some place. Session variable? Bottom line: big data sets are a disaster! 13 Data Set: Generate Once the adapter is created, it is easy to generate a data set in the main DB component. Right-click on the adapter. Generate Data set Select New and enter a unique data set name. Make sure the proper table is checked. Check the box to add it to the designer. If you need to delete a dataset, you will have to delete it from the .xsd list of files (and delete it from the DB code). 14 What We Have So Far A Data Connection that can open the database A Data Adapter that can send SQL to the database and receive data rows. A definition of a Data Set that can temporarily hold data that is read from one table (or view). You can add other adapters and datasets. You can create relationships if there are parent/child tables, but you will see that these are not very useful. Things we do not have yet. There is no data in the data set. The data set is not connected to the form 15 Filling the Data Set In the main DB component (View + Code) add a subroutine: Public Sub FillEmployee(ByVal ds As DataSet) adpEmployee.Fill(ds) End Sub At first glance, the routine seems pointless, but its purpose is to keep the database-specific code in one component. Remember that the adapter is specific to the database. This way, your form can use the dataset only, while the adapter stays in the one component. We will add code to your data form to call this routine. 16 Creating the Data Form Three basic types of forms are commonly used in business applications. Each has slightly different considerations and setup. They will be covered in later lessons. Consider a standard form with one row of data. To start, add a new blank Web Form, named Employee Drop a Data Set onto the blank design. Select the employee data set you created earlier. Then do View + Code to see the code. You need to tell the form to go to the database and fill the data set with data. There are two possible times/places to do this: Page_Load or Page_Init. For now, Page_Load is easier. Near the top of the class, add: Protected cmpMain as New DBMain() Protected tmf As New TMF() In the Sub Page_Load, add: cmpMain.FillEmployee(DsEmployee1) 17 Starting the Data Form Page Add a style. Open the style sheet and add a class for PageTitle Choose a font family (e.g., Arial) and font size (e.g., 2 em) In Text, set the horizontal alignment to Centered Save and close the style sheet and drop it onto the data form. Add a Title. From the Web Forms toolbox, Drop a Label near the top of the form In the Text property, enter the title: Employee Set the CssClass: PageTitle Change the ID: lblPageTitle (not strictly required, but a useful habit) Link the page. Open the main form (rename it, e.g., AAStart) Add the style sheet and a title. Add a HyperLink that opens the data form. Set: Text (Employee Form), ID (lnkEmployee), NavigateURL (Employee.aspx) Save everything and build and browse to test it. If the DB form crashes, check the security settings first. 18 Adding Data Controls Design view on the data form. Drag and drop labels and text boxes onto the form. Keep a list handy for the data columns. For labels, set properties ID: Text: lblColumnlblEmployeeID Column Employee ID For text boxes, set properties ID: txtColumn Tab Index txtEmployeeID Number from top to bottom (0 … 5) Add a Button, Call it Save (ID=btnSave) (Do not write code for it yet). Use Format Menu to vertically space and align everything. Data binding the controls For each text box, click the Data Binding property Choose simple binding, and click your way down the list until you can select the matching column. (See next slide) 19 Simple Data Binding Make sure Text property is selected on the left. Click your way down the list on the right until you can select the matching data column. You can set a format, for example for dates. 20 Activate Data Binding The control properties simply set a mapping from the dataset table to the control. You still need to activate the binding and transfer the data. Each control has a DataBind method that performs this one-way transfer. But, it is easier to use the Page.DataBind method because it does the transfer for all of the controls. The problem: Calling DataBind replaces the content of target, destroying whatever was there before. Forms use a PostBack approach. When users click a button, the data is sent back to the same page. You must avoid destroying the data changed by the user. In Page_Load, add: If Not Page.IsPostBack Then Test it Page.DataBind() End If 21 Saving Data: The Simple Approach When you created the data adapter, by default, it created SQL statements to Update, Insert, and Delete. If the data in the in-memory data set changes, ADO .Net can automatically apply the changes (including insert and delete) back to the stored database. First, in the Main database component, add: Public Sub UpdateEmployee(ByVal ds As DataSet) adpEmployee.Update(ds) End Sub In the data form, write code for the Save button to transfer the form data to the data set. Double-click the Save button to start. Add: DsEmployee1.Tables("Employee").Rows(0).BeginEdit() DsEmployee1.Tables("Employee").Rows(0)("EmployeeID") = txtEmployeeID.Text DsEmployee1.Tables("Employee").Rows(0)("LastName") = txtLastName.Text … (do all of the columns) DsEmployee1.Tables("Employee").Rows(0).EndEdit() cmpMain.UpdateEmployee(DsEmployee1) 22 Problems with the Simple Approach We have ignored concurrency issues. It would actually be better if you build the dataset without testing for concurrency. Right now, if someone else changes the data, the update will crash, and cannot be recovered unless you back out of the form and reload the data from the database. However, notice that every time the form is loaded, including during post back, the page goes to the database and gets the current version. So, this version will actually overwrite most changes made by other people with no warning. The only concurrency issue that will be caught and crashed is on the server between re-reading the page and writing the new data; which is a relatively minor chance. It will be incredibly boring to duplicate these steps for every form you will have. It will also be painful to return later and make changes. Note that the dataset is not saved across post back. Which is probably good, since the system is designed to read the entire table. Which is crazy, since the form only uses one row at a time. 23 Customer Form: A Better Method It is pointless and painful to have to write the same complex update code every time you need a form. So, I created some generic routines to help. This example relies heavily on them. Make sure you have the TMF file in your project. Copy the generic code from the DBMain in the sample project. (especially BatchUpdate and MergeConcurrency) Getting started Create the Data Adapter in DBMain as usual, be sure to take the default to generate optimistic concurrency. Generate the Data Set as usual. Keep a copy of the column names. Add two routines to the DBMain code: Public Sub FillCustomer(ByVal ds As DataSet, ByVal sID As String) Dim tmf As TMF = New TMF() adpCustomer.SelectCommand.CommandText = tmf.AddWhereToSelect(adpCustomer.SelectCommand.CommandText, "CustomerID=" & sID) adpCustomer.Fill(ds) End Sub Public Function UpdateCustomer(ByVal ds As DataSet, ByVal sID As String) As String Return BatchUpdate(ds, adpCustomer, "Customer", AddressOf Me.FillCustomer, sID) 24 End Function Customer Form: Construction Create a new Web Form for Customer. Add the style sheet and Page Title Add the data set Add the labels and text boxes, set the tab index Use DataBinding to link the text boxes to the dataset. Add the Save button (do not write code yet) Add a blank Label at the top, across the page, ID: lblMessage View + Code, below the control definitions (Protected WithEvents ) Protected cmpMain As DBMain = New DBMain() Protected tmf As tmf = New tmf() 25 Customer Form: aryColumns The generic code needs a method to map the column names to the controls. The easiest method is to create a special array above the Page_Load routine. For each column, you enter Column Name Control ID Type of control (Text, Label, DDL, Check) Whether or not the column is part of the primary key Private aryColumns() As tmf.SingleEditItem = { _ New tmf.SingleEditItem("CustomerID", "txtCustomerID", "Text", True), _ New tmf.SingleEditItem("FirstName", "txtFirstName", "Text", False), _ New tmf.SingleEditItem("LastName", "txtLastName", "Text", False), _ New tmf.SingleEditItem("Address", "txtAddress", "Text", False), _ … Do all of the columns … New tmf.SingleEditItem("EMail", "txtEmail", "Text", False), _ New tmf.SingleEditItem("URL", "txtURL", "Text", False) _ } 26 Customer: Page_Load The main change to Page_Load is that it only reads from the database the first time the page is loaded (not during PostBack), and the dataset has to be saved in a Session so we can hang onto the original values. Also notice that the Fill routine has been modified to read only one row of data. If Not Page.IsPostBack Then cmpMain.FillCustomer(DsCustomer1, "1") Session("dsCustomer1") = DsCustomer1 Page.DataBind() Else DsCustomer1 = CType(Session("dsCustomer1"), dsCustomer) End If 27 Customer: btnSave Saving the data is now handled by the generic routines. Add the following code to the btnSave_Click routine: tmf.CopyFormToDataSet(Me, aryColumns, DsCustomer1, "Customer", 0) lblMessage.Text = cmpMain.UpdateCustomer(DsCustomer1) The first routine copies the data from the form to the dataset (using the aryColumns mapping). The second routine does the update, and checks for concurrency. What happens if there is a concurrency collision? Try it. Where are the underlying values stored? They are kept as properties within the dataset table. The routine re-reads the database, and replaces the underlying values with the new current values. However, it leaves this user’s changes on the form. So clicking Save a second time will force update the database if there was a concurrency error. You really need to add some message and probably more choices to the user. But that is a user interface decision. 28 Customer: State DropDownList To provide a drop down list (selection box) for states, you first need to create a table of StateCodes, with the state and the name. Users will see the names (alphabetized), and the two-letter state code will be inserted into the database. Second, create a new data adapter and dataset. Retreive StateCode and Name, sorted. Deselect the build update. Create the FillStateCodes routine. Customer form, delete the existing state text box Add the StateCodes data set, then add a DDL and set its tab index ID ddlState DataSource DsStateCodes1 DataMember StateCodes DataTextField: Name DataValueField State In the aryColumns array, change the control type to “DDL” and ID to ddlState In Page_Load, fill the new dataset cmpMain.FillStateCodes(DsStateCodes1) 29 DDL: Tricky TMF Microsoft did not bother to create a simple mechanism to get drop down lists to display the currently assigned value. For instance, if the database state value is CT, you want the box to start with a display of Connecticut. So, turn to the trusty TMF class for help. In properties for the DDL, click the Data Bindings button. Make sure you choose the SelectedIndex on the left side! Choose custom binding, and add: (It’s that easy, see next slide) tmf.SelectDefaultDDL(sender, DsStateCodes1, DataBinder.Eval(DsCustomer1, "Tables[Customer].DefaultView.[0].State")) Dataset name that holds the DDL list Dataset name for the overall form Column name from the overall form that holds the default value Table name for the overall form 30 DDL Binding for SelectedIndex 31 Customer Form 32 The Form is Still not Usable The generic code can be used for any form. The aryColumns array is the key to providing the information the subroutines need. You really need to think about a user interface to explain how to handle concurrency collisions. The grid code (later) shows one way to handle the problem. But feel free to experiment, there are no great solutions yet. Several steps remain to make this a useful form. Selecting a customer/row (tricky, depends on user interface goals) Finding a customer (moderately tricky code) Adding a new customer (relatively easy) Deleting a customer (easy, until you have to show the next customer) Security (relatively easy, but requires more forms) More? 33 Concurrency How does the update command handle optimistic concurrency? The SQL command uses the original value for each column in its Where clause. But where does it get (and store) the original value, which is the value that existed when the data row was retrieved? Answer: the data row holds multiple versions of each data column. You can access them with DataRowVersion enumeration. If there is a concurrency problem, the BatchUpdate routine retrieves the current value and places it into the “Original” value. It leaves the current user’s value as the primary/current value. You can compare these values in each column to see where the problem was and show both values. ‘ Set ID= the primary key value of the row being edited Dim dsRow As DataRow = DsMine1.Tables(“MyTable”).Rows.Find(ID) ‘ Check each column/loop If (dsRow(j) <> dsRow(j, DataRowVersion.Original)) Then ‘ Display concurrency error/value End If 34 Customer: Add Code Public Function AddCustomer(ByVal ds As DataSet) As String Dim cID As Integer = GenKey("Customer") Dim strMsg As String If (cID < 0) Then strMsg = "Unable to generate a new Customer ID." End If Dim dr As DataRow = ds.Tables("Customer").NewRow() dr("CustomerID") = cID ds.Tables("Customer").Rows.Add(dr) strMsg &= UpdateCustomer(ds, cID) ds.Clear() ' Clear the data set and reload just the one new row ds.AcceptChanges() ' Need to tell it not to delete the original row FillCustomer(ds, cID) Return strMsg End Function GenKey is another generic routine you need to add to your DBMain. If you are running under Oracle, rewrite GenKey to use Sequences. 35 Customer: Delete Code The Delete code is much trickier, because we need to guess which CustomerID to display next, and bad things happen to the form if we delete all of the rows in the dataset. Public Function DeleteCustomer(ByVal ds As DataSet) As String Dim sID As String = ds.Tables("Customer").Rows(0)("CustomerID") Dim strMsg As String ' The hard part: What customer do I load next? ' Try finding just the first customer sorted by ID Dim rdr As OleDb.OleDbDataReader Dim cmd As OleDb.OleDbCommand = New OleDb.OleDbCommand() cmd.CommandText = "SELECT CustomerID FROM Customer ORDER BY CustomerID" cmd.Connection = Me.cnnOleSimple cnnOleSimple.Open() Dim cID As Integer 36 Customer: Delete Code 2 Try rdr = cmd.ExecuteReader(CommandBehavior.Default) rdr.Read() cID = CType(rdr("CustomerID"), Integer) If (cID = CType(sID, Integer)) Then ' If delete 1st one, try to get the next row rdr.Read() cID = rdr("CustomerID") End If rdr.Close() Catch e2 As Exception cID = -1 strMsg = "No customers remain. Please add at least one." End Try cnnOleSimple.Close() 37 Customer: Delete Code 3 Try ds.Tables("Customer").Rows(0).Delete() If (cID <> -1) Then UpdateCustomer(ds, cID.ToString) Else UpdateCustomer(ds, Nothing) End If strMsg &= "Deleted customer " & sID Catch e As Exception strMsg = "Error deleting customer. " & e.Message End Try If (cID = -1) Then ' Dataset can never be empty or form will crash Dim dr As DataRow = ds.Tables("Customer").NewRow dr("CustomerID") = -1 ds.Tables("Customer").Rows.Add(dr) End If Return strMsg End Function 38 Summary The form is getting closer. It still needs to be within an application, that can handle things like selecting a customer and security. But, building several forms of this type for an application is still going to be painful, with lots of repetition. Ultimately, it would be nicer if a data wizard existed to build most of the generic code automatically. Lacking the time to write such a wizard, the next best step is to create a set of generic templates, that can be copied and used to quickly build forms that handle standard problems. 39