C#中的ListView如何取消选中一个项目?

8
我正在开发一个 Windows 应用程序,其中包含一个 ListView ,其中包含许多项目。当用户单击项目时,应用程序会显示项目的详细信息。用户随后有机会编辑这些详细信息。每次更改后,用户都应该单击“保存”按钮,但是这并不总是发生。
如果用户进行更改而没有单击保存,则应用程序将显示一个消息框,询问他们是否想要保存更改。此框包括一个“取消”按钮,如果他们单击取消,我希望中止选择其他项目,并使用户保留在他们正在编辑的项目上。
我找不到方法来实现这一点。如果用户更改选项并且不保存,则从 itemselecedchanged 事件中显示对话框。如果用户单击取消,则我从事件中删除我的函数并手动更改所选项目,之后我将函数返回到事件中。但是之后事件被调用,手动选择的项目未被选中。
    private bool EnsureSelected()
    {
        bool continue_ = true;
        if (_objectChange)
        {
            var res = MessageBox.Show("Do you want to save changes?", "Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
            switch (res)
            {
                case DialogResult.Cancel:
                    if (!string.IsNullOrEmpty(_selectedKey))
                    {
                        listView_Keys.ItemSelectionChanged -= listView_Keys_ItemSelectionChanged;
                        listView_Keys.Focus();
                        listView_Keys.Items[_selectedKey].Selected = true;
                        listView_Keys.ItemSelectionChanged += listView_Keys_ItemSelectionChanged;
                    }
                    continue_ = false;
                    break;
                case DialogResult.Yes:
                    button_Save.PerformClick();
                    _objectChange = false;
                    break;
                case DialogResult.No:
                    _objectChange = false;
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }              
        }
        return continue_;
    }

更新:

我尝试了以下解决方案:

        public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }
    private ListViewItem currentSelection = null;
    private bool pending_changes = false;
    private void listView1_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
    {
        if (e.Item == currentSelection)
        {
            // if the current Item gets unselected but there are pending changes
            if (!e.IsSelected && pending_changes)
            {
                var res = MessageBox.Show("Do you want to save changes?", "Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
                switch (res)
                {
                    case DialogResult.Cancel:
                        // we dont want to change the selected item, so keep it selected
                        e.Item.Selected = true;
                        break;
                    case DialogResult.Yes:
                        //button_Save.PerformClick();
                        pending_changes = false;
                        break;
                    case DialogResult.No:
                        pending_changes = false;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
            }

        }
        else // not the selected button
        {
            if (!pending_changes && e.IsSelected)
            {
                // Item may be selected and we save it as the new current selection
                currentSelection = e.Item;
            }
            else if (pending_changes && e.IsSelected)
            {
                // Item may not be enabled, because there are pending changes
                e.Item.Selected = false;
            }
        }
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        listView1.Items[0].Selected = true;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        pending_changes = true;
    }
}

但这并没有起作用,第一次待处理的更改为真时,消息框调用了两次,第二次什么都没有发生。

2个回答

5

首先,无论何时选择另一个项目,事件都应该触发两次。

第一次是为了未被选中的项(其中e.IsSelectedfalse);

第二次是为了被选中的项(其中e.IsSelectedtrue)。

假设您有一个标志pending_changes,用于表示存在未保存的更改,则以下代码应该取消项目选择。

不幸的是,每当显示MessageBox时,listView会再次失去焦点。当您关闭MessageBox时,焦点会回到listView,这会导致控件再次触发其事件。因此,需要使用一个笨拙的解决方法,它需要记住我们在消息框上点击了“取消”,并在下一个事件上执行操作。

以下是包括解决方法的代码:

private ListViewItem currentSelection = null;
private bool pending_changes = false;
private bool cancel_flag = false;
private void listView1_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
    Console.WriteLine("Item " + e.ItemIndex + " is now " + e.IsSelected);
    if (e.Item != currentSelection)
    {
        // if another item gets selected but there are pending changes
        if (e.IsSelected && pending_changes)
        {
            if (cancel_flag)
            {
                // this handles the second mysterious event
                cancel_flag = false;
                currentSelection.Selected = true;
                e.Item.Selected = false;
                return;
            }
            Console.WriteLine("uh oh. pending changes.");
            var res = MessageBox.Show("Do you want to save changes?", "Warning", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Warning);
            switch (res)
            {
                case DialogResult.Cancel:
                    // we dont want to change the selected item, so keep it selected
                    currentSelection.Selected = true;
                    e.Item.Selected = false;
                    // for some reason, we will get the same event with the same argments again, 
                    // after we click the cancel button, so remember our decision
                    cancel_flag = true;
                    break;
                case DialogResult.Yes:
                    // saving here. if possible without clicking the button, but by calling the method that is called to save
                    pending_changes = false;
                    currentSelection = e.Item;
                    break;
                case DialogResult.No:
                    pending_changes = false;
                    currentSelection = e.Item;
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
        else if (e.IsSelected && !pending_changes)
        {
            currentSelection = e.Item;
        }
    }
}

第一次不起作用,第一次挂起更改为真时,消息框会调用两次,在第二次什么也不会发生。显示更新问题。 - aviad facebook
我重构了那段代码,关于ItemSelectionChanged事件的控件行为有点令人困惑。这是一个临时解决方案,但对我有效。 - h3n
1
谢谢,它可以工作,但如果我设置断点并开始调试事件,它的行为会有所不同,并且还会取消当前选择。 - aviad facebook
这是因为当你调试它时,控件会再次失去焦点,从而产生两个新事件:一个取消选择,一个选择,在重新获得焦点后。如果这篇回答对你有帮助,请考虑接受/点赞,因为我花了相当多的时间才找到答案。 - h3n
它的工作很好,只是在MessageBox之后的MouseClick事件没有被触发,所以如果用户按下Yes/No/Cancel,所有额外的逻辑都应该在ItemSelectionChanged内处理。 - Dima

1
您可以重新选择项目并在布尔标志中保留当前状态,以避免引起不必要的代码运行(例如,如果选定的项目实际上没有更改,则重新加载选定项目的值)。
另一种方法是处理LVN_ITEMCHANGING事件,但遗憾的是,在WinForms中没有实现该事件。您可以找到一个扩展的列表视图类,支持此事件,并允许防止在ListView item changing event thread on MSDN forums中更改选择。
以下是来自该线程的代码:
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

public partial class Form1 : Form
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }

    public Form1()
    {
        ListViewEx listView;
        Controls.Add(listView = new ListViewEx { Dock = DockStyle.Fill, Items = { "One", "Two", "Three" } });
        listView.ItemSelectionChanging += (s, e) =>
            {
                if (e.Index == 1)
                    e.Cancel = true;
                Debug.WriteLine(e);
            };
    }
}

