Вы находитесь на странице: 1из 51

ADO.

NET Sample Application


The following is a simple ADO.NET application that returns results from a data source and writes the output to the console or command prompt. The samples in this topic show how to connect to and retrieve data using the .NET Framework Data Provider for SQL Server (System.Data.SqlClient), the .NET Framework Data Provider for OLE DB (System.Data.OleDb), the .NET Framework Data Provider for ODBC (System.Data.Odbc), and the .NET Framework Data Provider for Oracle (System.Data.OracleClient). All data providers can be used in a single application, if desired. The SqlClient example below assumes that you can connect to the Northwind sample database on Microsoft SQL Server 7.0 or later, and returns a list of records from the Categories table using a SqlDataReader. The OleDb and Odbc examples assume a connection to the Microsoft Access Northwind sample database. The OracleClient example assumes a connection to DEMO.CUSTOMER on an Oracle server. You must also add a reference to the System.Data.OracleClient.dll. For more information about the DataReader, see Retrieving Data Using a DataReader).

SqlClient
C# VB using System; using System.Data; using System.Data.SqlClient; class Program { static void Main() { string connectionString = GetConnectionString(); string queryString = "SELECT CategoryID, CategoryName FROM dbo.Categories;"; using (SqlConnection connection = new SqlConnection(connectionString)) { SqlCommand command = connection.CreateCommand(); command.CommandText = queryString; try { connection.Open(); SqlDataReader reader = command.ExecuteReader(); while (reader.Read()) { Console.WriteLine("\t{0}\t{1}", reader[0], reader[1]);

} reader.Close(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } static private string GetConnectionString() { // To avoid storing the connection string in your code, // you can retrieve it from a configuration file. return "Data Source=(local);Initial Catalog=Northwind;" + "Integrated Security=SSPI"; } }

OleDb
C# VB using System; using System.Data; using System.Data.OleDb; class Program { static void Main() { string connectionString = GetConnectionString(); string queryString = "SELECT CategoryID, CategoryName FROM Categories;"; using (OleDbConnection connection = new OleDbConnection(connectionString)) { OleDbCommand command = connection.CreateCommand(); command.CommandText = queryString; try { connection.Open(); OleDbDataReader reader = command.ExecuteReader(); while (reader.Read()) { Console.WriteLine("\t{0}\t{1}",

reader[0], reader[1]); } reader.Close(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } static private string GetConnectionString() { // To avoid storing the connection string in your code, // you can retrieve it from a configuration file. // Assumes Northwind.mdb is located in the c:\Data folder. return "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + "c:\\Data\\Northwind.mdb;User Id=admin;Password=;"; } }

Odbc
C# VB using System; using System.Data; using System.Data.Odbc; class Program { static void Main() { string connectionString = GetConnectionString(); string queryString = "SELECT CategoryID, CategoryName FROM Categories;"; using (OdbcConnection connection = new OdbcConnection(connectionString)) { OdbcCommand command = connection.CreateCommand(); command.CommandText = queryString; try { connection.Open(); OdbcDataReader reader = command.ExecuteReader(); while (reader.Read())

{ Console.WriteLine("\t{0}\t{1}", reader[0], reader[1]); } reader.Close(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } static private string GetConnectionString() { // To avoid storing the connection string in your code, // you can retrieve it from a configuration file. // Assumes Northwind.mdb is located in the c:\Data folder. return "Driver={Microsoft Access Driver (*.mdb)};" + "Dbq=c:\\Data\\Northwind.mdb;Uid=Admin;Pwd=;"; } }

OracleClient
C# VB using System; using System.Data; using System.Data.OracleClient; class Program { static void Main() { string connectionString = GetConnectionString(); string queryString = "SELECT CUSTOMER_ID, NAME FROM DEMO.CUSTOMER"; using (OracleConnection connection = new OracleConnection(connectionString)) { OracleCommand command = connection.CreateCommand(); command.CommandText = queryString; try { connection.Open(); OracleDataReader reader = command.ExecuteReader();

while (reader.Read()) { Console.WriteLine("\t{0}\t{1}", reader[0], reader[1]); } reader.Close(); } catch (Exception ex) { Console.WriteLine(ex.Message); } } } static private string GetConnectionString() { // To avoid storing the connection string in your code, // you can retrieve it from a configuration file. // Assumes Northwind.mdb is located in the c:\Data folder. return "Data Source=ThisOracleServer;Integrated Security=yes;"; } }

Sometimes, in addition to querying and updating data in a database, you also need to retrieve information about the database itself and its contents. This information is called Database Metadata. The OleDbConnection Class allows you to retrieve this kind of information. It's GetOleDbSchemaTable() method can be used to retrieve any information about the database and its metadata.

1. Develop the DatabaseInfo.cs Application


After a database connection is opened in the DatabaseInfo's constructor, metadata information is obtained from the database that lists the table metadata and the column metadata information. 1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: /////////////////////////////////////////////////////////// /// The following example shows querying database Metadata /// Information developed using C# and the .NET Framework. /// /// author: Gopalan Suresh Raj /// Copyright (c), 2002. All Rights Reserved. /// URL: http://gsraj.tripod.com/ /// email: gopalan@gmx.net /// /////////////////////////////////////////////////////////// using System; using System.Data; using System.Data.OleDb; /// <summary> /// Summary description for DatabaseInfo. /// </summary> class DatabaseInfo { /// <summary> /// The Connection String /// </summary> static readonly string CONNECTION_STRING = "Provider=Microsoft.Jet.OLEDB.4.0;"+ "Data Source=C:/Program Files/Microsoft Office/Office10/Samples/Northwind.mdb;" ; /// <summary> /// The Connection Object /// </summary> OleDbConnection connection_; /// <summary> /// Default No-Argument constructor that /// Creates a connection to the database /// </summary> DatabaseInfo() { this.connection_ = new OleDbConnection( DatabaseInfo.CONNECTION_STRING ); this.connection_.Open(); } /// <summary> /// Closes the Connection to the Database

42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96:

/// </summary> void Dispose() { this.connection_.Close(); } /// <summary> /// Retrieves Database Metadata information about Tables /// of the specific database exposed to this user /// </summary> public void RetrieveTableInformation() { DataTable tables = this.connection_.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null); Console.WriteLine("\nListing Table Metadata Information ..."); foreach( DataColumn column in tables.Columns ) { Console.WriteLine(column); } Console.WriteLine("\nListing Tables ..."); foreach( DataRow row in tables.Rows ) { Console.WriteLine(row["TABLE_NAME"]); } } /// <summary> /// Retrieves Database Metadata information about Columns /// of the specific database exposed to this user /// </summary> public void RetrieveColumnInformation() { DataTable tables = this.connection_.GetOleDbSchemaTable(OleDbSchemaGuid.Columns, null); // Print out the columns Console.WriteLine("\nListing Column Metadata Information ..."); foreach( DataColumn column in tables.Columns ) { Console.WriteLine(column); } Console.WriteLine("\nListing Columns (TableName : ColumnName format)..." ); foreach( DataRow row in tables.Rows ) { Console.WriteLine(row["TABLE_NAME"]+" : "+ row["COLUMN_NAME"]); } } /// <summary> /// Default Entry Point that tests the system /// </summary> /// <param name="args"></param> static void Main(string[] args) { try { DatabaseInfo info = new DatabaseInfo(); info.RetrieveTableInformation(); info.RetrieveColumnInformation(); info.Dispose(); } catch( OleDbException exception ) { foreach(OleDbError error in exception.Errors) { Console.WriteLine("Error :"+error); } }

97: } 98: }

2. Build and Run the Application


Build the files that make up the App and run it . Command Prompt
C:\MyProjects\Cornucopia\DatabaseMetaData\bin\Debug>DatabaseMetaData Listing Table Metadata Information ... TABLE_CATALOG TABLE_SCHEMA TABLE_NAME TABLE_TYPE TABLE_GUID DESCRIPTION TABLE_PROPID DATE_CREATED DATE_MODIFIED Listing Tables ... Alphabetical List of Products Categories Category Sales for 1997 Current Product List Customers Employees Invoices MSysAccessObjects MSysACEs MSysCmdbars MSysIMEXColumns MSysIMEXSpecs MSysObjects MSysQueries MSysRelationships Order Details Order Details Extended Order Subtotals Orders Orders Qry Product Sales for 1997 Products Products Above Average Price Products by Category Quarterly Orders Sales by Category Shippers Suppliers Ten Most Expensive Products Listing Column Metadata Information ... TABLE_CATALOG TABLE_SCHEMA TABLE_NAME COLUMN_NAME COLUMN_GUID COLUMN_PROPID ORDINAL_POSITION COLUMN_HASDEFAULT COLUMN_DEFAULT COLUMN_FLAGS IS_NULLABLE DATA_TYPE TYPE_GUID CHARACTER_MAXIMUM_LENGTH CHARACTER_OCTET_LENGTH NUMERIC_PRECISION NUMERIC_SCALE DATETIME_PRECISION

