☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
ASP.NET Core 進階

🌐 Blazor 基礎入門

📌 什麼是 Blazor?

Blazor 讓你用 C# 寫前端!不用學 JavaScript,也能打造互動式的網頁應用程式。

想像以前蓋網站就像經營一家中日合作餐廳

  • 前台(前端)說日文(JavaScript)
  • 後台(後端)說中文(C#)
  • 兩邊溝通需要翻譯(API 呼叫)

有了 Blazor,就像整間餐廳都說中文——前後台無障礙溝通,用同一種語言(C#)搞定一切!


🔀 Blazor Server vs Blazor WebAssembly

Blazor Server(伺服器模式):
┌──────────┐    SignalR    ┌──────────┐
│  瀏覽器   │ ◄──────────► │   伺服器   │
│ (只負責   │   即時連線    │ (負責所有  │
│  顯示畫面)│              │  邏輯計算) │
└──────────┘              └──────────┘
像遙控電視——遙控器(瀏覽器)按鈕,電視(伺服器)換台

Blazor WebAssembly(瀏覽器模式):
┌─────────────────────────────┐
│          瀏覽器               │
│  ┌───────┐  ┌────────────┐  │
│  │ .NET  │  │ 你的 C# 程式│  │
│  │Runtime│  │  整個跑在    │  │
│  │       │  │  瀏覽器裡   │  │
│  └───────┘  └────────────┘  │
└─────────────────────────────┘
像離線遊戲——整個程式下載到你的電腦上執行
項目 Blazor Server Blazor WebAssembly
首次載入 快(只下載少量 JS) 慢(要下載 .NET Runtime)
執行位置 伺服器 瀏覽器
離線支援 不支援 支援
伺服器負擔 高(每個使用者都佔連線) 低(邏輯在用戶端)
適用場景 企業內部系統 公開面向使用者的應用

🧩 Component 元件基礎

@* Components/Counter.razor - Blazor 元件範例 *@
@* 每個 .razor 檔案就是一個元件(像樂高積木,可以組合使用) *@

<h3>計數器</h3>

<p>目前計數:@currentCount</p>  @* 用 @ 符號顯示 C# 變數 *@

<button class="btn btn-primary"
        @onclick="IncrementCount">  @* 按鈕點擊事件綁定到 C# 方法 *@
    點我加一
</button>

@code {
    // 元件內的 C# 程式碼區塊
    private int currentCount = 0; // 計數變數

    private void IncrementCount() // 按鈕點擊時執行的方法
    {
        currentCount++; // 計數加一
        // Blazor 會自動更新畫面!不需要手動操作 DOM
    }
}

🔄 元件生命週期

@* Components/LifecycleDemo.razor - 生命週期範例 *@

@implements IDisposable  @* 實作 IDisposable 以便清理資源 *@

<h3>@Title</h3>
<p>資料:@data</p>

@code {
    [Parameter] // 標記為元件參數(像函式的參數,由父元件傳入)
    public string Title { get; set; } = ""; // 接收父元件傳來的標題

    private string data = "載入中..."; // 資料狀態

    // 1. OnInitialized:元件初始化時呼叫(像開店前的準備工作)
    protected override void OnInitialized()
    {
        Console.WriteLine("元件已初始化"); // 只在第一次建立時呼叫
    }

    // 1b. 非同步版本(適合需要呼叫 API 的情況)
    protected override async Task OnInitializedAsync()
    {
        data = await LoadDataFromApi(); // 從 API 載入資料
    }

    // 2. OnParametersSet:參數變更時呼叫(像收到新的訂單就更新菜單)
    protected override void OnParametersSet()
    {
        Console.WriteLine($"參數已更新:Title = {Title}"); // 每次參數改變都會呼叫
    }

    // 3. OnAfterRender:畫面渲染完成後呼叫(像裝潢完成後的驗收)
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender) // 只在第一次渲染後執行
        {
            Console.WriteLine("第一次渲染完成!"); // 適合做 JS Interop
        }
    }

    // 4. Dispose:元件被移除時呼叫(像關店時的清理工作)
    public void Dispose()
    {
        Console.WriteLine("元件已銷毀,清理資源"); // 取消訂閱、關閉連線等
    }

    private async Task<string> LoadDataFromApi() // 模擬 API 呼叫
    {
        await Task.Delay(1000); // 模擬延遲 1 秒
        return "資料載入完成!"; // 回傳資料
    }
}

🔗 @bind 雙向綁定

@* Components/BindingDemo.razor - 資料綁定範例 *@

<h3>雙向綁定示範</h3>

@* 雙向綁定:輸入框的值和 C# 變數自動同步(像對講機,兩邊都能講) *@
<input @bind="userName" />       @* 當輸入框改變時,userName 自動更新 *@
<p>你好,@userName!</p>         @* userName 改變時,畫面自動更新 *@

@* 指定綁定事件:oninput 表示每打一個字就更新(像即時翻譯) *@
<input @bind="searchText"
       @bind:event="oninput" />  @* 預設是 onchange(失去焦點時才更新) *@
<p>搜尋:@searchText</p>

@* 綁定到不同的資料型別 *@
<input type="number" @bind="quantity" />  @* 綁定到整數 *@
<input type="date" @bind="selectedDate" /> @* 綁定到日期 *@
<input type="checkbox" @bind="isChecked" /> @* 綁定到布林值 *@

@* 下拉選單綁定 *@
<select @bind="selectedCity">                  @* 綁定選中的值 *@
    <option value="">請選擇城市</option>       @* 預設選項 *@
    <option value="taipei">台北</option>       @* 選項 1 *@
    <option value="taichung">台中</option>     @* 選項 2 *@
    <option value="kaohsiung">高雄</option>    @* 選項 3 *@
</select>
<p>你選的城市:@selectedCity</p>

@code {
    private string userName = "世界"; // 使用者名稱
    private string searchText = "";   // 搜尋文字
    private int quantity = 1;          // 數量
    private DateTime selectedDate = DateTime.Today; // 選擇日期
    private bool isChecked = false;    // 是否勾選
    private string selectedCity = ""; // 選擇的城市
}

📡 EventCallback:父子元件溝通

@* Components/ChildButton.razor - 子元件 *@
@* 子元件像一個按鈕零件,按下去會通知父元件 *@

<button class="btn btn-success"
        @onclick="OnButtonClick">   @* 按鈕被點擊時執行 *@
    @ButtonText                     @* 顯示按鈕文字 *@
</button>

@code {
    [Parameter] // 從父元件接收按鈕文字
    public string ButtonText { get; set; } = "按我"; // 預設文字

    [Parameter] // EventCallback:當事件發生時通知父元件(像對講機呼叫總部)
    public EventCallback<string> OnClicked { get; set; } // 可以傳送字串訊息

    private async Task OnButtonClick() // 按鈕點擊處理
    {
        await OnClicked.InvokeAsync("子元件被點擊了!"); // 通知父元件,傳送訊息
    }
}
@* Pages/ParentPage.razor - 父元件 *@
@page "/parent"

<h3>父元件</h3>
<p>收到的訊息:@message</p>

@* 使用子元件,並監聽事件 *@
<ChildButton ButtonText="點擊我"
             OnClicked="HandleChildClick" />  @* 當子元件觸發事件時呼叫此方法 *@

@code {
    private string message = "(等待點擊)"; // 訊息狀態

    private void HandleChildClick(string msg) // 處理子元件的事件
    {
        message = msg; // 更新訊息
        // 畫面會自動更新,顯示新的訊息
    }
}

💉 在元件中使用依賴注入

@* Components/WeatherDisplay.razor - 使用 DI 的元件 *@
@inject HttpClient Http         @* 注入 HttpClient 服務 *@
@inject ILogger<WeatherDisplay> Logger  @* 注入日誌服務 *@

<h3>天氣預報</h3>

