如何避免数千次不必要的ListView.SelectedIndexChanged事件?

17

如果用户在.NET 2.0 ListView中选择了所有项,ListView将为每个项目触发一个 SelectedIndexChanged 事件,而不是触发一个事件来指示选择已更改。

如果然后用户单击以在列表中选择一项,ListView将为正在取消选择的每个项以及为单个新选择的项触发一个SelectedIndexChanged事件,而不是触发一个事件来指示选择已更改。

如果您有SelectedIndexChanged事件处理程序中的代码,那么当列表中有几百/几千项时,程序将变得非常不响应。

我曾考虑过使用等待计时器等方法。

但是,是否有任何好的解决方法可以避免产生成千上万个无用的ListView.SelectedIndexChange事件,而实际上只需要一个事件即可?

15个回答

12

Ian提供了一个好的解决方案。我将它制作成可重复使用的类,确保正确处理计时器的释放。我还减少了间隔以获得更敏感的应用程序。此控件还使用双缓冲技术来减少闪烁。

  public class DoublebufferedListView : System.Windows.Forms.ListView
  {
     private Timer m_changeDelayTimer = null;
     public DoublebufferedListView()
        : base()
     {
        // Set common properties for our listviews
        if (!SystemInformation.TerminalServerSession)
        {
           DoubleBuffered = true;
           SetStyle(ControlStyles.ResizeRedraw, true);
        }
     }

     /// <summary>
     /// Make sure to properly dispose of the timer
     /// </summary>
     /// <param name="disposing"></param>
     protected override void Dispose(bool disposing)
     {
        if (disposing && m_changeDelayTimer != null)
        {
           m_changeDelayTimer.Tick -= ChangeDelayTimerTick;
           m_changeDelayTimer.Dispose();
        }
        base.Dispose(disposing);
     }

     /// <summary>
     /// Hack to avoid lots of unnecessary change events by marshaling with a timer:
     /// https://dev59.com/R3VD5IYBdhLWcg3wGXpI
     /// </summary>
     /// <param name="e"></param>
     protected override void OnSelectedIndexChanged(EventArgs e)
     {
        if (m_changeDelayTimer == null)
        {
           m_changeDelayTimer = new Timer();
           m_changeDelayTimer.Tick += ChangeDelayTimerTick;
           m_changeDelayTimer.Interval = 40;
        }
        // When a new SelectedIndexChanged event arrives, disable, then enable the
        // timer, effectively resetting it, so that after the last one in a batch
        // arrives, there is at least 40 ms before we react, plenty of time 
        // to wait any other selection events in the same batch.
        m_changeDelayTimer.Enabled = false;
        m_changeDelayTimer.Enabled = true;
     }

     private void ChangeDelayTimerTick(object sender, EventArgs e)
     {
        m_changeDelayTimer.Enabled = false;
        base.OnSelectedIndexChanged(new EventArgs());
     }
  }

如果有需要改进的地方,请告诉我。


如果不在终端会话/RDP中,则仅使用双缓冲区+1。 - Ian Boyd
我会接受这个答案,但并未测试代码。希望不会出现任何崩溃。 - Ian Boyd
如果有的话,请告诉我。 :) - Robert Jeppesen
我仍然不明白为什么在OnSelectedIndexChanged事件中,计时器的启用条件会在没有任何区域的情况下同时被切换为false和true。 code m_changeDelayTimer.Enabled = false; m_changeDelayTimer.Enabled = true;code - hungrycoder
1
@hungrycoder,我在代码中添加了一些注释。希望这样能解释清楚。 - Robert Jeppesen
@RobertJeppesen,Listview构造函数在我的代码中导致了一些严重的闪烁问题。我一直在使用这个单独的Listview类,在一个winform中它很好用,但是在另一个winform中,其中包含并排放置的listview和treeview在不同的面板中,它会出现严重的闪烁问题。就像每当我们悬停在行上时,只有listview项目才可见,但它仍然会再次消失。这个listview中有将近2000行数据。当我删除构造函数时,它就没问题了,但是还是会有轻微的闪烁,不过不那么分散注意力,还算可以接受。 - hungrycoder

3

这是我目前正在使用的停留计时器解决方案(dwell只是意味着“稍等一下”)。这段代码可能会出现竞争条件,也许会出现空引用异常。

Timer changeDelayTimer = null;

private void lvResults_SelectedIndexChanged(object sender, EventArgs e)
{
        if (this.changeDelayTimer == null)
        {
            this.changeDelayTimer = new Timer();
            this.changeDelayTimer.Tick += ChangeDelayTimerTick;
            this.changeDelayTimer.Interval = 200; //200ms is what Explorer uses
        }
        this.changeDelayTimer.Enabled = false;
        this.changeDelayTimer.Enabled = true;
}

