ListView(.Net)を仮想モードで高速化する

# ListViewの高速化

`.Net`の`ListView`に大量のデータを表示しようとすると時間が掛かってしまいます。
そこでいくつかある高速化の手段の1つとして、`仮想化モード`を紹介します。

## 仮想モードのすごいところ

仮想モードではすべてをデータを描画するのではなく、`見えているものだけを描画`します。
なので、非常に軽量です。

## どのくらい遅いのか(動画)

軽い対策のものを載せていきます。

- 1行ずつAdd()
- BeginUpdate()とEndUpdate()を導入
- AddRange()を導入

### 1行ずつAdd()

これは対策なしです。
ただひたすら1行ずつ`Add()`していく
遅すぎるので、表示する行数は10000にしています。(他は100000です。)
約5.73秒でした。

```c++
foreach (ListViewItem listViewItem in this.listViewItemList)
{
    this.listView1.Items.Add(listViewItem);
}
```
![add.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225372/ed72b8a5-8d16-37a3-3a87-5261a0dd6953.gif)

### `BeginUpdate()`と`EndUpdate()`を導入

描画を止めておく関数`BeginUpdate()`と`EndUpdate()`を導入する
件数は100000件です。
約3.71秒でした。

```c#
this.listView1.BeginUpdate();
foreach (ListViewItem listViewItem in this.listViewItemList)
{
    this.listView1.Items.Add(listViewItem);
}
this.listView1.EndUpdate();
```

![begin-end.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225372/a11ac0c5-768d-9a36-98d0-14dec0ebe802.gif)


### `AddRange()`に置き換え

データ追加を`AddRange()`でまるごとに追加します。
件数は100000件です。
約2.57秒でした。

```c#
this.listView1.BeginUpdate();
this.listView1.Items.AddRange(this.listViewItemList.ToArray());
this.listView1.EndUpdate();
```


![addrange.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225372/b86d37e8-bcfe-980b-ae9b-9fe7b282dce7.gif)





## 仮想モードだとどのくらい早いのか(動画)

![ezgif-1-f822828ecd41.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/225372/19e2c536-d6a2-ebc5-3ae9-e976534847b4.gif)

## できなくなること

仮想モードを使用することで、できなくなることがあります。
これは選択されている項目がわからなくなるということです。
仮想モードが`オン`と`listView1.SelectedItems`がアクセスできなるなります。
しかし選択されたインデックス`listView1.SelectedIndices`は利用可能なので、そちらをうまく使いましょう。
## 仮想モードでもできること

`データの更新`, `選択された項目のデータ取得`, `Insert`, `Remove`です。

以下のコードで実装されているので、躓いた際には参考にしてください。

## 仮想モードの実装コード

```c#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Forms;

namespace TestTask
{
    public partial class Form1 : Form
    {
        List<ListViewItem> listViewItemList;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // ListViewに仮想モードでデータを更新するイベントを追加
            this.listView1.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(listView1_RetrieveVirtualItem);
            // 仮想モードをオンに
            this.listView1.VirtualMode = true;
        }

        private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
        {
            e.Item = this.listViewItemList[e.ItemIndex];
        }

        private void data1_Click(object sender, EventArgs e)
        {
            this.setDate("Hello");
            this.start();
        }

        private void data2_Click(object sender, EventArgs e)
        {
            this.setDate("こんにちは");
            this.start();
        }

        private void start()
        {
            // 1回仮想モードに表示されるデータ数を0にすることで、別のデータを入れたときに全て更新されるようになる
            this.listView1.VirtualListSize = 0;
            this.listView1.VirtualListSize = this.listViewItemList.Count;
        }

        private void listView1_SelectedIndexChanged(object sender, EventArgs e)
        {
            ListView.SelectedIndexCollection selectedIndices = this.listView1.SelectedIndices;

            foreach(int index in selectedIndices)
            {
                string text = this.listView1.Items[index].Text;
                this.label1.Text = "選択したのは" + text + "です";
            }
        }

        private void insert_Click(object sender, EventArgs e)
        {
            // 追加
            ListViewItem listViewItem = new ListViewItem();
            listViewItem.Text = "追加された項目";

            this.listViewItemList.Insert(10, listViewItem);

            // 仮想モードの表示する数が一つ増えたので更新
            this.listView1.VirtualListSize = this.listViewItemList.Count;
            this.label1.Text = "10番目にInsertしました";
        }

        private void remove_Click(object sender, EventArgs e)
        {
            // 選択された項目を削除
            ListView.SelectedIndexCollection selectedIndices = this.listView1.SelectedIndices;

            foreach (int index in selectedIndices)
            {
                ListViewItem deleteItem = this.listView1.Items[index];
                this.listViewItemList.Remove(deleteItem);
                this.label1.Text = index + "番目の[" + deleteItem.Text + "]が削除されました";

            }

            // 仮想モードの表示する数が一つ減ったので更新
            this.listView1.VirtualListSize = this.listViewItemList.Count;
        }
        private void setDate(string greeting)
        {
            Stopwatch stopWatch = new Stopwatch();

            stopWatch.Start();

            List<ListViewItem> listViewItemList = new List<ListViewItem>();

            for (int i = 0; i < 100000; i++)
            {
                ListViewItem listViewItem = new ListViewItem();
                listViewItem.Text = greeting + i.ToString();
                listViewItemList.Add(listViewItem);
            }
            this.listViewItemList = listViewItemList;

            stopWatch.Stop();

            string time = stopWatch.Elapsed.TotalSeconds.ToString();
            this.label1.Text = "データ生成に" + time + "秒掛かりました。";
        }
    }
}
```