Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Advanced DataGrid sizing

0.00/5 (No votes)
4 Dec 2003 2  
This article implements an important new feature for the WinForms DataGrid, automatic row height sizing! Also shown is column hiding and column auto sizing.

New Update

The row height sizing method has been fixed to work with both ReadOnly DataGrids and DataGrids that can be written to. It's all explained here. Also a new more advanced explanation how it all works is available.

Introduction

It looks like everyone is annoyed with the CSharp implementation of Windows controls. Textboxes, combos and DataGrids are popping up like mushrooms. After building a graphical debugging tool, I too became annoyed with one control in particular : the DataGrid.

There are loads of things that this control has built in; data binding, some events and a professional view are available. Next to that, there are also a lot of customized implementations available for adding comboboxes to the grid, or making the columns automatically size their widths. But despite all of this, there is (at least) one thing that is still not found at all.

Auto sizing row heights is the keyword. After using the DataGrid I found that the rows wouldn�t resize automatically. A multiple line text shows as a single line row! Look at the picture below to see what the DataGrid looks like in its normal state.

Forget about the ugly buttons on top (Filter & Add), this is the picture of the demo application without the autosizing enabled. The buttons are for showing the height calculation still works while adding or filtering the rows in autosizing mode.

The grid is displaying a typed dataset I made for the debugging tool. Notice the texts �long� and �medium�? Yup, there�s more data down there somewhere. The �long� lines are actually three lines, the �medium� ones are two. Also notice the ugly piece of background shown besides the last column. This calls for some remodeling of the DataGrid.

In this article building a customized DataGrid that is able to automatically resize the column�s width and the row�s height will be achieved. And as a bonus I will throw in hiding and showing columns on demand. We'll call the new control AutoSizeDataGrid. The picture below shows how the grid will look, the picture is again taken from the test application included in the download, now enabled.

As you can see the empty space behind the last column is now filled, and it will stay that way while resizing the grid. Also the rows are drawn with their correct heights, greatly improving the readability of the data shown.

The problems faced

Trying to rebuild a control can not always be done as easily as you would like. There are several problems that need to be overcome when trying to implement the new features. For starters, the first uneditable column has width. This makes the calculation of the actual grid width a bit difficult. The picture above shows this area, the column before the �ID� column.

Next, the DataGridColumnStyles that make up the DataGrid do not have a Visible property, thus making them hard to hide. And for the resizing of the rows, you really don�t want to know how that works. The final code is quite easy however, there were some problems with re-sorting and filtering the DataGrid, but those problems are solved now.
Let�s handle each problem separately.

Calculating the grid width

This can be easily achieved, there is only one setback. There have to be rows present in the viewed DataSource. This is because the width of the first uneditable column is calculated by obtaining the X position of the first cell. No cell, no width. When there are no rows, a default value is used.

Hiding the grid columns

Each column in the DataGrid is represented by a DataGridColumnStyle object. This is the object that houses the Width property that the columns adheres to. After working with the DataGrid I noticed that it�s impossible for the user to resize a column to a width of zero. So it�s possible for us to use that width to make the column invisible. We can also make use of this for checking whether a column is visible or not. This is necessary because we don�t want the auto sizing behavior to resize hidden columns, making them visible again. Setting the width to zero hides the column and will act like a switch when auto resizing the column widths.

Resizing the row heights

This was the hardest nut to crack. The first problem that I faced was finding out how the sizing of a single row works. Searching the documentation turned up a method in the DataGridColumnStyle called GetMinimumHeight. This of course peeked my interest. I started playing around with the method, first making it return a higher value than normal. Wham! The rows resize. This really is the method that does the work! Now that the right method has been found, we need to find out for which cell GetMinimumHeight is called for, or maybe it�s called on a row basis?

Finding out was a bit harder than it would seem. The GetMinimumHeight does not supply any parameters that can be used, possibly leaving it next to impossible to guess which cell is queried for its height.

