在Blazor中實現拖放(drag and drop)


前言

我在實現一個含有待辦列表功能的頁面時,發現了一個好看的設計,它將待辦分為——“待辦”,“正在進行”,和“已完成”三種狀態,並且將待辦通過拖拽的方式在這三種狀態之間進行切換。

image-20211104150546076

這種方式看起來真不錯,但是使用Blazor來實現這種拖拽效果看起來似乎並不太容易。

經過一番百度難以找到,最后搜索英文通過Google找到了合適的解決方案。

感謝Chris Sainty的博客,通過學習他的代碼,最終實現了自己想要的效果。

解決方案

需要實現的目標:

  • 跟蹤用戶正在拖拽的項目

  • 控制該項目在哪里可以掉落

  • 反饋給用戶哪些列表可以掉落,哪些不能

  • 鼠標松開時更新項目

數據結構:

public class TodoItemDto
{
    public int Id { get; set; }
    public string Title { get; set; }

    public string Description { get; set; }

    public TodoStatusEnum Status { get; set; }

    /// <summary>
    /// 截止日期
    /// </summary>
    public DateTime ClosingDate { get; set; }

    /// <summary>
    /// 標簽
    /// </summary>
    public string Tag { get; set; }

    public bool IsImportant { get; set; }

    public int RepeatTimes { get; set; }
}

public enum TodoStatusEnum
{
    Todo,                     // 待辦
    InProgress,				  // 正在進行
    Completed                 // 已完成
}

Components

使用三個組件來實現該效果:

  • TodoContainer.razor

    • 使用TodoContainer包含Todo,InProgress,Completed三個待辦列表
    • 跟蹤容器內被用戶拖拽的項目
    • 包含一個事件供狀態更新時調用

    TodoContainer.razor

    <div class="todo-view-content-layout">
        @* 將自己作為級聯參數傳遞給子組件(TodoList) *@
        <CascadingValue Value="this">
            @ChildContent
        </CascadingValue>
    </div>
    

    TodoContainer.razor.cs

    [Parameter] public List<TodoItemDto> Todos { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }
    
    public TodoItemDto Payload { get; set; }  // 這個屬性用來跟蹤被用戶拖拽的待辦項
    
    /// <summary>
    /// 當子組件更新狀態時,調用父組件的該方法,改變待辦集合中被拖拽項的狀態
    /// </summary>
    /// <param name="newStatus"></param>
    public async Task UpdateJobAsync(TodoStatusEnum newStatus)
    {
        var task = Todos.SingleOrDefault(x => x.Id == Payload.Id);
    
        if (task != null)
        {
            task.Status = newStatus;
            task.ClosingDate = DateTime.Now;
            await InvokeAsync(StateHasChanged);
        }
    }
    
  • TodoList.razor

    • 使用TodoList包含 每個狀態 的待辦列表(一共三個TodoList)
    • 當用戶拖拽待辦經過列表時呈現不同的UI效果

    TodoList.razor

    <div class="todo-view-content-item-layout @_dropClass">
        <div class="todo-view-content-item">
            @* 展示第一行,標題和任務數等(代碼省略) *@
            ...
            @* 展示第二行,添加欄(代碼省略)*@
    		...
            @* 第三行展示任務列表 *@
            <div class="todo-view-content-list"
                // 調用html5關於拖拽的原生API,默認(default)阻止其它容器拖拽項目,(preventDefault)則允許容器進行拖拽放置
                 ondragover="event.preventDefault();" 
                // 啟用FireFox瀏覽器對拖放的支持
                 ondragstart="event.dataTransfer.setData('', event.target.id);"
                // 當用戶進行放置操作時調用HandleDrop方法
                 @ondrop="HandleDrop"
                // 當用戶進入該list范圍時調用HandleDragEnter來判斷能不能放置
                 @ondragenter="HandleDragEnter"
                // 當用戶離開list范圍后將dropClass置為默認狀態
                 @ondragleave="HandleDragLeave">
    
                @foreach (var todo in Todos)
                {
                    <TodoDisplay TodoItem="todo"/>
                }
            </div>
        </div>
    </div>
    

    TodoList.razor.cs

    [CascadingParameter] TodoContainer Container { get; set; }
    [Parameter] public TodoStatusEnum ListStatus { get; set; }
    [Parameter] public TodoStatusEnum AllowedStatuses { get; set; }
    
    List<TodoItemDto> Todos = new List<TodoItemDto>();
    private string _dropClass = "";  // 用來判斷當前list能否放置
    
    protected override void OnParametersSet()
    {
        Todos.Clear();
        Todos.AddRange(Container.Todos.Where(x => x.Status == ListStatus));
    }
    
    private void HandleDragEnter()
    {
        if (ListStatus == Container.Payload.Status) return;
    
        if (AllowedStatuses != Container.Payload.Status)
        {
            _dropClass = "can-drop";
        }
    }
    
    private void HandleDragLeave()
    {
        _dropClass = "";
    }
    
    private async Task HandleDrop()
    {
        _dropClass = "";
    
        if (AllowedStatuses == Container.Payload.Status) return;
    
        await Container.UpdateJobAsync(ListStatus);
    }
    
  • TodoDisplay.razor

    • 這個組件用來展示單個待辦
    • 當它被拖拽的時候就通知父組件(TodoContainer)跟蹤它

    該組件代碼邏輯很簡單,但是html的代碼量過多就不展示了

