After you’ve assembled your application from the required forms, controls, and compo-nents, you build it and deploy it. To run, however, applications need certain information, and that often differs between installations, users, and application sessions. Consequently, you can’t compile this information directly into the application assemblies. Instead, the information must reside in a location independent from those assemblies, from which it is read and to which it is written as needed during execution. To solve this problem, .NET pro-vides a complete infrastructure whose fundamental element is the setting.
.NET considers there to be two types of settings: those for users and those for appli-cations. User settings, such as information you might find in a Tools | Options dialog for an application like VS05, change from one application session to the next. Application set-tings, such as database connection strings, change from one installation to the next. You can add one or more of each type of setting to your application by using the Settings Editor. To open this editor, right-click your project and select Properties | Settings, as shown in Figure 1.15.
Figure 1.14 Hosting a User Control
Each setting has a name, a type, a scope, and a value. The name is the way you refer to the setting; type specifies the type of value it stores; scope determines whether a setting is a user setting or an application setting; and value is the setting’s initial value. All the settings you create are stored in your project’s app.config file, in which they are grouped by scope:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
...
</configSections>
<userSettings>
<MySecondApp.Properties.Settings>
<setting name="WindowLocation" serializeAs="String">
<value>100, 100</value>
</setting>
</MySecondApp.Properties.Settings>
</userSettings>
<applicationSettings>
<MySecondApp.Properties.Settings>
<setting name="Pi" serializeAs="String">
<value>3.1415927</value>
</setting>
</MySecondApp.Properties.Settings>
</applicationSettings>
</configuration>
When you build your application, the content and settings stored in app.config are copied into your application’s configuration file, which is named ApplicationName.exe.config.
Figure 1.15 Configuring Settings
When your app executes, it needs a way to retrieve these values and, if necessary, save new values. To provide a simple way to do this, VS05 generates a special class, Settings, in your project:
namespace MySecondApp.Properties {
internal sealed class Settings : ApplicationSettingsBase { public static Settings Default {
get {...}
}
public Point Location { get {...}
set {...}
}
public decimal Pi { get {...}
} } }
This class, generated in the ApplicationName.Properties namespace, exposes each setting as a property that holds a value of the type specified when the setting was added. Although you could create an instance of this class manually, you can use the static Default method to take on that burden for you. The Settings class derives from ApplicationSettingsBase, a .NET class located in the System.Configuration namespace that implements all the support to read and write settings. This support is encapsulated by the generated properties, so all you need to worry about is the Settings class itself. Additionally, because the properties are strongly typed, you’ll receive compile-time errors if you use them incorrectly.
You may have noticed that the user setting is read-write, whereas the application setting is read-only. User settings are read-write to allow users to change values between applica-tion sessions. In contrast, applicaapplica-tion settings are likely to store configuraapplica-tion informaapplica-tion;
so to prevent developers from writing code that could change them—potentially breaking the application—application settings are generated as read-only properties. The following code shows how you might use both user and application settings at run-time:
// Read an application setting
decimal pi = Properties.Settings.Default.Pi;
// Write to an application setting // NOTE: This won't compile
Properties.Settings.Default.Pi = 3.142;
// Write a user setting
Properties.Settings.Default.WindowLocation = this.Location;
When you use the Settings class like this, the settings values are initially retrieved from the application’s configuration file and subsequently are operated on in memory. But because user settings need to be persisted across application sessions, all user-scoped property val-ues held by the Settings object should be persisted back to the configuration file if changed.
To do this, you call the Save method on the Settings class:
void saveSettingsButton_Click(object sender, EventArgs e) { // Save all user settings
Properties.Settings.Default.Save();
}
Changed user settings are not stored back to an application’s configuration file, as you might expect; the only settings stored in an application’s configuration file are application settings and default user settings. Altered user settings are persisted to a file named user.config, which is placed in one of several Windows logo-compliant locations within the file system, depending on where the application is installed and whether the user is roam-ing. The path to user.config for a locally installed application executed by a nonroaming user conforms to the following:
%SystemDrive%\Documents and Settings\UserName\
Local Settings\Application Data\ProductName\
ApplicationName.exe_Url_UrlHash\AssemblyVersionNumber
Sometimes, users change settings to values they are not happy with and then can’t remember what the previous defaults were. Fortunately, the settings infrastructure offers two simple backup options to rollback to the previous settings values. First, you can pro-vide a mechanism for users to revert to the last saved settings by calling the Settings object’s Reload method:
void reloadSettingsButton_Click(object sender, EventArgs e) { // Revert to last saved user settings
Properties.Settings.Default.Reload();
}
Second, if user settings are damaged beyond recovery, you can allow users to revert to the application’s default installed user settings—the default values stored in the application’s configuration file. Retrieving them is a matter of calling the Settings object’s Reset method:
void resetSettingsButton_Click(object sender, EventArgs e) { // Revert to default installed user settings
Properties.Settings.Default.Reset();
}
User settings are managed so that if the user.config file is deleted, the default values for those settings are loaded into the next application session. This is the same as calling the Reset method.
The settings subsystem comes with more exotic capabilities, including versioning support, settings profiles, and even the ability to create custom settings providers, a fea-ture that lets you save user settings to, for example, a web service. Additionally, you can bind form and control properties directly to settings from the Properties window, a prac-tice that can save a lot of coding effort. To explore these in detail, look at Chapter 15:
Settings.
Resources
Application and user settings data is used to control an application’s look and feel, as well as its behavior, while remaining separate from the code itself. Alternatively, this kind of application and control data can be stored as part of an assembly’s resources. A resource is a named piece of data bound into the executable (EXE) or dynamic link library (DLL) at build time. For example, you could set the background image of a form in your applica-tion by loading a bitmap from a file:
// ResourcesForm.cs
partial class ResourcesForm : Form { public ResourcesForm() {
InitializeComponent();
this.BackgroundImage =
new Bitmap(@"C:\WINDOWS\Web\Wallpaper\Azul.jpg");
} }
Unfortunately, the problem with this code is that not all installations of Windows have Azul.jpg, and even those that have it may not have it in the same place. Even if you shipped this picture with your application, a space-conscious user might decide to remove it, caus-ing your application to fault. The only safe way to make sure that the picture, or any file, stays with code is to embed it as a resource.
Resources can be conveniently embedded in two ways. First, you can use the Resource Editor, which can be opened by right-clicking on your project in Solution Explorer and choosing Properties | Resources. The Resource Editor provides a simplified UI, shown in Figure 1.16, that allows you to manage resources and, just as important, see what your resources will look like at design time.
With the Resource Editor, you can add new and existing resources in a variety of ways, including using the drop-down list shown in Figure 1.17, pasting them from the Clip-board, or dragging and dropping onto the Resource Editor itself.
All resources added to and managed by the Resource Editor are categorized by resource type. You can use the drop-down list shown in Figure 1.18 to navigate between categories.
In addition to the categories you would expect to find—strings, images, icons, text files, and sound files—there is another category, Other, for extra resource data such as component-defined serialization of design-time data. Depending on the category, you can
Figure 1.16 The Resource Editor
Figure 1.17 Adding a Resource with the Resource Editor
even view your resources in one of several ways. For example, you can display image resources using the List, Details, or Thumbnails view.
By default, string resources added with the Resource Editor are embedded in Resources.resx, a resource file located in the Properties folder of a Windows Forms project.
Other resources are copied into a local project folder named “Resources” and linked (rather than embedded) with a file path reference stored in Resources.resx. As with settings, VS05 exposes your resources as strongly typed properties of the Resources class. Here’s the abridged version:
internal class Resources { ...
internal static Bitmap Azul { get; } ...
}
Apart from the advantage of compile-time type checking, the code you write to use resources is simple:
// Load strongly typed image resource
this.BackgroundImage = Properties.Resources.Azul;
If you are currently working on a form, control, or component, you can avoid having to write this code: Set the value of many properties by using the Properties window directly.
For example, to set the background image for a form, you merely press the ellipses (“ . . . ”) Figure 1.18 Viewing Resource Categories
button in the Properties window next to the BackgroundImage property, opening the Select Resource dialog shown in Figure 1.19.
This dialog allows you to import or select a resource from the form itself (if you choose Local Resource) or from your project’s Resources.resx file (or any additional .resx files you may have added). This action causes the image to be shown in the Designer and generates the code that loads the resource at run time:
// Form1.cs
partial class Form1 : Form { public Form1() {
InitializeComponent();
} }
// Form1.Designer.cs partial class Form1 {
...
void InitializeComponent() { ...
this.BackgroundImage = Properties.Resources.Azul;
...
} }
As you can see, the generated code is pretty much the same code you’d write yourself.
Figure 1.19 Selecting a Resource for a Form Property
For more details about resources and their relationship to localization and internation-alization, see Chapter 13: Resources.
Dialogs
You’ve seen how to create and show forms, but you can instead show them as dialogs.
Although it’s not always the case, dialogs are typically modal and exist to take information from a user before a task can be completed—in other words, a dialog is a form that has a
“dialog” with the user. For example, we created the Options dialog in Figure 1.20 by right-clicking on a project in Solution Explorer and choosing Add Windows Form. Implementing the form was a matter of exposing the favorite color setting as a property, dropping the con-trols onto the form’s design surface, and setting the ControlBox property to false so that it looks like a dialog.
You can use this form as a modal dialog by calling the ShowDialog method:
// OptionsDialog.cs
partial class OptionsDialog : Form { public OptionsDialog() {
InitializeComponent();
}
void optionsToolStripMenuItem_Click(object sender, EventArgs e) { OptionsDialog dlg = new OptionsDialog();
dlg.FavoriteColor = this.BackColor;
if( dlg.ShowDialog() == DialogResult.OK ) { this.BackColor = dlg.FavoriteColor;
} } }
Notice that the custom OptionsDialog class is instantiated, but before it’s shown, the ini-tial color value is passed in via the FavoriteColor property. When the modal ShowDialog method completes, it returns the value of the DialogResult enumeration—in this case, either OK or Cancel. Although it’s possible to implement the Click events for the OK and
Figure 1.20 A Dialog
Cancel buttons inside the OptionsDialog class, there’s a much easier way to make OK and Cancel act as they should: You set each button’s DialogResult property appropriately, and set the OptionsDialog form’s AcceptButton and CancelButton properties to the OK button and the Cancel button, respectively. In addition to closing the dialog and returning the result to the caller of ShowDialog, setting these properties enables the Enter and Esc keys and highlights the OK button as the default button on the form.
You may still feel the need to handle the OK Click event to validate data captured by the dialog. Although you can do that, Windows Forms provides built-in support for validation.
In conjunction with an ErrorProvider component, you can handle the control’s Validating event and thereby validate the contents of each control when the user moves focus from the control. For example, if we want the user to specify a color with some green in it, we can drop an ErrorProvider component onto the OptionsDialog form and handle the Validating event for the Change button whenever it loses focus:
// OptionsDialog.cs
partial class OptionsDialog : Form { ...
void changeColorButton_Validating(object sender, CancelEventArgs e) { byte greenness = this.changeColorButton.BackColor.G;
string err = "";
if( greenness < Color.LightGreen.G ) {
err = "I'm sorry, we were going for leafy, leafy...";
e.Cancel = true;
}
this.errorProvider.SetError(changeColorButton, err);
} }
In the Validating event handler, notice that we set the CancelEventArgs.Cancel property to true; this cancels the loss of focus from the control that caused the validating event. Also notice the ErrorProvider.SetError invocation; SetError accepts as arguments the control that is being validated and a string, which is the message displayed by the ErrorProvider. When this string is null, the error provider’s error indicator for that control is hidden. When this string contains something, the error provider shows an icon to the right of the control and provides a tooltip with the error string, as shown in Figure 1.21.
Figure 1.21 ErrorProvider Providing an Error (See Plate 1)
The Validating event handler is called whenever focus is moved from a control whose CausesValidation property is set to true (the default) to another control whose CausesVal-idation property is also set to true.
One side effect of setting CancelEventArgs.Cancel to true is that focus is retained on an invalid control until valid data is entered, thereby preventing users from navigating away from the control. The Form class’s AutoValidate property dictates this behavior to remain consistent with previous versions of Windows Forms. AutoValidate’s default value is EnablePreventFocusChange. If you prefer to let your users navigate away from invalid con-trols—generally considered better from a user’s point of view—you can change AutoVali-date to EnableAllowFocusChange in the Properties window, as shown in Figure 1.22.
The host form’s AutoValidate value applies to all controls hosted by the form that per-form validation. AutoValidate and validation are explored further in Chapter 3: Dialogs.
If you do allow free navigation across invalid controls, it means that users can conceiv-ably tab to, or click, a form’s AcceptButton without having any valid data on the form. Con-sequently, you need to write additional code to validate the entire form from the AcceptButton. Fortunately, the Form class exposes the Validate method for this purpose:
// OptionsDialog.cs
partial class OptionsDialog : Form { ...
void okButton_Click(object sender, EventArgs e) { // Validate form
bool isValid = this.Validate();
// Don't close form if data is invalid
if( !isValid ) this.DialogResult = DialogResult.None;
} }
Figure 1.22 Setting the AutoValidate Property to Allow Focus to Change from an Invalid Control
The Validate method fires the Validating event for each of the controls hosted on a form, harvesting the results along the way. If any of the Validating event handlers set CancelEventArgs.Cancel to true, Validate returns false.
ErrorProvider and the Validating event give you most of what you need for basic vali-dation, but more complicated validation scenarios require some custom coding. Chapter 3 explores these. It also discusses the standard Windows Forms dialogs and explains how to support communication between your modal and modeless dialogs and other parts of your application.
Drawing
As nifty as all the built-in controls are, and as nicely as you can arrange them, sometimes you need to take things into your own hands and render the state of your form or control yourself. For example, if you want to compose a fancy About dialog, as shown in Fig-ure 1.23, you must handle the form’s Paint event and do the drawing yourself.
Figure 1.23 Custom Drawing (See Plate 2)
The following is the Paint event-handling code to fill the inside of the About dialog:
// AboutDialog.cs using System.Drawing;
using System.Drawing.Drawing2D;
partial class AboutDialog : Form { ...
void AboutDialog_Paint(object sender, PaintEventArgs e) {
Graphics g = e.Graphics;
g.SmoothingMode = SmoothingMode.AntiAlias;
Rectangle rect = this.ClientRectangle;
int cx = rect.Width;
int cy = rect.Height;
float scale = (float)cy / (float)cx;
LinearGradientBrush brush =
ColorBlend blend = new ColorBlend();
blend.Colors =
new Color[] { Color.Red, Color.Green, Color.Blue };
blend.Positions = new float[] { 0, .5f, 1 };
StringFormat format = new StringFormat();
try {
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
string s = "Ain't graphics cool?";
g.DrawString(s, this.Font, brush, rect, format);
}
Notice the use of the Graphics object from the PaintEventArgs passed to the event han-dler. This provides an abstraction around the specific device we’re drawing on, which we do with constructs like pens, brushes, shapes, and text. All this and more are explored in Chapter 5: Drawing Basics, Chapter 6: Drawing Text, and Chapter 7: Advanced Drawing.
You may be wondering what the try-finally blocks are for. Because the pen and brush objects hold underlying resources managed by Windows, we’re responsible for releasing the resources when we’re finished, even in the face of an exception. Like many classes in .NET, the Pen and Brush classes implement the IDisposable interface, which serves as a sig-nal for an object’s client to call the IDisposable Dispose method when it’s finished with an object. This lets the object know that it’s time to clean up any unmanaged resources it’s holding, such as a file, a database connection, or a graphics resource.
To simplify things in C#, you can replace the try-finally block with a using block (shown here for the Brush object):
// AboutDialog.cs using System.Drawing;
using System.Drawing.Drawing2D;
partial class AboutDialog : Form { ...
void AboutDialog_Paint(object sender, PaintEventArgs e) { using( LinearGradientBrush brush =
new LinearGradientBrush(this.ClientRectangle, Color.Empty,
Color.Empty, 45) ) { ...
// Wrap Pen and StringFormat usage in "using" blocks, too ...
} // brush.Dispose called automatically }
}
The C# using block instructs the compiler to wrap the code it contains in a try-finally block and call the IDisposable Dispose method at the end of the block for objects created as part of the using clause. This is a convenient shortcut for C# programmers, a good prac-tice to get into, and something you’ll see used extensively in the rest of this book.
Printing
Printing is just a matter of getting at another Graphics object that models the printer. We can
Printing is just a matter of getting at another Graphics object that models the printer. We can