Windows Forms 2.0 Programming (Microsoft .NET Development Series)
Although the .NET Framework offers a wide variety of standard components, it can't possibly cover every scenario where a component makes sense. When you need a component that's beyond the scope of the intrinsic .NET Framework components, or when you have reusable code that would benefit from the Windows Forms Designer support enabled by a component, you can easily create your own custom components. For example, the singlefire alarm we constructed earlier might be useful in more than just your application and would be better repackaged as a custom component. Deriving Directly from System.ComponentModel.Component
The easiest way to create a new component is to rightclick on a project, choose Add | Add Component, enter the name of your component class (our example is AlarmComponent), and press OK. You're greeted with a blank, nonvisual design surface, as shown in Figure 9.5. Figure 9.5. A New Component's Wholly Nonvisual Design Surface
Much like the nonvisual design surface of a form, this design surface is meant to host the components you need in order to implement your new component. For example, we can drop a Timer component from the Toolbox onto the AlarmComponent's design surface. In this way, we can create and configure a timer just as if we were hosting the timer on a form. Figure 9.6 shows the alarm component with a timer component configured for our needs. Figure 9.6. A Timer Component Hosted on a Custom Component's Nonvisual Design Surface
Switching to Code view for the component displays the following skeleton,[2] which is generated by the component project item template and filled in by the Windows Forms Designer for the timer: [2] You can switch to Code view from Windows Forms Designer view by choosing View | Code, and switch back by choosing View | Designer. You can toggle between the two by pressing F7. // AlarmComponent.Designer.cs partial class AlarmComponent { ... Timer timer; ... #region Component Designer generated code /// <summary> /// Required method for Designer support do not modify /// the contents of this method with the code editor. /// </summary> void InitializeComponent() { this.components = new Container(); this.timer = new Timer(this.components); ... // timer this.timer.Enabled = true; this.timer.Interval = 1000; this.timer.Tick += this.timer_Tick; } #endregion } // AlarmComponent.cs using System; using System.ComponentModel; using System.Collections.Generic; using System.Diagnostics; using System.Text; partial class AlarmComponent : Component { public AlarmComponent() { InitializeComponent(); } public AlarmComponent(IContainer container) { container.Add(this); InitializeComponent(); } void timer_Tick(object sender, EventArgs e) {...} }
Notice that a default custom component derives from the Component class from the System.ComponentModel namespace. Component is the base implementation of IComponent, which enables integration with VS05 features such as the Properties window and automatic resource management. Component Resource Management
The Windows Forms Designer also generates code that enables components to automatically add them to their container's list of components. When the container shuts down, it uses this list to notify all the components that they can release any managed and native resources that they're holding. To let the Windows Forms Designer know that it would like to be notified when its container goes away, a component can implement a public constructor that takes a single argument of type IContainer: // AlarmComponent.cs partial class AlarmComponent : Component { ... public AlarmComponent(IContainer container) { // Add object to container's list so that // we get notified when the container goes away container.Add(this); InitializeComponent(); } ... }
Notice that the component uses the container passed to its constructor to add itself to its host and become a contained component. In the presence of this constructor, the Windows Forms Designer generates code that uses this constructor, passing it a container for the component to add itself to. Because the AlarmComponent implements this special constructor, the following code is generated when an AlarmComponent is added to a form: // AlarmComponentSampleForm.Designer.cs partial class AlarmComponentSampleForm { ... AlarmComponent alarmComponent; IContainer components = null; ... void InitializeComponent() { this.components = new Container(); this.alarmComponent = new AlarmComponent(this.components); ... } } // AlarmComponentSampleForm.cs partial class AlarmComponentSampleForm : Form { public AlarmComponentSampleForm() { InitializeComponent(); } }
Several VS05generated classes can contain components: forms, user controls, controls, and components themselves. When classes of these types are disposed of, they automatically notify their contained components as part of the Dispose method implementation: // AlarmComponentSampleForm.Designer.cs partial class AlarmComponentSampleForm { ... // Overridden from the base class Component.Dispose method protected override void Dispose(bool disposing) { if( disposing && (components != null) ) { components.Dispose(); } base.Dispose(disposing); } ... }
A component that has added itself to the container can override the Component base class's Dispose method to catch the notification that it is being disposed of. In this way, components like AlarmComponent's contained Timer component can release its own resources: // AlarmComponent.Designer.cs partial class AlarmComponent { ... Timer timer; IContainer components = null; ... protected override void Dispose(bool disposing) { if( disposing ) { // Release managed resources ... // Let contained components know to release their resources if( components != null ) { components.Dispose(); } } // Release native resources ... } ... void InitializeComponent() { this.components = new Container(); this.timer = new Timer(this.components); ... } } Notice the call to components.Dispose. This call walks the list of contained components, calling each component's Dispose(bool) method much like this: namespace System.ComponentModel { ... class Container : IContainer { // IContainer inherits IDisposable void Dispose() { // Container is being proactively disposed of from client code Dispose(true); ... } // Logical implementation of Container's Dispose(bool) method void Dispose(bool disposing) { if( disposing ) { foreach( Component component in this.components ) { component.Dispose(); } } } ... } ... } Each component implements IComponent, which extends IDisposable so that it can be used in just this way. The Component base class routes the implementation of IDisposable. Dispose() to call its own Dispose(bool) method, passing true. When true is passed to Dispose (bool), it means that was called by a client that remembered to properly dispose of the component. In the case of our alarm component, the only managed resources we have to reclaim are those of the timer component we're using to provide our implementation, so we ask our own component list (the "components" field) to dispose of the components it's holding on our behalf. Because the Windows Forms Designer-generated code added the timer to our container, that's all we need to do. A disposing argument of false means that the client forgot to properly dispose of the object and that the .NET Garbage Collector (GC) is calling our object's finalizer. The finalizer is the method that the GC calls when it's about to reclaim the memory associated with the object (called Finalize and defined in Object, the ultimate base class of all .NET classes). Because the GC calls the finalizer at some indeterminate timepotentially long after the component is no longer needed (perhaps hours or days later)the finalizer is a bad place to reclaim resources, but it's better than not reclaiming them at all. The Component base class's finalizer implementation calls the Dispose method, passing a disposing argument of false, which indicates that the component shouldn't touch any of the managed objects it may contain. The other managed objects should remain untouched because the GC may have already disposed of them, and their state is undefined. Consequently, the only resources that should be released at this stage are native resources. Any component that contains other objects that implement IDisposable, or handles to native resources, should implement the Dispose(bool) method to properly release those objects' resources when the component itself is being released by its container. Implementing IComponent
As you've seen, automatic resource management and Properties window integration are both features we get by deriving from Component's implementation of IComponent and IDisposable. In most cases, Component should serve you well as the starting point for building custom components, though, at times is not possible. For example, suppose you have a class that you'd like to drop onto a form and offer the same level of integration with VS05 offered by existing components like Timer. If the class already derives from a base class other than Component and if that base class doesn't implement IComponent, you must implement it. Likewise with IDisposable, the base of IComponent. For example, in Chapter 8: Printing, we created the PageCountPrintController class, which derives from PreviewPrintController: // PageCountPrintController.cs class PageCountPrintController : PreviewPrintController { #region PageCountPrintController implementation ... #endregion }
To provide consistency with other printoriented components, it would be great to drop this class onto a form in the Windows Forms Designer. However, PreviewPrintController implements neither IComponent nor IDisposable. To do so requires implementing IComponent: namespace System.ComponentModel { ... interface IComponent : IDisposable { // Properties ISite Site { get; set; } // Events event EventHandler Disposed; } ... }
The Site property is what enables VS05 and Windows Forms Designer integration, a topic that's explored in detail in Chapter 11: Design-Time Integration: The Properties Window, and Chapter 12: Design-Time Integration: Designers and Smart Tags. The Disposed event is fired by a component to let its hosts know it's going away, something that is particularly useful to containers that need to remove it from their list of managed components when that happens. Consequently, the implementation of IComponent is relatively simple: // PageCountPrintController.cs class PageCountPrintController : PreviewPrintController, IComponent { public PageCountPrintController() {} public PageCountPrintController(IContainer container) { container.Add(this); } #region PageCountPrintController implementation ... #endregion #region IComponent public event EventHandler Disposed; private ISite site; [Browsable(false)] [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden)] public ISite Site { get { return this.site; } set { this.site = value; } } #endregion #region IDisposable ... #endregion } Because the Site property is configured by VS05, and not developers, it should be hidden from the Properties window via attribution with both the Browsable and DesignerSerializationVisibility attributes, which are discussed in Chapter 11. To complete our custom IComponent, we also need to implement IDisposable. Implementing IDisposable
IDisposable declares only one method, Dispose, which client code calls to notify the component that it should release its managed and native resources immediately: // PageCountPrintController.cs class PageCountPrintController : PreviewPrintController, IComponent { #region PageCountPrintController implementation ... #endregion #region IComponent ... #endregion #region IDisposable private bool disposed; public void Dispose() { if( !this.disposed ) { // Release managed and native resources ... // Release resources only once this.disposed = true; // Let interested parties know if( this.Disposed != null ) this.Disposed(this, EventArgs.Empty); } } #endregion }
The use of the disposed flag ensures that we release resources only once. When disposal occurs, we also fire the Disposed event, as required by our implementation of IComponent, to ensure that interested parties are kept in the loop. This implementation of Dispose is a fine one, as long as it is called. If client code forgets to do so, then we must implement the Finalize method as backup, as discussed earlier. Also discussed was the fact that by the time Finalize is called, managed resources are in an indeterminate state and shouldn't be touched. Thus, the component needs to distinguish whether it's being disposed of or finalized when it releases resources: // PageCountPrintController.cs class PageCountPrintController : PreviewPrintController, IComponent { public PageCountPrintController() { } public PageCountPrintController(IContainer container) { container.Add(this); } #region PageCountPrintController implementation ... #endregion #region IComponent ... #endregion #region IDisposable bool disposed; public void Dispose() { Dispose(true); } // Finalize method in C# is implemented using destructor syntax ~PageCountPrintController() { // Finalizer is called in case Dispose wasn't, although we // can release only native resources at this stage Dispose(false); } // Dispose of managed and native resources protected virtual void Dispose(bool disposing) { if( !this.disposed ) { // If IDisposable.Dispose() was called if( disposing ) { // Release managed resources ... } // If IDisposable.Dispose() or finalizer was called, // release native resources ... // Only release resources once this.disposed = true; // Let interested parties know if( this.Disposed != null ) this.Disposed(this, EventArgs.Empty); } } #endregion } Here, we create an overload of the Dispose method that accepts a Boolean argument indicating whether the class is being disposed of from client code (true) or during finalization (false), and it is called from both the Dispose and the Finalize (implemented as a destructor) methods. If the class is being disposed of, true is passed, and both managed and native resources are released. If the class is being finalized, however, false is passed to ensure that only native resources are released. The Dispose method overload is marked as protected virtual, so any derivations of PageCountPrintController can override the Dispose method and extend it as needed (remembering to call the base's Dispose implementation, of course). An appropriate constructor is also provided to allow VS05 to add this component to its container's component list, ensuring that the disposal logic is automatically called when the container goes away. If PageCountPrintController is hooked up to a host by the Windows Forms Designer, then Dispose is automatically called as part of the resource management chain created on our behalf. However, because our component may be created manually by developers, we still need to support finalization. Disposal Optimization
Finalizers must ensure that native resources are released, but implementing them can have an undesirable performance hit, as described in the MSDN Library: "Reclaiming the memory used by objects with Finalize methods requires at least two garbage collections."[3] [3] See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconfinalizemethodscdestructors.asp (http://tinysells.com/16). If Dispose is not called by client code, this performance hit must be taken on the component's chin. However, if Dispose(bool) is proactively called from client code, there is no need to call Finalize, because native resources have been released. In this case, you can take advantage of this knowledge and influence the Garbage Collector's treatment of your component instance by instructing it not to execute Finalize. You call the SuppressFinalize method of the .NET Framework's Garbage Collector wrapper class, GC, from Dispose: // PageCountPrintController.cs class PageCountPrintController : PreviewPrintController, IComponent { #region PageCountPrintController implementation ... public void Dispose() { Dispose(true); // Prevent Finalize method from being called GC.SuppressFinalize(this); } // NOT CALLED IF COMPONENT IS ALREADY DISPOSED OF // Finalize method in C# is implemented using destructor syntax ~PageCountPrintController() { // Finalizer is called in case Dispose wasn't, although we // can release only native resources at this stage Dispose(false); } ... }
Implementing IComponent with proper IDisposable finalization in mind is a necessary but onerous task if you have to do it manually. It is certainly easier to derive from Component when you canas AlarmComponent doesto inherit this support automatically. Adding a Custom Component to the Toolbox
Having had all the code to create, manage, and dispose of a component automatically generated on our behalf, we now have the most basic possible implementation that we could drop into a form. Before this can happen, though, a component should be made available from VS05's Toolbox. Fortunately, VS05 makes this easy for you if your component is in the same project as your Windows Forms application; after you recompile a project that contains one or more components, they are automatically added to the Toolbox under a new tab, which lists all the components in the currently active project, as shown in Figure 9.7. Figure 9.7. Alarm Component Automatically Added to Toolbox after Compilation
If your component is deployed to a different assembly outside the scope of your project, you need to spend a little more effort adding it to the Toolbox. You rightmouseclick the Toolbox and select Choose Items, which opens a dialog where you select either .NET or COM components, as shown in Figure 9.8. Figure 9.8. Selecting Components and Controls to Add to the Toolbox
If your component doesn't appear in the list by default, simply browse to and select the assembly (.dll or .exe) that contains it. The public components in your assembly are added to the list and selected and checked by default.[4] Uncheck the components you don't want, and click OK to add the remaining checked components to the Toolbox. By default, the components are added to whichever Toolbox tab you have currently selected, which could be either one of the defaults or a custom tab you created by rightmouseclicking the Toolbox and choosing Add Tab. It can be very handy to have custom tabs for custom controls so that they don't get lost among the standard controls and components. [4] Chapter 11 discusses how to control whether public components can be added to the Toolbox at all using special designtime attributes. Once you've got a component onto the Toolbox, you can drag it onto a form and use the Properties window to set properties and hook up events. Custom Functionality
Properties, events, and, indeed, methods comprise the ways in which a component, like any other .NET class, exposes custom functionality to solve the problem at hand. Custom Properties
The only way AlarmComponent can make itself useful is by letting users actually set an alarm date/time value. You can use either fields or properties in .NET to store values, but the Properties window shows any public property without your doing anything special to make it work. It's an easy way to simplify the designtime experience of your component. AlarmComponent implements the Alarm property: // AlarmComponent.cs partial class AlarmComponent : Component { ... DateTime alarm = DateTime.MaxValue; // No alarm ... public DateTime Alarm { get { return this.alarm; } set { this.alarm = value; } } }
Components need to be recompiled before public properties appear in the Properties window, after which they appear the way Alarm does in Figure 9.9. Figure 9.9. A Custom Property Shown in the Properties Window
Not only does the Properties window display the custom Alarm property without extra code, but it has also determined that the property is a date/time value and provides additional propertyediting support with a date/time pickerstyle UI. Custom Events
As with properties, the Properties window shows any public event without a lick of additional code.[5] For example, if you want to fire an event when the alarm sounds, you can expose a public event such as AlarmSounded: [5] For an introduction to delegates and events, see Chapter 1: Hello, Windows Forms. For a thorough explanation, refer to Appendix C: Delegates and Events. // AlarmComponent.cs partial class AlarmComponent : Component { ... public event EventHandler AlarmSounded; ... void timer_Tick(object sender, EventArgs e) { // Check to see whether we're within 1 second of the alarm double seconds = (DateTime.Now this.alarm).TotalSeconds; if( (seconds >= 0) && (seconds <= 1) ) { this.alarm = DateTime.MaxValue; // Show alarm only once if( this.AlarmSounded != null ) { AlarmSounded(this, EventArgs.Empty); } } } }
AlarmSounded is an event of the EventHandler delegate type. When it's time to sound the alarm, as determined by code inside the timer control's Tick event handler, the code looks for event subscribers. If there are any, it lets them know that the alarm has sounded, passing the sender (AlarmComponent) and an empty EventArgs object. When your component has a public event like AlarmSounded, it shows up as just another event in the Properties window, as shown in Figure 9.10. Figure 9.10. A Custom Event Shown in the Properties Window
Just like handling any other event, handling a custom event causes the Windows Forms Designer to generate a code skeleton for you to fill. When defining your event, you may find that you'd like to pass contextual information about the event to the event handler. If that's the case, you need to create a custom delegate type to operate over a custom arguments class with the information you'd like to pass: // AlarmSoundedEventArgs.cs public class AlarmSoundedEventArgs : EventArgs { DateTime alarm; public AlarmSoundedEventArgs(DateTime alarm) { this.alarm = alarm; } public DateTime Alarm { get { return this.alarm; } } } // AlarmSoundedEventHandler.cs public delegate void AlarmSoundedEventHandler( object sender, AlarmSoundedEventArgs e); // AlarmComponent.cs partial class AlarmComponent : Component { ... // AlarmSounded event public event AlarmSoundedEventHandler AlarmSounded; void timer_Tick(object sender, EventArgs e) { // Check to see whether we're within 1 second of the alarm double seconds = (DateTime.Now - this.alarm).TotalSeconds; if( (seconds >= 0) && (seconds <= 1) ) { DateTime alarm = this.alarm; this.alarm = DateTime.MaxValue; // Show alarm only once if( this.AlarmSounded != null ) { AlarmSounded(this, new AlarmSoundedEventArgs(alarm)); } } } }
Notice the custom delegate we created, AlarmSoundedEventHandler, which uses the same patternno return value, an object sender argument, and an EventArgsderived typeas the last argument. This is the pattern that .NET follows, and it's a good one for you to emulate with your own custom events. In our case, AlarmSoundedEventHandler accepts a custom AlarmSoundedEventArgs as its event argument. AlarmSoundedEventArgs derives from EventArgs and extends it with a property to store and pass the alarm time. You can, and should, define new event argument classes by deriving from an appropriate .NET event arguments class. For example, in this case, it was fine to derive from EventArgs, because we extended it only with a new property. However, if you want your custom event arguments to support cancellation, you can instead derive from CancelEventArgs because it extends EventArgs with cancellation functionality. Custom Methods
Although methods don't appear in the Properties window, they are slightly easier to use because you don't have to worry about creating and managing a component instance to call them against. In general, creating methods like DelayAlarm for components is the same as creating methods for plain types: // AlarmComponent.cs partial class AlarmComponent : Component { ... DateTime DelayAlarm(double minutes) { // Delay alarm by specified minutes if less than maximum date/time if( this.alarm < DateTime.MaxValue.AddMinutes(minutes) ) { this.alarm = this.alarm.AddMinutes(minutes); } return this.alarm; } }
However, in some scenarios, you may need to take special care when creating methods, particularly if they need to distinguish between design time and run time. Events and properties need to make this consideration, which is discussed in depth in Chapter 11: Design-Time Integration: The Properties Window. Putting the Alarm property, the AlarmSounded event, and the DelayAlarm method together produces a designtime experience that's much less time and code intensive than would be possible using a Timer and code. With the Windows Forms Designer generating code on our behalf to create and configure the AlarmComponent, as well as hook up the AlarmSounded event, the only code we need to write is to allow users to set and delay the alarm and to respond when the alarm is sounded: // AlarmComponentSampleForm.cs partial class AlarmComponentSampleForm : Form { public AlarmComponentSampleForm() { InitializeComponent(); } void setAlarmButton_Click(object sender, EventArgs e) { // Set the Alarm property this.alarmComponent.Alarm = dateTimePicker.Value; ... } void alarmComponent_AlarmSounded( object sender, AlarmSoundedEventArgs e) { // Handle the alarm sounded event MessageBox.Show("It's " + e.Alarm.ToString() + ". Wake up!"); } void delayAlarmButton_Click(object sender, EventArgs e) { // Call the DelayAlarm method double minutes = (double)this.numericUpDown.Value; DateTime newAlarm = this.alarmComponent.DelayAlarm(minutes); this.dateTimePicker.Value = newAlarm; } }
Figure 9.11 shows the formhosted, custom AlarmComponent in action. Figure 9.11. An Alarming AlarmComponent in Action
Extending Existing Components
Custom components require you to write a complete implementation from scratch. In some cases, that may result in your writing code that mostly implements a subset of properties, events, and methods already provided by an existing component. In these situations, a better approach is to extend the existing component with your specific functionality and save a lot of effort. As you may have guessed, all you need to do is derive from the desired component. For example, instead of creating a completely custom alarm component, it may be sufficient to derive directly from a Timer. To derive from an existing component, you first add a new component to your project just as you would to create a custom component. Then, you update the generated code to derive from the desired component: // AlarmComponent.cs partial class AlarmComponent : Timer {...}
After this is in place, you add your custom members as needed. The following is the alarm component implementation, refactored as a subclass of the Timer class: // AlarmComponent.cs public partial class AlarmComponent : Timer { public AlarmComponent() { InitializeComponent(); } public AlarmComponent(IContainer container) { container.Add(this); InitializeComponent(); } // Alarm property DateTime alarm = DateTime.MaxValue; // No alarm public DateTime Alarm { get { return this.alarm; } set { this.alarm = value; // Enable timer for tenth of a second intervals this.Interval = 100; this.Enabled = true; } } protected override void OnTick(EventArgs e) { // Check to see whether we're within 1 second of the alarm double seconds = (DateTime.Now - this.alarm).TotalSeconds; if( (seconds >= 0) && (seconds >= 1) ) { this.alarm = DateTime.MaxValue; // Show alarm only once MessageBox.Show("Wake Up!"); // Disable timer this.Enabled = false; } } }
One key difference is that we override Timer's protected virtual OnTick method rather than handle its Tick event. Most base classes provide protected virtual methods for public, protected, and internal events to save your having to write event registration code and to improve performance. When you extend an existing component in this fashion, you enjoy all the Windows Forms Designer support that custom components provide, including form containment and automatic resource management. Both features are enabled when a component is dragged onto a form from the Toolbox. |
Категории