Windows Forms 2.0 Programming (Microsoft .NET Development Series)
In Chapter 11: Design-Time Integration: The Properties Window, you saw how properties are exposed to the developer at design-time, plus the set of design-time services that allow your components to integrate with it. The goal of these features is to replace programmatic configuration experience with a comparatively easier declarative configuration experience. Configuration targets properties are required to manage state at run time and, in the case of controls, to help render UIs. However, there are times when a component requires functionality that is useful only at design-time. In these situations, it is much cleaner to separate design-time only code from that executes at run time. To help in this endeavor, we have designers. A Designer is a class that implements the IDesigner interface: namespace System.ComponentModel.Design { interface IDesigner : IDisposable { void DoDefaultAction(); void Initialize(IComponent component); IComponent Component { get; } DesignerVerbCollection Verbs { get; } } } A component uses the Designer attribute to associate themselves with a designer which, at design-time, is created and used by the Windows Forms Designer. The reason you haven't yet needed to think about designers is that all the types from which your custom components will most likely deriveComponent, Control, UserControl, and Formhave exactly two designers associated with them: using System.ComponentModel; // Designer attribute using System.ComponentModel.Design; // ComponentDesigner using System.Windows.Forms.Design;// ControlDesigner // ComponentDocumentDesigner // UserControlDocumentDesigner // FormDocumentDesigner [Designer(typeof(ComponentDesigner))] [Designer(typeof(ComponentDocumentDesigner))] interface IComponent : IDisposable {...} class Component : IComponent, ... {...} [Designer(typeof(ControlDesigner))] class Control : Component, ... {...} [Designer(typeof(ControlDesigner))] [Designer(typeof(UserControlDocumentDesigner))] class UserControl : ContainerControl {...} [Designer(typeof(FormDocumentDesigner))] class Form : ContainerControl, ... {...}
Only two of these classes have the two required designers defined; that's because if you don't define exactly two designers, the ones you don't associate with a component are associated by the base class, all the way up to IComponent. Each component needs two designers because there are two contexts in which you use designers. The first is the Designer tab in VS05, which is shown by default when you double-click a Form, UserControl, or Component in Solution Explorer. The Designer tab hosts the component document designer that provides the full document-viewing experience (hence its name). [1] [1] The component document designer is also known as the root designer because it implements the IRootDesigner interface (from the System.ComponentModel.Design namespace) to separate it from a plain component designer. The second designer context is provided by a component designer, which is the designer you get when you interact with the component as contained by a document designer. For example, if you have a Timer in the component tray of a Form, the Timer's designer is a component designer (specifically the default ComponentDesigner provided by the IComponent interface). A normal component designer implements only the IDesigner interface. If you'd like to replace either the component document designer or the plain component designer, you implement the appropriate interface and associate it with your component using the Designer attribute. Although the implementation of a full-blown component document designer is beyond the scope of this book, it's easy to add a plain old custom designer to your custom components to deploy design-time-only functionality. ComponentDesigner
Consider the ability of a control to gain access to its parent at run time, using either the FindForm method or the Parent property. Either of these is suitable if the child control wants to gain access to functionality provided by its parent (such as the parent's caption text): Control host = this.Parent; // Find parent MessageBox.Show("My parent says: " + host.Text);
Further, it's not hard to imagine the need for a noncontrol component to gain access to the functionality exposed by the parent. Unfortunately, components don't offer any support for discovering their host container. An IComponent implementation can be hosted by any ISite implementation, but the site is not the same as the component's hosting form or user control. For example, suppose the AlarmComponent from Chapter 9: Components needs the name of its container to use as a caption for any message boxes it may need to display. In this case, we need AlarmComponent to store a reference to its host container and to use the host container's Text property for all message boxes: // AlarmComponent.cs ... partial class AlarmComponent : Component { ... ContainerControl host; // Hide from Properties window and persist any value // to container's InitializeComponent [Browsable(false)] [DefaultValue(null)] public ContainerControl Host { get { return this.host; } set { this.host = value; } } void timer_Tick(object sender, System.EventArgs e) { ... // If no handlers, display a message box MessageBox.Show("Wake up!", this.host.Text); ... } } ... }
AlarmComponent can't determine its host container on its own, so its default value is null. However, this means that we must supply the value for use at run time. To do this, we influence the Windows Forms Designer's serialization behavior to persist the host reference to the host's InitializeComponent method at design time: // MainForm.Designer.cs partial class MainForm { ... void InitializeComponent() { ... this.alarmComponent = new AlarmComponent (this.components); ... // alarmComponent this.alarmComponent.Host = this; } }
Unfortunately, we can't use the specific serialization techniques you saw in Chapter 11 because they rely on interaction with the Properties window; we don't want developers to have to risk proper functioning of the component by making them responsible for setting it, particularly when the information is available already. A custom designer helps solve this problem, but rather than implement IDesigner directly, we're better off deriving from ComponentDesigner. ComponentDesigner not only happens to implement IDesigner, but also lets us grab a component's host container and ensure that its value is serialized to InitializeComponent: // HostComponentDesigner.cs using System.ComponentModel.Design; // From System.Design.dll ... class HostComponentDesigner : ComponentDesigner { }
We then use the Designer attribute to assign our new designer to a component: // AlarmComponent.cs [Designer(typeof(HostComponentDesigner))] partial class AlarmComponent : Component {...}
The next step is to identify which property on the component will store the host container reference that HostContainerComponentDesigner will look for. A custom attribute is perfect for this job, which has been assigned to the component: // HostPropertyAttribute.cs class HostPropertyAttribute : Attribute { string propertyName; public HostPropertyAttribute(string propertyName) { this.propertyName = propertyName; } public string PropertyName { get { return this.propertyName; } } } // HostComponentDesigner.cs [Designer(typeof(HostComponentDesigner))] [HostProperty("Host")] partial class HostContainerComponent : Component {...}
Finally, we need for the custom designer to acquire a reference to the component and to set the property specified by the HostProperty attribute to its container component. For components and container components, ComponentDesigner helps by providing the Component and ParentComponent properties: // HostComponentDesigner.cs using System.ComponentModel.Design; // From System.Design.dll ... class HostComponentDesigner : ComponentDesigner { public override void InitializeNewComponent( System.Collections.IDictionary defaultValues) { base.InitializeNewComponent(defaultValues); IComponent customComponent = this.Component; IComponent parentComponent = this.ParentComponent; // Don't bother if parent is not a container if( !(parentComponent is ContainerControl) ) return; // Get the name of the property on the component // that will store the host reference, defaulting // to "Host" if not found string propertyName = "Host"; AttributeCollection attributes = TypeDescriptor.GetAttributes(customComponent); foreach( Attribute attribute in attributes ) { if( attribute is HostPropertyAttribute ) { HostPropertyAttribute hpAttribute = (HostPropertyAttribute)attribute; if( !string.IsNullOrEmpty(hpAttribute.PropertyName) ) { propertyName = hpAttribute.PropertyName; } break; } } // Set property with host container PropertyInfo property = customComponent.GetType().GetProperty(propertyName); if( property != null ) { property.SetValue( customComponent, (ContainerControl)parentComponent, null); } } }
This code is deployed to an override of ComponentDesigner's InitializeNewComponent method, which is called by the designer host when a component is first dropped onto a form, and turns out to be a nice time to do our processing. We then grab the parent component, check whether it's a container control, and, if it is, set the specified property on the component (in our case, AlarmComponent). This ensures that the desired code is serialized to InitializeComponent. By default, our implementation of InitializeNewComponent automatically looks for a property named "Host" on the component if the HostProperty attribute isn't provided. The next time AlarmComponent is dropped onto a container control's designer surface, the desired property initialization is automatically serialized via InitializeComponent. Because the code in both HostComponentDesigner and the HostProperty attribute is generic, the only code that we need to write is a property to store a reference to the host and, of course, the code to use the reference after it's been acquired. Further, if the property you add to your component is called "Host," you don't need to use the HostProperty attribute at all: // AlarmComponent.cs ... [Designer(typeof(HostComponentDesigner))] partial class AlarmComponent : Component { ... // Set by HostComponentDesigner // ("Host" is the default property name used by our custom designer) ContainerControl host; [Browsable(false)] [DefaultValue(null)] public ContainerControl Host { get { return this.host; } set { this.host = value; } } void timer_Tick(object sender, System.EventArgs e) { ... // Use the host set by the designer MessageBox.Show("Wake up!", this.host.Text); ... } } ... }
In addition to leveraging a nice design-time feature, the key reason to use a custom designer is to create a clean separation of design-time and run-time code. This practice follows the tradition honored by type converters and UI type editors, as you saw in Chapter 11.[2] [2] As an alternative, you can use the same technique used by System.Timers.Timer: It implements the SynchronizingObject property, which contains the code to find Timer's parent component using design-time services. Your favorite decompiler will help here. ControlDesigner
Beyond capturing design-time information for run-time processing, designers are well suited to performing design-time-only processing on behalf of a custom control, such as rendering additional design-time UI elements to optimize its appearance in the Windows Forms Designer. For example, the SplitContainer control displays a dashed border when its BorderStyle is set to BorderStyle.None. This design makes it easier for developers to find it on the form's design surface in the absence of a visible border and to spot the areas within which they can place other controls, as illustrated in Figure 12.1. Figure 12.1. SplitContainer Dashed Border When BorderStyle Is None
Because BorderStyle.None means "Don't render a border at run time," the dashed border is drawn only at design time for the developer's benefit. Of course, if BorderStyle is set to BorderStyle.FixedSingle or BorderStyle.Fixed3D, the dashed border is not necessary, as illustrated in Figure 12.2. Figure 12.2. SplitContainer with BorderStyle.Fixed3D
Although it's not obvious, the dashed border is not actually rendered from the control implementation. Instead, this work is conducted on its behalf by a custom control designer. The AlarmClockControl from Chapter 11 could benefit from this capability; when it has an Analog clock face, it's difficult to determine where the edges and corners of the control are when it's not selected on the design surface. To help out, we can render a SplitContainer-style dashed border at design time, which would look something like Figure 12.3. Figure 12.3. Border Displayed from AlarmClockControlDesigner
ControlDesigner doesn't implement the required dashed border functionality, so we create a custom designer and associate it with AlarmClockControl. Because AlarmClockControl derives from ScrollableControl, the most suitable way to start is to derive from ScrollableControlDesigner itself: class AlarmClockControlDesigner : ScrollableControlDesigner {...} To paint the dashed border, AlarmClockControlDesigner overrides the OnPaintAdornments method: class AlarmClockControlDesigner : ScrollableControlDesigner { ... protected override void OnPaintAdornments(PaintEventArgs e) {...} ... }
You could manually register with the Control.Paint event to add your design-time UI, but overriding OnPaintAdornments is a better option because it is called only after the control's design-time or run-time UI is painted, letting you put the icing on the cake: class AlarmClockControlDesigner : ScrollableControlDesigner { ... protected override void OnPaintAdornments(PaintEventArgs e) { base.OnPaintAdornments(e); // Draw border Graphics g = e.Graphics; using( Pen pen = new Pen(Color.Gray, 1) ) { pen.DashStyle = DashStyle.Dash; g.DrawRectangle( pen, 0, 0, this.AlarmClockControl.Width - 1, this.AlarmClockControl.Height - 1); } } // Helper property to acquire an AlarmClockControl reference AlarmClockControl AlarmClockControl { get { return (AlarmClockControl)this.Component; } } }
Then, we associate AlarmClockControlDesigner with AlarmClockControl, aided by the Designer attribute: [Designer(typeof(AlarmClockControlDesigner))] partial class AlarmClockControl : ScrollableControl, ... {...}
The result is that AlarmClockControl's design-time-only dashed border is now displayed, just like the one shown in Figure 12.3. Design-Time-Only Properties
One way to improve on the dashed border is to give developers the option of not showing it (maybe it offends their WYSIWIG sensibilities). Because this is not a feature that should be accessible at run time, what's needed is a design-time-only property, ShowBorder. And designers are exactly the right location to implement them. You start by adding the basic property implementation to the custom AlarmClockControlDesigner with the appropriate attributes: class AlarmClockControlDesigner : ScrollableControlDesigner { ... bool showBorder = true; ... protected override void OnPaintAdornments(PaintEventArgs e) { ... // Don't show border if hidden or does not have an Analog face if( (!this.showBorder) || (this.alarmClockControl.Face == ClockFace.Digital) ) return; ... } // Provide implementation of ShowBorder to provide // storage for created ShowBorder property [Category("Design")] [DesignOnly(true)] [DefaultValue(true)] [Description("Show/Hide a border at design time.")] public bool ShowBorder { get { return this.showBorder; } set { // Change property value PropertyDescriptor property = TypeDescriptor.GetProperties( typeof(AlarmClockControl))["ShowBorder"]; this.RaiseComponentChanging(property); this.showBorder = value; this.RaiseComponentChanged( property, !this.showBorder, this.showBorder); // Update clock UI this.AlarmClockControl.Invalidate(); } } }
The ShowBorder set accessor stores the new value and invalidates the control to request a repaint. Additionally, it hooks into the design time's component change service, which broadcasts the property change in a manner that ensures the use of certain designer features, including an immediate Properties window refresh and undo. This isn't enough on its own, however, because the Properties window doesn't examine a custom designer for properties when the associated component is selected. The Properties window acquires a list of a component's properties using the TypeDescriptor class's static GetProperties method (which in turn uses reflection to acquire the list of properties from the type). To inject a design-time-only property into the list of properties returned by GetProperties, a custom designer can override the PreFilterProperties method and add the property manually: class AlarmClockControlDesigner : ScrollableControlDesigner { ... protected override void PreFilterProperties(IDictionary properties) { base.PreFilterProperties(properties); // Create design-time-only property entry and add it to the // Properties window's Design category properties["ShowBorder"] = TypeDescriptor.CreateProperty(typeof(AlarmClockControlDesigner), "ShowBorder", typeof(bool), null); } ... }
The IDictionary argument of the PreFilterProperties method allows you to populate new properties by creating PropertyDescriptor objects using TypeDescriptor's CreateProperty method, passing arguments that appropriately describe the new property. Although we pass null as the last argument, you can pass an array of Attributes instead of adorning the custom designer property with those attributes: // Create design-time-only property entry and add it to the // Properties window's Design category properties["ShowBorder"] = TypeDescriptor.CreateProperty( typeof(AlarmClockControlDesigner), "ShowBorder", typeof(bool), new Attribute[] { new CategoryAttribute("Design"), new DesignOnlyAttribute(true), new DefaultValueAttribute(true), new DescriptionAttribute("Show/Hide a border at design time.") } );
Either way, because the property is adorned with a DesignOnly attribute whose constructor is passed a value of true, ShowBorder's value is serialized to the form's resource file rather than to InitializeComponent when its value differs from the default (is false), as shown in Figure 12.4. Figure 12.4. ShowBorder Property Value Serialized to the Host Form's Resource File
This also has the effect of clearly delineating the difference between design-time-only properties and those that can be set at design time and run time. If you need to alter or remove existing properties, you override PostFilterProperties and act on the list of properties after TypeDescriptor has filled it using reflection. Pre and Post filter pairs can also be overridden for events if necessary. Figure 12.5 shows the result of adding the ShowBorder design-time property. Figure 12.5. ShowBorder Option in the Properties Window
The key concept is that when you have design-time-only functionality, you should first consider custom designers to avoid burdening your components with code that is not useful at run time. You can achieve much with custom designers, although the scope of such possibilities is beyond this chapter. However, one specific feature warrants further attention, particularly if you want your controls to be more usable. That feature is known as smart tags. |
Категории