Click here to Skip to main content
15,887,746 members
Articles / Programming Languages / XML

In-place Editing of ListView subitems

Rate me:
Please Sign up or sign in to vote.
4.91/5 (117 votes)
19 Oct 2004CPOL4 min read 616.5K   17.7K   197   131
An extended ListView to allow for in-place editing of subitems using arbitrary controls as editors

Image 1

Introduction

While talking with one of our customers, he wanted an additional feature in one of our programs to let him drop a number of files onto our application and then modify certain properties of the resulting documents in a list.

Showing these documents in a ListView can be done easily, but editing of single properties requires a little work (since built-in ListView only allows plain editing of a ListViewItem's text). Because I didn't find anything pre-built, I decided to write my own in-place editing for ListViews, so here it is...

How It Is Done

In fact, in-place editing in a ListView isn't too much magic, but there are a few places where the plain .NET Framework classes aren't sufficient, so I had to use a little Interop.

First, you have to have a control to perform the actual editing of the SubItem. Which control you use is (almost) completely up to you. TextBox, ComboBox or DateTimePicker works fine, for example. Since this control is used only when a SubItem has been clicked, it should be invisible in the beginning.

Then you have to find out which SubItem has been clicked. This part is quite straightforward, I've added a method GetSubItemAt() to my ListViewEx to make things a little easier.

A little twist comes from column reordering. Standard ListView allows you to rearrange its columns while in report view (AllowColumnReorder property). Unfortunately, there is no built-in way to find out the current order of your columns, so this is where Interop came in handy:

C#
[DllImport("user32.dll", CharSet=CharSet.Ansi)]
private static extern IntPtr SendMessage(IntPtr hWnd, 
                        int msg, int len, ref int [] order);

// ListView messages
private const int LVM_FIRST = 0x1000;
private const int LVM_GETCOLUMNORDERARRAY = (LVM_FIRST + 59);

Using these declarations, you can use the LVM_GETCOLUMNORDERARRAY message to get the ListView's current column order.

The next step is to move the editor control in place and to make it visible. Once the actual editing is being performed, the user must be able to accept or reject any changes he makes, so there are a few events that have to be caught while editing.

Usually, a click outside the editor control accepts any changes made, as does the Return key. Pressing ESC while in in-place editing mode converts back to the original SubItem text.

Because the editor control actually is not part of the ListView, I also had to look for any action that might change the size or location of the editor control. This was done overriding WndProc:

C#
protected override void WndProc(ref Message msg)
{
    switch (msg.Msg)
    {
        // Look for WM_VSCROLL, WM_HSCROLL or WM_SIZE messages.
        case WM_VSCROLL:
        case WM_HSCROLL:
        case WM_SIZE:
            EndEditing(false);
            break;
        case WM_NOTIFY:
        // Look for WM_NOTIFY of events that might also change the
        // editor's position/size: Column reordering or resizing
        NMHDR h = (NMHDR)Marshal.PtrToStructure(msg.LParam, typeof(NMHDR));
        if (h.code == HDN_BEGINDRAG ||
            h.code == HDN_ITEMCHANGINGA ||
            h.code == HDN_ITEMCHANGINGW)
            EndEditing(false);
        break;
    }

    base.WndProc(ref msg);
}

Here, scrolling and resizing of the ListView are monitored as well as changes to the ListView's column headers. If one of these messages is received, the input focus is transferred back to the ListView, thus ending in-place editing.

How to Use ListViewEx for in-place Editing

There are two ways to perform in-place editing with ListViewEx. First, you can use the new SubItemClicked event together with GetSubItemBounds() to position your editor control by yourself, or you can use StartEditing(), which performs all required calculations and control positioning by itself.

So, usually you would start by adding a ListViewEx and at least one control used as a cell editor to your Form. Don't forget to make your cell editor control invisible! Then wire up an event handler for SubItemClicked and actually start editing:

C#
private void listViewEx1_SubItemClicked(object sender, 
                         ListViewEx.SubItemClickEventArgs e)
{
    // Here, I use a ComboBox (comboBox1) as a cell editor:
    listViewEx1.StartEditing(comboBox1, e.Item, e.SubItem);
}

That's it!

I've included a small sample application to show you how to use ListViewEx with several different cell editors. Feel free to use the control or the source to your heart's desire and have fun!

Additional Features

Your comments gave me some hints on missing features, so meanwhile I've added an additional property DoubleClickActivation so that you can decide if the ListViewEx should enter editing mode when you click on a subitem or if a double click is required.

Another point was adding two new events (SubItemBeginEditing and SubItemEndEditing) to give the caller the possibility to control what's displayed in the editing control and what gets put back into the ListViewSubItem.
Now you're able to add a password field as a cell editor and transfer the plain password to and from the edit control without having it shown in the listview. Take a look at the sample project to see how it's done.

History

  • 09.04.2004
    • Initial release
  • 19.04.2004
    • Update to account for editor control and ListViewEx not sharing the same parent (thanks Eric-Paul)
    • Fixed code has been uploaded
  • 19.10.2004
    • Reviewed the whole project
    • Fixed a few bugs
    • Added new features (DoubleClickActivation, new events, higher level of control over the editing process,...

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) 4voice AG
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralDatepicker in demo doesn't work Pin
Anteros27-Aug-04 10:11
Anteros27-Aug-04 10:11 
GeneralRe: Datepicker in demo doesn't work Pin
mav.northwind27-Aug-04 10:38
mav.northwind27-Aug-04 10:38 
GeneralRe: Datepicker in demo doesn't work Pin
Anteros27-Aug-04 15:40
Anteros27-Aug-04 15:40 
GeneralRe: Datepicker in demo doesn't work Pin
mTrilby2-Feb-05 15:18
mTrilby2-Feb-05 15:18 
GeneralHaving trouble editing sub-items Pin
tupacs0120-Jun-04 15:10
tupacs0120-Jun-04 15:10 
GeneralRe: Having trouble editing sub-items Pin
mav.northwind20-Jun-04 20:05
mav.northwind20-Jun-04 20:05 
GeneralRe: Having trouble editing sub-items Pin
tupacs0121-Jun-04 2:15
tupacs0121-Jun-04 2:15 
Generalallow extra processing at end edit Pin
Matthew Hintzen27-Apr-04 14:46
Matthew Hintzen27-Apr-04 14:46 
Mav,

Thanks for the code. I have made a couple of improvements I thought you might be interested in. We will be using the control in SmartForms web deployment and so I had to get rid of the API calls so we didn't need to have our app's assembly's permissions set to FullTrust (don't want to end up with the ActiveX all or nothing). So when creating the control I set the reorder columns to false, warn the developer thru the output window and on all sub item edits I raise an error if the developer tried to get around my change by setting reorder columns to true.

