Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ContainerListView and TreeListView: Writing VS.NET design-surface compatible controls

0.00/5 (No votes)
13 Jan 2003 4  
Learn how to properly integrate your custom .NET control into the Visual Studio .NET design environment with TypeConverters and UITypeEditors. The article includes two useful controls, a container ListView, and a complete, feature-loaded TreeListView.

TreeListView - extendedlistviews.gif

ContainerListView - extendedlistviews2.gif

Introduction

In today�s world, I�ve found that it becomes increasingly more complicated to render data in a meaningful and compact way. I�ve also found that the variety of controls, particularly ones for sale, seem to be created in a hurry, either because of time constraints or simply because of the desire to make money as cheaply as possible.

It was because of a lack of quality in existing components that I decided to take the time and create two controls that I desperately needed. The first is a listview that provides containers for controls or an image for every column, rather than only allowing text. The second is a quality hybrid tree-list that also provides the same features of the above mentioned list.

This article will overview these two controls, ContainerListView and TreeListView. Both controls were written purely using .NET classes in C#. I tried to avoid external API calls as much as possible. These two controls both make use of library for .NET, created by Pierre Arnaud, OPaC Bright Ideas. Pierre�s library allows the use of WindowsXP visual styles, by providing a wrapper for uxtheme.dll functions. This excellent library is available for download here on CodeProject, and an extended version (adds DrawThemeEdge function, required for these controls) is available in the zip. Many thanks to Pierre for creating this library, its served me well many times.

This is my first attempt at writing custom controls. Some of the logic is not optimized and is a little bloated. Several functions suck up a lot of CPU cycles, mostly the OnMouseDown code which tries to match a mouse click position to a visible item. I�m open to suggestions and bug fixes, and would greatly appreciate them. I will continue to work on both controls, adding features and optimizations however and wherever possible.

Topic focus

This article does not focus much on the creation of these two controls, although it does touch on some points. The inspiration for this article, in truth, was the amount of time I spent, trying to learn how to successfully integrate a control with the Visual Studio.NET design environment. Which was too much time. Both controls are fully compatible with the design surface in VS.NET, utilizing UITypeEditors and properly serializing source code.

The steps to accomplish such a task are very simple, but poorly documented and not well known. Most control developers skip the process of integrating their control with the design surface. My goal with this article is to familiarize you with .NET�s design features, and the few simple steps to add proper code serialization to your control.

The ContainerListView control

The standard .NET ListView control supports multiple columns, which can each hold a unique text value. In many instances, that simplicity is enough, or all that�s needed. All too often, though, the need arises to embed something other than text in a ListView's column.

The ContainerListView provides the ability to embed text, an image, or a control into each subitem of a ListView item. The control also adds a few fancy features, like column and row tracking, 3 unique context menus (column header, row, general), WindowsXP visual styles integration, and the ability to edit the items in design mode (including subitems and their image or control). The control also wires MouseDown events fired from a subitems control to the ContainerListView itself, ensuring proper row selection even when clicking on a sub-control.

Creation and population of a ContainerListView control is relatively simple. As mentioned, its entirely possible to drag and drop controls onto the design-surface in VS.NET, and have complete control over almost all design settings. If you prefer to manually code your GUI, here is an example:

ContainerListView clv = new ContainerListView();
clv.Text = "Sample ContainerListView";
clv.Name = "clv";
clv.Dock = DockStyle.Fill;
clv.VisualStyles = true;   // enable integration with

                           // WindowsXP visual styles


ContainerListViewItem clvi = new ContainerListViewItem();
clvi.Text = "Test";

// Add column headers

ToggleColumnHeader tch = new ToggleColumnHeader();
tch.Text = "Column 1";
clvi.Columns.Add(tch);
tch = new ToggleColumnHeader();
tch.Text = "Column 2";
clvi.Columns.Add(tch);
tch = new ToggleColumnHeader();
tch.Text = "Column 3";
clvi.Columns.Add(tch);

