首页 / 知识

关于.net:如何避免成千上万个不必要的ListView.SelectedIndexChanged事件?

2023-04-16 19:14:00

关于.net:如何避免成千上万个不必要的ListView.SelectedIndexChanged事件?

How to avoid thousands of needless ListView.SelectedIndexChanged events?

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

如果用户随后单击以仅选择列表中的一个项目,则ListView将为每个未选中的项目触发一个SelectedIndexChanged事件,然后为单个新选择的项目触发一个SelectedIndexChanged事件,而不是触发一个事件来指示 选择已更改。

如果您在SelectedIndexChanged事件处理程序中有代码,则当列表中开始有成百上千个项目时,程序将变得无响应。

我已经考虑过停留计时器等。

但是,没有人有一个好的解决方案来避免成千上万个不必要的ListView.SelectedIndexChange事件,而实际上只有一个事件可以做到吗?


伊恩的好解决方案。我将其放入可重用的类中,确保正确处置了计时器。我还缩短了获取响应速度更快的应用程序的时间间隔。此控件还具有双缓冲区以减少闪烁。

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
  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:
     /// http://stackoverflow.com/questions/86793/how-to-avoid-thousands-of-needless-listview-selectedindexchanged-events
     /// </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());
     }
  }

让我知道是否可以改善。


这是我目前使用的驻留计时器解决方案("驻留"仅表示"稍等片刻")。此代码可能会出现竞争状况,并且可能会出现空引用异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
}

我知道一个老问题,但这似乎仍然是一个问题。

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

在触发SelectionChanged事件之前,它将等待MouseUp或KeyUp事件。
如果您以编程方式更改选择,则此操作将不起作用,该事件将不会触发,但是您可以轻松添加FinishedChanging事件或触发该事件的内容。

(它也有一些东西可以阻止闪烁,这与该问题无关)。

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
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);
    }
}

您可以使用asyncawait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}

标志适用于Windows表单/ Web表单/移动表单的OnLoad事件。
在单选Listview中,而不是多选中,以下代码易于实现,并且可以防止多次触发该事件。

当ListView取消选择第一个项目时,第二个项目将取消您需要的内容,并且集合应只包含一个项目。

在移动应用程序中使用了以下相同的内容,因此某些集合名称可能会因为使用紧凑框架而有所不同,但是适用相同的原理。

注意:确保OnLoad并填充您设置要选择的第一项的列表视图。

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
// ################ 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


计时器是最好的整体解决方案。

Jens建议的一个问题是,一旦列表中有很多选定项(数千个或更多),获取选定项列表就需要很长时间。

与其每次发生SelectedIndexChanged事件时都创建一个计时器对象,不如将它与设计者一起在表单上放置一个永久对象,然后让它检查类中的布尔变量以查看是否应调用更新函数,这比较简单。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
  }
}

如果您仅将信息用于显示目的,例如将状态栏更新为"选择的X中的X",则此方法很好用。


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

我的情况:

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

我的解决方案:

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

码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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();

}

Raymond Chen的博客文章(可能)解释了为什么有成千上万的变更事件,而不仅仅是一个:

Why is there an LVN_ODSTATECHANGED notification when there's already a perfectly good LVN_ITEMCHANGED notification?

...
The LVN_ODSTATECHANGED notification
tells you that the state of all items
in the specified range has changed.
It's a shorthand for sending an
individual LVN_ITEMCHANGED for all
items in the range [iFrom..iTo]. If
you have an ownerdata list view with
500,000 items and somebody does a
select-all, you'll be glad that you
get a single LVN_ODSTATECHANGED
notification with iFrom=0 and
iTo=499999 instead of a half million
individual little LVN_ITEMCHANGED
notifications.

我说大概可以解释原因,因为不能保证.NET列表视图是Listview公共控件的包装器-这是一个实现细节,可以随时更改(尽管几乎可以肯定不会更改)。

提示的解决方案是在虚拟模式下使用.NET列表视图,从而使控件难以使用一个数量级。


保留ListView和所有旧控件。

使DataGridView为您的朋友,一切都会好起来的:)


梅隆>>>

目的是永远不要使用超过数百项的列表,但是...
我已经测试了10.000项的整体用户体验,并且一次选择了1000-5000项(以及"选定"和"取消选定"中1000-3000项的更改)...

计算的总持续时间从未超过0.1秒,其中一些最高的测量值为0.04秒,我发现这对于许多项目来说是完全可以接受的。

而对于10.000个项目,仅初始化列表就需要10秒钟以上的时间,所以在这一点上,我本以为还有其他事情要发挥作用,正如乔钟(Joe Chung)指出的虚拟化一样。

就是说,应该清楚的是,代码不是如何计算选择差异的最佳解决方案,如果需要的话,它可以得到很大的改善,并且可以通过各种方式,我着重于对代码概念的理解。比性能。

但是,如果您的性能下降,我对以下某些方面非常感兴趣:

  • 列表中有几项?
  • 一次有多少个选择/取消选择的元素?
  • 筹集活动大约需要多长时间?
  • 硬件平台?
  • 有关使用案例的更多信息?
  • 您还能想到其他相关信息吗?

否则,帮助改进解决方案并不容易。


如果列表视图包含数百或数千个项目,我建议对其进行虚拟化。


也许这可以帮助您无需使用计时器即可完成所需的工作:

http://www.dotjem.com/archive/2009/06/19/20.aspx

我不喜欢计时器等的用户。正如我在帖子中所说的那样...

希望能帮助到你...

哦,我忘了说了,它是.NET 3.5,如果您可以称呼它为O.O,我正在使用linq中的某些功能来完成"选择更改评估"。

无论如何,如果您使用的是旧版本,则必须使用更多的代码来进行此评估...>。<...


昨天我只是想解决这个问题。我不确切知道"驻留"计时器的含义,但是我尝试实现自己的等待版本,直到完成所有更改。不幸的是,我想到的唯一方法是在单独的线程中,事实证明,当您创建单独的线程时,在该线程中无法访问UI元素。 .NET引发异常,指出只能在创建元素的线程中访问UI元素!因此,我找到了一种方法来优化对SelectedIndexChanged的响应,并使其足够快地到达可接受的位置-尽管它不是可伸缩的解决方案。让我们希望有人有一个聪明的主意,可以在一个线程中解决这个问题。


我可以尝试将回发与按钮绑定在一起,以允许用户提交其更改并取消挂钩事件处理程序。


事件用户项目选择

最新内容

相关内容

猜你喜欢