Extending Windows Forms - lightweight views

This article describes an implementation of lightweight views - visual objects that behave like Windows Forms controls but are windowless. Such objects simplify some user interface design tasks and conserve system resources.

This article describes an implementation of lightweight views - visual objects that behave like Windows Forms controls but are windowless. Such objects simplify some tasks user interface design tasks and conserve system resources.

In about 1990 I was working at the Computer Centre of Russian Academy of Science. At the time a colleague of mine was developing a kind of an interactive quiz application. He was working on a pretty descent computer of the time, an Intel 80386-based PC with 8 megabytes of memory running Windows 3.1 in Extended Mode and used an excellent developer's environment Borland C++ 3.1. His first attempt with the quiz UI was a scrollable window that contained a 12x12 matrix of panels. Each panel contained a question (a static text control) and 3 radio buttons with possible answers. The UI was build on top of the Object Windows Library and stylish Borland Windows Custom Controls (BWCC). The program worked, but was making the computer to crawl slowly.

The explanation of this fact was simple. Each panel with a question consumed at least 5 window handles: itself, a static box with the question and three radio buttons. Thus the whole UI consumed at least 1 + 12*12*5 = 721 window handles and the good old Windows 3.1 couldn't bear it.

The developer solved the problem, of course. He turned the matrix into a kind of a wizard. But I remembered well the whole awkward situation.

Recently I faced it again. In a project I needed something that was suspiciously familiar - a grid of panels with controls. The difference is that the number of panels would be smaller and the number of controls on a panel would be significantly bigger. Consuming hundreds of window handless seems to me unwise even if you are running a modern NT-bases operating system.

Obviously, I am not the only person who faces such a problem. Microsoft developers themselves recommend to keep number of controls on a form at minimum. It is recommended to draw text messages, icons and images manually. Sometimes it is easy to perform custom drawing but usually it involves a lot of calculations to lay out manually drawn text and pictures properly. There is, however, a bit simpler solution - use of windowless user interface elements.

I can think of two implementation of such controls right away. The first one is from Borland's class library for Delphi named VCL. It's developers introduce a class named the TControl that is essentially a windowless control and derive from it the class TWinControl, a control with a handle and a window function. Most of layout and mouse logic goes to the TControl, everything related to Win32 API goes to the TWinControl and it's descendants. For instance, the VCL has two classes for text labels - a windowless TLabel and windowed TStaticText. In most cases you can replace the latter with the former thus reducing the number of windows consumed by your application.

The second implementation is a bit more exotic. In the Be Operating System the designers split the UI elements to windows and views. A window is a heavyweight, kernel mode object that represents an almost rectangular area on the screen. The BWindow class (a side note: BeOS is written in C++ and most system services are implemented as C++ classes) is a proxy for this object. A view in BeOS is a lightweight object that resides in the application space. The BView class, the root of BeOS' controls or widgets hierarchy, is a rectangular area inside a window that can draw itself and respond on incoming events. BWindow supports a list of BView objects, delegates the drawing of it's interior to them, maintains the input focus, passes keyboard and mouse events to the views and lays them out properly when the window changes it's dimensions.

Because in Windows Forms class library the Control class is derived right from the Component class and all logic related to input events and layout is in the Control class it is impossible to implement windowless controls VCL way. So I decided to go BeOS way. There going to be an ordinary control that would host the views.

The goals to achieve:

The lightweight views should consume as little memory as possible;

A lightweight view should be a simple object from the GC point of view, i.e. it should be neither finalizable nor disposable;

The whole system should be easily extendable, flexible and adoptable;

The source code should be simple.

The lightweight views can be used in particular to create following custom controls:

Non-standard toolbars;

Grids and grid-like controls like list views;

Scrollable icon and image strips;

Image grids like ones in the Adobe Album application.

Note that in the listings I omit the comments and also most of the implementation code. If you want to see them just download the source code.

A view in this implementation is a rectangular area (or a non-rectangular area that can be inscribed into a rectangle) in the host control that can draw itself and react on some mouse events. I declared an interface with all the properties and callback methods. The interface is called, obviously, IView.

NB: The declaration of the interface has been changed since the last article review.

