7.2 Simple Integration AttributesThere are certain custom attributes that Visual Studio .NET will look for when you use a component. These allow you to improve your component's integration with the development environment, often without having to write any additional code at all. (Some of the more advanced integration features require both attributes and code, but we shall look at those later.) 7.2.1 Toolbox BitmapOne of the first things you will want to do to your component is to change its toolbox bitmap. By default, all components get the cog icon shown in Figure 7-1, and there is no way for the end user to change it. It would be hard to locate specific items in a full toolbox if they all had the same bitmap, so it is best to supply a graphic for your component. You can specify a custom toolbox bitmap by applying the ToolboxBitmap attribute (defined, inexplicably, in the System.Drawing namespace) to your class. The ToolboxBitmap attribute has three constructors. The first takes a string specifying the name of the file containing the bitmap. You will not normally use this, since it requires the bitmap to be stored in a separate file. It is much more convenient for a component to be a single self-contained file, so you will want to use one of the other constructors—both of these expect the bitmap to be an embedded resource in the component (see the Embedded Resources' sidebar). The most commonly used constructor is the one that takes just a type object; its use is illustrated in Example 7-2. Example 7-2. The ToolboxBitmap attributeusing System.Drawing; using System.ComponentModel; [ToolboxBitmap(typeof(MyComponent))] public class MyComponent : Component { . . . as before By convention, the type object passed to the ToolboxBitmap attribute is the component's type, but this is not a requirement. The type object serves two roles here. First, Visual Studio .NET will look for the bitmap resource in the assembly in which the type is defined. (Although in principle you could use a bitmap defined in an external assembly by referring to a type defined in that assembly, in practice you will always want this to be the same assembly as the component itself.) Second, the resource name will be based on the fully qualified name of the type. So if MyComponent in Example 7-2 is defined in the MyNamespace namespace, Visual Studio .NET will look for an embedded resource called MyNamespace.MyComponent.bmp. (In other words, it takes the fully qualified name of the type and appends .bmp.) For this to be of use, you must make sure that your component contains an embedded bitmap resource with the right name. To do this, first add a bitmap to the project. (Select Add New Item... from the project's context menu in the Solution Explorer and choose Bitmap File from the Resources category.) Your bitmap should have the same name as the component class. Visual Studio .NET automatically prepends the project default namespace to the bitmap when embedding it as a resource, so you do not need to supply the fully qualified name. In our example, the bitmap filename would be MyComponent.bmp. Your bitmap should be 16x16 pixels. Visual Studio .NET will look at the color of the bottom-left pixel and will draw all pixels with that color as transparent. By default, bitmaps do not get compiled into projects, so simply adding a bitmap to the project is not enough. You must change the bitmap's Build Action to be Embedded Resource. You can do this from the bitmap project item's Properties page, shown in Figure 7-3. Note that bitmaps have two different property pages: the one that appears when you are editing the bitmap and the one that appears when you select the bitmap item in the Solution Explorer. The Build Action is located in the latter. Note that the filenames are, somewhat unusually, case sensitive. This is because the mechanism by which the bitmap resource is retrieved from the component is case sensitive. (Assembly.GetManifestResourceStream is used.) So you must make sure that the bitmap name's case matches that of your class exactly and that the .bmp extension is all lowercase.
There is a third constructor for the ToolboxBitmap attribute, which takes both a type reference and a string. The documentation is a little misleading here, as it suggests that the type reference is used only to determine which assembly the name is in and that you can specify the name of the resource with the string. This is not quite true—the type object is used in two ways. The embedded resource name is formed by taking the namespace of the type and appending the string supplied. So if you were to supply the following reasonable-looking parameters to the custom attribute: [ToolboxBitmap(typeof(MyComponent), "MyNamespace.MyComponent.bmp")] Visual Studio .NET would look for an embedded resource called MyNamespace.MyNamespace.MyComponent.bmp! Sadly, just as there is no way to force Visual Studio .NET to compile in an embedded bitmap with the exact name that you require, there is also no way to specify the precise name of the embedded resource to use with the ToolboxBitmap attribute. In both cases, the namespace will be added, and there is nothing you can do about it. So you should stick to the following rules:
Since the Visual Studio .NET wizards will always add new components into the project default namespace, it is easy to stick to these rules in practice. 7.2.2 Categories and DescriptionsWhen Visual Studio .NET displays properties and events in the Properties window, it usually provides two hints as to their use: it groups members by category, and it provides a short textual description when an item is selected. By default, your components' members will appear in the Misc category and will have an empty description, but it is easy to fix this. You can use the Category attribute (defined in the System.ComponentModel namespace) to determine the category in which members appear in the Properties window. The attribute just takes a string, which is the name of the category. You can use whatever you like as a string, but it is recommended that you use one of the standard categories if possible. (These are Action, Appearance, Behavior, Data, Design, DragDrop, Focus, Format, Key, Layout, Mouse, and WindowStyle.) The Description attribute is also very simple to use. As with the Category attribute, it just takes a string. This string will be displayed in the Description pane of the Properties window when the property is selected. Example 7-3 shows both the Category and the Description attributes in use on the Title property of our component. With these in place, the Properties window will look like Figure 7-3. Figure 7-3. Category and description in the Properties windowExample 7-3. A property with a category and a descriptionusing System.ComponentModel; . . . [Category("Behavior")] [Description("The component's name")] public string Title { . . . as before 7.2.2.1 LocalizationBoth the Category and the Description attributes cause text to be displayed in Visual Studio .NET's user interface. If your component might be used at design time in different countries, this presents a localization issue—how do you ensure that the category and description strings are appropriate to the locale? With the Category attribute, life is very easy if you stick to the built-in category names (listed earlier). Visual Studio .NET recognizes these names and will translate them for you automatically. The Description attribute presents more of a challenge. (As does using nonstandard category names.) If you want your description strings to be localized, you must create your own attribute class that derives from the Description attribute, overriding its Description property to perform the appropriate lookup. (You would normally use the ResourceManager class in the System.Resources namespace to look the name up in the appropriate satellite resource assembly.) To make custom category names localizable, you use a similar technique—you create your own class that derives from the Category attribute. For some reason, instead of overriding the Description property, you are required to override a protected method called GetLocalizedString and look up the translated resource there; the Category attribute class will call this when translation is required. 7.2.3 Default Events and PropertiesWith most of the .NET Framework's built-in components, double-clicking on them will cause an event handler to be added to your code. (This is true of all components that raise events, not just controls.) With components that raise multiple events, Visual Studio .NET always seems to know which event handler to add—for a button it will handle the Click event, for a text box it will handle TextChanged, and so on. And likewise, if you drag a new component onto a form and just type in some data without first selecting a property in the grid, it will usually pick a sensible property to modify (e.g., Text for most controls). For our own components, we can determine which event and property Visual Studio .NET will choose under these circumstances. There are attributes for choosing a default property and event. These are applied to the class and simply take the name of the relevant member as a construction parameter, as shown in Example 7-4. Example 7-4. A component with a default event and property[DefaultEvent("OnTitleChanged")] [DefaultProperty("Title")] public class MyComponent : Component { 7.2.4 Property VisibilitySometimes your components may have properties that you will not wish to be displayed in the Properties window. This is particularly common with controls—the base control classes in Windows Forms and Web Forms provide many standard properties, not all of which make sense in derived controls. (For example, the Panel control does not use the Text property.) Also, some properties are designed to be used only from code, such as the Windows Forms Control class's Created property, and it would be confusing and unhelpful for them to appear in the property grid. To prevent such properties from appearing, you can mark them with the Browsable attribute. This takes a Boolean; specifying false prevents the property from appearing in the Properties window. (If you are using this to hide an unused property inherited from the base class, you will need to override that property in order to use this attribute. If the only reason you are overriding the property is to apply an attribute, you should just defer to the base class in the implementation, as Example 7-5 does.) Example 7-5. A nonbrowsable property[Browsable(false)]
public override string Text
{ get { return base.Text; } set { base.Text = value; } }
Although the property in Example 7-5 will not appear in the Properties window, it will still show up in IntelliSense in source editing windows. If you wish to prevent it from appearing even in source windows, you can apply the EditorBrowsable attribute. If you pass the EditorBrowsableState.Never enumeration value to the constructor, the member will not appear in IntelliSense lists. (Developers who know the property is there will still be able to use it however—the compiler itself ignores this attribute.) If you do not supply an EditorBrowsable attribute, the effective default is EditorBrowsable.Always. There is also an EditorBrowsable.Advanced setting, which is supposed to hide the property for all but advanced users. By default, this hides the property in Visual Basic .NET projects but does not hide it in C# projects. (See Appendix F for information on how to change this and other text editor settings.) 7.2.5 Designer SerializationWhen users change your component's properties in the Properties window, Visual Studio .NET generates code that will set the property at runtime. (It does this in the autogenerated InitializeComponent method; it effectively serializes the properties as code.) Of course, you will want code to be generated only when the property has actually been changed. Visual Studio .NET relies on knowing what your property's default value is to work out whether the value has been changed. (It doesn't just remember what the value was before the user started making edits.) You can tell Visual Studio .NET what a property's default value is by applying the DefaultValue attribute. This has a wide array of constructors—most of the intrinsic types get their own constructor, and there is also one that takes an object, allowing you to pass any value at all. When Visual Studio .NET generates the InitializeComponent method, it will compare your component's property's current value to the default value, and generate initialization code only if they differ. Some properties' default values are determined at runtime. For example, a Control object's default BackColor property value is determined by its parent. Under these circumstances, a DefaultValue attribute cannot be used. Instead, the property should have an associated ShouldSerialize method. (For example, if the property in question is called Title, then there should be a ShouldSerializeTitle method.) This method should return a Boolean, indicating whether the property has been set, and the designer therefore needs to generate code to serialize this property. If you supply a ShouldSerialize method, you should also supply a corresponding Reset method (e.g., ResetTitle). Visual Studio .NET will use this when the user selects the Reset item from the property's context menu. This method should cause the property to return to its original state (i.e., the value should revert to the dynamically determined default, and the ShouldSerialize method should return false). You can disable designer serialization entirely if necessary. (For example, it would not be worth serializing a property whose value is calculated from other properties at runtime.) The DesignerSerializationVisibility attribute allows you to control what code is generated at serialization time. If you construct the attribute with the DesignerSerializationVisibility.Hidden enumeration member, the property will never have any code generated for it in the InitializeComponent method. The default setting is the Visible enumeration member. There is also a Content member, which indicates that the designer should enumerate the property's contents, rather than trying to serialize the whole property in one step. You would normally do this only if the property's type does not serialize correctly, but each of its individual member properties can be serialized. (For example, if you have a custom type that has no corresponding TypeConverter, Visual Studio .NET will not know how to generate code to serialize properties of this type. But if this custom type's own properties are all of standard types, the Content setting would cause Visual Studio .NET to generate code to serialize each of these individually.) 7.2.6 Data BindingIf you are writing a control, you may wish for certain properties to be presented under the (DataBindings) section of the Properties window—this is where Visual Studio .NET allows data bindings to be configured interactively. Programmatically, any control property can be bound to a data source, but only those explicitly marked as bindable will appear under (DataBindings). By default, the Control class's Tag and Text properties are bindable, but you can add your own with the Bindable attribute. Simply mark any property that you want to appear in the data binding section with this attribute, passing in true as a constructor parameter. |