
Visual CSharp .NET Developer's Handbook (2002) [eng]
.pdf
MessageBoxIcon.Error);
if (DR == DialogResult.Yes) // Exit the application. Close();
else
{
// Otherwise, reject the user changes. addressDataset1.RejectChanges(); addressDataset1.Clear(); AddressDA.Fill(addressDataset1);
}
}
catch (OleDbException ODBE)
{
//If an error occurs, see if the user wants
//to exit the application.
DR = MessageBox.Show("OLE-DB Error\r\n" +
ODBE.Message + "\r\n" +
ODBE.Source + "\r\n" +
"Exit Application?",
"Database Update Error",
MessageBoxButtons.YesNo,
MessageBoxIcon.Error);
if (DR == DialogResult.Yes) // Exit the application. Close();
else
{
// Otherwise, reject the user changes. addressDataset1.RejectChanges(); addressDataset1.Clear(); AddressDA.Fill(addressDataset1);
}
}
}
The actual update process is easy. All you need to do is call the Update() method of the AddressDA data adapter and supply the data set, addressDataset1, as input. However, this can be an error-prone process. The two common exceptions are DBConcurrencyException and OleDbException. You handle both exceptions in the same way. The application displays an error message that tells the user what went wrong and offers a choice of exiting the application or attempting to recover from the error. The update is lost, in either case, and you shouldn't attempt to retry the update.
The three-step recovery process works in most cases. First, you reject the current changes in the dataset using the RejectChanges() method to ensure they don't remain in memory and threaten the stability of your application again. Second, you clear the dataset to return it to its original state using the Clear() method. At this point, the dataset is clear and ready for data. It's empty, so you must provide new data to display or the application will generate an index range exception. The third step is to use the Fill() method of the data adapter to replace the missing data. At this point, the application is stable again and should continue processing data as before.

