2023年4月19日 星期三

觀察 Razor Directive 做了甚麼事情?

觀察 Razor Directive 做了甚麼事情?

Razor 是 ASP.NET Core 中用來撰寫動態網頁的標記語法,它可以讓我們在 HTML 中嵌入 C# 程式碼,並在伺服器端執行,透過這樣機制來設計出來的網站服務,當瀏覽器端需要取得一個使用 Razor 語法設計的網頁時候,此時,在後端伺服器上便會開始執行該網頁上的 C# 程式碼,將執行後的結果與當前的 HTML 結合,如此,便會看到一個動態、具有最新狀態的網頁了,因此,當後端伺服器的網頁上有了 Razor 語法這樣優秀的能這樣優秀的能力之後,就不再是單純的靜態內容網站了。

Razor Directive 指示詞是一種特殊的 Razor 標記,以 @ 符號開頭,後面接一個保留的關鍵字,用來控制 Razor 編譯器的行為或設定 Razor 頁面的屬性。若是採用傳統採的 MVC 與 Razor Page 方式開發出來的網站服務,在這裡所用到的 Razor 語法,將會與採用 Blazor UI 工具集開發方式所用到的 Razor Component / Blazor Component 組件做法,這兩種情境下所使用到的 Razor 語法將會有一點點差異,可是,絕大部分的用法都是相同的,而在這一系列的文章中,將會假設是在使用 Blazor Component 下來進行開發,並且介紹 Razor Syntax 語法在 Blazor Component 內的使用方式。

Razor 指示詞是一種用於控制 Razor 引擎的語法元素,可以影響頁面或元件的行為或輸出。一般用法將會是加上 @ 標記的 Razor ,緊接著會使用相關保留關鍵字,這樣將會形成 Razor 指示詞分為 [指示詞] 和 [指示詞屬性] 這兩大類,更多這方面的資訊,可以參考 https://learn.microsoft.com/zh-tw/aspnet/core/blazor/components/

  • 指示詞

    將原有的 HTML 標記宣告用法,切換到 C# 程式語言下來執行特定工作。 例如, @page 指示詞會指定具有路由範本的可路由元件,而且可以透過使用者在瀏覽器中的特定 URL 要求直接連線。這裡列出可以使用的保留字:attribute,code,functions,implements,inherits,inject,layout,namespace,page,preservewhitespace,using

  • 指示詞屬性

    將會是由隱含運算式所表示,其具有符號後面的 @ 保留關鍵字。這裡列出可以使用的保留字:attributes,bind,bind:culture,on{EVENT},on{EVENT}:preventDefault,@on{EVENT}:stopPropagation,key,ref,typeparam

  • 不適用於 Razor Component(.razor) 的指示詞

    這裡列出僅能夠在 Razor Page(.cshtml) 下可以使用的保留字:model,section,addTagHelper,removeTagHelper,tagHelperPrefix

現在來了解有沒有使用指示詞,對於所指定 HTML 內容會有甚麼影響?

建立 Blazor Wasm 專案,觀察 Razor Directive 做了甚麼事情?

為了要了解甚麼是 Razor Component,現在來由 Blazor 專案範本建立一個專案,實際觀察 Razor Component 樣貌。

  • 打開 Visual Studio 2022 IDE 應用程式

  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕

  • 在 [建立新專案] 對話窗右半部

    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [Web]
  • 在中間的專案範本清單中,找到並且點選 [Blazor WebAssembly 應用程式] 專案範本選項

    此專案範本可用於建立在 WebAssembly 上執行的 Blazor 應用程式,並可以選擇是否要裝載在 ASP.NET Core 應用程式上。此範本可供搭載豐富動態使用者介面(UI)的 Web 應用程式使用。

  • 點選右下角的 [下一步] 按鈕

  • 在 [設定新的專案] 對話窗

  • 找到 [專案名稱] 欄位,輸入 MB002 作為專案名稱

  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項

  • 點選右下角的 [下一步] 按鈕

  • 現在將會看到 [其他資訊] 對話窗

  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 7.0 (標準字詞支援)

  • 在這個練習中,不需要勾選 [ASP.NET Core 託管] 這個檢查盒控制項

  • 在這個練習中,不需要勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

  • 請點選右下角的 [建立] 按鈕