TodoDisplay.razor

<div class="draggable" draggable="true" title="@TodoItem.Title" @ondragstart="@(() => HandleDragStart(TodoItem))">
	// 內容(待辦展示)...
</div>

TodoDisplay.razor.cs

[CascadingParameter] TodoContainer Container { get; set; }
[Parameter] public TodoItemDto TodoItem { get; set; }

/// <summary>
/// 通過每次drag開始將Container的Payload賦值以跟蹤被拖拽的待辦項
/// </summary>
/// <param name="selectedJob"></param>
private void HandleDragStart(TodoItemDto selectedJob)
{
    Container.Payload = selectedJob;
}

使用

從TodoView.razor中進行調用

@page "/todo"
@attribute [Authorize]

<div class="todo-layout">
    <TodoViewTitle></TodoViewTitle>
    <TodoContainer Todos="Todos">
        <TodoList ListStatus="TodoStatusEnum.Todo" AllowedStatuses="TodoStatusEnum.Todo"/>
        <TodoList ListStatus="TodoStatusEnum.InProgress" AllowedStatuses="TodoStatusEnum.InProgress"/>
        <TodoList ListStatus="TodoStatusEnum.Completed" AllowedStatuses="TodoStatusEnum.Completed"/>
    </TodoContainer>
</div>

@code {

    List<TodoItemDto> Todos = new List<TodoItemDto>
    {
        new TodoItemDto()
        {
            Id = 1,
            Title = "開發大創項目右側狀態欄",
            Description = "無",
            IsImportant = true,
            RepeatTimes = 1,
            Tag = "工作",
            ClosingDate = DateTime.Now
        },
        new TodoItemDto()
        {
            Id = 12,
            Title = "每日背單詞",
            Description = "無",
            IsImportant = true,
            RepeatTimes = 1,
            Tag = "學習",
            ClosingDate = DateTime.Now
        },
        new TodoItemDto()
        {
            Id = 11,
            Title = "AAAAAAAAAA",
            Description = "無",
            IsImportant = true,
            RepeatTimes = 1,
            Tag = "工作",
            ClosingDate = DateTime.Now
        },
        new TodoItemDto()
        {
            Id = 111,
            Title = "BBBBBBBBBBBB",
            Description = "無",
            IsImportant = true,
            RepeatTimes = 1,
            Tag = "工作",
            ClosingDate = DateTime.Now
        },
        new TodoItemDto()
        {
            Id = 2,
            Title = "CCCCCCCCCCCCCCCC",
            Description = "無",
            IsImportant = true,
            RepeatTimes = 1,
            Tag = "工作",
            ClosingDate = DateTime.Now
        }
    };
}

實現效果如下:

image-20211104210632441

進行拖拽...

image-20211104210703150

拖拽成功!

image-20211104210724878


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM