Initialize It
A control created with Visual Basic is normally made up of a UserControl and any constituent controls placed on the UserControl. The Visual Basic documentation spends hundreds of pages explaining what this means and I’m not going to try to duplicate it. Instead, I will describe a common pattern that many controls follow—delegating all of the outer behavior of the control to a single internal control.
The XEditor control is, at heart, a RichTextBox control. XEditor has a UserControl that acts as the surface of the control, but after some initialization, it is mostly ignored. The internal RichTextBox control does all the work. When I created XEditor, the first step was to place a RichTextBox control on the UserControl. I gave this internal control the name txt. You’ll be seeing a lot more of the txt variable. I sized the internal control to about the size of the UserControl and wrote code to make sure the user sees the rich text box as being the control. I also enforced a minimum size on the control. In theory, this code should simply go in UserControl_Resize. Unfortunately, a bug in UserControl resizing prevents the obvious from working. If you simply put your resizing logic in the Resize event, your minimum size logic will be ignored every other time you try to resize by dragging from the top or left. To work around this limitation, put a Timer control on the UserControl and add code like the following:
Private Sub hack_Timer()
hack.Enabled = False
‘ Enforce minimum size
If Width < xMin Or Height < yMin Then Size xMin, yMin
‘ Adjust internal RichTextBox to be the size of the UserControl
txt.Move 0, 0, UserControl.ScaleWidth, UserControl.ScaleHeight
End Sub
Private Sub UserControl_Resize()
‘ Bug in resizing code forces us to move resizing to Timer event
hack.Enabled = True
End Sub
I can’t imagine why this hack is necessary. I don’t think it’s the delay that does the trick, since the hack.Interval property is set to 1. You should also be wary of recursion in the Resize event. In some cases, resizing in the UserControl_Resize event can cause the event to be called a second time. These aren’t the only gotchas you’re likely to encounter when coding standard UserControl events such as Initialize, InitProperties, ReadProperties, WriteProperties, Show, AmbientChanged, and others. There are rules for using these properties (and exceptions to those rules), but you’ll have to figure most of them out from the documentation or from trial and error.
But before we abandon UserControl and move on to the delegated txt control, I want to identify a new UserControl “event” that isn’t in the documentation. This one is called UserControl_Load, and it works kind of like Form_Load except that you have to create and raise it yourself. Often you need to do some initialization task after a control’s properties have been initialized, no matter how the control was created. But there’s no built-in event that always occurs. Instead, there are two alternate paths where you need to create by convention the UserControl_Load event that Visual Basic doesn’t give you.
If you create a new control by clicking on the Toolbox, the control will get a UserControl_Initialize event where you are supposed to initialize properties. At the end of this event, raise the UserControl_Load event like this:
Private Sub UserControl_InitProperties()
‘ Initialize properties
§
UserControl_Load
End Sub
If you load an existing control by loading its form, the control will get a UserControl_ReadProperties event where properties will be initialized from disk (through a PropertyBag object). At the end of this event, raise the UserControl_Load event like this:
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
‘ Read properties from disk
§
UserControl_Load
End Sub
You have to write UserControl_Load from scratch because Visual Basic won’t show it as a standard event in the Procedure drop-down list. For example:
Private Sub UserControl_Load()
‘ Initialize standard properties
§
End Sub
The XEditor control uses this event to initialize some internal variables that won’t be persisted in the property bag. For example, some properties might be initialized from the registry with GetSetting rather than persisted on the form. I’ll talk later about some of the things XEditor sets here, including variables for the dirty bit and overwrite mode. In my first attempt, I put this initialization code in the UserControl_Show event. It always happens at the right time (just after my UserControl_Load), but it can also happen at the wrong time—such as when the Visible property changes from False to True.
In the CEditor class provided with the previous edition, I used conditional compilation to allow clients to choose, at design time, whether they wanted to delegate to a TextBox or to a RichTextBox control. Those who wanted minimal functionality and minimal resource use could choose a TextBox control. This version could have supported a similar feature, but not at design time because there’s no way to use conditional compilation to indicate embedding different controls in a Form or UserControl.
To provide the same functionality here, I could have created two separate controls—one delegating TextBox and one delegating RichTextBox. These could have, in turn, delegated most of their shared functionality to a separate standard module. Alternatively, I could have put both a TextBox and a RichTextBox control on the same UserControl and provided a property to determine which one to delegate. Instead, I chose a different strategy. XEditor just delegates the RichTextBox control but provides a TextMode in which the RichTextBox is dumbed down to act more like a TextBox.
In the original CEditor class I had to check that the user hadn’t passed an invalid RichTextBox. For example, the MultiLine property had to be True and the ScrollBars property had to be rtfBoth to create a real editor. I checked these in the Create method and threw out clients who dared to violate my rules. XEditor doesn’t have these problems. I set the properties exactly the way I wanted them on the internal RichTextBox control at design time. I can make sure that the internal control has the normal settings of an editor or viewer rather than those of a data entry field.
The client view of XEditor is also completely changed from CEditor. Rather than creating a CEditor object and passing it a RichTextBox control at run time, the client simply adds an XEditor control from the Toolbox.
If I had my choice, XEditor would be part of VBCore. Unfortunately, Visual Basic doesn’t allow you to put public controls in a public class component or to put public classes in a controls component. This is kind of an artificial distinction, but we have to live with it. As a result, Editor and VBCore are separate but not completely independent. The XEditor control depends on procedures or classes in VBCore, so you must ship VBCore with any projects that use XEditor. VBCore, however, has no dependency on Editor.