ListBoxで、行をドラッグアンドドロップで並び替えられるようにした

# はじめに
一覧機能になってるやつでよくありますよね。
ドラッグアンドドロップで並び替える機能。
あれを実際に実装してみたよーっていう話です。

タイトルに毎回.NET FrameworkのC#の...って書いてたけど邪魔かなーと思って削ってみました。
タグに書いてあるし！
これをまとめてうまいこと伝えることができるワードがあるのであれば、教えてください。

# 前提条件
下記のような構造になっている想定です。
ListBox (name = "ColumnBox" ここはご自由にどうぞ！)
└ ListBoxItem
　└ お好みの要素

# 実装
ドラッグアンドドロップでの実装は結構いろいろなサイトで紹介されていたので、そのあたりの情報を拾ったり自分なりに解釈して組んでみました。
今回の動きを実装するのにあたって用意した関数は5つあるのですが、それぞれ分けて軽く説明しておこうと思います。
(間違ってるところもあるかと思います、ご了承下さい)

### 変数、初期化用関数
割りとコメントどおりなので、特筆することはないと思います。
<details><summary>コード</summary>

```c#
private ListBoxItem dragLbi;
private int? dragIndex;
private Point? startPos;
private DataObject dragData;

/// <summary>
/// ドラッグアンドドロップ用のデータを初期化する
/// </summary>
private void CleanDragDropData()
{
    dragLbi = null;
    dragIndex = null;
    startPos = null;
    dragData = null;
}
```
</details>

### PreviewMouseLeftButtonDown(マウス左クリック事前処理)
左クリックされたListBoxItemと、それが何行目のアイテムなのかを変数に保持します。
それと、後々で利用するため左クリックされた瞬間のマウスの位置も保持します。
dragDataはいまいち使い方がわかっていないのですが、後々必要になるのでとりあえず作ってます。
(正しい使い方教えてください・・)

<details><summary>コード</summary>

```c#
private void ColumnBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    FrameworkElement dragFe = (FrameworkElement)e.Source;
    ItemsControl itemsControl = (ItemsControl)sender;

    dragLbi = (ListBoxItem)itemsControl.ContainerFromElement(dragFe);
    if (dragLbi == null) { return; } // リストボックス内の、アイテムではない余白部分などをドラッグした場合nullとなる

    dragIndex = ColumnBox.Items.IndexOf(dragLbi);
    startPos = Mouse.GetPosition(ColumnBox);

    dragData = new DataObject("ListBoxItemDragAndDropData", dragLbi);
}
```
</details>

### PreviewMouseMove(マウス移動事前処理)
マウスが動いた位置を見て、ドラッグアンドドロップをしたいのかちょっと動いちゃっただけなのかによって処理を分岐してます。
これはドラッグアンドドロップをしたいに違いない！となった場合、DoDragDropを呼び出すことでドラッグアンドドロップ処理が始まります。
ちなみに、ドラッグアンドドロップが終わったタイミングでようやく、その下の`CleanDragDropData()`が呼び出されます。

<details><summary>コード</summary>

```c#
private void ColumnBox_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (startPos == null) { return; } // リストボックスアイテムをドラッグしていた場合のみ、後続の処理を行う

    Point curPos = Mouse.GetPosition(ColumnBox);
    Vector diff = curPos - (Point)startPos;

    // クリックした際にちょっとだけマウスが動いた程度なのか、明確な意思を思ってドラッグアンドドロップしたかを判断するため
    // 移動距離をチェック
    if (SystemParameters.MinimumHorizontalDragDistance + 2 < Math.Abs(diff.X)
        || SystemParameters.MinimumVerticalDragDistance + 2 < Math.Abs(diff.Y)
        )
    {
        DragDrop.DoDragDrop(dragLbi, dragData, DragDropEffects.Move); // ドラッグアンドドロップ処理開始

        CleanDragDropData();
    }
}
```
</details>

### PreviewMouseLeftButtonUp(マウス左クリックやめた時事前処理)
問答無用でドラッグアンドドロップ処理用のデータを初期化します。
主に、下記の順序のときにゴミデータが残ってしまうのを防ぐために入っています。
1.左クリックする(ColumnBox_PreviewMouseLeftButtonDownが呼び出され、いくつかの変数が代入される)
2.ちょっとしか動いてないため、ドラッグアンドドロップは開始されない
3.左クリックやめる

<details><summary>コード</summary>

```c#
private void ColumnBox_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    CleanDragDropData();
}
```
</details>

### ColumnBox_PreviewDrop(ドロップ事前処理)
ドロップ先のListBoxItemを取得します。
あとはもともとドラッグしていたListBoxItemを削除して、並び替え先に追加することで擬似的に並び替え処理を行っています。

<details><summary>コード</summary>

```c#
private void ColumnBox_PreviewDrop(object sender, DragEventArgs e)
{
    FrameworkElement dropFe = (FrameworkElement)e.Source;
    ItemsControl itemsControl = (ItemsControl)sender;

    ListBoxItem dropLbi = (ListBoxItem)itemsControl.ContainerFromElement(dropFe);
    int dropIndex = ColumnBox.Items.IndexOf(dropLbi);

    if (dropIndex < 0) { return; }

    ColumnBox.Items.Remove(dragLbi);
    ColumnBox.Items.Insert(dropIndex, dragLbi);
}
```
</details>

# おわりに
普段の業務ではこういった動的な動きを実装することはないので楽しいです。
もっといろいろな機能を作ってみたい。