前言
我在實現一個含有待辦列表功能的頁面時,發現了一個好看的設計,它將待辦分為——“待辦”,“正在進行”,和“已完成”三種狀態,並且將待辦通過拖拽的方式在這三種狀態之間進行切換。
這種方式看起來真不錯,但是使用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
}
};
}
實現效果如下:
進行拖拽...
拖拽成功!