Подтвердить что ты не робот

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

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

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

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

Я думал о таймерах пребывания и т.д.

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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
}

Ответ 3

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

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

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

Ответ 4

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

Поскольку 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

Ответ 5

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

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

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

Ответ 6

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

Ответ 7

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

Ответ 8

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

Ответ 9

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

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

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

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

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

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

Ответ 10

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

Ответ 11

Майлон → >

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

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

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

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

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

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

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

Ответ 12

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

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

Ответ 13

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

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

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

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

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

Ответ 14

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

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

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

Мое решение:

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

}