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

StackOverflow https://stackoverflow.com/questions/86793

Вопрос

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

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

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

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

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

Это было полезно?

Решение

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

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

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

Другие советы

Это решение с таймером ожидания, которое я использую сейчас (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
}

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

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

Вместо того чтобы создавать объект timer каждый раз, когда происходит событие SelectedIndexChanged, проще просто поместить постоянный объект timer в форму с помощью дизайнера и попросить его проверить логическую переменную в классе, чтобы узнать, следует ли ей вызывать функцию обновления.

Например:

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".

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

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

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

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

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

Я надеюсь, что это поможет.

С наилучшими пожеланиями,

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

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

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

Он ожидает события наведения курсора мыши или нажатия клавиши, прежде чем запустить событие 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);
    }
}

Вы можете использовать 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;
}

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

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

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

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

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

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

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

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

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

Майлон >>>

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

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

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

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

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

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

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

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

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

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

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

...
Тот Самый LVN_ODSTATECHANGED уведомление сообщает вам, что состояние всех элементов в указанном диапазоне изменилось.Это сокращение для отправки индивидуального LVN_ITEMCHANGED для всех товаров в ассортименте [iFrom..iTo].Если у вас есть список ownerdata, содержащий 500 000 элементов, и кто-то выполняет select-all, вы будете рады, что получите один LVN_ODSTATECHANGED уведомление с iFrom=0 и iTo=499999 вместо полумиллиона отдельные маленькие LVN_ITEMCHANGED уведомления.

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

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

Возможно, у меня есть решение получше.

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

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

Мое решение:

  • Запишите, на какой элемент пользователь нажал мышью
  • Игнорируйте событие SelectedIndexChanged, если этот элемент не равен null и 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();

}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top