How to make your ListView columns reorderable

Another finishing touch that I like to see in applications that use ListViews is the ability for the end user to re-order the columns to suit their own preferences. This blog entry discusses one approach for adding this functionality to the ListView control present within the .NET Compact Framework. Although it is difficult to convey in a static screenshot, the screenshot above shows a user dragging the stylus over the header of the listview control to move the position of the “First Name” column.

Obtaining Draggable Columns

The System.Windows.Forms.ListView control is a wrapper over top of the native ListView control. The native ListView control supports the notion of extended styles, which allow various optional features to be enabled or disabled as desired. One of the extended styles is called LVS_EX_HEADERDRAGDROP. If this extended style is enabled the user can re-order the columns by dragging and dropping the headers shown at the top of the listview while it is in report mode.

Although the .NET Compact Framework ListView control does not expose a mechanism to enable extended styles, we can use a technique discussed in a previous blog entry of mine to add or remove the LVS_EX_HEADERDRAGDROP extended style as desired.

private const int LVM_SETEXTENDEDLISTVIEWSTYLE = 0x1000 + 54;
private const int LVS_EX_HEADERDRAGDROP = 0x00000010;
 
public static void SetAllowDraggableColumns(this ListView lv, bool enabled)
{
  // Add or remove the LVS_EX_HEADERDRAGDROP extended
  // style based upon the state of the enabled parameter.
  Message msg = new Message();
  msg.HWnd = lv.Handle;
  msg.Msg = LVM_SETEXTENDEDLISTVIEWSTYLE;
  msg.WParam = (IntPtr)LVS_EX_HEADERDRAGDROP;
  msg.LParam = enabled ? (IntPtr)LVS_EX_HEADERDRAGDROP : IntPtr.Zero;
 
  // Send the message to the listview control
  MessageWindow.SendMessage(ref msg);
}

This method allows the drag feature to be turned on and off for a given ListView control. Notice that this method makes use of a C# 3.0 feature called Extension Methods. The “this” keyword in front of the first parameter means that this method can be called as if it was part of the standard ListView control, meaning the following code snippet will work (assuming listView1 is an instance of the System.Windows.Forms.ListView control).

listView1.SetAllowDraggableColumns(true);

This is pure syntactic sugar, behind the scenes the C# compiler is simply passing in listView1 as the first parameter to the SetAllowDraggableColumns method.

Persisting Column Order Preferences

Once you have reorder-able columns it can be desirable to persist the user’s preferred layout across multiple executions of your application. It would be a pain if the columns always defaulted back to a standard order everytime the form was displayed.

The native ListView control provides two window messages, LVM_GETCOLUMNORDERARRAY and LVM_SETCOLUMNORDERARRAY that can be used to implement this feature. The code sample available for download wraps up these two window messages to allow you to query the current order of the columns by using a statement such as the following:

int[] columnOrder = listView1.GetColumnOrder();
// TODO: save 'columnOrder' to the registry
// or another persistent store

When columns are added to a ListView they are given an index. The first column is column 0 while the second is column 1 and so on. When columns are re-ordered they keep their index value but their position on screen changes. The array returned by the GetColumnOrder function contains the index for each column in the order that they are visible on screen. For example if the array contains the values 2, 0, and 1 it means that the last column (column 2) has been dragged from the right hand side of the listview to become the left most column.

Once we have obtained the order of the columns we can store the data in any persistent storage mechanism such as a file, a database table, or registry key. When the form is reloaded we can initialise the default order of the columns by calling the equivalent SetColumnOrder method with the value we previously saved:

// TODO: should read 'columnOrder' from the registry
// or other persistent store
int[] columnOrder = new int[]{2, 0, 1};
 
listView1.SetColumnOrder(columnOrder);

Sample Application

[Download ListViewExtenderTest.zip - 11 KB]

The sample application displays a list of three columns. While running on a Windows Mobile Professional device you should be able to re-order the columns by dragging and dropping the column headers with your stylus. If you exit and restart the application you should see that your custom column ordering is persisted. Via the left soft key menu item you can select an option that will disable the user from re-ordering the columns.

