KNOWLEDGE - COLUMN ナレッジ - コラム

【エバンジェリスト・ボイス】Blazorと、JavaScript召喚の巻

先端技術部
エバンジェリスト ベロフ・ドミトリー (ディーマ)

前回は「Webの 未来? WebAssemblyとBlazor」というコラムでWebAssemblyBlazorを軽く紹介してみましたが、今回はBlazorのデモアプリを元にBlazorのプロジェクトの構成やBlazorJavaScriptの間の情報交換について説明したいと思います。前回のコラムをすでに読まれた前提で書きますので、前回のコラムの内容を忘れた方や読んでない方は読んでみてください。

 

プロジェクトの構成
さて、先ずはプロジェクトに入っている主なフォルダーとファイルを確認しましょう。

コラム_3018_1_308x630

・Pages
アプリで閲覧できるページを定義するファイルが集まっています。ページを定義する為にrazorというマークアップ構文が使われています。平たく言えば、razorHtmlC#コードを埋め込めるのを可能にする技術です。元々ASP.NETにおいてサーバー側のコードをHtmlに埋め込む為に作られました。

・Shared
アプリのページの共通部分を定義するrazorファイルが集まっています。全ページで使われているMainLayout.razorと左にあるメインメニューのNavMenu.razorがデフォルトで追加されています。 

wwwroot
HtmlCSSのような静的ファイルが集まっています。 

_Imports.razor
ファイルにおいて使うネームスペースを設定するには、C#だとusing構文を使えます。Blazorのページでは@using構文を使えます。似ているでしょう。そして、_Imports.razorでは各ページで使えるネームスペースを設定できます。

App.razor
デフォルトのルーティングが設定されています。 

・Program.cs
Mainメソッドがありますが、その中にWebAssembly上で走るアプリのホストを作成します。下記のコードについてもう少し説明したいと思います。           

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new

Uri(builder.HostEnvironment.BaseAddress) });


BlazorにはDependency Injection (DI、依存性の注入)フレームワークが搭載されていて、アプリ中には良く使われています。上記のコードはDIにクラスを登録するコードで、自分のクラスでもDIに登録できます。また、後ほどもう一つDIに登録する例を紹介します。 

 

ページの構成

続きまして、Counter.razorを例としてBlazorのページの構成を見てみましょう。 

コラム_0318_2_709x327

 

Blazorのページを大まかに3つの部分に分けられます。 

@page "/counter"


一番上にある@page構文によりブラウザーでページにアクセスできるアドレスを設定します。よってローカル端末で実行する場合、Counterページは 「http://localhost:5000/counter」 のアドレスで閲覧できます。 

@code {

    private int currentCount = 0; 

    private void IncrementCount()

    {
        currentCount++;
    }

}


一番下にある@code構文にページのロジックを実装するC#コードを定義します。このページでは表示されたカウンターがボタンのクリックで増加するため、カウンターの現在値とカウンターを増加させるメソッドが定義されています。

 
<h1>Counter</h1>
 
<p>Current count: @currentCount</p>
 
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>


真ん中には、普通のhtmlによりページのデザインが定義されています。通常のhtmlと言ってもBlazorの特別なタグも入っています。@currentCountのようにC#コードで定義されたフィールドやプロパティーにアクセス出来ます。CounterページにはcurrentCountのフィールドの現在値が表示され、そのフィールドの値が変わったらBlazorのエンジンはその変化を認識し、ページのDOMに更新すべき部分を検出し更新します。また、@onclickのようなタグでC#コードで定義されたメソッドをhtml要素のイベントハンドラーとして登録できます。このページでは、ボタンがクリックされた事でIncrementCountメソッドが走ります。

 
 

分離コード
デモアプリですので、ページなども簡単であり、htmlもC#コードも同じファイルに入っていても何の問題ないのですが、ページが大きくなればなるほどhtmlやC#コードが同じファイルにあると少し複雑になりますね。幸いな事に、ASP.NETのようにBlazorも分離コードに対応しています。そこでCounterページのC#コードをhtmlと分離しましょう。

