Chapter 12. Data Binding with Windows Forms Controls

Topics in This Chapter

  • DataBinding Overview: Associating data with a control is easy on the surface; however, it is important to understand what's going on underneath. This section provides an overview of simple and complex data binding, one-way and two-way data binding, and the role of the Binding and BindingManagerBase classes.

  • A Data Binding Application: A Windows Forms application illustrates how to bind data in a DataSet and DataTable to simple and complex controls.

  • The DataGridView: The DataGridView introduced with .NET 2.0 has a rich set of features that enable it to display and manipulate relational data. Examples illustrate how to create a master-detail grid and a virtual mode grid.

Chapter 11, “ADO.NET.” discussed how to access data using ADO.NET. This chapter extends the discussion to describe the techniques by which data is “bound” to the Windows Forms controls that display data. Because all controls derive their data binding capabilities from the base Control class, knowledge of its properties and methods can be applied to all controls. Although many of the same concepts apply to Web controls, data binding for them is discussed separately in Chapter 16, “ASP.NET Web Forms and Controls.”

Data binding comes in two flavors: simple and complex. Controls that contain one value, such as a label or Textbox, rely on simple binding. Controls populated with rows of data, such as a ListBox, DataGrid, or DataGridView, require complex binding. We'll look at how both are implemented.

Of the Windows Forms controls that bind to data, the DataGridView is the most complex and useful. Its layout maps directly to the rows and columns of a relational database or similarly structured XML document. This chapter takes a detailed look at the properties and methods of this control, and provides examples of how this control can be used to implement common database applications.

Overview of Data Binding

Data binding provides a way to link the contents of a control with an underlying data source. The advantage to this linkage or “binding” is that changes to the immediate data source can be reflected automatically in data controls bound to it, and changes in the data control are posted automatically to the intermediate data source. The term intermediate data source is used to distinguish it from the original data source, which may be an external database. The controls cannot be bound directly to a data source over an active connection. Binding is restricted to the in-memory representation of the data. Figure 12-1 shows the basic components of the binding model: the original data source, the intermediate storage, and the Form controls that are bound to values in the local storage through a binding object. Let's examine the model in more detail.

Multiple controls bound to a single data source

Figure 12-1. Multiple controls bound to a single data source

Simple Data Binding

Simple data binding, which is available to all controls, links a data source to one or more properties of a control. A good example is the Textbox control that exposes easily recognizable properties such as Text, Width, and BackColor. An application can set these dynamically by binding them to a data source. Here is a code segment that creates an object whose public properties are mapped to the properties on the TextBox.

// Create object (width, text, color)
TextParms tp = new TextParms(200, "Casablanca", Color.Beige);
// Bind text and BackColor properties of control
txtMovie.DataBindings.Add("Text", tp, "Tb_Text");
txtMovie.DataBindings.Add("BackColor", tp, "Tb_Background");

// Or create binding and then add in two steps
Binding binding = new Binding("Width", tp, "Tb_Width");
txtMovie.DataBindings.Add(binding);

The DataBindings.Add method creates a collection of bindings that links the data source to the control's properties. The method's syntax is

DataBindings.Add( control property, data source, data member)

control property

Property on the control that is being bound.

data source

Object that contains data being bound to control.

data member

Data member on the data source that is being used. Set this to null if the data source's ToString() method provides the value.

A control may have multiple bindings associated with it, but only one per property. This means that the code used to create a binding can be executed only once; a second attempt would generate an exception. To avoid this, each call to add a binding should be preceded with code that checks to see if a binding already exists; if there is a binding, it should be removed.

if (txtMovie.DataBindings["Text"] != null) 
    txtMovie.DataBindings.Remove(txtMovie.DataBindings["Text"]);
txtMovie.DataBindings.Add("Text", tp, "Tb_Text");

Binding to a List

The true value of data binding becomes obvious when the data source contains multiple items to be displayed. In the preceding example, the control was bound to a single object. Let's now create an array of these objects—each representing a different movie. Instead of binding to a single object, the control is bound to the array (see Figure 12-2). The control can still only display a single movie title at a time, but we can scroll through the array and display a different title that corresponds to the current array item selected. This scrolling is accomplished using a binding manager, which is discussed shortly.

Binding TextBox properties to objects in a list

Figure 12-2. Binding TextBox properties to objects in a list

This example creates an ArrayList of objects that are used to set the TextBox properties on the fly.

ArrayList tbList = new ArrayList();
// Beige color indicated movie won oscar as best picture
tbList.Add(new TextParms(200,"Casablanca",Color.Beige));
tbList.Add(new TextParms(200, "Citizen Kane", Color.White));
tbList.Add(new TextParms(200, "King Kong", Color.White));
// Bind to properties on the Textbox
txtMovie.DataBindings.Add("Text", tbList, "Tb_Text");
txtMovie.DataBindings.Add("BackColor", tbList, 
                          "Tb_Background");
txtMovie.DataBindings.Add("Width", tbList, "Tb_Width");

The one difference in the bindings from the preceding example is that the data source now refers to the ArrayList. By default, the TextBox takes the values associated with the first item in the array. When the index of the array points to the second row, the displayed value changes to “Citizen Kane”.

Simple Binding with ADO.NET

Binding to a table in a DataSet is basically the same as binding to a list. In this example, the Text property of the control is bound to the movie_Year column in a DataTable.

ds = new DataSet("films");
string sql = "select * from movies order by movie_Year";
da = new SqlDataAdapter(sql, conn);
da.Fill(ds,"movies");      // create datatable "movies"
// Bind text property to movie_Year column in movies table
txtYr.DataBindings.Add("Text", ds,"movies.movie_Year");

Although the control could be bound directly to a DataTable, the recommended approach is to bind the property to a DataSet and use the DataTable name as a qualifier to specify the column that provides the data. This makes it clear which table the value is coming from.

Complex Data Binding with List Controls

Complex binding is only available on controls that include properties to specify a data source and data members on the data source. This select group of controls is limited to the ListBox, CheckedListBox, ComboBox, DataGrid, and DataGridView. Complex binding allows each control to bind to a collection of data—the data source must support the IList interface—and display multiple items at once. Because the DataGridView is discussed at length in the last half of this chapter, let's look at how complex binding is implemented on the ListBox control. The details also apply to other List controls.

Binding a list control to a data source requires setting a minimum of two properties: DataSource, which specifies the source, and DisplayMember, which describes the member—usually a data column or property—in the data source that is displayed in the control. This code segment illustrates how a ListBox bound to a DataSet displays movie titles:

da.Fill(ds,"movies");
DataTable dt = ds.Tables[0]; 
// Minimum properties to bind listbox to a DataTable
listBox1.DataSource = ds;
listBox1.DisplayMember = "movies.movie_Title";

// Optional property that assigns a value to each item row
listBox1.ValueMember = "movies.movie_ID";

After these values are set, the list box is automatically filled. The DataSource property can be changed programmatically to fill the control with a different set of data, or it can be set to null to clear the control's content. Note also that although no Binding object is explicitly created, a DataBindings collection is created underneath and is accessible through code.

The bound list box control is often grouped with other controls, such as a text box or label, in order to display multiple values from a row of data. When the controls are bound to the same data source, scrolling through the list box causes each control to display a value from the same data row. To illustrate, let's add the following simple bindings to the preceding code:

txtStudio.DataBindings.Add("Text", ds,"movies.studio");
txtYear.DataBindings.Add("Text", ds,"movies.movie_Year");

These text boxes display the studio name and year of the movie currently selected in the list box (see Figure 12-3).

