何千もの不要な ListView.SelectedIndexChanged イベントを回避するにはどうすればよいですか?
-
01-07-2019 - |
質問
ユーザーが .NET 2.0 ListView 内のすべての項目を選択すると、ListView は 選択されたインデックスが変更されました ことを示すイベントを発生させるのではなく、すべてのアイテムに対してイベントを発生させます。 選択 変更されました。
ユーザーがクリックしてリスト内の項目を 1 つだけ選択すると、ListView は 選択されたインデックスが変更されました のイベント 毎 選択が解除されつつある項目、そして 選択されたインデックスが変更されました 選択内容が変更されたことを示すイベントを発生させるのではなく、新しく選択された 1 つの項目に対するイベントを発生させます。
にコードがある場合は、 選択されたインデックスが変更されました イベント ハンドラーを使用すると、リストに数十万または数千の項目が含まれ始めると、プログラムはかなり応答しなくなります。
考えてみた ドエルタイマー, 、など。
しかし、何千もの不要な 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());
}
}
これを改善できるかどうかお知らせください。
他のヒント
これは私が現在使用している滞留タイマー ソリューションです (滞留とは「少し待つ」という意味です)。このコードは競合状態の影響を受ける可能性があり、場合によっては null 参照例外が発生する可能性があります。
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
}
タイマーは総合的に最適なソリューションです。
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 個が選択されました」と表示するなど、単に表示目的で情報を使用する場合には問題なく機能します。
Windowsフォーム/Webフォーム/モバイルフォームのOnLoadイベントに対してフラグが機能します。複数選択ではなく単一選択のリストビューでは、次のコードは実装が簡単で、イベントの複数の発生を防ぎます。
ListView では最初の項目の選択が解除され、2 番目の項目は必要なものになり、コレクションには 1 つの項目のみが含まれるようになります。
以下の同じものはモバイル アプリケーションで使用されているため、コンパクトなフレームワークを使用しているため、コレクション名の一部が異なる可能性がありますが、同じ原則が適用されます。
注記: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 で簡単に再利用および配布できるようにする必要があります。イベントはその動作に対して適切に機能するため、このコードは複数選択ではあまり使用されません。
それが役立つことを願っています。
敬具、
アンソニー N.アーウィンhttp://www.manatix.com
古い質問ですが、これはまだ問題のようです。
これがタイマーを使用しない私の解決策です。
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);
}
}
使用できます 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;
}
ユーザーが変更を送信できるようにポストバックをボタンに結び付けて、イベント ハンドラーのフックを解除してみます。
私はちょうど昨日、まさにこの問題に取り組もうとしていました。「滞留」タイマーが何を意味するのか正確にはわかりませんが、すべての変更が完了するまで待機する独自のバージョンを実装してみました。残念ながら、これを行う唯一の方法は別のスレッドで行うことであり、別のスレッドを作成すると、そのスレッドでは UI 要素にアクセスできなくなることがわかりました。.NET は、UI 要素が作成されたスレッドでのみアクセスできることを示す例外をスローします。そこで、SelectedIndexChanged への応答を最適化し、耐えられる速度まで高速化する方法を見つけました。ただし、これはスケーラブルなソリューションではありません。誰かがこの問題を 1 つのスレッドで解決する賢いアイデアを持っていることを願いましょう。
おそらくこれは、タイマーを使用せずに必要なことを達成するのに役立つかもしれません。
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 秒以上かかるため、この時点では、Joe Chung が指摘する仮想化のように、他のことが影響していると考えていたでしょう。
とはいえ、このコードは、選択の差を計算する方法において最適な解決策ではないことは明らかです。必要に応じて、これはさまざまな方法で大幅に改善できます。私はコードの概念を理解することに重点を置きました。パフォーマンスよりも。
ただし、パフォーマンスの低下が発生している場合は、次の点に非常に興味があります。
- リストには項目がいくつありますか?
- 一度に選択/選択解除できる要素の数はいくつですか?
- イベントが発生するまでにおおよそどのくらい時間がかかりますか?
- ハードウェアプラットフォーム?
- さらに詳しく 使用事例は?
- 他に考えられる関連情報はありますか?
そうしないと、ソリューションの改善に貢献するのは簡単ではありません。
出て ListView
そしてすべての古いコントロール。
作る DataGridView
あなたの友人、そしてすべてがうまくいくでしょう:)
レイモンド・チェン (おそらく) 説明するブログ投稿があります なぜ 何千もの変化イベントがあります, 1 つだけではなく:
すでに完全に適切な LVN_ITEMCHANGED 通知があるのに、なぜ LVN_ODSTATECHANGED 通知があるのでしょうか?
...
のLVN_ODSTATECHANGED
通知によれば、指定された範囲内のすべてのアイテムの状態が変更されたことがわかります。それは個人を送るための速記ですLVN_ITEMCHANGED
範囲内のすべてのアイテムについて[iFrom..iTo]
. 。500,000アイテムの所有者dataリストビューがあり、誰かがセレクトオールを行う場合、シングルを手に入れることができてうれしいですLVN_ODSTATECHANGED
通知付きiFrom=0
そしてiTo=499999
50万人の個人の代わりにLVN_ITEMCHANGED
通知。
私は言う おそらく その理由は、.NET リスト ビューが Listview Common Control のラッパーであるという保証がないためです。実装の詳細はいつでも自由に変更できます (ただし、変更されることはほとんどありません)。
解決策として示唆されているのは、.NET リストビューを仮想モードで使用することです。これにより、コントロールの使用が 1 桁難しくなります。
もっと良い解決策があるかもしれません。
私の状況:
- (複数選択ではなく) 単一選択リスト ビュー
- 以前に選択した項目の選択を解除するためにイベントが発生したときに、イベントを処理することを避けたいと考えています。
私の解決策:
- ユーザーが MouseDown をクリックした項目を記録します
- この項目が null ではなく、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();
}