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.
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
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.
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
iMaxColIndex = GetMaxColumnIndex()
mLastRowClicked = -1
3. Getting the text in the column headers
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:
Private Function GetMappingName(ByVal src As Object) _
Dim list As IList = Nothing
Dim t As Type = Nothing
If TypeOf (src) Is Array Then
t = src.GetType()
list = CType(src, IList)
If TypeOf src Is IListSource Then
src = CType(src, IListSource).GetList()
If TypeOf src Is IList Then
t = src.GetType()
list = CType(src, IList)
If TypeOf list Is ITypedList Then
Return (CType(list, _
Private Function FindTableStyleByMappingName(ByVal _
strName As String) As DataGridTableStyle
Dim ts As DataGridTableStyle
If strName = "" Then
For Each ts In Me._Grid.TableStyles
If ts.MappingName = strName Then
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):
Private Function GetMaxColumnIndex() As Integer
Dim i As Integer = 0
Dim obj As Object
obj = Grid.Item(0, i)
i += 1
Catch ix As ArgumentOutOfRangeException
If i > 0 Then
Catch ex As Exception
5. Copying options
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.
- 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.
- 7/28/2005 - Initial publication.