@if (forecasts == null) // 如果資料還沒載入
{
    <p>載入中...</p>    @* 顯示載入提示 *@
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>日期</th>
                <th>溫度 (°C)</th>
                <th>天氣</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var f in forecasts) @* 走訪每筆天氣資料 *@
            {
                <tr>
                    <td>@f.Date.ToShortDateString()</td> @* 顯示日期 *@
                    <td>@f.TemperatureC</td>             @* 顯示溫度 *@
                    <td>@f.Summary</td>                  @* 顯示天氣描述 *@
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[]? forecasts; // 天氣資料陣列

    protected override async Task OnInitializedAsync() // 元件初始化時
    {
        try
        {
            Logger.LogInformation("正在載入天氣資料..."); // 記錄日誌
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("api/weather");
            // 從 API 取得天氣資料
        }
        catch (Exception ex) // 如果載入失敗
        {
            Logger.LogError($"載入天氣資料失敗:{ex.Message}"); // 記錄錯誤
            forecasts = Array.Empty<WeatherForecast>(); // 設定為空陣列
        }
    }
}

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:在 OnInitializedAsync 中阻塞 UI 執行緒

@code {
    // ❌ 錯誤寫法:用 .Result 阻塞(像在高速公路上停車等人)
    protected override void OnInitialized()
    {
        var data = Http.GetFromJsonAsync<Data[]>("api/data").Result; // 阻塞 UI!
        // 整個頁面會凍結,使用者什麼都不能做
    }
}
@code {
    // ✅ 正確寫法:使用 async/await(像預約好時間再去取餐)
    protected override async Task OnInitializedAsync()
    {
        var data = await Http.GetFromJsonAsync<Data[]>("api/data"); // 非阻塞
        // 頁面會先顯示「載入中...」,資料來了再自動更新
    }
}

解釋: Blazor 的 UI 渲染和你的程式碼跑在同一條線上。如果你用 .Result.Wait() 阻塞,就像在單線道上停車——所有人(包括畫面更新)都被堵住了。使用 async/await 就像設置一個等候區,不影響其他車輛通行。

❌ 錯誤 2:沒有清理資源(Dispose)

@code {
    // ❌ 錯誤寫法:訂閱了事件但沒有取消訂閱
    private Timer? _timer; // 計時器

    protected override void OnInitialized()
    {
        _timer = new Timer(UpdateTime, null, 0, 1000); // 每秒更新一次
        // 當元件被移除時,Timer 還在跑!記憶體洩漏!
    }
}
@implements IDisposable  @* 實作 IDisposable *@

@code {
    // ✅ 正確寫法:在 Dispose 中清理資源
    private Timer? _timer; // 計時器

    protected override void OnInitialized()
    {
        _timer = new Timer(UpdateTime, null, 0, 1000); // 每秒更新
    }

    public void Dispose() // 元件被移除時自動呼叫
    {
        _timer?.Dispose(); // 停止並釋放計時器
    }
}

解釋: 不清理資源就像退房時不還鑰匙——你已經離開了,但鑰匙(Timer、事件訂閱)還在佔用資源。久了就會造成記憶體洩漏(Memory Leak),整個應用程式越來越慢。

❌ 錯誤 3:忘記通知 Blazor 更新畫面

@code {
    // ❌ 錯誤情境:在非 Blazor 事件中修改狀態(像背景工作完成後)
    private string status = "等待中"; // 狀態

    protected override void OnInitialized()
    {
        var timer = new System.Threading.Timer(_ =>
        {
            status = "已更新"; // 修改了變數,但畫面不會自動更新!
            // 因為這不是 Blazor 的事件,Blazor 不知道要重新渲染
        }, null, 3000, Timeout.Infinite);
    }
}
@code {
    // ✅ 正確寫法:呼叫 StateHasChanged 通知 Blazor
    private string status = "等待中"; // 狀態

    protected override void OnInitialized()
    {
        var timer = new System.Threading.Timer(_ =>
        {
            InvokeAsync(() =>             // 切回 Blazor 的同步上下文
            {
                status = "已更新";       // 修改狀態
                StateHasChanged();        // 通知 Blazor 重新渲染畫面
            });
        }, null, 3000, Timeout.Infinite);
    }
}

解釋: Blazor 只有在自己的事件(@onclickOnInitializedAsync 等)觸發後才會自動更新畫面。如果是外部的 Timer 或背景工作修改了資料,必須用 InvokeAsync + StateHasChanged 手動通知 Blazor「嘿,資料變了,該重新畫畫面了!」。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