稍微等候一下,這個主控台專案將會建立完成

建立一個 Hello World 文字的 Razor Component

一旦這個專案成功建立之後,將會看到 Visual Studio 2022 這個工具顯示在螢幕上,從 Visual Studio 2022 中看到如下圖的 [方案總管] 視窗

*滑鼠專案節點內的 [Pages] 資料夾

  • 從彈出功能表清單內,點選 [加入] > [Razor 元件]

  • 當出現 [新增項目] 對話窗視窗

  • 在該對話窗下方的 [名稱] 文字輸入盒內,輸入 Hello.razor

    對於一個 Razor Component 元件檔案名稱,必須第一個字母要為大寫

  • 點選該對話窗右下方的 [新增] 按鈕

現在,可以從 [方案總管] 視窗內的 [Pages] 資料夾內,看到有一個 [Hello.razor] 項目建立起來

在 Visual Studio 2022 內,將會看到一個新的 [Hello.razor] 視窗出現,在這個檔案內僅會有一個 HTML 標記 : <h3>Hello</h3> 和一個 code 指示詞 : @code { },如底下螢幕截圖

  • 按下 [Shift] + [f6] 或者

  • 使用滑鼠右擊專案節點,從彈出功能表中點選 [建置]

  • 建置成功之後

  • 點選 [方案總管] 視窗最上方的 [顯示所有檔案] 按鈕,以便可以看到這個專案內的所有檔案,包含自動生成與建置後的所有檔案

  • 依序展開專案節點內的 [obj] > [debug] > [net7.0]

  • 在 [net7.0] 節點內,將會看到如底下螢幕截圖

在 .NET 6 之後,對於 .razor 這裡 Razor 元件檔案的處理方式,都是透過了 來源產生器 Source Generators 來根據這個 .razor 檔案內容,產生出一個與該檔案名稱相同的 C# 類別;可是,整個產生最終內容沒有明碼可以看到,必須透過 .NET IL 反組譯器來看到。

在這裡將會在這個專案設定檔案內(也就是 MB002.csproj 這個檔案),加入一個參數宣告,如此,便可以從 [obj] 目錄下看到編譯器對於 [Hello.razor] 檔案做了甚麼處理。

  • 滑鼠雙擊 [MB002] 專案節點
  • 底下剛剛建立好的這個專案設定檔案內容
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

	<PropertyGroup>
		<TargetFramework>net7.0</TargetFramework>
		<Nullable>enable</Nullable>
		<ImplicitUsings>enable</ImplicitUsings>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.4" />
		<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.4" PrivateAssets="all" />
	</ItemGroup>

</Project>
  • 找到 <PropertyGroup> 節點
  • 在這個節點內加入 <UseRazorSourceGenerator>false</UseRazorSourceGenerator> 宣告
  • 現在編譯器將不會透過 [來源產生器] 來產生這個 Razor 元件的 C# 類別檔案
  • 完成後的專案設定檔案如下
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

	<PropertyGroup>
		<TargetFramework>net7.0</TargetFramework>
		<Nullable>enable</Nullable>
		<ImplicitUsings>enable</ImplicitUsings>

		<UseRazorSourceGenerator>false</UseRazorSourceGenerator>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.4" />
		<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.4" PrivateAssets="all" />
	</ItemGroup>

</Project>
  • 使用滑鼠右擊專案節點,從彈出功能表中點選 [重建]

  • 重新建置成功之後

  • 依序展開專案節點內的 [obj] > [debug] > [net7.0]

  • 在 [net7.0] 節點內,將會看到如底下螢幕截圖

  • 切換到 [Razor] > [Pages] 資料夾內

  • 現在將會看到一個 [Hello.razor.g.cs] 檔案

  • 這就是編譯器依據 [Hello.razor] 檔案所編譯後產生的新類別檔案

  • 滑鼠雙擊 [Hello.razor.g.cs] 這個檔案,將會看到如下內容

#pragma checksum "C:\Vulcan\Projects\MB002\MB002\Pages\Hello.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "d2c60b8b619b27c5cc4f0f7f0e8b23df05fb127b"
// <auto-generated/>
#pragma warning disable 1591
namespace MB002.Pages
{
    #line hidden
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using System.Net.Http;

#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using System.Net.Http.Json;

#line default
#line hidden
#nullable disable
#nullable restore
#line 3 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Forms;

#line default
#line hidden
#nullable disable
#nullable restore
#line 4 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Routing;

#line default
#line hidden
#nullable disable
#nullable restore
#line 5 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Web;

#line default
#line hidden
#nullable disable
#nullable restore
#line 6 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Web.Virtualization;

#line default
#line hidden
#nullable disable
#nullable restore
#line 7 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.WebAssembly.Http;

#line default
#line hidden
#nullable disable
#nullable restore
#line 8 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.JSInterop;

#line default
#line hidden
#nullable disable
#nullable restore
#line 9 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using MB002;

#line default
#line hidden
#nullable disable
#nullable restore
#line 10 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using MB002.Shared;

#line default
#line hidden
#nullable disable
    public partial class Hello : global::Microsoft.AspNetCore.Components.ComponentBase
    {
        #pragma warning disable 1998
        protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.AddMarkupContent(0, "<h3>Hello</h3>");
        }
        #pragma warning restore 1998
    }
}
#pragma warning restore 1591

在這個 [Hello.razor.g.cs] 檔案的第 82 行,將會看到編譯器產生了一個部分類別,這個部分類別的名稱為 [Hello],這正是這個 Razor Component 元件的名稱(請參考下圖中的編號1區塊)

這個新建立的 [Hello] 類別內,僅有一個 [BuildRenderTree] 方法,這個方法被觸發的時機將會是這個 Razor 元件有需要更新到最新狀態的時候,這也就是 Razor 元件所支援資料綁定的運作方式(請參考下圖中的編號2區塊)

最後,當 [BuildRenderTree] 方法被觸發之後,將會依據原先 Razor 元件內的 HTML 內容,使用 [__builer] 物件,產生出這些 HTML 內容

加入 Razor Directive 指示詞,觀察 Hello 類別的變化

  • 再度回到 [Hello.razor] 視窗下
  • 加入一些 Razor 指示詞到這個 Razor 元件內
  • 請將底下 Razor 內容,替換掉原先檔案內的內容
@page "/hello"
@inject IJSRuntime JSRuntime

<h3>Hello</h3>
<p>@Now</p>

@code {
    DateTime Now = DateTime.Now;
}

這裡使用了 @page , @inject , @Now , @code 這四個指示詞,因為他們都是使用 @ 開頭的。

  • 使用滑鼠右擊專案節點,從彈出功能表中點選 [重建]
  • 重新建置成功之後
  • 依序展開專案節點內的 [obj] > [debug] > [net7.0] > [Razor] > [Pages] 資料夾內
  • 現在將會看到一個
  • 這就是編譯器依據最新 [Hello.razor] 檔案所編譯後產生的新類別檔案
  • 滑鼠雙擊 [Hello.razor.g.cs] 這個檔案,將會看到如下內容
