Как избежать тысяч ненужных событий ListView.SelectedIndexChanged?

Если пользователь выберет все элементы в ListView.NET 2.0, ListView будет запускать событие SelectedIndexChanged для каждого элемента, а не запускать событие, чтобы указать, что выбор изменился.

Если пользователь затем щелкает, чтобы выбрать только один элемент в списке, ListView будет запускать событие SelectedIndexChanged для каждого элемента, который становится невыбранным, а затем событие SelectedIndexChanged для одного нового выбранного элемента, вместо того, чтобы запускать событие, чтобы указать, что выбор изменился.

Если у вас есть код в обработчике событий SelectedIndexChanged, программа перестанет отвечать на запросы, когда вы начнете иметь несколько сотен / тысяч элементов в списке.

Я думал о таймерах задержки и т. Д.

Но есть ли у кого-нибудь хорошее решение, чтобы избежать тысяч ненужных ListView. События SelectedIndexChange, когда действительно одно событие будет делать?

15 ответов

Решение

Хорошее решение от Яна. Я взял это и превратил в класс многоразового использования, убедившись, что таймер правильно настроен. Я также сократил интервал, чтобы получить более отзывчивое приложение. Этот контроль также удваивает буфер, чтобы уменьшить мерцание.

  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://stackru.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());
     }
  }

Дайте мне знать, если это можно улучшить.

Это решение для таймера задержки, которое я сейчас использую (просто означает "подождите немного"). Этот код может страдать от состояния гонки и, возможно, исключать нулевую ссылку.

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
}

Ты можешь использовать async & await:

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

Старый вопрос, я знаю, но это все еще кажется проблемой.

Вот мое решение, не использующее таймеры.

Он ожидает события 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);
    }
}

Флаг работает для события OnLoad формы windows / веб-формы / мобильной формы. В одном представлении Listview, а не в множественном выборе, следующий код прост в реализации и предотвращает многократное срабатывание события.

Поскольку ListView отменяет выбор первого элемента, второй элемент - это то, что вам нужно, и коллекция должна содержать только один элемент.

То же самое ниже использовалось в мобильном приложении, поэтому некоторые имена коллекций могут отличаться, так как используется компактная структура, однако применяются те же принципы.

Примечание: Убедитесь, что в OnLoad и заполнении списка вы указали первый элемент, который будет выбран.

// ################ 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. Этот код не будет много использовать в множественном выборе, так как событие работает так, как должно для этого поведения.

Надеюсь, это поможет.

С уважением,

Энтони Н. Урвин http://www.manatix.com/

Таймер - лучшее общее решение.

Проблема с предложением Йенса заключается в том, что если в списке много выбранных элементов (тысяч и более), получение списка выбранных элементов начинает занимать много времени.

Вместо создания объекта таймера каждый раз, когда происходит событие 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();
  }
}

Это прекрасно работает, если вы используете информацию просто для целей отображения, например, для обновления строки состояния, чтобы сказать "X из Y выбран".

Я просто пытался решить эту проблему вчера. Я не знаю точно, что вы подразумеваете под таймерами задержки, но я попытался реализовать свою собственную версию ожидания, пока все изменения не будут сделаны. К сожалению, единственный способ сделать это - создать отдельный поток, и получается, что когда вы создаете отдельный поток, ваши элементы пользовательского интерфейса недоступны в этом потоке. .NET создает исключение, утверждая, что элементы пользовательского интерфейса могут быть доступны только в потоке, в котором они были созданы! Итак, я нашел способ оптимизировать свой ответ на SelectedIndexChanged и сделать его достаточно быстрым, чтобы его можно было переносить - хотя это не масштабируемое решение. Будем надеяться, что у кого-то есть умная идея для решения этой проблемы в одном потоке.

Я бы попробовал привязать обратную передачу к кнопке, чтобы пользователь мог отправить свои изменения и отцепить обработчик событий.

У меня может быть лучшее решение.

Моя ситуация:

  • Один список выбора (а не множественный выбор)
  • Я хочу избежать обработки события, когда оно запускается для отмены выбора ранее выбранного элемента.

Мое решение:

  • Запишите, какой элемент пользователь щелкнул на MouseDown
  • Игнорируйте событие SelectedIndexChanged, если этот элемент не является нулевым, и SelectedIndexes.Count == 0

Код:

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

}

