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:
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
sb.Append(GetHeaderRow)
iMaxColIndex = GetMaxColumnIndex()
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 DataGridTableStyle
s, 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:
Private Function GetMappingName(ByVal src As Object) _
As String
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
Return ""
End Function
Private Function FindTableStyleByMappingName(ByVal _
strName As String) As DataGridTableStyle
Dim ts As DataGridTableStyle
If strName = "" Then
Return Nothing
End If
For Each ts In Me._Grid.TableStyles
If ts.MappingName = strName Then
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 DataGridColumnStyle
s 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):
Private Function GetMaxColumnIndex() As Integer
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:
- Selecting “Copy” copies whatever row the user clicked on.
- 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.
- 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 DataRelation
s 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 DataGridColumnStyle
s 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.