[Blazor][Todo][筆記]Blazor 開發範例 ToDo List

Blazor 是新一代的開發方式,可以透過雙向綁定,可以用純C#來開發Web端的SPA應用。這一篇是起手式的範例,參考官方的內容,自己加上官方沒有的完成disabled,並且加上刪除的按鈕功能。(基本上與Vue的範例類似的功能。筆記下來提供自己未來參考,也提供網友參考

 

緣起

Blazor 是新一代的開發方式,可以透過雙向綁定,可以用純C#來開發Web端的SPA應用。這一篇是起手式的範例,參考官方的內容,自己加上官方沒有的完成disabled,並且加上刪除的按鈕功能。(基本上與Vue的範例類似的功能。筆記下來提供自己未來參考,也提供網友參考。

新增專案

小喵開發的的工具是 Visual Studio 2019 ,所以操作過程與官方的說明流程會有一點點不太一樣。

我們開啟 Visual Studio 2019,新增專案的時候,選擇Blazor如下圖


接著按【下一步】

接著,設定專案名稱與路徑,如下圖

按【建立】

接著,有兩種類型的Blazor可以供選擇,我們選擇上面的那個,進階那邊暫時取消https,按下建立,如下圖

就醬子,初始的專案就建立完成。

觀察專案

我們首先來觀察一下專案,主要對比MVC的專案,所以如果對MVC有一些基本的認知,會比較容易理解這樣的專案結構是怎麼回事

首先,看看一開始建立好的專案樣子

執行起來的樣子

我們先把專案執行起來,然後再來細說這些內容在程式裡面是哪個部分

這篇文章,不會針對初始的所有項目詳細說明只會針對TodoList會動用到的做一些簡單的說明。

  1. 首先是左邊的Menu,我們會新增一個項目,可以點選到TodoList的頁面。
  2. 接著,會新增一個TodoItem的類別,用來記錄Todo的內容與是否完成。
  3. 再接著,我們要新增一個 TodoList 元件(頁面),並且大部分的程式都會寫在這個元件(頁面)。

Pages資料夾

顧名思義,這個資料夾就是放我們的頁面。不過如果您去觀察裡面相關檔案的副檔名,您會發現很多的副檔名是razor而不是cshtml。

Blazor 的副檔名 razor 是【元件(Component)】,您可以想像他是頁面中的某一部分區塊,可以單獨的一個元件運作成類似一個頁面,也可以在元件中放入多個別的元件。

我們來新增一個Todos的元件在Pages裡面:

  1. 在Pages資料夾上按右鍵,【加入】→【Razor元件】,名稱就叫做【Todos.razor】
  2. 在元件的程式碼中,最上面,新增【@page "/Todos"】來指定他的【Routing】。您沒看錯,指定Routing就這麼簡單

接著我們把專案執行起來,然後在網址的後面加上【/Todos】,就可以從首頁,切換到這一元件(頁)來。

相關內容如下:

@page "/Todos"
<h3>Todos</h3>

@code {

}

Shared 資料夾

這與MVC的View很像,會把一些共用的部分,放在這,例如MVC裡面會放 Layout 在 Shared一樣。Blazor在Shared中有個【MainLayout.razor】他的副檔名是【razor】,在Blazor裡面是屬於【Component】。

我們觀察裡面有個【NavMenu.razor】,這就是畫面中的左邊Menu,我們新增以下的程式碼,讓他多一個項目,並指向Todos

<li class="nav-item px-3">
	<NavLink class="nav-link" href="Todos">
		<span class="oi oi-list-rich" aria-hidden="true"></span> Todos
	</NavLink>
</li>

再次執行起來,Menu就可以直接切換到Todos元件(頁)。

 

Data資料夾

有點類似MVC裡面的Model,我們會把用到的資料類別,比較共用的部分,放在這裡。但是也可以建立在獨立的Component裡面,這部分要依據實際專案的需求來決定怎麼放。

我們把TodoItem這樣的類別加入到 Data 資料夾中。在Data資料夾上按右鍵,【加入】→【類別】,名稱就設定為【TodoItemInfo.cs】,相關程式碼如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace BlazorTodos.Data
{
    public class TodoItemInfo
    {
        public string Title { get; set; } = "";
        public bool Finish { get; set; } = false;
    }
}

 

雙向綁定測試

之前在開發 Web SPA 應用的時候,無論是 Angular 或者是 Vue ,他們的一大特色就是雙向綁定,可以讓 Script 的 JSON 物件與畫面中的html進行雙向綁定的設定,這簡化的在開發上的許多撰寫。

上訴的這些都是純 Client 端透過 Javascript 的一些 Framework 來做到。但是 Blazor 可是使用 C# 在 Server 端開發,他透過 SignalR 的機制,讓我們可以用純 Server 端的寫法,就可以做到 SPA 的效果,而且是雙向的綁定。這微軟的黑科技真是令人驚豔。以下就來先簡單的體驗一下:

首先,我們剛剛新增了Todos的元件,我們先在下面 Code 的地方,定義好一個private的欄位變數 newTodoTitle,是一個字串。
並且在畫面中,我們安排了一個input,設定bind到【newTodoTitle】這個欄位上。然後下方,再用Razor顯示newTodoTitle。就像以下這樣的程式碼:

<input type="text" @bind="newTodoTitle" />
<hr />
@newTodoTitle


@code {
    
    private string newTodoTitle = "abc";

}

就醬子,我們把畫面執行起來,並且試著運作看看。

您會發現到,當input裡面會直接出現預設值 abc
當你修改他的內容,然後游標跳離這個input的時候,下方的顯示資料,會直接地跟著變化。
您根本不需要去寫送回、取得資料、在把資料放回畫面中的任何相關程式碼。您只需要知道一點,他的綁定是雙向的,有異動,會自動傳回並且渲染到對應使用該欄位的相關內容。

在撰寫這類的雙向綁定,重點要放在【資料(類別物件)的異動】。去處理好資料的異動,程式中不會對html的dom去做直接的操作與修改。

有了這樣的概念後,就可以來開始撰寫Todo的相關程式碼了。

Todo相關程式碼

首先,我們在畫面上方,加上一個using的宣告,用以告知我們要引用到Data中的類別。

@using BlazorTodos.Data;

接著在Code的地方,我們Code宣告一下用來存放TodoList的物件集合

private List<TodoItemInfo> oTodos = new List<TodoItemInfo>();

然後是畫面上的安排與榜定,我們新增一個ul>li的項目,在li外撰寫Razor的foreach,把oTodos的每個項目的Title顯示出來。就像醬子

<ul>
    @foreach (var tTodo in oTodos)
    {
        <li>@tTodo.Title</li>
    }
</ul>

在input後面新增一個button的按鈕,透過【@onclick】來宣告他要運作的void函數【AddTodo】,然後在 Code 的地方撰寫 AddTodo 的內容
相關完整的程式碼如下:

@page "/Todos"
@using BlazorTodos.Data;
<h3>Todos</h3>

<input type="text" @bind="newTodoTitle" />
<button @onclick="AddTodo">AddTodo</button>
<hr />
<ul>
    @foreach (var tTodo in oTodos)
    {
        <li>@tTodo.Title</li>
    }
</ul>


@code {

    private string newTodoTitle = "abc";
    private List<TodoItemInfo> oTodos = new List<TodoItemInfo>();


    private void AddTodo()
    {
        //宣告一個todoItem
        TodoItemInfo tTodo = new TodoItemInfo();
        //將Input的內容給todoItem的Title,並設定預設的Finish是False
        tTodo.Title = newTodoTitle;
        tTodo.Finish = false;
        //把tTodo加到oTodos裡面去
        oTodos.Add(tTodo);
        //把input用到的newTodoTitle清為空字串,等待下一個Todo輸入
        newTodoTitle = "";
    }
}

就醬子,我們執行後,就可以把input輸入的內容,加到oTodos裡面,並且顯示出來。

官方的範例,大概就介紹到這邊。

但我們希望可以做更多....

  1. 我們希望可以用input text在li中顯示Todo的內容,並且可以直接修改
  2. 我們用一個Checkbox來綁上Todo的Finish,當工作完成我們就打勾
  3. 當打勾設定完成,我們希望input text設定為disabled,完成了就不能再修改
  4. 在每一項的後面,新增一個按鈕,完成的時候才能看到
  5. 可以刪除該條Todo的項目

好,願望清單開出來了,我們就來一一的實踐他。

這裡面的1~4並不會特別的難,所以這部份就如以下的程式碼即可。

@page "/Todos"
@using BlazorTodos.Data;
<h3>Todos</h3>

<input type="text" @bind="newTodoTitle" />
<button @onclick="AddTodo">AddTodo</button>
<hr />
<ul>

    @foreach (var tTodo in oTodos)
    {
        
    <li>
        <input type="checkbox" @bind="tTodo.Finish" />
        <input type="text" @bind="tTodo.Title" disabled="@tTodo.Finish" />
        @if (tTodo.Finish)
        {
            <button>X</button>
        }

    </li>
        
    }
</ul>
<hr />


@code {

    private string newTodoTitle = "abc";
    private List<TodoItemInfo> oTodos = new List<TodoItemInfo>();
    

    private void AddTodo()
    {
        //宣告一個todoItem
        TodoItemInfo tTodo = new TodoItemInfo();
        //將Input的內容給todoItem的Title,並設定預設的Finish是False
        tTodo.Title = newTodoTitle;
        tTodo.Finish = false;
        //把tTodo加到oTodos裡面去
        oTodos.Add(tTodo);
        //把input用到的newTodoTitle清為空字串,等待下一個Todo輸入
        newTodoTitle = "";
    }

    private void Del(int currentIdx)
    {
        oTodos.RemoveAt(currentIdx);
    }
}