private void ChangeDelayTimerTick(object sender, EventArgs e)
{
    this.changeDelayTimer.Enabled = false;
    this.changeDelayTimer.Dispose();
    this.changeDelayTimer = null;

    //Add original SelectedIndexChanged event handler code here
    //todo
}

需要注意的是,这个“逗留”解决方案并不是一个完美的答案。它只是我实现的一种临时变通方法,直到我能够得到一个真正的解决方案。 - Ian Boyd
Timer类的事件在UI线程中运行,因此代码应该按预期工作。 - Thanatos
这并不意味着代码在窗体关闭时能够正确停止计时器,或者不会在第一个计时器运行时尝试启动另一个计时器,或者计时器在窗体被处理后不能触发,或者在引用之前不是空值。 “仅仅因为它能工作,并不意味着它是正确的。” - Ian Boyd

2

虽然这是一个老问题,但仍然存在问题。

这是我的解决方案,不使用计时器。

它会在MouseUp或KeyUp事件发生之前等待,然后再触发SelectionChanged事件。 如果您正在通过编程方式更改选择,则此方法将不起作用,事件不会触发,但您可以轻松添加FinishedChanging事件或其他事件来触发事件。

(它还有一些停止闪烁的东西,与此问题无关)。

public class ListViewNF : ListView
{
    bool SelectedIndexChanging = false;

    public ListViewNF()
    {
        this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
        this.SetStyle(ControlStyles.EnableNotifyMessage, true);
    }

    protected override void OnNotifyMessage(Message m)
    {
        if(m.Msg != 0x14)
            base.OnNotifyMessage(m);
    }

    protected override void OnSelectedIndexChanged(EventArgs e)
    {
        SelectedIndexChanging = true;
        //base.OnSelectedIndexChanged(e);
    }

    protected override void OnMouseUp(MouseEventArgs e)
    {
        if (SelectedIndexChanging)
        {
            base.OnSelectedIndexChanged(EventArgs.Empty);
            SelectedIndexChanging = false;
        }

        base.OnMouseUp(e);
    }

    protected override void OnKeyUp(KeyEventArgs e)
    {
        if (SelectedIndexChanging)
        {
            base.OnSelectedIndexChanged(EventArgs.Empty);
            SelectedIndexChanging = false;
        }

        base.OnKeyUp(e);
    }
}

非常好用。谢谢。这应该是被接受的答案,因为它不依赖于计时器。 - Robert S.

1

计时器是最好的综合解决方案。

Jens的建议存在一个问题,即一旦列表有很多选定的项目(成千上万个),获取选定项目列表开始变得很慢。

不要在每次SelectedIndexChanged事件发生时创建计时器对象,而是更简单地将永久计时器放置在设计师的表单上,并检查类中的布尔变量以查看是否应调用更新函数。

例如:

bool timer_event_should_call_update_controls = false;

private void lvwMyListView_SelectedIndexChanged(object sender, EventArgs e) {

  timer_event_should_call_update_controls = true;
}

private void UpdateControlsTimer_Tick(object sender, EventArgs e) {

  if (timer_event_should_call_update_controls) {
    timer_event_should_call_update_controls = false;

    update_controls();
  }
}

如果您仅将信息用于显示目的,例如更新状态栏以显示“已选择Y中的X个”,则此方法可正常工作。


1

标志对于窗体/网络表单/移动表单的OnLoad事件起作用。 在单选Listview中,不是多选,以下代码很容易实现,并防止事件的多次触发。

由于ListView取消选择第一个项目,第二个项目是您需要的,集合应始终只包含一个项目。

下面相同的内容用于移动应用程序,因此一些集合名称可能不同,因为它使用了紧凑框架,但是相同的原则适用。

注意:确保在Listview的OnLoad和populate中将第一个项目设置为选定状态。

// ################ CODE STARTS HERE ################
//Flag  to create at the form level
System.Boolean lsvLoadFlag = true;

//Make sure to set the flag to true at the begin of the form load and after
private void frmMain_Load(object sender, EventArgs e)
{
    //Prevent the listview from firing crazy in a single click NOT multislect environment
    lsvLoadFlag = true;

    //DO SOME CODE....

    //Enable the listview to process events
    lsvLoadFlag = false;
}

//Populate First then this line of code
lsvMain.Items[0].Selected = true;

//SelectedIndexChanged Event
 private void lsvMain_SelectedIndexChanged(object sender, EventArgs e)
{
    ListViewItem lvi = null;

    if (!lsvLoadFlag)
    {
        if (this.lsvMain.SelectedIndices != null)
        {
            if (this.lsvMain.SelectedIndices.Count == 1)
            {
                lvi = this.lsvMain.Items[this.lsvMain.SelectedIndices[0]];
            }
        }
    }
}
################ CODE END HERE    ################

