In-Depth
Share Controls Between WinForms
Pass a user control from Windows Form to Windows Form to rearrange your graphical user interface (GUI) layout without needing to reinitialize the shared user control and more.
Technology Toolbox: VB.NET, C#, Windows Forms
You can use various techniques for working with user controls in .NET that involve complex and time-intensive rendering. The primary one I present gives you the capability to pass a user control from Windows Form to Windows Form so you can rearrange your graphical user interface (GUI) layout dramatically without needing to reinitialize the shared user control.
Sometimes sharing a user control makes a lot of sense. However, most of the time, that's not the case. I state this plainly now to avoid any confusion about what I'm advocating here. When this technique is useful, it's extremely useful. However, use it only when it's necessary because most of the time it makes your applications unnecessarily complex.
The benefit of passing user controls between Windows Forms (or control containers of any sort, for that matter) is that the passed control retains its state. Consider a control that draws complex scenes. When you want to display the same information using the same type of control on another Windows Form, this control could take any amount of time to reinitialize. For the sake of this discussion, suppose you have a scene that takes 15 seconds to render. If you have a scene drawn on a given Windows Form and want to load a second Windows Form with the same scene, this form could take another 15 seconds just to load.
Alternatively, you could pass the existing control to the second Windows Form if the original Windows Form doesn't need it. This allows the application to display the second Windows Form immediately with the control in exactly the same state it was when it was on the first Windows Form.
I've provided a sample app and Visual Studio solution with this article that demonstrates when passing user controls between Windows Forms is useful. It includes a control that takes a long time to render. In fact, this screenshot took about 12 hours to render (see Figure 1). It could have been drawn much faster, but the idea behind the sample app is to show you a control that takes long enough to initialize that it can benefit from the techniques I cover in this article.
Given the actual implementation for passing controls between Windows Forms is simple, I've created a UI that is more flashy and fun than Windows UIs typically are. It has brighter colors and includes slightly unconventional UI elements such as hyperlinks for a flat menu system and a link (with the text >>>) to pop the control on and off the main Windows Form. If you don't like the UI, please look past it. It has nothing to do with the main concepts I'm presenting.
Pass Controls Between Containers
I use these techniques in the sample application and code: passing user controls between control containers, dynamic control manipulation, double-buffered rendering in GDI+, and animating GDI+.
You can pass user controls between Windows Forms just as easily with control containers such as Panels, TabPages, and GroupBoxes, but given passing controls between different Windows Forms is an extreme case, it's the one I'll cover.
It might or might not be obvious to you that you can pass user controls between Windows Forms. I must admit it caught me by surprise. I thought originally that once a control was created in its parent, it had to stay there. One day I was feeling particularly rebellious and tried to move a control from one Windows Form to another anyway. I was pleasantly surprised to find out that it worked perfectly.
Now those of you who were not surprised might wonder why I thought this. A main reason is that there are analogous issues when accessing controls across threads. Certain controls can only be accessed directly from the thread that created them. So in a multithreaded application, you can't do something as simple as add a control created on a worker thread to the Form.Controls collection. If you do, you get this error: "An unhandled exception of type 'System.ArgumentException' occurred in system.windows.forms.dll. Additional information: Controls created on one thread cannot be parented to a control on a different thread."
I had thought if these restrictions exist between controls and threads, certainly passing the control between Windows Forms would have even bigger issues. Fortunately, that doesn't seem to be the case. Now that I've discussed some technical details of passing controls, I'll show you the simple code involved. Suppose myControl is a user control placed on the design surface of a Form called MainForm. To pass the user control from MainForm to another Form, you'll need to remove the control from the current MainForm first (see Listing 1).
What happens in the receiving Windows Form's constructor (the OtherForm class in the example) is the other side of the coin (see Listing 2). It's pretty straightforward. In the constructor, the receiving Windows Form takes the passed control and adds it to its Controls collection. Then the Windows Form sets layout properties for the control, such as the Dock property, and ensures the control is visible. Finally, after the receiving Windows Form (OtherForm) is done with the call to ShowDialog and has been closed, the main Windows Form adds the control back to its own Controls collection.
That's all there is to passing controls between Forms. The situation gets more complicated if the two Windows Forms are siblings as opposed to being a parent and child. I just discussed a parent and child example: MainForm displays OtherForm using ShowDialog, which makes OtherForm a child of MainForm.
The sample code for the pop-off Windows Form addresses a complex sibling relationship. I won't go into detail here, but its solution uses .NET delegate-based events in the pop-off Windows Form. These events help track when the main Windows Form should reclaim the control from the pop-off Windows Form.
Manipulate Controls Dynamically
I've covered dynamic control manipulation implicitly, but I'll touch on it again. To add a control to your Windows Form's surface at run time, simply set the layout properties of the control, then add that control to your Form's Controls collection. The sample code includes a complex layout scenario in the pop-off related code. Here's a simple example:
// ...
this.myControl = myControl;
myControl.Dock = DockStyle.Fill;
this.Controls.Add(myControl);
myControl.Visible = true;
// Just in case it's not visible.
Note that you set the layout properties before adding the control to the Windows Form's Controls collection. Technically, these operations are commutative. But each layout property requires the control be repainted or repositioned. If the Windows Form is visible already, the control jumps around needlessly while it's being initialized. By setting the properties before adding the control to the Controls collection, you ensure the control becomes visible in just the size and shape you want it to.
Double buffering provides flicker-free rendering in Windows and other GUI systems. With many GDI+ objects, the painting is done so it creates flickering (that is, it uses single buffering). While I consider it poor design, even some built-in controls in Windows Forms flicker. The TreeView control comes to mind.
Luckily, when you're doing your own painting in GDI+, you can easily create a double-buffered surface that provides truly flicker-free rendering. The sample uses this technique to avoid flickering when drawing the control that is passed around.
To get a double-buffered GDI+ surface, simply pick your favorite basic control. I used System.Windows.Forms.Label because it's a lightweight control, but you could also choose Panels, GroupBoxes, and so on. Next, create a derived class from your chosen control:
public class RenderLabel :
System.Windows.Forms.Label
{
public RenderLabel()
{
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.ResizeRedraw, true);
}
}
Then add the base control to your Windows Form's design surface in the designer. Once you've done this, go to the code behind for that Windows Form and replace the declaration of the control with your derived type as well as the one line in InitializeComponent where the instance is actually created. Declare this member variable in the class that defines the control in the sample:
private RenderLabel labelRenderTarget = null;
It used to be like this:
private Label labelRenderTarget = null;
Now, in the designer, add a handler to the Paint event of that control. That's where you'll do all your rendering logic. You called SetStyle with the correct parameters in your derived control class, so the double buffering is handled automatically. You don't need to write special double-buffering code. Simply implement your paint handler as you would normally, and Windows takes care of the rest.
Just remember that you told Windows that all painting will be done in the Paint event handler. This corresponds to the call to SetStyle with the parameter ControlStyles.AllPaintingInWmPaint. So be sure to stand by your word. That is, don't try to paint to that label outside of the paint event handler. If you need to refresh or repaint it, call Invalidate instead of trying to paint directly to it.
Animate GDI+
It's simple to add animation to the control in the sample application. The control creates a timer and starts it in response to your request to start the animation. Then, call Invalidate on the RenderLabel, which is your GDI+ double-buffered surface in the handler for the Tick event from that timer. That, in turn, calls your Paint event handler indirectly, and the scene is slightly changed and redrawn. The sample code includes details on how the animation for those lines is handled.
A more advanced animation system takes the time elapsed between updates to provide an animation experience that's frame-rate independent. Your animation slows down as you increase the size of the window. This is because the animation happens on a per-frame basis rather than a per-slice-of-time basis. You can avoid this when you put more effort into animation handling, but that is a topic for another article and another time.
You might be wondering where you can pass controls between Windows Forms in real-world applications. One useful place to use this technique is when you create a full-screen text editor mode in an application. Imagine you have a text editor with many docking windows and controls such as Visual Studio. You want to support a full-screen minimal view that shows only the actual text. But suppose there's a considerable lag time to load the text editor control for truly large files. You could pass the already initialized text editor control to a full-screen Windows Form that can serve as the minimal view. This saves you whatever initialization issues you might have with the full-screen view, as well as synchronization issues when you go back to the standard view.
About the Author
Michael Kennedy is a founding partner and software engineer at United Binary. He has been developing software for more than nine years. The last three of those years, he has been focused solidly on .NET development. Reach Michael at [email protected].