ADO Pgms
ADO Pgms
ADO Pgms
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.
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: }
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>
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: [email protected] /// /// <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; }
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.
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
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.
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
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: [email protected] /// ////////////////////////////////////////////////////// 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: [email protected] /// ////////////////////////////////////////////////////// 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: }
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.
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.
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.
Now you need to build a client application that can access this Distributed Component.
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.
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;
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;
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.
[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();
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();
authorList.Add(newAuthor);
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.
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.
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. }
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
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;
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");
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();