理想情况下,这段代码应该放在一个UserControl中,以便在单选ListView中轻松重用和分发。对于多选,这段代码不会有太大用处,因为事件对于该行为的工作方式是正确的。
希望这可以帮到您。
此致敬礼,
安东尼·N·厄温 http://www.manatix.com

1
你可以使用 asyncawait

private bool waitForUpdateControls = false;

private async void listView_SelectedIndexChanged(object sender, EventArgs e)
{
    // To avoid thousands of needless ListView.SelectedIndexChanged events.

    if (waitForUpdateControls)
    {
        return;
    }

    waitForUpdateControls = true;

    await Task.Delay(100);

    waitForUpdateControls = false;

    UpdateControls();

    return;
}

0

我可能有更好的解决方案。

我的情况:

  • 单选列表视图(而不是多选)
  • 我想避免在取消先前选择的项目时触发事件时进行处理。

我的解决方案:

  • 记录用户在MouseDown上单击的项目
  • 如果此项不为空且SelectedIndexes.Count == 0,则忽略SelectedIndexChanged事件

代码:

ListViewItem ItemOnMouseDown = null;
private void lvTransactions_MouseDown(object sender, MouseEventArgs e)
{
    ItemOnMouseDown = lvTransactions.GetItemAt(e.X, e.Y);
}
private void lvTransactions_SelectedIndexChanged(object sender, EventArgs e)
{
    if (ItemOnMouseDown != null && lvTransactions.SelectedIndices.Count == 0)
        return;

    SelectedIndexDidReallyChange();

}

0

对我有效的方法是只使用OnClick事件。

我只需要获取一个单一的值并退出,第一个选择就可以了,无论是相同的原始值还是新值。

点击似乎发生在所有选择更改完成之后,就像计时器一样。

点击确保真正发生了点击,而不仅仅是鼠标松开。虽然在实践中可能没有区别,除非他们用鼠标按下并释放滑入下拉列表。

这对我有用,因为点击似乎只在客户端区域承载列表项时触发。而且我没有要点击的标题。

我只有一个简单的单控制弹出式下拉菜单。我不必担心键移动选择项目。属性网格下拉菜单上的任何键移动都会取消下拉菜单。

尝试在SelectedIndexChanged中间关闭很多次也会导致崩溃。但是在Click期间关闭是可以的。

崩溃的事情是导致我寻找替代方案并找到这篇文章的原因。

    void OnClick(object sender, EventArgs e)
    {
        if (this.isInitialize) // kind of pedantic
            return;

        if (this.SelectedIndices.Count > 0)
        {
            string value = this.SelectedItems[0].Tag;
            if (value != null)
            {
                this.OutValue = value;
            }
        }

        //NOTE: if this close is done in SelectedIndexChanged, will crash
        //  with corrupted memory error if an item was already selected

        // Tell property grid to close the wrapper Form
        var editorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        if ((object)editorService != null)
        {
            editorService.CloseDropDown();
        }
    }

0

如果您的列表视图有几百或几千个项目,我建议您进行虚拟化。


1
虚拟列表视图不允许您选择项目吗? - Ian Boyd

0

Maylon >>>

原本的目标是不与多于几百项的列表一起工作,但是...... 我已经测试了包含1万个项目和同时选择1000-5000个项目(并且在已选和未选中的项目中更改1000-3000项)的用户体验......

整个计算持续时间从未超过0.1秒,最高测量值之一为0.04秒,我认为这是可以接受的。

而在10,000项的情况下,仅初始化列表就需要超过10秒,所以此时我认为其他因素已经发挥了作用,如Joe Chung所指出的虚拟化。

也就是说,应该清楚代码在计算选择差异方面并不是最优解,如果需要,可以通过很多方式进行改进,但我更加关注代码概念的理解而非性能。

然而,如果您遇到性能下降的情况,我对以下几点非常感兴趣:

  • 列表中有多少项?
  • 一次选择/取消选择多少个元素?
  • 事件大约需要多长时间才能触发?
  • 硬件平台是什么?
  • 更多关于使用情况的信息?
  • 您能想到的其他相关信息?

否则很难帮助改进解决方案。


列表中有10,000个项目,请按下“全选”键盘快捷键。然后清除选择。 - Ian Boyd
这个概念非常简单,你需要对用户输入做出反应,而不是元素属性的变化或类似的东西。你需要使用新的事件,因为旧的事件将像往常一样工作,而新的事件将在以下两种情况下检查是否对列表视图进行了选择更改:
  1. 鼠标键被释放。
  2. 键盘键被释放。
这样,事件只会根据用户交互触发一次,而不是每个元素更改一次。如果是这样,它将引发“ListSelectionChanged”事件。“全选”快捷键在ListView中默认不起作用,所以这必须是你添加的东西?
- Jens

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