But actually the biggest change I made was adding an extra event SubItemChanged, changed the event args to a SubItemEditEvent, with a Cancel Parameter that allows you to halt the edit from the handler. In our case we want the grid to show text but we want to assign a data id to the underlying data represented in the grids sub item, not the actually text.

This allows you to do functions like:

Private Sub lvePermissions_SubItemChanged(ByVal sender As Object, ByVal e As ListViewEx.SubItemEditEventArgs) Handles lvePermissions.SubItemChanged
Dim oLVItem As cListItemDataRow
Dim row As UsrGrpOpData.GroupPermissionRow
Dim oCboItem As cListItemDataRow

'get a reference to the DataItem on this row
oLVItem = CType(mlviItem, cListItemDataRow)
'and its underlying Datarow
row = CType(oLVItem.datarow, UsrGrpOpData.GroupPermissionRow)

If e.SubItem = 1 Then
'get the selected item in the combo box
oCboItem = CType(cboPermissions.SelectedItem, cListItemDataRow)
'and assign the ObjID from the combo item to the DataRow
row.PermissionLevelObjID = oCboItem.GuidID
Else
'this is here just to show the cancel edit usage.
e.Cancel = True
End If
End Sub

=================================Here is the modified code for the ListViewEx

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace ListViewEx
{
///
/// Event Handler for SubItemClicked event
///

public delegate void SubItemClickEventHandler(object sender, SubItemEditEventArgs e);

///
/// Event Handler for SubItemChanged event
///

public delegate void SubItemChangedEventHandler(object sender, SubItemEditEventArgs e);

///
/// Inherited ListView to allow in-place editing of subitems
///

public class ListViewEx : System.Windows.Forms.ListView
{
///
/// MessageHeader for WM_NOTIFY
///

private struct NMHDR
{
public IntPtr hwndFrom;
public Int32 idFrom;
public Int32 code;
}


// [DllImport("user32.dll")]
// private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wPar, IntPtr lPar);
// [DllImport("user32.dll", CharSet=CharSet.Ansi)]
// private static extern IntPtr SendMessage(IntPtr hWnd, int msg, int len, ref int [] order);

// ListView messages
private const int LVM_FIRST = 0x1000;
private const int LVM_GETCOLUMNORDERARRAY = (LVM_FIRST + 59);

///
/// Required designer variable.
///

private System.ComponentModel.Container components = null;

public event SubItemClickEventHandler SubItemClicked;
public event SubItemChangedEventHandler SubItemChanged;

public ListViewEx()
{
// This call is required by the Windows.Forms Form Designer.
InitializeComponent();

}

///
/// Clean up any resources being used.
///

protected override void Dispose( bool disposing )
{
if( disposing )
{
if( components != null )
components.Dispose();
}
base.Dispose( disposing );
}

#region Component Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///

private void InitializeComponent()
{
//
// ListViewEx
//
this.Layout += new System.Windows.Forms.LayoutEventHandler(this.ListViewEx_Layout);

}
#endregion

// ///
// /// Retrieve the order in which columns appear
// ///

// /// <returns>Current display order of column indices
// public int[] GetColumnOrder()
// {
// IntPtr lPar = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(int)) * Columns.Count);
//
// IntPtr res = SendMessage(Handle, LVM_GETCOLUMNORDERARRAY, new IntPtr(Columns.Count), lPar);
// if (res.ToInt32() == 0) // Something went wrong
// {
// Marshal.FreeHGlobal(lPar);
// return null;
// }
//
// int [] order = new int[Columns.Count];
// Marshal.Copy(lPar, order, 0, Columns.Count);
//
// Marshal.FreeHGlobal(lPar);
//
// return order;
// }


///
/// Find ListViewItem and SubItem Index at position (x,y)
///

/// <param name="x" />relative to ListView
/// <param name="y" />relative to ListView
/// <param name="item" />Item at position (x,y)
/// <returns>SubItem index
public int GetSubItemAt(int x, int y, out ListViewItem item)
{
if(this.AllowColumnReorder){
throw new System.InvalidOperationException("SubItem retrieval is only allowed when AllowColumnReorder = false");
} //end if

item = this.GetItemAt(x, y);

if (item != null)
{
//int[] order = GetColumnOrder();
Rectangle lviBounds;
int subItemX;

lviBounds = item.GetBounds(ItemBoundsPortion.Entire);
subItemX = lviBounds.Left;
//for (int i=0; i<order.length; i++)
="" for="" (int="" i="0;" i<columns.count;="" {
="" columnheader="" h="this.Columns[order[i]];
" if="" (x="" <="" subitemx+h.width)
="" return="" h.index;
="" }
="" subitemx="" +="h.Width;
"
="" -1;
="" }

="" windows="" messages="" which="" abort="" editing
="" private="" const="" int="" wm_hscroll="0x114;
" wm_vscroll="0x115;
" wm_size="0x05;
" wm_notify="0x4E;

" hdn_first="-300;
" hdn_begindrag="(HDN_FIRST-10);
" hdn_itemchanginga="(HDN_FIRST-0);
" hdn_itemchangingw="(HDN_FIRST-20);

" protected="" override="" void="" wndproc(ref="" message="" msg)
="" switch="" (msg.msg)
="" look="" wm_vscroll,wm_hscroll="" or="" messages.
="" case="" wm_vscroll:
="" wm_hscroll:
="" wm_size:
="" focus();
="" break;
="" wm_notify:
="" of="" events="" that="" might="" also="" change="" the
="" editor's="" position="" size:="" column="" reordering="" resizing
="" nmhdr="" typeof(nmhdr));
="" (h.code="=" ||
="" h.code="=" hdn_itemchangingw)
="" base.wndproc(ref="" msg);
="" onmouseup(system.windows.forms.mouseeventargs="" e)
="" base.onmouseup(e);

="" listviewitem="" item;
="" idx="GetSubItemAt(e.X," e.y,="" out="" item);

="" (idx="">= 0)
{
if (SubItemClicked != null)
SubItemClicked(this, new SubItemEditEventArgs(item, idx));
}
}

///
/// Get bounds for a SubItem
///

/// <param name="Item" />Target ListViewItem
/// <param name="SubItem" />Target SubItem index
/// <returns>Bounds of SubItem (relative to ListView)
public Rectangle GetSubItemBounds(ListViewItem Item, int SubItem)
{
//int[] order = GetColumnOrder();

Rectangle subItemRect = Rectangle.Empty;
if(this.AllowColumnReorder){
throw new System.InvalidOperationException("SubItem retrieval is only allowed when AllowColumnReorder = false");
} //end if

//if (SubItem >= order.Length)
if (SubItem >= Columns.Count)
throw new IndexOutOfRangeException("SubItem "+SubItem+" out of range");

if (Item == null)
throw new ArgumentNullException("Item");

Rectangle lviBounds = Item.GetBounds(ItemBoundsPortion.Entire);
int subItemX = lviBounds.Left;

ColumnHeader col;
int i;
//for (i=0; i<order.length; i++)
="" for="" (i="0;" i<columns.count;="" {
="" col="this.Columns[order[i]];
" if="" (col.index="=" subitem)
="" break;
="" subitemx="" +="col.Width;
" }=""
="" subitemrect="new" rectangle(subitemx,="" lvibounds.top,="" this.columns[order[i]].width,="" lvibounds.height);
="" this.columns[i].width,="" return="" subitemrect;
="" }

="" #region="" in-place="" editing="" functions
="" the="" control="" performing="" actual="" editing
="" private="" _editingcontrol;
="" lvi="" being="" edited
="" listviewitem="" _edititem;
="" subitem="" int="" _editsubitem;

="" <summary="">
/// Begin in-place editing of given cell
///
/// <param name="c" />Control used as cell editor
/// <param name="Item" />ListViewItem to edit
/// <param name="SubItem" />SubItem index to edit
public void StartEditing(Control c, ListViewItem Item, int SubItem)
{
if(this.AllowColumnReorder){
throw new System.InvalidOperationException("SubItem retrieval is only allowed when AllowColumnReorder = false");
} //end if

Rectangle rcSubItem = GetSubItemBounds(Item, SubItem);

if (rcSubItem.X < 0)
{
// Left edge of SubItem not visible - adjust rectangle position and width
rcSubItem.Width += rcSubItem.X;
rcSubItem.X=0;
}
if (rcSubItem.X+rcSubItem.Width > this.Width)
{
// Right edge of SubItem not visible - adjust rectangle width
rcSubItem.Width = this.Width-rcSubItem.Left;
}

// Subitem bounds are relative to the location of the ListView!
rcSubItem.Offset(Left, Top);

// In case the editing control and the listview are on different parents,
// account for different origins
Point origin = new Point(0,0);
Point lvOrigin = this.Parent.PointToScreen(origin);
Point ctlOrigin = c.Parent.PointToScreen(origin);

rcSubItem.Offset(lvOrigin.X-ctlOrigin.X, lvOrigin.Y-ctlOrigin.Y);

// Position and show editor
c.Bounds = rcSubItem;
c.Text = Item.SubItems[SubItem].Text;
c.Visible = true;
c.BringToFront();
c.Focus();

_editingControl = c;
_editingControl.Leave += new EventHandler(_editControl_Leave);
_editingControl.KeyPress += new KeyPressEventHandler(_editControl_KeyPress);

_editItem = Item;
_editSubItem = SubItem;
}

private void _editControl_Leave(object sender, EventArgs e)
{
// cell editor losing focus
EndEditing(true);
}

private void _editControl_KeyPress(object sender, System.Windows.Forms.KeyPressEventArgs e)
{
switch (e.KeyChar)
{
case (char)(int)Keys.Escape:
{
EndEditing(false);
break;
}

case (char)(int)Keys.Enter:
{
EndEditing(true);
break;
}
}
}

///
/// Accept or discard current value of cell editor control
///

/// <param name="AcceptChanges" />
public void EndEditing(bool AcceptChanges)
{
if (_editingControl == null)
return;

if (AcceptChanges && SubItemChanged != null){
SubItemEditEventArgs osieea = new SubItemEditEventArgs(_editItem, _editSubItem);
SubItemChanged(this, osieea);
AcceptChanges = !osieea.Cancel;
}


if (AcceptChanges)
_editItem.SubItems[_editSubItem].Text = _editingControl.Text;
//else
//_editingControl.Text = _editItem.SubItems[_editSubItem].Text;

_editingControl.Leave -= new EventHandler(_editControl_Leave);
_editingControl.KeyPress -= new KeyPressEventHandler(_editControl_KeyPress);

_editingControl.Visible = false;

_editingControl = null;
_editItem = null;
_editSubItem = -1;
}
#endregion

private void ListViewEx_Layout(object sender, System.Windows.Forms.LayoutEventArgs e) {
if(this.AllowColumnReorder){
this.AllowColumnReorder = false;
System.Diagnostics.Debug.WriteLine("ListViewEx can only be used to edit subitems when AllowColumnReorder = false\nAllowColumnReorder has been set to False");
} //end if

}
}

///
/// Event Args for SubItemClicked event
///

public class SubItemEditEventArgs : EventArgs
{
public SubItemEditEventArgs(ListViewItem item, int subItem)
{
_subItemIndex = subItem;
_item = item;
}
private int _subItemIndex = -1;
private ListViewItem _item = null;
private bool mbCancel = false;
public int SubItem
{
get { return _subItemIndex; }
}
public ListViewItem Item
{
get { return _item; }
}
public bool Cancel{
get { return mbCancel;}
set { mbCancel = value;}
}
}

}


Matthew C. Hintzen
CTO/CEO JADIP EasyWare, Inc.
www.jadip.com
GeneralRe: allow extra processing at end edit Pin
dwhearn21-May-04 3:48
dwhearn21-May-04 3:48 
GeneralRe: allow extra processing at end edit Pin
temp55568-Sep-04 2:34
temp55568-Sep-04 2:34 
GeneralRe: allow extra processing at end edit Pin
mav.northwind8-Sep-04 3:18
mav.northwind8-Sep-04 3:18 
GeneralCatch the update Pin
Hugo Hallman21-Apr-04 22:44
Hugo Hallman21-Apr-04 22:44 
GeneralRe: Catch the update Pin
mav.northwind22-Apr-04 21:38
mav.northwind22-Apr-04 21:38 
GeneralEdit control at the wrong location Pin
Eric-Paul18-Apr-04 23:39
Eric-Paul18-Apr-04 23:39 
GeneralRe: Edit control at the wrong location Pin
mav.northwind19-Apr-04 1:34
mav.northwind19-Apr-04 1:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.