[ASP.NET Blazor Server][Radzen]Radzen Blazor DropZoneの使い方

本記事は以下の公式ドキュメントを解釈していく記事です。

Radzen Blazor Components
Blazor DropZone Component | Free UI Components by Radzen Demonstration and configuration of the Radzen Blazor DropZone component.
@* --- ドラッグ&ドロップの全体を管理するコンテナ --- *@
<RadzenDropZoneContainer TItem="MyTask" 
                         Data="data"
                         ItemSelector="@ItemSelector"
                         ItemRender="@OnItemRender"
                         CanDrop="@CanDrop"
                         Drop="@OnDrop">
    <ChildContent>
        @* 横並びのレイアウト設定 *@
        <RadzenStack Orientation="Orientation.Horizontal" Gap="1rem" Wrap="FlexWrap.Wrap" class="rz-p-12">
            
            @* --- 未着手ゾーン --- *@
            <RadzenDropZone Value="Status.NotStarted" class="rz-display-flex rz-flex-column rz-background-color-warning-lighter rz-border-warning-light rz-border-radius-2 rz-p-4" Style="flex: 1; gap: 1rem;">
                <RadzenText Text="Not started" TextStyle="TextStyle.Subtitle2" />
            </RadzenDropZone>

            @* --- 進行中ゾーン --- *@
            <RadzenDropZone Value="Status.Started" class="rz-display-flex rz-flex-column rz-background-color-info-lighter rz-border-info-light rz-border-radius-2 rz-p-4" Style="flex: 1; gap: 1rem;">
                <RadzenText Text="Started" TextStyle="TextStyle.Subtitle2" />
            </RadzenDropZone>

            @* --- 完了ゾーン --- *@
            <RadzenDropZone Value="Status.Completed" class="rz-display-flex rz-flex-column rz-background-color-success-lighter rz-border-success-light rz-border-radius-2 rz-p-4" Style="flex: 1; gap: 1rem;">
                <RadzenText Text="Completed" TextStyle="TextStyle.Subtitle2" />
            </RadzenDropZone>

            @* --- 削除ゾーン(ここに入れると見えなくなる) --- *@
            <RadzenDropZone Value="Status.Deleted" class="rz-display-flex rz-flex-column rz-background-color-danger-lighter rz-border-danger-light rz-border-radius-2 rz-p-4" Style="flex: 1; gap: 1rem;">
                <RadzenText Text="Drop here to delete" TextStyle="TextStyle.Subtitle2" />
            </RadzenDropZone>
            
        </RadzenStack>
    </ChildContent>

    @* --- 各タスク(カード)の中身のデザイン --- *@
    <Template>
        <strong>@context.Name</strong>
    </Template>
</RadzenDropZoneContainer>

<style>
    /* ドロップ可能な場所に重なった時の背景色(青っぽくなる) */
    .rz-can-drop {
        background-color: var(--rz-background-color-primary);
    }
</style>

@code {
    /// <summary>
    /// アイテム振り分けロジック:どのアイテムをどのゾーンに表示するか
    /// </summary>
    Func<MyTask, RadzenDropZone<MyTask>, bool> ItemSelector = (item, zone) => 
        item.Status == (Status)zone.Value && item.Status != Status.Deleted;

    /// <summary>
    /// ドロップ許可ロジック:移動させて良いかどうかを判定
    /// </summary>
    Func<RadzenDropZoneItemEventArgs<MyTask>, bool> CanDrop = request =>
    {
        // 1. 同じ列内での並び替えはOK
        // 2. 「削除」列への移動はどこからでもOK
        // 3. ステータスが「1段階だけ」変わる移動(例: 未着手→進行中)はOK
        return request.FromZone == request.ToZone || 
               (Status)request.ToZone.Value == Status.Deleted ||
               Math.Abs((int)request.Item.Status - (int)request.ToZone.Value) == 1;
    };

    /// <summary>
    /// 各アイテムの描画カスタマイズ
    /// </summary>
    void OnItemRender(RadzenDropZoneItemRenderEventArgs<MyTask> args)
    {
        // 名前が "Task2" の場合のみドラッグ禁止にする
        if (args.Item.Name == "Task2")
        {
            args.Attributes["draggable"] = "false";
            args.Attributes["style"] = "cursor:not-allowed";
            args.Attributes["class"] = "rz-card rz-variant-flat rz-background-color-primary-lighter rz-color-on-primary-lighter";
        }
        else
        {
            // 通常のアイテムの見た目
            args.Attributes["class"] = "rz-card rz-variant-filled rz-background-color-primary-light rz-color-on-primary-light";
        }

        // ステータスが Deleted なら画面上に表示しない
        args.Visible = args.Item.Status != Status.Deleted;
    }

    /// <summary>
    /// ドロップが完了した時の処理
    /// </summary>
    void OnDrop(RadzenDropZoneItemEventArgs<MyTask> args)
    {
        // 別の列に移動した場合、アイテムのステータスを更新
        if (args.FromZone != args.ToZone)
        {
            args.Item.Status = (Status)args.ToZone.Value;
        }

        // 列内での並び替え、または新しい列の特定の位置への挿入
        if (args.ToItem != null && args.ToItem != args.Item)
        {
            data.Remove(args.Item); // 一旦リストから外す
            data.Insert(data.IndexOf(args.ToItem), args.Item); // ターゲットの場所に差し込む
        }
    }

    // 表示データ保持用
    IList<MyTask> data;

    // 初期データ作成
    protected override void OnInitialized()
    {
        //Task0-5 が作られる
        data = Enumerable.Range(0, 5)
            .Select(i => 
                new MyTask() 
                { 
                    Id = i, 
                    Name = $"Task{i}", 
                    Status = i < 3 ? Status.NotStarted : Status.Started 
                })
            .ToList();
    }

    // タスクのデータモデル
    public class MyTask
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public Status Status { get; set; } = Status.NotStarted;
    }

    // 状態を定義する列挙型
    public enum Status
    {
        NotStarted, // 0
        Started,    // 1
        Completed,  // 2
        Deleted     // 3
    }
}