CHARACTER_SET_CATALOG CHARACTER_SET_SCHEMA CHARACTER_SET_NAME COLLATION_CATALOG COLLATION_SCHEMA COLLATION_NAME DOMAIN_CATALOG DOMAIN_SCHEMA DOMAIN_NAME DESCRIPTION Listing Columns (TableName : ColumnName format)... Alphabetical List of Products : CategoryID Alphabetical List of Products : CategoryName Alphabetical List of Products : Discontinued Alphabetical List of Products : ProductID Alphabetical List of Products : ProductName Alphabetical List of Products : QuantityPerUnit Alphabetical List of Products : ReorderLevel Alphabetical List of Products : SupplierID Alphabetical List of Products : UnitPrice Alphabetical List of Products : UnitsInStock Alphabetical List of Products : UnitsOnOrder Categories : CategoryID Categories : CategoryName Categories : Description Categories : Picture Category Sales for 1997 : CategoryName Category Sales for 1997 : CategorySales Current Product List : ProductID Current Product List : ProductName Customers : Address Customers : City Customers : CompanyName Customers : ContactName Customers : ContactTitle Customers : Country Customers : CustomerID Customers : Fax Customers : Phone Customers : PostalCode Customers : Region Employees : Address Employees : BirthDate Employees : City Employees : Country Employees : EmployeeID Employees : Extension Employees : FirstName Employees : HireDate Employees : HomePhone Employees : LastName Employees : Notes Employees : Photo Employees : PostalCode Employees : Region Employees : ReportsTo Employees : Title Employees : TitleOfCourtesy Invoices : Address Invoices : City Invoices : Country Invoices : CustomerID Invoices : Customers.CompanyName Invoices : Discount Invoices : ExtendedPrice Invoices : Freight Invoices : OrderDate Invoices : OrderID Invoices : PostalCode Invoices : ProductID Invoices : ProductName Invoices : Quantity Invoices : Region Invoices : RequiredDate Invoices : Salesperson Invoices : ShipAddress Invoices : ShipCity Invoices : ShipCountry Invoices : ShipName Invoices : ShippedDate Invoices : Shippers.CompanyName Invoices : ShipPostalCode Invoices : ShipRegion Invoices : UnitPrice

MSysAccessObjects : Data MSysAccessObjects : ID MSysCmdbars : Grptbcd MSysCmdbars : TbName MSysIMEXColumns : Attributes MSysIMEXColumns : DataType MSysIMEXColumns : FieldName MSysIMEXColumns : IndexType MSysIMEXColumns : SkipColumn MSysIMEXColumns : SpecID MSysIMEXColumns : Start MSysIMEXColumns : Width MSysIMEXSpecs : DateDelim MSysIMEXSpecs : DateFourDigitYear MSysIMEXSpecs : DateLeadingZeros MSysIMEXSpecs : DateOrder MSysIMEXSpecs : DecimalPoint MSysIMEXSpecs : FieldSeparator MSysIMEXSpecs : FileType MSysIMEXSpecs : SpecID MSysIMEXSpecs : SpecName MSysIMEXSpecs : SpecType MSysIMEXSpecs : StartRow MSysIMEXSpecs : TextDelim MSysIMEXSpecs : TimeDelim MSysRelationships : ccolumn MSysRelationships : grbit MSysRelationships : icolumn MSysRelationships : szColumn MSysRelationships : szObject MSysRelationships : szReferencedColumn MSysRelationships : szReferencedObject MSysRelationships : szRelationship Order Details : Discount Order Details : OrderID Order Details : ProductID Order Details : Quantity Order Details : UnitPrice Order Details Extended : Discount Order Details Extended : ExtendedPrice Order Details Extended : OrderID Order Details Extended : ProductID Order Details Extended : ProductName Order Details Extended : Quantity Order Details Extended : UnitPrice Order Subtotals : OrderID Order Subtotals : Subtotal Orders : CustomerID Orders : EmployeeID Orders : Freight Orders : OrderDate Orders : OrderID Orders : RequiredDate Orders : ShipAddress Orders : ShipCity Orders : ShipCountry Orders : ShipName Orders : ShippedDate Orders : ShipPostalCode Orders : ShipRegion Orders : ShipVia Orders Qry : Address Orders Qry : City Orders Qry : CompanyName Orders Qry : Country Orders Qry : CustomerID Orders Qry : EmployeeID Orders Qry : Freight Orders Qry : OrderDate Orders Qry : OrderID Orders Qry : PostalCode Orders Qry : Region Orders Qry : RequiredDate Orders Qry : ShipAddress Orders Qry : ShipCity Orders Qry : ShipCountry Orders Qry : ShipName Orders Qry : ShippedDate Orders Qry : ShipPostalCode Orders Qry : ShipRegion Orders Qry : ShipVia Product Sales for 1997 : CategoryName Product Sales for 1997 : ProductName Product Sales for 1997 : ProductSales Product Sales for 1997 : ShippedQuarter

Products : CategoryID Products : Discontinued Products : ProductID Products : ProductName Products : QuantityPerUnit Products : ReorderLevel Products : SupplierID Products : UnitPrice Products : UnitsInStock Products : UnitsOnOrder Products Above Average Price : ProductName Products Above Average Price : UnitPrice Products by Category : CategoryName Products by Category : Discontinued Products by Category : ProductName Products by Category : QuantityPerUnit Products by Category : UnitsInStock Quarterly Orders : City Quarterly Orders : CompanyName Quarterly Orders : Country Quarterly Orders : CustomerID Sales by Category : CategoryID Sales by Category : CategoryName Sales by Category : ProductName Sales by Category : ProductSales Shippers : CompanyName Shippers : Phone Shippers : ShipperID Suppliers : Address Suppliers : City Suppliers : CompanyName Suppliers : ContactName Suppliers : ContactTitle Suppliers : Country Suppliers : Fax Suppliers : HomePage Suppliers : Phone Suppliers : PostalCode Suppliers : Region Suppliers : SupplierID Ten Most Expensive Products : TenMostExpensiveProducts Ten Most Expensive Products : UnitPrice C:\MyProjects\Cornucopia\DatabaseMetaData\bin\Debug>

1. Develop the KeeperClient.cs Application


Create a regular C# client application. Our objective as a Client to a Distributed Component Server is to activate the remote object. We need to use the types in the System.Runtime.Remoting andSystem.Runtime.Remoting.Channels.Tcp namespaces, as we need to use the TCP Channel. So we use these as shown in Lines 21 and 22. The Client does not need to derive from anything as it's not a server-side entity that needs to have a distributed identity. As we're developing a client application, we don't need to specify a client port when we instantiate a TcpChannel on Line 51. The most important part of this application is object activation that is shown on Line 53. To invoke remote methods, you have to first activate the remote object and obtain a reference to the associated proxy object on the client side. As shown on Line 53, to activate the object and retrieve a reference to the associated proxy object, you can call the GetObject() method of the Activator class. When doing so, you have to also pass along the remote class name, and its fully qualified location, including the complete URI. Once this is done, you can invoke methods on this object as shown on Lines 73 and 84.

Client.cs
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: ////////////////////////////////////////////////////// /// The following example illustrates a Client to a /// Distributed component developed using C# and .NET. /// /// author: Gopalan Suresh Raj /// Copyright (c), 2001-2002. All Rights Reserved. /// URL: http://gsraj.tripod.com/ /// email: gopalan@gmx.net /// /// <compile> /// csc /r:BookKeeper.exe /t:exe /out:KeeperClient.exe KeeperClient.cs /// </compile> /// <run> /// KeeperClient create Checking "Athul Raj" 10000 /// KeeperClient delete 1 /// </run> ///////////////////////////////////////////////////////// using System; // Include Remoting API functionality using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace BookKeeper { /// <summary> /// The KeeperClient class. /// </summary> public class KeeperClient { /// <summary> /// Static Entry Point to the Application

32: /// </summary> 33: /// <param name="args"></param> 34: /// <returns></returns> 35: public static int Main (String[] args) { 36: int argLength = args.Length; 37: if ( argLength < 2) { 38: Console.WriteLine ("Usage: KeeperClient <create|delete> 39: <accountNumber|Checking|Savings> <customerName> <startingBalance>" ); 40: return -1; 41: } 42: 43: string operation = "", type = ""; 44: string []customerNames = new String [1]; 45: int accountNumber = 0; 46: float startingBalance = 0; 47: 48: AccountKey key = new AccountKey (); 49: 50: try { 51: // Get a reference to the remote Distributed Component 52: TcpChannel channel = new TcpChannel (); 53: ChannelServices.RegisterChannel (channel); 54: BookKeeper keeper = (BookKeeper) Activator.GetObject (typeof( BookKeeper ), 55: 56: "tcp://127.0.0.1:1099/BookKeeper"); 57: Console.WriteLine ("Obtained a reference to the Server Object..."); 58: 59: operation = args [0]; 60: 61: if (argLength > 2) { 62: type = args [1]; 63: // This can be a create operation 64: if (operation.Trim().ToLower() == "create") { 65: if (type.Trim().ToLower() == "checking") { 66: key.Type = AccountType.CheckingAccount; 67: } 68: if (type.Trim().ToLower() == "savings") { 69: key.Type = AccountType.SavingsAccount; 70: } 71: customerNames[0] = args[2]; 72: startingBalance = (float)System.Double.Parse (args[3]); 73: Console.WriteLine ("Invoking createAccount() now ..."); 74: // Invoke operations on the Distributed Component 75: key = keeper.createAccount (key.Type, customerNames, startingBalance); 76: Console.WriteLine ("Key of new Row is: {0}", key.Key); 77: } 78: } 79: else { 80: // This can be a delete operation 81: if (operation.Trim().ToLower() == "delete") { 82: accountNumber = System.Int32.Parse (args[1]); 83: key.Key = accountNumber; 84: Console.WriteLine ("Invoking deleteAccount() now ..."); 85: // Invoke operations on the Distributed Component

