目前的 4 篇文章是來自我們寫好的假資料,但正常來說不會這樣做,而是有個按鈕讓用戶點擊了之後,增加或減少文章的數量。
增加的按鈕會放在<Blog>,點擊了「增加」按鈕產生一則新的 Post 供用戶輸入,再讓用戶點擊「確認」按鈕儲存文章。
刪除的按鈕則可以放在<Blog>,再在<Post>加入 checkbox,讓用戶自己勾選要刪除哪些 Post;或是放在<Post>,點擊刪除按鈕就刪除該篇文章。
新增按鈕很簡單,只要在<MyButton>加上@onclick事件即可,開始之前,先將版面稍作修改,順便把FontSizeStyle移除。
Blog.razor
@page "/Blog" @inherits BlogBase @if (Blog == null) {
<p>Loading...</p>
} else {
<div class="container">
<div class="row">
<div class="col pl-0">
<label>部落格名稱</label>
<input @bind-value="Blog.Name" class="form-control w-25" />
<MyButton
value="Add"
class="btn btn-info my-2"
type="button"
@onclick="Add"
/>
</div>
</div>
<div class="row">
@if (Blog.Posts != null) { foreach (var post in Blog.Posts) {
<div class="col-md-3 border rounded p-3 mr-2 mb-2 w-25">
<CascadingValue Value="ColorStyle" Name="ColorStyle" IsFixed="true">
<Post Post="post" />
</CascadingValue>
</div>
} }
</div>
</div>
}
C#部分則加入Add()方法,原來LoadData()的日誌刪除。
BlogBase.razor.cs
using BlazorServer.Models;
using Microsoft.AspNetCore.Components;
namespace BlazorServer.Pages;
public class BlogBase : ComponentBase
{
public BlogModel? Blog { get; set; }
public string? ColorStyle { get; set; } = "color: goldenrod";
public string? FontSizeStyle { get; set; } = "color: goldenrod";
protected override Task OnInitializedAsync()
{
LoadData();
return base.OnInitializedAsync();
}
private void LoadData()
{
Blog = new BlogModel
{
Id = 1,
Name = "我的部落格",
Posts = new List<PostModel>(),
CreateDateTime = new DateTime(2021, 12, 14, 23, 46, 59)
};
}
protected void Add()
{
Blog?.Posts?.Add(new PostModel());
}
}
接著點擊 Add 按鈕,就可以看到文章數量增加了。

接著來做 Delete 功能,在Post.razor加入 Delete 按鈕。

但問題來了,當我點擊 Delete 按鈕,<Blog>怎麼知道我刪除的是哪一條 Post?這時候就需要Id可以識別,於是加入一個私有變數_postId,每次點擊Add()都+1,正常來說 PostId 會跟著 Post 而不是由 Blog 產生,不過因為還沒接觸到資料庫,所以先這樣將就,後面連接資料庫後就會改變。

為了驗證是否正確,刪除原來Post.razor註解的 Post.Id,加入新樣式的 Post.Id,可以看到沒有問題。

現在有了識別 Id,又產生了新問題,要怎麼讓<Blog>收到Id?目前 Id 由<Blog>產生,所以沒這問題,等後面 Id 由資料庫產生後,<Blog>就不會知道 Id 了。前面說的都是從父元件傳遞資料到子元件的方法,我們現在要從子元件傳資料到父元件,有辦法做到反向傳回去嗎?
有的,那就是EventCallback,但是要把Delete改成<input>而非<MyButton>,因為EventCallback是由子元件傳向父元件,如果用<MyButton>,Id 的流向就必須先這樣<Blog> => <Post> => <MyButton>,接著再用EventCallback反向<MyButton> => <Post> => <Blog>,實在太麻煩了。
先把 Delete 按鈕改成<input>,加入@onclick="ReturnPostId"。

接著在PostBase.razor.cs定義類型為EventCallback<int>的屬性 GetPostId,記住一定要加上[Parameter]特性,因為這要被<Blog>呼叫。然後完整定義ReturnPostId()方法,裡面做的就是GetPostId.InvokeAsync(Post!.PostId);,當外部傳來的GetPostId被觸發時,就將Post.PostId傳給父元件也就是<Blog>。

再在BlogBase.razor.cs定義同名方法GetPostId(int id),名字不需要一樣,這邊只是為了方便取同名,裡面做的事情就是移除跟收到的 Id 有相同值的 Post。

最後在Blog.razor的<Post>的GetPostId放入剛剛定義的方法就可以了。

我們來驗證看看,先新增 4 篇文章,再刪除第 2 篇,可以看到 Id 等於 2 的那篇成功被刪除了。

除了 EventCallback,還有 Delegate 可以使用,不過局限性較大,我們也來試試看。
先在PostBase.razor.cs定義類型為Action<int>的屬性 GetPostIdForDelegate,ReturnPostId()改用GetPostIdForDelegate。

接著在Blog.razor的<Post>改用GetPostIdForDelegate。

但是實際點擊後會發現不會刪除文章,這是因為 EventCallback 會監控 Component,一旦有變化就會重新渲染,委託則不會,委託必須在父元件也就是BlogBase.razor.cs呼叫StateHasChanged();方法,讓父元件知道狀態改變了。

另外委託一旦在子元件中定義了,父元件就必須要呼叫,否則會發生錯誤,EventCallback 則沒這問題。

站長註:當然,善於使用 nullable 也可以避免這種異常:

引用:
- Blazor EventCallback
- EventCallback
- Blazor Tutorial - Ep11 - EventCallback and how it is different from delegate
註:本文程式碼通過 .NET 6 + Visual Studio 2022 重構,可點擊原文連結與重構後程式碼比較學習,謝謝閱讀,支持原作者