New developers often struggle with the .NET DataGrid when trying to replicate grid functionality from other platforms.

More experienced developers lament the deficiencies of the .NET DataGrid to address end user requirements. Consequently, many developers seek sophisticated third-party alternatives. In this article, I'll present a set of classes for the DataGrid to help address some of the more common struggles. Although third-party tools always offer more capabilities than a native control, this article also demonstrates how it's possible for you to implement some of the functions found in these third-party tools.

Beginning with the End in Mind

In any development endeavor, one of the key steps is to identify the requirements. I often use the phrase “Begin with the end in mind.” It's part of a philosophy of visualizing the end-result from 30,000 feet, as well as understanding all the details.

I had been using the grid class in ccControls for quite some time when realized that I was duplicating certain controls necessary for things like Find and Find Next.

This article provides some code samples to help ease the transition for those new to .NET. As developers, we sometimes seek a fast-track answer. “Just show me a code example, and I'll take it from there.”

Specifically, my objective is to demonstrate a set of classes in C# to cover some of the commonly-asked questions and requests concerning grids:

  • How to populate grid columns with minimal lines of code
  • How to implement drop-down lists with descriptions from different tables...and how to implement other controls in a grid column-again, with minimal code
  • How to implement Find/Find Next text-searches in a grid?for all columns or only for certain columns
  • How to turn your Find/Find Next search text into a filter
  • How to perform other types of filters on the contents of a grid
  • How to bookmark a specific row in a grid by providing a right-click pop-up, so that it can be retrieved later
  • How to read data from the current column in a grid, even if the grid has been sorted
  • How to use the binding manager for a grid
  • How to conditionally set the background color of an individual cell based on a condition
  • How to set multiple rows in a grid with code, and how to read which rows were selected by a user
  • How to turn on/off grid row highlighting
  • How to define a custom function for displaying the caption of a grid
  • How to save column sizing and other user preferences and retrieve them later

That's a total of thirteen productivity tips, a Baker's Dozen (inspired by the famous Saint Nicholas Cookies from Van Amsterdam's bakery).

Figure 1 shows a test form with a real world scenario that I'll use to demonstrate all of these capabilities.

Figure 1: This runtime form demonstrates some of the DataGrid capabilities.
Figure 1: This runtime form demonstrates some of the DataGrid capabilities.

A few of these capabilities use reflection, and I'll discuss this topic that also raises many questions. I'll also present a reusable function to deal with reflection. Along the way, I'll also touch briefly on other topics, such as inheritance and user controls.

The Development Project

Figure 2 shows the development solution I'll use to demonstrate grid functionality. It consists of the following four projects:

  • A test form (TestProjectDataGrid), along with a Form Manager
  • A library of class controls (ccControls), including the grid class (ccGrid)
  • A UserControl library (ccContainers), which houses the ccGrid class and tools for searching the grid
  • A library of reflection functions (ReflectionLibrary) to simplify the task of executing a function whose name is not known until runtime

For each section, I'll present a scenario where the productivity tip might be needed, how to use the grid and grid container classes to address the scenario, and then how the classes work.

Figure 2: The development project is underway.
Figure 2: The development project is underway.

Now, on to the baker's dozen!

Tips 1 and 2: Getting Started

I began this project by creating a custom grid class, and added additional properties and methods to support the functionality discussed. I also sub-classed other basic controls (TextBox, Label, CheckedListBox) that I'll use in this project. All of these can be found in ccControls.cs. Table 1 lists all of the properties and methods used by the grid class.

The new grid class makes life a little simpler when it comes to defining columns.

I had been using the grid class in ccControls for quite some time when realized that I was duplicating certain controls necessary for things like Find and Find Next. Any time I find myself doing a copy/paste, my internal control radar goes off! So I created a UserControl class that contained my custom grid control and all the controls necessary for the other operations that I'll discuss later. This UserControl is called ccGridContainer and is found in ccContainers.cs.

After building ccControls and ccContainers as DLLs, I added ccContainers to my Visual Studio .NET IDE Toolbox, and then created a new Windows Forms project. I then dropped an instance of ccGridContainer onto my form, and changed the name of the control from ccGridContainer1 to oGridCtr. Next, to get an object reference directly to the grid control itself, I added the following bit of code:

ccControls.ccGrid oGrid = oGridCtr.oGrid;

Of course, you can't get very far without creating some test data. I didn't want to complicate matters by introducing a data access layer into this example, so I'll create a method to produce some test data and assume that in a production environment, this data would be returned as a result set from a business object working with a data component.

In the example, an example from a basic accounting application, I present the user with a list of construction jobs for a set of customers. The construction table (DtConstrJobs) contains the following:

  • Job ID
  • Customer #
  • Job Date
  • Job Class
  • Is the job complete?
  • Original Bid Amount, and Amount Paid
  • Balance Due

There will be two support tables: a customer account table (DtAccounts) with account descriptions, and a job class table (DtClsCode) with class code descriptions. The code to create this test data and define data relations can be found in the CreateTestData function in the main test form.

Finally, I want to define the grid columns. Normally, to define an individual column, you need to do something like this:

DataGridTextBoxColumn Fld1;
Fld1 = new DataGridTextBoxColumn(); 
Fld1.Alignment = HorizontalAlignment.Left;
Fld1.HeaderText = "Job #";
Fld1.MappingName = "ConstrID";
Fld1.ReadOnly = true;
Fld1.Width = 190;

Fortunately, the new grid class makes life a little simpler when it comes to defining columns. The grid class accomplishes the same thing in one function call:

oGrid.AddText("ConstrID", "Job #",100,
  "Left",true,"");

The parameters for AddTextColumn are:

  • Column name (mapping name)
  • Column Heading
  • Column Width
  • Column Alignment (Left, Center, or Right)
  • Read only (True or False)
  • Any special formatting (mainly for currency columns)

You might be saying, “OK, but what about other columns? I want to display an account name from a foreign table, and I also want to display the list of classes from my description table, yet have any changes map to the class code column of my job list table. I also want to show a checkbox for the completed flag.”

No worries, mate! There are similar functions for each of these. Table 2 lists all the functions to set Grid Columns for different column types. If you are defining a drop-down with a description coming from another table, you'll need to specify the foreign table and description. Similarly, if you're defining a text column with a description from a related table, you'll have to supply the name of the ADO.NET relationship. The sample project has an example of each of these. All functions to set grid columns require Mapping Name, Heading, Width, Alignment, and Formatting.

The SetGridColumns function in the test form contains an example of each call to define a grid column. As you add columns to the Grid class using these functions, the Grid class holds the column information in a DataTable named DtGridDef. The column names and a brief description for each can be found in the function InitializeGrid in ccControls.ccGrid. Any additional column definitions you'd like to set up can be added to this table.

Once you've made all the calls to SetGridColumns that you need, simply call the function ShowGrid, and you'll see the grid appear with the columns you defined!

Tips 3 and 4: Implementing Find/Find Next

When the application presents the user with a result DataSet in a grid, the data could be a handful of rows, or maybe a hundred, or maybe even more, depending on what condition was used to retrieve the data from the backend.

Many times, I look to Microsoft Word or Internet Explorer to get some ideas for user interface enhancements, and this was no exception.

Similar to Microsoft Word or other applications, the user may wish to locate a particular row by typing in search text, and clicking Find or Find Next if the first one wasn't the desired record. The user may also want to narrow the scope; maybe the row being searched is a job number that begins with 688, but the user doesn't want the software to return a match on other columns that might have 688 in them. Finally, the user may want to take the text search and turn it into a filter, perhaps to only show those jobs where the word LIBRARY appears in the account name.

The grid Container class has a text search and command buttons for the Find/Find Next filter, all in the upper left corner. All you need to do to make these visible is to set a property in the container and call ShowGridTools:

oGridCtr.lShowGridTools = chkUseGridTools.Checked;
oGridCtr.ShowGridTools(); 

If you don't wish to provide this functionality to users, set the property to False. The find and filter prompts disappear, and the grid automatically stretches vertically to cover the screen area.

Note the checkbox in the lower right corner of Figure 1, labeled Use Search and Bookmark Tools. This demonstrates the use of the property lShowGridTools, as described above.

(Note that when turning on Grid Tools, you'll also see a bookmark drop-down on the right hand side. I'll cover that a little later in Tip 6**.)**

As for how the Find and Find Next filter works, theGgrid class in ccControls.ccGrid contains a function called TextSearch. It takes two parameters: the text to search on, and a Boolean for whether to start at the top of the grid based on the current sort order (True), or whether to start at one row beyond the current row position in the grid (False).

The text searching is fairly basic. First, it starts at either the first row of the grid or the current position plus one (and I'll cover how it gets the current position when I cover the Binding Manager in Tip 8). Next, for each row, the search scans the columns from left to right, and checks whether or not the search text appears in the column.

cSearchText = cSearchText.ToUpper().Trim();

string cColValue;
cColValue = 
  this[nRowCtr,nColCtr].ToString().ToUpper();

if(cColValue.IndexOf(cSearchText,0)>=0) {
   lFound=true;
   break;
}
if(lFound==true) 
   this.Select(nRowCtr);

Note that the search automatically converts both sides to upper case, making the search case-insensitive. Again, it also searches for partial occurrences within a string, using IndexOf(). Finally, note the use of the grid UnSelect() function, to unhighlight the row that was highlighted right before the search began (so that you can highlight the row that matches the search with the Select() function).

The code snippet above was an excerpt from FindText, where all columns were searched. As I mentioned, you can search only certain columns, if a user specifically wants to exclude a column or column from being searched.

At the time you executed the ShowGrid method, the grid Container class scanned through the grid class definition table, DtGridDef, read all the column headings passed with AddText, AddDropDown, and so on, and populated a CheckedListBox control with those headings. I like the CheckedListBox control because it provides an easy and graphical way for users to select multiple items when the total list of items isn't too large. By default, all heading boxes are checked. If the user wants to exclude columns from a search, he can just uncheck that column's name in the list.

When the user clicks Find or Find Next, the grid Container class reads through each item in the CheckedListBox, finds the corresponding column definition in DtGridDef, and sets the SEARCHCOLUMN flag in DtGridDef to either True or False. (With a little bit of additional work, the table DtGridDef could have been bound to the CheckedListBox control).

At the time of the actual text search when you are looping through the grid columns, if the user has only selected certain columns, you can check whether or not the column was defined as searchable before actually trying the search. To do so, first identify the column name for the current column in the loop. You get this by tapping into the grid's GridColumnStyles array and then examining the mapping name:

DataGridColumnStyle oCol;
oCol =
 this.TableStyles[0].GridColumnStyles[nColCtr];
string cColumnName = oCol.MappingName.ToString();

Then look up the column name in DtGridDef and verify whether the SEARCH flag is set to True.

DataRow DrColumn;
DrColumn = this.DtGridDef.Rows.Find(cColumnName);
bool lSearch;
lSearch = 
  bool.Parse(DrColumn["Search"].ToString()); 
if(lSearch==true)  // continue with search
   if(cColValue.IndexOf(cSearchText,0)>=0)

Finally, you may wish to filter on all rows in the grid that match the text search. The Filter button on the grid container calls a function in the grid class called SetTextFilter**().** This function performs the same logic as the text search logic, except that for each row where a hit occurs, the function sets a flag in the grid's DataSource called FilterSelected to True. At the end, the function sets a RowFilter for the grid's DataSource to only show those rows where FilterSelected was set to True.

this.dvGrid.DefaultView[nRowCtr]
   ["filterselected"] = lFoundMatch;

this.dvGrid.DefaultView.RowFilter = 
   "FilterSelected = true";

Note the use of the property dvGrid. This is a custom property for the grid class. (All custom properties and methods are shown in Table 3 at the end of this article). Because the grid has no way of knowing the name of the DataSource, you use the custom property dvGrid to refer to the default DataView of the grid.

Tip 5: Other Filters

The example form I've provided doesn't explicitly have an example of a custom RowFilter. However, all you need to do is set the RowFilter property for the table's default view to any valid ADO.NET RowFilter Expression. For example, if I wanted to show all jobs that were incomplete with an amount greater than ten thousand dollars, all I need to do in the test form is execute the following:

string cRowFilter;
cRowFilter="Amount > 10000 AND Complete = false";
DtConstrJobs.DefaultView.RowFilter = cRowfilter;
oGridCtr.Refresh();

If you want to turn the filter off, just pass an empty string to the RowFilter property. Note that the columns specified in a RowFilter expression can only refer to columns in that DataTable. ADO.NET does not support filter conditions that span multiple related tables.

Tips 6 - 8: Defining a Shortcut Menu to Bookmark a Row from the Binding Manager

I was doing some development testing of a fairly large result set, and had to bounce back and forth between records. I found myself copying ID numbers from the grid to Notepad so that I could paste them back into a search later. Eventually, I thought to myself, “there's gotta be a better way!” Many times, I look to Microsoft Word or Internet Explorer to get some ideas for user interface enhancements, and this was no exception. In the same way that both products have a recent document list and bookmark capability (which you pretty much see in most applications these days, including Visual Studio .NET), I decided to implement bookmark capability.

To set up a bookmark, all you do is call the grid container function SetupBookMark, and pass the column name used in the bookmark display.

When a user right-clicks on the grid, a pop-up menu appears with options to bookmark the current row, and also to clear the bookmark list

To set up a bookmark, all you do is call the grid container function SetupBookMark, and pass the column name used in the bookmark display. (Note that only one column name can be passed; that's something I plan to expand very soon.)

oGridCtr.SetupBookMark("ConstrId");

And that's it! This sets up a property in the grid container (cColumnToBookMark) that will be used in a few minutes.

Now, when someone right-clicks, the current job ID can be bookmarked. It appears in the bookmark drop-down list in the upper right. When the user navigates to a different area of the result set, and then wants to return back to the ID that was bookmarked, selecting it from the drop-down list returns the user to that ID.

As far as how it's handled in the grid container class...first, you need to define a pop-up menu.

private ContextMenu popUpMenu;
private MenuItem MiAddBookMark;
private MenuItem MiClearBookMarks;

Next, you need to define an event handler to trap a right-click for the grid:

oGrid.MouseDown += new 
   MouseEventHandler(ccGridMouseDown);

In the handler (ccGridMouseDown), you need to check whether the right mouse button was clicked and if so, show the pop-up menu.

if(e.Button==MouseButtons.Right)
   {
   popUpMenu =new ContextMenu();
   popUpMenu.MenuItems.Add("Add To 
      BookMark",new EventHandler(popup));
   popUpMenu.MenuItems.Add("Clear 
      Bookmarks",new EventHandler(popup));

   this.ContextMenu = popUpMenu;
      MiAddBookMark=this.ContextMenu.MenuItems[0];
      MiClearBookMarks=
         this.ContextMenu.MenuItems[0];
   }

In the custom event popup, take the sender object, cast it to MenuItem, and examine the text. In this case, you want to do something if the text is Add To Bookmark: call the AddRowToBookMark function. (A nice little enhancement here would be to set up an interface, to avoid relying on menu text.)

The AddRowToBookMark function in the grid Container class calls a method in the Grid class called GetCurrentRow. GetCurrentRow returns a DataRow object for the current row position. The function respects the sort order by reading the DataRowView that corresponds to the current Binding Manager position. The Binding Manager is defined as a custom grid property, and is defined when the data source is first set.

this.bMgr = this.BindingContext[this.DataSource,
this.DataMember];
...
return ((DataRowView)this.bMgr.Current).Row;

I'll talk a little more about the Binding Manager at the end of this section.

So AddRowToBookMark takes the current row returned from GetCurrentRow, examines the value of the bookmark's Column property (cColumnToBookMark), and then checks to make sure the value hasn't already been bookmarked. If it hasn't, AddRowToBookMark adds the value to a collection, resorts the collection, and the repopulates the bookmark drop-down list.

Finally, RetrieveDataFromBookMark in the grid Container class (ccContainers.ccGridContainer) is called any time the user makes a selection from the drop-down list. This function simply takes the current value from the drop-down list and performs a lookup against the default view for the grid's DataSource.

DataView dv = oGrid.dvGrid;
for(int nCtr=0;nCtr<dv.Count;nCtr++)
if(dv[nCtr][this.cColumnToBookMark].ToString
  ()==cBookMarkValue) 
   {
   if(oGrid.bMgr.Position >= 0)
      oGrid.UnSelect(oGrid.bMgr.Position);
   oGrid.bMgr.Position = nCtr;
   oGrid.Select(nCtr);
   lFound = true;
   break;
   }

Because it's possible that a bookmarked entry may not exist in the current default view (perhaps the user refreshed the result set with a different condition or the current filter is such that a bookmark value is no longer part of the current view), the function generates a message indicating that the bookmark value could not be found.

Within your application, any time you want to pull back the current row to examine the contents (say, to get a particular string or dollar value from one of the columns), just call the GetCurrentRow function in ccGridContainer.

I mentioned a few paragraphs above that I'd talk about the Binding Manager. In twenty-five words or less, the Binding Manager maintains synchronization between Windows Form controls and the data bound to those controls. There has been plenty of coverage about the details of the Binding Manager (most recently, Dino Esposito wrote an excellent article about Windows Forms data binding in the September/October 2004 issue of this journal), so I won't repeat it.

The Binding Manager is a class derived from the Binding context of the control. Two common instances where you need to directly use the binding manager are when you need to get/set the row position of a grid, and when you want to raise an event when the row has changed (if the user scrolls to a different row, for instance). Quite often, you'll see the following:

BindingManagerBase bMgr = 
  this.BindingContext[this.DataSource, 
    this.DataMember];
int nPos = bMgr.Position;

The ccGrid class exposes a Binding Manager object as a custom property, to avoid having to repeatedly refer to the binding context:

int nPos = oGrid.bMgr.Position;

Then when you want to set up an event any time the position changes, you can define a custom event (like your own function, RowChange).

oGrid.bMgr.PositionChanged += new 
  System.EventHandler(RowChange);

In RowChange, you could take the current job ID from the current row, and perform a query, or anything else you need to do.

private void RowChange(object sender, EventArgs e)
{
oGrid.GetCurrentRow();
string cID = Dr["ConstrId"].ToString(); 
// now go do something with the ID
}

There are other uses for the Binding Manager in a data-driven application. Exposing the Binding Manager as a property simplifies the coding to explore them.

Tip 9: Highlighting a Cell Based on a Condition

A question I see frequently with regard to the .NET DataGrid is how to highlight a cell in a different color based on a runtime condition. I come from the FoxPro world, where a developer could make use of DynamicForeColor and DynamicBackColor to shade a cell based on an inline IF statement or a function.

For me, this capability was the coolest feature of the Visual FoxPro grid. The .NET DataGrid does not have native support for this. Some of the third-party DataGrids do, but each implementation of it still requires some code, and is not as simple as the Visual FoxPro approach. This is perhaps the most powerful feature of the grid class presented in this article.

In the example, I want to highlight the job balance in red if the balance is higher than a user-entered value at runtime. You need to:

  • Write a function that returns a True or False for whether the column should be highlighted. The function is called from within the Grid class, and the function receives three parameters. The three parameters are the DataRow object for the current row position, the current row position value, and the string value for the current cell.
  • Implement logic in the function to determine whether the current cell should be highlighted.
  • Place the function in a separate class from the user interface form.
  • Supply the name of the function to the grid, along with the class and assembly name in which it resides. The Grid class invokes the function through reflection to determine whether each cell should be highlighted.

Let's work our way backward. In the test form, there is a checkbox for whether to highlight a cell in a particular gradient fill if the balance exceeds the value specified in a textbox. If checked, the following call is made to the grid Container object:

Color clrBackFrom = Color.Red;
Color clrBackTo = Color.Blue;
Color clrForeFrom = Color.White;
Color clrForeTo = Color.Yellow;

Assembly aCurrAssembly;
aCurrAssembly = 
   Assembly.GetExecutingAssembly();
string cAssemblyName;
cAssemblyName = 
   aCurrAssembly.Location.ToString(); 

oGrid.ColorColumn("Balance",cAssemblyName, 
  "FormManager","CheckBalance",clrBackFrom, 
   clrBackTo, clrForeFrom, clrForeTo);

this.Refresh();

This forces the Grid class to call the CheckBalance function in the FormManager class, which is part of the current assembly. (If the function resides in a different assembly, the name of that assembly must be provided). CheckBalance is invoked every time the balance column is painted. (I'll cover the internals of that later).

Next, you need to create a FormManager class and include a CheckBalance function. The CheckBalance function reads the cell value passed to it, and compares it to a property that was bound to the balance threshold textbox column on this main form so that the balance function doesn't have to query the contents of the textbox.

Decimal nThreshold;
 
nThreshold =  
 Decimal.Parse(this.cBalance.ToString());
Decimal nCurrentCell;
nCurrentCell = Decimal.Parse(cValue);
if(nCurrentCell > nThreshold)
   return true;
else
   return false;

This specific example doesn't make use of the entire DataRow object passed to it. However, the IF test could be changed to see if the value of the current cell is more than half the value of the amount column. In that instance, the code reads:

Decimal nThreshold;
 
nThreshold =
 Decimal.Parse(Dr["amount"].ToString())
        * .5;
Decimal nCurrentCell;
nCurrentCell = Decimal.Parse(cValue);
if(nCurrentCell > nThreshold)
   return true;
else
   return false;

Finally, let's take a look at how this code is implemented. The CheckBalance function is invoked from within the DataGrid column's Paint function. This means creating a class derived from the native DataGridTextBoxColumn class, and overriding the Paint event. As the main function of the Paint event is to draw a string using a specific font, location, and brush object to determine the color and texture, you need to capture the string for the grid cell before it is drawn and override the default Brush with your own custom Brush if the function to be invoked (CheckBalance) returns True.

That's a mouthful! OK, let's break the custom Paint event down. First, you need to capture the object for the current cell and convert to a string:

object oCell;
string cCellValue;
oCell = this.GetColumnValueAtRow(source, rowNum);
cCellValue = oCell.ToString();

Then you need to look up the mapping name (column name) in DtGridDef to see whether a custom function was specified. If the Paint event is running for the Balance column, it will find that a function has been defined (CheckBalance). In that case, you need to run it.

You know the function name (as well as the class name and assembly to which it belongs), so you can utilize reflection to invoke the function. To make life a little easier, I've provided a function in the ReflectionLibrary called RunMethod (which I'll talk about at the end of this section). You need to pass a few things to this function, and you'll recall that any function used for this purpose receives the current data row, the row position, and the value of the current cell:

object[] args = new object[3];
DataRow dr=oGrid.dvGrid.DefaultView[rowNum].Row;
args[0] = dr;
args[1] = rowNum;
args[2] = cCellValue;

ReflectionLibrary.ReflectionLibrary oReflection = 
   new ReflectionLibrary.ReflectionLibrary();

lChangeAppearance = 
  (bool)oReflection.RunMethod(cFunctionAssembly, 
   cFunctionClass, cFunction, args);

Because the custom method could return many different data types, the generic reflection function RunMethod returns an object and you're responsible for casting it to the necessary data type. In this case, you want to see if CheckBalance returned True or False.

Finally, if the function returned True, you need to change the foreground and background color Brush. Then you can call FillRectangle and DrawString at the end, specifying either the default color Brush or the new color Brush.

backBrush = new LinearGradientBrush(bounds,
      clrBackFrom, clrBackTo,
   LinearGradientMode.BackwardDiagonal);
foreBrush = new LinearGradientBrush(bounds, 
   clrForeFrom, clrForeTo,
   LinearGradientMode.BackwardDiagonal);

g.FillRectangle(backBrush, bounds); 
g.DrawString(cCellValue, FontToUse,foreBrush, x ,
   bounds.Y);

(For more in-depth details on GDI+, check out the two-part article by Markus Egger in the May/June and July/August 2003 issues of CoDe Magazine).

OK, I promised to talk a little more about the reflection library and invoking functions. After I became more comfortable with reflection, I put together a few separate functions to address most of my needs. It just goes against my grain to have LoadAssembly, GetType, and InvokeMethod sprayed all over my code. And speaking of which, those are some of the main .NET Framework functions to execute a method dynamically at runtime. In the ReflectionLibrary, you'll find the function RunMethod, which receives Assembly name, Class name, and Function name. It also receives an object array with the arguments to be passed to the function.

First, the assembly is loaded:

Assembly aAssembly = 
   Assembly.LoadFrom(cAssembly);

Then the assembly is scanned to locate the desired class:

foreach(Type tType in aAssembly.GetTypes()) 
{
if(tType.Name.ToString().ToUpper()==
   cClassName.ToUpper())      

Once the class is found, create a method object for the function, activate the class, invoke the method object, and pass the argument object. The invocation always returns an object which you can cast to whatever type of parameter your custom object returned.

MethodInfo MyMethodInfo =  
   tType.GetMethod(cFunctionName);
object oActivator = 
   Activator.CreateInstance(tType);
oReturn = MyMethodInfo.Invoke(oActivator,args);

Tip 10: Selecting Multiple Rows and Reading the Rows That Have Been Selected

This is a straight-forward tip that has already been mentioned in this article. If you want to select a particular row in a DataGrid, just call the grid's Select() function, passing the row position relative to the current DefaultView. Note that you can select multiple rows in a grid (in advance of an operation that applies to multiple rows). There is always only one single position reported by the grid's Binding Manager.

To determine which row or rows have been selected, call the grid's IsSelected function, passing the row in question. The function ShowSelectedRows in the test form demonstrates an example of finding the selected rows and displaying them in a MessageBox.

Tip 11: Highlighting a Row

The code to highlight a DataGrid row is often found online in forums. In brief, you must define an event for the grid's MouseUp event. In the event, perform a HitTest for the coordinates reported by the MouseEventArgs, and get the row associated with the HitTest. Once you have the row, you can perform a grid SELECT() to highlight the entire row.

System.Drawing.Point pt = new Point(e.X,e.Y);
DataGrid.HitTestInfo hti = this.HitTest(pt);
if(hti.Type == DataGrid.HitTestType.Cell)
{
   this.CurrentCell = new DataGridCell(hti.Row, 
   hti.Column);
   this.Select(hti.Row);
}

I have to pause for a moment and admit what might be a minority viewpoint: I'm not always a fan of highlighting the current row. So I've added a grid property called lHighlightRow to turn on and off this behavior.

Tip 12: Updating the Grid Caption

A grid caption can provide some general summary information about the contents of the grid. In the example, the caption can display the number of jobs and the total dollar value. You want to define a function to call at the beginning of the grid session, and also any time the RowFilter for the grid's DataSource changes.

In the same manner as you defined a function to color a column, you need to pass a custom function to the grid class, to indicate that any change in the RowFilter should automatically trigger a call to this custom function. The function call in the test form shows a call to SetCaptionFunction, passing the assembly and class name, along with a function called GridCaption, which can be found with the other FormManager function, CheckBalance.

public string GridCaption(DataView dv) 
{
Decimal nJobAmt = 0; 

foreach(DataRowView Dr in dv) 
nJobAmt += 
Decimal.Parse(Dr["Balance"].ToString());

return dv.Count.ToString() + " jobs with an 
   open balance of " + 
   nJobAmt.ToString("C2");
}

Tip 13: Saving Preferences

End users expect software to save any settings they define. I'll demonstrate a quick example of saving column widths to an XML file, so that any column-sizing performed by users is retained when they reload the test form.

In the closing event of the test form, I have a method called SaveGridSettings. It simply scans through the GridColumns for the grid's default TableStyle, takes the widths, and saves them to a DataTable.

for(int nCtr=0; nCtr < oGrid.VisibleColumnCount; 
    nCtr++)

this.DtColWidths.Rows[nCtr]["ColumnWidth"] =
oGrid.TableStyles[0].GridColumnStyles[nCtr].Width;

The DataTable (which is part of a DataSet) is then written to an XML file, so that when the form loads, the column width settings in the XML file can be used with AddText, AddDropDown, and so on. These settings are called to repopulate the grid. Users (like developers) always appreciate when the software remembers their preferences!

Although not part of the sample code, column settings aren't the only thing that could be saved here. The bookmarks could be saved as well, simply by adding a second table to the DataSet that is written to the XML file and writing a small function to add the saved bookmarks back into the bookmark drop-down list. Other miscellaneous settings (text search, the balance threshold, etc.) could be written out as well. Because a DataSet/XML file can have many DataTables with different structures, there really are very few limits as to the level of detail you can store. I was involved in a .NET application where the user settings XML file contained over 50 DataTables!

Closing Thoughts

The complete source code and project for this article can be pulled down from www.commongroundsolutions.net I hope that anyone who previously discounted the DataGrid might reconsider its capabilities after reading this article. It was only a short time ago that I set out to implement Tip #9 (highlighting a cell based on a condition), so I hope I've saved you a little of the wear and tear I experienced before I found it.

When I started in .NET back in late 2001, I was constantly looking for code samples to help translate my knowledge of other development tools. For many developers, a simple code sample can make all the difference in the world in understanding something. I encourage you to pull down the code and test project, and tweak it to your needs. I plan to maintain and update this library on our Web site (www.commongroundsolutions.net). If you have suggestions or ideas you'd like to share, I'd love to hear from you.

Table 1: Exposed properties and functions for the Grid class (ccControls.ccGrid).

Grid Properties/FunctionsDescription
BindingManagerBase bMgrThe Binding Manager for the DataGrid
bool lHighlightRowFlag for whether the current row should be highlighted
DataView dvGridThe current DataView for the DataGrid
Void SetCaptionFunction()Defines a custom function for setting the grid caption. It's helpful if the caption displays a running summary calculation. Pass the function assembly, function class, and function name. The function is invoked through the reflection library.
DataRow GetCurrentRow()Returns the current DataRow from the DataGrid
Void ColorColumn()Defines a custom function that evaluates each column during the Paint Event, and returns a Boolean to indicate whether the column should be displayed in a different color. Pass the following:Column Name to be evaluated (string)Function Assembly (string)Function Class Name (string)Function Name (string)Four sets of Color RGB to use for column display if the function returns a True (uses Gradient Fill)Background Color From, Background Color ToForeground Color From, Foreground Color To
Void TextSearchPerforms a text search based on a certain string, either starting at the top of the grid, or one row beyond the current position. Utilizes the scoped columns if that setting is on.
Void SetTextFilterMarks the FilterSelected Column as True for each DataRow that matches the textsearch (uses the same rules as the textsearch)
Void InitializeGridInitializes the grid
Void AddTextAdds a textbox column to the grid
Void AddDateAdds a date column to the grid
Void AddCheckBoxAdds a checkbox column to the grid
Void AddDropDownAdds a drop-down column to the grid
Void AddRelatedTextAdds a textbox column to the grid (where the text comes from a column from a target relation)
DataGridColumnStyle GetColumnReturns a column object, based on the name of the column passed as a string
Void ShowGridThe last function to be called just prior to displaying the DataGrid. Pass the name of the DataSource as a parameter

Table 2: The functions for defining grid columns.

Function Parameters
AddTextThe standard six
AddRelatedTextThe standard six, plus ADO.NET relation nameColumn object for the related column to be displayed
AddDropDownThe standard six, plusColumn Name to displayForeign key nameForeign table table name
AddCheckBoxThe standard six
AddDateThe standard six

Table 3: Exposed properties and functions for Grid Container (ccContainers.GridContainer).

Container Properties/FunctionsDescription
String CcolumnToBookMarkProperty for the column name to be bookmarked
Bool LsearchInColumnsProperty for whether the user should be allowed to scope a text search to specific columns
Bool LshowGridToolsProperty for whether the text search and bookmark options should appear (if set to False, the grid expands to cover the entire container)
ArrayList aBookMarksArrayList of the rows that have been bookmarked
Void SetupBookMark()Sets the name of the column to be displayed in the bookmark drop-down. Pass the name of the column as a string.
Void ShowGridTools()Displays the grid and grid search tools according to how lShowGridTools and lSearchInColumns are set, as well as whether a bookmark is defined

Kevin S. Goff is the founder and principal consultant of Common Ground Solutions, a consulting group that provides custom Web and desktop software solutions in .NET, Visual FoxPro, SQL Server, and Crystal Reports. Kevin has been building software applications for seventeen years. He has received several awards from the U.S. Department of Agriculture for systems automation. He has also received special citations from Fortune 500 Companies for solutions that yielded six-figure returns on investment. He has worked in such industries as insurance, accounting, public health, real estate, publishing, advertising, manufacturing, finance, consumer packaged goods, and trade promotion, In addition, Kevin provides many forms of custom training. Contact Kevin at kgoff@commongroundsolutions.net