// Add a row item with a child progressbar

ContainerSubListViewItem cslvi = new ContainerSubListViewItem("Test");
clvi.SubItems.Add(cslvi);
ProgressBar pb = new ProgressBar();
pb.Value = 25;
cslvi = new ContainerSubListViewItem(pb);
clvi.SubItems.Add(cslvi);

clv.Items.Add(clvi);

This ContainerListView control inherits from the System.Windows.Forms.Control class, rather than extending the previously existing ListView control. Part of the goal was to see if I could create a control from scratch, and part was to keep it as low-profile as possible, without hooking into the Windows Common Controls (as the standard ListView does) or using Windows API calls. I wanted a purely .NET implementation. If you wish to examine the specifics of the control, take a look at the source code in the zip.

The TreeListView control

The TreeListView is a hybrid control. It blends a TreeView with the ContainerListView control above, allowing the first column to behave as a tree. TreeListView controls have become popular recently, and there are many available here on CodeProject and other sites. None had quite the features I was looking for, so I extended ContainerListView and added the tree. This TreeListView supports all the features of the ContainerListView except row tracking, which is currently in progress.

Here is an example:

TreeListView tlv = new TreeListView();
tlv.SmallImageList = smallImageList;
tlv.VisualStyles = true;

// Add column headers

ToggleColumnHeader tch = new ToggleColumnHeader();
tch.Text = "Tree Column";
tlv.Columns.Add(tch);
tch = new ToggleColumnHeader();
tch.Text = "Column 2";
tlv.Columns.Add(tch);
tch = new ToggleColumnHeader();
tch.Text = "Column 3";
tlv.Columns.Add(tch);

// Add tree nodes

TreeListNode tln = new TreeListNode();
tln.Text = "Test";
tln.ImageIndex = 1;
tln.SubItems.Add("Sub Item 1");
tln.SubItems.Add("Sub Item 2");

TreeListNode tln2 = new TreeListNode();
tln2.Text = "Child 1";
tln2.ImageIndex = -1; // Setting to -1 will suppress icon

tln2.SubItems.Add("Sub Item 1.1");
tln2.SubItems.Add("Sub Item 2.1");
tln2.Nodes.Add(tln2);

tlv.Nodes.Add(tln);

tln = new TreeListNode();
tln.Text = "Second Test";
tln.ImageIndex = 1;
tln.SubItems.Add("Test Item");
tln.SubItems.Add("Test Item");

tlv.Nodes.Add(tln);

Custom control rendering

The initial versions of these two controls used several private member functions to draw elements such as buttons, focus boxes, etc. A very large amount of code was required to render each state of the column header buttons, borders, etc.

The .NET framework provides a very handy class, the ControlPaint class in the System.Windows.Forms namespace. This handy little class is loaded with static functions that provide rendering facilities for all sorts of objects, including buttons, borders, focus boxes, and plenty more. The use of the ControlPaint class can greatly reduce the amount of drawing code in your control, and help keep the proper Windows look and feel.

An important feature of these two controls is their Windows XP Visual Styles integration. This was accomplished using a library written by Pierre Arnaud. It wraps the visual styles rendering functions found in uxtheme.dll, and provides a very simple way to utilize them in .NET applications. This excellent library is available here on CodeProject: http://www.codeproject.com/cs/miscctrl/themedtabpage.asp. I highly recommend this library to anyone who wishes to easily integrate their controls with Windows XP. Pierre's article also covers how to get tab pages to properly render under XP. Many thanks, Pierre, for a great library.

Integrating with the design surface

Now on to the meat of the article, integrating a control with the VS.NET design surface. Both these controls took approximately 4 days total, to write. Of those 4 days, over 3 were spent researching how to use .NET's design facilities to make these controls as professional as possible, and as useful as possible.

Integrating a control, particularly a control that uses collections, requires the use of several design services available in .NET: UITypeEditors, TypeConverters, and design-time attributes. UITypeEditors provide a means of displaying a GUI editing dialog for any kind of object. TypeConverters provide a means of converting your custom classes into the proper source code. Design-time attributes provide the means to enable these design-time editing facilities in your control.

Selecting a UITypeEditor

Depending on your project, implementing a UITypeEditor can be very simple, or very complex. Using one of the supplied editors in the framework can make life very simple, and in very many cases, one of the supplied editors gets the job done very well. Other times, you may be required to write your own UITypeEditor, and that is beyond the scope of this article.

Some of the supplied UITypeEditors in the .NET framework follow. The System.Drawing.Design.FontEditor displays the standard font selection dialog when applied to a property. The default Control.Font property uses this editor. The System.Drawing.Design.ImageEditor displays an open file dialog filtered to image types, and is used in many places throughout the .NET framework. The System.Windows.Forms.Design.FileNameEditor displays the standard file open dialog for properties that require a filename. The System.Windows.Forms.Design.AnchorEditor displays the unique dropdown used for setting the Control.Anchor setting of every Windows Forms control.

The most unique and probably the most useful editor is the System.ComponentModel.Design.CollectionEditor. This displays a two-paned dialog box for editing collections of nearly any kind. The CollectionEditor expects only an indexer and add function in your collection to work properly with it. Implementing a CollectionEditor properly, ensuring proper code serialization, while not complex, is not documented well, and tedious to figure out by trial-and-error.

Integrating CollectionEditor into a control

To integrate a CollectionEditor for your control, you will need to do several, if not all, of the following tasks. Depending on how you implement the item class that will be contained in your collection class, there may be fewer things to implement than will be described here.

The first step in integrating CollectionEditor is developing your collection class and item class to be contained in that collection. A simple example follows:

public class MyCollectionItem
{

    // required for type converter

    public MyCollectionItem() { }

    #region Properties
    // properties here

    #endregion

    #region Methods
    // public methods here

    #endregion
}

public class MyCollection: CollectionBase
{
    // a basic indexer of the type of your collection 

    // item is required

    public MyCollectionItem this[int index]
    {
        get { return List[index]  as MyCollectionItem; }
        set { List[index] = value; }
    }

    // an Add method with a parameter of the type of

    // your collection item is required

    public int Add(MyCollectionItem item)
    {
        return List.Add(item);
    }
}

For the CollectionEditor to work, you must supply an indexer that takes an integer parameter, and returns the type of your collection item (in this case, MyCollectionItem), and an Add method that takes one parameter of the same type.

The next step requires that you add a property with a couple of attributes to your control class.

public class MyControl: Control
{
      protected MyCollection theCollection;

      [
        Category(�Data�),
        Description(�The collection of items for this control.�),
        DesignerSerializationVisibility
        (DesignerSerializationVisibility.Content), 
        Editor(typeof(CollectionEditor), typeof(UITypeEditor))
      ] 
      public MyCollection TheCollection
      {
          get { return theCollection; }  // only getter, no setter

       }
}

The first two attributes are optional, specifying only where in the properties editor the property should go, and what it does. The next two are of more importance. The DesignerSerializationVisibility attribute instructs the design editor to serialize the contents of the collection to source code. This will place all the code required to add the items to a collection variable of your collection item type (in this case, MyCollectionItem).

The last attribute, Editor(), specifies what kind of editor the design editor should display. The attribute takes two arguments, System.Type, which specify the type of editor, and its parent type. In the example, we specified CollectionEditor, and UITypeEditor, from which all type editors should extend.

Integrating a CollectionEditor can become a little more complex at this point, although by no means hard. With the above examples, you will notice that no code is added to your source. This is because our collection item class, MyCollectionItem, inherits from nothing. The CollectionEditor only knows internally, how to serialize classes that extend Component. Often, simply extending Component will make CollectionEditor properly serialize your code.

Sometimes, though, you may be required to implement a TypeConverter for your class. While it may seem intimidating to some, implementing a TypeConverter is, for the most part, a no brainer. This simple class will implement a TypeConverter for our MyCollectionItem class:

public class MyCollectionItemConverter: TypeConverter
{
    public override bool CanConvertTo
        (ITypeDescriptorContext context, Type destinationType)
    {
        if (destinationType == typeof(InstanceDescriptor))
        {
            return true;
        }

        return base.CanConvertTo(context, destinationType);
     }

     public override object ConvertTo(ITypeDescriptorContext context, 
             CultureInfo culture, object value, Type destinationType)
     {
         if (destinationType == typeof(InstanceDescriptor) 
                     && value is MyCollectionItem)
         {
              MyCollectionItem item = (MyCollectionItem)value;
              
               ConstructorInfo ci = typeof(MyCollectionItem).
                                   GetConstructor(new Type[] {});
               if (ci != null)
               {
                     return new InstanceDescriptor(ci, null, false);
               }
          }
          return base.ConvertTo(context, culture, value, destinationType);
     }
}

All type converters must extend the TypeConverter class, and all must override CanConvertTo and ConvertTo methods. You should also call the base methods to make sure the converter can convert to types other than your collection item.

The second function does most of the work. It returns an InstanceDescriptor, which is a class in .NET that provides all the information required to create an instance of an object. In our type converter, we supply information about the constructor of our item class, and specify that the constructor does not describe the whole object. Specifying that this InstanceDescriptor only describes the constructor, will ensure that code to set the properties for your item class will be serialized to source.

Why do we need supply a TypeConverter? The CollectionEditor will attempt to serialize as much information about your class as it can in the form of properties. If your collection is part of a control, the CollectionEditor will need to know what the contstructor for your collection item is. Once it has this information, the CollectionEditor can add the MyControl.TheCollection.Add() lines to your source, adding each item to the collection contained in the control. Without knowledge of the constructor, the CollectionEditor can only create an instance of the collection item and add code to set its properties.

The final step in adding a CollectionEditor is adding two attributes to your collection item class. The first of these attributes will prevent instances of your class from cluttering the design surface. The second will associate your new TypeConverter with your item class.

[DesignTimeVisible(false), TypeConverter(�MyCollectionItemConverter�)]
public class MyCollectionItem
{

    // required for type converter

    public MyCollectionItem() { }

    #region Properties
    // properties here

    #endregion

    #region Methods
    // public methods here

    #endregion
}

Once you have your TypeConverter applied to your collection item class, you should be ready to go. A simple test with the design editor will let you know if the code is being serialized properly. In most cases, this should be enough. In extreme cases, you will need to implement ISerializable, and write your own serialization code for your class. That is beyond the scope of this article.

Final words

While there will be occasions that the information provided above will not help you successfully integrate a CollectionEditor into your control, my hope is that it will help most. Visual Studio .NET is a very rich development environment, and provides very powerful ways to implement design-surface compatible controls of your own. Many hard-core coders, including myself, prefer to code their UI manually. On the other hand, having a design editor available can save you in a pinch, and its important to have controls that work properly with it. I hope this article is useful to you, and I hope it will encourage the development of more design-aware controls.

The two controls introduced at the beginning of this article both implement CollectionEditors. This allows you to edit items and nodes in the design editor, if needed. CollectionEditors are even used nested, so when editing an item, you can open another editor to edit subitems. When adding controls to a ListView subitem, you must first add the control to the design surface. You will then be able to select it from a list in the subitems control property. Once the control is added to the list, you can still edit its properties simply by clicking on it. An unexpected side effect, but it was very welcome. I recommend caution when adding controls to ListView subitems. Not all controls will render properly, and some may have inadvertent issues when added. Controls tested so far have been ProgressBar, TextBox, PictureBox, and ComboBox.

Finally, I would like to ask two things of you if you have read this article and used the controls. First, please rate this article using the bar just above the comments. I'd like to get this moved from the "Unedited" section to a more appropriate section now that its more stable, and apparently that doesn't happen unless people rate it.

Second, if you use these controls in a commercial application, some monetary support would be extremely helpful. I don't have any plans to sell these controls right now, but the tech industry is a hard one to land a job in, and money doesn't come easy for me. I don't require that you pay for the controls, but if you can, you will really help me out. You can reach me through E-mail at jrista@hotmail.com if you would like to help.

Thanks for reading, and I hope the controls are useful to you. :)

Control features

ContainerListView

  • Windows XP Visual Styles integration
  • CollectionEditors for column headers and row items
  • Selected column highlighting
  • Row and column tracking (background highlighted)
  • Multiple selection
  • Row subitems can contain controls
  • Row subitems can contain an image
  • ImageList support (small and state images)
  • Column headers can have an icon
  • Every item can have custom foreground and background colors
  • Mouse wheel scrolling
  • Fast insertion, hundreds of thousands of items in a few seconds

TreeListView

  • Inherits all the features of ContainerListView
  • First column behaves as a TreeView
  • Toggleable root and child lines
  • Can select whether double-click activates or expand/collapses item
  • Fast insertion, tens of thousands of items in a few seconds

Change log

  • December 1, 2002
    • Known bug with clipping of child controls not yet fixed
  • December 2, 2002
    • Fixed scrolling bug in TreeListView. Scrollbar properly adjusts when a node is collapsed or expanded.
    • Found bug with rendering of child controls. Child controls still render when a node is collapsed.
    • Fixed scrolling bug which leaves content of the list partially scrolled without a scrollbar.
  • December 6, 2002
    • Added keyboard navigation. Bug (or feature) of .NET takes focus away from my controls when arrow keys are pressed, when there are any other controls in the same container.
    • Fixed SelectedNodes collection for TreeListView.
    • Fixed scroll bar placement bugs.
    • Child control clipping still not fixed.
  • December 9, 2002
    • Fixed highlighted row text color
    • Fixed KeyDown event bug
    • Added several 1 ms Thread.Sleep() to prevent excessive CPU usage
  • January 8, 2003
    • Added EnsureVisible to ContainerListView
    • Fixed arrow navigation bug that left a focus rectangle behind
    • Fixed arrow navigation bug in TreeListView that prevented proper navigation when more than two levels deep into the tree
    • Fixed scrollbar updating bugs in TreeListView
    • Updated rendering code to increase performance. Checks are made before rendering, to determine what items actually fall within the viewport, and only those items are rendered, all others are skipped
    • Added checks to prevent the rendering of columns that are not within the viewport. Provided a fair performance boost.
    • Fixed text rendering bugs. Text now properly cuts off at the end of its column, and the elipses are added to truncated strings. Works in both items and column headers.
    • Fixed colors for both controls, and for custom item colors. The colors selected in the property editor are now used when rendering the controls.

If you find any other bugs, feel free to contact me. These will eventually be used in a commercial application, and I hope to have quality controls.

Arrow key navigation bug

As per request, I added arrow key navigation to these controls. When the control is alone in a container (i.e. a Form, tab page, Panel, etc.), the arrow keys work as they are supposed to. When the control shares a container with one or more other controls, .NET seems to preempt keyboard control from my controls, removes the focus from them, and transfers it to the next control in the container. A simple test will show that the arrow keys move focus from one control to the next until it reaches one that can "keep" the focus (some of those are TextBox, ComboBox, ListBox). Once a control that can keep the focus has it, using the arrow keys will navigate that control.

I have been unable to figure out why .NET takes the focus away from my control when the arrow keys are pressed. My observations have shown that the OnKeyDown event is never fired in my controls when the Up, Down, Left, or Right keys are pressed. All other key presses are passed properly. For some reason, .NET has a low-level filter that checks for arrow keys, and tries to navigate controls FIRST, before allowing a custom control to handle the events. If anyone knows how to notify the .NET framework that a control wishes to use those key events, and how to prevent .NET from removing the focus from a custom control when an arrow key is pressed, I would be very greatful to know how. Thanks for any help.

Bug Fixed!!

Many thanks to Lion Shi, who responded to a post I made on msnews.microsoft.com newsgroups. The KeyDown events for arrow keys are handled by the system to make it possible to move between controls on a form. In particular, radio buttons and checkboxes. If a control needs access to these events, it needs to override the PreProcessMessage(ref Message) method and capture the appropriate messages. To solve the problem with these two controls, I added the following:

protected const int WM_KEYDOWN = 0x0100;
// windows key codes, not used any more

protected const int VK_LEFT = 0x0025; 
protected const int VK_UP = 0x0026;
protected const int VK_RIGHT = 0x0027;
protected const int VK_DOWN = 0x0028;

public override bool PreProcessMessage(ref Message msg)
{
    if (msg.Msg == WM_KEYDOWN)
    {
        if (focusedItem != null && items.Count > 0)
        {
            // Convert key code to a .NET Keys structure

            Keys keyData = ((Keys) (int) msg.WParam) | ModifierKeys;
            Keys keyCode = ((Keys) (int) msg.WParam);

            // handle message

            if (keyData == Keys.Down)
            {
                Debug.WriteLine("Down");

                if (focusedIndex < items.Count-1)
                {
                    focusedItem.Focused = false;
                    focusedItem.Selected = false;
                    focusedIndex++;

                    items[focusedIndex].Focused = true;
                    items[focusedIndex].Selected = true;
                    focusedItem = items[focusedIndex];

                    Invalidate(this.ClientRectangle);
                }

                return true;
            }
            else if (keyData == Keys.Up)
            {
                Debug.WriteLine("Up");

                if (focusedIndex > 0)
                {
                    focusedItem.Focused = false;
                    focusedItem.Selected = false;
                    focusedIndex--;

                    items[focusedIndex].Focused = true;
                    items[focusedIndex].Selected = true;
                    focusedItem = items[focusedIndex];

                    Invalidate(this.ClientRectangle);
                }

                return true;
            }
            else if (keyData == Keys.Left)
            {
                Debug.WriteLine("Left");
                //return false;

            }
            else if (keyData == Keys.Right)
            {
                Debug.WriteLine("Right");
                //return false;

            }
        }
    }

    return base.PreProcessMessage(ref msg);
}

PreProcessMessage returns a bool value, true if the message was handled, false if not. If the function returns true for a KeyDown message, the problem described above (focus being removed from the control and given to the next control in a container) is eliminated. Its possible to handle any message in the PreProcessMessage function, so for any custom control that seems to have problems capturing an event, try overriding PreProcessMessage.

An alternate method is to catch the WM_GETDLGCODE message in the following manner. This allows you to utilize arrow keys in the normal OnKeyDown/OnKeyUp methods of a control.

protected override void WndProc(ref System.Windows.Forms.Message m)
{
      base.WndProc( ref m );
      if( m.Msg == (int)Msg.WM_GETDLGCODE )
      {
        m.Result = new IntPtr( (int)DialogCodes.DLGC_WANTCHARS |
           (int)DialogCodes.DLGC_WANTARROWS |
           (int)DialogCodes.DLGC_WANTTAB |
        m.Result.ToInt32() );
      }
}

About Jon Rista

Jon Rista is a student of Computer Science, currently attending ACCIS online to complete his education. Since the age of 8 he has been fascinated with computers and computer programming. Twenty three today, and he has the ambitious goal of starting his own software company that aims to develop quality tools to help people build internet communities and connect people. Two projects include a featuer-rich and extensible bulletin board system, and the creation of a feature-rich, quality, modular, ad-free peer-to-peer networking system.

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