You might wonder how this simple piece of code could possibly perform every type of update the data adapter is capable of performing. Remember that the data adapter includes the
DeleteCommand, InsertCommand, SelectCommand, and UpdateCommand properties. These properties contain the commands that actually perform the work. When the code calls the Update() method, the data adapter calls upon the correct command to process the data updates.
Moving from record-to-record is another Grid view concern. The example application supports the four common record movement commands to move to the first, previous, next, and last records. However, it supports these commands from both the Record menu and the toolbar, so creating a common set of methods makes sense. The Record menu and toolbar event handlers simply call the correct common method for the desired record movement. Listing 11.2 shows the record movement command code.
Listing 11.2: Record Movement Commands
private void DoFirst()
{
// Move to the first record. AddrDataGrid.CurrentRowIndex = 0; AddrDataGrid.Select();
}
private void DoNext()
{
// Move to the next record.
if (AddrDataGrid.CurrentRowIndex != addressDataset1.Tables[0].Rows.Count - 1)
{
AddrDataGrid.CurrentRowIndex++;
AddrDataGrid.Select();
}
else
// Display an error message. MessageBox.Show("Already at last record!",
"Data Grid Pointer",
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
}
private void DoPrevious()
{
// Validate pointer location.
if (AddrDataGrid.CurrentRowIndex != 0) // Move to the previous record. AddrDataGrid.CurrentRowIndex--;
else
// Display an error message. MessageBox.Show("Already at first record!",
"Data Grid Pointer",
MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
AddrDataGrid.Select();
}
private void DoLast()
{
// Move to the last record.

AddrDataGrid.CurrentRowIndex = addressDataset1.Tables[0].Rows.Count - 1;
AddrDataGrid.Select();
}
DoFirst() is the easiest of the record movement commands. Simply set the Current-RowIndex property to 0, then use the Select() method to change the row pointer. The DoNext() and DoPrevious() method both validate the current row index using the Current-RowIndex property. If the row index isn't at the end of the dataset for DoNext() or beginning of the dataset for DoPrevious(), the code increments or decrements the CurrentRowIndex property as appropriate and uses Select() to make the change permanent. Otherwise, a message box appears to tell the user that they're at the beginning or end of the dataset. The DoLast() and DoNext() methods both require some means to find the end of the dataset. Theoretically the data grid should have an end of dataset event, but it doesn't, so the code uses addressDataset1.Tables[0].Rows.Count – 1 as the means for determining the last row.
At this point, we have a data Grid view that can position the record pointer. It can also delete, add, and modify records. Modern database applications have a lot of gizmos we haven't covered here, such as search and replace, spelling and grammar checkers, and a variety of other useful utilities. However, the example does provide everything you'd need from a basic application. Figure 11.11 shows the resulting application.
Figure 11.11: An example of a basic Grid view application.
Printing in Grid View
The database application shown in Figure 11.11 is still incomplete. While this application will provide essential data management functionality, it doesn't provide any type of printed output. This section looks at a custom print job. The "Working with Crystal Reports" section of this chapter tells you about an easier, but less custom, method of creating printed output.
Most applications provide three essential pieces of print functionality: printer setup, print preview, and print output. Listing 11.3 shows the implementation of these three key pieces of functionality for the example application.
Listing 11.3: Print, Print Preview, and Printer Setup Functionality
// Print object
PrintDialog PrnDialog; PrinterSettings PrnSettings;
private void mnuFilePageSetup_Click(object sender, System.EventArgs e)
{
//Use the current printer settings (default if not set). PrnDialog.PrinterSettings = PrnSettings;
//Show the printer setup dialog. PrnDialog.ShowDialog();
}
void DoPrintPreview()
{
// Create a printer document and a dialog to display it.
PrintDocument |
PD = new PrintDocument(); |
PrintPreviewDialog |
ShowPreview = new PrintPreviewDialog(); |
//Add an event handler for document printing details. PD.PrintPage += new PrintPageEventHandler(PD_PrintPage);
//Assign the printer document to the dialog and then display it. ShowPreview.Document = PD;
ShowPreview.ShowDialog();
}
void DoPrint() |
|
|
{ |
Dlg = new PrintDialog(); |
// Print Dialog |
PrintDialog |
||
PrintDocument |
PD = new PrintDocument(); |
// Document Rendering |
//Add an event handler for document printing details. PD.PrintPage += new PrintPageEventHandler(PD_PrintPage);
//Set up the Print Dialog.
Dlg.PrinterSettings = PrnSettings;
// Obtain the print parameters.
if (Dlg.ShowDialog() == DialogResult.OK)
{
//Set up the document for printing. PD.PrinterSettings = Dlg.PrinterSettings;
//Print the document.
PD.Print();
}
else
{
// Exit if the user selects Cancel. return;
}
}
void PD_PrintPage(Object sender, PrintPageEventArgs ev)
{
Font |
docFont; |
// Document Font |
Font |
headFont; |
// Heading Font |
Font |
columnFont; |
// Column Font |
float |
yPos = 20; |
// Position of text on page. |
int |
Counter; |
// Loop counter. |
DataTable |
Current; |
// Data table array. |
// Create |
the font. |
|
docFont = new Font("Arial", 11); headFont = new Font("Arial", 24);
columnFont = new Font("Arial", 12, FontStyle.Bold);
// Print the heading.
ev.Graphics.DrawString("KATZ! Corporation Contact List", headFont,
Brushes.Black,
20,
yPos);
yPos = yPos + headFont.GetHeight() + 20;
// Print the column headings. ev.Graphics.DrawString("Name",
columnFont,
Brushes.Black,
20,
yPos);
//
// See the source code CD for additional headings.
//
ev.Graphics.DrawString("Home Phone", columnFont, Brushes.Black, 695,
yPos);
yPos = yPos + columnFont.GetHeight() + 15;
//Continue printing as long as there is space on the page and
//we don't run out of things to write.
Current = addressDataset1.Tables[0];
for (Counter = 0; Counter < Current.Rows.Count; Counter++)
{
// Print the line of text. ev.Graphics.DrawString(AddrDataGrid[Counter, 0].ToString()
+ " " +
AddrDataGrid[Counter, 1].ToString() + " " +
AddrDataGrid[Counter, 2].ToString(), docFont,
Brushes.Black,
20,
yPos); ev.Graphics.DrawString(AddrDataGrid[Counter, 3].ToString(),
docFont,
Brushes.Black,
175,
yPos);
//
// See the source code CD for additional code.
//
ev.Graphics.DrawString(AddrDataGrid[Counter, 12].ToString(), docFont,
Brushes.Black,
695,
yPos);
// Determine the next print position. yPos = yPos + docFont.GetHeight() + 10;
}
// Tell the application there are no more pages to print. ev.HasMorePages = false;

}
PrnDialog and PrnSettings are two objects that many of the methods in this section share. PrnSettings contains the settings for the currently selected printer. If you want to use a different printer, then you need to regenerate the printer settings. As shown in the mnuFilePageSetup_Click() method, the PrnSettings object appears as input to the PrnDialog. The property settings within PrnSettings control the options that PrnDialog will present to the user. As Figure 11.12 shows, the default settings are minimal, but usable.
Figure 11.12: The Print dialog contains a minimal set of options unless you change the printer setting object options.
Because the Print Preview and Print options appear on the File menu as well as the toolbar, the example uses a common set of methods to implement both. The DoPrintPreview() method begins by creating a new print document, PD, and a print preview dialog, ShowPreview. PD has an event named PrintPage() that your code must handle or the application won't output any data. Both the print preview and the print methods can use the same event handler. In fact, you'll definitely want to use the same event handler for both to ensure that what the user sees on screen is what they'll see in the printed output. Once the event handler is in place, the code assigns PD to ShowPreview, then uses the ShowDialog() method to display the printed output.
It's important to remember that the print process relies on the printer settings stored in PrnSettings. If the user hasn't selected printer options, then the print routine will fail, no matter what else you want to do. The DoPrint() method performs essentially the same tasks as DoPrintPreview(). The big difference is that it must also ensure that PrnSettings contains valid information.
The PD_PrintPage() event handler performs the grunt work of outputting data to the printer. The print routine always handles one page at a time. Printing means drawing the image on a virtual screen, and then sending that screen to the output device. The .NET Framework makes printing somewhat easier than it was under the Win32 API by providing some items for you, such as the device context used for drawing. The PrintPageEventArgs object, ev, contains a Graphics object that you use for drawing purposes. The example uses the DrawString() method to output text to the display using one of three font objects. Notice that you must track the position of the drawing cursor on the virtual screen at all times to ensure the data remains within the page borders. Figure 11.13 shows the output from the drawing routine.