86: 87: 88: 89: 90: 91: 92: 93: 94: 95: 96: } }

bool result = keeper.deleteAccount (key); } } // Dispose of the object keeper = null; } catch (Exception exception) { Console.WriteLine (exception.ToString()); } return 0; }

2. Build the Application


Build the files that make up the Client. Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>csc /r:BookKeeper.exe /t:exe /out:KeeperClient.exe KeeperClient.cs Microsoft (R) Visual C# Compiler Version 7.00.9254 [CLR version v1.0.2914] Copyright (C) Microsoft Corp 2000-2001. All rights reserved. C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>

Ignore the warning about the assembly being a satellite assembly.

3. Run the Client


Make sure the Distributed Component Server is up and running before firing-up the client.
Note: There is an inherent and 'intentional bug' in the way deletes are handled in the BookKeeper Server. You can only delete records in the order that they were created - i.e., you'd have to delete the latest record first !!! If you do try to delete in any other order, you will not be able to create the next new Account !!! This is because of the way new Primary Keys are generated. For explanation, please look at the BookKeeper::getNextKey() method. I intentionally introduced this bug in the program to demonstrate that even though ADO.NET's DataSet is physically disconnected from the DataBase, it maintains and manages its data internally, and still checks

Constraints and behaves very much like a regular database.

Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>KeeperClient create Checking "Athul Raj" 100000

Obtained a reference to the Server Object... Invoking createAccount() now ... Key of new Row is: 6 C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>KeeperClient delete 6 Obtained a reference to the Server Object... Invoking deleteAccount() now ... C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>

ADO.NET's DataSet
The DataSet is actually an in-memory view of the database. It can contain multiple DataTable and DataRelation objects. This allows developers to navigate and manipulate the collection of tables and their relationships. ADO.NET involves disconnected DataSets as it is modeled for a distributed architecture. As the DataSet itself is disconnected from the data source, it must provide a way to track changes to itself. For this purpose, the DataSet class provides a number of methods which can be used to reconcile changes to itself with the actual database (or other data source) at a later point in time. Some of these methods include HasChanges(), HasErrors(), GetChanges(), AcceptChanges(), and RejectChanges(). These methods can be used to check for changes that have happened to the DataSet, obtain modifications in the form of a changed DataSet, inspect the changes for errors, and then either accept or reject those changes. If the changes need to be communicated back to the data store back-end, the Data Adapter can be asked to be updated using the Data Adapter's Update() method. The DataSet is intended to help web applications which are by their very nature disconnected. You never know that the data in the database has changed until you have updated the records that you were editing or perform other tasks which needs reconciliation with the back end Data Store.

The BookKeeper Module


In this article, we are going to build a Distributed Component to illustrate ADO.NET's disconnected operation facility - the DataSet. As mentioned earlier, the DataSet facilitates the client to manipulate and update a local copy of any number of related tables while still disconnected from the data source and submit the modified data back for processing using a related data adapter at a later point in time. As a result, all our data is going to be maintained on the server in an XML datastore (or flat-file) until changes are committed to a database back-end at a later point in time !!! Our hypothetical BookKeeper Module (that we will build in this article), is actually a Distributed component that performs just a bunch of functions. It is the module that manages creation and deletion of accounts (Checking accounts or Savings accounts) for a Large Commercial Bank Project. It offers no other services except "Create Account", and "Delete Account", and "Find Account".

Building Distributed Components using the .NET Framework


While traditional Distributed COM (DCOM - the wire protocol that provides support for distributed computing in COM) is fine for distributed computing, it is not appropriate for the internet because it does'nt work well in the face of firewalls and NAT software. The other shortcomings of DCOM are expensive lifecycle management, protocol negotiation, and binary formats. To ease these shortcomings, .NET provides the Remoting API that allows you to use a host of channels - such as TCP and HTTP (which uses SOAP) - for distributed computing. It also permits you to plug in your own custom channels, if you require this functionality.

To ease development, I recommend using the Visual Studio.NET IDE. However, you are free to develop your application in the favorite editor of your choice, using the command-line to execute the various commands to build and deploy it. The various steps that are involved in creating a Distributed Component using C# and the .NET Framework are as follows (I'm going to assume you're using the VS.NET IDE):

1. 2. 3. 4. 5. 6. 7.

Create a Visual C# - Console Application project Generate a Key-Value pair to use when deploying your Shared Assembly Configure your Project Property Pages with the right information Develop the BookKeeper.cs library Modify the generated AssemblyInfo.cs to add the right assembly information Build the Project Files Deploy the application as a Shared Assembly

1. Create a Visual C# - Console Application project


Create a new Visual C# Console Application project.

2. Generate a Key-Value pair to use when deploying your Shared Assembly


Shared Assemblies are those that can be used by any client application, such as a system DLL that every process in the system can use. Unlike private-assemblies, shared assemblies must be published or registered in the system's Global Assembly Cache (GAC). As soon as

they are registered in the GAC, they act as system components. An essential requirement for GAC registration is that the component must possess originator and version information. In addition to other metadata information, these two items allow multiple versions of the same component to be registered and executed on the same machine. Unlike Classic COM, we don't have to store any information in the system registry for clients to use these shared assemblies. There are three general steps to registering shared assemblies in the GAC:

1. 2. 3.

The Shared Name (sb.exe) utility should be used to obtain the public/private key pair. This utility generates a random key pair value, and stores it in an output file for example,BookKeeper.key. Build the assembly with an assembly version number and the key information in the BookKeeper.key Using the .NET Global Assembly Cache (gacutil.exe) utility, register the assembly in the GAC.

The assembly now becomes a shared assembly and can be used by any client in the system. Therefore, as a first step, use the Shared Name Utility to obtain a public/private key pair and store it in a file (BookKeeper.key, in this case) as shown below. Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper>sn -k BookKeeper.key Microsoft (R) .NET Framework Strong Name Utility Version 1.0.2914.16 Copyright (C) Microsoft Corp. 1998-2001. All rights reserved. Key pair written to BookKeeper.key C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper>

The -k option generates the random key pair and saves the key information in the BookKeeper.key file. We use this file as input when we build our Shared Assemblies.

3. Configure your Project Property Pages with the right information


Configure the Project Properties with the right information. Make sure you specify the Assembly Name that you want for the Assembly. Specifically, move to the General tab, and in the Wrapper Assembly Key File area, enter the key file to use. In this case, it is BookKeeper.key.

4. Develop the BookKeeper.cs library


The DataSet's properties expose the Tables and Relations that it contains. After creating a new DataSet object on Line 54, we add a Table and Columns to it. The createSchema() method from Lines 143-163 demonstrates how to create a Table, and it's columns dynamically. Line 147 shows how to create a Table using the Add() method of the Table object. We go through a similar process from Lines 149 to 156 to create Columns in a similar fashion by invoking the Add() method on each Table's Column collections. Each of these Tables or Columns can later be referred to by name. In order to assign the primary key for the Accounts table, we have to create the DataColumn array to hold one or more fields representing a key or a composite key as shown from Lines 159 to 161. In this case we only have a single key field, AccountKey. We set the PrimaryKey property of the table to this array of key columns. The insertData() method from Lines 172 to 191 shows how to insert data into the Table. Tables and Relations are important properties of the DataSet. Not only do they describe the structure of the in-memory database, but the DataTables inside the collection also hold the content of the DataSet.
Note: There is an inherent and 'intentional bug' in the way deletes are handled in the BookKeeper Server. You can only

delete records in the order that they were created - i.e., you'd have to delete the latest record first !!! If you do try to delete in any other order, you will not be able to create the next new Account !!! This is because of the way new Primary Keys are generated. For explanation, please look at the BookKeeper::getNextKey() method. I intentionally introduced this bug in the program to demonstrate that even though ADO.NET's DataSet is physically disconnected from the DataBase, it maintains and manages its data internally, and still checks

Constraints and behaves very much like a regular database.

XML and the DataSet


The DataSet tightly integrates with XML and is therefore universally interoperable. The three methods that facilitate this are WriteXml(), WriteXmlSchema(), and ReadXml(). WriteXmlSchema()only dumps the schema of the tables, including all tables and the relationship between the tables. WriteXml() can dump both the schema and the table data as an XML encoded string as shown on Lines 283 and 285. Both WriteXmlSchema() and WriteXml()accept a Stream, TextWriter, XmlWriter, or a String representing a filename. WriteXml() accepts an XmlWriteMode as the second argument. When the XmlWriteMode.WriteSchema is passed in as the second argument to WriteXml() method, the method dumps both the Schema and the Data as shown on Lines 283 and 285. You can retrieve only the data portion of the XML by using the XmlWriteMode.IgnoreSchema property. The DataSet also provides methods to reconstruct itself from an XML Document. Use ReadXmlData() for reading XML data documents, and ReadXmlSchema() for reading XML Schema documents. As shown on Line 58, use ReadXml() to read both the schema and the data from an XML file and reconstruct and populate the DataSet object. The DataSet is therefore the most important construct in ADO.NET. Since the DataSet does not tie to an underlying representation such as Microsoft Access or SQL Server, it is extremely portable. Its data format is self-described in its schema, and its data is in pure XML. A DataSet is self-contained regardless of how it was created, perhaps be reading data from Microsoft Access, from SQL Server, from an external XML file, or even being dynamically generated as we've seen in this example. This portable XML-based entity sets a new standard for data exchange.