実装マニュアル

1. 事前準備

Radzenコンポーネントがプロジェクトで使用可能な状態か確認してください。

  • Program.cs: builder.Services.AddRadzenComponents(); が記述されていること。
  • _Imports.razor: @using Radzen および @using Radzen.Blazor が記述されていること。
  • MainLayout.razor: <RadzenComponents /> が配置されていること。

2. コンポーネントの作成

この機能を独立したページとして作成します。

  1. プロジェクト内の Pages フォルダ(または Components/Pages)に新しいファイルを作成します。
    • ファイル名: TaskBoard.razor
  2. コードファイルの**一番上(1行目)**に以下を追加して、ブラウザからアクセスできるようにします。
@page "/taskboard"
@using Radzen
@using Radzen.Blazor

3. 動作確認(テストシナリオ)

アプリを起動し、/taskboard にアクセスして以下の動作を確認してください。

A. 基本的な移動(正常系)

  • 操作: 「Not started」にある Task0 を「Started」の列へドラッグする。
  • 結果: ドロップでき、その列に収まること。

B. ルールによる制限(異常系)

このコードには CanDrop ロジックにより「1段階ずつしか進めない」というルールがあります。

  • 操作: 「Not started」にあるタスクを、いきなり「Completed」へドラッグする。
  • 結果: ドロップ禁止マークが表示され、移動できないこと(Math.Abs(...) == 1 の判定)。

C. ドラッグ禁止アイテムの確認

  • 操作: Task2 (色が薄く表示されているカード)をドラッグしようとする。
  • 結果: マウスカーソルが禁止マークになり、掴むことができないこと(OnItemRender での制御)。

D. 並び替え(ソート)

  • 操作: 同じ列の中で、タスクの上下を入れ替える。
  • 結果: 順番が入れ替わること。

E. 削除機能

  • 操作: 任意のタスクを「Drop here to delete」ゾーン(赤色)へドラッグする。
  • 結果: ドロップした瞬間に画面からタスクが消えること(StatusDeleted になり、表示対象外になるため)。

4. 実践的なカスタマイズ(DB連携など)

実際の業務アプリでは、メモリ上のリスト(data)だけでなく、データベースを更新する必要があります。その場合の修正箇所は以下の通りです。

データの読み込み

OnInitializedOnInitializedAsync に変更し、DBからデータを取得します。

@inject TaskService TaskService // サービスを注入

protected override async Task OnInitializedAsync()
{
    // DBからタスク一覧を取得
    data = (await TaskService.GetTasksAsync()).ToList();
}

ドロップ時の保存処理

OnDrop メソッド内で、変更をDBに保存します。

async Task OnDrop(RadzenDropZoneItemEventArgs<MyTask> args)
{
    // ...(既存のステータス更新・並び替えロジック)...

    if (args.FromZone != args.ToZone)
    {
        args.Item.Status = (Status)args.ToZone.Value;
        
        // 【追加】DB更新処理
        await TaskService.UpdateTaskStatusAsync(args.Item.Id, args.Item.Status);
        
        // 通知を表示(オプション)
        NotificationService.Notify(new NotificationMessage 
        { 
            Severity = NotificationSeverity.Success, 
            Summary = "更新完了", 
            Detail = $"{args.Item.Name} を移動しました", 
            Duration = 2000 
        });
    }
}

ステータス定義の分離

現在 @code ブロック内にある public class MyTaskpublic enum Status は、実際の開発では Models フォルダなどの別ファイル(例: MyTask.cs)に移動させることを推奨します。これにより、他のページからもこのクラスを参照できるようになります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする