Blazorでドラッグ&ドロップを実装する

C#

Blazorで、ドラッグ&ドロップを実装していきます。一から作成するのはさすがに大変なので、「BlazorSortable」を利用した方法を紹介します。

前提

以下の環境で実行しています。

oswindows11
dotnet8.0.304
UIBlazor(WebAssembly standalone)
IDEVisual Studio 2022 Community

本記事に記載しているソースコードは説明に必要な個所のみ抜粋していること、ご了承ください。

また、Blazorプロジェクト(WebAssembly)は作成済みであることを前提としています。

BlazorSortable

リストの項目をドラッグ&ドロップで並び替える処理を簡単に実現できる「SortableJS」というJavaScriptのライブラリがあります。SortableJSを利用して、Blazor用のコンポーネントを作ってくれているのが、「BlazorSortable」になります。

GitHubやブログ、動画の内容をもとに、1つの使い方を紹介します。そのほかの利用方法や詳細は以下をご確認ください。

セットアップ

まずは、「Index.html」にSortableJSを追加します。冒頭でも述べた通り、内部ではSortableJSを使用しているため必要になります。

@* index.html *@

<head>
    ・・・
</head>

<body>
    ・・・
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.13.0/Sortable.min.js"></script>
</body>

続いて、SortableListコンポーネントを自身のプロジェクトに移植します。GitHubから内容をコピーすればOKです。コピーするファイルはSharedフォルダにある以下の3つのファイルです。

  • SortableList.razor
  • SortableList.razor.css
  • SortableList.razor.js

実装

アイテムをドラッグ&ドロップすることで、2つのリストの中身を更新する処理を実装します。GitHubにはサンプルもいくつか用意されているので簡単に実装できます。最終的に以下のイメージのようなものを作成します。

テキストボックスと「+」ボタンは、左側のリストに値を追加するものですが、今回の内容には関係ありません。

2つのリストの作成

初めにアイテムを格納するリストを2つ作成します。リストの型はなんでも大丈夫です。また、今回はEditFormの中に作成しているので、バインドするModelを作成していますが、EditFormを使う必要はありません。

    [SupplyParameterFromForm]
    private Test Model { get; set; } = default!;

    protected override void OnInitialized()
    {
        Model ??= new();
        Model.ListLeft.Add(new TestInfo { Id = "1", Name = "aa" });
        Model.ListLeft.Add(new TestInfo { Id = "2", Name = "bb" });
        Model.ListLeft.Add(new TestInfo { Id = "3", Name = "cc" });
    }

    public class Test
    {
        public List<TestInfo> ListLeft { get; set; } = new();
        public List<TestInfo> ListRight { get; set; } = new();
    }

    public class TestInfo
    {
        public string? Id { get; set; }
        public string? Name { get; set; }
    }

リストの中身を入れ替える処理の追加

一方のリストのアイテムを別のリストにドラッグ&ドロップするときに、リストの中身を入れ替えるイベントを作成します。SortableListコンポーネントには「OnRemove」という、アイテムが(画面上で)削除された際に発生するイベントがあるので、これを利用します。

このイベントには、削除されるアイテムのインデックスと追加されるインデックスを引数として受け取ります。左のリストから右のリストに移動するときのイベントでの処理は以下のようになります。

    private void OnRemovedLeft((int oldIndex, int newIndex) indices)
    {
        var targetItem = Model.ListLeft[indices.oldIndex];
        Model.ListRight.Insert(indices.newIndex, targetItem);
        Model.ListLeft.Remove(targetItem);
    }

逆のパターンも同様に作成してください。

同じリスト内での並び替え

先ほどはリスト間の処理を書きましたが、同じリスト内での並び替えもできます。この場合は、「OnUpdate」イベントを利用します。OnRemoveと同じように、移動元と移動先のアイテムのインデックスを引数として受け取ります。このインデックスを利用してリスト内のアイテムの順番を入れ替えます。

    private void OnUpdateLeft((int oldIndex, int newIndex) indices)
    {
        var movedItem = Model.ListLeft[indices.oldIndex];
        Model.ListLeft.Remove(movedItem);
        if (indices.newIndex < Model.ListLeft.Count)
        {
            Model.ListLeft.Insert(indices.newIndex, movedItem);
        }
        else
        {
            Model.ListLeft.Add(movedItem);
        }
        StateHasChanged();
    }

HTMLの作成

最後に、htmlを書いていきます。

<div class="row mt-3">
    <div class="col-4">
        <div class="card p-1 border-dark" style="height:200px">
            <SortableList Id="sortableLeft" Items="Model.ListLeft" Context="left" Group="test" OnRemove="OnRemovedLeft"
                            OnUpdate="OnUpdateLeft" >
                <SortableItemTemplate>
                    <div class="d-grid">
                        <button class="btn btn-outline-secondary m-1">@left.Name</button>
                    </div>

                </SortableItemTemplate>
            </SortableList>
        </div>
    </div>
    <div class="col-4">
        <div class="card p-1 border-dark" style="height:200px">
            <SortableList Id="sortableRight" Items="Model.ListRight" Context="right" Group="test" OnRemove="OnRemovedRight"
                            >
                <SortableItemTemplate>
                    <div class="d-grid">
                        <button class="btn btn-outline-primary m-1">@right.Name</button>
                    </div>
                </SortableItemTemplate>
            </SortableList>
        </div>
    </div>
</div>

実行すると以下のようになります。

リスト内の並び替え

リスト間の移動

同じリスト内の入れ替えは問題なくできます。ただし、リスト間でアイテムを移動させようとすると、うまくいきません。移動はできますが、判定領域が非常に狭く使い勝手が悪いですね。そこで、この判定領域を広げましょう。

Sortableコンポーネントの修正

今までは「card」に対して高さを設定していました。それをSortableListコンポーネントのdivに対して設定します。

@* SortableList *@
@* heightを追加 *@
<div id="@Id" class="overflow-auto" style="height:@height">
    @foreach (var item in Items)
    {
        @if (SortableItemTemplate is not null)
        {
            @SortableItemTemplate(item)
        }
    }
</div>

@code{
    // 以下を追加
    [Parameter]
    public int Height { get; set; }
    private string height => $"{Height}px";
}

後はSortableListを呼び出す際に高さを指定すればOKです。

<SortableList Id="sortableRight" Items="Model.ListRight" Context="item2" Group="test" 
	OnRemove="OnRemovedRight" Height="200">
</SortableList>

以上、Blazorで簡単にドラッグ&ドロップを実装することができました。「SortableList」コンポーネントを作成するにあたり、様々な試行錯誤があったようです。この辺の詳細はすべて冒頭に記載した動画内で触れられていますので、確認してみてください。

コメント