Figure 11.13: The print event handler draws the text and other visual elements on screen.
The final piece of code in the PD_PrintPage() method looks simple enough, but it's an essential part of the print routine. You must set the ev.HasMorePages property to false if the print routine is done printing, or the application will continue to call the print routine forever. This property ensures that the application continues to call the print routine until it runs out of pages to print, so you need to set it to true or false as needed.
Creating the Detail View
The Grid view is great for looking at a group of records at one time, but it also hides information for individual records. There are too many entries to see each record in its entirety all at once, so the application also requires a Detail view. The Detail view will show one record at a time, but it will show all of the data in that record.
Creating the Detail view means adding another Windows Form to the project using the Add → Add New Item command in Solution Explorer. The example uses textbox controls for all data fields, except for the Business field, which requires a checkbox. The Notes field contains multiple lines of text. You'll need to set the Multiline property of the txtNotes object to true, and then resize the textbox to fill the lower part of the display area.
The example application could have used a menu system and toolbar similar to the Grid view, but it uses buttons in this case. You need to supply a button to close the Detail view and at least four buttons for controlling the record pointer. Figure 11.14 shows the final output of the Detail view. Notice the use of the toolbar buttons as part of the record buttons—this addition helps the user see the relationship between the buttons on this page and the buttons on the toolbar.

