Click here to Skip to main content
Click here to Skip to main content

MergedDataGridView Control

By , 23 Apr 2009
 
Prize winner in Competition "Best VB.NET article of April 2009"

MergedDataGridView.png

Introduction

As we all know, the DataGridView control doesn't allow us to merge cells, and if we think a little on that, we can ask ourselves the question, "Why?” Well, the DataGridView is bound to a data source (even if you don't define that), and each cell represents a field in a record, so what field will that merged cell belong to? Maybe, it was because of this that Microsoft didn't include this feature. But, sometimes, we may want to show some extra information, and the only solution is to popup a form/message box, or “steal” some space in the current form and fill it with textboxes, combo’s, etc.

The goal of this customization is to show some extra information in a DataGridView. It basically uses a RichTextBox that is inserted in the grid and sized according to the parent row. The parent row must have a unique ID that will be the textbox name. This will then be used to size and position the RichTextBox in the right place.

I have also included some icon animation and grid customization to improve the end look.

Using the code

To start, you must include the class MergedDataGridView in your application. After you build the project, the MergedDataGridView control will be available in the toolbox. Then, just drag it to your form.

Next, you need to define the custom properties for the MergedDataGridView:

With Me.MergedDataGridView1
 
    ' Define the datasource
    .DataSource = ds.Tables(0).DefaultView

    ' Custom definitions for the RichTextBox
    .StartColumnIndex = 1
    .EndColumnIndex = 7
    .RowHeight = 60

    ' Custom definitions for the DataGridView
    .AllowUserToAddRows = False
    .AllowUserToDeleteRows = False

    .RowsDefaultCellStyle.BackColor = Color.White
    .AlternatingRowsDefaultCellStyle.BackColor = Color.AliceBlue
    .AutoResizeColumns(DataGridViewAutoSizeColumnsMode.DisplayedCells)

End With 

After this, you can include the two DataGridViewImageColumns that will be used to show the RichTextBox and to display a MessageBox containing the information.

' Create an image column in the datagrid that will open the merged row
Dim ImageColumn1 As New DataGridViewImageColumn
ImageColumn1.DefaultCellStyle.Alignment = DataGridViewContentAlignment.TopCenter
ImageColumn1.Image = My.Resources.DownArrow
ImageColumn1.Width = 25

' Create an image column in the datagrid that will open an extra window
Dim ImageColumn2 As New DataGridViewImageColumn
ImageColumn2.DefaultCellStyle.Alignment = DataGridViewContentAlignment.TopCenter
ImageColumn2.Image = My.Resources.Info
ImageColumn2.Width = 25

' Add the two columns to the datagridview
Me.MergedDataGridView1.Columns.AddRange(New _
   DataGridViewImageColumn() {ImageColumn1, ImageColumn2})

Finally, in the CellMouseClick event, check to see if the user clicked on the right column; if so, add the new row:

' Add a new row in the correct postion (e.RowIndex + 1)
Dim rowPos As Integer = e.RowIndex + 1

Dim dv As DataView = Me.MergedDataGridView1.DataSource
Dim row As DataRow = dv.Table.NewRow()
dv.Table.Rows.InsertAt(row, rowPos)

' Get the text from the hidden columns that will be used to fill the RichTextBox
Dim mergedRowText As New System.Text.StringBuilder
mergedRowText.AppendLine(Me.MergedDataGridView1("Description", e.RowIndex).Value.ToString)
mergedRowText.AppendLine(Me.MergedDataGridView1("Link", e.RowIndex).Value.ToString)

' Call the AddMergedRow sub
Me.MergedDataGridView1.AddMergedRow(rowPos, mergedRowText.ToString)

Or delete it:

' Remove the row from the datasource
Dim rowPos As Integer = e.RowIndex + 1

Dim dv As DataView = Me.MergedDataGridView1.DataSource
dv.Table.Rows.RemoveAt(rowPos)

' Call the RemoveMergedRow sub
Me.MergedDataGridView1.RemoveMergedRow(Me.MergedDataGridView1(0, _
                                       e.RowIndex).Value)

The other code used in the form, as you can see in the attached example, is for validation, error handling, and generic animation.

Looking at the control

Class.png