#pragma checksum "C:\Vulcan\Projects\MB002\MB002\Pages\Hello.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "24c8d7f4cbc6e2e31aac2d131d20ee5f30716c7e"
// <auto-generated/>
#pragma warning disable 1591
namespace MB002.Pages
{
    #line hidden
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components;
#nullable restore
#line 1 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using System.Net.Http;

#line default
#line hidden
#nullable disable
#nullable restore
#line 2 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using System.Net.Http.Json;

#line default
#line hidden
#nullable disable
#nullable restore
#line 3 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Forms;

#line default
#line hidden
#nullable disable
#nullable restore
#line 4 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Routing;

#line default
#line hidden
#nullable disable
#nullable restore
#line 5 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Web;

#line default
#line hidden
#nullable disable
#nullable restore
#line 6 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.Web.Virtualization;

#line default
#line hidden
#nullable disable
#nullable restore
#line 7 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.AspNetCore.Components.WebAssembly.Http;

#line default
#line hidden
#nullable disable
#nullable restore
#line 8 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using Microsoft.JSInterop;

#line default
#line hidden
#nullable disable
#nullable restore
#line 9 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using MB002;

#line default
#line hidden
#nullable disable
#nullable restore
#line 10 "C:\Vulcan\Projects\MB002\MB002\_Imports.razor"
using MB002.Shared;

#line default
#line hidden
#nullable disable
    [global::Microsoft.AspNetCore.Components.RouteAttribute("/hello")]
    public partial class Hello : global::Microsoft.AspNetCore.Components.ComponentBase
    {
        #pragma warning disable 1998
        protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.AddMarkupContent(0, "<h3>Hello</h3>\r\n");
            __builder.OpenElement(1, "p");
#nullable restore
#line (5,5)-(5,8) 24 "C:\Vulcan\Projects\MB002\MB002\Pages\Hello.razor"
__builder.AddContent(2, Now);

#line default
#line hidden
#nullable disable
            __builder.CloseElement();
        }
        #pragma warning restore 1998
#nullable restore
#line 7 "C:\Vulcan\Projects\MB002\MB002\Pages\Hello.razor"
       
    DateTime Now = DateTime.Now;

#line default
#line hidden
#nullable disable
        [global::Microsoft.AspNetCore.Components.InjectAttribute] private IJSRuntime JSRuntime { get; set; }
    }
}
#pragma warning restore 1591

在這裡將會把一些註解移除掉,並且僅留下有類別宣告的程式碼,讓整個程式碼比較好閱讀

namespace MB002.Pages
{
    public partial class Hello : global::Microsoft.AspNetCore.Components.ComponentBase
    {
        protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.AddMarkupContent(0, "<h3>Hello</h3>\r\n");
            __builder.OpenElement(1, "p");
            __builder.AddContent(2, Now);

            __builder.CloseElement();
        }
       
        DateTime Now = DateTime.Now;
        [global::Microsoft.AspNetCore.Components.InjectAttribute]   
        private IJSRuntime JSRuntime { get; set; }
    }
}

從上面精簡後的程式碼可以看出,這裡產生一個 partial 部分 Hello 類別,這個類別內有兩個欄位,一個是 Now 另外一個是 JSRuntime,其中對於後者,JSRuntime,這個欄位,將會透過相依性注入方式來取得這個物件。

可以看出原先在 @code{ } 內的程式碼,其實就是這個類別的相關成員宣告的地方。

對於 [BuildRenderTree] 方法內,為了因應現在的 [Hello.razor] Blazor Component 檔案內,多了一些 Razor 指示詞與 HTML 標記,因此,這個方法多了一些程式碼:

這個 __builder.AddMarkupContent(0, "<h3>Hello</h3>\r\n"); 與之前相同,所以,沒有變化

因為在新的 Razor 元件內加入了 <p>@Now</p> ,所以,將會產生底下的三行程式碼

__builder.OpenElement(1, "p");
__builder.AddContent(2, Now);
__builder.CloseElement();

這三行程式碼可以從方法名稱看的出來要做到甚麼事情,首先,建立一個開放的 p 標籤,接著,設定這個 p 標籤的內容值為 Now 這個 C# 物件內容值,最後將這個 p 標籤封閉起來。

結論

從以上的內容與操作過程,可以得到,所謂的 Razor 指示詞,就是可以用於插入到 HTML 標記語言的一種擴充用法,這個 Razor 元件檔案,將會透過 Razor 編譯器或者來源產生器轉換成為一個 C# 類別,對於這些 Razor 指示詞,將會成為該類別內的成員或者轉換成為 C# 敘述或者表示式,在 [BuildRenderTree] 方法內來呼叫。 






沒有留言:

張貼留言