This interface provides bare minimum of methods and properties, required for the views to position and draw themselves in the host window. The Parent property provides for the view itself an object that implements IControlService interface. This interface gives the view the ability to invalidate itself and provides access to the parent windowed control. I give more details on this interface below.

Although a view is enclosed into a rectangle, you may create non-rectangular views. For such a view you have to provide a specific implementation of the HitText method. It returns true if a coordinates provided are inside the view's boundaries and false if they are not.

While developing this set of classes I came up with an interesting, in my opinion, solution related to caching GDI+ objects.

System.Drawing namespace provides a whole lot of pre-build objects like various pens and solid brushes. However, if you need more complex objects as linear gradient brushes, you have to create those relatively heavyweight objects. You usually dispose of them right after use or keep for the application lifetime. The DrawHelper class provides a compromise by caching the graphics objects and releasing the resources at the application's idle time. When you need a new GDI+ object the instance of the DrawHelper creates it for you, when you ask again for an object with the same parameters, the DrawHelper first looks up the cache and returns the already created object. It is especially useful for the lightweight views because the container would keep many similar views that draw themselves in similar ways.

However, this class has a drawback. I keep graphics objects in hash tables and keys to them are integers calculated from the parameter's hash values. While it works for simple pens and brushes, there might be a problem with TextureBrush objects. A TextureBrush is based on an image. But both the Image class descendants use the default implementation of the GetHashCode method. It means that two identical Image objects (the same image loaded from file or resource twice or an image and it's cloned copy) would have two different hash codes. Thus the DrawHelper will create and keep two identical TextureBrush objects. On the other hand, calculating a hash code by scanning the image would be too expensive. Just keep this in mind.

The factory methods have parameters that match the most common constructors of the graphical objects they create.

Also take a look at CloneXXX methods. They make clones of built-in StringFormat.GenericDefault and StringFormat.GenericTypographics objects so you may use these as templates for your own tweaked string formats, see LabelView class implementation for an example on these usage. The clones, of course, will also be disposed of at the idle time.

NB: The declaration of the interface has been changed since the last article review.

The interface provides for a view the reference to it's host control and a frequently used facility that invalidates a rectangular area inside the view.

At the beginning of the development all the view objects kept back references to the ViewContainer objects. But I introduced the CompositeView class, a view that also hosts other view objects. Because these two classes are completely different I had to create this interface to unify access to the parent control for views.

This class represents a typed collection of objects that implement IView interface. I implement it as an old-style collection not only because I still use .Net Framework 1.1 but I also derive from it two private classes with custom functionality.

The class has some methods rarely found in usual collections. For instance, there are MoveXXX methods that help you to manage the view's z-order. The host control draws views one by one, from index 0 to the upper bound of the collection. It is obvious that views with bigger index are on top of the others in the z-order. You can move a view one step up or down in the z-order with MoveForward and MoveBackward methods or bring it to the front or the back of the z-order with MoveFront and MoveBack methods. Of course, you can use Remove and Insert methods to achieve the same goal but remember that these methods force repaint of the whole host control and recalculation of it's scrollable area but MoveXXX methods don't. It can be important if a host contains hundreds of views.

This is a host for the views. It's implementation is pretty simple because the Windows Forms controls provide excellent support for the contents scrolling. I derived the class from the System.Windows.Forms.Panel because it exposes the BorderStyle property that I need for prettier UI. However, by modifying control flags in the constructor I make the ViewContainer selectable so you can set TabStop property for the instances of this class and also scroll it's contents (the views) with the keyboard. I also set the styles that make the ViewContainer transparent, perform the background painting on it's own, perform custom mouse processing and double buffer it's drawing. Note that you can turn double buffering off, but the control might flicker, especially when two or more views that use heavy drawing operations overlap.

The class declares two important methods: HitTest and CalcExtent. The first one is used to find a view the mouse cursor points at by enumerating all the views:

Note the Updating property, it's used to prevent changing of the extent when you insert a bunch of views to the host control at once. You can toggle the Updating property by calling BeginUpdate/EndUpdate methods.

Both methods have rather generic implementations for this class; the derived classes, if they know the exact layout of the views, can override these methods to perform faster search and calculation. I'll show how it can be done in the StringBox sample.