1.Pageフォルダーにrazor.csファイルを作成します。ファイル名と格納場所は任意ですが、他の.NET技術の分離コードはこのパターンを使っているのでBlazorでもこのようにしましょう。

2.ネームスペースとしてPagesを設定します。HelloBrazorはプロジェクト名で、PagesはCounter.razorが格納されたフォルダーですのでBlazorがビルド時にCounterページに当てはまるクラスをHelloBlazor.Pagesのネームスペースに置きます。

3.クラス名としてはページのファイル名を設定して、クラスをpartialクラスとして定義します。

4.razor.csの@codeの内容をCounterクラスに移動します。 

結果のファイルは下記の通りとなります。

Counter.razor:

@page "/counter" 

<h1>Counter</h1

<p>Current count: @currentCount</p

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




Counter.razor.cs

namespace HelloBlazor.Pages
{
    partial class Counter
    {
        private int currentCount = 0;
        private void IncrementCount()
        {
            currentCount++;
        }

    }

JavaScriptとの情報交換
今は、Counterページのカウンターはページを更新するとゼロに戻ります。そのカウンターの値をブラウザーに保存してみましょう。その為にはHTML5のWeb Storageを使いたいのですが、残念な事にBlazorは(未だ)Web Storageに対応していません。しかし、恐れる事はありません。BlazorからJavaScriptを呼び出すのは簡単です。 

JavaScriptのメソッドの定義
まずはWeb Storageを使うJavaScriptのメソッドを作成しましょう。その為に、wwwroot/jsフォルダーにextensions.jsファイルを作成し、下記の内容を追記します。

// Blazorから呼び出すメソッドをblazorExtensionsオブジェクトに集める

window.blazorExtensions = {

    // Web Storageに値を保存するメソッド

    SetItem: function (key, value) {

        localStorage.setItem(key, value);

    },

    // Web Storageから値を取得するメソッド

    GetItem: function (key) {

        return localStorage.getItem(key);

    }

}


 
SetItemメソッドとGetItemメソッドをBlazorアプリから呼び出しますが、blazorExtensionsオブジェクトがページのwindowオブジェクトを追加する為にextensions.jsファイルへの参照をwwwroot/index.htmlファイルに追記します。

・・・    

    </div>

    <script src="_framework/blazor.webassembly.js"></script>

    <script src="js/extensions.js"></script>

</body>

・・・


C#のラッパーを作成
上記のJavaScriptのメソッドはBlazorのページから直接呼び出すことができますが、ページのクラスにとってJavaScriptを知っている必要はなく、必要なのはWeb Storageにアクセス出来る事です。ですのでJavaScriptを呼び出しているコードをLocalStorageクラスに集めて、DIによりそのクラスをページに注入するのが適切なやり方です。 

1.Web Storageを使うJavaScriptのメソッドを呼び出すラッパークラスを作成します。名前をLocalStorageにしましょう。

2.BlazorからJavaScriptを呼び出す為にJSInterop.IJSRuntimeというインタフェースがありますので、そのタイプのフィールドを定義します。

    private IJSRuntime js;


3.IJSRuntimeインタフェースを実装するクラスのインスタンスが、アプリの起動時にBlazorにより作成されるので改めて作成する必要はありません。DIでそのインスタンスをこのクラスに注入しましょう。(厳密に言えば、LocalStorageをDIに登録すると、DIによりLocalStorageのインスタンスが作成される際、IJSRuntimeを実装するクラスがLocalStorageのコンストラクターに注入されます)