The control has only two methods: AddMergedRow and RemoveMergedRow, and three properties. The properties just get the indication of the column where the RichTextBox will start, end, and what height it will have.

The AddMergedRow looks for the number (the ID) of the previous row, that will be the parent one, and creates a new RichTextBox in the new row, with that ID as the name.

''' <summary>
''' Adds a new row with a merged cell using a richtextbox
''' </summary>
''' <param name="rowIndex">Index where the row will be added</param>
''' <param name="cellText">Text that will be displayed on the RichTextBox</param>
''' <remarks></remarks>
Public Sub AddMergedRow(ByVal rowIndex As Integer, ByVal cellText As String)

    Try

        Me.SuspendLayout()


        ' Defines the location/size of the textbox
        Dim x As Integer = _
            Me.GetColumnDisplayRectangle(Me.StartColumnIndex, False).Left + 1
        Dim y As Integer = Me.GetRowDisplayRectangle(rowIndex, False).Top
        Dim w As Integer = _
            Me.GetColumnDisplayRectangle(Me.EndColumnIndex, False).Right - x - 2
        Dim h As Integer = Me.GetRowDisplayRectangle(rowIndex, False).Size.Height - 1

        ' Gets the ID from the previous row, that will be used for the name
        ' of the textbox. This ID will be used to find the control for the row
        Dim parentRowID As Integer = Me(0, rowIndex - 1).Value

        ' Creates a new textbox and place it in the right position
        Dim rtb As New RichTextBox
        With rtb
            .Name = parentRowID
            .Text = cellText
            .Multiline = True
            .BorderStyle = BorderStyle.None
            .ScrollBars = ScrollBars.Vertical
            .ReadOnly = True
            .Font = New Font(Me.DefaultCellStyle.Font, Me.DefaultCellStyle.Font.Style)
            .SetBounds(x, y, w, h)
        End With
        Me.Controls.Add(rtb)
 
        ' Define the same color for the RichTextBox as the row color
        rtb.BackColor = Me(0, rowIndex).InheritedStyle.BackColor

        ' Define the row height
        Me.Rows(rowIndex).Height = Me.RowHeight

        ' Define a new image for the imagecell (up arrow)
        Dim arrow As DataGridViewImageCell = Me(Me.ColumnCount - 2, rowIndex - 1)
        arrow.Value = My.Resources.UpArrow

    Catch ex As Exception
        Throw New ArgumentException(ex.Message)

    Finally
        Me.ResumeLayout()
    End Try

End Sub

The second method, RemoveMergedRow, looks for the parent row and removes it from the grid.

