Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

Extending Windows Forms - lightweight views

Rate me:
Please Sign up or sign in to vote.
4.63/5 (27 votes)
20 Aug 200619 min read 118.8K   1.1K   103   34
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.

Lightweight views demo

Contents.

Introduction.

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.

The problem.

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.

Existing solutions.

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.

Lightweight views.

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:

  1. The lightweight views should consume as little memory as possible;
  2. A lightweight view should be a simple object from the GC point of view, i.e. it should be neither finalizable nor disposable;
  3. The whole system should be easily extendable, flexible and adoptable;
  4. 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.

IView interface.

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.

C#
public interface IView
{
    IControlService Parent
    {
        get;
        set;
    }

    int X
    {
        get;
        set;
    }

    int Y
    {
        get;
        set;
    }

    int Width
    {
        get;
        set;
    }

    int Height
    {
        get;
        set;
    }

    bool Enabled
    {
        get;
        set;
    }

    bool Visible
    {
        get;
        set;
    }

    void Invalidate(int x, int y, int width, int height);

    void Draw(Graphics graphics);

    void OnMouseDown(int posX, int posY, MouseButtons buttons);

    void OnMouseUp(int posX, int posY, MouseButtons buttons);

    void OnMouseEnter(int posX, int posY);

    void OnMouseLeave(int posX, int posY);

    void OnMouseHover(int posX, int posY);

    bool HitTest(int posX, int posY);

}

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.

DrawHelper class.

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.

C#
public sealed class DrawHelper
{
    public static DrawHelper Instance
    {
        get
        {
            . . .
        }
    }

    public Pen CreateColorPen(Color color, float width)
    {
        . . .
    }

    public Pen CreateColorPen(Color color)
    {
        . . .
    }

    public Pen CreateBrushPen(Brush brush, float width)
    {
        . . .
    }

    public Pen CreateBrushPen(Brush brush)
    {
        . . .
    }

    public SolidBrush CreateSolidBrush(Color color)
    {
        . . .
    }

    public TextureBrush CreateTextureBrush(Image image, 
           WrapMode wrapMode, RectangleF dstRect)
    {
        . . .
    }

    public TextureBrush CreateTextureBrush(Image image, 
           WrapMode wrapMode, Rectangle dstRect)
    {
        . . .
    }

    public Font CreateFont(string familyName, float emSize, 
           FontStyle style, GraphicsUnit unit, byte characterSet, 
           bool isVerticalFont)
    {
        . . .
    }

    public LinearGradientBrush CreateLinearGradientBrush(Rectangle rect, 
           Color color1, Color color2, LinearGradientMode linearGradientMode)
    {
        . . .
    }

    public HatchBrush CreateHatchBrush(HatchStyle hatchStyle, 
           Color foreColor, Color backColor)
    {
        . . .
    }

    public PathGradientBrush CreatePathGradientBrush(GraphicsPath path)
    {
        . . .
    }

    public PathGradientBrush CreatePathGradientBrush(Point[] points, 
                                                     WrapMode wrapMode)
    {
        . . .
    }

    public PathGradientBrush CreatePathGradientBrush(Point[] points)
    {
        . . .
    }

    public PathGradientBrush CreatePathGradientBrush(PointF[] points, 
                                                     WrapMode wrapMode)
    {
        . . .
    }

    public PathGradientBrush CreatePathGradientBrush(PointF[] points)
    {
        . . .
    }

    public StringFormat CloneDefaultStringFormat()
    {
        . . .
    }

    public StringFormat CloneTypographicStringFormat()
    {
        . . .
    }
}

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.

IControlService interface.

C#
public interface IControlService
{
    ViewContainer Control
    {
        get;
    }

    void Invalidate(int x, int y, int width, int height);
}

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.

ViewCollection class.

C#
public class ViewCollection: IList, ICollection, IEnumerable
{
    . . .
}

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.

ViewContainer control.

C#
[Designer(typeof(ViewContainerDesigner))]
public class ViewContainer: Panel, IControlService
{
    . . .
}

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:

C#
protected virtual IView HitTest(int posX, int posY)
{
    for(int i = views.Count - 1; i >= 0; i--)
    {
        IView view = views[i];
        if(view.Visible)
        {
            if((view.X <= posX) && (view.X + view.Width > posX) && 
               (view.Y <= posY) && (view.Y + view.Height > posY))
                return view;
        }
    }
    return null;
}

The second one calculates the size of the canvas that encloses all the views in the ViewContainer:

C#
protected virtual void CalcExtent()
{
    if(Updating)
        return;
    Size size = new Size();
    foreach(IView view in views)
    {
        if(view.Visible)
        {
            size.Width = Math.Max(size.Width, view.X + view.Width);
            size.Height = Math.Max(size.Height, view.Y + view.Height);
        }
    }
    AutoScrollMinSize = size;
}

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.

ViewContainerDesigner class.

C#
internal class ViewContainerDesigner: ControlDesigner
{
    protected override void PostFilterProperties(IDictionary properties)
    {
        . . .
    }

    protected override void PostFilterEvents(IDictionary events)
    {
        . . .
    }
}

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.

AbstractView class.

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.

View class.

This class defines a stand-alone view that has boundaries. I. e. essentially it is the AbstractView with X, Y, Width and Height properties implemented.

CompositeView class.

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.

LabelView.

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.

ButtonView.

This view mimics the System.Windows.Forms.Button control. It has Text and TextAlign properties, just like the LabelView, and Click event.

ImageView.

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.

PanelView.

This view mimics a bit the Sysytem.Windows.Forms.Panel control and is actually a CompositeView with borders.

Restrictions.

I'm sure that both design and implementation of the lightweight views framework is far from perfect. Consider it kind of a blueprint for real applications.

Sample code.

I provide a sample project with the code for the views. It contains several subprojects.

StringBox.

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:

C#
protected override IView HitTest(int posX, int posY)
{
    if(Views.Count <= 0)
        return null;
    int index = posY / GetStringHeight();
    if(index < Views.Count)
        return Views[index];
    return null;
}

The second one assumes that the StringBox scrolls only in vertical direction and never has a horizontal scroll bar:

C#
protected override void CalcExtent()
{
    if(Updating)
        return;
    int width = ClientSize.Width;
    int height = Views.Count * GetStringHeight();
    if(height > ClientSize.Height)
        width -= SystemInformation.VerticalScrollBarWidth;
    AutoScrollMinSize = new Size(width, height);
}

Both implementations significantly speed up the StringBox operations.

TestBench.

This project demonstrates use of the views in various situation. The main interface is a window with tabs, each tab contains a test of a particular views functionality:

  • Composites - presents two groups of button-like views, with real functionality (state and events); the second group is enclosed into a composite view; it's a test for generic views functionality;
  • Rectangles - shows a number of rectangles with hot tracking and scrolling; this is a test for scrolling and mouse handling;
  • Nesting - shows a number of nested composite views with hot tracking; this is a test for view nesting;
  • Non-rectangular - shows a number of elliptic views with hot tracking, you can select the views with mouse; this is a test for non-rectangular views support and z-ordering.

Building samples.

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.

License.

The core set of classes that reside in Pvax.UI and enclosed namespaces are covered by a BSD-like license. The rest of the code is public domain.

Further improvements.

There are ways to improve the views framework.

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.

Third, more views, they are nice.

Lessons learnt.

Automatic contents scrolling in Windows Forms is excellent.

Some native Windows' controls are weird.

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.

Links.

History.

  • 05/21/2006 - First version of the document;
  • 06/12/2006 - A #D 2.0/VS2005-compatibe solution added and .Net 2.0 compatibility ensured;
  • 06/14/2006 - An issue with NUnit assemblies (partially) resolved.
  • 07/23/2006 - Breaking changes:
    • 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.

A list of licenses authors might use can be found here


Written By
Web Developer
Russian Federation Russian Federation
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.

Comments and Discussions

 
QuestionWhat about Accessibility? Pin
BitFlipper25-Feb-07 8:00
BitFlipper25-Feb-07 8:00 
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.

BitFlipper
AnswerRe: What about Accessibility? Pin
Alexey A. Popov26-Feb-07 7:02
Alexey A. Popov26-Feb-07 7:02 
QuestionRe: What about Accessibility? Pin
igogo19-Jun-07 1:29
igogo19-Jun-07 1:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.