WindowsForm ListView リストアイテムを挿入したときに末尾に追加されてしまうことへの対処法


## このドキュメントの内容

WindowsForm の ListView には、インデックスを指定してリストアイテムを挿入したときに画面表示上は末尾に追加されてしまう現象があります。その対処法を紹介します。

## ケース１：SmallIcon, LargeIcon, Tile で表示しているときにリストアイテムを挿入する

### サンプルコード

現象を確認できる簡単なサンプルコードです。ListView.Items.Insert メソッドを使用して、ダブルクリックされたリストアイテムの前に新規リストアイテムを挿入しています。

```c#
// SmallIcon を設定する
listView1.View = View.SmallIcon;

for (int i = 1; i <= 1000; i++)
{
    var item = new ListViewItem(i.ToString());
    listView1.Items.Add(item);
}

listView1.DoubleClick += (sender, e) =>
{
    var listView = (ListView)sender;

    // 選択されたリストアイテムの前に新規リストアイテムを挿入する
    if (listView.SelectedItems.Count == 0) { return; }
    var item = listView.SelectedItems[0];

    var newItem = new ListViewItem("newItem");

    listView.Items.Insert(item.Index, newItem);

    for (int i = 0; i < listView.Items.Count; ++i)
    {
        System.Diagnostics.Debug.WriteLine($"[{i}] {listView.Items[i]}");
    }
};
```

「990」をダブルクリックします。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/f680e6eb-ccb3-979a-c29c-914bbc7078e6.png)

「newItem」は末尾に追加されます。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/ae17e309-8167-fdcf-31e3-d91e617dfc6c.png)

デバッグ出力された内容からは、Items コレクションには意図したとおり「990」の前に挿入されていることがわかります。
```console
[988] ListViewItem: {989}
[989] ListViewItem: {newItem}
[990] ListViewItem: {990}
[991] ListViewItem: {991}
[992] ListViewItem: {992}
[993] ListViewItem: {993}
[994] ListViewItem: {994}
[995] ListViewItem: {995}
[996] ListViewItem: {996}
[997] ListViewItem: {997}
[998] ListViewItem: {998}
[999] ListViewItem: {999}
[1000] ListViewItem: {1000}
```

### 対処法

View プロパティの値をいったん別の値に変更して戻します。

```c#
var newItem = new ListViewItem("newItem");

listView.BeginUpdate();
try
{
    listView.Items.Insert(item.Index, newItem);

    listView.View = View.List;
    listView.View = View.SmallIcon;
}
finally
{
    listView.EndUpdate();
}
```

「990」の前に追加されるようになります。
但し、リストアイテムの個数によってはスクロールが発生してしまいます。表示領域を維持するよい方法は見つかりませんでした。SmallIcon, LargeIcon, Tile のときには TopItem プロパティを呼び出すと例外がスローされます。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/62d997bd-acfa-135c-e0df-a3975fea1fc8.png)



## ケース２：グループ表示しているときにグループにリストアイテムを挿入する

### サンプルコード

現象を確認できる簡単なサンプルコードです。ListViewGroup.Items.Insert メソッドを使用して、ダブルクリックされたリストアイテムを別のグループの先頭に挿入しています。

```c#
listView1.View = View.SmallIcon;

// 二つのグループを作り、それぞれにリストアイテムを追加する
var group1 = listView1.Groups.Add("g1", "Group1");
var group2 = listView1.Groups.Add("g2", "Group2");

for (int i = 1; i <= 10; i++)
{
    var item = new ListViewItem(i.ToString());
    item.Group = group1;
    listView1.Items.Add(item);
}

for (int i = 11; i <= 20; i++)
{
    var item = new ListViewItem(i.ToString());
    item.Group = group2;
    listView1.Items.Add(item);
}

listView1.DoubleClick += (sender, e) =>
{
    var listView = (ListView)sender;

    // 選択されたリストアイテムをリストアイテムのグループを他方のグループの先頭に移動させる
    if (listView.SelectedItems.Count == 0) { return; }
    var item = listView.SelectedItems[0];

    if (item.Group == group1)
    {
        InsertItemToGroup(item, group2, 0);
    }
    else
    {
        InsertItemToGroup(item, group1, 0);
    }
};

// 指定されたリストアイテムをグループに追加する
void InsertItemToGroup(ListViewItem item, ListViewGroup group, int index)
{
    group.Items.Insert(index, item);

    for (int i = 0; i < group.Items.Count; ++i)
    {
        System.Diagnostics.Debug.WriteLine($"[{i}] {group.Items[i]}");
    }
}
```

Group1 の「6」をダブルクリックします。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/e2b383b5-3f83-e3b4-4a97-b311f3765189.png)

「6」は Group2 に移動されましたが、末尾に追加されました。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/0165d9f4-1120-a14e-3a9f-b0df6b68430e.png)

デバッグ出力された内容からは、Items コレクションには意図したとおり先頭に挿入されていることがわかります。
```console
[0] ListViewItem: {6}
[1] ListViewItem: {11}
[2] ListViewItem: {12}
[3] ListViewItem: {13}
[4] ListViewItem: {14}
[5] ListViewItem: {15}
[6] ListViewItem: {16}
[7] ListViewItem: {17}
[8] ListViewItem: {18}
[9] ListViewItem: {19}
[10] ListViewItem: {20}
```

### 対処法

グループの挿入位置以降のリストアイテムをいったん削除して追加しなおします。
こちらのケースでは、View プロパティを変更する方法では解決しませんでした。

```c#
void InsertItemToGroup(ListViewItem item, ListViewGroup group, int index)
{
    if (group.Items.Count < index)
    {
        throw new IndexOutOfRangeException();
    }

    group.ListView.BeginUpdate();

    try
    {
        // 指定されたインデックス以降のリストアイテムを取得する
        var groupItems = group.Items.Cast<ListViewItem>().Skip(index).ToArray();

        // 同一グループに挿入する場合はグループから削除する
        if (item.Group == group)
        {
            item.Group = null;
        }

        // 指定されたリストアイテムを末尾に追加する
        group.Items.Add(item);

        // 指定されたインデックス以降のリストアイテムを追加しなおす
        for (int i = 0; i < groupItems.Length; i++)
        {
            // 指定されたリストアイテム自身は無視する
            // （同一グループに挿入する場合）
            if (groupItems[i] == item) { continue; }

            group.Items.Remove(groupItems[i]);
            group.Items.Add(groupItems[i]);
        }
    }
    finally
    {
        group.ListView.EndUpdate();
    }

    for (int i = 0; i < group.Items.Count; ++i)
    {
        System.Diagnostics.Debug.WriteLine($"[{i}] {group.Items[i]}");
    }

}
```

「6」が先頭に追加されるようになりました。
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280969/3a2f4099-c22a-116e-69e3-0b3511e973ce.png)

### 補足

ListView ではグループ表示が有効である場合、グループに属していないリストアイテムは既定のグループに属しているものとして表示されます。この既定のグループは ListView のプライベートメンバであるためアクセスできません。グループを用いる場合、「その他」や「未分類」というグループを明示的に追加するように設計したほうがよいと考えます。