Most of the magic occurs within a file you can reuse in your own applications called ListViewExtender.cs. The sample application targets .NET CF 3.5 and hence requires Visual Studio 2008. With minor tweaks the source code would also be usable within .NET CF 2.0 projects. I would be keen to hear what your thoughts are about this. Is it time to shift code samples to .NET CF 3.5/Visual Studio 2008 or are you still wanting .NET CF 2.0 and Visual Studio 2005 compatible samples?

5 Responses to “How to make your ListView columns reorderable”

  1. Cocotteseb says:

    Hi,

    I wanted to know for a long time how to do this. Thank you! :)

    Concerning your question, I prefer having .NET CF 2.0 samples since I stay with it for now. The best would be to have the two samples: CF 2.0 and 3.5 but this requires more work…

  2. Carl says:

    To get your ListViewExtender class working on .NET CF 2.0 (I’m using VS2008), all I had to do was remove the “this” keyword from before the ListView parameter where present in the ListViewExtender methods.

    One thing to note is that if a ListView has checkboxes enabled, the checkboxes are only drawn neatly when they’re in the first column. In other columns the far right side of the check box is covered by the item text.

    Interesting is your design approach of using the extender class as a “helper class” as opposed to deriving the extender class directly from the ListView. Your approach I think will prevent WIndows Forms Designer problems that I have come across when trying to use the Designer on a control that is derived from a control in the .NET base class library. In C# we have a new design decision to make: “derivation” vs. “delegation”, what do you think?

    Great article.

  3. Carl says:

    Subtly related to this is an MSDN article on how to sort ListView items by tapping the column header:

    http://msdn.microsoft.com/en-us/library/ms229643.aspx

    Being able to reorder and sort columns in a ListView will make for much more user friendly lists.

  4. redwolf says:

    GGGGGGGGGGGGGGGGRRRRRRRRRRRREEEEEEEEAAAAAAAAAAAAATTTTTTTTT
    Because you are so great i will give you my code where i wrapped dat shit up, plus adding background changing + trasparency dehavior. Keep up the good work!

    public static class ListViewUtil
    {
    private const uint CLR_NONE = 0xFFFFFFFF;

    public static void SetBackgroundImage(ListView listView, Bitmap bitmap)
    {
    LVBKIMAGE lvBkImage = new LVBKIMAGE();
    lvBkImage.ulFlags = LVBKIF.SOURCE_HBITMAP | LVBKIF.STYLE_TILE;
    lvBkImage.hbm = bitmap.GetHbitmap();
    lvBkImage.cchImageMax = 0;
    lvBkImage.xOffsetPercent = 0;
    lvBkImage.yOffsetPercent = 0;

    Message msg = Message.Create(listView.Handle, (int)LVM.SETBKIMAGEW, // LVM_SETBKIMAGE,
    (IntPtr)0, Marshal.AllocCoTaskMem(Marshal.SizeOf(lvBkImage)));
    Marshal.StructureToPtr(lvBkImage, msg.LParam, false);
    MessageWindow.SendMessage(ref msg);
    Marshal.FreeCoTaskMem(msg.LParam);
    }

    public static void SetBackgroundTransparent(ListView listView)
    {
    HwndUtil.SendMessage(listView.Handle, (int)LVM.SETBKCOLOR, 0, CLR_NONE);
    }

    public static void SetTextBackgroundTransparent(ListView listView)
    {
    HwndUtil.SendMessage(listView.Handle, (int)LVM.SETTEXTBKCOLOR, 0, CLR_NONE);
    }

    public void SetAllowDraggableColumns(ListView lv, bool enabled)
    {
    Message msg = new Message();
    msg.HWnd = lv.Handle;
    msg.Msg = LVM.SETEXTENDEDLISTVIEWSTYLE;
    msg.WParam = (IntPtr)LVS.NOSCROLL;
    msg.LParam = enabled ? (IntPtr)LVS_NOSCROLL : IntPtr.Zero;

    MessageWindow.SendMessage(ref msg);
    }

    public enum LVBKIF : int
    {
    SOURCE_NONE = 0,
    SOURCE_HBITMAP = 1,
    SOURCE_URL = 2,
    SOURCE_MASK = 3,
    STYLE_NORMAL = 0,
    STYLE_TILE = 0×10,
    STYLE_MASK = 0×10,
    FLAG_TILEOFFSET = 0×100
    }

    public enum LVBKIF_SOURCE : int
    {
    NONE = 0×0,
    HBITMAP = 0×1,
    URL = 0×2,
    MASK = 0×3
    }

    public enum LVBKIF_STYLE : int
    {
    NORMAL = 0×0,
    TILE = 0×10,
    MASK = 0×10
    }

    public enum LVBKIF_FLAG : int
    {
    TILEOFFSET = 0×100
    }

    public class LVBKIMAGE
    {
    public LVBKIF ulFlags = LVBKIF.SOURCE_NONE;
    public IntPtr hbm = IntPtr.Zero;
    public IntPtr pszImage = IntPtr.Zero;
    public uint cchImageMax = 0;
    public int xOffsetPercent = 100;
    public int yOffsetPercent = 100;
    };

    public enum LVA : int
    {
    DEFAULT = 0,
    ALIGNLEFT = 1,
    ALIGNTOP = 2,
    SNAPTOGRID = 5
    }

    public enum LVCFMT : int
    {
    LEFT = 0×0,
    RIGHT = 0×1,
    CENTER = 0×2,
    JUSTIFYMASK = 0×3,
    IMAGE = 0×800,
    BITMAP_ON_RIGHT = 0×1000,
    COL_HAS_IMAGES = 0×8000
    }

    public enum LVCF : int
    {
    FMT = 0×1,
    WIDTH = 0×2,
    TEXT = 0×4,
    IMAGE = 0×10,
    ORDER = 0×20,
    SUBITEM = 0×8,
    }

    public enum LVFI : int
    {
    PARAM = 0×1,
    STRING = 0×2,
    PARTIAL = 0×8,
    WRAP = 0×20,
    NEARESTXY = 0×40
    }

    [Flags]
    public enum LVHT : int
    {
    NOWHERE = 0×1,
    ONITEM = ONITEMICON | ONITEMLABEL | ONITEMSTATEICON,
    ONITEMICON = 0×2,
    ONITEMLABEL = 0×4,
    ONITEMSTATEICON = 0×8,
    ABOVE = 0×8,
    BELOW = 0×10,
    TORIGHT = 0×20,
    TOLEFT = 0×40
    }

    public enum LFIF : int
    {
    DI_SETITEM = 0×1000,
    IMAGE = 0×2,
    INDENT = 0×10,
    NORECOMPUTE = 0×800,
    PARAM = 0×4,
    STATE = 0×8,
    TEXT = 0×1
    }

    public enum LVIR : int
    {
    BOUNDS = 0,
    ICON = 1,
    LABEL = 2,
    SELECTBOUNDS = 3
    }

    public enum LVIS : int
    {
    FOCUSED = 0×1,
    CUT = 0×4,
    SELECTED = 0×2,
    DROPHILITED = 0×8,
    OVERLAYMASK = 0xF00,
    STATEIMAGEMASK = 0xF000
    }

    public enum LVM : int
    {
    FIRST = 0×1000,
    APPROXIMATEVIEWRECT = (FIRST + 64),
    ARRANGE = (FIRST + 22),

    CREATEDRAGIMAGE = (FIRST + 33),

    DELETEALLITEMS = (FIRST + 9),
    DELETECOLUMN = (FIRST + 28),

    DELETEITEM = (FIRST + 8),
    EDITLABELA = (FIRST + 23),

    EDITLABELW = (FIRST + 118),
    ENSUREVISIBLE = (FIRST + 19),

    FINDITEMA = (FIRST + 13),
    FINDITEMW = (FIRST + 83),

    GETBKCOLOR = (FIRST + 0),

    GETBKIMAGEA = (FIRST + 69),
    GETBKIMAGEW = (FIRST + 139),
    GETCALLBACKMASK = (FIRST + 10),
    GETCOLUMNA = (FIRST + 25),
    GETCOLUMNORDERARRAY = (FIRST + 59),

    GETCOLUMNW = (FIRST + 95),
    GETCOLUMNWIDTH = (FIRST + 29),

    GETCOUNTPERPAGE = (FIRST + 40),
    GETEDITCONTROL = (FIRST + 24),

    GETHEADER = (FIRST + 31),
    GETHOTCURSOR = (FIRST + 63),

    GETHOTITEM = (FIRST + 61),
    GETHOVERTIME = (FIRST + 72),

    GETIMAGELIST = (FIRST + 2),
    GETISEARCHSTRINGA = (FIRST + 52),

    GETISEARCHSTRINGW = (FIRST + 117),
    GETITEMCOUNT = (FIRST + 4),

    GETITEMPOSITION = (FIRST + 16),
    GETITEMSPACING = (FIRST + 51),

    GETITEMSTATE = (FIRST + 44),
    GETITEMTEXTA = (FIRST + 45),
    GETITEMTEXTW = (FIRST + 115),
    GETNEXTITEM = (FIRST + 12),
    GETORIGIN = (FIRST + 41),
    GETSELECTEDCOUNT = (FIRST + 50),

    GETSELECTIONMARK = (FIRST + 66),
    GETSTRINGWIDTHA = (FIRST + 17),
    GETSTRINGWIDTHW = (FIRST + 87),
    GETSUBITEMRECT = (FIRST + 56),
    GETTEXTBKCOLOR = (FIRST + 37),
    GETTEXTCOLOR = (FIRST + 35),
    GETTOPINDEX = (FIRST + 39),
    GETVIEWRECT = (FIRST + 34),
    GETWORKAREA = (FIRST + 70),
    HITTEST = (FIRST + 18),
    INSERTCOLUMNA = (FIRST + 27),

    INSERTCOLUMNW = (FIRST + 97),
    INSERTITEMA = (FIRST + 7),
    INSERTITEMW = (FIRST + 77),
    REDRAWITEMS = (FIRST + 21),
    SCROLL = (FIRST + 20),

    SETBKCOLOR = (FIRST + 1),
    SETBKIMAGEA = (FIRST + 68),

    SETBKIMAGEW = (FIRST + 138),
    SETCALLBACKMASK = (FIRST + 11),
    SETCOLUMNA = (FIRST + 26),
    SETCOLUMNORDERARRAY = (FIRST + 58),
    SETCOLUMNW = (FIRST + 96),
    SETCOLUMNWIDTH = (FIRST + 30),
    SETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 54,
    SETHOTCURSOR = (FIRST + 62),
    SETHOTITEM = (FIRST + 60),
    SETHOVERTIME = (FIRST + 71),
    SETICONSPACING = (FIRST + 53),
    SETIMAGELIST = (FIRST + 3),
    SETITEMCOUNT = (FIRST + 47),

    SETITEMPOSITION = (FIRST + 15),
    SETITEMPOSITION32 = (FIRST + 49),
    SETITEMSTATE = (FIRST + 43),

    SETITEMTEXTA = (FIRST + 46),
    SETITEMTEXTW = (FIRST + 116),
    SETSELECTIONMARK = (FIRST + 67),
    SETTEXTBKCOLOR = (FIRST + 38),

    SETTEXTCOLOR = (FIRST + 36),
    SETWORKAREA = (FIRST + 65),
    SORTITEMS = (FIRST + 48),
    SUBITEMHITTEST = (FIRST + 57),
    UPDATE = (FIRST + 42)
    }

    public enum LVNI : int
    {
    ALL = 0×0,
    FOCUSED = 0×1,
    SELECTED = 0×2,
    CUT = 0×4,
    DROPHILITED = 0×8,
    ABOVE = 0×100,
    BELOW = 0×200,
    TOLEFT = 0×400,
    TORIGHT = 0×800
    }

    public enum LVN : int
    {
    FIRST = 0,
    BEGINDRAG = (FIRST – 9),
    BEGINLABELEDITA = (FIRST – 5),
    BEGINLABELEDITW = (FIRST – 75),
    BEGINRDRAG = (FIRST – 11),
    COLUMNCLICK = (FIRST – 8),
    DELETEALLITEMS = (FIRST – 4),
    DELETEITEM = (FIRST – 3),
    ENDLABELEDITA = (FIRST – 6),
    ENDLABELEDITW = (FIRST – 76),

    GETDISPINFOA = (FIRST – 50),
    GETDISPINFOW = (FIRST – 77),

    HOTTRACK = (FIRST – 21),
    INSERTITEM = (FIRST – 2),
    ITEMACTIVATE = (FIRST – 14),
    ITEMCHANGED = (FIRST – 1),
    ITEMCHANGING = (FIRST – 0),
    KEYDOWN = (FIRST – 55),
    MARQUEEBEGIN = (FIRST – 56),
    ODCACHEHINT = (FIRST – 13),
    ODFINDITEMA = (FIRST – 52),
    ODFINDITEMW = (FIRST – 79),
    ODSTATECHANGED = (FIRST – 15),
    SETDISPINFOA = (FIRST – 51),
    SETDISPINFOW = (FIRST – 78)
    }

    public enum LVSCW : int
    {
    AUTOSIZE = -1,
    AUTOSIZE_USEHEADER = -2
    }

    public enum LVSIL : int
    {
    NORMAL = 0,
    SMALL = 1,
    STATE = 2
    }

    public enum LVS : int
    {
    ALIGNLEFT = 0×800,
    ALIGNMASK = 0xC00,
    ALIGNTOP = 0×0,
    AUTOARRANGE = 0×100,
    EDITLABELS = 0×200,
    EX_CHECKBOXES = 0×4,
    EX_FLATSB = 0×100,
    EX_FULLROWSELECT = 0×20,
    EX_GRIDLINES = 0×1,
    EX_HEADERDRAGDROP = 0×10,
    EX_INFOTIP = 0×400,
    EX_ONECLICKACTIVATE = 0×40,
    EX_REGIONAL = 0×200,
    EX_SUBITEMIMAGES = 0×2,
    EX_TRACKSELECT = 0×8,
    EX_TWOCLICKACTIVATE = 0×80,
    ICON = 0×0,
    LIST = 0×3,
    NOCOLUMNHEADER = 0×4000,
    NOLABELWRAP = 0×80,
    NOSCROLL = 0×2000,
    NOSORTHEADER = 0×8000,
    OWNERDATA = 0×1000,

    OWNERDRAWFIXED = 0×400,
    REPORT = 0×1,
    SHAREIMAGELISTS = 0×40,

    SHOWSELALWAYS = 0×8,
    SINGLESEL = 0×4,
    SMALLICON = 0×2,

    SORTASCENDING = 0×10,

    SORTDESCENDING = 0×20,
    TYPEMASK = 0×3,
    TYPESTYLEMASK = 0xFC00,
    }
    }

  5. Le Sage says:

    Thanks redwolf, except that…
    SetAllowDraggableColumns has to be static,
    SETEXTENDEDLISTVIEWSTYLE = LVM_FIRST + 54, has to be replaced by SETEXTENDEDLISTVIEWSTYLE = FIRST + 54,
    HwndUtil isn’t provided (maybe using Microsoft.WindowsCE.Forms.MessageWindow would be what you want?),
    LVS_NOSCROLL has to be replaced by LVS.NOSCROLL,
    msg.Msg = LVM.SETEXTENDEDLISTVIEWSTYLE; needs an int cast.
    :)

Leave a Reply