Core C# and .NET
< Day Day Up > |
The DataGridView control, introduced with .NET 2.0, supersedes the DataGrid which now exists primarily for legacy purposes. With more than a hundred properties and methods, the DataGridView is by far the most complex Windows Forms control for displaying data. Accordingly, it is also the most flexible. Styles that govern appearance can be applied on a cell-by-cell basis, by rows, by columns, or across all cells in the grid. Cells are not limited to text. They may contain a TextBox, Image, CheckBox, Link, or Button control. Data binding is supported by the DataSource property, just as with the controls defined in the previous section. In addition, the DataGridView provides a unique virtual mode that permits it to handle more than 100,000 rows of data. DataGridView methods, events, and properties allow an application to easily manage the mapping between virtual and physical storage. All of these features are discussed in this section. We'll look at selected properties and events along with code examples that illustrate their use. Properties
Despite its myriad features, the DataGridView has an elegantly simple structure. As shown in Figure 12-6, in its most elemental form, it consists of column headers, row headers, and cells. To these, we can add the Columns and Rows collections that allow an application to access the grid by indexing a row or column. That is the foundation. Each property and event discussed in this section relates to one of these five classes. Figure 12-6. Basic DataGridView elements
The DataGridView class inherits many of its properties from the Control class; to these, it adds properties required to support its own special characteristics and behavior. The properties listed in Table 12-1 are primarily in this latter category. The list is not meant to be exhaustive; instead, it presents those properties you'll refer to most frequently when implementing a grid.
Constructing a DataGridView
Listing 12-6 shows how to define columns for a DataGridView, set properties to define its appearance and behavior, and add rows of data. (We'll see in the succeeding example how to use the more common approach of loading data from a database.) Note that the column header cells and data cells have different styles. If a style is not set for the header, it uses the same DefaultCellStyle as the data cells. Listing 12-6. Setting DataGridView Properties and Adding Rows of Data
// Set properties of a DataGridView and fill with data private void CreateGrid() { // (1) Define column headers dataGridView1.ColumnCount = 3; dataGridView1.Columns[0].HeaderText = "Movie Title"; dataGridView1.Columns[1].HeaderText = "Year"; dataGridView1.Columns[2].HeaderText = "Director"; dataGridView1.Columns[1].Name = "Year"; dataGridView1.Columns[0].Width = 150; dataGridView1.Columns[1].Width = 40; dataGridView1.Columns[2].Width = 110; // (2) Define style for data cells DataGridViewCellStyle style = new DataGridViewCellStyle(); style.BackColor = Color.Bisque; style.Font = new Font("Arial", 8, FontStyle.Bold); style.ForeColor = Color.Navy; // (left,top,right,bottom) style.Padding = new Padding(5, 2, 5, 5); style.SelectionBackColor = Color.LightBlue; dataGridView1.DefaultCellStyle = style; // (3) Define style for column headers DataGridViewCellStyle styleHdr = new DataGridViewCellStyle(); styleHdr.Padding = new Padding(1, 1, 1, 1); styleHdr.BackColor = Color.OldLace; styleHdr.ForeColor = Color.Black; dataGridView1.ColumnHeadersDefaultCellStyle = styleHdr; // (4) Define user capabilities dataGridView1.AllowUserToAddRows = false; dataGridView1.AllowUserToOrderColumns = false; dataGridView1.AllowUserToDeleteRows = false; // (5) Place data in grid manually (datasource is better) object[] row1 = {"Casablanca", "1942","Michael Curtiz"}; dataGridView1.Rows.Add(row1); object[] row2 = {"Raging Bull","1980","Martin Scorsese"}; dataGridView1.Rows.Add(row2); object[] row3 = {"On the Waterfront","1954","Elia Kazan"}; dataGridView1.Rows.Add(row3); object[] row4 = {"Some Like it Hot","1959","Billy Wilder"}; dataGridView1.Rows.Add(row4); }
Figure 12-7 shows the DataGridView created by this code. Figure 12-7. DataGridView built from code in Listing 12-6
DataBinding with a DataGridView
A DataGridView is bound to a data source using complex binding. As in our list box example, the DataSource property specifies the data source. The similarity ends there, however, because a DataGridView must display multiple data values. To do so, the DataMember property is set to the name of a table within the data source. The data to be displayed in each column is specified by setting the column's DataPropertyName property to the name of the underlying data table column. // Turn this off so column names do not come from data source dataGridView1.AutoGenerateColumns = false; // Specify table as data source dataGridView1.DataSource = ds; // Dataset dataGridView1.DataMember = "movies"; // Table in dataset // Tie the columns in the grid to column names in the data table dataGridView1.Columns[0].DataPropertyName = "Title"; dataGridView1.Columns[1].DataPropertyName = "Year"; dataGridView1.Columns[2].DataPropertyName = "director"; The DataGridView supports two-way data binding for ADO.NET data sources: Changes made to the grid are reflected in the underlying table, and changes made to the table are reflected in the grid. For example, this code responds to a button click by adding a new row to the grid's data source. The addition is immediately reflected in the control. However, if we try to add a row directly to the DataGridView, an exception occurs because adding directly to a bound control is not permitted. private void buttonAdd_Click(object sender, EventArgs e) { // Adding to underlying table is okay r[0] = "TAXI"; r[1] = "1976"; r[2] = "Martin Scorsese"; dt.Rows.Add(r); // Adding directly to DataGridView does not work object[] row = {"The Third Man", "1949", "Orson Welles"}; DataRow r = dt.NewRow(); DataGridView1.Rows.Add(row4); // Fails! } Updating the original database from which a grid is loaded can be done by issuing individual SQL commands or using a DataAdapter. The discussion in the previous section applies. Core Note
Setting the Row Height
The default height of rows in a DataGridView is based on accommodating a single line of text. If the row contains large sized fonts or images, they are truncated. It is usually better to force the grid to take the size of each cell in the row into account and base the overall height on the tallest cell. That's the role of the grid's AutoSizeRows method. Its simplest overloaded version takes a single parameter a DataGridViewAutoSizeRowsMode enumeration value that indicates the criterion used for setting row height. The two most useful enumeration members are ColumnAllRows, which bases the row height on all columns in the row, and ColumnsDisplayedRows, which applies the same criterion, but to visible rows only. dataGridView1.AutoSizeRows( DataGridViewAutoSizeRowsMode.ColumnsAllRows); The AutoSizeRows method sets the row size when it is executed. If subsequent updates cause the height of cells in a row to change, the row height does not adjust to the changes. Also, if a row is sortable, clicking a column header to sort the grid causes all rows to revert to the default row height. Fortunately, the DataGridView has an AutoSizeRowsMode property that causes row heights to automatically adjust to changes in grid content. dataGridView1.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.HeaderAndColumnsAllRows;
Note that this statement does not take effect until the AutoSizeRows method is executed, and that it prevents users from manually resizing rows. Working with Columns and Column Types
The DataGridView is not a full-blown spreadsheet, but it does offer some features a user expects from a spreadsheet. These include the following:
Any of the column types may be bound to a data source. Although a button is usually set manually, it can be bound to a property in the grid's data source in two ways: // Use the DataGridviewButtonColumn class buttons.DataPropertyName = "Title"; // Use the Columns class (button is in column 1 of the grid) dataGridView3.Columns[1].DataPropertyName = "Title"; Buttons provide a convenient way for a user to select a grid row and trigger an action such as a pop-up form that displays further information related to the row. Buttons located in grid cells, however, have no direct event, such as a Click, associated with them. Instead, events are associated with an action on the overall grid or specific cells on the grid. By identifying a cell for instance, an event handler can determine which button is clicked. Events
Just about every mouse and cursor movement that can occur over a DataGridView can be detected by one of its events. In addition, events signify when data is changed, added, or deleted. Table 12-2 provides a summary of the most useful events. Accompanying the table is a list of the delegate used to implement these events. (See Appendix B for a complete list of events.)
The following are delegates associated with events in Table 12-2: (1) public sealed delegate void DataGridViewCellEventHandler( object sender, DataGridViewCellEventArgs e) (2) public sealed delegate void DataGridViewCellM_useEventHandler( object sender, DataGridViewCellMouseEventArgs e) (3) public sealed delegate void EventHandler( object sender, EventHandlerArgs e) (4) public sealed delegate void DataGridViewRowEventHandler ( object sender, DataGridViewRowEventArgs e) (5) public sealed delegate void DataGridViewCellFormattingEventHandler( object sender, DataGridViewCellFormattingEventArgs e) (6) public sealed delegate void DataGridViewCellPaintingEventHandler( object sender, DataGridViewCellPaintingEventArgs e) (7) public sealed delegate void DataGridViewDataErrorEventHandler( object sender, DataGridViewDataErrorEventArgs e)
Let's look at some common uses for these events. Cell Formatting
The CellFormatting event gives you the opportunity to format a cell before it is rendered. This comes in handy if you want to distinguish a subset of cells by some criteria. For example, the grid in Figure 12-7 contains a column indicating the year a movie was released. Let's change the background color of cells in that column to red if the year is less than 1950. // Set cells in year column to red if year is less than 1950 private void Grid3_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (this.dataGridView3.Columns[e.ColumnIndex].Name == "Year") { string yr = (string)e.Value; if (Int32.Parse(yr) < 1950) { e.CellStyle.ForeColor = Color.Red; e.CellStyle.SelectionForeColor = Color.Red; // Indicate that event was handled e.FormattingApplied = true; } } } The ColumnIndex property of the EventArgs parameter is used to determine if the year column is being formatted. If so, the code checks the year and formats the cell accordingly. Note that the FormattingApplied property must be set if custom formatting is performed. Recognizing Selected Rows, Columns, and Cells
As shown in Table 12-2, selecting a cell in a grid can trigger any number of events that can be used to indicate the current cell or the cell just left. Some of the events are almost over-engineered. For example, there seems little to distinguish CellContentClick and CellClick. Others exist to recognize grid navigation using both the mouse and keyboard: The CellClick is not triggered by arrow keys; however, the CellEnter event is fired no matter how a cell is selected. All of these cell-related events have a consistent event handler signature. The EventArgs parameter provides column and row index properties to identify the cell. Here is an example: private void Grid1_CellEnter(object sender, DataGridViewCellEventArgs e) { // Both of these display the column index of the selected cell MessageBox.Show("enter "+e.ColumnIndex.ToString()); MessageBox.Show( DataGridView1.CurrentCell.ColumnIndex.ToString()); } Core Note
The cell events can be used to recognize a single row and column selection. However, a grid may also permit multiple row, column, and cell selections. In these cases, it is necessary to use the SelectedRows, SelectedColumns, and SelectedCells collections to access the selected grid values. Multiple row selection is made available on a DataGridView by setting its MultiSelect property to TRue which is the default value. A row is selected by clicking its row header. It can also be selected by clicking any cell in the row if the grid's SelectionMode property is set to DataGridViewSelectionMode.FullRowSelect. The property can also be set to FullColumnSelect, which causes a cell's column to be selected. Note that column and row selection are mutually exclusive: only one can be in effect at a time. This segment illustrates how to iterate through the collection of selected rows. The same approach is used for columns and cells. // Display selected row numbers and content of its column 1 if (dataGridView1.SelectedRows.Count > 0) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < dataGridView1.SelectedRows.Count; i++) { sb.Append("Row: "); sb.Append( dataGridView1.SelectedRows[i].Index.ToString() ); sb.Append( dataGridView1.SelectedRows[i].Cells[1].Value); sb.Append(Environment.NewLine); } MessageBox.Show (sb.ToString(), "Selected Rows"); }
Data Error Handling
The DataError event fires when a problem occurs loading data into a grid or posting data from the grid to the underlying data store. The error is quite easy to detect: compare the value of the Context property of the ErrorEventArgs parameter with the DataGridViewDataErrorContext enumeration values. Here is an example: // Define event handler DataGridView1.DataError += new DataGridViewDataErrorEventHandler(DataGridView1_DataError); // DataError Event Handler private void dataGridView1_DataError(object sender, DataGridViewDataErrorEventArgs dgError) { // Context provides information about the grid when the // error occurred. MessageBox.Show("Error: " + dgError.Context.ToString()); // Problem committing grid data to underlying data source if (dgError.Context == DataGridViewDataErrorContext.Commit) { MessageBox.Show("Commit error"); } // Occurs when selection cursor moves to another cell if (dgError.Context == DataGridViewDataErrorContext.CurrentCellChange) { MessageBox.Show("Cell change"); } if (dgError.Context == DataGridViewDataErrorContext.Parsing) { MessageBox.Show("parsing error"); } // Could not format data coming from/going to data source if (dgError.Context == DataGridViewDataErrorContext.Formatting) { MessageBox.Show("formatting error"); } } Setting Up Master-Detail DataGridViews
One of the more common relationships between tables in a database is that of the master-detail relationship, where the records in the master table have multiple associated records in the detail table. DataGridViews provide a natural way of displaying this relationship. To illustrate, let's create an application based on the Films database that displays a master grid containing a list of movies and a detail grid that display actors who played in the movie selected in the first grid. To make it interesting, we'll include an image column in the movie grid that contains a picture of the Oscar statuette for movies that won for best picture. The master grid is bound to the movies table; the details grid is bound to the actors table. Both tables, as shown in Figure 12-8, contain the columns that are bound to their respective DataGridView columns. In addition, they contain a movieID column that links the two in the master-detail relationship. Figure 12-8. Master-detail tables
The tables and their relationships are created using the techniques described in Chapter 11: ds = new DataSet(); DataTable dt = new DataTable("movies"); // Master DataTable da = new DataTable("actors"); // Detail da.Columns.Add("movieID"); da.Columns.Add("firstname"); da.Columns.Add("lastname"); // dt.Columns.Add("movieID"); dt.Columns.Add("Title"); dt.Columns.Add("Year"); dt.Columns.Add("picture", typeof(Bitmap)); // To hold image ds.Tables.Add(dt); ds.Tables.Add(da); // Define master-detail relationship DataRelation rel = new DataRelation("movieactor", dt.Columns["movieID"], da.Columns["movieID"]); ds.Relations.Add(rel);
After defining the table schemas, they are populated from the database using a DataReader object. Because the database does not contain an image although it could the image is inserted based on the value of the bestPicture field. Bitmap oscar = new Bitmap(@"c:\oscar.gif"); // Oscar image Bitmap nooscar = new Bitmap(@"c:\nooscar.gif"); // Blank image // Populate movies table from datareader while (dr.Read()) { DataRow drow = dt.NewRow(); drow["Title"] = (string)(dr["movie_Title"]); drow["Year"] = ((int)dr["movie_Year"]).ToString(); drow["movieID"] = (int)dr["movie_ID"]; if ((string)dr["bestPicture"] == "Y") drow["picture"] = oscar; else drow["picture"] = nooscar; dt.Rows.Add(drow); }
The actors table is filled with the results of the query: sql = "SELECT am.movie_ID, actor_first,actor_last FROM actors a JOIN actor_movie am ON a.actor_ID = am.actor_ID";
After the tables are created and populated, the final steps are to define the grids and bind their columns to the tables. This segment adds three columns to the master grid one of which is an image type column. DataGridViewImageColumn vic = new DataGridViewImageColumn(); dataGridView1.Columns.Add(vic); // Add image type column // dataGridView1.ColumnCount = 3; dataGridView1.Columns[0].Name = "Oscar"; dataGridView1.Columns[1].HeaderText = "Movie Title"; dataGridView1.Columns[2].HeaderText = "Year";
Then, the binding is performed: // Bind grids to dataset dataGridView1.DataSource = ds; dataGridView1.DataMember = "movies"; dataGridView2.DataSource = ds; // ***Set to DataRelation for detail dataGridView2.DataMember = dt.TableName+".movieactor"; // Bind grid columns to table columns dataGridView1.Columns[0].DataPropertyName = "picture"; dataGridView1.Columns[1].DataPropertyName = "Title"; dataGridView1.Columns[2].DataPropertyName = "Year"; dataGridView1.Columns[3].DataPropertyName = "director"; dataGridView2.Columns[0].DataPropertyName = "firstname"; dataGridView2.Columns[1].DataPropertyName = "lastname"; Pay close attention to the binding of dataGridView2. It is bound to the relationship defined between the tables, rather than directly to the actors table. This binding causes the names of the movie's cast to be displayed in the grid when a movie is selected. Figure 12-9 shows a sample screen. Much of the excluded code in this example deals with setting grid styles and capabilities. A full code listing is available in the book's code download. (See the Preface for the download URL addresses and instructions.) Figure 12-9. Master-detail relationship
Virtual Mode
When a DataGridView is bound to a data source, the entire data source must exist in memory. This enables quick refreshing of the control's cells as a user navigates from row to row. The downside is that a large data store may have prohibitive memory requirements. To handle excessive memory requirements, a DataGridView can be run in virtual mode by setting its VirtualMode property to TRue. In this mode, the application takes responsibility for maintaining an underlying data cache to handle the population, editing, and deletion of DataGridView cells based on actions of the user. The cache contains data for a selected portion of the grid. If a row in the grid cannot be satisfied from cache, the application must load the cache with the necessary data from the original data source. Figure 12-10 compares virtual storage with binding to a DataTable. Figure 12-10. Data binding versus virtual mode
Virtual mode implementation requires that an application handle two special virtual mode events: CellValueNeeded, which occurs when a cell value must be displayed; and CellValuePushed, which occurs when a cell's value is edited. Other events are also required to manage the data cache. These are summarized in Table 12-3.
To illustrate the fundamentals of implementing a DataGridView in virtual mode, let's look at the code used to create the DataGridView shown in Figure 12-11. Figure 12-11. DataGridView using virtual mode
The variables having class scope are shown here. Note that the data cache is implemented as a generics List object that holds instances of the movie class. The movie class exposes three properties that are displayed on the grid: Title, Movie_Year, and Director. DataGridView dgv; List<movie> movieList = new List<movie>(20); // cache bool rowNeeded; // True when new row appended to grid int storeRow = 0; int currRow = -1; // Set to row being added movie currMovie; // Holds movie object for current row
Listing 12-7 shows the overhead code to initialize the DataGridView, register the event handlers, and populate the data cache (this would usually come from a database). Listing 12-7. Virtual DataGridView: Initialization
// Set properties of a DataGridView and fill with data dgv = new DataGridView(); // Event handlers for virtual mode events dgv.CellValueNeeded += new DataGridViewCellValueEventHandler(CellNeeded); dgv.CellValuePushed += new DataGridViewCellValueEventHandler(CellPushed); dgv.NewRowNeeded += new DataGridViewRowEventHandler(RowNeeded); // Event handlers always available for DataGridView dgv.UserDeletingRow += new DataGridViewRowCancelEventHandler (RowDeleting); dgv.RowValidated += new DataGridViewCellEventHandler( RowValidated); dgv.VirtualMode = true; dgv.RowCount = 5; dgv.ColumnCount = 3; // Headers for columns dgv.Columns[0].HeaderText = "title"; dgv.Columns[1].HeaderText = "year"; dgv.Columns[2].HeaderText = "director"; // Fill cache. In production, this would come from database. movieList.Add(new movie("Citizen Kane",1941,"Orson Welles")); movieList.Add(new movie("The Lady Eve",1941," "Preston Sturges")); // ... Add other movies here
The heart of the application is represented by the event handler methods shown in Listing 12-8. To summarize them:
Listing 12-8. Virtual DataGridView: Event Handlers
// Called when a new row is appended to grid private void RowNeeded(object sender, DataGridViewRowEventArgs e) { rowNeeded = true; currRow = dgv.Rows.Count - 1; } // Called when a cell must be displayed/refreshed private void CellNeeded(object sender, DataGridViewCellValueEventArgs e) { if (rowNeeded) { rowNeeded = false; currMovie = new movie(); return; } storeRow = MapRow(e.RowIndex); if(storeRow >=0 && currRow ==-1) currMovie = movieList[storeRow]; string colName = dgv.Columns[e.ColumnIndex].HeaderText; if(storeRow>=0) // Refresh cell from cache { if (colName == "title")e.Value = movieList[storeRow].Title; if (colName == "year") e.Value = movieList[storeRow].Movie_Year.ToString(); if (colName == "director") e.Value = movieList[storeRow].Director; } else // refresh cell from object for new row { if (colName == "title")e.Value = currMovie.Title; if (colName == "year")e.Value = currMovie.Movie_Year.ToString(); if (colName == "director") e.Value = currMovie.Director; } } // Cell has been updated private void CellPushed(object sender, DataGridViewCellValueEventArgs e) { // Update property on movie object for this row storeRow = MapRow(e.RowIndex); string colName = dgv.Columns[e.ColumnIndex].HeaderText; if (colName == "title") currMovie.Title = (string)e.Value; if (colName == "year") { int retval; if(int.TryParse((string)e.Value,out retval)) currMovie.Movie_Year = retval; } if (colName == "director") currMovie.Director = (string)e.Value; } // Occurs when user changes current row // Update previous row in cache when this occurs private void RowValidated(object sender, DataGridViewCellEventArgs e) { storeRow = MapRow(e.RowIndex); if (storeRow < 0) storeRow = movieList.Count; currRow = -1; if (currMovie != null) { // Save the modified Customer object in the data store. storeRow = MapRow(e.RowIndex); if (storeRow >= 0) movieList[storeRow] = currMovie; else movieList.Add(currMovie); currMovie = null; } } // Row selected and Del key pushed private void RowDeleting(object sender, DataGridViewRowCancelEventArgs e) { if (MapRow(e.Row.Index)>=0) { movieList.RemoveAt(e.Row.Index); } if (e.Row.Index == currRow) { currRow = -1; currMovie = null; } } // Maps grid row to row in cache. More logic would be added // for application that refreshes cache from database. private int MapRow(int dgvRow) { if (dgvRow < movieList.Count)return dgvRow; else return -1; }
This example provides only the basic details for implementing a virtual DataGridView. The next step is to extend it to include a virtual memory manager that reloads the cache when data must be fetched from disk to display a cell. |
< Day Day Up > |