Interesting. This is actually the expected behaviour (I'll get back to that
further down), but apparently there is a bug in my code that causes it to
work only if you throw up something that gets focus away from the listview
(e.g. a message box)
In it's current form, my code only work properly for preventing new nodes
from being selected. I guess that is what most people want because no one
has ever said anything about it (I've posted that code to others as well)
:-)
The intended behaviour is to follow the behaviour of the
SelectedIndexChanged event, which actually fires twice when changing a
selection. It fires once when the old node is being deselected and once when
the new node is being selected. Fixing my code though was harder than I
expected. The Windows API works on an item level, it has no concept of
collections like .NET exposes. Windows provides a notification when the
state (selected, focused etc) of a particular item changes, but it doesn't
provide any information regarding related items. And notifications regarding
other items is independent of each other, so even if you prevent a change to
one item you can't tell Windows to stop fire notifications for the other
items. The collection that .NET uses to wrap this is easy to modify after
changes has occured but it's hard to prevent changes from occuring to it
(maybe that's why there isn't any changing event in the framework)
So basically it is easy to prevent an item from being selected (that is what
my code in practice does). It's much harder to prevent the selection from
*changing* (which is what my code was designed to do) simply because Windows
doesn't tell us what happens next and also doesn't provide any way for us to
stop the entire chain of notifications.
One fix is as follows. Change the if ((nmlv.uNewState... to the following
instead:
if ((nmlv.uOldState & LVIS_SELECTED) != LVIS_SELECTED)
&& ((nmlv.uNewState & LVIS_SELECTED) == LVIS_SELECTED)
{
// Item is being selected
ItemChangingEventArgs e = new ItemChangingEventArgs(nmlv.iItem);
OnSelectedIndexChanging(e);
if (e.Cancel)
{
m.Result = new IntPtr(1);
return;
}
}
elseif ((nmlv.uOldState & LVIS_SELECTED) == LVIS_SELECTED)
&& ((nmlv.uNewState & LVIS_SELECTED) != LVIS_SELECTED)
{
// Item is being deselected
ItemChangingEventArgs e = new ItemChangingEventArgs(nmlv.iItem);
OnSelectedIndexChanging(e);
if (e.Cancel)
{
m.Result = new IntPtr(1);
return;
}
}
This works better, but it still has a problem. If you prevent an item from
being deselected you must also prevent any other item from being selected.
Otherwise you'll see two selected items. And even if you do that there is
still a problem; once you allow the selection to change you might end up
with the old item still selected (Windows doesn't do anything with its state
unless you click on it). I was thinking that maybe prevent focus changes
(i.e. prevent the LVIS_FOCUSED (0x1) state change) would fix this, but I
haven't tried it.
/claes
Post by Keith. OI re-wrote it in C# -- I have to write codes in C# in my current project --
and tested it. I believe that it's identical to what you show me in VB.NET.
As a result, I could get a SelectedIndexChanging event (Thanks Claes :-) )
But the event occurs "twice." So I get a popup prompt twice (see my test
client code below.)
How can I prevent the event occurs twice or how can I filter unnecessary
event?
----------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace SelectTest
{
public partial class ListViewEx : ListView
{
const int WM_USER = 0x0400;
const int WM_NOTIFY = 0x4e;
const int OCM_BASE = WM_USER + 0x1C00;
const int OCM_NOTIFY = OCM_BASE + WM_NOTIFY;
const int LVN_FIRST = 0 - 100;
const int LVN_ITEMCHANGING = LVN_FIRST - 0;
const int LVIF_STATE = 0x0008;
const int LVIS_SELECTED = 0x0002;
[StructLayout(LayoutKind.Sequential)]
public struct NMHDR
{
public IntPtr hwndFrom;
public int idFrom;
public int code;
}
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Sequential)]
public struct NMLISTVIEW
{
public NMHDR hdr;
public int iItem;
public int iSubItem;
public int uNewState;
public int uOldState;
public int uChanged;
public POINT ptAction;
public int lParam;
}
public delegate void SelectedIndexChangingEventHandler (object
sender, ItemChangingEventArgs e );
public event SelectedIndexChangingEventHandler
SelectedIndexChanging;
protected virtual void
OnSelectedIndexChanging(ItemChangingEventArgs
e)
{
SelectedIndexChanging(this, e);
}
protected override void WndProc(ref Message m)
{
if (m.Msg == OCM_NOTIFY)
{
NMHDR nm = (NMHDR)m.GetLParam(typeof(NMHDR));
if (nm.code == LVN_ITEMCHANGING)
{
NMLISTVIEW nmlv =
(NMLISTVIEW)m.GetLParam(typeof(NMLISTVIEW));
if ( ( nmlv.uChanged & LVIF_STATE ) == LVIF_STATE)
{
if ((nmlv.uNewState & LVIS_SELECTED) ==
LVIS_SELECTED)
{
ItemChangingEventArgs e = new
ItemChangingEventArgs(nmlv.iItem);
OnSelectedIndexChanging(e);
if (e.Cancel)
{
m.Result = new IntPtr(1);
return;
}
}
}
}
}
base.WndProc(ref m);
}
public ListViewEx()
{
InitializeComponent();
}
}
public class ItemChangingEventArgs : CancelEventArgs
{
int m_index;
public ItemChangingEventArgs(int index)
{
m_index = index;
}
public int Index
{
get
{
return m_index;
}
}
}
}
----------------------------------------
----------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace SelectTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void OnSelectedIndexChanging(object sender,
ItemChangingEventArgs e)
{
if (DialogResult.Cancel == MessageBox.Show("Change selected
item?", "", MessageBoxButtons.OKCancel))
{
e.Cancel = true;
}
}
}
}
----------------------------------------
Thanks in advance,
Keith