Что сработало для меня, так это просто использование события OnClick.

Мне просто нужно было получить одно значение и выйти, и первый выбор был в порядке, будь то то же исходное значение или новое.

Кажется, что щелчок происходит после того, как все изменения выбора сделаны, как это сделал бы таймер.

Щелчок гарантирует, что произошел настоящий щелчок, а не просто движение мыши. Хотя на практике, вероятно, нет никакой разницы, если только они не скользнули в раскрывающийся список с нажатой мышью и не отпустили.

Это сработало для меня, потому что щелчок, похоже, срабатывает только в элементе списка, имеющем клиентскую область. И у меня не было заголовков, на которые можно было бы нажать.

У меня просто было простое раскрывающееся всплывающее окно с одним элементом управления. И мне не нужно было беспокоиться о ключевых движениях при выборе элементов. Любые ключевые движения в раскрывающемся списке сетки свойств отменяют раскрывающийся список.

Попытка закрытия в середине SelectedIndexChanged также часто приводила к сбою. Но закрытие во время клика в порядке.

Сбой был тем, что заставило меня искать альтернативы и найти этот пост.

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

Возможно, это поможет вам достичь того, что вам нужно, без использования таймеров:

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

Я не люблю пользователей таймеров и т. Д. Как я также заявляю в посте...

Надеюсь, поможет...

Ооо, я забыл сказать, что это.NET 3.5, и я использую некоторые функции в linq, чтобы выполнить "Оценку изменений выбора", если вы можете назвать это oO..

В любом случае, если вы используете более старую версию, эта оценка должна быть выполнена с небольшим количеством кода... >.<...

У Раймонда Чена есть пост в блоге, который (вероятно) объясняет, почему существуют тысячи событий изменений, а не только одно:

Почему есть уведомление LVN_ODSTATECHANGED, когда уже есть очень хорошее уведомление LVN_ITEMCHANGED?

...
LVN_ODSTATECHANGED Уведомление сообщает, что состояние всех элементов в указанном диапазоне изменилось. Это сокращение для отправки индивидуума LVN_ITEMCHANGED для всех предметов в ассортименте [iFrom..iTo], Если у вас есть представление списка данных о владельце с 500000 элементов, а кто-то делает выборочный выбор, вы будете рады получить один LVN_ODSTATECHANGED уведомление с iFrom=0 а также iTo=499999 вместо полумиллиона человек мало LVN_ITEMCHANGED уведомления.

я говорю, вероятно, объясняет почему, потому что нет никакой гарантии, что представление списка.NET является оберткой вокруг общего элемента управления Listview - это детали реализации, которые могут быть изменены в любое время (хотя почти наверняка никогда не будут).

Намеченным решением является использование списка.NET в виртуальном режиме, что затрудняет использование элемента управления на порядок.

Майлон >>>

Целью никогда не было работать со списком из нескольких сотен элементов, но... Я проверил общий пользовательский опыт с 10 000 элементов и выборками 1000-5000 элементов за один раз (и изменениями 1000-3000 элементов в обоих выбранных и не выбран)...

Общая продолжительность расчета никогда не превышала 0,1 с, некоторые из самых высоких измерений составляли 0,04 с. Я обнаружил, что это вполне приемлемо для такого количества элементов.

И из 10.000 предметов, простая инициализация списка занимает более 10 секунд, поэтому в этот момент я бы подумал, что в игру вступят другие вещи, как отмечает Виртуализация, как отмечает Джо Чунг.

Тем не менее, должно быть ясно, что код не является оптимальным решением в том, как он вычисляет разницу в выборе, при необходимости это может быть значительно улучшено, и различными способами я сосредоточился на понимании концепции с кодом, а чем производительность.

Однако, если вы испытываете ухудшение производительности, меня очень интересуют некоторые из следующего:

  • Сколько предметов в списке?
  • Сколько выделенных / отмененных элементов одновременно?
  • Сколько времени примерно нужно, чтобы событие поднялось?
  • Аппаратная платформа?
  • Подробнее о случае использования?
  • Другая важная информация, которую вы можете придумать?

В противном случае не так-то просто помочь улучшить решение.

Я рекомендую виртуализировать ваш список, если в нем несколько сотен или тысяч элементов.

Оставь ListView и все старые элементы управления.

Делать DataGridView твой друг и все будет хорошо:)

Другие вопросы по тегам