Blazor - ルーティング

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

Blazorでは、URLパターンとコンポーネントを紐付けることでルーティングを実現する。

Blazorのルーティングは、@pageディレクティブを使用して設定する。
このように定義することにより、/counterというURLにアクセスした時にこのコンポーネントが表示される。

※注意
ファイル名は大文字で始まる必要があり、razorファイルの拡張子は.razorの必要がある。

 // Pages/Counter.razorファイル
 
 @page "/counter"
 @page "/counter/{currentCount:int}"
 
 <h1>Counter</h1>
 
 <p>Current count: @currentCount</p>
 
 @code {
    // オプションのルートパラメータ
    [Parameter]
    public int currentCount { get; set; }
 }



ルーティングファイル

Blazorでは、Pagesディレクトリに配置されたrazorファイルがルーティングの対象となる。

以下に示す/counterの場合では、デフォルトでは以下に示すファイルが参照される。

  • /<プロジェクトのトップディレクトリ>/Pages/Counter.razorファイル


 @page "/counter"
 
 <h1>Counter</h1>


ただし、これは規約ベースのルーティングであり、@pageディレクティブで明示的に別のルートを指定することも可能である。

以下に示すように設定することにより、異なるファイル配置でも/counterにルーティングすることができる。

 // 例 : /Pages/MyCounter/Index.razorファイル
 
 @page "/counter"


Pagesディレクトリ配下のファイルへのルーティング

  • /counter --> /Pages/Counter.razor


また、ネストされたディレクトリの場合は以下のようになる。

  • /mypage --> /Pages/MyPage.razor
  • /admin/dashboard --> /Pages/Admin/Dashboard.razor
  • /settings/profile --> /Pages/Settings/Profile.razor


特殊なケースとして、Indexページは以下のようになる。

  • / --> /Pages/Index.razor
  • /admin --> /Pages/Admin/Index.razor


なお、@pageディレクティブで明示的にルートを指定した場合は、この規約が上書きされる。


レンダーフラグメント

レンダーフラグメントは、UIの一部を表すデリゲート型であり、コンポーネント間でコンテンツを受け渡すための仕組みである。
これは、JSXのchildren propやReactのprops.childrenに相当する概念である。

また、@Bodyは、このレンダーフラグメントの特別な実装である。

 [Parameter]
 public RenderFragment CustomContent { get; set; }
 
 // 使用例
 <div>
    @CustomContent
 </div>


レンダーフラグメントは、Blazorにおける重要な型であり、以下に示す用途で使用される。

 // LayoutComponentBaseクラスでの定義
 public RenderFragment Body { get; set; }
 
 // カスタムコンポーネントでの使用例
 [Parameter]
 public RenderFragment ChildContent { get; set; }
 
 // テンプレート用途での使用例
 [Parameter]
 public RenderFragment<TItem> ItemTemplate { get; set; }


@Body

@Bodyは、レイアウトコンポーネント内でページコンテンツを表示する場所を指定するプレースホルダーである。

@Bodyは、レイアウトを使用する各ページのコンテンツが配置される場所を示す。

例えば、以下に示すようなページコンポーネントが存在する場合

 // MainLayout.razorファイル
 
 <div class="page">
    <div class="sidebar">
       <NavMenu />
    </div>
    <main>
       <div class="content">
          @Body
       </div>
    </main>
 </div>


 // Pages/Counter.razorファイル
 
 @page "/counter"
 
 <h1>Counter</h1>
 <p>Current count: @currentCount</p>
 <button @onclick="IncrementCount">Click me</button>
 
 @code {
    private int currentCount = 0;
    private void IncrementCount() => currentCount++;
 }


上記のCounter.razorファイルの内容が、MainLayout.razorファイルの@Body部分に展開されて表示される。

 // MainLayout.razorファイル
 
 <div class="page">
    <div class="sidebar">
       <NavMenu />
    </div>
    <main>
       <div class="content">
          // ここに、Counter.razorファイルの内容が展開される
          <h1>Counter</h1>
          <p>Current count: 0</p>
          <button>Click me</button>
       </div>
    </main>
 </div>



パラメータの受け渡し

ルートパラメータは波括弧で囲んで指定して、対応するプロパティに[Parameter]属性を付与する。

Blazorのパラメータ受け渡しには、3つの方法がある。

ルートパラメータ

 @page "/user/{Id:int}"
 
 @code {
    [Parameter]
    public int Id { get; set; }
 
    // 整数以外の値が渡された場合は404エラー
 }


複数パラメータ

 @page "/orders/{CustomerId:int}/{OrderId:int}"
 
 @code {
    [Parameter]
    public int CustomerId { get; set; }
 
    [Parameter]
    public int OrderId { get; set; }
 }


クエリパラメータ

 // URL: /search?query=blazor&page=1
 
 @page "/search"
 @inject NavigationManager Navigation
 
 @code {
    private string SearchQuery;
    private int Page;
 
    protected override void OnInitialized()
    {
       var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
 
       if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("query", out var query))
       {
          SearchQuery = query;
       }
 
       if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("page", out var page))
       {
          Page = Convert.ToInt32(page);
       }
    }
 }


制約パターンの使用

制約パターンも使用できる。

 @page "/date/{date:datetime}"
 @page "/products/{id:guid}"
 @page "/blog/{*slug}"  // catch-allパラメータ


カスケード

これらのパラメータは、コンポーネント間でのカスケード時にも使用可能である。

 <ChildComponent CustomerId="@CustomerId" />



ナビゲーション

プログラムによる画面遷移とリンクによる遷移の2つの方法がある。

NavigationManagerを使用したプログラムによる遷移

 @inject NavigationManager Navigation
 
 @code {
    private void Navigate()
    {
       // 基本的な遷移
       Navigation.NavigateTo("counter");
 
       // 強制的なページリロード
       Navigation.NavigateTo("counter", forceLoad: true);
 
       // 相対パスでの遷移
       Navigation.NavigateTo("counter", forceLoad: false);
 
       // クエリパラメータを含む遷移
       Navigation.NavigateTo($"search?query={Uri.EscapeDataString(searchTerm)}");
 
       // 戻る
       Navigation.NavigateTo(Navigation.Uri);
    }
 
    // URLの変更を検知
    protected override void OnInitialized()
    {
       Navigation.LocationChanged += LocationChanged;
    }
 
    private void LocationChanged(object sender, LocationChangedEventArgs e)
    {
       // URLが変更された時の処理
    }
 }


NavLinkコンポーネントを使用したリンクによる遷移

NavLinkコンポーネントを使用して遷移する。

 // 基本的な使用方法
 <NavLink href="counter">Counter</NavLink>
 
 // アクティブ時のスタイル指定
 <NavLink href="counter" ActiveClass="active">Counter</NavLink>
 
 // Match属性による一致条件の指定
 <NavLink href="users" Match="NavLinkMatch.Prefix">Users</NavLink>



オプションパラメータ / クエリパラメータ

パラメータ処理により、以下に示すようなURLに対応することができる。

  • /products
    カテゴリなし
  • /products/1
    カテゴリID指定
  • /products?search=keyboard
    検索キーワード指定
  • /products/1?search=keyboard&page=2
    全てのパラメータ指定


オプションパラメータ

波括弧内に?を付加することにより、実現できる。

 @page "/user/{id?}"                     // idパラメータはオプション (省略可能)
 @page "/blog/{year:int?}/{month:int?}"  // 年月も省略可能
 
 @code {
    [Parameter]
    public string Id { get; set; }
 
    [Parameter]
    public int? Year { get; set; }
 
    [Parameter]
    public int? Month { get; set; }
 
    protected override void OnInitialized()
    {
       // パラメータが省略された場合のデフォルト値設定
       Id    ??= "default";           // Idが未指定の場合は、"default"を設定
       Year  ??= DateTime.Now.Year;   // 年が未指定の場合は現在の年を設定
       Month ??= DateTime.Now.Month;  // 月が未指定の場合は現在の月を設定
    }
 }


クエリパラメータ

 @page "/search"
 @inject NavigationManager NavigationManager  // URLの操作に必要なサービスの注入
 
 @code {
    // クエリパラメータを格納するディクショナリ
    private Dictionary<string, string> QueryParameters;
 
    protected override void OnInitialized()
    {
       // 現在のURLからクエリパラメータを解析
       var uri = new Uri(NavigationManager.Uri);
       QueryParameters = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(uri.Query)
                         .ToDictionary(p => p.Key, p => p.Value.ToString());
    }
 
    // 検索条件を更新してページ遷移を行うメソッド
    private void UpdateSearch(string query, int page)
    {
       // クエリパラメータの設定
       var parameters = new Dictionary<string, string>
       {
          { "q", query },              // 検索キーワード
          { "page", page.ToString() }  // ページ番号
       };
 
       // クエリパラメータを含むURLを生成して遷移
       var url = QueryHelpers.AddQueryString("/search", parameters);
       NavigationManager.NavigateTo(url);
    }
 }


2つのパラメータの組み合わせ

 @page "/products/{categoryId?}"              // カテゴリIDはオプション
 @inject NavigationManager NavigationManager
 
 @code {
    [Parameter]
    public string CategoryId { get; set; }  // URLパラメータ
 
    // クエリパラメータから検索キーワードを取得
    private string SearchTerm => GetQueryParameter("search");
 
    // クエリパラメータからページ番号を取得 (デフォルト : 1)
    private int Page => int.Parse(GetQueryParameter("page") ?? "1");
 
    // クエリパラメータを取得するヘルパーメソッド
    private string GetQueryParameter(string key)
    {
       var uri = new Uri(NavigationManager.Uri);
       var parameters = QueryHelpers.ParseQuery(uri.Query);
 
       return parameters.TryGetValue(key, out var value) ? value.ToString() : null;
    }
 }



