【ASP.NET Core】【Blazor Server】SPA を試す
はじめに
今回は Blazor を使って Single Page Application を作ってみますよ。
ということでルーティングやコンポーネントの中にコンポーネントを作る、といったところを試します。
ルーティング
まずはルーティングから。
「localhost:5000/」と「localhost:5000/{ページ名}」にアクセスしたときに、 Blazor のページが表示されるようにしてみます。
HomeController.cs
... [Route("/")] [Route("/{page}")] public ActionResult OpenPage(string page) { return View("Views/_Host.cshtml"); } ...
_Host.cshtml は Blazor のルーターを呼んでるだけです。
_Host.cshtml
@using BlazorSample.Views; @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
App.razor
@using BlazorSample.Views.Shared; <Router AppAssembly="@typeof(Startup).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <p>Sorry, there's nothing at this address.</p> </NotFound> </Router>
MainLayout.razor は Blazor の共通レイアウトです。 Razor でいうところの _Layout.cshtml ですね。
MainLayout.razor
@inherits LayoutComponentBase @Body
SearchPage.razor
@page "/SearchPage"; <input type="text" @bind="productName"> <button @onclick="UpdateValue">Update</button> @code{ public string productName = ""; public async Task UpdateValue() { productName = "Hello World!"; } }
Blazor では、「@page」で表示する画面のパスが決まります。
なので上記の SearchPage は 「localhost:5000/SearchPage」で表示できます。
- ASP.NET Core Blazor のルーティング | Microsoft Docs
- ASP.NET Core Razor コンポーネントの作成と使用 | Microsoft Docs
- アプリのプロジェクト構造 Blazor | Microsoft Docs
_Layout.cshtml
現状 SearchPage の構造はこんな感じになってます。
ただ、他に Razor のページがないのであれば、「ViewStart.cshtml」を削除して、「Layout.cshtml」の中身を「_Host.cshtml」にまとめてしまうこともできます。
_Host.cshtml
@using BlazorSample.Views; <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>@ViewData["Title"]</title> <base href="/"> <script type="text/javascript"> if (/MSIE \d|Trident.*rv:/.test(navigator.userAgent)) { document.write('<script src="https://polyfill.io/v3/polyfill.min.js?features=Element.prototype.closest%2CIntersectionObserver%2Cdocument.querySelector%2Cfeatures=Array.prototype.forEach%2CNodeList.prototype.forEach"><\/script>'); document.write('<script src="js/blazor.polyfill.min.js"><\/script>'); } </script> </head> <body> <div>Hello Host</div> @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered)) <script src="_framework/blazor.server.js"></script> </body> </html>
共通のレイアウト、ということでは「MainLayout.razor」に書くこともできるのですが、(少なくともデフォルトでは) Blazor ファイルに <script> を書くことができないため、 JavaScript については「Layout.cshtml」や「Host.csthml」に書く必要があります。
ViewData
「ViewData」や「ViewBag」も Blazor から扱うことができないため、 ViewData["Title"] は Controller クラスや Razor で設定する必要があります。
HomeController.cs
... [Route("/")] [Route("/{page}")] public ActionResult OpenPage(string page) { ViewData["Title"] = GetTitle(page); return View("Views/_Host.cshtml"); } private string GetTitle(string page) { switch(page) { case "SearchPage": return "Search"; default: return "Home"; } } ...
ルーターからコンポーネントにデータを渡す
前回 Razor から Blazor コンポーネントにデータを渡すのに「[Parameter]」を使っていました。
ではルーターは?と思ったら、 Razor と Blazor でのデータのやり取りと同じ方法で渡すことができました。
App.razor
@using BlazorSample.Views.Shared; <Router AppAssembly="@typeof(Startup).Assembly"> <Found Context="routeData"> @{ var values = routeData.RouteValues as Dictionary<string, object> ?? new Dictionary<string, object>() ; values.Add("Name", "Hello World"); var newRouteData = new RouteData(routeData.PageType, values); } <RouteView RouteData="@newRouteData" DefaultLayout="@typeof(MainLayout)" /> </Found> ... </Router>
上記のような感じで、ページが見つからない場合にデフォルトのページを表示する、ということもできます。
@using BlazorSample.Views.Shared; <Router AppAssembly="@typeof(Startup).Assembly"> ... <NotFound> @{ var routeData = new RouteData(typeof(SearchPage), new Dictionary<string, object>()); } <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </NotFound> </Router>
Blazor コンポーネントの中に Blazor コンポーネントを追加する
Angular ではコンポーネントの中に子供のコンポーネントを持たせることができます。
では Blazor ではどうかというと、ほぼ同じ感じで扱うことができます。
SearchPage.razor
... @foreach (var item in Products) { <SearchResultRow Product=item></SearchResultRow> } ...
SearchResultRow.razor
@using Models; <div class="search_result_row"> <div class="search_result_row_id">@Product.Id</div> <div class="search_result_row_name">@Product.Name</div> </div> @code{ [Parameter] public Product Product {get; set; } }
Blazor(HTML + C#) から C# のコードを分割する
小さいコンポーネントならこれまでと同じく HTML と C# が同じファイルにあっても問題ないのですが、複雑かつ大きくなってくると、両者を分けたくなってきます。
これを partial class を使うことで実現できます。
SearchPage.razor.cs
using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Models; namespace BlazorSample.Views { public partial class SearchPage { [Inject] public Services.IBlazorService Blazor{ get; set; } [Parameter] public string Name {get; set;} public string productName = ""; public List<Product> Products = new List<Product> { new Product { Id = 0, Name = "Hello", }, new Product { Id = 1, Name = "World", }, }; public async Task UpdateValue() { var product = await Blazor.GetMessageAsync("Hello"); Console.WriteLine(productName); Console.WriteLine(Name); productName = product.Name; } } }
これで SearchPage.razor から C# のコードを取り除くことができます。
SearchPage.razor
@page "/SearchPage"; <input type="text" @bind="productName"> <button @onclick="UpdateValue">Update</button> @foreach (var item in Products) { <SearchResultRow Product=item></SearchResultRow> }
一つ重要なのが、両者の namespace を同じにする、ということがあります。
(SearchPage.razor.cs の namespace を Views にしていたためコンパイルエラーになり、ちょっとハマりました)