BookKeeper.cs
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: ////////////////////////////////////////////////////// /// The following example shows a Distributed Component /// developed using C# and the .NET Framework. /// /// author: Gopalan Suresh Raj /// Copyright (c), 2001-2002. All Rights Reserved. /// URL: http://gsraj.tripod.com/ /// email: gopalan@gmx.net /// ////////////////////////////////////////////////////// using System; // Include the following to use the Remoting API

13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66:

using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; // Include the following for the Window Message Box using System.Windows.Forms; // Include the following for ADO.NET functionality using System.Data; // Include the BookKeeper namespace using BookKeeper; namespace BookKeeper { /// <summary> /// The AccountDetails structure /// </summary> /// Make this structure serializable [Serializable] public struct AccountDetails { public AccountKey key; public string customerNames; public float balance; } /// <summary> /// The BookKeeper Class. /// </summary> /// Make this class serializable [Serializable] public class BookKeeper : MarshalByRefObject, IDisposable { int nextKey_; DataSet dataSet_; bool isDisposed_ = false; /// <summary> /// Public No-argument Default Constructor /// </summary> public BookKeeper () { // Construct a new DataSet object dataSet_ = new DataSet ("BookKeeper"); try { // Populate the object with the contents of the XML File which // contains its schema and data from a previous invocation. dataSet_.ReadXml ("Accounts.xml"); } catch (Exception exception) { Console.WriteLine (exception.ToString ()); // If XML File is not found, the schema has not been // created yet. So create a schema. this.createSchema (); MessageBox.Show ("BookKeeper::BookKeeper() created Schema", "BookKeeper"); }

67: nextKey_ = getNextKey (); 68: } 69: 70: /// <summary> 71: /// The Destructor method 72: /// </summary> 73: ~BookKeeper () { 74: if (false == this.isDisposed_) { 75: Dispose (); 76: } 77: } 78: 79: /// <summary> 80: /// The Dispose method 81: /// </summary> 82: public void Dispose () { 83: this.writeDataSet (); 84: MessageBox.Show ("BookKeeper::Dispose() wrote Schema to disk", "BookKeeper"); 85: // No need to finalize if user called Dispose() 86: // so suppress finalization 87: GC.SuppressFinalize (this); 88: this.isDisposed_ = true; 89: } 90: 91: 92: /// <summary> 93: /// The accessor for the DataSet member 94: /// </summary> 95: public DataSet dataSet { 96: get { 97: return this.dataSet_; 98: } 99: } 100: 101: /// <summary> 102: /// The createAccount method 103: /// </summary> 104: /// <param name="accountType">Specifies Checking or Savings account</param> 105: /// <param name="customerNames">The Customer Names, owners of this 106: account</param> 107: /// <param name="startingBalance">The tarting balance</param> 108: /// <returns></returns> 109: public AccountKey createAccount (AccountType accountType, 110: string[] customerNames, 111: float startingBalance) { 112: MessageBox.Show ("BookKeeper::createAccount() invoked", "BookKeeper"); 113: AccountKey key = null; 114: key = new AccountKey (); 115: key.Key = nextKey_++; 116: key.Type = accountType; 117: // Concatenate all customer names 118: string names = ""; 119: foreach (string element in customerNames) { 120: names += (" "+element);

121: } 122: int type = 1; 123: if (key.Type == AccountType.SavingsAccount) { 124: type = 2; 125: } 126: this.insertData (key.Key, type, names.Trim(), startingBalance); 127: MessageBox.Show ("Key is :"+key.Key, "BookKeeper"); 128: 129: return key; 130: } 131: 132: /// <summary> 133: /// The deleteAccount method 134: /// </summary> 135: /// <param name="key">The Account Number to delete</param> 136: public bool deleteAccount (AccountKey key) { 137: MessageBox.Show ("BookKeeper::deleteAccount() with Key :"+key.Key, "BookKeeper"); 138: return this.removeAccount (key.Key); 139: } 140: 141: /// <summary> 142: /// The createSchema method 143: /// </summary> 144: protected void createSchema () { 145: MessageBox.Show ("BookKeeper::createSchema() invoked", "BookKeeper"); 146: lock (this) { 147: // Add a new Table named "Accounts" to the DataSet collection tables 148: dataSet_.Tables.Add ("Accounts"); 149: // Add new columns to the table "Accounts" 150: dataSet_.Tables ["Accounts"].Columns.Add ("AccountKey", 151: Type.GetType ("System.Int32")); 152: dataSet_.Tables ["Accounts"].Columns.Add ("AccountType", 153: Type.GetType ("System.Int32")); 154: dataSet_.Tables ["Accounts"].Columns.Add ("CustomerNames", 155: Type.GetType ("System.String")); 156: dataSet_.Tables ["Accounts"].Columns.Add ("Balance", 157: Type.GetType ("System.Currency")); 158: 159: // Register the column "AccountKey" as the primary key of the table "Accounts" 160: DataColumn[] keys = new DataColumn [1]; 161: keys [0] = dataSet_.Tables ["Accounts"].Columns ["AccountKey"]; 162: dataSet_.Tables ["Accounts"].PrimaryKey = keys; 163: } 164: } 165: 166: /// <summary> 167: /// The insertData method 168: /// </summary> 169: /// <param name="accountKey">The Account Number to create</param> 170: /// <param name="accountType">Specifies Checking or Savings account</param> 171: /// <param name="customerNames">The Customer Names, owners of this 172: account</param> 173: /// <param name="balance">The tarting balance</param> 174: protected void insertData(int accountKey,

175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228:

int accountType, string customerNames, float balance) { MessageBox.Show ("BookKeeper::insertData() invoked", "BookKeeper"); lock (this) { try { DataRow newRow = dataSet_.Tables ["Accounts"].NewRow () ; newRow ["AccountKey"] = accountKey; newRow ["AccountType"] = accountType; newRow ["CustomerNames"] = customerNames; newRow ["Balance"] = balance.ToString(); dataSet_.Tables ["Accounts"].Rows.Add (newRow); } catch (Exception exception) { Console.WriteLine (exception.ToString ()); MessageBox.Show ("BookKeeper::insertData() Failed", "BookKeeper"); } } } /// <summary> /// The findAccount method /// </summary> /// <param name="accountKey">The Account Number</param> /// <returns>The Details of this account</returns> protected AccountDetails findAccount (int accountKey) { MessageBox.Show ("BookKeeper::findAccount() invoked", "BookKeeper"); AccountDetails details = new AccountDetails (); lock (this) { DataTable table = dataSet_.Tables ["Accounts"]; // Find an order from the table DataRow row = table.Rows.Find (accountKey); // Populate the details object AccountKey key = new AccountKey (); key.Key = accountKey; int type = (int)System.Int32.Parse(row ["AccountType"].ToString ()); if (type == 1) { key.Type = AccountType.CheckingAccount; } else { key.Type = AccountType.SavingsAccount; } details.key = key; details.customerNames = row ["CustomerNames"].ToString (); details.balance = (float)System.Double.Parse(row ["Balance"].ToString ()); } return details; } /// <summary> /// The removeAccount method

229: /// </summary> 230: /// <param name="accountKey">The Account Number</param> 231: /// <returns>true if successful, false if not</returns> 232: protected bool removeAccount (int accountKey) { 233: MessageBox.Show ("BookKeeper::removeAccount() invoked with key: 234: "+accountKey, "BookKeeper"); 235: bool result = false; 236: lock (this) { 237: DataTable table = dataSet_.Tables ["Accounts"]; 238: try { 239: 240: table.Rows.Find (accountKey).Delete (); 241: table.AcceptChanges (); 242: dataSet_.AcceptChanges (); 243: result = true; 244: } 245: catch (Exception exception) { 246: Console.WriteLine (exception.ToString ()); 247: MessageBox.Show (exception.ToString (), "BookKeeper::removeAccount"); 248: } 249: } 250: return result; 251: } 252: 253: /// <summary> 254: /// The getNextKey method 255: /// </summary> 256: /// <returns>The New Account Number (primary key)</returns> 257: protected int getNextKey () { 258: int result = 1; 259: lock (this) { 260: try { 261: DataTable table = dataSet_.Tables ["Accounts"]; 262: // This is a hack. But what the heck! 263: // This is just a demo !!! 264: if (null != table) { 265: result = table.Rows.Count+1; 266: } 267: } 268: catch (Exception exception) { 269: Console.WriteLine (exception.ToString ()); 270: } 271: finally { 272: result = (result == 0)?1:result; 273: } 274: } 275: return result; 276: } 277: 278: /// <summary> 279: /// The writeDataSet method 280: /// </summary> 281: public void writeDataSet () { 282: MessageBox.Show ("BookKeeper::writeDataSet() invoked", "BookKeeper");

