Visual Studio .NET And Data Access


This section discusses some of the new ways that Visual Studio allows data to be integrated into the GUI. More specifically, it discusses how to create a connection, select some data, generate a DataSet, and use all of the generated objects to produce a simple application. The available tools enable you to create a database connection with the OleDbConnection or SqlConnection classes. The class you use depends on the type of database you are using. After a connection has been defined, you can create a DataSet and populate it from within Visual Studio .NET. This generates an XSD file for the DataSet(similar to the file that was created manually in Chapter 19) and the .cs code. The result is a type-safe DataSet.

Creating a Connection

First, create a new Windows application and then create a new database connection. Using the Server Explorer (see Figure 24-19), you can manage various aspects of data access.

image from book
Figure 24-19

For this example, create a connection to the Northwind database. Select the Add Connection option from the context menu available on the Data Connections item to launch a wizard that enables you to choose a database provider. Select the .NET Framework Provider for SQL Server. Figure 24-20 shows the second page of the Connection Properties dialog box.

image from book
Figure 24-20

Depending on your .NET Framework installation, the sample databases might be located in SQL Server, MSDE (Microsoft SQL Server Data Engine), or both.

To connect to the local MSDE database, if it exists, type (local)\\NETSDK for the name of the server. To connect to a regular SQL Server instance, type (local) or '.' to select a database on the current machine, or the name of the desired server on the network. You may need to enter a username and password to access the database.

Select the Northwind database from the drop-down list of databases, and to ensure everything is set up correctly, click the Test Connection button. If everything is set up properly, you should see a message box with a confirmation message.

Visual Studio 2005 has numerous changes when accessing data, and these are available from several places in the user interface. I prefer to use the new Data menu, which permits you to view any data sources already added to the project, add a new data source, and preview data from the underlying database (or other data source).

The following example uses the Northwind database connection to generate a user interface for selecting data from the Employees table. The first step is to choose Add New Data Source from the Data menu, which begins a wizard that walks you through the process. You can also configure the data sources for your application with the new Data Sources window shown in Figure 24-21 which is available from the Data menu in the IDE.

image from book
Figure 24-21

As you progress through the wizard you can choose the data source, which can be a database, local database file (such as an .mdb file), a WebService, or an object. You'll then be prompted for further information based on the type of data source chosen. For a database connection this includes the name of the connection (which is subsequently stored in the application configuration file shown in the following code), and you can then select the table, view, or stored procedure that supplies the data. Ultimately, this generates a strongly typed DataSet within your application.

 <?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <add name="SimpleApp.Properties.Settings.NorthwindConnection" connectionString="Server=.;Integrated Security=True;Database=Northwind" providerName="System.Data.SqlClient" /> </connectionStrings> </configuration> 

This includes the name of the connection, the connection string itself, and a provider name, which is used when generating the connection object. You can manually edit this information as necessary. To display a user interface for the employee data, you can simply drag the chosen data from the Data Sources window onto your form. This will generate one of two styles of User Interface for you — a grid style UI that utilizes the DataGridView control described earlier, or a Details view that presents just the data for a single record at a time. Figure 24-22 shows the Details view.

image from book
Figure 24-22

Dragging the data source onto the form generates a number of objects, both visual and non-visual. The non-visual objects are created within the tray area of the form, and comprise a DataConnector, a strongly typed DataSet, and a TableAdapter, which contains the SQL used to select/update the data. The visual objects created depend on whether you have chosen the DataGridView or the Details view. Both include a DataNavigator control that can be used to page through the data. Figure 24-23 shows the user interface generated using the DataGridView control — one of the goals of Visual Studio 2005 was to simplify data access to the point where you could generate functional forms without writing a single line of code.

image from book
Figure 24-23

When the data source is created, it adds a number of files to your solution — to view these click the Show All Files button in the Solution Explorer. You'll then be able to expand the dataset node and view the extra files added. The main one of interest is the .Designer file, which includes the C# source code used to populate the dataset.

You'll find two classes and one interface in the .Designer file. The interface exposes methods to select, insert, update, and delete data. The classes represent the strongly typed dataset and an object that implements the defined interface, which acts in a similar way to the standard DataAdapter class. This class internally uses the DataAdapter to fill the DataSet.

Selecting Data