The best way to solve this problem is adding debug statements and playing around. We need to find the pattern of the GetMinimumHeight calling mechanism. When the pattern is known, it will be possible to link the pattern to specific rows or cells. The debug statements revealed the calling pattern quickly enough. It�s on a cell basis, quite logical because we�re working with columns here.

The second problem was how to obtain the correct text to measure. Again the DataGridColumnStyle has a method available that might help. It�s called GetColumnValueAtRow, and it will need a CurrencyManager, whatever that is, for obtaining the values. This shifts the problem to obtaining a CurrencyManager, the documentation helps out again. In the docs an example of how to obtain the CurrencyManager is shown. I tried it out, and did obtain correct string values. Nice, second problem solved.

It is now possible to implement our own mechanism. It will need to count how many times the GetMinimumHeight method is called, and return a string height using the GetColumnValueAtRow method and a CurrencyManager obtained from the grid. When the count reaches the number of rows in the displayed source, the counter will need to be reset. This will allow for more dynamic behavior (not doing so will totally ruin the program when sorting, adding, or filtering, try it out).

The resetting of the counter was the last problem faced. The DataSource wouldn�t return the right number sometimes. After trying some other possibilities, the CurrencyManager turned out to be the correct source for the information required.

The code below is a cutout from the overridden GetMinimumHeight method, it shows the simplicity of the mechanism, and is all that�s needed for row height sizing!

// Get CurrencyManager

CurrencyManager cur = (CurrencyManager)this.DataGridTableStyle.
    DataGrid.BindingContext[this.DataGridTableStyle.DataGrid.DataSource,
    this.DataGridTableStyle.DataGrid.DataMember];
// Rows available?

if(cur == null || cur.Count == 0)
    return base.GetMinimumHeight();
// Increment counter

this.currentIteration++;
// Initialize return value

int retVal = base.GetMinimumHeight();
// Calculate height of row at currentIteration - 1

retVal = this.CalcStringHeight(GetColumnValueAtRow(cur,
    currentIteration - 1).ToString());
// Reset when last cell reached

if(currentIteration == cur.Count)
    this.ResetIterations(); // sets currentIteration to 0

// Return

return retVal;

The solution offered

As the introduction stated, the new DataGrid implements autosizing behavior. In this chapter I will explain the functions offered.

First lets talk a bit about the auto width. The grid will attempt to resize the columns so they fit exactly in the ClientSize.Width. It will resize all columns the same amount, except those that have been resized by the user, those columns will stay their set size. Read the acknowledgement about this idea. The columns will never automatically resize smaller than the PreferredColumnWidth, which can be set in the DataGrid.

Hiding the columns is performed by setting their width to zero. A ContextMenu is provided for showing and hiding the columns. It can be brought up by right clicking anywhere in the grid. It also houses two resetting options, one for the width of the columns, one for resetting the auto size behavior of columns that have been sized by the user.

The row heights are done automatically, the only thing that has to be done is adding the right DataGridTableStyle (one with the supplied DataGridColumnStyle for each column) to the TableStyles collection of the grid. This can be done by calling the style creation method supplied with the grid.

There is a demo project supplied which shows a form, a grid and some buttons. It demonstrates that automatic resizing works while sorting, filtering or adding new rows. Play with it and be amazed (I hope ;) )

Problems

Only one small one so far. Do not change the ReadOnly property of the grid to false. This will cause the row counter for the auto height to fail. You could do it, but expect to recalculate the currentIteration counter in the DataGridColumnStyle supplied in the source. But you probably won�t want to, my User Interface Design teacher once told me 'DataGrids are for viewing ONLY'.

Acknowledgement

The idea to resize all columns the same amount, and overriding the autosizing when the user has resized the column was borrowed from Mike Griffin, read all about his DataGrid.

Thank you Mike! Hope you like the row sizing.

Conclusion

