Creating a Base Control
The best way to create skinned controls is by letting them inherit from a base class that implements the basic features a skinned control needs. Listing 1
shows the code implementing this base class, which is an abstract class (that is, it needs to be inherited) named SkinnedControlBase. Note that the base class inherits from Control, not from WebControl. You would use the WebControl
class only if you create a custom control that has visible output, and which should support styling. Technically that is also true for a skinned control, but it uses the skin to actually display itself. There is no display logic in the control itself, and any styling properties associated with the WebControl
class would serve no purpose. Because the control will contain child controls, it also needs to implement the INamingContainer
interface, so each child control will get a unique name within the page.
One property that all skinned controls need is, of course, where to find the skin. Since this is something that should be configurable, this should be a property of the control. In Listing 1
property serves this purpose. Note that the value of the SkinPath
property is not saved in the ViewState
, so a change to another skin at runtime will not be persistent. Don't worry about this, however, because in most cases you either have a specific set of skins, or you select the skins based on some profile.
|You don't need to register the user control with the page you're using it in because the user control is retrieved by the skinned control.|
The CreateChildControls method plays an important role in skinned controls. With "normal" server controls, this is the place where you would add child controls to the controls collection. With a skinned control, this is where you add the skin to the controls collection and then wire up the child controls of the control to those in the skin. Before you add the skin, you should clear the control tree and initialize it by calling the base method, to make sure that any controls added during earlier phases in the page's execution are removed. Then you lookup the skin and add it to the controls collection. You don't need to register the user control with the page you're using it in because the skinned control does the work to retrieve the user control. This is logical; otherwise you would need to register each skin with the page, making skins much less useful.
Note that to keep the sample code short, I didn't add any error handling code. You should, however, make sure that the skin you want exists and that it initializes correctly. During initialization, my sample code wires the child controls to controls in the user control. You'll have an error if a child control of the control doesn't exist in the skin, or if you use the wrong control type. You should properly handle each case so the skin designer knows what he or she is doing wrong. The initialize process should always occur, but it is specific to the control. I took the extra step to define an abstract method named Initialize, which the actual control should override for wiring up the controls. Alternatively you could tag the child controls of a skinned control with custom attributes, and use reflection to handle this task from within the base class. Then control developers don't need to worry about wiring up child controls in the Initialize method. However, such a discussion is beyond the scope of this article.
Creating the Skinned Control
With the base control in place, now we can implement an actual skinned control. I've found that once I've implemented the base class, creating a skinned control is easier than creating a non-skinned control because you don't have to add controls, formatting, and such with the Render, RenderContents, RenderChildren, or CreateChildControls methods. That is all part of the skin, so you can actually focus 100% on adding the functionality the control should have.
Listing 2 shows the code of a skinned control inheriting the base control defined in Listing 1. In this case, I've created a LoginBox control that consists of a username TextBox, a password TextBox, and a login Button. Each of these controls is declared globally within the class. I've also defined a default skin path in case the designer doesn't add one. In the constructor, my code checks the skin path given by the designer, and if it's not there, the controls will use the default skin path. If you intend to use a skin defined in a profile, use the default skin path to implement that. You can also implement the default skin path in a constructor of the base class, but I've found this to be tricky at best.
The LoginBox control contains two string properties: Username and Password. The values of these properties correspond to the Text properties of the username and password TextBox controls. Before accessing these controls, you need to make sure that they've been added to the control tree by calling EnsureChildControls. If you don't do that, you may get an erroneous value when accessing the values of the Username or Password TextBox, providing you don't get an error first. The LoginBox control also contains a Login event to notify the user of the control that someone pressed the login button. Read the sidebar, "Event Handling," to learn how I handle events in a skinned control (or a composite control). In Listing 4, which I'll discuss later, you can see that I attached event handling code to the event exposed by the LoginBox.
Last but not least, the LoginBox control overrides the Initialize method of the base control. You can see that I attach the privately declared child controls of the LoginBox control to the corresponding control in the skin using the FindControl method. As I mentioned earlier, this is where things can go wrong. The designer needs to use exactly the same type and name for controls defined in the skinned control. This means that you need to document which child controls are part of the skinned control, and what their names are.