【ASP.NET Core】在Blazor中獲取 HTTP 上下文資訊

2022-09-04 12:01:03

今天咱們來扯一下 Blazor 應用程式怎麼存取 HttpContext。其實這句話有坑,為了避免大夥伴們掉茅坑,老周直接說明:Blazor 是不能存取 HttpContext 的。哪怕你在服務容器中註冊了 IHttpContextAccessor 也不行,無法返回有效的上下文。

為啥?這得從 Blazor 的執行方式說起。銀河系周知,Blazor 允許用 .NET 程式碼編寫使用者端邏輯,在某種程度(注意,是某種程度)上替代「奸商」……哦不,是 JS。Javascript 雖然語法上很像C某某,但和C某某比還是差得很遠,程式碼閱讀起來總感覺XYZ。儘管不能完全替換,但能實現一部分也是很好的。Blazor 主要是照顧像老周這種屌絲程式猴,一旦來專案了,一個人承擔後臺前臺,連美工、圖片處理都要一手包。所以用寫後臺程式碼的方式寫前臺更習慣。

上面一段全是廢話!Blazor 只是在第一次執行的時候(不管是 Server 版還是 WASM 版)才會產生 HTTP 請求,之後的互動都由 Web Socket 來擔任。往上說一層就是 SignalR。說直白一點就是,它只有第一次存取伺服器才有 HttpContext,因為 Blazor 應用還沒載入。一旦它載入成功了,後面就沒 HttpContext 的事了。

結論:要取得 HttpContext 中需要的資料,只能在第一次請求時獲得,然後你想盡辦法讓這些資料傳給 Blazor 應用程式。本文老周將演示兩種方法——基本能對付過去。

 方法一:級聯引數

Blazor 需要一個HTML頁面來承載,然後 Page 之間的切換都在這個HTML頁面上進行。專案模板預設使用 Razor Pages—— 生成一個名為 _Host 的頁面(當然你改用 MVC 的檢視來載入也可以,一樣的原理)。然後使用 component 標記幫助器來載入 Blazor 應用程式。Blazor 應用是通過元件來構建的,一般會有一個名為 App 的元件,充當根元件。

<component type="typeof(App)" render-mode="ServerPrerendered" />

App 元件放個 Router,作用無非就是能找到 Blazor 應用頁面就呈現,找不到就顯示反饋。

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

其實,Blazor 頁面元素就是顯示在 RouteView 元件下面的。所以,要使用級聯引數元件,就可以把 CascadingValue 元件作為 RouteView 父級。

咱們先看一下資料的傳遞順序:

1、要讀 HttpContext 物件裡的東西,獲取資料的程式碼得在 _Host 頁面上寫;

2、在用 component 元件載入 App 元件時通過引數把資料傳遞給 App 內部;

3、App 元件內將資料傳遞給 CascadingValue;

4、各個 Blazor 頁面元件都能夠從 CascadingValue 元件中獲取到資料。

HttpContext --> _Host.cshtml --> App.razor --> CascadingValue  --> XXX

下面是實現過程:(假設我們要獲取 URL 查詢引數)

1、在 App 元件中定義名為 Version 的屬性,字串型別。要應用 Parameter 特性,說明它是一個元件引數,在 component 標記幫助器中可以傳參。

2、把 RouteView 元件放到 CascadingValue 元件下面,並讓它的值(Value)參照 Version 屬性的值。

// App.razor 檔案

<Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
        <CascadingValue Name="ver" TValue="string" Value="@Version">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </CascadingValue>
    </Found>
    <NotFound>
        <PageTitle>Not found</PageTitle>
        <LayoutView Layout="@typeof(MainLayout)">
            <p role="alert">Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code
{
    [Parameter]
    public string? Version { get; set; }
}

3、在 _Host.cshtml 檔案中(或你自己定義的 MVC 檢視檔案)中,讀取 URL 查詢中的 「v」 欄位的值,然後通過 component 元件把值傳遞 App 元件。

……
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = "_Layout";

    // 從URL引數中讀引數
    if(!HttpContext.Request.Query.TryGetValue("v",out var value))
    {
        value = "0.0.0";
    }
}

<component type="typeof(App)" render-mode="ServerPrerendered" param-Version="@value.ToString()" />

用 component 元件傳遞引數,可以用 param-* 特性,後面的 * 表示 App 元件接收引數的成員名稱,這裡是我們前面定義的 Version 屬性,直接寫成 param-Version 即可。如果屬性名為 Name,那就寫成 param-Name。

4、假設現在 Index.razor 元件要使用資料。需要在 Index 元件中定義一個屬性成員,一定要應用 CascadingParameter 特性。注意這裡 Name = "ver"。這個名字和剛才 App 元件中 CascadingValue 的 Name 是匹配的。

<p>接收到的資料:@Data</p>

@code
{
    [CascadingParameter(Name = "ver")]
    public string? Data{ get; set; }
}

要呈現 Data 屬性的內容,只需在 HTML 中參照即可。

在執行程式後,存取時加上 v=5.0.3,這個欄位的值就能傳到 Index 元件中。

 

 

二、單範例服務

這個方案是運用了依賴注入的功能,咱們定義一個類,它的屬性用於儲存引數。

    public class PassDataService
    {
        public int Key1 { get; set; }
        public string? Key2 { get; set; }
    }

然後把它註冊為單範例服務。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
……
builder.Services.AddSingleton<PassDataService>();

var app = builder.Build();

在 _Host.cshtml 檔案或你自定義的 MVC 檢視檔案中,讀出 HttpContext 中資料,然後設定到 PassDataService 範例的屬性上。

@page "/"
@namespace TestApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject PassDataService _datasv;
@{
    Layout = "_Layout";

    // 讀取URL查詢
    if(!HttpContext.Request.Query.TryGetValue("key1", out var kVal1))
    {
        kVal1 = "0";
    }
    if(!HttpContext.Request.Query.TryGetValue("key2", out var kVal2))
    {
        kVal2 = string.Empty;
    }
    // 賦值
    _datasv.Key1 = int.TryParse(kVal1, out int k1) ? k1 : 0;
    _datasv.Key2 = kVal2;
}

<component type="typeof(App)" render-mode="ServerPrerendered" />

@inject 指令可以注入需要的服務範例。

@inject <型別> <變數名>

 

在 Index 元件中,只要注入 PassDataService 服務範例,就能獲取到資料。

@page "/"
@inject PassDataService _datasv

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

<p>獲取到的資料:</p>
<div>Key1 = @_datasv.Key1</div>
<div>Key2 = @_datasv.Key2</div>

執行應用程式,在 URL 後面加上 key1 和 key2 引數。就得到傳遞的資料。