Finally! A DataGrid with some proper sizing functions is implemented and available. I found some resizing algorithms on the internet, but there was never one available that resized the row heights, my biggest annoyance. Now it�s available and ready for the next challenge, text wrapping. It�s around the corner now!

I tested the new control, but now it faces it�s biggest test, YOU. Please let me know of any problems that arise while using this control. But never, ever, ever set the ReadOnly property to false unless you want trouble.

Row height sizing explained

Row height sizing, why does it work? First of all, let me explain how the datagrid does it's thing. The DataGrid builds it's rows by calling the private CreateDataGridRows method. Interesting to know is that sorting, filtering, adding or any other such type action, will cause the DataGrid to rebuild it's rows fully, again using the CreateDataGridRows method. Each row created will call GetMinimumHeight exactly one time, this is also the only time that GetMinimumHeight is called. This is interesting, because it enables us to relate the number of times that GetMinimumHeight is called to a specific row in the displayed data. When we know how the CreateDataGridRows method creates it's rows, we can build a counter that indicates which row height is requested. When we reset the counter to 0, after the last time that GetMinimumHeight is called, we enable the row height sizing algorithm to work time and time again. We need to find out how many times CreateDataGridRows will call GetMinimumHeight. For this we will need some knowledge about the implementation of CreateDataGridRows. We can obtain this knowledge in two ways, decompiling or monitoring. After taking one of those actions I found out the following:

// CreateDataGridRows pseudocode

int count = Rows.Count;
for (int i = 0; i< count; i+)
{
    DataGridRelationShipRow row = new DataGridRelationShipRow() 
      // will call GetMinimumHeight

}
if(AllowAdd)
    DataGridAddNewRow row = new DataGridAddNewRow() 
       // will call GetMinimumHeight

As you can see, determining how many times that GetMinimumHeight will be called is quite easy: Rows.Count + (AllowAdd ? 1 : 0)

Just so you know, it's not Rows.Count, but CurrencyManager.Count. And the AllowAdd property is a private field, making it all more difficult for us to determine the right number. (But we can make use of the different type of row that's created for empty lines of data!!)

Update

Ok, now for the actual new method. Notice that the DataGridAddNewRow object is only created when the AllowAdd property allows this. Secondly, that type of row is always created as the last item. We know that if it was the DataGridAddNewRow that called GetMinimumHeight, we will need to reset the counter. Secondly, when it wasn't the DataGridAddNewRow object, but the DataGridRelationShip row, we know that the CurrenyManager.Count is larger than 0. This will enables us to build the following method:
if class that called GetMinimumHeight == DataGridAddNewRow
{
    resetCounter;
    return;
}else
{
    counter++;
    int retVal = GetStringHeightAt(counter - 1);
    if(counter == CurrencyManager.Count)
        resetCounter;
    return retVal;
}

We can find out which class called GetMinimumHeight(AddNewRow, or RelationShipRow) by watching the stack. This leads to the following implementation:

StackFrame frame = new StackFrame(4);
MethodBase method = frame.GetMethod();
string s = method.DeclaringType.FullName;
if(s.EndsWith("DataGridAddNewRow"))
{
    this.ResetIterations();
    return base.GetMinimumHeight();
}
else
{
    CurrencyManager cur = (CurrencyManager)
        this.DataGridTableStyle.DataGrid.BindingContext
        [this.DataGridTableStyle.DataGrid.DataSource,
        this.DataGridTableStyle.DataGrid.DataMember];
    if(cur == null || cur.Count == 0)
        return base.GetMinimumHeight();
    this.currentIteration++;
    int retVal = base.GetMinimumHeight();
    retVal = this.CalcStringHeight(GetColumnValueAtRow(
        cur,currentIteration - 1).ToString());
    if(currentIteration ==    cur.Count)
        this.ResetIterations();
    return retVal;
}

This new method will work ALWAYS. This is because the CreateDataGridRows is the only method that leads to querying of the row height, and only 1 thread at a time will be in the CreateDataGridRows method.

I hope it's all a bit cleared now, have fun!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here