Because the ViewContainer performs all mouse processing itself by relaying mouse events to it's views, it does not raise any events of MouseXXX family. It also performs custom drawing and uses different default values for some properties. It is possible to hide events and change the default values of the properties from the forms designer by overriding them and changing their attributes, but because the number of those changes is big I decided to create for the ViewContainer it's own custom designer and perform the modifications by implementing PostFilterEvents and PostFilterProperties methods. The implementation of these methods is pretty straightforward, just look at the source.

Note that I derive the ViewContainerDesigner from neither PanelDesigner nor ScrollableControlDesigner but right from the ControlDesigner. I do it so because the advanced designers provide some features like background grid in design mode that I don't need for the ViewContainer.

This is the root of the lightweight views hierarchy. The AbstractView class implements the IView interface, adds a lot of state properties the derived classes may use and with explicit interface implementation hides certain methods from outsiders (it keeps FxCop happy). Note that X, Y, Width and Height properties are abstract and must be overridden in derived classes. Also keep in mind that by default views are believed to be rectangular, see AbstractView.HitTest implementation. Derive your custom views from this class if you want the parent control to manage the view's boundaries.

The CompositeView is a container for other views. Just like ViewContainer it implements IControlService interface, has it's own private implementation of the ViewCollection class and corresponding Views property and it's own HitTest method. However, it does not support scrolling for obvious reasons.

In version 3 of the views library I have implemented views nesting. It means that you can insert a CompositeView into another CompositeView. This view nesting is good but don't overuse it, drawing and relative mouse position calculations utilize recursion.

This lightweight view mimics the System.Windows.Forms.Label control. It has two useful properties: Text and TextAlign. If you need more flexible solution either use the standard control or add required properties and events yourself.

This view mimics the System.Windows.Forms.PictureBox control. It has Image property that allows one to set and retrieve the current image, associated with the view, and SizeMode property that specifies how the image will be handled by the view (stretched, centered etc.).

Note that though an Image object is disposable, the ImageView objects do not dispose of it. Thus is the rule of thumb for the views: because they are lightweight, they do not manage any resources. Keep track of pens and brushes in the DrawHelper, Image objects in your forms or ViewContainer-derived objects and never add any resource management code to the views.

In the source code provided as an addendum to this article you find the StringBox project. This project actually gave me inspiration to write this article.

The lightweight views framework were an internal project until one day I made a typo. I was writing code for a ListBox-like control and in a test program, in the initialization method, I by mistake put constant 1200000 instead of 12000. The constant was the number of views to create and put to the control. When I run the program, it hung and then I realized my mistake. But curiosity took over and I left it running. I wanted to see how much memory it would eat. A half of an hour later the ViewContainer painted itself.

To my surprise I was able to work with it. I could scroll, I could select views. Even hot-tracking worked. It surely was slow, but it worked. At that moment I realized that the views can be used not only in my toy project.