The table adapter generated contains commands for SELECT, INSERT, UPDATE, and DELETE. Needless to say, these can (and probably should) be tailored to call stored procedures rather than using straight SQL. The wizard-generated code will do for now, however. Visual Studio .NET adds the following code to the.Designer file:

 private System.Data.SqlClient.SqlCommand m_DeleteCommand; private System.Data.SqlClient.SqlCommand m_InsertCommand; private System.Data.SqlClient.SqlCommand m_UpdateCommand; private System.Data.SqlClient.SqlDataAdapter m_adapter; 

An object is defined for each of the SQL commands with the exception of the Select command, and also a SqlDataAdapter. Further down the file, in the InitializeComponent() method, the wizard has generated code to create each one of these commands as well as the data adapter. Strangely, the code emitted to set up the DataAdapter doesn't use the commands defined within this class. Presumably, this and the omission of the Select command are beta issues that will be resolved before the software is formally released.

In previous versions of Visual Studio .NET, the commands generated for Insert and Update also included a select clause — this was used as a way to resynchronize the data with that on the server, just in case any fields within the database were calculated (such as identity columns and/or computed fields). Visual Studio 2005 only generates the Insert/Update clauses, so you may need to manually resynchronize the data if appropriate.

I think that generation of an additional Select clause wasn't necessary in most circumstances, so I am glad that the product group has altered this feature.

The wizard-generated code works but is less than optimal. For a production system, all the generated SQL should probably be replaced with calls to stored procedures. If the INSERT or UPDATE clauses didn't have to resynchronize the data, the removal of the redundant SQL clause would speed up the application a little.

Updating the Data Source

So far, the applications have selected data from the database. This section discusses how to persist changes to the database. If you followed the steps in the previous section, you should have an application that contains everything needed for a rudimentary application. The one change necessary is to enable the Save button on the generated toolbar and write an event handler that will update the database.

From the IDE, select the Save button from the data navigator control, and change the Enabled property to true. Then double-click the button to generate an event handler. Within this handler, save the changes made onscreen to the database:

 private void dataNavigatorSaveItem_Click(object sender, EventArgs e) { employeesTableAdapter.Update(employeesDataset.Employees); } 

Because Visual Studio 2005 has done the donkeywork for you, all that's needed is to use the Update method of the table adapter class that was generated. Six Update methods are available on the table adapter — this example uses the override that takes a DataTable as the parameter.

Building a Schema

Chapter 19 showed you how to define an XSD schema. Visual Studio .NET includes an editor for creating XSD schemas, which you can access by choosing Add New Item from the Project menu and selecting the XML Schema item (see Figure 24-24). Name your schema TestSchema.xsd.

image from book
Figure 24-24

This adds two new files to the project: an .xsd file and a corresponding .xsx file (which is used by the designer to store layout information for the schema elements that are designed). To create a corresponding set of code for the schema, choose the Generate Dataset option from the Schema menu (see Figure 24-25).

image from book
Figure 24-25

Choosing this option adds an extra C# file to the project, which is displayed below the XSD file in Solution Explorer. This file is automatically generated whenever changes are made to the XSD schema and should not be edited manually; it is generated with the xsd.exe tool.

The Visual Studio .NET editor has two views of an XSD file: Schema view and XML view. Clicking the XML tab displays the raw schema template:

 <?xml version="1.0" encoding="utf-8" ?> <xs:schema   targetNamespace="http://tempuri.org/TestSchema.xsd" elementFormDefault="qualified" xmlns="http://tempuri.org/TestSchema.xsd" xmlns:mstns="http://tempuri.org/TestSchema.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema"> </xs:schema> 

This XSD script generates the following C# in the file TestSchema.cs. In the following code some of the bodies of the methods have been omitted and/or formatted for easier reading; you can inspect the code generated as you work through the example:

 using System; using System.Data; using System.Xml; using System.Runtime.Serialization; [Serializable()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Diagnostics.DebuggerStepThrough()] [System.ComponentModel.ToolboxItem(true)] [System.Xml.Serialization.XmlSchemaProviderAttribute("GetTypedDataSetSchema")] [System.Xml.Serialization.XmlRootAttribute("TestSchema")] public class TestSchema : DataSet { public TestSchema() { ... } protected TestSchema(SerializationInfo info, StreamingContext context) { ... } public override DataSet Clone() { ... } protected override bool ShouldSerializeTables() { ... } protected override bool ShouldSerializeRelations() { ... } protected override void ReadXmlSerializable(XmlReader reader) { ... } protected override System.Xml.Schema.XmlSchema GetSchemaSerializable()  { ... } internal void InitVars() { ... } private void InitClass() { ... } private void SchemaChanged(object sender,  System.ComponentModel.CollectionChangeEventArgs e) { ... } } 