但是第五項(刪除),是比較令人頭疼的~

首先刪除必須知道要刪除第幾個,因此我需要有一個變數來記錄目前是第幾個。
並且,由於這是雙向的綁定,所以每次跑完迴圈的結果,都會持續地記著在變數上,因此,要記得加上歸零讓他從新計算。
另外,直接拿idx來用,由於他有非同步的狀況,所以還需要另一個變數來取得當下的index
最後,要呼叫傳遞參數的函數,必須有特別的呼叫方式

最終程式碼

最後,相關的程式碼如下:

@page "/Todos"
@using BlazorTodos.Data;
<h3>Todos</h3>

<input type="text" @bind="newTodoTitle" />
<button @onclick="AddTodo">AddTodo</button>
<hr />
<ul>
    @{ 
        //每次呼叫,都歸零重新計算
        idx = 0;
    }
    @foreach (var tTodo in oTodos)
    {
        //用變數紀錄當次的index
        int currentIdx = idx;
    <li>
        <input type="checkbox" @bind="tTodo.Finish" />
        <input type="text" @bind="tTodo.Title" disabled="@tTodo.Finish" />
        @if (tTodo.Finish)
        {
            //onclick如果直接用Del(currentIdx)會有錯誤產生
            <button @onclick="@(e=>Del(currentIdx))">X</button>
        }
    </li>
        idx += 1;
    }
</ul>
<hr />


@code {

    private string newTodoTitle = "abc";
    private List<TodoItemInfo> oTodos = new List<TodoItemInfo>();
    private int idx = 0;

    private void AddTodo()
    {
        //宣告一個todoItem
        TodoItemInfo tTodo = new TodoItemInfo();
        //將Input的內容給todoItem的Title,並設定預設的Finish是False
        tTodo.Title = newTodoTitle;
        tTodo.Finish = false;
        //把tTodo加到oTodos裡面去
        oTodos.Add(tTodo);
        //把input用到的newTodoTitle清為空字串,等待下一個Todo輸入
        newTodoTitle = "";
    }

    private void Del(int currentIdx)
    {
        oTodos.RemoveAt(currentIdx);
    }
}

 

以上小喵自己記錄筆記,並有需要的網友參考

^_^

補充

上面的這個是沒有套用視覺的範例,以下這邊小喵套用了Bootstrap的相關設定的範例,也筆記一下,同時提供網友們參考

@page "/todos"

<div class="container">
    <div class="row">
        <div class="col-sm-1"></div>
        <div class="col-sm-10">
            <div class="card border-primary mb-3">
                <div class="card-header">
                    <h3>Todos</h3>
                </div>
                <div class="card-body">
                    <div class="row">
                        <div class="col-10">
                            <input type="text" placeholder="請輸入Todo的標題" class="form-control" @bind="newTodoTitle" />
                        </div>
                        <div class="col-2">
                            <button class="btn btn-primary" @onclick="AddTodo">Add Todo</button>
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-12">
                            <hr />
                        </div>
                    </div>
                    @{ 
                        idx = 0;
                    }
                    @foreach (var tTodo in oTodos)
                    {
                        int currentIdx = idx;
                        <div class="row">
                            <div class="col-1">
                                <input type="checkbox" class="form-control" @bind="tTodo.Finish" value="" />
                            </div>
                            <div class="col-10">
                                <input type="text" class="form-control @(tTodo.Finish? "border-success":"border-danger")" @bind="tTodo.Title" disabled="@tTodo.Finish" />
                            </div>
                            <div class="col-1">
                                <button class="btn" @onclick="@(e=>Del(currentIdx))" hidden="@(!tTodo.Finish)">X</button>
                            </div>
                        </div>
                        idx += 1;
                    }

                </div>
            </div>
        </div>
        <div class="col-sm-1"></div>
    </div>
</div>

@code {
    public class TodoItemInfo
    {
        public string Title { get; set; } = "";
        public bool Finish { get; set; } = false;
    }

    private string newTodoTitle = "";
    private List<TodoItemInfo> oTodos = new List<TodoItemInfo>();
    private int idx = 0;

    private void AddTodo()
    {
        TodoItemInfo tTodo = new TodoItemInfo();
        tTodo.Title = newTodoTitle;
        tTodo.Finish = false;
        oTodos.Add(tTodo);
        newTodoTitle = "";
    }

    private void Del(int currentIdx)
    {
        oTodos.RemoveAt(currentIdx);
    }
}

另外,小喵在練習的過程中,當遇到一些問題,不知道怎麼撰寫時,可以透過關鍵詞搜尋找一些網路的範例,大部分的文章是英文的,例如想知道click時要傳參數,於是就搜尋【blazor onclick pass parameter】,就找到相關的做法。

目前Blazor在中文的文章偏少一些,建議網友們,如果有需要找一些資訊時,建議用英文找,會比較容易找到解答。

 

 


以下是簽名:


Microsoft MVP
Visual Studio and Development Technologies
(2005~2019/6)