Using data binding to populate controls on a form

Figure 12-3. Using data binding to populate controls on a form

One-Way and Two-Way Data Binding

The data bound to a control can be changed in two ways: by updating the underlying data source, such as adding a row to a table, or by modifying the visible contents of the control. In both cases, the changes should be reflected in the associated control or data source—a process referred to as two-way data binding. In general, that is what happens. However, a control may be bound to a data source in read-only mode when its only purpose is to present data. To understand how these techniques are implemented, let's look at how updating occurs—from the perspective of the control and the data source.

Effects on the Data Source of Updating a Control Value

By default, changes made to data in a control are also made to the underlying in-memory data source. If the year value in Figure 12-3 is changed, the value in the corresponding row and column of the DataTable is also changed. Note that if the year is represented as an integer in the table, the value entered in the control must be an integer value. Data binding automatically checks types and rejects values (keeps the same value in the control) that do not match the type of the underlying data.

In the case where a control is bound to a property on an object, the property must provide write support in order for its value to be updated. For example, if the year and studio list boxes in the preceding example were bound to the following properties, respectively, only year could be updated; changes made to the studio control would be ignored and it would revert to its original value.

public int Movie_Year { set { myYear = value;  }
                        get { return myYear;   } }

// Read only property. Control cannot update this.
public string Studio  { get { return myStudio; } }

Note that changes in a control are not propagated to the data source until the user moves to another item in the GUI control. Underneath, this changes the current position within the binding manager—firing an event that causes the data to be updated.

Effects on a Control of Updating the Data Source

When a DataSet is used as the data source for controls, any additions, deletions, or changes made to the data are automatically reflected in the associated bound control(s). Custom data sources require some programming assistance to accomplish this.

If a control is bound to an object property, a change to the value of that property is not automatically sent to the control. Instead, the binding manager looks for an event named propertyChanged on the data source. If found, it provides a handler for this event to receive notice when that property's value changes. To enable the binding manager to handle a changed value, you must define a propertyChanged event on the data source class, and fire the event when a change occurs. To illustrate, let's extend the previous example to add the event to the class containing the Movie_Year property, and add code to fire the event when the property changes.

// Event to notify bound control that value has changed
public event EventHandler Movie_YearChanged;

// Property control is bound to year value
public int Movie_Year {
   set {
          myYear = value;
          // Notify bound control(s) of change
          if (Movie_YearChanged != null) 
             Movie_YearChanged(this, EventArgs.Empty);
   }
   get { return myYear; }
}

The other situation to handle is when a data item is deleted from or added to the data source. Controls that are bound to the source using simple binding are updated automatically; controls using complex binding are not. In the latter case, the update can be forced by executing the Refresh method of a CurrencyManager object. As we see next, the CurrencyManager is a binding manager especially designed for list data sources.

Using Binding Managers

As illustrated in Figure 12-4, each data source has a binding manager that keeps track of all connections to it. When the data source is updated, the binding manager is responsible for synchronizing the values in all controls bound to the data. Conversely, if a value is changed on one of the bound controls, the manager updates the source data accordingly. A binding manager is associated with only one data source. Thus, if an application has controls bound to multiple data sources, each will have its own manager.

Binding managers synchronize the data source and controls

Figure 12-4. Binding managers synchronize the data source and controls

Binding requires the interaction of several objects to coordinate the two-way flow of data between a data source and control. Let's look at the four most important objects, which are denoted by numbers in Figure 12-4.

  1. BindingMaintains a simple binding between a property on a control and a property on a single object. The following statements demonstrate how to create a binding and gain access to it:

    txtYr.DataBindings.Add("Text", ds, 
                           "movies.movie_Year");
    Binding binding = txtYr.DataBindings["Text"];
    // "txtYr"
    MessageBox.Show(binding.Control.ToString()); 
    // Create a binding manager object
    BindingManagerBase mgr= binding.BindingManagerBase;
    
  2. CurrencyManagerThis class derives from the abstract BindingManagerBase class and serves as a binding manager for list data sources such as a DataTable or Array. This object provides five members that an application can use to manage the relationship between a data source and control:

    • BindingsReturns the collection of bindings being managed.

    • CountThe number of rows in the list that is being managed.

    • CurrentReturns the current item (such as a row) in the data source as an object.

    • PositionGets/sets the position in the data source currently indexed by the control.

    • PositionChangedFires when the Position in the list changes.

    • CurrentChangedIs triggered when the bound value changes.

  3. PropertyManagerThis class, which also derives from BindingManagerBase, maps the properties on an object to properties on a bound control.

  4. BindingContextObserve in Figure 12-4 that the BindingContext is linked to a form and a collection of BindingManagerBase objects. Its job is to manage a collection of binding managers for a specific control—in this case, a Form. The control could just as well be a Panel or GroupBox on a form. A program's main interest in the BindingContext is to use it to gain access to the binding manager for a data source. These statements, for example, return the manager for the table movies.

    BindingManagerBase mgr = this.BindingContext[ds,"movies"];
    // Or use casting to get specific manager.
    CurrencyManager mgr= (CurrencyManager)
                          this.BindingContext[ds,"movies"];
    

Using the BindingManagerBase to Navigate a List

Let's now look at how the members of the BindingManagerBase class are used to move through the items in a source data list and simultaneously update the contents of controls bound to the list. The example binds a list box and text box to the familiar movies data table.

// Bind listbox to a dataset.datatable
listBox1.DataSource = ds;
listBox1.DisplayMember = "movies.movie_Title";
// Bind to TextBox
txtStudio.DataBindings.Add("text", ds, "movies.studio");
// BindingManagerBase bmb has class-wide scope 
bmb = this.BindingContext[ds, "movies"];
// Create delegate pointing to event handler 
bmb.PositionChanged += new 
      EventHandler(bmb_PositionChanged);

The following method moves to the next item in the data source list when a button is clicked. It would typically be paired with a method to move backward in the list.

// This method moves to the next row in the table. 
// If at the end of the table it moves to the beginning.
private void Forward_Click (object sender, EventArgs e)
{
   if (listBox1.Items.Count > 0)
   {
      bmb.Position = bmb.Position >= bmb.Count - 1 ? 0 : 
             ++bmb.Position;
   }
}

The PositionChanged event is fired each time the binding manager moves to a new position in the list. This could be triggered programmatically or by the user clicking a row in the list box control.

private void bmb_PositionChanged(object sender,
                                 EventArgs e)
{
   BindingManagerBase bmb = (BindingManagerBase)sender;
   // Item should be a DataRowView if from a table
   object ob = bmb.Current.GetType();
   if (ob == typeof(System.Data.DataRowView))
   {
      DataRowView view = (DataRowView)bmb.Current;
      // Could access: ((string)view["movie_Title"]);
   }
}

Note that the Current property is used to return an object representing the current item. The data source in this example is a data table, but the object returned is not the expected DataRow—it is a DataRowView object. It is up to the code to provide the proper casting to access properties in the selected item.

Using Simple and Complex Data Binding in an Application

Several concepts were introduced in the first section. Let's bring them together in an application that relies on data binding to display and update information from the Films database. Figure 12-5 shows a screen shot of the application's user interface.

Application combining complex and simple binding

Figure 12-5. Application combining complex and simple binding

Each control on the Windows Form—except buttons—is bound to a data source. A ListBox and ComboBox illustrate complex binding; two text boxes, a CheckBox, and a PictureBox, are bound using simple binding. The controls can be bound dynamically to either a data table or an array that contains custom objects. The Scroll button moves down the list box by internally using a binding manager to advance to the next item in the data source list. Let's dissect the program by looking at code associated with the buttons. Much of the code should be familiar from code segments in the previous section and does not require further explanation.

Binding to a DataTable

The code in Listing 12-1 is executed when the Bind to Table button is clicked. It loads the necessary data from the Films database into a table and binds the controls on the form to it. This populates the ListBox and ComboBox with a list of movie titles. The value in the other controls is derived from the content of the current row (highlighted in the list box). The most interesting of these is the PictureBox, which has its BackgroundImage property bound to a column in the table containing images. Because the database does not contain images, the program adds this column to the data table and fills it with images for the movie in each row.

Example 12-1. Binding Controls to a DataSet

// Bind control to data from a database
private void btnTableBind_Click(object sender, EventArgs e)
{
   SqlConnection conn = new SqlConnection(GetString());
   conn.Open();
   ds = new DataSet("films");
   string sql = "SELECT movie_ID, movie_title, movie_year, 
         studio, afi_rank, CASE WHEN bestpicture ='Y' 
         THEN 1 ELSE 0 END as BestPicture FROM movies ORDER BY 
         movie_Year";
   da = new SqlDataAdapter(sql, conn);
   // Command builder keeps track of changes to data
   SqlCommandBuilder sb = new SqlCommandBuilder(da);
   da.Fill(ds,"movies");
   DataTable dt = ds.Tables[0];
   Data Column dCol = new DataColumn("movie_Image",
         Type.GetType("System.Object"));
   dt.Columns.Add(dCol);
   // Place image in new column. Name is based on movie ranking.
   Image defaultImage = Image.FromFile(@"c:defaultimg.jpg");
   foreach (DataRow dRow in dt.Rows)
   {
      string rank = ((int)dRow["afi_rank"]).ToString();
      string imgFile = "c:\afi" + rank + ".gif";
      try
      {
         Image imgObject = Image.FromFile(imgFile);
         dRow["movie_Image"] = imgObject; 
      }
      catch (Exception ex)
      {
         dRow["movie_Image"] = defaultImage;
      }
   }
   // Nothing to this point should be considered a change
   dt.AcceptChanges();
   // Bind listbox and combobox to datasource
   listBox1.DataSource = ds;
   listBox1.DisplayMember = "movies.movie_Title";
   listBox1.ValueMember = "movies.movie_ID";
   comboBox1.DataSource = ds;
   comboBox1.DisplayMember = "movies.movie_Title";
   // Binding manager has global scope
   bmb = this.BindingContext[ds, "movies"];
   bmb.PositionChanged += new
         EventHandler(bmb_PositionChanged);
   try
   {
      // TextBox.Text  – binds to studio name
      if(txtStudio.DataBindings["text"] != null)
            txtStudio.DataBindings.Remove(  
               txtStudio.DataBindings["Text"]);
      txtStudio.DataBindings.Add("text", ds, "movies.studio");

      // TextBox.Text – binds to year movie released
      if(txtYear.DataBindings["text"] != null)
            txtYear.DataBindings.Remove(
                  txtYear.DataBindings["Text"]);
      txtYear.DataBindings.Add("text", ds, 
                               "movies.movie_year");

      // CheckBox.Checked - binds to best picture value (0 or 1)
      if (checkBox1.DataBindings["Checked"] != null)
            checkBox1.DataBindings.Remove( 
               checkBox1.DataBindings["Checked"]);
      checkBox1.DataBindings.Add("Checked", ds, 
            "movies.BestPicture");

      // PictureBox.BackgroundImage – Binds to image
      if (pictureBox1.DataBindings["BackgroundImage"] != null)
            pictureBox1.DataBindings.Remove(
               pictureBox1.DataBindings["BackgroundImage"]);
      pictureBox1.DataBindings.Add("BackgroundImage", ds, 
            "movies.movie_Image");
   }
   catch (Exception ex)
   {
      MessageBox.Show(ex.Message);
   }
}

Binding Controls to an ArrayList

Clicking the Bind to Array button, binds the controls to an ArrayList that is filled with instances of the custom class MyMovie (see Listing 12-2). After the data source is created, the binding process is identical to that followed with the data set.

Example 12-2. Binding Controls to an Array of Objects

// Bind control to array populated with instances of custom class
private void BindToArray()
{
   movieList = new ArrayList();
   Image movieImg = Image.FromFile(@"c:defaultimg.jpg");
   // Create objects and add to array
   movieList.Add(new MyMovie("2","Casablanca",1942,
         "Warner Bros.",true, Image.FromFile("c:afi2.gif")));
   movieList.Add(new MyMovie("1","Citizen Kane", 1941, 
         "RKO", false, 
         Image.FromFile("c:afi1.gif")));
   movieList.Add(new MyMovie("4","Gone with the Wind", 1941, 
         "Selznick International", true, 
         Image.FromFile("c:afi4.gif")));
   //
   listBox1.DataSource =  movieList;
   listBox1.DisplayMember = "Movie_Title";
   //
   comboBox1.DataSource = movieList;
   comboBox1.DisplayMember = "Movie_Title";
   bmb = this.BindingContext[movieList]; ;
   bmb.PositionChanged += new
         EventHandler(bmb_PositionChanged);
   if (txtStudio.DataBindings["Text"] != null)
         txtStudio.DataBindings.Remove(
               txtStudio.DataBindings["Text"]);
   txtStudio.DataBindings.Add("Text", movieList, "Studio");
   //
   if (txtYear.DataBindings["Text"] != null)
         txtYear.DataBindings.Remove(
               txtYear.DataBindings["Text"]);
   txtYear.DataBindings.Add("Text", movieList, "Movie_Year");
   //
   if (checkBox1.DataBindings["Checked"] != null) 
         checkBox1.DataBindings.Remove(
            checkBox1.DataBindings["Checked"]);
   checkBox1.DataBindings.Add("Checked", movieList, 
                              "BestPicture");
   //
   if (pictureBox1.DataBindings["BackgroundImage"] != null)
         pictureBox1.DataBindings.Remove(
            pictureBox1.DataBindings["BackgroundImage"]);
   pictureBox1.DataBindings.Add("BackgroundImage", movieList, 
         "Movie_Image");
}

When designing a custom class to be used as a data source, the primary consideration is whether the bindable properties provide read-only or read/write access. If they are read-only, the only requirement is that they be public. For properties that can be updated, the class must expose and fire an event to which the binding can subscribe. Recall that the name of this event is propertynameChanged. This event is fired in the Set block of the property (see Listing 12-3).

Example 12-3. Custom Data Source Class

// Bind control to array populated with instances of 
// custom class
public class MyMovie
{
   private string myID;
   private string myTitle;
   private int myYear;
   private string myStudio;
   private bool myBestPicture;
   private Image myImage;
   // 
   public event EventHandler Movie_YearChanged;
   public event EventHandler StudioChanged;
   public MyMovie(string id, string title, int year, 
                  string studio, 
         bool bp, Image img)
   {
      myTitle = title;
      myYear = year;
      myStudio = studio;
      myBestPicture = bp;
      myImage = img;
      myID = id;
   }
   // Only public properties can be bound to control
   public string Movie_Title  { get { return myTitle; } }
   // Make read/write so update can occur
   public int Movie_Year {
      get { return myYear; }
      set { 
         myYear = value;
         if (Movie_YearChanged != null) 
             Movie_YearChanged(this, EventArgs.Empty);
      }
   }

   public string Studio {
      get { return myStudio; }
      set {
         myStudio = value;
         if (StudioChanged != null) StudioChanged(this, 
             EventArgs.Empty);
      }
   }
   public Image  Movie_Image  { get { return myImage; } }
   public bool   BestPicture  { get { return myBestPicture; } }
}

Adding an Item to the Data Source

Clicking the Add Movie button causes information about a single movie to be added to the data source (see Listing 12-4). If the source is a table, a row is added; if an array, an object is created and inserted. An addition to a data table is automatically pushed to the control and made visible. When a custom object is added, the Refresh method of the CurrencyManager must be executed to synchronize the control. Note that Refresh is specific to the CurrencyManager class and not available on BindingManagerBase.

Example 12-4. Add an Item to a Data Source

// Test effects of adding a new item to the data source
private void button2_Click(object sender, EventArgs e)
{
   if (ds != null)
   {
      // Add a row to the table
      DataTable dt = ds.Tables[0];
      DataRow dRow = dt.NewRow();
      dRow["movie_ID"] = 99;
      dRow["movie_Title"] = "Rear Window";
      dRow["movie_Year"] = "1954";
      dRow["studio"] = "Paramount";
      dRow["BestPicture"] = 0;
      dRow["afi_rank"] = 42;
      Image defaultImage = Image.FromFile(@"c:afi42.gif");
      dRow["movie_Image"] = defaultImage;
      dt.Rows.Add(dRow);
   }
   else
   {
      Image movieImg = Image.FromFile(@"c:afi42.gif");
      movieList.Add(new MyMovie("42", "Rear Window", 1954,
            "Paramount", false, movieImg));
      // Refresh() is needed to display item in ListBox/ComboBox
      CurrencyManager cm =
             (CurrencyManager)this.BindingContext[movieList];
      cm.Refresh();
   }
}

Identifying Updates

The rows in a table have a RowState property that can be used to determine if a value in the row has been changed (discussed in Chapter 11). This method checks the value of that property for each row in the data source table. If the value is DataRowState.Modified, each column in the row is checked to determine which values have changed (see Listing 12-5). This routine can be used to determine whether an update to the original database is necessary. Observe that the method checks only for data changes. You can easily extend it to check for deletions and additions.

Example 12-5. Check Data Source for Any Updates

// Checks status of each row in data table to identify any 
// changes. This works only when data source is a Data Table.
private bool DataIsDirty(DataTable dt){
   bool result = false;
   foreach(DataRow drw in dt.Rows){
      // Check all rows in the table for a modified state
      if(drw.RowState == DataRowState.Modified) 
      {
         string msg = (string)drw["movie_Title"]+":";
         string curr;
         string orig;
         // Examine each column in the row for a change
         foreach(DataColumn col in dt.Columns)
         {
            curr= drw[col, 
               DataRowVersion.Current].ToString().Trim();
            orig= drw[col, 
               DataRowVersion.Original].ToString().Trim();
            if(!curr.Equals(orig) || curr != orig || 
               string.CompareOrdinal(curr,orig) !=0) 
            {
               msg += "
" + orig + " " + curr;
               result=true;
            }
         }
         MessageBox.Show(msg);   // Display changes in a row
      }
   }
   return result;
}

Update Original Database with Changes

When the modifiable data source is a data table, the Update method of its associated DataAdapter can be used to flush changes to the database. This topic is discussed in detail in Section 11.4, “DataSets, DataTables, and the Disconnected Model.”

try
{
   int updates = da.Update(ds, "movies");
   MessageBox.Show("Updates: "+updates.ToString());
}
catch (Exception ex)
{
   MessageBox.Show(ex.Message);
}

The DataGridView Class

The DataGridView control, introduced with .NET 2.0, supersedes the DataGrid—which now exists primarily for legacy purposes. With more than a hundred properties and methods, the DataGridView is by far the most complex Windows Forms control for displaying data. Accordingly, it is also the most flexible. Styles that govern appearance can be applied on a cell-by-cell basis, by rows, by columns, or across all cells in the grid. Cells are not limited to text. They may contain a TextBox, Image, CheckBox, Link, or Button control.

Data binding is supported by the DataSource property, just as with the controls defined in the previous section. In addition, the DataGridView provides a unique virtual mode that permits it to handle more than 100,000 rows of data. DataGridView methods, events, and properties allow an application to easily manage the mapping between virtual and physical storage.

All of these features are discussed in this section. We'll look at selected properties and events along with code examples that illustrate their use.

Properties

Despite its myriad features, the DataGridView has an elegantly simple structure. As shown in Figure 12-6, in its most elemental form, it consists of column headers, row headers, and cells. To these, we can add the Columns and Rows collections that allow an application to access the grid by indexing a row or column. That is the foundation. Each property and event discussed in this section relates to one of these five classes.

Basic DataGridView elements

Figure 12-6. Basic DataGridView elements

The DataGridView class inherits many of its properties from the Control class; to these, it adds properties required to support its own special characteristics and behavior. The properties listed in Table 12-1 are primarily in this latter category. The list is not meant to be exhaustive; instead, it presents those properties you'll refer to most frequently when implementing a grid.

Table 12-1. Selected Properties of the DataGridView Class

Category

Property Name

Description

User functionality

AllowUserToAddRows
AllowUserToDeleteRows

Indicates whether a user may add/delete rows. Default: true.

 

AllowUserToOrderColumns

Indicates whether user can rearrange columns.

 

ColumnHeadersHeightResizable

Indicates whether the user can change the height of the column headers. Default: true.

 

MultiSelect

Indicates whether user may select one or more rows at a time.

 

SelectionMode

Indicates the cells selected when clicking any individual or header cell.

enum DataGridViewSelectionMode values:

ColumnHeaderSelect
RowHeaderSelect
FullColumnSelect
FullRowSelect
CellSelect
 

ReadOnly

Indicates whether user can modify data in cells. Values: true or false.

Appearance

AlternatingRowsDefaultCellStyle

Gets or sets the default cell style applied to the odd numbered rows in the grid.

 

BackColor

The background color of the grid.

 

BackgroundColor

Gets or sets the background color for the area of the grid not containing data cells or column/row headers.

 

BorderStyle

Gets or sets the border style for the DataGridView.

enum BorderStyle values:

BorderStyle.Fixed3D
BorderStyle.FixedSingle
BorderStyle.None
 

CellBorderStyle

Gets or sets the border style used for cells in the grid.

enum DataGridViewCellBorderStyle values:

(to draw between rows)

SingleHorizontal
SunkenHorizontal
RaisedHorizontal

(to draw between columns)

SingleVertical
SunkenVertical
RaisedVertical

(to place a border between rows and columns)

SingleSunken
Raised
 

ColumnCount

Gets or sets the number of columns in the DataGridView.

 
ColumnHeadersBorderStyle
RowHeadersBorderStye

Border style applied to to column/row headers.

enum DataGridViewHeaderBorderStyle values:

Custom     Raised     Sunk
None       Single
 
ColumnHeadersVisible
RowHeadersVisible

Displays or suppresses headers. Values: true or false.

 

ColumnHeaderDefaultCellStyle

Defines cell style properties for column header cells.

 

DefaultCellStyle

DataGridViewCellStyle object that defines the default cell style properties for cells. Note that this includes column header cells.

 

FirstDisplayedCell

The first cell displayed in the grid, usually upper-left corner.

 

GridColor

The color of the lines separating the cells.

Collections

Columns

Collection of all grid columns. Individual columns are accessed by an index: Columns[iI].

 

Rows

Collection of all grid rows. Individual rows are accessed by an index: Rows[i].

 

SelectedColumns