This code provides the starting point for this section, so that the code changes can be described as items are added into the XSD schema. The two main things to note are that an XSD schema is mapped to a DataSet, and that this DataSet is serializable — note the protected constructor that can be used by an ISerializable implementation. Serialization is covered in greater depth in Chapter 21, "Manipulating XML."

Adding an element

To add a new top-level element, right-click inside your workspace and choose Add New Element from the context menu. This displays in a new, unnamed element. Figure 24-26 shows the attributes for this example's product element.

image from book
Figure 24-26

When the XSD file is saved, the C# file is modified and a number of new classes are generated, as shown in the following code. The most pertinent aspects of the code generated in the file TestSchema. Designer.cs are discussed in the next section.

 public class TestSchema : DataSet { private ProductDataTable  tableProduct ; [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] public ProductDataTable Product { get { return this.tableProduct ; } } } 

A new member variable of the class ProductDataTable is created. This object is returned by the Product property and is constructed within the updated InitClass() method. From this small section of code, it's evident that the user of these classes can construct a DataSet from the class in this file and use DataSet.Products to return the products DataTable.

Generated DataTable

The following code is generated for the DataTable (Product) that was added to the schema template:

 public delegate void ProductRowChangeEventHandler (object sender, ProductRowChangeEvent e); public class ProductDataTable : DataTable, System.Collections.IEnumerable { internal ProductDataTable() : base("Product") { this.InitClass(); } [System.ComponentModel.Browsable(false)] public int Count { get { return this.Rows.Count;} } public ProductRow this[int index] { get { return ((ProductRow)(this.Rows[index]));} } public event ProductRowChangeEventHandler ProductRowChanged; public event ProductRowChangeEventHandler ProductRowChanging; public event ProductRowChangeEventHandler ProductRowDeleted; public event ProductRowChangeEventHandler ProductRowDeleting; 

The generated ProductDataTable class is derived from DataTable, and includes an implementation of the IEnumerable interface. Four events are defined that use the delegate defined above the class when raised. This delegate is passed an instance of the ProductRowChangeEvent class, again defined by Visual Studio .NET.

The generated code includes a class derived from DataRow, which permits type-safe access to columns within the table. A new row can be created in one of two ways:

  • Call the NewRow() (or generated NewProductRow()) method to return a new instance of the row class. Pass this new row to the Rows.Add() method (or the type-safe AddProductRow()).

  • Call the Rows.Add() (or generated AddProductRow()) method and pass an array of objects, one for each column in the table.

The following code demonstrates the AddProductRow() methods:

 public void AddProductRow(ProductRow row) { this.Rows.Add(row); } public ProductRow AddProductRow ( string Name, string SKU, string Description,  decimal Price ) { ProductRow rowProductRow = ((ProductRow)(this.NewRow())); rowProductRow.ItemArray = new Object[0] { Name, SKU, Description, Price};   this.Rows.Add(rowProductRow); return rowProductRow; } 

As you can see, the second method creates a new row, inserts that row in the Rows collection of the DataTable, and then returns this object to the caller. The bulk of the other methods on the DataTable are for raising events.

Generated DataRow

The following code shows the ProductRow class:

 public class ProductRow : DataRow { private ProductDataTable tableProduct; internal ProductRow(DataRowBuilder rb) : base(rb) { this.tableProduct = ((ProductDataTable)(this.Table)); } public string Name { ... } // Other accessors/mutators omitted for clarity } 

When attributes are added to an element, a property is added to the generated DataRow class as shown in the preceding code. The property has the same name as the attribute; in this example, the Product row has properties for Name, SKU, Description, and Price.

For each attribute added, several changes are made to the .cs file. In the following example, suppose there is an attribute called ProductId of type int.

At first a private member is added to the ProductDataTable class (derived from DataTable), which is the new DataColumn:

 private DataColumn columnProductId; 

This is joined by a property named ProductIDColumn. This property is defined as internal:

 internal DataColumn ProductIdColumn { get { return this.columnProductId; } } 

The AddProductRow() method shown previously is also modified; it now takes an integer ProductID and stores the value entered in the newly created column:

 public ProductRow AddProductRow ( ... , int ProductId) { ProductRow rowProductRow = ((ProductRow)(this.NewRow())); rowProductRow.ItemArray = new Object[] { ... , ProductId}; this.Rows.Add(rowProductRow); return rowProductRow; } 

Finally, in the ProductDataTable, there is a modification to the InitClass() method:

 private void InitClass() { ... this.columnProductID = new DataColumn("ProductID", typeof(int), null,  System.Data.MappingType.Attribute); this.Columns.Add(this.columnProductID); this.columnProductID.Namespace = ""; } 

This creates the new DataColumn and adds it to the Columns collection of the DataTable. The final parameter to the DataColumn constructor defines how this column is mapped to XML; this is of use when the DataSet is saved to an XML file, for example.

The ProductRow class is updated to add an accessor for this column:

 public int ProductId { get { return ((int)(this[this.tableProduct.ProductIdColumn])); } set { this[this.tableProduct.ProductIdColumn] = value; } } 

Generated EventArgs

The final class added to the source code is a derivation of EventArgs, which provides methods for directly accessing the row that has changed (or is changing), and for the action that is applied to that row. This code has been omitted for brevity.

Other Common Requirements

A common requirement when displaying data is to provide a pop-up menu for a given row. You can do this in numerous ways. The example in this section focuses on one approach that can simplify the code required, especially if the display context is a DataGrid, where a DataSet with some relations is displayed. The problem here is that the context menu depends on the row that is selected, and that row could be part of any source DataTable in the DataSet.

Because the context menu functionality is likely to be general-purpose in nature, the implementation here uses a base class (ContextDataRow) that supports the menu-building code, and each data row class that supports a pop-up menu derives from this base class.

When the user right-clicks any part of a row in the DataGrid, the row is looked up to check if it derives from ContextDataRow, and if so, PopupMenu()can be called. This could be implemented using an interface; however, in this instance a base class provides a simpler solution.

This example demonstrates how to generate DataRow and DataTable classes that can be used to provide type-safe access to data in much the same way as the previous XSD sample. However, this time you write the code yourself to show how to use custom attributes and reflection in this context.

Figure 24-27 illustrates the class hierarchy for this example.

image from book
Figure 24-27

Here is the code for this example:

 using System; using System.Windows.Forms; using System.Data; using System.Data.SqlClient; using System.Reflection; public class ContextDataRow : DataRow { public ContextDataRow(DataRowBuilder builder) : base(builder) { } public void PopupMenu(System.Windows.Forms.Control parent, int x, int y) { // Use reflection to get the list of popup menu commands MemberInfo[] members = this.GetType().FindMembers (MemberTypes.Method, BindingFlags.Public | BindingFlags.Instance , new System.Reflection.MemberFilter(Filter), null); if (members.Length > 0) { // Create a context menu ContextMenu menu = new ContextMenu(); // Now loop through those members and generate the popup menu // Note the cast to MethodInfo in the foreach foreach (MethodInfo meth in members) { // Get the caption for the operation from the // ContextMenuAttribute ContextMenuAttribute[] ctx = (ContextMenuAttribute[]) meth.GetCustomAttributes(typeof(ContextMenuAttribute), true); MenuCommand callback = new MenuCommand(this, meth); MenuItem item = new MenuItem(ctx[0].Caption, new EventHandler(callback.Execute)); item.DefaultItem = ctx[0].Default; menu.MenuItems.Add(item); } System.Drawing.Point pt = new System.Drawing.Point(x,y); menu.Show(parent, pt); } } private bool Filter(MemberInfo member, object criteria) { bool bInclude = false; // Cast MemberInfo to MethodInfo MethodInfo meth = member as MethodInfo; if (meth != null) { if (meth.ReturnType == typeof(void)) { ParameterInfo[] parms = meth.GetParameters(); if (parms.Length == 0) { // Lastly check if there is a ContextMenuAttribute on the // method... object[] atts = meth.GetCustomAttributes (typeof(ContextMenuAttribute), true); bInclude = (atts.Length == 1); } } } return bInclude; } } 

The ContextDataRow class is derived from DataRow, and contains just two member functions: PopupMenu and Filter(). PopupMenu uses reflection to look for methods that correspond to a particular signature, and it displays a pop-up menu of these options to the user. Filter() is used as a delegate by PopupMenu when enumerating methods. It simply returns true if the member function does correspond to the appropriate calling convention:

 MemberInfo[] members = this.GetType().FindMembers(MemberTypes.Method, BindingFlags.Public | BindingFlags.Instance, new System.Reflection.MemberFilter(Filter), null); 

This single statement is used to filter all methods on the current object and return only those that match the following criteria:

  • The member must be a method

  • The member must be a public instance method

  • The member must return void

  • The member must accept zero parameters

  • The member must include the ContextMenuAttribute

The last of these criteria refers to a custom attribute, written specifically for this example. (It's discussed after discussing the PopupMenu method.)

 ContextMenu menu = new ContextMenu(); foreach (MethodInfo meth in members) { // ... Add the menu item } System.Drawing.Point pt = new System.Drawing.Point(x,y); menu.Show(parent, pt); 

A context menu instance is created, and a pop-up menu item is added for each method that matches the preceding criteria. The menu is subsequently displayed as shown in Figure 24-28.

image from book
Figure 24-28

The main area of difficulty with this example is the following section of code, repeated once for each member function to be displayed on the pop-up menu:

 System.Type ctxtype = typeof(ContextMenuAttribute); ContextMenuAttribute[] ctx = (ContextMenuAttribute[])  meth.GetCustomAttributes(ctxtype, true); MenuCommand callback = new MenuCommand(this, meth); MenuItem item = new MenuItem(ctx[0].Caption,  new EventHandler(callback.Execute)); item.DefaultItem = ctx[0].Default; menu.MenuItems.Add(item); 

Each method that should be displayed on the context menu is attributed with the ContextMenuAttribute. This defines a user-friendly name for the menu option, because a C# method name cannot include spaces, and it's wise to use real English on pop-up menus rather than some internal code. The attribute is retrieved from the method, and a new menu item is created and added to the menu items collection of the pop-up menu.

This sample code also shows the use of a simplified Command class (a common design pattern). The MenuCommand class used in this instance is triggered by the user choosing an item on the context menu, and it forwards the call to the receiver of the method — in this case the object and method that was attributed. This also helps keep the code in the receiver object more isolated from the user interface code. This code is explained in the following sections.

Manufactured tables and rows

The XSD example earlier in the chapter showed the code produced when the Visual Studio .NET editor is used to generate a set of data access classes. The following class shows the required methods for a DataTable, which are fairly minimal (and they all have been generated manually):

 public class CustomerTable : DataTable  { public CustomerTable() : base("Customers") { this.Columns.Add("CustomerID", typeof(string)); this.Columns.Add("CompanyName", typeof(string)); this.Columns.Add("ContactName", typeof(string)); } protected override System.Type GetRowType() { return typeof(CustomerRow); } protected override DataRow NewRowFromBuilder(DataRowBuilder builder) { return(DataRow) new CustomerRow(builder); } } 

The first prerequisite of a DataTable is to override the GetRowType() method. This is used by the .NET internals when generating new rows for the table. The type used to represent each row should be returned from this method.

The next prerequisite is to implement NewRowFromBuilder(), which is called by the runtime when creating new rows for the table. That's enough for a minimal implementation. The corresponding CustomerRow class is fairly simple. It implements properties for each of the columns within the row, and then implements the methods that ultimately are displayed on the context menu:

 public class CustomerRow : ContextDataRow { public CustomerRow(DataRowBuilder builder) : base(builder) { } public string CustomerID { get { return (string)this["CustomerID"];} set { this["CustomerID"] = value;} } // Other properties omitted for clarity [ContextMenu("Blacklist Customer")] public void Blacklist() { // Do something } [ContextMenu("Get Contact",Default=true)] public void GetContact() { // Do something else } } 

The class simply derives from ContextDataRow, including the appropriate getter/setter methods on properties that are named the same as each field, and then a set of methods may be added that are used when reflecting on the class:

 [ContextMenu("Blacklist Customer")] public void Blacklist() { // Do something } 

Each method that is to be displayed on the context menu has the same signature and includes the custom ContextMenu attribute.

Using an attribute

The idea behind writing the ContextMenu attribute is to be able to supply a free text name for a given menu option. The following example also adds a Default flag, which is used to indicate the default menu choice. The entire attribute class is presented here:

 [AttributeUsage(AttributeTargets.Method,AllowMultiple=false,Inherited=true)] public class ContextMenuAttribute : System.Attribute { public ContextMenuAttribute(string caption) { Caption = caption; Default = false; } public readonly string Caption; } 

The AttributeUsage attribute on the class marks ContextMenuAttribute as only being usable on a method, and it also defines that there can only be one instance of this object on any given method. The Inherited=true clause defines whether the attribute can be placed on a superclass method and still reflected on by a subclass.

A number of other members could be added to this attribute, including the following:

  • A hotkey for the menu option

  • An image to be displayed

  • Some text to be displayed in the toolbar as the mouse pointer rolls over the menu option

  • A help context ID

Dispatching methods

When a menu is displayed in .NET, each menu option is linked to the processing code for that option by means of a delegate. In implementing the mechanism for connecting menu choices to code, you have two options:

  • Implement a method with the same signature as the System.EventHandler. This is defined as shown in this snippet:

     public delegate void EventHandler(object sender, EventArgs e); 

  • Define a proxy class, which implements the preceding delegate, and forwards calls to the received class. This is known as the Command pattern and is what has been chosen for this example.

The Command pattern separates the sender and the receiver of the call by means of a simple intermediate class. This may be overkill for such an example, but it makes the methods on each DataRow simpler (because they don't need the parameters passed to the delegate), and it is more extensible:

 public class MenuCommand { public MenuCommand(object receiver, MethodInfo method) { Receiver = receiver; Method = method; } public void Execute(object sender, EventArgs e) { Method.Invoke(Receiver, new object[] {} ); } public readonly object Receiver; public readonly MethodInfo Method; } 

The class simply provides an EventHandler delegate (the Execute method), which invokes the desired method on the receiver object. This example handles two different types of row: rows from the Customers table and rows from the Orders table. Naturally, the processing options for each of these types of data are likely to differ. Figure 24-28 shows the operations available for a Customer row, whereas Figure 24-29 shows the options available for an Order row.

image from book
Figure 24-29

Getting the selected row

The last piece of the puzzle for this example is how to work out which row within the DataSet the user has selected. You might think that it must be a property on the DataGrid. However, this control is not available in this context. The hit test information obtained from within the MouseUp() event handler might also be a likely candidate to look at, but that only helps if the data displayed is from a single DataTable.

Remember how the grid is filled:

 dataGrid.SetDataBinding(ds,"Customers"); 

This method adds a new CurrencyManager to the BindingContext, which represents the current DataTable and the DataSet. Now, the DataGrid has two properties, DataSource and DataMember, which are set when the SetDataBinding() is called. DataSource in this instance refers to a DataSet and DataMember are Customers.

Given the data source, a data member, and the binding context of the form, the current row can be located with the following code:

 protected void dataGrid_MouseUp(object sender, MouseEventArgs e) { // Perform a hit test if(e.Button == MouseButtons.Right) { // Find which row the user clicked on, if any DataGrid.HitTestInfo hti = dataGrid.HitTest(e.X, e.Y); // Check if the user hit a cell if(hti.Type == DataGrid.HitTestType.Cell) { // Find the DataRow that corresponds to the cell //the user has clicked upon 

After calling dataGrid.HitTest() to calculate where the user has clicked the mouse, the Binding ManagerBase instance for the data grid is retrieved:

 BindingManagerBase bmb = this.BindingContext[ dataGrid.DataSource, dataGrid.DataMember]; 

This uses the DataGrid's DataSource and DataMember to name the object to be returned. All that is left now is to find the row the user clicked and display the context menu. With a right-click on a row, the current row indicator doesn't normally move, but that's not good enough. The row indicator should be moved and then the pop-up menu should be displayed. The HitTestInfo object includes the row number, so the BindingManagerBase object's current position can be changed as follows:

 bmb.Position = hti.Row; 

This changes the cell indicator, and at the same time means that when a call is made into the class to get the Row, the current row is returned, not the previous one selected:

 DataRowView drv = bmb.Current as DataRowView; if(drv != null) { ContextDataRow ctx = drv.Row as ContextDataRow; if(ctx != null) ctx.PopupMenu(dataGrid,e.X,e.Y); } } } } 

Because the DataGrid is displaying items from a DataSet, the Current object within the Binding ManagerBase collection is a DataRowView, which is tested by an explicit cast in the previous code. If this succeeds, the actual row that the DataRowView wraps can be retrieved by performing another cast to check if it is indeed a ContextDataRow, and finally pop up a menu.

In this example, you'll notice that two data tables, Customers and Orders, have been created, and a relationship has been defined between these tables, so that when users click CustomerOrders they see a filtered list of orders. When the user clicks, the DataGrid changes the DataMember from Customers to Customers.CustomerOrders, which just so happens to be the correct object that the BindingContext indexer uses to retrieve the data being shown.




Professional C# 2005
Pro Visual C++ 2005 for C# Developers
ISBN: 1590596080
EAN: 2147483647
Year: 2005
Pages: 351
Authors: Dean C. Wills

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net