283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306:

lock (this) { dataSet_.AcceptChanges(); // Dump the DataSet to the Console dataSet_.WriteXml (Console.Out, XmlWriteMode.WriteSchema); // Dump the DataSet to an XML File dataSet_.WriteXml ("Accounts.xml", XmlWriteMode.WriteSchema); } } /// <summary> /// The Main method /// </summary> /// <param name="args"></param> static void Main(string[] args) { TcpChannel channel = new TcpChannel (1099); ChannelServices.RegisterChannel (channel); RemotingConfiguration.RegisterWellKnownServiceType (typeof(BookKeeper), "BookKeeper", WellKnownObjectMode.Singleton); System.Console.WriteLine ("Press <Enter> to Exit ..."); // Instruct the runtime to Garbage Collect GC.Collect (); System.Console.ReadLine (); } } }

Distributed BookKeeper
In the code example of BookKeeper.cs above, since we're using the TCP channel, we need to tell the compiler that we need definitions in the System.Runtime.Remoting andSystem.Runtime.Remoting.Channels.Tcp namespaces as shown on Lines 14 and 15. Also note on Line 43, that the BookKeeper class derives from MarshalByRefObject so that it can have a distributed identity. The default is marshaling-by-value, which means that a copy of the remote object is created on the client side. Subclassing from the MarshalByRefObject class gives the object a distributed identity, allowing the object to be referenced across application domains, or even across process and machine boundaries. Even though a marshal-by-reference object requires a proxy to be setup on the client side and a stub to be setup on the server side, the infrastructure handles this automatically, without us having to do any other extra work. Any external client can invoke all the public methods of the BookKeeper class because the Main() method uses the TcpChannel class. The Main() method on Line 294 instantiates a TcpChannel, passing in a port number from which the server will listen for incoming requests. Once we've created a Channel object, we register the Channel to the ChannelServices, which supports channel registration and object resolution on Line 295.

TheRegisterWellKnownServiceType() method of the RemotingConfiguration class allows you to register you object with the RemotingConfiguration so that it can be activated as shown on Line 297. When you invoke this method, you need to provide the class name, URI, and an object activation mode. The URI is important as the client will use it to refer specifically for this registered object.

AccountKey.cs
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: ////////////////////////////////////////////////////// /// The following example is a helper class for the /// BookKeeper example /// /// author: Gopalan Suresh Raj /// Copyright (c), 2001. All Rights Reserved. /// URL: http://gsraj.tripod.com/ /// email: gopalan@gmx.net /// ////////////////////////////////////////////////////// using System; namespace BookKeeper { /// <summary> /// This enum type maps to an inderlying byte type /// </summary> /// Make this type Serializable [Serializable] public enum AccountType : int { CheckingAccount = 1, SavingsAccount = 2 } /// <summary> /// Summary description for AccountKey. /// </summary> /// Make this type Serializable [Serializable] public class AccountKey { /// <summary> /// The Account Number /// </summary> int accountKey_; /// <summary> /// The Type of Account - Checking or Savings /// </summary> AccountType accountType_; /// <summary> /// The Public Default No-argument constructor

46: /// </summary> 47: public AccountKey() { 48: accountKey_ = 0; 49: accountType_ = AccountType.CheckingAccount; 50: } 51: 52: /// <summary> 53: /// Accessors for the account key 54: /// </summary> 55: public int Key { 56: 57: get { 58: return this.accountKey_; 59: } 60: 61: set { 62: this.accountKey_ = value; 63: } 64: } 65: 66: /// <summary> 67: /// Accessors for the account type 68: /// </summary> 69: public AccountType Type { 70: 71: get { 72: return this.accountType_; 73: } 74: 75: set { 76: this.accountType_ = value; 77: } 78: } 79: } 80: }

5. Modify the generated AssemblyInfo.cs to add the right assembly information


You provide the compiler with your assembly information in an assembly file called AssemblyInfo.cs. The assembly information file is compiled with the rest of the project's source files. The information is in the form of assembly attributes - directives to the compiler on the information to embed in the assembly.

AssemblyInfo.cs
1: using System.Reflection;

2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55:

using System.Runtime.CompilerServices; // // General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. // [assembly: AssemblyTitle("BookKeeper for Bank")] [assembly: AssemblyDescription("Manages to keep track of Account Numbers for the AccountManager")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("eCommWare Corporation")] [assembly: AssemblyProduct("COM+ Bank Server")] [assembly: AssemblyCopyright("(c) 2001, Gopalan Suresh Raj. All Rights Reserved." )] [assembly: AssemblyTrademark("Web Cornucopia")] [assembly: AssemblyCulture("")] // // Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: [assembly: AssemblyVersion("1.0.0.0")] // // // // // // // // // // // // // // // // // // // // // // // // In order to sign your assembly you must specify a key to use. Refer to the Microsoft .NET Framework documentation for more information on assembly signing. Use the attributes below to control which key is used for signing. Notes: (*) If no key is specified, the assembly is not signed. (*) KeyName refers to a key that has been installed in the Crypto Service Provider (CSP) on your machine. KeyFile refers to a file which contains a key. (*) If the KeyFile and the KeyName values are both specified, the following processing occurs: (1) If the KeyName can be found in the CSP, that key is used. (2) If the KeyName does not exist and the KeyFile does exist, the key in the KeyFile is installed into the CSP and used. (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility. When specifying the KeyFile, the location of the KeyFile should be relative to the project output directory which is %Project Directory%\obj\<configuration>. For example, if your KeyFile is located in the project directory, you would specify the AssemblyKeyFile attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] (*) Delay Signing is an advanced option - see the Microsoft .NET Framework documentation for more information on this.

56: // 57: [assembly: AssemblyDelaySign(false)] 58: [assembly: AssemblyKeyFile("BookKeeper.key")] [assembly: AssemblyKeyName("")]

In particular, pay attention to the fact that we specify a version number for this library using the AssemblyVersion attribute and also specify the assembly key file using the AssemblyKeyFileattribute.

6. Build the Project Files


Build the files that make up the project.
------ Rebuild All started: Project: BookKeeper, Configuration: Debug .NET -----Preparing resources... Updating references... Performing main compilation... Build complete -- 0 errors, 0 warnings Building satellite assemblies...

---------------------- Done ---------------------Rebuild All: 1 succeeded, 0 failed, 0 skipped

7. Deploy the component as a Shared Assembly


After you've built the assembly, you can use the .NET Global Assembly Cache (GAC) utility to register this assembly into the GAC as shown below. Command Prompt
C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>gacutil /i BookKeeper.exe Microsoft (R) .NET Global Assembly Cache Utility. Version 1.0.2914.16 Copyright (C) Microsoft Corp. 1998-2001. All rights reserved. Assembly successfully added to the cache C:\MyProjects\Cornucopia\COMplus\BankServer\BookKeeper\bin\Debug>

Successful registration against the cache turns this component into a shared assembly. A version of this component is copied into the GAC so that even if you delete this file locally, you will still be able to run your client program.

8. Execute the Server


Once you've built the Server, start the server program, which will wait endlessly until you hit the Enter Key. The server is now ready to service client requests.

Now you need to build a client application that can access this Distributed Component.

Introducing the ADO.NET Entity Framework


Database development with the .NET framework has not changed a lot since its first release. Many of us usually start by designing our database tables and their relationships and then creating classes in our application to emulate them as closely as possible in a set of Business Classes or (false) "Entity" Classes, and then working with them in out ADO.NET code. However, this process has always been an approximation and has involved a lot of groundwork. This is where the ADO.NET Entity Framework comes in; it allows you to deal with the (true) entities represented in the database in your application code by abstracting the groundwork and maintenance code work away from you. A very crude description of the ADO.NET Entity Framework would be "It allows you to deal with database concepts in your code." Of course, it's much more than that, but in this tutorial, instead of starting with a lengthy spiel about the Entity Framework and how it differs from LINQ to Sql, you will code first, talk later.

Contents
In this tutorial, you will go over the following:

Page 1: Set up the environment and database, generating the Entity Data Model Page 2: Basic ADO.NET Entity Framework operations with a form for Payrolls Page 3: Adding a little efficiency using another form for Authors Page 4: The case for using stored procedures in the Entity Framework Page 5: Using stored procedures to perform SELECT operations against the database in the Articles form Page 6: Using stored procedures for the INSERT, UPDATE, and DELETE operations in the Articles form Page 7: More information and conclusion

Setting Up Your Environment For this ADO.NET Entity Framework tutorial, you will need the following:

SP1 for .NET Framework 3.5/Visual Studio 2008 (which you can download here.) Some C# knowledge, because the code samples here are in C# A little prior knowledge of ADO.NET and SQL Approximately 250ml to 350ml of trimethylxanthine, otherwise associatedly known as coffee

Setting Up the Database You can either create your own project, or refer to the project files attached to this article (bottom of page), but I would recommend starting your own and glancing at the attached project files if you need to. Before you start coding, though, you will need to create the database and its objects that will be used and referred to in this tutorial. The DatabaseScript.zip file contains a .sql script that you need to run against your SQL Express or SQL Server database; this script will generate the database for a theoretical publishing company, inventively named PublishingCompany, and the tables and stored procedures required. Note: You don't need to use SQL Server. You can use any database you'd like, but then you will need to modify the script to work with the SQL implementation for your database. For the purposes of this tutorial, I will continue to refer to SQL Server as the database. Generating an Entity Data Model in Your Visual Studio Project Once you are satisfied that the database has been created and you have had a look through all of the tables and its fields, start by creating a new Windows Forms Application project. I suggest the name of the solution to be SodiumHydroxide. I chose this name because I'm hoping that this project will serve as a good base for your learning. (Chemistry pun alert!)

The very first step is to generate your Entity Data Model from the database that you created earlier; this will serve to be at the core of all your ADO.NET Entity Framework operations. To do this, right-click on the project and add a new item. Add an "ADO.NET Entity Data Model" and call it PublisherModel.edmx to correspond to your database.

The Entity Data Model Wizard shows up and you now can use this to query your database and generate the model diagram, as long as you supply it with the right credentials. In the Wizard, click "Generate from Database" and click Next.

Supply it with the right server name, authentication, credentials, and the database name PublishingCompany.

Yes, I do like to name various entities on my home network after arcane Mesoamerican civilizations. Finally, "Save entity connections settings in App.Config as" should be PublishingCompanyEntities.

In the next dialog box, choose all of the options tables, views, and stored proceduresand the model will be generated for you. You should end up with this:

This is a graphical representation of the Entity Data Model (EDM) that's generated by the wizard. Note that it isn't exactly a table mapping in the database, but it looks close. You'll also see that the Author entity has an article reference and payroll reference, even though you haven't actually created fields in the Author table; this relationship was derived from the foreign key constraint by the EDM generator. If you are like me, you probably want to know what's happening behind the scenes; you can right-click on the .edmx file in Solution Explorer and choose to view it with an XML Editor. Even if you aren't interested, I would encourage you to look at the XML anyways, because advanced Entity Framework operations will require you to directly edit the XML file, but not for this tutorial. As you can see, the EDM is essentially an XML file that's generated from the database schema, and which is understood by the Visual Studio designer to give you a graphical representation of your database entities. On the next page, you will start working on the first form with basic Entity Framework operations.

Basic ADO.NET Entity Framework Operations


Simplest stuff first; now that you're set up, you can start by creating a form to traverse through all the payrolls. To do this, you'll need a combobox that will enumerate the author names, a label, and a textbox to display the details of that author's payroll if it exists (keep in mind that some authors may not be on the payroll). Start by creating a form like this: [ado_10.jpg]

The 11 Key Questions to Ask of a BI Solution Download Now

Name it PayrollView and give the controls appropriate names. For now, you'll just populate the combobox with the author names. In the form's code, at the top of the class, add this: 1. PublishingCompanyEntities publishContext; 2. Payroll currentPayroll; And in the form load event, instantiate the publishContext object. 1. publishContext = new PublishingCompanyEntities(); In the form closing event, always dispose it. 1. publishContext.Dispose(); This PublishingCompanyEntities objectpublishContextis very important; it serves as the basis of all the ADO.NET Entity queries that you will be using. To watch it at work at its most basic level, populate the combobox with a list of authors. In the form's load event, add this: 1. //This is a simple ADO.NET Entity Framework query! 2. authorList.DataSource = publishContext.Author; 3. authorList.DisplayMember = "FirstName"; In the code above, authorList is the name of the combobox. Press F5 and watch the form load. You should see the combobox with the author names in it! Loop through that list for a bit and marvel at your handiwork. [ado_11.jpg] Behind the scenes, when you set the DataSource and DisplayMember of the combobox to the publishContext.Author property, the publishContext performed the query against the database and returned the results for the combobox to use. You didn't have to open a connection or create a command; the housework was taken care of by the publishContext object. Now, you can populate the textboxes that represent the payroll properties for each author. Handle the combobox's SelectedIndexChanged event. Add this code to the event: 1. Author selectedAuthor = (Author)authorList.SelectedItem; 2. int selectedAuthorID 3. 4. //Uses Linq-to-Entities 5. IQueryable<Payroll> payrollQuery = 6. 7. 8. 10. 11. if (selectedPayroll != null && selectedPayroll.Count > 0) 12. { from p in publishContext.Payroll where p.Author.AuthorID == selectedAuthorID select p; = selectedAuthor.AuthorID;

9. List<Payroll> selectedPayroll = payrollQuery.ToList();

13. 14. }