Figure 11.14: The Detail view enables you to see an entire record at one time.
Coding the Detail View
The Detail view doesn't require quite as much code as the Grid view for display purposes because the Detail view is actually easier to develop and much of the data manipulation work is already complete. However, we do need a way to open the Detail view. Most users will expect to double-click on the row selector to open a Detail view, so we'll handle it with the AddrDataGrid_DoubleClick() method. When a user double-clicks a row, the code will need to create an instance of the DetailView class then use the ShowDialog() method of that class to display the Detail view. (We'll see in the "Synchronizing the Grid View and Detail View" section that you need to perform some additional work before the Detail view is ready to display.)
The constructor of the DetailView class will create the components as usual. It then needs to fill those components with data from the GridView class using some type of data exchange. Unfortunately, the data grid on the Grid view is inaccessible, even if you make it public. This means you have to find some one other means of exchanging data with the Detail view. The first technique is to send the information to the Detail View constructor. This approach only works if you want to create a Detail view that handles a single record at a time with no record movement. A second method is to provide some form of intermediate variable, which is the technique we'll use with the example.
Synchronizing the Grid View and Detail View
Data synchronization is an essential consideration when you have two views of the same data to work with. You'll need to perform several tasks to ensure the data in the first view remains in synchronization with data in the second view. Generally, you won't require more than two views because most users relate well to a Grid and Detail view combination; but there are times when you might require three or even four views, making the synchronization task even harder.
The first task is to get the data into the Detail view from the Grid view. In some cases, maintaining two connections to the database might work, but normally its better to rely on the Grid view as a source of data to conserve resources, improve performance, and maintain

reliability. Listing 11.4 shows the GridView class code for filling static variables with data. These variables must be static or you won't be able to access them from the DetailView class.
Listing 11.4: Creating Static Variables to Hold Grid Data
private void ExchangeFill()
{
// Fill the static variables with data.
ExchFirst = AddrDataGrid[AddrDataGrid.CurrentCell.RowNumber, 0].ToString();
//
// See the source code CD for additional code.
//
ExchNotes = AddrDataGrid[AddrDataGrid.CurrentCell.RowNumber, 16].ToString();
}
As you can see, the code relies on the Item enumerator of the AddrDataGrid control to access each cell in the current row. The Item enumerator takes the form AddrDataGrid[row, column] in this example. The code obtains the current row using the AddrDataGrid.CurrentCell.RowNumber property. The code selects each column in the row in turn to obtain the current cell value for the column and places it in the static string.
It might be tempting to access the static variables directly from the DetailView class. However, in this case, you'll want to use an accessor method to aid in smooth application performance, debugging, and data integrity. The DetailView class contains the ExchangeData() method shown in Listing 11.5.
Listing 11.5: The DetailView Class Provides a Central Data Exchange Method
private void ExchangeData()
{
string IsBusiness; // Business if true. // Obtain all of the data fields. txtFirst.Text = GridView.GetData("First");
//
// See the source code CD for additional code.
//
txtTelephone3.Text = GridView.GetData("Telephone3"); IsBusiness = GridView.GetData("Business");
if (IsBusiness == "True")
{
cbBusiness.Checked = true;
}
DateTime TempDate = DateTime.Parse(GridView.GetData("Contacted")); txtContacted.Text = TempDate.ToLongDateString();
txtProduct.Text = GridView.GetData("Product"); txtNotes.Text = GridView.GetData("Notes");
}
Notice that the ExchangeData() method calls upon the static GetData() method in the GridView class to obtain each static string. You could accomplish this task in a number of

ways, but the example uses an access string to make the GetData() method a single solution for all fields in the database. Notice the special handling required for both the cbBusiness and the txtContacted fields. The cbBusiness field requires a bool value, while the txtContacted field requires special formatting to obtain a usable display. The GetData() method is a simple case statement as shown in Listing 11.6.
Listing 11.6: The GetData() Method Acts as an Accessor
public static string GetData(string FieldName)
{
//Determine which field the calling program wants, then
//return the appropriate static value.
switch (FieldName)
{
case "First": return ExchFirst;
//
// See the source code CD for additional code.
//
case "Notes": return ExchNotes;
}
//If the FieldName didn't match any of the entries,
//return a default value.
return "Value Not Found";
}
You should notice something peculiar about this switch statement. In previous chapters, I always included a break or a goto after each case statement. In this case, if you add the break or goto after each case statement, the compiler will complain about inaccessible code; the return statement takes the place of the break or goto. The GetData() method also includes a default return value in case the developer provides an inaccurate input value.
At this point, the Detail view will display data when you first create it, but there's no functionality for updating the data as the user moves from one record to the next. The record update task is actually a two-part process. First, the GridView class must move the record pointer and update the static variables. Second, the DetailView class must recognize that the change has occurred and update the data displayed on the form. Listing 11.7 shows the GridView class portion of the code.
Listing 11.7: The GridView Class Updates Local Variables During a Record Change
public delegate void DataChangeDelegate();
public static event DataChangeDelegate DataChanged;
private void GoFirst(object sender, System.EventArgs e)
{
//Move to the next record. DoFirst();
//Fill the AddrDataGrid variables. ExchangeFill();