I started to tweak and optimize the code and soon made it possible for the StringBox (my quick-and-dirty replacement for Windows' ListBox) to handle 1000000 lines of text. On my computer it takes about 20 seconds to populate the list then you can scroll it and select the items as if you were using the standard ListBox control. But the standard control cannot handle this many strings on my computer, after an hour of populating it the program crashes with OutOfMemory exception.

I had created a managed control that was faster and more reliable than the standard, unmanaged control. There was something very wrong with it┼ Anyway, the StringBox subproject is a grandchild of my buggy program that once generated 12000000 views.

I never wanted to create a replacement for the Windows' own list box, but tried to make my StringBox to look and feel close to the standard control.

Two points of interest are HitTest and CalcExtent implementations. The first one does not enumerate all the views but returns the selected view by calculating it's vertical position:

To build samples you need either Views.cmbx combine to build with SharpDelelop v 1.1 or nant.build file to build with NAnt v 0.85. I also provide two batch files to build the samples.

SharpDevelop combine contains two targets, Debug and Release, the names say for themselves. NAnt build file contains four targets, Debug, Release, Doc and Clean with Debug set to default. First two targets are identical to the combine, the Doc target builds the documentation for Pvax.UI.dll, the Clean target, obviously, deletes all files generated.

I also included an FxCop project for FxCop 1.32. It isn't happy about me not validating arguments of the public methods in my private collection classes. I don't do it because my methods call base class implementation that do validate the parameters. It also suggests not to call virtual methods in constructors for ViewCollection class because in the derived classes the overridden versions would be called - but it is right what I want. On your computer FxCop might also be unhappy about short parameter names like X and Y. I disabled these warnings by adding X and Y to the FxCop custom dictionary. You can find an XML file named CustomDictionary.xml in the folder C:\Documents and Settings\{Your profile name}\ApplicationData\Micrososft FxCop\{FxCop version number}. Just add two <Word> tags with X and Y texts to <Dictionary>/<Words>/<Recognized> section.

SharpDevelop 2.0 uses msbuid instead of it's own old project engine so I created a VisualStudio 2005-compatible (?) solution. It's, however, is not tested with "real" VisualStudio so use it on your own risk. If it really works, please inform me. And yes, the lightweight views are fully compatible with .Net Framework 2.0.

During the upgrade to SharpDevelop 2.0.0.1462 I found that Pvax.Views.Tests project references old versions of NUntit.Core and NUnit.Framework assemblies. Those of you who get errors in this particular subproject should remove references to them and add back again (from the GAC). Alternatively you may reference private copies of these assemblies. I could do it myself but it would bloat the ZIP with the source code. Anyway, you are warned - beware of NUnit versioning.

First, add design-time capabilities to the views. It can be done fairly easily by implementing System.ComponentModel.IComponent interface or by even deriving the IView interface from it. The downside is that by doing so you turn lightweight views into not-so-lightweight components. Probably by using ADAPTER (wrapper) pattern it is possible to separate IComponent-derived and IView-derived hierarchies.

Second, further speed optimizations for the generic views host, the ViewContainer.

If you want to use keyboard for navigation in your custom control, first make sure that System.Windows.Forms.ControlStyles.Selectable flag for it is set to true. Then, if you want to intercept arrow keys, do not override OnKeyDown/OnKeyPressed methods, they never receive neither arrow keys nor other navigation keys like PgUp/PgDown. Don't waste your time with interop and just override the low-level keyboard hook ProcessDialogKeys.

Cache you GDI+ resources, use built-in pens and brushes whenever it is possible.

Custom controls, at least, ViewContainer, don't work if the application is downloaded from the Internet because of security restrictions.

http://www.beunited.org/bebook/ - BeBook, the BeOS developer's bible; if you're interested in BeOS windows and views, open this page and click "The Interface Kit" link, you'll find links that lead to BWindow and BView documentations.

The DrawHelper becomes a global singleton; the IView declaration changes;

Now a CompositeView may contain another CompositeView (nesting restrictions removed), IControlService and IView declaration change, all code that uses and implements these interfaces change (almost all classes);

A view named PanelView introduced, borders support moved to it from the CompositeView, ViewTest sample changed accordingly;

A new sample Nesting provided.

08/14/2006 - Breaking changes:

Non-rectangular views supported;

Z-order of views inside the ViewContainer supported;

Views/controls invalidation bugs fixed;

The IView and IControlService declaration and implementations changed accordingly;

IView.HitTest method introduced for non-rectangular views support.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

Share

About the Author

I'm a system administrator from Moscow, Russia. Programming is one of my hobbies. I presume I'm one of the first Russians who created a Web site dedicated to .Net known that time as NGWS. However, the Web page has been abandoned a long ago.

do you have any ideas how to realize a designer support?
I've added a bar meter view that just looks and behaves like a classic bar meter control,
and now it would be nice to be able to use this View at design time just like any other
controls.

The simpliest way is to make each View a Component or a MarshalByValueComponent or just implement IComponent interface, then you get basic designer support for free. But you need to implement a designer canvas for the ViewContainer or all the View's go into the component tray.
I saw a sample on the Net that implemented the whole designer support for non-Controls objects, I think it's name was Shapes. But #D designer implementation sucks, the sample didn't even compile, so I abandoned the idea to make the designer for views.
Anyway, WPF/XAML supercedes all those things.

I have... Now.
You are right, these guys are far beyound the goals I've set before me. They have not only created a set of lightweight views or widgets, they have also created a graphics abstraction layer. I. e. they have reinvented Swing and made it multiplatform. I am impressed!

Sorry if I sound negative but what do you do about accessibility? At one of my previous jobs, I was given the task to fix our application that at the time was using a similar non-windows way of displaying controls. The main problems with that were:

1) No accessibility functionality. That means that things like screen reader and the magnifier doesn't work because all they see is one main window with no child windows. If you want to sell to the government, they require your software to be Section 508 compliant. See here: Section 508 compliance[]