public class ItemSelectionChangingEventArgs : CancelEventArgs
{
    public int Index { get; private set; }
    public bool NewValue { get; private set; }
    public bool CurrentValue { get; private set; }

    public ItemSelectionChangingEventArgs(int index, bool newValue, bool currentValue)
    {
        Index = index;
        NewValue = newValue;
        CurrentValue = currentValue;
    }

    public override string ToString()
    {
        return String.Format("Index={0}, NewValue={1}, CurrentValue={2}", Index, NewValue, CurrentValue);
    }
}

public class ListViewEx : ListView
{
    private static readonly Object ItemSelectionChangingEvent = new Object();
    public event EventHandler<ItemSelectionChangingEventArgs> ItemSelectionChanging
    {
        add { Events.AddHandler(ItemSelectionChangingEvent, value); }
        remove { Events.RemoveHandler(ItemSelectionChangingEvent, value); }
    }

    protected virtual void OnItemSelectionChanging(ItemSelectionChangingEventArgs e)
    {
        EventHandler<ItemSelectionChangingEventArgs> handler =
            (EventHandler<ItemSelectionChangingEventArgs>)Events[ItemSelectionChangingEvent];
        if (handler != null)
            handler(this, e);
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == 0x2000 + 0x004E) // [reflected] WM_NOTIFY
        {
            uint nmhdrCode = (uint)Marshal.ReadInt32(m.LParam, NmHdrCodeOffset); 
            if (nmhdrCode == LVN_ITEMCHANGING)
            {
                NMLISTVIEW nmlv = (NMLISTVIEW)Marshal.PtrToStructure(m.LParam, typeof(NMLISTVIEW));
                if ((nmlv.uChanged & LVIF_STATE) != 0)
                {
                    bool currentSel = (nmlv.uOldState & LVIS_SELECTED) == LVIS_SELECTED;
                    bool newSel = (nmlv.uNewState & LVIS_SELECTED) == LVIS_SELECTED;

                    if (newSel != currentSel)
                    {
                        ItemSelectionChangingEventArgs e = new ItemSelectionChangingEventArgs(nmlv.iItem, newSel, currentSel);
                        OnItemSelectionChanging(e);
                        m.Result = e.Cancel ? (IntPtr)1 : IntPtr.Zero;
                        return;
                    }
                }
            }
        }

        base.WndProc(ref m);
    }

    const int LVIF_STATE = 8;

    const int LVIS_FOCUSED = 1;
    const int LVIS_SELECTED = 2;

    const uint LVN_FIRST = unchecked(0U - 100U);
    const uint LVN_ITEMCHANGING = unchecked(LVN_FIRST - 0);
    const uint LVN_ITEMCHANGED = unchecked(LVN_FIRST - 1);

    static readonly int NmHdrCodeOffset = IntPtr.Size * 2;

    [StructLayout(LayoutKind.Sequential)]
    struct NMHDR
    {
        public IntPtr hwndFrom;
        public IntPtr idFrom;
        public uint code;
    }

    [StructLayout(LayoutKind.Sequential)]
    struct NMLISTVIEW
    {
        public NMHDR hdr;
        public int iItem;
        public int iSubItem;
        public int uNewState;
        public int uOldState;
        public int uChanged;
        public IntPtr lParam;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接