Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

DataGrid Copy Helper Component

4.74/5 (20 votes)
28 Jul 20057 min read 1   2.1K  
A component providing Copy functionality to a Windows Forms DataGrid. Copied data can then be pasted in Excel or a text editor.

Introduction

This article describes a component I created to add copy functionality to a Windows Forms DataGrid. It grew out of my frustration about all the work I find myself doing in order to show users data in a grid, and even let them edit it, but still have to turn around and write them a report so that they could print it (other than via a screen shot), and/or use the data elsewhere (such as in a spreadsheet). Any other modern application seems to be able to let you copy data you see in grids and paste it elsewhere, and it’s gotten embarrassing having to explain to my users that my application can’t do that.

Background

One common approach I’ve seen requires the use of a DataTable as the grid’s DataSource, and does some sort of magic with the CurrencyManager and/or DataViewManager to iterate through each bound row and query the value of each bound column. It’s been quite a while, so the details aren’t so fresh in my memory anymore and unfortunately this type of approach won’t work for me. Most of my grids are bound to custom collections of objects and make use of a DataGridTableStyle with its MappingName property set to the type name of the custom collection and specific DataGridColumnStyle objects to display the properties I want to show. So in terms of flexibility, I wanted something that would work with virtually anything bound to the grid.

Design goals

In designing this component, I wanted to avoid having to derive a custom DataGrid for the specific purpose of copying data. While inheriting from a standard DataGrid might have provided access to some protected members that might be helpful, I wanted to see if I could do it without resorting to this approach – which might also be a benefit if someone wanted to add this functionality to a derived DataGrid previously written for another reason. I found a very nice article by Palomraz that was very helpful in understanding some of the finer points of components and how they are created and disposed by Windows Forms.

I also thought it would be very nice if I could get the text of the column headers whenever possible and copy those with whatever data was listed below them. As I will discuss further, this little feature proved to be the most challenging aspect for me, and I still haven’t figured out how to do it in all cases.

Using the code

To add copy functionality to a Windows Form containing a DataGrid, simply add the DataGridCopyHelper to the Components pane of the VS.NET Toolbox. Drag the DataGridCopyHelper to the form's designer and set its Grid property to the DataGrid you want to use it for in the DataGridCopyHelper's properties window.

Points of interest

1. Editing the ContextMenu

The first challenge I encountered was in designing the component’s ContextMenu. While the VS.NET IDE allowed me to view a designer for the component, and add a ContextMenu to it from the Toolbox, VS.NET produced a NullReferenceException as soon as I clicked the “Edit Menu” hyperlink. Although a net search suggested that perhaps I ought to look into DesignerVerbs, I opted instead to manually add the 15 or so lines of code to the “Windows Code Designer Generated Code” region. This seemed to produce no ill effects and compiled.

2. The basic copy process

The process of actually copying the data is pretty simple. Basically, a Copy command builds a big string containing the data from the appropriate row(s). Within each row, each cell’s data is represented in string format (gotten by calling DataGrid(iRow, iColumn).ToString.) Adjacent cells are delimited by Tab characters, and the end of each row is delimited by a Return (vbCRLF) character. Finally, the string is put on the clipboard:

VB
Private Sub CopySingleRowToClipboard()
    If mLastRowClicked < 0 Then Exit Sub

    Dim iRow As Integer = mLastRowClicked
    Dim iCol As Integer = 0
    Dim iMaxColIndex As Integer

    Dim sb As New System.Text.StringBuilder


    Try
        'try to get tab-delimited string
        'representing column header text
        sb.Append(GetHeaderRow)

        iMaxColIndex = GetMaxColumnIndex()
        'get tab-delimited string representing
        'cells of the row clicked on
        sb.Append(GetGridRow(iRow, iMaxColIndex))

        Clipboard.SetDataObject(sb.ToString, True)
    Catch
        Beep()
    Finally
        mLastRowClicked = -1
    End Try

End Sub

3. Getting the text in the column headers

Unfortunately the DataGrid provides no direct access to the text shown in its column headers. The DataGridColumnStyle objects contained in a DataGridTableStyle object, however, do know what their header text is. While the DataGrid does expose a collection of DataGridTableStyles, it unfortunately doesn’t provide any way to determine which one is "currently in use" (if any) for the data being displayed. Therefore, the DataGridCopyHelper tries to resolve the MappingName of the object bound to the grid (GetMappingName function), and finds a DataGridColumnStyle with the same MappingName associated with the DataGrid. If the type of the object is such that the MappingName can't be resolved, or if no DataGridTableStyle is found with a matching MappingName, then the FindTableStyleByMappingName function returns nothing:

VB
Private Function GetMappingName(ByVal src As Object) _
                                             As String
    'try to resolve mapping name of some object
    'being used as the grid's data source.
    'based on SyncFusion WinForms FAQ 5.76 snippet

    Dim list As IList = Nothing
    Dim t As Type = Nothing

    If TypeOf (src) Is Array Then
        t = src.GetType()
        list = CType(src, IList)
    Else
        If TypeOf src Is IListSource Then
            src = CType(src, IListSource).GetList()
        End If

        If TypeOf src Is IList Then
            t = src.GetType()
            list = CType(src, IList)
        Else
            Return ""
        End If
    End If

    If TypeOf list Is ITypedList Then
        Return (CType(list, _
                  ITypedList).GetListName(Nothing))
    Else

        Return (t.Name)

    End If

    'unknown source
    Return ""

End Function
VB
Private Function FindTableStyleByMappingName(ByVal _
               strName As String) As DataGridTableStyle
    'iterate through table styles and try to find
    'one having a mapping name matching the input arg.
    Dim ts As DataGridTableStyle

    If strName = "" Then
        'no point
        Return Nothing
    End If


    For Each ts In Me._Grid.TableStyles
        If ts.MappingName = strName Then
            'found match
            Return ts
        End If
    Next

    Return Nothing

End Function

4. Iteration

The DataGridCopyHelper provides two options for copying multiple rows - Copy All Rows and Copy Selected Rows. In these cases where multiple rows are being copied, we need the count of all rows contained in the grid, not just those that are currently visible. The count of all rows has to be determined from the CurrencyManager associated with the grid’s data source - since the only row count the DataGrid provides is a VisibleRowCount. Copying selected rows requires the count because it too must check the DataGrid.IsSelected(iRow) property for each row in the grid to make sure it gets all the highlighted rows.

Similarly, column iteration requires knowing how many columns are in the grid, which again cannot be determined from the DataGrid itself. Since my approach does not assume that we’re guaranteed to have a current accessible DataGridTableStyle to work with, I couldn't even rely on a simple count of the DataGridColumnStyles in the current table style. Therefore, I resorted to a hack that I’m sure is up there with the worst ever. I wrote a GetMaxColumnIndex function that repeatedly tries to access the DataGrid.Item(iRow, iCol) property, using row index 0 (the first row), and incrementing the column index from 0 until an ArgumentOutOfRangeException is encountered. If there are no columns, or no data in the grid, this function returns -1; otherwise, it will return the zero-based index of the right-most column available in the grid (even if scrolled out of view):

VB
Private Function GetMaxColumnIndex() As Integer
    'Cruddy hack to count the columns
    'currently displayed in the grid.
    'Starting at cell (0,0), we iterate through
    'columns until an ArgumentOutOfRangeException occurs.
    'If there are no columns, no grid, or no data
    'source, the iterator won't have been incremented
    'before an exception is encountered,
    'and -1 will be returned.

    Dim i As Integer = 0
    Dim obj As Object

    Try

        Do
            obj = Grid.Item(0, i)
            i += 1
        Loop

    Catch ix As ArgumentOutOfRangeException

        If i > 0 Then
            Return i
        Else
            Return -1
        End If

    Catch ex As Exception
        Return -1
    End Try


End Function

5. Copying options

The ContextMenu actually provides three options for copying:

  1. Selecting “Copy” copies whatever row the user clicked on.
  2. Selecting “Copy Selected Rows” copies a range of highlighted rows by iterating through the rows in the grid and querying the IsSelected(iRow) property of the DataGrid. This menu item is only available if there are rows selected, which I enforce by checking whether the row they clicked on was highlighted.
  3. Finally, there is a “Copy all rows” option that iterates through each row and copies its data.

6. Limitations

  • Column headings: The most significant known limitation is the way in which I had to get the column heading text. Headings will not be copied for grids bound to a DataTable for sure, and the same probably goes for a DataSet. This isn't a priority for me because I use a lot of custom collections, but it should be possible to add a branch somewhere that handles the specific case of a simple bound DataTable. I understand it's next to impossible to guarantee anything, however, especially for the case where a DataSet is used and the actual DataMember is some complicated string of DataRelations leading to a child table.
  • Disabling: There is also no way to turn the functionality on and off at present, but it should be child's play to add an Enabled property to this component if you want one.
  • Hidden or missing columns: Columns hidden by setting their Width property to 0 will get copied anyway. And, I have not tested the case where a column defined by the DataGridTableStyle is absent from being displayed in the grid because the column was assigned an incorrect value for its MappingName property. Based on the code, I'm pretty confident that the heading would be copied, but the data underneath it would belong to the next column if this were the case).
  • Formatting: Because the data is copied using the ToString method of whatever is returned by evaluating each cell, formatting associated with DataGridColumnStyles is ignored.

Looking back on this project, I feel like I've gotten wrapped around the axle regarding copying the header text. I wish it were easier to get this information from the DataGrid itself, so that more cases could be handled. In any event, I believe the methods I've used will get data from the rows shown in the grid in nearly any case.

History

  • 7/28/2005 - Initial publication.

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