currentPayroll = selectedPayroll.First();

15. else 16. { 17. 18. } 19. 20. PopulateFields(); In the code above, you do the following: 1. 2. 3. 4. 5. Get the current Author object from the combobox by looking at the SelectedItem property. Use a LINQ-to-Entities query against publishContext to filter the Payrolls on the AuthorID. The return type for the LINQ-to-Entities query is IQueryable<>, which you convert to a List<>. Check whether it has values and get the first row from the returned results because you only want one author's payroll. Assign this value to the currentPayroll object which then is used in your common PopulateFields method. The PopulateFields method, shown below, simply reads the properties of the Payroll object and places the value in corresponding labels/textboxes. 1. private void PopulateFields() 2. { 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. } Run the application again, and when you select different authors from the combobox, you should get their corresponding salaries. You'll also see that the "Add" button is disabled for authors with payrolls, and enabled for authors without payrolls. Coding the Update and Delete buttons Because this is a simple example, the only property that the user can modify is the author's Salary. In the Update event, set the currentPayroll's Salary property to be the value of the numeric up-down control. Then, simply call the SaveChanges method on publishContext. Here is the Update button's click event: } } else { payrollIDLabel.Text salaryUpDown.Value addButton.Enabled = "Not on payroll"; = 0; = true; if (currentPayroll != null) { payrollIDLabel.Text salaryUpDown.Value addButton.Enabled = currentPayroll.PayrollID.ToString(); = (decimal)currentPayroll.Salary; = false; currentPayroll = null;

deleteButton.Enabled = true; updateButton.Enabled = true;

deleteButton.Enabled = false; updateButton.Enabled = false;

1. currentPayroll.Salary = Convert.ToInt16(salaryUpDown.Value); 2. int rowsAffected = publishContext.SaveChanges(); 3. MessageBox.Show(rowsAffected.ToString() + 4. " changes made to the table"); The SaveChanges method is akin to the dataadapter's Update method in regular ADO.NET; it will go through the collection of objects for 'marked' entities, and then update them in the database. In the Delete button's click event, use the publishContext's DeleteObject method before calling SaveChanges(). 1. publishContext.DeleteObject(currentPayroll); 2. publishContext.SaveChanges(true); 3. currentPayroll = null; 4. PopulateFields(); You called the DeleteObject method, passing it the current Payroll object, which marks it for deletion. You then called the SaveChanges method that performs the deletion. Run the form again. Update a few salaries and try deleting one or two payrolls. Note that this method will delete the payroll associated with an author; it will not delete the author itself. Coding the Add button The Add button's click event will require a little more code. You first must create a brand new Payroll object, assign it values, and then call the AddToPayroll() method of the publishContext. The AddTo< EntityName> methods are generated by the Entity Framework based on the entities it generated from the database all entities have one. It will perform the INSERT against the database and return the PayrollID of the new row in the table. 1. Payroll newPayroll = new Payroll(); 2. Author selectedAuthor = (Author)authorList.SelectedItem; 3. newPayroll.Author 4. newPayroll.Salary = selectedAuthor; = Convert.ToInt16(salaryUpDown.Value);

5. publishContext.AddToPayroll(newPayroll); 6. //INSERT against database and get new PayrollID 7. publishContext.SaveChanges(); 8. //newPayroll.PayrollID now matches the database table 9. currentPayroll = newPayroll; 10. PopulateFields(); Because PopulateFields is called right at the end, you will see that after you add a new Payroll to the database, the PayrollID label has been filled with the new value. Again, the Entity Framework has taken care of the new ID and assigned it to newPayroll.PayrollID for you to use. Run your form and, if you haven't deleted any authors yet, do so now. Once you delete the author, their ID label will say "Not on payroll" and their salary will be 0. Modify the salary and click "Add". Your form now displays authors and their payrolls, and allows you to add, update, and delete payrolls. Have a play with it and marvel at your handiwork again before you continue. [ado_11b.jpg] On the next page, you will use the same concepts learned here, but with a little more efficiency.

Adding a Little Efficiency


In the Payroll form, every action performed meant one database call to get the data. For the Authors form, you will use almost all of the same logic as the Payroll form, but take a slightly different approach; we will minimize the number of database calls made by getting all the authors in one call and saving the data changes together. It will feel a little clumsy doing this, but it's only to show you how you can take another approach for basic operations in the Entity Framework. Create a new form for Authors that looks like this. It has a previous, next, first, last, update, and add new button. The "Clear for new Author" button will simply empty out the controls and "Create New" button will work off it by creating the Author object.

The 11 Key Questions to Ask of a BI Solution Download Now

The 11 Key Questions to Ask of a BI Solution Download Now

[ado_13.jpg] Agreed, not the most intuitive interface, but this is just a tutorial. The "Send to Database" is the only button that will make a database call; the rest will manipulate the Author objects. As before, create the PublishingCompanyEntities object and instantiate it in the form load event. This time, declare a List<Author> as well; this is what you will use to hold the authors. Also, add an indexing integer to hold your current 'position'. At the top of the class: 1. PublishingCompanyEntities publishContext; 2. List<Author> authorList; 3. int currentAuthorIndex = 0; In the form's load event: 1. publishContext = new PublishingCompanyEntities(); 2. authorList = new List<Author>(); Back in the PayrollView form, you got all the authors using publishContext.Author. This is actually an ADO.NET Entity Query Builder method expression. Query builder methods allow you to use various methods to filter the data you get back. You will read more on these methods later, but for now you should know that the result of most query builder method expression is an ObjectQuery<>, ObjectResult<>, or IQueryable<>. These classes represent the returned entity collections for the queries you perform. You will read more about these later. For the sake of variety, you will use an ObjectQuery<> next. You will get all the authors from the database, except for anyone named Mark, because you don't really care about Mark. For business logic reasons, of course.

1. ObjectQuery<Author> authorQuery = 2. publishContext.Author.Where("it.FirstName <> 'Mark'"); 3. authorList = authorQuery.ToList(); 4. PopulateFields(); Now you have a list of Author objects that you can manipulate in your form. The PopulateFields method will look slightly different. 1. private void PopulateFields() 2. { 3. 4. 5. 6. 7. } You are traversing the List<>, an in-memory object. The previous, next, first, and last buttons are now easy to implement. 1. private void firstButton_Click(object sender, EventArgs e) 2. { 3. 4. 5. } 6. 7. private void previousButton_Click(object sender, EventArgs e) 8. { 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. } 19. 20. private void nextButton_Click(object sender, EventArgs e) 21. { 22. 23. 24. 25. 26. 27. 28. 29. 30. } else { currentAuthorIndex += 1; PopulateFields(); if (currentAuthorIndex == authorList.Count - 1) { MessageBox.Show("No next author"); PopulateFields(); } } else { currentAuthorIndex -= 1; PopulateFields(); if (currentAuthorIndex == 0) { MessageBox.Show("No previous author"); currentAuthorIndex = 0; PopulateFields(); Author currentAuthor = authorList[currentAuthorIndex]; firstName.Text lastName.Text authorIDLabel.Text = currentAuthor.FirstName; = currentAuthor.LastName; = currentAuthor.AuthorID.ToString();

31. 32. } 33.

34. private void lastButton_Click(object sender, EventArgs e) 35. { 36. 37. 38. } Run the application (set AuthorView as the startup object) and ensure that the navigation buttons are working. The Update button is easy too. Get the current author and set its values from the textboxes. 1. private void update_Click(object sender, EventArgs e) 2. { 3. 4. 5. 6. } Note that this only modifies an existing author. Nothing has been sent to the database yet. You can do that now; in the click event for the "Send to database" button, 1. private void sendToDB_Click(object sender, EventArgs e) 2. { 3. 4. 5. } Try it out. Modify an author or authors and see whether your changes make it through to the database. A simple call to publishContext.SaveChanges() works because it still holds a reference to the same objects that were returned from the original ObjectQuery which is in your authorList as well. Now, try adding a new author. The "Clear for new author" button should clear the fields to make it obvious (or not) that a new author is being created. The "Save New" button should actually create the Author object. 1. private void clearForNew_Click(object sender, EventArgs e) 2. { 3. 4. 5. 6. } 7. 8. private void createNew_Click(object sender, EventArgs e) 9. { 10. 11. 12. 13. 14. Author newAuthor newAuthor.LastName newAuthor.AuthorID = new Author(); = lastName.Text; = -1; //To make it obvious that it's new newAuthor.FirstName = firstName.Text; firstName.Text lastName.Text = string.Empty; = string.Empty; int rowsAffected = publishContext.SaveChanges(true); MessageBox.Show(rowsAffected.ToString() + " changes made."); Author currentAuthor currentAuthor.LastName = authorList[currentAuthorIndex]; = lastName.Text; currentAuthor.FirstName = firstName.Text; currentAuthorIndex = authorList.Count - 1; PopulateFields();

authorIDLabel.Text = "Not saved yet";

authorList.Add(newAuthor);

15. 16. 17. 18. 19. }

publishContext.AddToAuthor(newAuthor); //Set the index to the last, new item. currentAuthorIndex = authorList.Count - 1; PopulateFields();

The key here is to use the AddToAuthor method to add the new Author object to your publishContext 'database'. You also are adding it to the list of authors that you are holding. Because the list and the publishContext reference the same new Author object, when you add a new author and click "Send to database", you'll notice that the new Author object gets the proper primary key ID instead of the -1 that we placed there. Same principle as beforethe new identity is returned and given to the new Author object. On the next page, you will look at the efficiency of the SQL generated behind the scenes for your queries.

The Case for Stored Procedures


The AuthorView form isn't that much different from the PayrollView form, but it does show you that you can deal with the data in different ways, depending on your preference. You did manage to cut down the database calls when navigating through the forms,, which is great. But, the profiler trace shows you queries that aren't exactly satisfactory. This is what I got in the profiler when I updated an author: 1. exec sp_executesql N'update [dbo].[Author] 2. set [FirstName] = @0, [LastName] = @1 3. where ([AuthorID] = @2) 4. ',N'@0 nvarchar(6),@1 nvarchar(7),@2 5. int',@0=N'Lerroy',@1=N'Jenkins',@2=2 If you've done a lot of enterprise development, you will probably be tearing the last few strands of your remaining hair follicles out right now. Dynamic SQL, that's just not acceptable. In fact, if you were to perform a lot of 'joining' (in a LINQ-to-SQL style) with your objects, you will see some extremely large and convoluted SELECT statements being run against the database in SQL Profiler. If you use an ObjectQuery, you can use its .ToTraceString() method to see the SQL statement that will be used. Go back to the AuthorView form and use a MessageBox to display the ToTraceString of the ObjectQuery: 1. private void AuthorView_Load(object sender, EventArgs e) 2. { 3. 4. 5. 6. 7. 8. 9. 10. 11. } ObjectQuery<Author> authorQuery = publishContext.Author.Where("it.FirstName <> 'Mark'"); authorList = authorQuery.ToList(); MessageBox.Show(authorQuery.ToTraceString()); PopulateFields(); publishContext = new PublishingCompanyEntities(); authorList = new List<Author>();

The 11 Key Questions to Ask of a BI Solution

Download Now

But the query in this specific example isn't that bad: 1. SELECT [Extent1].[AuthorID] AS [AuthorID], 2. [Extent1].[FirstName] AS [FirstName], [Extent1]. 3. [LastName] AS [LastName]FROM [dbo].[Author] AS 4. [Extent1]WHERE [Extent1].[FirstName] <> 5. 'Mark' To illustrate this point further (and show you another query method), go back to the AuthorView form and add another label. You will use this label to display the number of articles that this author has written. For this purpose, you will use an ObjectQuery method called Include. What is the Include method? You have seen that query methods will only retrieve what you ask; for instance, in your current query 1. ObjectQuery<Author> authorQuery = 2. publishContext.Author.Where("it.FirstName <> 'Mark'"); you won't have any information about the author's payroll or articles. You only have information about authors not named Mark. If you look at authorList in a quickwatch window after it has been populated, you will see that the Payroll and Article properties of each Author object have no values. The Include method will therefore load the entities associated with the author object, those that you ask for. Go back to the AuthorView form's load event, and modify the ObjectQuery like this to use the Include method. 1. ObjectQuery<Author> authorQuery = 2. 3. publishContext.Author.Where("it.FirstName <> 'Mark'").Include("Article");

This means get all the authors, except Mark, and for each author, include their associated Article entities. In the PopulateField() method, you now can read the article count. 1. articleCountLabel.Text = currentAuthor.Article.Count.ToString(); Run the form and you should see the articles count label change for each author. But, did you notice the SQL trace string? 1. SELECT 2. [Project1].[AuthorID] AS [AuthorID], 3. [Project1].[FirstName] AS [FirstName], 4. [Project1].[LastName] AS [LastName], 5. [Project1].[C1] AS [C1], 6. [Project1].[C3] AS [C2], 7. [Project1].[C2] AS [C3], 8. [Project1].[ArticleID] AS [ArticleID], 9. [Project1].[Title] AS [Title], 10. [Project1].[Body] AS [Body], 11. [Project1].[AuthorID1] AS [AuthorID1] 12. FROM ( SELECT 13. 14. 15. 16. [Extent1].[AuthorID] AS [AuthorID], [Extent1].[FirstName] AS [FirstName], [Extent1].[LastName] AS [LastName], 1 AS [C1],

17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. )

[Extent2].[ArticleID] AS [ArticleID], [Extent2].[Title] AS [Title], [Extent2].[Body] AS [Body], [Extent2].[AuthorID] AS [AuthorID1], CASE WHEN ([Extent2].[ArticleID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2], CASE WHEN ([Extent2].[ArticleID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C3] FROM [dbo].[Author] AS [Extent1] LEFT OUTER JOIN [dbo].[Article] AS [Extent2] ON [Extent1].[AuthorID] = [Extent2].[AuthorID] WHERE [Extent1].[FirstName] <> 'Mark' AS [Project1]

30. ORDER BY [Project1].[AuthorID] ASC, [Project1].[C3] ASC In case you're shaking your head and are about to give in to the temptation of criticizing large, faceless corporations for poor standards, you must keep in mind that in any given system, ease-of-use and the generic-ness is inversely proportional to the efficiency. So, to make it more convenient to query and load objects, there is a slight hit with your SQL statements. However, this is where the ADO.NET Entity Framework shines through; you can tell it to use your own stored procedures instead of the generated SELECT, UPDATE, DELETE, and INSERT statements. Because stored procedures are generally more efficient than dynamic SQL and because you are given this flexibility with the Entity Framework, this is where the real 'power' of the framework is apparent. On the next page, you will map stored procedures with the ADO.NET Entity Framework.

Using Stored Procedures with the ADO.NET Entity Framework


Any proper enterprise environment will have a DBA or a set of DBAs who guard and watch over their databases in the same way that a mother bear watches over her cub. If you inappropriately step between the DBAs and the DBOs, you will get mauled. This means that even with the ADO.NET Entity Framework, they will still want to retain ownership of the database and the objects in it. The ADO.NET Entity Framework allows you to use most of the application code as you did before, but with stored procedures that you or they may have written in the most 'optimal' way. It'll take a bit of work to get this set up and running, but the effort pays off in the end. You are going to work on using stored procedures with the Article entities on a new form, first starting with a few tasks that you're already familiar with, and then get on to the good stuff. 1. 2. 3. 4. 5. 6. 7. Creating the SELECT stored procedure(s) Layout of the ArticleView form Importing the stored procedure Using the stored procedure to get entities in our code Another stored procedure, and using it Navigation and update code Ta-daa!

The 11 Key Questions to Ask of a BI Solution Download Now

Obviously, the first step is to create the SELECT stored procedure. 1. CREATE PROCEDURE GetArticle 2. 3. AS 4. BEGIN 5. 6. 7. 8. 10. GO Next, you need to import this newly created stored procedure into your EDM. There are two ways to do this: You can regenerate the EDM and import it via the designer, or you can go directly into the XML and edit it there. You will cover the visual method in this tutorial. 1. 2. 3. 4. 5. Right-click anywhere in the Entity Designer view and click "Update Model from Database". Choose Tables and Stored procedures from the dialog box and click Finish. Next, open the Model Browser tab and search for the newly created stored procedure, GetArticle. Right-click on it and choose "Create Function Import". Set the return type as the Article Entities. SET NOCOUNT ON; SELECT ArticleID, Title, Body, AuthorID FROM Article WHERE ArticleID = @ArticleID @ArticleID INT

9. END

[ado_14.jpg] [ado_15.jpg] Stored procedures within the EDM are imported as functions, and a function that you import always returns a collection of entities. In future releases, this may change to allow a stored procedure to return a single entity. The result of a stored procedure goes into an ObjectResult<>, similar to how publishContext.Author's return type was ObjectQuery<>. To see it working, quickly create an ArticleView form. Add a new form to the solution, ArticleView, with these controls. [ado_16.jpg] A little simple for now, but you'll expand it as you go along. In the code, Top of the class: 1. PublishingCompanyEntities publishContext; 2. Article currentArticle; In the form's load event: 1. private void ArticleView_Load(object sender, EventArgs e) 2. { 3. publishContext = new PublishingCompanyEntities();

4. 5. 6. 7. 8. 9. }

currentArticle = new Article(); ObjectResult<Article> articleQuery = publishContext.GetArticle(1); currentArticle = articleQuery.ToList().First(); PopulateFields();

And your old friend: 1. private void PopulateFields() 2. { 3. 4. 5. 6. } This time, you will notice that you make an explicit call to the GetArticle stored procedure, passing it the ArticleID 1. Run the form and you'll see the first article loaded up. And, it's done using your GetArticle stored procedure. This is good because it means that you can optimize complicated queries if you need to and use stored procedures to help you. However, in this particular case, when you introduce navigation buttons to the ArticleView form, you'll have to make a new stored procedure call for each button click event (for each ID). Avoid that situation and get all of the Articles in one go instead. Create a GetArticles (plural) stored procedure now. 1. CREATE PROCEDURE GetArticles 2. AS 3. BEGIN 4. 5. 6. 8. GO Import the GetArticles function as shown earlier. You then can use an ObjectResult<Article>, convert it ToList(), and assign it to a List<> object. Top of the class: 1. PublishingCompanyEntities publishContext; 2. List<Article> articleList; 3. int currentArticleIndex = 0; Form load: 1. publishContext = new PublishingCompanyEntities(); 2. articleList = new List<Article>(); 3. IEnumerable<Article> articleQuery = 4. 5. from ar in publishContext.GetArticles() select ar; SET NOCOUNT ON; SELECT ArticleID, Title, Body, AuthorID FROM Article articleIDLabel.Text = currentArticle.ArticleID.ToString(); titleText.Text bodyText.Text = currentArticle.Title; = currentArticle.Body;

7. END

6. articleList = articleQuery.ToList(); 7. PopulateFields();

I used a LINQ-to-Entities query instead of a method expression, hoping you would notice the flexibility available to you. You can introduce your filters into the expression and it won't affect the SP call. To illustrate, just as a test: 1. IEnumerable<Article> articleQuery = 2. 3. 4. from ar in publishContext.GetArticles() where ar.ArticleID > 5 select ar;

This will perform a GetArticles SP call and then filter the values returned afterwards. However, you're not interested in filtering it right now, so remove the where clause from the LINQ expression. Again, there is a PopulateFields method in this form that changes slightly. 1. private void PopulateFields() 2. { 3. 4. 5. 6. 7. } Run the form and make sure that the first article still shows. Now, go back to the form designer and add the navigation buttons. Also, add an "Update" button. a "Clear for new" button. an "Add as new article" button. and a "Delete" button. Same principles as before you navigate through the List<> for the navigation buttons, update an object's properties in the List<> for the Update button, clear the fields for the "Clear for new" button, and add a new object to the publishContext for "Add as new article". [ado_17.jpg] Based on work done in the past few pages, you must have an idea of what the various buttons will do now, so I'll simply list the code for the buttons here, and then you can get down to the main point of this task using stored procedures for INSERT, UPDATE, and DELETE. 1. private void firstButton_Click(object sender, EventArgs e) 2. { 3. 4. 5. } 6. 7. private void previousButton_Click(object sender, EventArgs e) 8. { 9. 10. 11. 12. 13. 14. 15. } else { if (currentArticleIndex > 0) { currentArticleIndex -= 1; PopulateFields(); currentArticleIndex = 0; PopulateFields(); Article currentArticle = articleList[currentArticleIndex]; articleIDLabel.Text titleText.Text bodyText.Text = currentArticle.ArticleID.ToString(); = currentArticle.Title; = currentArticle.Body;

16. 17. 18. } 19. }

MessageBox.Show("No more articles to display");

20. private void nextButton_Click(object sender, EventArgs e) 21. { 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. } 32. 33. private void lastButton_Click(object sender, EventArgs e) 34. { 35. 36. 37. } 38. 39. private void updateButton_Click(object sender, EventArgs e) 40. { 41. 42. 43. 44. } 45. 46. private void clearForNewButton_Click(object sender, EventArgs e) 47. { 48. 49. 50. 51. } 52. 53. private void saveAsNew_Click(object sender, EventArgs e) 54. { 55. 56. 57. 58. 59. 60. 61. Article newArticle = new Article(); newArticle.Title = titleText.Text; newArticle.Body = bodyText.Text; newArticle.ArticleID = -1; publishContext.AddToArticle(newArticle); articleList.Add(newArticle); currentArticleIndex = articleList.Count - 1; articleIDLabel.Text = "-1"; titleText.Text = string.Empty; bodyText.Text = string.Empty; Article currentArticle = articleList[currentArticleIndex]; currentArticle.Title = titleText.Text; currentArticle.Body = bodyText.Text; currentArticleIndex = articleList.Count - 1; PopulateFields(); } } else { currentArticleIndex += 1; PopulateFields(); if (currentArticleIndex == articleList.Count - 1) { MessageBox.Show("No more articles to display");

62. 63. } 64.

PopulateFields();

65. private void deleteButton_Click(object sender, EventArgs e) 66. { 67. 68. 69. 70. 71. 72. } 73. 74. private void submitToDatabase_Click(object sender, EventArgs e) 75. { 76. 77. 78. } Note that although the code looks just like it did in the AuthorView form, when you do map your stored procedures, you won't have to change any of the code. On the next page, you can (finally!) map the INSERT, UPDATE, and DELETE stored procedures. publishContext.SaveChanges(); PopulateFields(); Article currentArticle = articleList[currentArticleIndex]; publishContext.DeleteObject(currentArticle); articleList.Remove(currentArticle); currentArticleIndex = 0; PopulateFields();

About the Author


SM Altaf
Mendhak is a web developer and a Microsoft MVP who works with ASP.NET and PHP among the usual array[] of web technologies. He is also rumored to be a three eyed frog, but the evidence is lacking. He can be contacted via his website, www.mendhak.com.

Вам также может понравиться