''' <summary>
''' Removes the cell (RichTextBox) from the DataGridView
''' </summary>
''' <param name="rowID">ID of the row</param>
''' <remarks></remarks>
Public Sub RemoveMergedRow(ByVal rowID As Integer)

    Try

        ' Find the control in the DataGridView and remove it
        Dim ctrl() As Control = Me.Controls.Find(rowID.ToString, False)
        If ctrl.Length = 1 Then
            Me.Controls.Remove(ctrl(0))
        End If

        ' Define a new image for the imagecell (down arrow)
        Dim arrow As DataGridViewImageCell = Me(Me.ColumnCount - 2, Me.CurrentRow.Index)
        arrow.Value = My.Resources.DownArrow

    Catch ex As Exception
        Throw New ArgumentException(ex.Message)

    Finally
        Me.ResumeLayout()
    End Try

End Sub

The Paint event arranges the position of the RichTextBoxes, and adjusts the size.

Since it’s not easy to calculate the positions and keep the empty rows below the parent rows after you sort it, I have turned sorting off in the ColumnAdded event.

Points of interest

This control not only shows some extra information on a DataGridView control, but also demonstrates how to customize the grid and handle row positioning, which may be useful for other projects.

I really hope that this can help improve your projects, or help you get some new ideas.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

jpaulino
Software Developer
Portugal Portugal
Member
Jorge Paulino
Microsoft Visual Basic MVP
Portuguese Software Developer
VB.NET, ASP.NET, VBA, SQL
http://vbtuga.blogspot.com/

http://twitter.com/vbtuga

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralGood and simple replacement for an Outlook style listviewmemberFrancisco J. Rodríguez12 Feb '12 - 3:36 
This is what I was looking for very long time. Thank you very much, excellent component.
GeneralUpdate??memberdherrmann4 Jun '11 - 23:48 
Hi,
have you thought about an update of your solution?
 
Thanks-
Dietrich
GeneralRe: Update??memberjpaulino5 Jun '11 - 0:01 
Any ideas? I have some changes already, but you can give move ideas.
Jorge Paulino
Microsoft Visual Basic MVP
http://vbtuga.blogspot.com/

http://twitter.com/vbtuga

GeneralRe: Update??memberdherrmann5 Jun '11 - 0:07 
hi,
 
I think you can show us your changes first so we can see which further ideas could be expressed... Smile | :)
 
Regards-
Dietrich
GeneralThanksgroupYZK30 Mar '11 - 0:00 
Thanks
Generaladd columnsmembercreativesoul3 Dec '10 - 2:23 
Hi,
 
has someone tried adding columns?...
as soon as i add one more or change something it start going nuts
someone any idea?
GeneralFine component!memberMarcelo Ricardo de Oliveira8 Dec '09 - 7:23 
Column merging is some of those fine features that Microsoft should provide in her components...
 
Meus parabéns pelo premio, Jorge!
 
Thanks for sharing, got 5 from me!
regards from Brazil
 
Take a look at full source code C# Snooker game here in Code Project.

GeneralRe: Fine component!memberjpaulino9 Dec '09 - 10:06 
Thanks Marcelo!
 
(E um abraço de Portugal Smile | :) )
 
Jorge Paulino
VB.NET, ASP.NET, VBA, SQL
http://vbtuga.blogspot.com/

GeneralParabens - CongratulationsmemberSc0rp1026 May '09 - 9:05 
Parabens pelo artigo, esta muito bom, e gostei muito do codigo.
 
E sempre agradavel ver Portugueses a fazerem bom codigo.
 
Tambem ja sabia que lhe davas bem Smile | :)
 
Pwyll do P@P - Sei k nao tenho la ido mas tenho tido montes de trabalho.
 

P.S. I know i write in portuguese, my apologize to the others but I want to congratulate this user in your native language.
GeneralRe: Parabens - Congratulationsmemberjpaulino26 May '09 - 9:10 
Obrigado! Wink | ;)
 
(thanks)
 
Jorge Paulino
VB.NET, ASP.NET, VBA, SQL, XML
http://vbtuga.blogspot.com/

Generalsome improvementsmemberMr.PoorEnglish18 May '09 - 10:37 
Hi!
 
I think, there can be done a lot to make that code better.
 
what I worked out until now:
there is no need to add a DGV-Row. Just resize that one you call "ParentRow".
That improves the look, because if the Rtb is placed on a new Row it gets another Color than the parentRow.
you dont need a cellpainting just to remove vertical gridlines.
just set DGVs CellBorderStyle to CellBorderStyle.HorizontalSingle.
 
Whats the use of all that TryCatches?
First you catch an Exceptions, then you throw another one.
Remove that TryCatches, and the Exceptions will be thrown exactly at that line of code, where the error is (and not where you throw the exception-substitue).
That is much easier to debug.
 
I put your AddMergeRow and your RemoveMergeRow together to a "ToggleRtb". It takes 1/3 lines of Code:
 
<code>
   ''' <summary>
   ''' Adds a new row with a merged cell using a RichTextBox
   ''' </summary>
   ''' <param name="rowIndex">Index where the row will be added</param>
   ''' <param name="cellText">Text that will be displayed on the RichTextBox</param>
   Public Sub ToggleRtb(ByVal rowIndex As Integer, ByVal cellText As String)
         Me.SuspendLayout()
         Dim arrow As DataGridViewImageCell = Me(Me.ColumnCount - 2, rowIndex)
         Dim ctl = Me.Controls(rowIndex.ToString)
         If ctl Is Nothing Then
            Dim rtb As New RichTextBox
            With rtb
                  .Name = rowIndex
                  .Multiline = True
                  .BorderStyle = BorderStyle.None
                  .ScrollBars = ScrollBars.Vertical
                  .ReadOnly = True
                  .SelectionIndent = 20
                  .Font = Me.DefaultCellStyle.Font
                  .Text = cellText
            End With
            Me.Controls.Add(rtb)
            Me.Rows(rowIndex).Height += Me.RowHeight
            arrow.Value = My.Resources.UpArrow
         Else
            Me.Controls.Remove(ctl)
            arrow.Value = My.Resources.DownArrow
            Me.Rows(rowIndex).Height -= Me.RowHeight
         End If
         Me.ResumeLayout()
   End Sub</code>
GeneralRe: some improvementsmemberjpaulino18 May '09 - 10:51 
Hi Mr.PoorEnglish,
 
Thanks for looking at it!
 
I have to test it, and since I also have made a some small improvements, I can then join all.
 
Thanks
 
Jorge Paulino
VB.NET, ASP.NET, VBA, SQL, XML
http://vbtuga.blogspot.com/

GeneralRe: some improvements [modified]memberBib3477021 May '09 - 1:16 
i've found the pb in this code...
paint don't find control because name is cellValue, not CellIndex
replace "rowIndex.ToString" by "Me(0, rowIndex - 1).Value.ToString" work better
 
Adding control in the parent row seems effectively a better solution instead of adding a row, but Paint must be rewritted.
new method seems to be :
for each visible row you must search if RTB exists...if exists, locate it between Row text bottom and row bottom.
 
are you ok with this analyse ?
 
modified on Thursday, May 21, 2009 12:45 PM

GeneralBug fix + add on + question [modified]memberBib347703 May '09 - 5:25 
it's à realy good component...congratulations !
 
1) Little bug Fix :
you wrote :
mergedRowText.AppendLine(Me.MergedDataGridView1("Link", e.RowIndex).Value.ToString)
...appendLine add an empty line at end...replaced by Append work fine.
 
2)AutoHeight Add on suggest :
' Define the row height
If (Me.RowHeight > 0) Then
Me.Rows(rowIndex).Height = Me.RowHeight
Else
Dim g As Graphics = rtb.CreateGraphics()
Dim lH As SizeF = g.MeasureString(rtb.Text, rtb.Font, rtb.ClientSize.Width)
Me.Rows(rowIndex).Height = lH.Height + 1
End If
 
3) My question :
Image is always visible, even if no description and no Link.
I don't know how to update this for showing image only when something to develop...have you an idea ?
 

thanks for this component.
 
modified on Sunday, May 3, 2009 11:38 AM

GeneralRe: Bug fix + add on + questionmemberjpaulino18 May '09 - 10:55 
Thanks Bib34770,
 
Sorry for the delay of this reply!
 
What image do you mean ? The information and the arrow icons ?
 
I will check your suggestions, test it and include that on a future revision of the control.
 
Jorge Paulino
VB.NET, ASP.NET, VBA, SQL, XML
http://vbtuga.blogspot.com/

GeneralRe: Bug fix + add on + questionmemberBib3477019 May '09 - 10:12 
thanks
GeneralRe: Bug fix + add on + questionmemberjpaulino19 May '09 - 10:19 
But what image do you mean ? The information and the arrow icons ?
 
Have you used without show the richtextbox until the end of the columns ?
 
I will be glad to help if I can! Smile | :)
 
Jorge Paulino
VB.NET, ASP.NET, VBA, SQL, XML
http://vbtuga.blogspot.com/

GeneralRe: Bug fix + add on + questionmemberBib3477019 May '09 - 10:51 
yes, information icon for example.
 
The icon must indicate there is something else.
if there is no information for a row, we must hide the icon (only for this row).
 
...
GeneralWowmemberPL0127 Apr '09 - 20:25 
Looks fantastic, I will try this Smile | :)
... it also looks like there is only one step to show a grid under the parent row...
 
/ PL01
 
Good software is not build nor developed, it just grows up
- C. Stoll

GeneralRe: Wowmemberjpaulino27 Apr '09 - 22:04 
Thanks PL01
 
Jorge Paulino
VB.NET, ASP.NET, VBA, SQL, XML
http://vbtuga.blogspot.com/

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 23 Apr 2009
Article Copyright 2009 by jpaulino
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid