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 UITypeEditor
s 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;
ContainerListViewItem clvi = new ContainerListViewItem();
clvi.Text = "Test";
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);
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;
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);
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;
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: UITypeEditor
s, TypeConverter
s, and design-time attributes. UITypeEditor
s provide a means of displaying a GUI editing dialog for any kind of object. TypeConverter
s 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 UITypeEditor
s 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 Form
s 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
{
public MyCollectionItem() { }
#region Properties
#endregion
#region Methods
#endregion
}
public class MyCollection: CollectionBase
{
public MyCollectionItem this[int index]
{
get { return List[index] as MyCollectionItem; }
set { List[index] = value; }
}
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; }
}
}
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
{
public MyCollectionItem() { }
#region Properties
#endregion
#region Methods
#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 CollectionEditor
s. This allows you to edit items and nodes in the design editor, if needed. CollectionEditor
s 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
CollectionEditor
s 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;
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)
{
Keys keyData = ((Keys) (int) msg.WParam) | ModifierKeys;
Keys keyCode = ((Keys) (int) msg.WParam);
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");
}
else if (keyData == Keys.Right)
{
Debug.WriteLine("Right");
}
}
}
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.