As I posted previously here, Sorting a Listview can be something of a pain in the butt.
In that article, I covered some basics on providing a class that would essentially give you sorting capabilities for free, without all the messy code that would normally be required. A lot of the code required for sorting is mostly boilerplate with a few modifications for sorting various types. As a result, the generic implementation works rather well.
However, as with any class, adding features never hurts. In this case, I got to thinking- why not have right-clicking the ColumnHeaders show a menu for sorting against that Column? Seems simple enough. I quickly learned that apparent simplicity often is misattributed.
I faced several issues. The first thought was that I could hook a Mouse event for Right-Clicking a column header. Unfortunately, I soon discovered two facts about the .NET ListView control. First, was that there was no event for right-clicking a header control. Second, no even was fired at all by the ListView control when you right-clicked a header.
This left me stymied. How the heck do I implement this feature? I discovered something of a “hack” however, in that when the ListView’s ContextMenuStrip property is set, that ContextMenu Strip will be shown regardless of the location the ListView is clicked. This at least gave me something to work with. Since a ContextMenuStrip’s “Opening” event can be easily hooked, we can use that as an entry point and perform needed calculations to determine if we are indeed on a columnheader.
Which brings me to the next problem, which is determining when a columnheader was in fact the item that was clicked. This requires determining the rectangle the Header control occupies, first. The Header Control is a child control of the ListView; as such, a platform Invoke using the EnumChildWindows() API was required, something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private Rectangle _HeaderRect; private delegate bool EnumWindowsCallBack(IntPtr hwnd,IntPtr lparam); [DllImport("user32.dll")] private static extern int EnumChildWindows(IntPtr hwndParent,EnumWindowCallBack callbackFunction,IntPtr lParam); [DllImport("user32.dll"] private static extern bool GetWindowRect(IntPtr hWnd,out RECT lpRect); [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left; public int Top; public int Right; public int Bottom; } private bool EnumWindowCallback(IntPtr hwnd, IntPtr lParam) { RECT rct; if(!GetWindowRect(hwnd,out rct)) { //first child of the listview should be the header control _HeaderRect=Rectangle.Empty; //likely the listview is not in Details mode, so there is no header control. } else { _HeaderRect = new Rectangle(rct.Left,rct.Top,rct.Right-rct.Left,rct.Bottom-rct.Top); } return false; //cancel enumeration. } private static ColumnHeader[] GetOrderedHeaders(ListView lvw) { ColumnHeader[] returnarray = new ColumnHeader[lvw.Columns.Count]; foreach(ColumnHeader loopheader in lvw.Columns) { returnarray[loopheader.DisplayIndex] = loopheader; } return returnarray; } |
Quite a bit of boilerplate to add in. Basically, the idea is that we will hook the contextMenu Opening event of the Listview, (and we add a context menu to hook if the listview in fact doesn’t have one) in our constructor; and then when we receive the event we need to determine if the click occured within the area of the header control of the listview, if so, we cancel the event (which stops the default context menu from appearing) and show our own menu for the columnheader, which we can acquire using a bit of math and the static “GetOrderedHeaders” function, which retrieves the array of columnheaders of a ListView in order of appearance Left to Right (since the user could rearrange the Columns).
So First, we need to add code to the GenericListViewSorter’s Constructor. We also have a few private variables that are added; in this case, we need a ContextMenuStrip variable called “_ghostStrip” which we will use if we need to create a context menu for the control, since we don’t want that to appear in the default case. Of course we create our own ContextMenuStrip which we will show in the event instead of the default when appropriate. so we add this beneath the existing code in the constructor:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if(handleListView.ContextMenuStrip==null) { handleListView.ContextMenuStrip = new ContextMenuStrip(); handleListView.ContextMenuStrip.Items.Add("GHOST"); //add a ghost item so we get the Opening Event _ghoststrip = handleListView.ContextMenuStrip; } //create OUR context menu _headerContextMenuStrip = new ContextMenuStrip(); //add a ghost item to make sure Opening will fire. _HeaderContextMenuStrip.Items.Add("ghost"); handleListView.ContextMenuStrip.Opening += ContextMenuStrip_Opening; handleListView.ContextMenuStripChanged += handleListView_ContextMenuStripChanged; |
Of course we need to add the two referenced event handlers, too. The ContextMenuStripChanged being a rather simple implementation designed to keep changes in the contextmenu of the listview from causing us to balls up and stop showing ours (since we are now hooking a orphaned context menu not being shown by the listview).
1 2 3 4 |
void handleListView_ContextMenuStripChanged(object sender,EventArgs e) { OurListView.ContextMenuStrip.Opening+=ContextMenuStrip_Opening; } |
Now the meat of the code is in the ContextMenuStrip_Opening() routine. This will need to determine wether its applicable to show the Column menu, or the already present menu (which it doesn’t show either if it happens to be the _ghoststrip). This is accomplished by use of the GetCursorPos() API routine paired with the already present GetWindowRect() implementation, which we update by calling EnumWindows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
void ContextMenuStrip_Opening(object sender,System.ComponentModel.CancelEventArgs e) { //first, get screen coordinates of Cursor. POINTAPI gapi; GetCursorPos(out gapi); Point gotposition = new Point(gapi.X,gapi.Y); //acquire the HeaderRect of the control... EnumChildWindows(OurListView.Handle,new EnumWindowCallBack(EnumWindowCallback),IntPtr.Zero); //if the mouse position is within the retrieved rectangle, cancel the display of the normal menu and create and show ours. if(_HeaderRect.Contains(gotposition)) { e.Cancel=true; int xoffset = gotposition.X - _HeaderRect.Left; ColumnHeader clickedheader = HeaderAtOffset(OurListView,xoffset); if(clickedheader != null) { //create the context menu as needed. _HeaderContextMenuStrip = new ContextMenuStrip(); _HeaderContextMenuStrip.Tag = clickedheader; //two items, one for ascending order, one for descending order. ToolStripMenuItem AscendingHeaderItem = new ToolStripMenuItem(String.Format("Sort Column \"{0}\" Ascending",clickedheader.Text)); ToolStripMenuItem DescendingHeaderItem = new ToolStripMenuItem(String.Format("Sort Column \"{1}\" Descending",clickedheader.Text)); //if the current sort column is the header, check it off and disable it. if(CurrentSortColumn == clickedheader) { if(OurListView.Sorting ==SortOrder.Ascending) { AscendingHeaderItem.Checked=true; AscendingHeaderItem.Enabled=false; } else if (OurListView.Sorting==SortOrder.Descending) { DescendingHeaderItem.Checked=true; DescendingHeaderItem.Enabled=false; } } AscendingHeaderItem.Tag = ClickedHeader; DescendingHeaderItem.Tag = ClickedHeader; //set event handlers for the two items. AscendingHeaderItem.Click+= AscendingHeaderItem_Click; DescendingHeaderItem.Click+= DescendingHeaderItem_Click; //add them to the context menu strip. _HeaderContextMenuStrip.Items.Add(AscendingHeaderItem); _HeaderContextMenuStrip.Items.Add(DescendingHeaderItem); //display the menu. _HeaderContextMenuStrip.Show(gotposition); } } else { //show the default menu, but only if it isn't the ghoststrip. if(OurListView.ContextMenuStrip == _ghoststrip) e.Cancel=true; } } |
The events for the two buttons basically sort based on the columnheader in their tag, nothing particularly special there. the actual details can be seen in the source file itself, really.
It actually works quite well, I’m using it in a production application, and it’s working quite well.
Some obvious enhancements, of course, include making it possible to customize the shown menu, to present other options; perhaps a delegate or event that can be hooked that is given the Strip and the clicked column, and any number of other parameters? This would essentially give the equivalent of a ColumnHeaderRightClicked type event, too.
Have something to say about this post? Comment!