NET User Interfaces in Csharp Windows Forms and Custom Controls [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

NET User Interfaces in Csharp Windows Forms and Custom Controls [Electronic resources] - نسخه متنی

Matthew MacDonald

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید




































Control Projects



When designing a custom control, you could create the control class directly in your application project. In this case, you'll need to manually create the control and add it to the Controls collection of a Form in code. However, to add flexibility, reuse your control, and add design-time support, you need to create a dedicated project for your custom controls. Every project that needs to use the control then needs to add a reference to the compiled DLL assembly that contains the control classes.



The Class Library Project



Typically, you'll create your control as either a Class Library Project (for most custom control projects) or a Windows Control Project (for user controls). The choice you make doesn't make much difference—essentially all the project type does is configure the default references and the namespaces that are initially imported into your project. The important fact is that you are creating a library project, which creates a DLL assembly, instead of a stand-alone executable. This DLL can then be shared with any other project that needs to use the control. Figure 7-1 shows the option you must select to create a Class Library project.




Figure 7-1: Creating a control project


When you begin your control project, you will probably find that you need to add a few assembly references and import some namespaces. If you try to use a standard type and you receive an error, the problem is probably a missing reference.


Typically, you need to add references to the System.Windows.Form.dll, System.Drawing.dll, and System.Design assemblies. Just right-click the project in the Solution Explorer and select Add Reference (see Figure 7-2).




Figure 7-2: Adding required assemblies


Having accomplished this step, you'll probably want to import some namespaces so you don't have to type fully qualified names (like System.Windows.Forms.Form instead of just Form). Useful namespaces include System.Windows.Forms, System.ComponentModel, and System.Drawing. Remember, importing namespaces isn't required—it's just a convenience that helps trim long lines of code.


You can then create your custom control classes. Generally, you place each control in a separate file, although this approach isn't required. You can create a class library project that contains several files and multiple controls, or you can create a separate project and assembly for each custom control you make. To build your project at any time, right-click it in the Solution Explorer and choose Build. The DLL file will be placed in the bin subdirectory of your project directory.





Referencing a Custom Control



For other projects to use your control, they need a reference to the compiled assembly. When you add a reference, Visual Studio .NET stores the location of the file. Every time you rebuild your client project, Visual Studio copies the latest version of the dependent assembly from its source directory into the client project's bin directory, where the client executable resides. This ensures that you are always testing against the most recent build of a control.


There are two ways to add a reference to a control project. First, you can use the familiar Add Reference command. In this case, the control won't appear in the toolbox, but you will be able to create it manually through code. Your other option is to customize the Toolbox (right-click the Toolbox, and choose Customize). Then, select the .NET Framework Components tab, and click the Browse button. Once you select the appropriate assembly, all controls are added to the list, and check marked automatically. Figure 7-3 shows the assembly for one of the custom controls developed in this chapter: the DirectoryTree.




Figure 7-3: Referencing an assembly with controls


When you click OK, your control is added to the bottom of the Toolbox alongside its .NET counterparts (see Figure 7-4). If you haven't configured a custom icon, it appears with the default gear icon. The next chapter describes how to modify this default icon.




Figure 7-4: Your custom control in the Toolbox


You can create instances of this control by dragging it to the design surface. The first time you add a control to a project, Visual Studio .NET adds a reference to the assembly where the control is defined, and copies this assembly to your project directory.


Note that the Toolbox is a Visual Studio .NET setting, not a project setting. This means that once you add a control to the Toolbox, it will remain there until you remove it, regardless of what project you are working with. You'll also notice that when you use the examples for this chapter, the control icons won't appear in your Toolbox (although the code will work perfectly well). To add the control icons, you'll need to follow the steps above.


When you actually deploy an application that uses a custom control, all you need to do is ensure that the required control DLL is in the same directory as the application executable. When you copy these files to another computer, you do not need to worry about registering them or performing additional steps. This is the infamous zero-touch deployment that is heavily hyped with .NET.



Tip


User controls are slightly more convenient because you don't need to add them to the Toolbox. Instead, the user controls in the current solution will appear there automatically. However, you will need to manually add a project reference to the assembly where the user control is defined before you can drop the user control onto a form.






The GAC



If multiple applications need to use the same control, you can copy the appropriate assembly to each application directory. This gives you the freedom to update some applications with additional functionality without worrying about backward compatibility. It also only requires a minuscule amount of disk space, and is thus the favored approach.


Another option is to install your component to the Global Assembly Cache (the same repository that contains the core .NET assemblies). The Global Assembly Cache (or GAC) allows multiple versions of a component to be installed side-by-side. The GAC also ensures that every application uses the version of a control that it was compiled with, which almost completely eliminates versioning headaches. The only disadvantage to using the GAC is that you need to sign your versioned assembly using a private key to ensure that it has a unique identifier (and can't conflict with other components), and to ensure that no other organization can release a new control that claims to be your own. This process is the same for any shared component, whether it is a control or a business object.


Many factors that required a central repository for components in the old world of COM don't apply with .NET. If you just want to share a control between specific applications, you probably don't need the additional complexity of the GAC. On the other hand, if you are a tool vendor who creates, sells, and distributes custom controls, you will almost certainly want to use it. This process is well documented in the MSDN reference, but the essential steps are explained in the following three sections.



Tip


You don't need to install your control to the GAC to use licensing (which is described at the end of the next chapter). In fact, I recommend that you don't place the controls developed in this chapter into the GAC, unless you have a clear idea of its advantages.

Creating a key



The first step for installing a control into the GAC is to use the sn.exe commandline utility included with the .NET framework. To create a key, you use the −k parameter, and specify the name for your key:



sn -k MyKey.snk


Each .snk file contains a private and a public key. Private and public keys provide a special, time-honored form of encryption (called asymmetric encryption). Anything encrypted with a private key can be read only with the corresponding public key. Conversely, anything encrypted with a public key can only be read with the corresponding private key. The public key is typically made available to the world. The private key is carefully guarded. Public and private key encryption is sometimes used with email. If you want to create a message that is only decipherable by a specific user, you would use that individual's public key to encrypt the message. If you want to create a message that anyone can read, but no one can impersonate, you would use your own private key. Thus, asymmetric encryption can protect data and your identity.


In .NET, the private key is used to compile the assembly, and the public key is embedded inside the assembly. When an application uses your control, the Common Language Runtime uses the public key to decode information from the manifest. Thus, no one else can create an update to your assembly because they need to have your original private key to encode it successfully.




Applying a key to a control



To add the key to a control project, you need to add an Assembly attribute to the AssemblyInfo.vb file that identifies the file.



[Assembly: AssemblyKeyFile("c:\KeyFiles\MyKey.snk")]


The next time you compile the project, the key information is added to the assembly. .NET also supports delayed assembly signing, which allows you to add the strong name just before shipping the control. This is useful in a large organization, because it allows you to debug the control without requiring the private key. The assembly can then be signed just before it is released by the individual who guards the private key. Delayed assembly assignment requires a little more gruntwork, and is described in the MSDN reference.




Installing a control to the GAC



Now that your control is signed, you can install it to the GAC using a dedicated setup program or the GACUtil.exe utility. You can event drag-and-drop the assembly to the C:\[WindowsDir]\Assembly directory in Windows Explorer, which installs it automatically. At this point, life couldn't be easier.


If you install later versions of the same assembly in the GAC, the original version remains. Clients automatically use the latest assembly that shares the same major and minor and version number as the one they were compiled with. In other words, if you compile an application that uses version 1.2.0.0 of your control, the application automatically upgrades itself to version 1.2.1.0 if it exists in the GAC. However, it won't support version 1.3.0.0.


When dealing with assemblies, you have many more options for configuring version policies. You should consult the MSDN reference or a book about .NET fundamentals for more information.


Now that you've digested the basics of creating, compiling, and consuming a control, let's look at some practical examples. All of these custom controls are included with the code download for this chapter. The control projects have names that end with "Control" (as in DirectoryTreeControl), while the Windows Forms projects that test the controls have names that end with "Host" (as in DirectoryTreeHost). The test project directory also contains a solution file that will open both projects at once in the IDE. Figure 7-5 shows the arrangement for the Progress user control.




Figure 7-5: A solution with a control project and a test projectUser Controls


Typically, user controls are created as a group of ordinary controls that are related in some way. For example, you might include a simple record browser, or related customer input fields that provide their own validation. The .NET documentation assumes that user controls are the most common type of custom control project, although they suffer from some serious drawbacks:





User controls tend to combine your business logic with an inflexible block of user interface. For example, if the application programmer doesn't like the way individual text boxes are arranged in an address user control, there's no way to change it. Similarly, if the underlying business logic needs to change, the control itself needs to be rebuilt and redistributed. It's also hard to make a useful derived control based on an existing user control. In other words, user controls tend to be fragile.





Unless you take additional steps, user controls hide all the properties and methods of their child controls. This is similar to the way ActiveX controls were created in Visual Basic 6.





That said, user controls are useful for quickly solving certain problems, or just creating composite controls.







Creating User Controls



To add a user control to a .NET custom control project, right-click the Solution Explorer window and select Add User Control. Figure 7-6 shows a user control in the Solution Explorer.




Figure 7-6: A user control at design-time


You'll notice from the designer that a user control is halfway between an ordinary control and a form. It helps to imagine that a user control is just a reusable portion of a form—more flexible than the visual inheritance you used in Figure 7-7.




Figure 7-7: User control inheritance


To add a control, just draw it onto the design surface in the same way as you would a form. You can (and should) use anchoring and docking with the controls in your user control. This ensures that they always resize to fit the bounds of their container. Remember, the size of the user control is dictated by the application programmer.


If you add a form and a user control to the same project, Visual Studio .NET thoughtfully adds your user control to the toolbar so that you can drag-and-drop it onto your form. In many ways, user controls have the most convenient designtime support, and don't require any additional work from the programmer. However, you will still need to add a reference to the DLL assembly that contains the user control, or you will receive an error when you try to add the control to the project. Also, note that as with visual inheritance, if you change the user control you need to recompile before the change will appear in any form that hosts it. Just right-click the project in the Solution Explorer and choose Build.


To understand the strengths and limitations of user controls, it helps to consider a couple of examples.





The Progress User Control



The first user control you'll consider is a simple coupling of a ProgressBar and Label control. This control solves a minor annoyance associated with the ProgressBar—there is no way to show the standard text description about the percent of work complete. You can easily get around this limitation by adding a label to every form that uses the ProgressBar, and manually synchronizing the two. Even better, the Progress user control implements a standard, reusable solution.


To begin, the user control is created with a label and progress bar, as shown in Figure 7-8.




Figure 7-8: The progress control at design-time


If you try to use the Progress control directly in a project, you'll discover that you can't access the label or the bar. Instead, the only properties and methods that are available are those of the user control itself, allowing you to modify the default font and background color (as you can with a form), but not much more. To actually make the Progress user control functional, you need to replicate all the important methods and properties. Then, in each method or property procedure for your user control, you simply call the corresponding method or property procedure in the label or progress bar.


This delegation pattern can add up to a lot of extra code for an advanced control! Fortunately, when you create a user control you will usually restrict and simplify the interface so that it is more consistent and targeted for a specific use. In the Progress user control, for example, don't worry about allowing the user to set a font or background color for the label control.



Tip


If your user control contains several controls with the same properties (like Font), you need to decide whether to provide individual user control properties (NameFont, AddressFont, etc.) or set them all at once in a single property procedure. The UserControl class makes your job a little easier. It defines Font and ForeColor properties that are automatically applied to all the composite controls unless they specify otherwise. (This is similar to how a form works.) The UserControl class also provides BackColor and BackImage properties that configure the actual user control drawing surface.The Progress user control provides access to three properties from the ProgressBar control (Value, Maximum, and Step), and one method (PerformStep).



public class Progress : System.Windows.Forms.UserControl
{
internal System.Windows.Forms.Label lblProgress;
internal System.Windows.Forms.ProgressBar Bar;
// (Designer code omitted.)
public int Value
{
get
{
return Bar.Value;
}
set
{
Bar.Value = value;
UpdateLabel();
}
}
public int Maximum
{
get
{
return Bar.Maximum;
}
set
{
Bar.Maximum = value;
}
}
public int Step
{
get
{
return Bar.Step;
}
set
{
Bar.Step = value;
}
}
public void PerformStep()
{
Bar.PerformStep();
UpdateLabel();
}
private void UpdateLabel()
{
lblProgress.Text = (Math.Round((decimal)(Bar.Value * 100) /
Bar.Maximum)).ToString();
lblProgress.Text += "% Done";
}
}


Every time the progress bar changes (either by modifying the Value or invoking the PerformStep() method), the code calls a special private method, UpdateLabel. This ensures that the label always remains completely synchronized.


Testing this control is easy. All you need is a simple form that hosts the Progress user control, and increments its value. In this case, a timer is used for this purpose. Each time the timer fires, the PerformStep() method increments the counter by its Step value.



private void tmrIncrementBar_Tick(object sender, System.EventArgs e)
{
status.PerformStep();
if (status.Maximum == status.Value)
{
tmrIncrementBar.Enabled = false;
}
}


The timer itself is enabled in response to a button click, which also configures the user control's initial settings:



private void cmdStart_Click(object sender, System.EventArgs e)
{
tmrIncrementBar.Enabled = false;
status.Value = 0;
status.Maximum = 20;
status.Step = 1;
tmrIncrementBar.Enabled = true;
}


Figure 7-9 shows the Progress control in the test application. Remember, though the Progress control will appear in the Toolbox automatically, you still need to add a reference to the appropriate project. Follow these steps if you are experiencing any trouble:





Compile the Progress control.





Create a new test project to the solution.





In the test project, add a reference to the compiled Progress DLL assembly.





Drop the Progress control onto the form, using the Toolbox.





Set the appropriate properties in code, or using the Properties window.







Figure 7-9: The Progress user control in action


Incidentally, the user can access one back door in the Progress user control: the Controls collection. If you search for the ProgressBar control by name, and modify it through the Controls collection, the label will not be refreshed. This technique relies on a string name, and is therefore not type-safe. It is strongly discouraged.


When creating any custom control, it helps to remember that you are designing a genuine class. As with any class, you should decide how it will communicate with other code, and how it can encapsulate its private data before you begin writing the code. The best approach is to start by designing the control's interface. Figure 7-10 presents a UML (Unified Modeling Language) diagram that defines the interface for the Progress user control.




Figure 7-10: The Progress control in UML


There are no clear rules for designing custom controls. Generally, you should follow the same guidelines that apply to any type of class in a program. Some of the basics include the following:





Always use properties in place of public class variables. Public variables don't offer any protection and won't appear in the Properties window.





If you provide a property, try to make it both readable and writable, unless there is a clear reason not to. Make sure that properties that can affect the control's appearance trigger a refresh when they are altered.





Don't expose your basic control methods. Instead, expose higher-level methods that call these lower-level methods as required. One difference is that private methods often need to be used in set ways, while public methods should be able to work in any order. Hide details that aren't important or could cause problems if used incorrectly.





Wrap errors in custom exception classes that provide additional information to the application programmer about the mistake that was made.





Always use enumerations when allowing the user to choose between more than one option (never fixed constant numbers of strings). Wherever possible, code so that invalid input can't be entered.





When all other aspects of the design are perfect, streamline your control for performance. This means reducing the memory requirements, adding threading if it's appropriate, and applying updates in batches to minimize refresh times.





Finally, whenever possible analyze the user interface for an application as a whole. You can then decide based on that analysis what custom controls can be made to reduce the overall development effort.





The Bitmap Thumbnail Viewer



The next example of user control development is a little more ambitious. It creates a series of thumbnails that show miniature versions of all the bitmap files found in a specific directory. This type of control could be created in a more flexible way, and with much more code, by using the GDI+ drawing features. Instead, this example uses control composition, and dynamically inserts a PictureBox control for every image. This makes it easier to handle image clicks and support image selection. It also previews the techniques you'll see in Chapter 11, where user interface is generated out of controls dynamically at runtime.


Possibly the best aspect of the BitmapViewer user control is that it communicates with your program in both directions. You can tailor the appearance of the BitmapViewer by setting properties, and the BitmapViewer notifies your code when a picture is selected by raising an event.


The design-time appearance of the BitmapViewer is unremarkable (see Figure 7-11). It contains a Panel where all the picture boxes will be added. Alternatively, the picture boxes could be added directly to the Controls collection of the user control, but the Panel allows for an attractive border around the control. It also allows automatic scrolling support—as long as AllowScroll is set to true, scrollbars are provided as soon as the image thumbnails won't fit in the Panel. As with our previous example, the Panel is anchored to all sides for automatic resizing.




Figure 7-11: The BitmapViewer at design-time



Note


The size of the user control in the user control designer sets the initial size that is used when the control is added to a form. This size can be changed by the user, but think of it as a best recommendation.Unlike the Progress control, the BitmapViewer cannot just hand off its property procedures and methods to members in one of the composite controls. Instead, it needs to retain a fair bit of its own information. The following code shows the key private variables:



// The directory that will be scanned for image.
private string directory = ";
// Each picture box will be a square of dimension X dimension pixels.
private int dimension;
// The space between the images and the top, left, and right sides.
private int border = 5;
// The space between each image.
private int spacing;
// The images that were found in the selected directory.
private ArrayList images = new ArrayList();


Some of the values are user configurable, while some are not. For example, the collection of images is drawn from the referenced directory. The property procedures for the modifiable values are shown here:



public string Directory
{
get
{
return directory;
}
set
{
directory = value;
GetImages();
UpdateDisplay();
}
}
public int Dimension
{
get
{
return dimension;
}
set
{
dimension = value;
UpdateDisplay();
}
}
public int Spacing
{
get
{
return spacing;
}
set
{
spacing = value;
UpdateDisplay();
}
}


Note


For simplicity's sake, this code doesn't provide any error-handling logic. For example, all the integer properties in the BitmapViewer should be restricted to positive numbers. Ideally, the property procedure code should refuse negative numbers and raise an error to alert the control user.Notice that every time a value is modified, the display is automatically regenerated by calling the UpdateDisplay() method. A more sophisticated approach might make this logic depend on a property like AutoRefresh. That way, the user could temporarily turn off the refresh, make several changes at once, and then re-enable it.


The set procedure for the Directory property also calls a special GetImages() method, which inspects the directory, and populates the Images collection. You might expect that the Images collection contains Image objects, but this is not the case. To provide useful event information, the BitmapViewer actually tracks the file name of every image it displays. To do this, a special NamedImage class is defined:



private class NamedImage
{
public Image Image;
public string FileName;
public NamedImage(Image image, string fileName)
{
this.Image = image;
this.FileName = fileName;
}
}


The NamedImage class is a private class nested inside the BitmapViewer control class. This means that NamedImage is used exclusively by the BitmapViewer, and not made available to the application using the BitmapViewer control.


The GetImages() method uses the standard .NET file and directory classes to retrieve a list of bitmaps. For each bitmap, a NamedImage object is created, and added to the Images collection.



private void GetImages()
{
images.Clear();
if (this.Directory != ")
{
DirectoryInfo dir = new DirectoryInfo(Directory);
foreach (FileInfo file in dir.GetFiles("*.bmp"))
{
images.Add(new NamedImage(Bitmap.FromFile(file.FullName), file.Name));
}
}
}


This code stores the entire Image object in memory. To save memory, especially with large directories, it's more practical to store only the thumbnail-sized image. To do so, use the Bitmap.GetThumbnailImage() method, and then add the resulting Bitmap object to the collection. The online code samples demonstrate this technique.


The bulk of the work for the BitmapViewer takes place in the UpdateDisplay() method, which generates the picture boxes, adds them to the panel, and sets their tag property with the name of the corresponding file for later reference. The BitmapViewer is filled from left to right, and then row-by-row.



private void UpdateDisplay()
{
// Clear the current display.
pnlPictures.Controls.Clear();
// Row and Col will track the current position where pictures are
// being inserted. They begin at the top-left corner.
int row = border, col = border;
// Iterate through the Images collection, and create PictureBox controls.
foreach (NamedImage image in images)
{
PictureBox pic = new PictureBox();
pic.Image = image.Image;
pic.Tag = image.FileName;
pic.Size = new Size(dimension, dimension);
pic.Location = new Point(col, row);
pic.BorderStyle = BorderStyle.FixedSingle;
// StretchImage mode gives us the "thumbnail" ability.
pic.SizeMode = PictureBoxSizeMode.StretchImage;
// Display the picture.
pnlPictures.Controls.Add(pic);
// Move to the next column.
col += dimension + spacing;
// Move to next line if no more pictures will fit.
if ((col + dimension + spacing + border) > this.Width)
{
col = border;
row += dimension + spacing;
}
}
}



Tip


This code could be optimized for speed. For example, all the picture boxes could be created and then added to the Panel control using the Controls.AddRange() method, ensuring that the control won't be updated and refreshed after each new picture is inserted.This code is also provided to the user through the public RefreshImages() method. This allows the user to trigger a refresh without needing to modify a property if the directory contents have changed.



public void RefreshImages()
{
GetImages();
UpdateDisplay();
}


The OnSizeChanged() method is also overriden to ensure that the pictures are redrawn when the user control size changes. This ensures that the pictures are automatically adjusted (in rows and columns) to best fit the new size.



protected override void OnSizeChanged(System.EventArgs e)
{
UpdateDisplay();
base.OnSizeChanged(e);
}


Figure 7-12 shows a stripped-down UML diagram for the BitmapViewer control, in keeping with my philosophy of clearly defining the interfaces for custom controls. This diagram omits private members and members that have been inherited. It also shows two other class dependencies: the private NamedImage class and the PictureSelectedEventArgs class, which is introduced shortly as a means of passing event data to the application that hosts the BitmapViewer.




Figure 7-12: The BitmapViewer in UML





Testing the BitmapViewer



To see the final BitmapViewer control, follow these steps:





Compile the BitmapViewer control.





Create a new test project to the solution.





In the test project, add a reference to the compiled BitmapViewer DLL assembly.





Drop the BitmapViewer control onto the form using the Toolbox.





Set the appropriate properties, like Directory, Dimension, and Spacing. In Figure 7-13, a dimension of 80 and spacing of 10 is used. (In the next chapter, we'll consider how to add some reasonable default values, so you won't need to always specify this sort of information to test your control.)




Figure 7-13: The BitmapViewer in action





Set the Directory property. A good place to do this is in the Form.Load event handler.





Figure 7-13 shows the BitmapViewer test project. In this example, the BitmapViewer is docked to the form so you can change the size and see the image thumbnails being reorganized.





BitmapViewer Events



To make the BitmapViewer more useful, you can add an event that fires every time a picture box is selected. Because the BitmapViewer is built entirely from PictureBox controls, which natively provide a Click event, no hit testing is required. All you need to do is register to handle the Click event when the picture box is first created in the UpdateDisplay() method.



pic.Click += new EventHandler(this.pic_Click);


To send an event to the application, the event must first be defined in the user control class. In this case, the event is named PictureSelected. In true .NET style, it passes a reference to the event sender and a custom EventArgs object that contains additional information.



public delegate void PictureSelectedDelegate(object sender,
PictureSelectedEventArgs e);
public event PictureSelectedDelegate PictureSelected;


The custom PictureSelectedEventArgs object follows. It provides the file name of the picture that was clicked, which allows the application to retrieve it directly for editing or some other task. Note that this class should not be private, as the client must use it to retrieve the event information.



public class PictureSelectedEventArgs : EventArgs
{
public string FileName;
public Image Image;
public PictureSelectedEventArgs(String fileName, Image image)
{
this.FileName = fileName;
this.Image = image;
}
}


The PictureBox.Click event handler changes the border style of the clicked picture box to make it appear "selected." If you were using GDI+, you could draw a more flexible focus cue, like a brightly colored outline rectangle. The PictureBox.Click event handler then fires the event, with the required information.



private PictureBox picSelected;
private void pic_Click(object sender, System.EventArgs e)
{
// Clear the border style from the last selected picture box.
if (picSelected != null)
{
picSelected.BorderStyle = BorderStyle.FixedSingle;
}
// Get the new selection.
picSelected = (PictureBox)sender;
picSelected.BorderStyle = BorderStyle.Fixed3D;
// Fire the selection event.
PictureSelectedEventArgs args = new
PictureSelectedEventArgs((string)picSelected.Tag, picSelected.Image);
if (PictureSelected != nul)
{
PictureSelected(this, args);
}
}


The application can now handle this event. In the example shown here (and pictured in Figure 7-14), a message box is displayed with the file name information.




Figure 7-14: A BitmapViewer event



private void bitmapViewer1_PictureSelected(object sender,
BitmapThumbnailViewer.PictureSelectedEventArgs e)
{
MessageBox.Show("You chose " + e.FileName);
}




BitmapViewer Enhancements and Threading



If you use the bitmap viewer with a directory that contains numerous large images, you start to notice a performance slowdown. One of the problems is that in its current form, the BitmapViewer stores the entire image in memory, even though it only displays a thumbnail. A better approach would be to scale the image immediately when it is retrieved. This is accomplished using the Image.GetThumbnail() method.


In the code that follows, the GetImages() method has been rewritten to use this more memory-friendly alternative.




private void GetImages()
{
if (Directory != ")
{
Image thumbnail;
DirectoryInfo dir = new DirectoryInfo(Directory);
foreach (FileInfo file in dir.GetFiles("*.bmp"))
{
thumbnail = Bitmap.FromFile(file.Name).GetThumbnailImage(
Dimension, Dimension, null, null);
Images.Add(new NamedImage(thumbnail, file.Name));
}
}
}


This technique also frees you up to use a simpler control than the PictureBox to contain the Image (or even draw it directly on the form surface), because the control no longer has to perform the scaling. However, it also means that you need to update the Dimension property procedure to call the GetImages() method— otherwise, the image objects won't be the correct size.



public int Dimension
{
get
{
return dimension;
}
set
{
dimension = value;
GetImages();
UpdateDisplay();
}
}


Assuming that the GetImages() method takes a significant amount of time, you might want to change the BitmapViewer to use multithreading. With this design, the GetImages() code runs on a separate thread, and then automatically calls the UpdateDisplay() method when it is completed. That way, the user interface wouldn't be tied up in the meantime. The remainder of this section walks you through the process.


First, change every property procedure that calls GetImages() so that it doesn't call UpdateDisplay(). An example is shown here with the Dimension() property.



public int Dimension
{
get
{
return dimension;
}
set
{
dimension = value;
GetImages();
UpdateDisplay();
}
}


Next, modify the GetImages() method so it actually starts the real ReadImagesFromFile() method on a separate thread.



private void GetImages()
{
Threading.Thread getThread = new Threading.Thread(new
ThreadStart(this.ReadImagesFromFile));
getThread.Start();
}


Finally, modify the file reading code and place it in the ReadImagesFromFile() method:



private void ReadImagesFromFile()
{
lock (Images)
{
if (Directory != ")
{
Image thumbnail;
DirectoryInfo dir = new DirectoryInfo(Directory);
foreach (FileInfo file in dir.GetFiles("*.bmp"))
{
ThumbNail = Bitmap.FromFile(file.Name).GetThumbnailImage(
Dimension, Dimension, null, null);
Images.Add(new NamedImage(thumbnail, file.Name));
}
}
}
// Update the display on the UI thread.
pnlpictures.Invoke(new MethodInvoker(this.UpdateDisplay));
}


Threading introduces numerous potential pitfalls and isn't recommended unless you really need it. When implementing the preceding example, you have to be careful that the UpdateDisplay() method happens on the user interface thread, not the ReadImagesFromFile() thread. Otherwise, a strange conflict could emerge in real-world use. Similarly, the lock statement is required to make sure that no other part of the control code attempts to modify the Images collection while the ReadImagesFromFile() method is in progress.






/ 142