2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Blazor】MainLayout コンポーネント側(親子関係)にデータを反映させる

Last updated at Posted at 2023-05-13

はじめに

サーバーリプレース作業に伴い、Classic ASP で作成されたアプリケーションを Blazor で作り直しています。内部ロジック部分をある程度組み終えたので、UI 部分に取り掛かったところで不明点が出てきました。

MainLayout コンポーネント側にヘッダーコンポーネントを追加して、そこにユーザーIDとユーザー名を表示する必要があります。仮データとして値をセットした状態では表示されるんですが、本来はログイン画面後にログインしたユーザーIDとユーザー名を表示する必要があるわけです。
金曜日にネットで調べただけで半日が経って帰宅時間になってしまいました、何となくあたりのサイトを見つけて土日で調査することにしました。

これは親子コンポーネント間のデータ受け渡しということに気が付き、その観点から土曜日にネットで再度調べ直して、下記2つの方法を見つけて解決することが出来ました。

  • カスケーディングを使用してパラメーターを渡す方法
  • プロパティ変更通知で変更する方法

サンプル仕様

Counterページの[Click Me]ボタンでカウントした値を上部の[About]の左横のカウント値に反映させます。
image.png

カスケーディングを使用してパラメーターを渡す方法

カスケード

カスケードとは、もともと「連なった小さな滝」という意味があります。
Blazor では滝が流れるイメージで、上位の階層から下位の階層にパラメーターを渡すことができる仕組みがあります。
これが「カスケーディングパラメーター」です。
https://blazor-master.com/cascading-parameter/

ソースコード

ポイントとしては「CascadingValue Value="this"」です。これが自分にはすぐには思い付かなかった。
あとはプロパティセット後に、「InvokeAsync(() => StateHasChanged())」で明示的に表示を反映させます。

MainLayout.razor
@inherits LayoutComponentBase

<PageTitle>BlazorServer</PageTitle>

<CascadingValue Value="this">
    <div class="page">
        <div class="sidebar">
            <NavMenu />
        </div>

        <main>
            <div class="top-row px-4">
                <span class="pe-4">Current count: @Message</span>
                <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
            </div>
            <article class="content px-4">
                @Body
            </article>
        </main>

    </div>
</CascadingValue>

@code {
    private string message = "";

    public string Message
    {
        get => message;
        set
        {
            message = value;
            InvokeAsync(() => StateHasChanged());
        }
    }
}

CascadingValue Value="this"により、MainLayoutにインスタンスがセットされるので、Messageプロパティに値をセットします。

Counter.razor
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    [CascadingParameter]
    public MainLayout Layout { get; set; } = default!;

    private int currentCount = 0;

    protected override void OnInitialized()
    {
        Layout.Message = "0";
    }

    private void IncrementCount()
    {
        currentCount++;
        Layout.Message = currentCount.ToString();
    }
}

.NET 8の場合

App コンポーネント (Components/App.razor) にて、アプリ全体に対話型レンダリング モードをセットしないと、.NET 7と同じようには動作しませんでした。

App.razor
<body>
    <Routes @rendermode="InteractiveServer" />

プロパティ変更通知で変更する方法

INotifyPropertyChanged を継承して、プロパティの変更通知で反映します。

INotifyPropertyChanged を継承しなくても同じことが出来ます。最初はこの方法で作成したのですが、INotifyPropertyChanged で検索してみると MVVM ではよく使われている方法らしいので切り替えました。

ソースコード

StateService.cs
using System.ComponentModel;

public class StateService : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private int _currentCount;
    public int CurrentCount
    {
        get => _currentCount;
        set
        {
            if (value != _currentCount)
            {
                _currentCount = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCount)));
            }
        }
    }
}

DI (Dependency Injection)DIコンテナに登録します。

Program.cs
builder.Services.AddScoped<StateService>();

プロパティ変更イベントに「StateHasChanged())」を登録して明示的に表示を反映させます。

MainLayout.razor
@inherits LayoutComponentBase
@inject StateService Service

<PageTitle>BlazorServer</PageTitle>

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <span class="pe-4">Current count: @Service.CurrentCount</span>
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>
        <article class="content px-4">
            @Body
        </article>
    </main>

</div>

@code {
    protected override void OnInitialized()
        => this.Service.PropertyChanged += (o, e) => StateHasChanged();
}
Counter.razor
@page "/counter"
@inject StateService Service

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code{
    private int currentCount = 0;

    protected override void OnInitialized()
    {
        Service.CurrentCount = currentCount;
    }

    private void IncrementCount()
    {
        currentCount++;
        Service.CurrentCount = currentCount;
    }
}

BindableBaseクラスでより汎用的にする

ジェネリック型にすれば、より汎用的になりますよね。
下記サイトのようにINotifyPropertyChangedインタフェースを継承したBindableBaseクラスを作成して、StateServiceクラスでBindableBaseクラスを継承するように書き直しても動作しました。

ReactiveProperty.Blazorがあるよ

C#でMVVMパターンを組んで開発しようとした際に役に立つライブラリの一つにReactivePropertyがあります。そのBlazorがリリースされています。

【2023/05/14追記】
ReactiveProperty.Blazor に書き換えてみました。

injectの記述方法

ディレクティブ記法でオブジェクトをインジェクトします。

@inject 【インジェクトするオブジェクトの型】 【インジェクトする変数名】

@inject StateService Service

上記の書き方以外にコードブックにて[Inject]属性を付与する記法があります。
今後コードビハインドで記述するならこっちの方が分かりやすいです。

@code{
    [Inject]
    private StateService Service { get; set; } = default!;

結果

Counter.gif

最後に

2つの方法を提示しました。
カスケーディングを使用してパラメーターを渡す方法が分かりやすいです。ただ、プロパティ変更通知の方法は、WPF の頃からある方法なのできっとすごいメリットがあるはずです。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?