レイアウトの適用

Blazorのルーティングでは、レイアウト機能も重要である。
レイアウト機能により、共通のUIパーツを効率的に管理でき、ページごとに異なるレイアウトを適用することができる。

_Imports.razorファイルにおいて、デフォルトレイアウトを指定して、必要に応じて個別のコンポーネントで@layout指示子を使用して上書きすることができる。

基本的なレイアウトファイルの構造

 // MainLayout.razorファイル
 
 @inherits LayoutComponentBase
 
 <div class="page">
    <div class="sidebar">
       <NavMenu />
    </div>
 
    <main>
       <div class="content">
          @Body
       </div>
    </main>
 </div>


_Imports.razorでのデフォルトレイアウト指定

 @using System.Net.Http
 @using Microsoft.AspNetCore.Components.Forms
 @using Microsoft.AspNetCore.Components.Routing
 @using Microsoft.AspNetCore.Components.Web
 @layout MainLayout    // デフォルトレイアウトの指定


個別コンポーネントでのレイアウト上書き

 // Pages/AdminPage.razorファイル
 
 @layout AdminLayout  // このコンポーネントのみ異なるレイアウトを使用
 @page "/admin"
 
 <h1>管理画面</h1>


入れ子レイアウトの例

 // AdminLayout.razorファイル
 
 @inherits LayoutComponentBase
 
 <div class="admin-layout">
    <nav class="admin-nav">
       <AdminMenu />
    </nav>
 
    <div class="admin-content">
       @Body
    </div>
 </div>



エラーハンドリング

404エラー等のルーティングエラーは、App.razorで設定したNotFoundテンプレートによって処理される。
これにより、ユーザフレンドリーなエラー画面を表示することが可能である。

例えば、NotFoundセクションをカスタマイズすることにより、より使用しやすいエラー画面を提供することができる。

 // App.razorファイル
 
 <Router AppAssembly="@typeof(App).Assembly">
    <Found Context="routeData">
       <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
       <PageTitle>Not found</PageTitle>
       <LayoutView Layout="@typeof(MainLayout)">
          <div class="alert alert-danger">
             <h3>ジが見つかりません</h3>
             <p>申し訳ありません。要求されたペジは存在しません。</p>
             <a href="/" class="btn btn-primary">ムに戻る</a>
          </div>
       </LayoutView>
    </NotFound>
 </Router>


カスタム404ページ

 // Pages/CustomNotFound.razorファイル
 
 @inject NavigationManager NavigationManager
 
 <div class="error-container">
    <h2>404 - ジが見つかりません</h2>
    <p>リクエストされたURL: @NavigationManager.Uri</p>
    <div class="error-actions">
       <button @onclick="GoBack">前のペジに戻る</button>
       <a href="/" class="btn">ムペジへ</a>
    </div>
 </div>
 
 @code {
    private void GoBack()
    {
       NavigationManager.NavigateTo(NavigationManager.Uri);
    }
 }


上記のファイルをApp.razorファイルで使用する。

 // App.razorファイル
 
 <NotFound>
    <LayoutView Layout="@typeof(MainLayout)">
       <CustomNotFound />
    </LayoutView>
 </NotFound>



認証 / 認可

[Authorize]属性を使用することにより、特定のルートへのアクセスを制限することができる。

基本的な認証要求

 @page "/secured-page"
 
 [Authorize]
 public class SecuredPageBase : ComponentBase
 {
    // このページは認証済みユーザーのみアクセス可能
 }


特定のロールに基づく認証

 @page "/admin-page"
 
 [Authorize(Roles = "Admin")]
 public class AdminPageBase : ComponentBase
 {
    // 管理者ロールを持つユーザーのみアクセス可能
 }


複数のロールの指定

 [Authorize(Roles = "Admin,Manager")]
 
 // ポリシーベースの認証
 [Authorize(Policy = "RequireAdminRole")]

 // カスタム認証要件
 [Authorize(Policy = "MinimumAgeRequirement")]


Program.csでの認証サービスの設定

 builder.Services.AddAuthentication().AddCookie();
 
 builder.Services.AddAuthorization(options =>
 {
    options.AddPolicy("RequireAdminRole", policy => policy.RequireRole("Admin"));
 
    options.AddPolicy("MinimumAgeRequirement", policy => policy.Requirements.Add(new MinimumAgeRequirement(18)));
 });


認証状態の確認と処理

 @inject AuthenticationStateProvider AuthenticationStateProvider
 
 @code {
    private async Task<bool> IsUserAuthenticated()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
 
        return authState.User.Identity.IsAuthenticated;
    }
 }