Collection of columns selected.

 

SelectedRows

Collection of rows selected.

 

SelectedCells

Collection of cells selected.

Constructing a DataGridView

Listing 12-6 shows how to define columns for a DataGridView, set properties to define its appearance and behavior, and add rows of data. (We'll see in the succeeding example how to use the more common approach of loading data from a database.)

Note that the column header cells and data cells have different styles. If a style is not set for the header, it uses the same DefaultCellStyle as the data cells.

Example 12-6. Setting DataGridView Properties and Adding Rows of Data

// Set properties of a DataGridView and fill with data
private void CreateGrid()
{
   // (1) Define column headers
   dataGridView1.ColumnCount = 3;
   dataGridView1.Columns[0].HeaderText = "Movie Title";
   dataGridView1.Columns[1].HeaderText = "Year";
   dataGridView1.Columns[2].HeaderText = "Director";
   dataGridView1.Columns[1].Name  = "Year";
   dataGridView1.Columns[0].Width = 150;
   dataGridView1.Columns[1].Width = 40;
   dataGridView1.Columns[2].Width = 110;
   // (2) Define style for data cells
   DataGridViewCellStyle style = new DataGridViewCellStyle();
   style.BackColor = Color.Bisque;
   style.Font = new Font("Arial", 8, FontStyle.Bold);
   style.ForeColor = Color.Navy;
   //                         (left,top,right,bottom)
   style.Padding = new Padding(5, 2, 5, 5);
   style.SelectionBackColor = Color.LightBlue;
   dataGridView1.DefaultCellStyle = style;
   // (3) Define style for column headers
   DataGridViewCellStyle styleHdr = new 
         DataGridViewCellStyle();
   styleHdr.Padding = new Padding(1, 1, 1, 1);
   styleHdr.BackColor = Color.OldLace;
   styleHdr.ForeColor = Color.Black;
   dataGridView1.ColumnHeadersDefaultCellStyle = styleHdr;
   // (4) Define user capabilities
   dataGridView1.AllowUserToAddRows      = false;
   dataGridView1.AllowUserToOrderColumns = false;
   dataGridView1.AllowUserToDeleteRows   = false;
   // (5) Place data in grid manually (datasource is better)
   object[] row1 = {"Casablanca", "1942","Michael Curtiz"};
   dataGridView1.Rows.Add(row1);
   object[] row2 = {"Raging Bull","1980","Martin Scorsese"};
   dataGridView1.Rows.Add(row2);
   object[] row3 = {"On the Waterfront","1954","Elia Kazan"};
   dataGridView1.Rows.Add(row3);
   object[] row4 = {"Some Like it Hot","1959","Billy Wilder"};
   dataGridView1.Rows.Add(row4);
}

Figure 12-7 shows the DataGridView created by this code.

DataGridView built from code in Listing 12-6

Figure 12-7. DataGridView built from code in Listing 12-6

DataBinding with a DataGridView

A DataGridView is bound to a data source using complex binding. As in our list box example, the DataSource property specifies the data source. The similarity ends there, however, because a DataGridView must display multiple data values. To do so, the DataMember property is set to the name of a table within the data source. The data to be displayed in each column is specified by setting the column's DataPropertyName property to the name of the underlying data table column.

// Turn this off so column names do not come from data source
dataGridView1.AutoGenerateColumns = false;
// Specify table as data source
dataGridView1.DataSource = ds;        // Dataset
dataGridView1.DataMember = "movies";  // Table in dataset
// Tie the columns in the grid to column names in the data table
dataGridView1.Columns[0].DataPropertyName = "Title";
dataGridView1.Columns[1].DataPropertyName = "Year";
dataGridView1.Columns[2].DataPropertyName = "director";

The DataGridView supports two-way data binding for ADO.NET data sources: Changes made to the grid are reflected in the underlying table, and changes made to the table are reflected in the grid. For example, this code responds to a button click by adding a new row to the grid's data source. The addition is immediately reflected in the control. However, if we try to add a row directly to the DataGridView, an exception occurs because adding directly to a bound control is not permitted.

private void buttonAdd_Click(object sender, EventArgs e)
{
   // Adding to underlying table is okay
   r[0] = "TAXI";
   r[1] = "1976";
   r[2] = "Martin Scorsese";
   dt.Rows.Add(r);
   // Adding directly to DataGridView does not work
   object[] row = {"The Third Man", "1949", "Orson Welles"};
   DataRow r = dt.NewRow();
   DataGridView1.Rows.Add(row4);  // Fails!
}

Updating the original database from which a grid is loaded can be done by issuing individual SQL commands or using a DataAdapter. The discussion in the previous section applies.

Core Note

Core Note

A DataGridView may have a mixture of bound and non-bound columns. Thus, columns can be added to a bound control, but rows cannot.

Setting the Row Height

The default height of rows in a DataGridView is based on accommodating a single line of text. If the row contains large sized fonts or images, they are truncated. It is usually better to force the grid to take the size of each cell in the row into account and base the overall height on the tallest cell. That's the role of the grid's AutoSizeRows method. Its simplest overloaded version takes a single parameter—a DataGridViewAutoSizeRowsMode enumeration value—that indicates the criterion used for setting row height. The two most useful enumeration members are ColumnAllRows, which bases the row height on all columns in the row, and ColumnsDisplayedRows, which applies the same criterion, but to visible rows only.

dataGridView1.AutoSizeRows(
      DataGridViewAutoSizeRowsMode.ColumnsAllRows);

The AutoSizeRows method sets the row size when it is executed. If subsequent updates cause the height of cells in a row to change, the row height does not adjust to the changes. Also, if a row is sortable, clicking a column header to sort the grid causes all rows to revert to the default row height. Fortunately, the DataGridView has an AutoSizeRowsMode property that causes row heights to automatically adjust to changes in grid content.

dataGridView1.AutoSizeRowsMode =
      DataGridViewAutoSizeRowsMode.HeaderAndColumnsAllRows;

Note that this statement does not take effect until the AutoSizeRows method is executed, and that it prevents users from manually resizing rows.

Working with Columns and Column Types

The DataGridView is not a full-blown spreadsheet, but it does offer some features a user expects from a spreadsheet. These include the following:

  • Frozen Column(s). For a grid that requires horizontal scrolling, it is often useful to “freeze” columns so that they always remain on the screen. Setting a column's Frozen property to true has the effect of freezing it and all columns to its left.

     dataGridView1.Columns[0].Frozen = true;
    
  • ReadOnly Columns. Selected column can be made read-only.

     dataGridView1.Columns[2].ReadOnly = true;
    
  • Minimum Width. By default, a user can widen and narrow columns in a grid. The minimum size permitted for a column can be controlled by setting the MinimumWidth property to a value in pixels:

     dataGridView1.Columns[0].MinimumWidth=100;
    
  • Sorting. By default, clicking a column header sorts the rows based on values in that column—if the column contains sortable values. It's SortMode property can be used to disable sorting:

     dataGridView1.Columns[0].SortMode = 
                  DataGridViewColumnSortMode.NotSortable;
    
  • Multiple Column Types. Six predefined column classes are available that can be used to represent information in a grid, using the familiar formats of the TextBox, CheckBox, Image, Button, ComboBox, and Link. The name for each of these controls follows the format DataGridViewControlnameColumn.

    This code segment adds a column of buttons to a grid. The first step is to create an instance of the column class. Its characteristics and data values—if any—are then set. Finally, the Columns.Add method is used to add the column to the grid's column collection.

    // (1) Create instance of column type
    DataGridViewButtonColumn buttons = new 
          DataGridViewButtonColumn();
    // Text to place in column header
    buttons.HeaderText = "Delete";
    // (2) Set characteristics of control
    buttons.Text = "Delete";   // Default text for button
    buttons.FlatStyle = FlatStyle.Standard;
    // Create a datagridview cell to use as a template to set
    // all buttons in the column to the same style.
    buttons.CellTemplate = new DataGridViewButtonCell();
    buttons.CellTemplate.Style.BackColor = Color.Yellow ;
    buttons.CellTemplate.Style.Font = new Font("Arial", 8);
    // Specify column position on grid
    buttons.DisplayIndex = 1;
    // (3) Add column to grid
    dataGridView.Columns.Add(buttons);
    

Any of the column types may be bound to a data source. Although a button is usually set manually, it can be bound to a property in the grid's data source in two ways:

// Use the DataGridviewButtonColumn class
buttons.DataPropertyName = "Title";
// Use the Columns class (button is in column 1 of the grid)
dataGridView3.Columns[1].DataPropertyName = "Title";

Buttons provide a convenient way for a user to select a grid row and trigger an action such as a pop-up form that displays further information related to the row. Buttons located in grid cells, however, have no direct event, such as a Click, associated with them. Instead, events are associated with an action on the overall grid or specific cells on the grid. By identifying a cell for instance, an event handler can determine which button is clicked.

Events

Just about every mouse and cursor movement that can occur over a DataGridView can be detected by one of its events. In addition, events signify when data is changed, added, or deleted. Table 12-2 provides a summary of the most useful events. Accompanying the table is a list of the delegate used to implement these events. (See Appendix B for a complete list of events.)

Table 12-2. Selected DataGridView Events

Category

Event (Delegate)

Description

Cell actions

CellValueChanged (1)

Occurs when the value of a cell changes.

 

CurrentCellChanged (3)

Occurs when the value of the current cell changes

 

CellClick (1)

Occurs when any part of the cell is clicked. This includes cell borders and padding.

 

CellContentClick (1)

Occurs only if the cell content is clicked.

 

CellEnter (1)

CellLeave (1)

Occurs when cell receives/loses input focus.

 

CellFormatting (5)

Occurs prior to formatting a cell for display.

 

CellMouseClick (2)

CellMouseDoubleClick (2)

Occurs whenever a mouse clicks/double clicks anywhere on a cell.

 

CellMouseDown (2)

CellMouseUp (2)

Occurs when a mouse button is pressed/raised while it is over a cell

 

CellMouseEnter (1)

CellMouseLeave (1)

Occurs when the mouse pointer enters or leaves a cell's area.

 

CellPainting (6)

Raised when a cell is to be painted.

Column actions

ColumnHeaderMouseClick (2)

ColumnHeaderMouseDouble-Click (2)

Occurs when a column header is clicked/double clicked.

Row actions

RowEnter (1)

RowLeave (1)

Occurs when a row receives/loses the input focus.

 

RowHeaderMouseClick (2)

RowHeaderDoubleMouse-Click (2)

Occurs when a user clicks/double clicks a row header

 

UserAddedRow (4)

UserDeletedRow (4)

Occurs when a user adds/deletes a row in the grid.

Data error

DataError (7)

Occurs when an external data parsing or validation operations fails. Typically occurs due to an attempt to load invalid data into a data grid cell.

The following are delegates associated with events in Table 12-2:

(1) public sealed delegate void DataGridViewCellEventHandler(
      object sender, DataGridViewCellEventArgs e)

(2) public sealed delegate void DataGridViewCellM_useEventHandler(
      object sender, DataGridViewCellMouseEventArgs e)

(3) public sealed delegate void EventHandler(
      object sender, EventHandlerArgs e)

(4) public sealed delegate void DataGridViewRowEventHandler (
      object sender, DataGridViewRowEventArgs e)

(5) public sealed delegate void 
      DataGridViewCellFormattingEventHandler(
         object sender, DataGridViewCellFormattingEventArgs e)

(6) public sealed delegate void 
      DataGridViewCellPaintingEventHandler(
         object sender, DataGridViewCellPaintingEventArgs e)

(7) public sealed delegate void
      DataGridViewDataErrorEventHandler(
         object sender, DataGridViewDataErrorEventArgs e)

Let's look at some common uses for these events.

Cell Formatting

The CellFormatting event gives you the opportunity to format a cell before it is rendered. This comes in handy if you want to distinguish a subset of cells by some criteria. For example, the grid in Figure 12-7 contains a column indicating the year a movie was released. Let's change the background color of cells in that column to red if the year is less than 1950.

// Set cells in year column to red if year is less than 1950
private void Grid3_CellFormatting(object sender,  
      DataGridViewCellFormattingEventArgs e)
{
   if (this.dataGridView3.Columns[e.ColumnIndex].Name == "Year")
   {
      string yr = (string)e.Value;
      if (Int32.Parse(yr) < 1950)
      {
          e.CellStyle.ForeColor = Color.Red;
          e.CellStyle.SelectionForeColor = Color.Red;
          // Indicate that event was handled
          e.FormattingApplied = true;
      }
   }
}

The ColumnIndex property of the EventArgs parameter is used to determine if the year column is being formatted. If so, the code checks the year and formats the cell accordingly. Note that the FormattingApplied property must be set if custom formatting is performed.

Recognizing Selected Rows, Columns, and Cells

As shown in Table 12-2, selecting a cell in a grid can trigger any number of events that can be used to indicate the current cell or the cell just left. Some of the events are almost over-engineered. For example, there seems little to distinguish CellContentClick and CellClick. Others exist to recognize grid navigation using both the mouse and keyboard: The CellClick is not triggered by arrow keys; however, the CellEnter event is fired no matter how a cell is selected. All of these cell-related events have a consistent event handler signature. The EventArgs parameter provides column and row index properties to identify the cell. Here is an example:

private void Grid1_CellEnter(object sender,
      DataGridViewCellEventArgs e)
{
   // Both of these display the column index of the selected cell
   MessageBox.Show("enter "+e.ColumnIndex.ToString());
   MessageBox.Show( 
         DataGridView1.CurrentCell.ColumnIndex.ToString());
}

Core Note

Core Note

Although row and column header cells cannot become “current cells,” they are assigned a column and row index value. Row headers always have a column index of –1, and column headers have row index of –1.

The cell events can be used to recognize a single row and column selection. However, a grid may also permit multiple row, column, and cell selections. In these cases, it is necessary to use the SelectedRows, SelectedColumns, and SelectedCells collections to access the selected grid values.

Multiple row selection is made available on a DataGridView by setting its MultiSelect property to true—which is the default value. A row is selected by clicking its row header. It can also be selected by clicking any cell in the row if the grid's SelectionMode property is set to DataGridViewSelectionMode.FullRowSelect. The property can also be set to FullColumnSelect, which causes a cell's column to be selected. Note that column and row selection are mutually exclusive: only one can be in effect at a time.

This segment illustrates how to iterate through the collection of selected rows. The same approach is used for columns and cells.

// Display selected row numbers and content of its column 1
if (dataGridView1.SelectedRows.Count > 0)
{
   StringBuilder sb = new StringBuilder();
   for (int i = 0; i < dataGridView1.SelectedRows.Count; i++)
   {
      sb.Append("Row: ");    
      sb.Append(
            dataGridView1.SelectedRows[i].Index.ToString() );
      sb.Append( dataGridView1.SelectedRows[i].Cells[1].Value);
      sb.Append(Environment.NewLine);
   }
   MessageBox.Show (sb.ToString(), "Selected Rows");
}

Data Error Handling

The DataError event fires when a problem occurs loading data into a grid or posting data from the grid to the underlying data store. The error is quite easy to detect: compare the value of the Context property of the ErrorEventArgs parameter with the DataGridViewDataErrorContext enumeration values. Here is an example:

// Define event handler
DataGridView1.DataError += new  
      DataGridViewDataErrorEventHandler(DataGridView1_DataError);

// DataError Event Handler
private void dataGridView1_DataError(object sender,  
      DataGridViewDataErrorEventArgs dgError)
{
   // Context provides information about the grid when the
   // error occurred.
   MessageBox.Show("Error: " + dgError.Context.ToString());
   // Problem committing grid data to underlying data source
   if (dgError.Context == DataGridViewDataErrorContext.Commit)
   {
      MessageBox.Show("Commit error");
   }
   // Occurs when selection cursor moves to another cell
   if (dgError.Context == 
         DataGridViewDataErrorContext.CurrentCellChange)
   {
      MessageBox.Show("Cell change");
   }
   if (dgError.Context == 
         DataGridViewDataErrorContext.Parsing)
   {
      MessageBox.Show("parsing error");
   }
   // Could not format data coming from/going to data source
   if (dgError.Context == 
         DataGridViewDataErrorContext.Formatting)
   {
      MessageBox.Show("formatting error");
   }
}

Setting Up Master-Detail DataGridViews

One of the more common relationships between tables in a database is that of the master-detail relationship, where the records in the master table have multiple associated records in the detail table. DataGridViews provide a natural way of displaying this relationship. To illustrate, let's create an application based on the Films database that displays a master grid containing a list of movies and a detail grid that display actors who played in the movie selected in the first grid. To make it interesting, we'll include an image column in the movie grid that contains a picture of the Oscar statuette for movies that won for best picture.

The master grid is bound to the movies table; the details grid is bound to the actors table. Both tables, as shown in Figure 12-8, contain the columns that are bound to their respective DataGridView columns. In addition, they contain a movieID column that links the two in the master-detail relationship.

Master-detail tables

Figure 12-8. Master-detail tables

The tables and their relationships are created using the techniques described in Chapter 11:

ds = new DataSet();
DataTable dt = new DataTable("movies");  // Master
DataTable da = new DataTable("actors");  // Detail
da.Columns.Add("movieID");
da.Columns.Add("firstname");
da.Columns.Add("lastname");
//
dt.Columns.Add("movieID");
dt.Columns.Add("Title");
dt.Columns.Add("Year");
dt.Columns.Add("picture", typeof(Bitmap));  // To hold image
ds.Tables.Add(dt);
ds.Tables.Add(da);
// Define master-detail relationship
DataRelation rel = new DataRelation("movieactor", 
      dt.Columns["movieID"], da.Columns["movieID"]);
ds.Relations.Add(rel);

After defining the table schemas, they are populated from the database using a DataReader object. Because the database does not contain an image—although it could—the image is inserted based on the value of the bestPicture field.

Bitmap oscar   = new Bitmap(@"c:oscar.gif");   // Oscar image
Bitmap nooscar = new Bitmap(@"c:
ooscar.gif"); // Blank image
// Populate movies table from datareader
while (dr.Read())
{
   DataRow drow = dt.NewRow();
   drow["Title"] = (string)(dr["movie_Title"]);
   drow["Year"]  = ((int)dr["movie_Year"]).ToString();
   drow["movieID"] = (int)dr["movie_ID"];
   if ((string)dr["bestPicture"] == "Y") drow["picture"] = 
         oscar; else drow["picture"] = nooscar;
   dt.Rows.Add(drow);
}

The actors table is filled with the results of the query:

sql = "SELECT am.movie_ID, actor_first,actor_last FROM actors a
      JOIN actor_movie am ON a.actor_ID = am.actor_ID";

After the tables are created and populated, the final steps are to define the grids and bind their columns to the tables. This segment adds three columns to the master grid—one of which is an image type column.

DataGridViewImageColumn vic = new DataGridViewImageColumn();
dataGridView1.Columns.Add(vic);   // Add image type column
//
dataGridView1.ColumnCount = 3;
dataGridView1.Columns[0].Name = "Oscar";
dataGridView1.Columns[1].HeaderText = "Movie Title";
dataGridView1.Columns[2].HeaderText = "Year";

Then, the binding is performed:

// Bind grids to dataset
dataGridView1.DataSource = ds;
dataGridView1.DataMember = "movies";
dataGridView2.DataSource = ds;
// ***Set to DataRelation for detail 
dataGridView2.DataMember = dt.TableName+".movieactor";
// Bind grid columns to table columns
dataGridView1.Columns[0].DataPropertyName = "picture";
dataGridView1.Columns[1].DataPropertyName = "Title";
dataGridView1.Columns[2].DataPropertyName = "Year";
dataGridView1.Columns[3].DataPropertyName = "director"; 
dataGridView2.Columns[0].DataPropertyName = "firstname";
dataGridView2.Columns[1].DataPropertyName = "lastname";

Pay close attention to the binding of dataGridView2. It is bound to the relationship defined between the tables, rather than directly to the actors table. This binding causes the names of the movie's cast to be displayed in the grid when a movie is selected.

Figure 12-9 shows a sample screen. Much of the excluded code in this example deals with setting grid styles and capabilities. A full code listing is available in the book's code download. (See the Preface for the download URL addresses and instructions.)

Master-detail relationship

Figure 12-9. Master-detail relationship

Virtual Mode

When a DataGridView is bound to a data source, the entire data source must exist in memory. This enables quick refreshing of the control's cells as a user navigates from row to row. The downside is that a large data store may have prohibitive memory requirements. To handle excessive memory requirements, a DataGridView can be run in virtual mode by setting its VirtualMode property to true. In this mode, the application takes responsibility for maintaining an underlying data cache to handle the population, editing, and deletion of DataGridView cells based on actions of the user. The cache contains data for a selected portion of the grid. If a row in the grid cannot be satisfied from cache, the application must load the cache with the necessary data from the original data source. Figure 12-10 compares virtual storage with binding to a DataTable.

Data binding versus virtual mode

Figure 12-10. Data binding versus virtual mode

Virtual mode implementation requires that an application handle two special virtual mode events: CellValueNeeded, which occurs when a cell value must be displayed; and CellValuePushed, which occurs when a cell's value is edited. Other events are also required to manage the data cache. These are summarized in Table 12-3.

Table 12-3. DataGridView Events Used to Implement Virtual Mode

Event

Description

NewRowsNeeded

Virtual mode event. Occurs when a row is appended to the DataGridView.

CellValueNeeded

Virtual mode event. Occurs when cell in grid needs to be displayed.

CellValuePushed

Virtual mode event. Occurs when a cell value is edited by the user.

RowValidated

Occurs when another row is selected.

UserDeletingRow

Occurs when a row is selected and the Delete key is pressed.

To illustrate the fundamentals of implementing a DataGridView in virtual mode, let's look at the code used to create the DataGridView shown in Figure 12-11.

DataGridView using virtual mode

Figure 12-11. DataGridView using virtual mode

The variables having class scope are shown here. Note that the data cache is implemented as a generics List object that holds instances of the movie class. The movie class exposes three properties that are displayed on the grid: Title, Movie_Year, and Director.

DataGridView dgv;
List<movie> movieList = new List<movie>(20);  // cache
bool rowNeeded;   // True when new row appended to grid  
int storeRow = 0;
int currRow = -1; // Set to row being added
movie currMovie;  // Holds movie object for current row

Listing 12-7 shows the overhead code to initialize the DataGridView, register the event handlers, and populate the data cache (this would usually come from a database).

Example 12-7. Virtual DataGridView: Initialization

// Set properties of a DataGridView and fill with data
dgv = new DataGridView();
// Event handlers for virtual mode events
dgv.CellValueNeeded += new
      DataGridViewCellValueEventHandler(CellNeeded);
dgv.CellValuePushed += new 
      DataGridViewCellValueEventHandler(CellPushed);
dgv.NewRowNeeded += new 
      DataGridViewRowEventHandler(RowNeeded);
// Event handlers always available for DataGridView
dgv.UserDeletingRow += new 
      DataGridViewRowCancelEventHandler (RowDeleting);
dgv.RowValidated += new 
      DataGridViewCellEventHandler( RowValidated);
dgv.VirtualMode = true;
dgv.RowCount = 5;
dgv.ColumnCount = 3;
// Headers for columns
dgv.Columns[0].HeaderText = "title";
dgv.Columns[1].HeaderText = "year";
dgv.Columns[2].HeaderText = "director";
// Fill cache. In production, this would come from database.
movieList.Add(new movie("Citizen Kane",1941,"Orson Welles"));
movieList.Add(new movie("The Lady Eve",1941,"
                        "Preston Sturges"));
// ... Add other movies here

The heart of the application is represented by the event handler methods shown in Listing 12-8. To summarize them:

  • RowNeededIs triggered when the user begins to add a new row at the bottom of the grid. currRow is set to the row number of any row being added.

  • CellNeededIs triggered when a cell needs to be redrawn. This does not require that a row be selected, but occurs as you move the cursor over cells in the grid. This routine identifies the column the cell is in and displays the data from the cache or the object that is created for new rows. Note that the MapRow() is called to translate a row in the grid to its corresponding row in the cache. In this simple example, there is always a one-to-one relationship because the cache and grid contain the same number of rows. In a production application, row 5000 in a grid might map to row 1 in the cache.

  • CellPushedCalled when a cell value is edited. This routine updates a movie object that represents the selected row with the new value.

  • RowValidatedSignals that a different row has been selected and is used to update the previous row. If the row exists in the cache, it is updated; a new row is added to the cache.

  • RowDeletingCalled when user selects a row to delete. If the row exists in the cache, it is removed.

Example 12-8. Virtual DataGridView: Event Handlers

// Called when a new row is appended to grid
private void RowNeeded(object sender, 
                       DataGridViewRowEventArgs e)
{
   rowNeeded = true;
   currRow = dgv.Rows.Count - 1; 
}

// Called when a cell must be displayed/refreshed
private void CellNeeded(object sender, 
                        DataGridViewCellValueEventArgs e)
{
   if (rowNeeded)
   {
      rowNeeded = false;
      currMovie = new movie();
      return;
   } 
   storeRow = MapRow(e.RowIndex);
   if(storeRow >=0 && currRow  ==-1) 
         currMovie =  movieList[storeRow];
   string colName = dgv.Columns[e.ColumnIndex].HeaderText;
   if(storeRow>=0)  // Refresh cell from cache
   {
      if (colName == "title")e.Value =
            movieList[storeRow].Title;
      if (colName == "year") e.Value = 
            movieList[storeRow].Movie_Year.ToString();
      if (colName == "director") e.Value = 
            movieList[storeRow].Director;
   } else           // refresh cell from object for new row
   {
      if (colName == "title")e.Value = currMovie.Title;
      if (colName == "year")e.Value = 
            currMovie.Movie_Year.ToString();
      if (colName == "director") e.Value = currMovie.Director;
   }
}
// Cell has been updated
private void CellPushed(object sender, 
                        DataGridViewCellValueEventArgs e)
{
   // Update property on movie object for this row
   storeRow = MapRow(e.RowIndex);
   string colName = dgv.Columns[e.ColumnIndex].HeaderText;
   if (colName == "title") currMovie.Title = (string)e.Value;
   if (colName == "year")
   {
      int retval;
      if(int.TryParse((string)e.Value,out retval)) 
            currMovie.Movie_Year = retval;
   }
   if (colName == "director") currMovie.Director = 
         (string)e.Value;
} 
// Occurs when user changes current row
// Update previous row in cache when this occurs
private void RowValidated(object sender,
                          DataGridViewCellEventArgs e)
{
   storeRow = MapRow(e.RowIndex);
   if (storeRow < 0) storeRow = movieList.Count;
   currRow = -1;
   if (currMovie != null)
   {
      // Save the modified Customer object in the data store.
      storeRow = MapRow(e.RowIndex);
      if (storeRow >= 0)
            movieList[storeRow] = currMovie;
      else movieList.Add(currMovie);
         currMovie = null;
   }
}
// Row selected and Del key pushed
private void RowDeleting(object sender, 
                         DataGridViewRowCancelEventArgs e)
{
   if (MapRow(e.Row.Index)>=0)
   { movieList.RemoveAt(e.Row.Index); }
   if (e.Row.Index == currRow)
   {
      currRow = -1;
      currMovie = null;
   }
}
// Maps grid row to row in cache. More logic would be added
// for application that refreshes cache from database.
private int MapRow(int dgvRow)
{
   if (dgvRow < movieList.Count)return dgvRow;
      else return -1;
}

This example provides only the basic details for implementing a virtual DataGridView. The next step is to extend it to include a virtual memory manager that reloads the cache when data must be fetched from disk to display a cell.

Summary

Data binding is used to link the data displayed in a control with an underlying data source. In many cases, it can eliminate the manual code required to populate controls. There are two basic types of binding: simple and complex. Simple is used with controls that display only one value; complex is used to display multiple data values in selected controls such as a list box or data grid.

Each data source has a binding manager that keeps track of all connections to it. This manager is responsible for synchronizing values in the data store and controls bound to it. For list data sources such as an array or data table, the binding manager is a CurrencyManager object; for a property on an object, the binding manager is a PropertyManger object. Both of these objects expose methods that allow them to be used to navigate through their data source.

Of the data bound controls, the DataGridView offers the richest interface. It permits data to be displayed and manipulated in a grid format. Style classes and appearance properties enable almost all of its features to be customized. Its event members allow virtually any action involving the grid to be detected—from a click on a cell to the addition of a row. It also permits control types such as a button, image, or ComboBox to be inserted into any of its cells. Although data binding is typically used to populate a DataGridView, the control also supports a virtual mode that allows an application to manage the grid's content using a custom data cache.

Test Your Understanding

1:

Indicate whether the following are true or false:

  1. A TextBox supports complex binding.

  2. The width of a control can be bound to a data source.

  3. All controls support simple binding.

  4. A data source can only be bound to one control at a time.

  5. A data source may have multiple binding managers.

  6. Changes made to the value of a control are always propagated to the data source.

  7. Controls can be bound to custom data objects.

  8. The PropertyManager class inherits from the CurrencyManager class.

  9. Only public properties may be bound to a control.

2:

What is the difference between simple and complex binding? One-way and two-way binding?

3:

Describe how to allow a custom data source to support two-way binding.

4:

Which property and enumeration cause the entire row in a DataGridView to be highlighted when a single cell in the row is selected?

5:

Which of these cannot be included in a DataGridView cell?

  1. TextBox
    
  2. ListBox
    
  3. Button
    
  4. ComboBox
    

6:

How do you ensure that a column in a DataGridView is always displayed on the screen?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset