Introduction
This control is another extension to the now standard and widely used ListView
control. I have included some of the more common features: shaded columns, column sorting (with data type), but the real addition is the FILTERBAR features of the header. This implementation eliminates all of the work of dealing with the filter messages and item filtering by incorporating it into the control. This could have been implemented as delegate
s, but we create controls to do the work for us, don't we? I would like to thank Carlos H. Perez since a lot of the implementation came from examples he set with his ListViewEx
control.
Background
The HeaderControl
FILTERBAR feature has been around since IE5.0 was released but it is not at all widely implemented even in controls like Windows Explorer! It can be a very useful addition to detailed ListView
controls but the code involved in using one has always limited the availability of it. You have to trap WM_NOTIFY
messages from the header control, change the header control style, and manually update the ListView
items. This implementation attempts to remove those hard parts and make ListView
filtering easier. I have applications currently bound that use this feature and the customer feedback has been great. Many have asked why Microsoft doesn't do this too.
This implementation has been tested only on XP and 2000. It all depends upon the comctl32.dll that is available on the platform. The Up/Down arrows for sort direction in the column header are only supported in version 6.0 and above, and you HAVE to supply a manifest file, or dynamically attach the .DLL and theme, in order for that to be implemented. Others may want to add a CUSTOMDRAW feature to the header control or ImageList for non-XP platforms to see the Up/Down arrows.
ListViewFilter Control Features
As with any .NET custom control you must make the .DLL available for use in the designer through the toolbox customization feature. Since this is as pure an implementation of a ListView
as possible, all existing and new features work in DesignMode
(except item filtering, but that's execution only). The Browsable
properties are:
- Filtered -
bool
, turn on/off the filterbar
- IgnoreCase -
bool
, ignore character case when filtering strings
- Shaded -
bool
, color the background of the sorted column differently
- ShadeColor -
Color
, the color to use for the shaded column
- SortColumn -
int
, the column to sort
- SortOrder -
bool
, true for ascending
Non-Browsable properties:
- Header -
ListViewFilterHeader
, readonly access to access column properties (see description of ListViewFilterHeader for column specific properties)
- SortType -
LVFDataType enum
, current sort column data type for comparison (String, Number, Date)
As with all implementations of a
FILTERBAR, a lot of functionality is hidden from the casual user. It takes a little explanation and practice before they become comfortable with it. I hid a
ContextMenu
attached to the filter button that pops up to allow changes to the alignment, data style, and other features. I guess hiding things in the user interface is too easy to ignore doing it!
The most hidden feature is the content of the filter text field. You are not limited to just entering ABC... or numbers or dates, you may preceed the actual string with a comparison type. By default comparison is equal to (=) if nothing is supplied. You may also use less then (<), less than or equal to (<=), greater than (>), greater than or equal to (>=) or not (!) comparison types. I will probably add ContextMenu
items to add/change the filter text comparison type at some time when I am more comfortable working with the MenuItem event
(there should be a better way to check which MenuItem
is the sender
instead of if then else logic).
Most of this control isn't difficult code, just implementation of features that have not really been exploited. Once you trap the correct messages (override WndProc
) and crack the message headers this is simply a matter of having the right algorithm. Note, I'm not saying all of the implementation is correct, only that it works. I truly believe there are many other and better ways to do this, hopefully others can use this as an example for what not to do!
Since the ListViewItemCollection
MUST contain all the visible Items we have to deal with taking Items out and putting them back into that collection to filter the list. Previous implementations in other languages allowed me to use OWNERDATA style ListView
controls and private containers for the items. That may still be possible in .NET but would require that all filtering and item additions/removals be done through delegate
s. The simplest way is to let the ListViewItemCollection
think it contains all the Items in the ListView
.
The primary function for for filterig items is FilterUpdate()
which does all the work when a filter content has changed. FilterBuild()
helps by creating LVFFilter
entries that describe all active column filters, FilterCheck()
is the helper to perform an item/filter comparison for FilterUpdate()
. FilterUpdate()
has three functions:
- Loop through all collection Items removing filter failures to a holding array
- Loop through all previous filtered items returning now viable items to the collection
- Add the holding array items from step 1 to the updated filtered items array
If no filters are applied all items in the current filtered array are returned to the
ListViewItemCollection
and the filtered Items array cleared. Before re-enabling update, a
Sort
is done to finish the process.
The trickiest piece of code was reading and writing the FILTERBAR content. This is done via messages to the header control (WM_GETITEMA/WM_SETITEMA
) and the HDITEM structure. What is tricky is that for access to the filters you have to reference a HDTEXTFILTER structure from the HDITEM structure. Doing that involved marshaling the memory for the HDTEXTFILTER prior to passing the reference to the HDITEM structure in the message.
HDTEXTFILTER hdTextfilter = new HDTEXTFILTER();
HDITEM hdItem = new HDITEM();
hdTextfilter.pszText = new string( new char[ 64 ]);
hdTextfilter.cchTextMax = hdTextfilter.pszText.Length;
hdItem.mask = W32_HDI.HDI_FILTER;
hdItem.type = (uint)W32_HDFT.HDFT_ISSTRING;
hdITEM.pvFilter = Marshal.AllocCoTaskMem(
Marshal.SizeOf( hdTextfilter ) );
Marshal.StructureToPtr( hdTextfilter, hdItem.pvFilter, false );
SendMessage( col_hdrctl.Handle, W32_HDM.HDM_GETITEMA, <code>column</code>,
ref hdItem );
// un-marshall the memory back into the HDTEXTFILTER structure
hdTextfilter = (HDTEXTFILTER)Marshal.PtrToStructure(
col_hditem.pvFilter, typeof( HDTEXTFILTER ));
// remember to free the marshalled IntPtr memory...
Marshal.FreeCoTaskMem( hdItem.pvFilter );
// return the string now in the text filter area
return hdTextfilter.pszText;
NOTE: None of this works without all the standard Win32 constants (
#define
) that are freely available to C++ programmers. I know that there are a number of 'libraries' built (re: UtilityLibrary) but since I only needed a subset (and some of the flags weren't in UtilityLibrary) I simplified this by creating all the needed enumerations and structures as W32_xxx values. See the Win32Enum.cs, Win32Msgs.cs, and Win32Struct.cs files included in the source.
ListViewFilterHeader Control Features
Since the Header
is available as a execution mode readonly
property by ListViewFilter
, it publishes a number of properties of it's own for use in manipulating the filters directly from code. Again my implementation may be called into question, but I wanted to access each property as an array. I could have made a Columns[]
class with it's own properties to do that, but I didn't. Some of these properties are accesible through the Columns
of the ListView
itself but I implemented them as Header
properties especially for the ContextMenu
so that it did not have to know about the ListView.Columns
, just the Header. The ContextMenu
appears when you click one of the column filter buttons. The available properties are:
- Alignment -
HorizontalAlignment
, column data format, Left, Right, Center
- DataType -
LVFDataType enum
, String, Number, Date format for comparison
- Filter -
string
, get/set the content of the filter
- Names -
string
, get/set the column header name
- SizeInfo -
Size
, readonly Width and Left in a (misused) Size
structure
Properties promoted by ListViewFilter itself
- Filtered -
bool
, turn on/off the filterbar
- SortColumn -
int
, the column to sort
- SortOrder -
bool
, true for ascending
The big thing about the
ListViewFilterHeader
control is that it is an encapsulation of the
System.Windows.Forms.NativeWindow
control and deals directly with the
Handle
of the
HeaderControl
itself. The
ListViewFilter
is responsible for the creation and destruction of this object. When the
ListViewFilter.Handle
changes this object is recreated and the new
Handle
attached in the constructor.
The primary purpose of this class is to set the header style, set the up/down sorted column arrow (this could be changed to an image index), and to promote access to the filter text, data type, and other properties of a column as needed. Any feature wanted for a header control accessible via SendMessage
could be added to this class and publised as properties just like these.
Caveats
Marshalling memory for structures isn't really that hard, it was just difficult to find the right documentation on how and when to do so. Creating the enumerations for all the needed Win32 definitions is time consuming and prone to error (I really want to see .NET implement that somehow). You may notice that some of the messages are the A version not W because string just is easier with non-unicode text.
There will be problems with this control on some machines; you have the source, make it work for you. I have been developing controls for many years now (mostly in Borland C++ Builder) and this is my first submission to any board or group (maybe I have been selfish). So, take it easy on me this time I've only been coding C# for two weeks now.