        public LocalStorage(IJSRuntime js)
        {

            this.js = js;
        }


4.SetItemメソッドとGetItemメソッドを作成します。InvokeVoidAsyncとInvokeAsyncというIJSRuntimeのメソッドでJavaScriptのメソッドを呼び出します。

await js.InvokeVoidAsync("blazorExtensions.SetItem", key, value);


InvokeVoidAsyncメソッドは戻り値がないメソッドを呼び出し、InvokeAsyncメソッドは戻り値があるメソッドを呼び出します。パラメーターとしては、一つ目のパラメーターはJavaScriptのメソッド名で、その後は順番にJavaScriptのメソッドのパラメーターを引き渡します。

LocalStorage.csの内容は下記となります。

using System;
using System.Threading.Tasks;
using Microsoft.JSInterop; 

namespace HelloBlazor
{
    public class LocalStorage
    {
        // JavaScriptのメソッドを呼び出す為のオブジェクト
        private IJSRuntime js 

        // IJSRuntimeを実装するオブジェクトをコンストラクターの引数として設定する。
        // 実行する時、Dependency Injectionによって注入される。

        public LocalStorage(IJSRuntime js)
        {
            this.js = js;

        } 
        // JavaScriptのblazorExtensionsオブジェクトに定義されたSetItemメソッドを呼び出すメソッド。
        public async Task SetItem(String key, String value)
        {
            await js.InvokeVoidAsync("blazorExtensions.SetItem", key, value);

        } 
        // JavaScriptのblazorExtensionsオブジェクトに定義されたGetItemメソッドを呼び出し、呼び出しの結果を戻すメソッド。
        public async Task<String> GetItem(String key)
        {
            return await js.InvokeAsync<String>("blazorExtensions.GetItem", key);        
    }
}


ページでラッパーを利用
1.前に説明した通り、Blazorアプリのホスト設定はMainメソッドで行われ、DIへのオブジェクトの登録もそこd行われるのでcsに次のコードを追加します。

builder.Services.AddTransient<LocalStorage>();


2.登録したクラスのオブジェクトをCounterページに注入します。CounterページのロジックがCSファイルにあるので依存性の注入はAspNetCore.Components.Inject属性で実行できます。注意点としては、LocalStorageのクラスのようなDIに登録するクラスの場合は依存性の注入はコンストラクターのパラメーターでやりますが、ページへの依存性の注入はプロパティーでやります。

        [Inject]

        private LocalStorage storage { get; set; }


3.IncrementCountメソッドでカウンターが増加された後、その値をWeb Storageに格納します。

       await storage.SetItem("Counter", currentCount.ToString()); 


追伸、SetItemは非同期のメソッドでawaitを使うのでIncrementCountメソッドも非同期にするのを忘れないように。



        private async void IncrementCount()


4.最後にCounterページを表示する時、保存したカウンターの値をWeb Storageから取得します。その為、ページの初期化後に呼び出されるOnInitializedAsyncメソッドを使います。 

        protected override async Task OnInitializedAsync()

        {
            var countStr = await storage.GetItem("Counter");           

            if (Int32.TryParse(countStr, out var count) == true)

                currentCount = count;
        }

 

5.アプリをビルドし、実行して結果を確認しましょう。
  a. Counterページに移動して、「Click me」ボタンを何回かクリックします。
  b. ブラウザーを閉じて、改めて開いたCounterページに移動するとクリックした回数が表示されるはずです。

 20210318コラム_3_575x375

           
  c. また、開発者ツールでCounterの値がLocal Storageに格納されているのを確認できます。

 20210318コラム_4_543x371

現時点最新版のEdgeで撮ったスクリーンショットです。他のブラウザーの開発者ツールは異なる可能性があります。

 
今日はここまで…

当サイトの内容、テキスト、画像等の転載・転記・使用する場合は問い合わせよりご連絡下さい。

エバンジェリストによるコラムやセミナー情報、
IDグループからのお知らせなどをメルマガでお届けしています。

メルマガ登録ボタン

ベロフ ドミトリー

株式会社インフォメーション・ディベロプメント 先端技術部 エバンジェリスト

この執筆者の記事一覧

関連するナレッジ・コラム

地味に見えて優秀!マネージドプレフィックスリストでアドレス管理を効率化

DockerでJupyterLabの環境を作ろう

残された攻撃の痕跡を追え! ~Post-Exploitationで起きていること~