Blazor tutorial for beginners - part 1

以 Blazor Server App 入門教學為主。

引言

為此 Blazor Fundamentals Tutorial 教學系列影片(於2019年第四季)練習與筆記。 發現另一個 Blazor tutorial for beginners 教學系列影片(於2021年第一季)。 這一套也可以 Blazor C# Tutorials 教學系列影片(於2020年五月)。

Blazor server-side vs client-side (WebAssembly) | What should you choose?

Blazor WASM App

Blazor Server App

Pros

(優點)

No need for a server side

No need for JavaScript

Offline support

No need for JavaScript

Small size, fast loading

You are using .NET Core

Debug like a boss

Runs on any browser

Your code stats on the server

Each browser session is an open SignalR connection

Cons

(弱點)

Big donwload size

Requires WebAssembly

Less mature runtime

Debugging is not great

Still in preview

Your DLLs are downloaded

You need a server-side

No offline support

Highher latency, worse UX

Hard to maintain and scale Scaling SignalR in non-Azure fashion is pretty hard

What are Razor Components? | Blazor Tutorial 1

Requirements

  1. C# 9.0

  2. Razor Pages (.cshtml)

React Component

  • Can be

    • Nested

    • Reused

    • Packaged

  • Each component has its own state.

  • Components can have Parameters.

How to supply parameters to the component?

Razor components support Data Binding

We can bind object to DOM elements and have object changes manipulate the DOM to reflect those changes in the state.

  • OnInitialized(), OnInitializedAsync()

    • 元件初始化

  • OnParmetersSet(), OnParametersSetAsync()

    • 元件參數值已變更。

    • 在 render 之前,在 initalize 之後。

    • 在父層刷新子層元件時。

  • OnAfterRender(bool firstRender), OnAfterRenderAsync(bool firstRender)

    • UI 已更新。

人工通知 StateHasChanged 一般來說 Blazor State 被更新時會自動刷新UI,不過有時無法自動判定這時可用StateHasChanged()指令通知元件其狀態已變更請立即刷新UI。

Components can be disposable

@implements IDisposable

Razor 元件入門範例

ParentComponent.razor
<style>
  .title {
     font-size:2em;
  }  
</style>

<h4 class="title">Parent Component</h4>
<div class="alert alert-success">
    @AlertText
</div>

<ChildComponent>我是次要訊息</ChildComponent>

@code{
    [Parameter]
    public string AlertText { get; set; }
}
ChildComponent.razor
<h4>Child Component</h4>
<div class="alert alert-info">
    @ChildContent
</div>

<div class="alert alert-@alertTheme">
    <label>
        <input type="checkbox" @bind="f_darkTheme" /> Toggle dark theme
    </label>
</div>

@code{
    bool f_darkTheme;
    private string alertTheme => f_darkTheme ? "dark" : "light";

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    protected override void OnInitialized()
    {
        f_darkTheme = true;
    }
}

Timer 範例

TimerCounter.razor
@page "/timer-counter"
@implements IDisposable ///------ ※ disposable
@using System.Timers

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;
    private Timer timer;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        /// ※ timer 的初始化在 firstRender 執行
        if (firstRender)
        {
            timer = new Timer();
            timer.Interval = 1000;
            timer.Elapsed += OnTimerInterval;
            timer.AutoReset = true;
            // Start the timer
            timer.Enabled = true;
        }        
    }
    
    public void Dispose()
    {
        /// ※ 別忘記要釋放 timer 否則會變成無人管理的流浪資源。
        timer?.Dispose();
        // During prerender, this component is rendered without calling OnAfterRender and then immediately disposed
        // this mean timer will be null so we have to check for null or use the Null-conditional operator ? 

    }
    
    private void OnTimerInterval(object sender, ElapsedEventArgs e)
    {
        IncrementCount();
    }
    
    private void IncrementCount()
    {
        currentCount++;
        Console.WriteLine($"Count incremented: {currentCount}");
    }
}

補充:Blazor Server App 啟動順序

以下為 Blazor App 在 .NET5 版本的啟動順序,.NET6 把 Startup.cs 併入 Program.cs 原則上啟動順序是一樣的。

  1. Program.cs

    1. system entry door.

    2. load appsettings.json.

  2. Startup.cs

    1. register service

    2. configure wetsite.

  3. _Host.cshtml

    1. import website resource and assets.

  4. App.razor

    1. authorization

    2. page routing

  5. MainLayout.razor

    1. ready to run.

補充:Blazor Component 完整說明

完整的官方文件說明 Blazor Component (官方名稱:Razor Component),為必讀文章。

Blazor Component 完整說明
ASP.NET Core Blazor 6.0 完整說明

Quoting Introduction to Razor Pages in ASP.NET Core:

Razor Pages can make coding page-focused scenarios easier and more productive than using controllers and views.

As you can see on tree structure, a razor page is a cshtmlfile (template) plus acs` file (behavior). The page is rendered as html and send to navigator.

Exists another kind of apps, blazor. Quoting Introduction to ASP.NET Core Blazor:

Blazor is a framework for building interactive client-side web UI with .NET

Important term "interactive", not only render html, is a language to make page interactive (no just render html on server and send it to client)

請直接參考附件。

ASP.NET Core Razor component lifecycle

補充:ASP.NET Core Blazor 資料系結(data-binding)

請直接參考附件。

Events up 範例

Props Down, Events Up現在依然是前端開發的父子層基本溝通機制。 在Blazor App中 Props Down的實作就是用Parameter傳遞參數到子元件。 Events Up的實作則是 EventCallback<TValue> 的傳遞與向上invoke。 這裡只展示events up 的範例。

@page "/foo-page"
@inject IJSRuntime jsr

<MyEvent OnMyMessage=HandleMyMessage />

@code{
    void HandleMyMessage(MyMessage msg)
    {
        jsr.InvokeVoidAsync("alert", $"MyMessage:{msg.code}:{msg.msg}");
    }
}
MyEvent.razor
<button @onclick="HandleClick">送出我的訊息</button>

@code{
    @* 接受父層的EventCallback event 參數 *@
    [Parameter] public EventCallback<MyMessage> OnMyMessage { get; set; }

    void HandleClick()
    {
        MyMessage msg = new() { 
            code = 6,
            msg = "我很讚"
        };
        
        @* events up:將訊息往上送給父層 *@
        OnMyMessage.InvokeAsync(msg);
    }
}

共用資源

MyMessage.cs
// resource
public class MyMessage
{
    public int code { get; set; }
    public string msg { get; set; }
}

Data-Binding 範例程式碼

雙向繫結範例

@* 基本 binding *@
<input @bind="value" />
<p>@value</p>

@code{
    string value = "123";
}

雙向繫結(2-way binding)運作原理除依循「Properties down, events up」原則外還加入一些其他規則。

@* 進一步設定 binding *@

<input @bind="value" @bind:event="onchange"/>
<code>@value2</code>

@* 可以把event從預設的 onchange 換成其他 oninput 等等。
<input @bind="value" @bind:event="oninput" /> 
*@ 

@code{
    string value = "123";
}

自訂輸入元件 Data-Binding

FooPage.razor
@page "/foo-page"
@* 自訂繫結範例 *@

@* 自訂繫結支援 2-way binding 語法 *@
<MyInput @bind-MyValue=value1 />

@* 也支援 1-way binding 語法 *@
<MyInput MyValue=value2 MyValueChanged=HandleMyValueChanged />

<p>@value1, @value2</p>

@code{
    // State
    decimal value1;
    decimal value2;

    void HandleMyValueChanged(decimal value)
    {
        value2 = value;
    }
}
MyInput.razor
<input type="number" value="@MyValue" @onchange="HandleChange" />

@code{
    @* ※自訂繫結的參數為一對,有命名規則,event的名稱需是field名稱加'Changed' *@
    [Parameter] public decimal MyValue { get; set; } = 0m;
    [Parameter] public EventCallback<decimal> MyValueChanged { get; set; }

    void HandleChange(ChangeEventArgs e)
    {
        MyValueChanged.InvokeAsync(e.Value == null ? 0m : decimal.Parse((string)e.Value));
    }
}

Dependency Injection | Blazor Tutorial 2

Dependency Injection (DI)

MVC6 引入DI 機制為基礎框架之一。用一句話說明:讓 service可以隨時隨地 @inject

DI 物件的生命週期 (※必考題)

  • Transient (短暫的)

    • The classs is instantiated once per service resolution.

    • 每當service被解譯使用時建構一次。

    • 用於數據交易,如:database transaction。

  • Singleton (唯一的)

    • The class is instantiated once for the whole application.

  • Scoped (有範圍的)

    • The class is instantiated once per scope.

    • scope? 與其引用者生命週期一致。

    • DI 物件生命週期預設是 scoped。

    • scoped in WebAssembly Blazor works like Singleton.

常用的 service

  • HttpClient

    • used to call APIs

  • IJSRuntime

    • used in JS Interop

  • NavigationManager

    • used in navigation and routing

一個簡單的 Service 註冊與 @inject 範例

Showcase.razor
@page "/showcase"
@inject RandomService randomSvc /// 插入 RandomService

<div class="alert alert-danger">
    My random id is: @randomSvc.RandomId
</div>

@code{
  ...
}
RandomService.cs
using System;

namespace BlazorServerTutor.Services
{
    public class RandomService
    {
        public Guid RandomId => Guid.NewGuid(); 
    }
}
Startup.cs
using ...
using BlazorServerTutor.Services;
namespace BlazorServerTutor
{
    public class Startup
    {
        ...
        public void ConfigureServices(IServiceCollection services)
        {
            ...
            // 註冊 service
            services.AddScoped<RandomService>();
            //services.AddSingleton<RandomService>();
            //services.AddTransient<RandomService>();            
            ...
        }
        ...
    }
}

補充:ASP.NET Core - Dependency Injection

關於註冊 service 至少要一門專篇,DI 編程又是另一門專篇,故不在入門期深入討論。@inject 的標的不只是 Razor Component, 也可以是一般的 Class,injection 的插入點可以是 constructor(建構式), method(成員函式), property(成員屬性) 都可以。

What are Blazor Layouts? | Blazor Tutorial 3

Everything starts with the Default Layout

In order to turn a componnet into a Layout you need to: 1) Inhert from LayoutComponentBase 2) Use the @Body parameter

Blazor Server App 啟動執行順序 (※必考題)

  • Program.cs

    • 載入appsettings.json

  • Startup.cs

    • ConfigureServices(),註冊 service

    • Configure(),網站啟動程序

  • Pages/_Host.cshtml

    • 載入網頁靜態資源,如: MudBlazor, Bootstrap 5 等等

  • App.razor

    • 導引以可以動態載入資源

    • routing to @page

  • Shared/MainLayout.razor

    • default layout

    • @Body

MainLayout 預設程式碼

MainLayout.razor
@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>
    <div class="main">
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>
        <div class="content px-4">
            @Body
        </div>
    </div>
</div>

可在@page指定layout。

Showcase.razor
@page "/showcase"
@layout NoLayout  //------ 可在Page指定layout
@inject RandomService randomSvc 

<h1>Showcase</h1>

@code{
  ...
}

Routing and Navigation | Blazor Tutorial 4

Page Routing --- 直接讀碼

Counter.razor
@page "/counter/{StartingCount:int?}"   ///←--- 有參數的URL

<h1>Counter</h1>
<p>展示 @page routing。</p>

<p class="my-counter">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
Counter.razor.cs
using Microsoft.AspNetCore.Components;
namespace BlazorServerTutor.Pages
{
    public partial class Counter
    {
        /// 此參數將由URL或上層元件取得後更新。
        [Parameter]
        public int StartingCount { get; set; } = 0;  ///←--- 元件外部參數

        private int currentCount; ///←--- 元件內部狀態

        protected override void OnInitialized()
        {
            currentCount = StartingCount; ///←--- 依外部參數初始化
        }

        private void IncrementCount()
        {
            currentCount++;
        }
    }
}

routing 詳情請直接看參考資料。

  • <NavLink/> 元件

  • @inject NavigationManager

返回首頁範例

@inject NavigationManager navMan

<button class="btn btn-primary" @onclick="MoveToHome" >
    返回首頁
</button>

@code{
    private void MoveToHome() 
    {
        navMan.NavigateTo("/"); /// 返回首頁。
    }
}

Last updated