2) Automation tools often use accessibility functionality to detect and interact with controls. By not having this functionality, you effectively eliminate use of these tools.

Anyway, in order to fix the application, I had to re-write the windowing system to use real child windows. It was quite a complex conversion.

You might be able to provide this functionality if you add the AccessibilityObject class to your controls, but I am not sure as they still will not show up as child windows to things like automation tools etc.

Of course, this only applies if you ever think your software might be purchased by the government, or if your software ever needs to be tested by automation tools.

You are absolutely correct, I have completely forgot this problem while developing the solution. My bad.
Though the software wasn't meant to be sold to the government, it doesn't mean that I shouldn't think about accessibility at all. I'm going to investigate this issue and come up with a solution.
Thank you for pointing out this problem!

I'm just wondering how Java Swing applications solve that problem.
As I know, you can only see main window using spy or other tools.
I know it's not a java section, but sometimes it's worth looking how competitor solves the problem.
I don't belive governement would never bye Java Swing app.

Great article! Have you ever used the Java Swing toolkit? It uses pretty much the exact same approach detailed here. If you haven't, it might be worth checking out for ideas. Also, have you considering implementing the control views using VisualStyleRenderer instead of ControlPaint? It would allow them to use themed visual styles, although unfortunately it requires .NET 2.0 and above. Swing allows varying look and feels by separating out control logic and using UI delegates for the actual drawing. Using the same approach you could have a set of UI delegates based on ControlPaint and another based on VisualStyleRenderer. Keep going though, and you'll end up writing your own toolkit

No, I've never used Swing though I know how and why they wrote it (HWNDs are bad, bad). My goal was not to re-create Swing or Avalon but to create something really simple and customizable. Actually, while using my toolkit I have found a whole lot of limitations and bad design decisions so I'm rewriting it - though the high-level API looks the same the core is completely different, like an abstract View class instead of IView interface and so on. How long would it take I don't know, I have significantly less spare time these days.
Using VisualStyleRenderer wasn't the option when I started - I used to work on Windows 2000 that didn't support visual styles. The bridge pattern for the UI widgets is possible but again, I'm not going to write a full-fledged UI framework - we've got Avalon now that makes my views way less valuable.
Anyway, thank you for your feedback and encouragement, I'll think about your suggestion, at least about the VisualStyleRenderer - we still have conditional compilation in C#

Thanks!
Actually, I haven't abandoned this project. Very slow, in my spare time, I'm rewriting it I've changed a bit the core code and the new version looks much cleaner. Unfortunately, the new version completely breaks the compatibility with current views.
I cannot guarantee that the new version will be available tomorrow or in a month, but check the CodeProject time to time.

I've tested your solution in VS2005 pro(may be somebody tested it already but still one more time ).It compiles and runs OK.
Just nedded to mark TestBench project as StartUp.project.
So good work mate.
privet

I did a little experiment creating a windows from with 200 labels; once with your lightweight labels, and once with Windows Forms labels. The refresh speed is incomparable, even when all I do is hide and then reveal the form by dragging another window in front.

I'm going to work on adding a little bit of functionality to the labels (font, color) so that they're useful to me; I'll provide you my code to add to the project if you wish.

Thanks for your encouragement! I'm slowly refactoring the code and rewriting the article. For instance, I moved the DrawHelper to another namespace and did make it a global singleton. I now use it in my other project with, I beleive, success. Although for lightweight GDI+ objects like the SolidBrush or the Pen it doesn't make noticeable difference, for TextureBrush or especially for LinearGradientBrush objects it definitely does.
I'll be updating the article in a week, stay tuned