Blazor イベント ハンドラーを使用して、C# コードを DOM イベントにアタッチする
ほとんどの HTML 要素では、ページの読み込みの完了、ユーザーによるボタンのクリック、HTML 要素の内容の変更など、何か重要なことが起きたときにトリガーされるイベントが公開されています。 アプリでは、次のいくつかの方法でイベントを処理できます。
- アプリでは、イベントを無視できます。
- アプリでは、JavaScript で記述されたイベント ハンドラーを実行して、イベントを処理できます。
- アプリでは、C# で記述された Blazor イベント ハンドラーを実行して、イベントを処理できます。
このユニットでは、3 番目のオプション (イベントを処理するために C# で Blazor イベント ハンドラーを作成する方法) について詳しく説明します。
Blazor と C# を使用してイベントを処理する
Blazor アプリの HTML マークアップの各要素では、多くのイベントをサポートしています。 これらのイベントのほとんどは、標準の Web アプリケーションで使用できる DOM イベントに対応していますが、コードの記述によってトリガーされるユーザー定義のイベントを作成することもできます。 Blazor でイベントをキャプチャするには、イベントを処理する C# メソッドを記述してから、Blazor ディレクティブを使ってイベントをメソッドにバインドします。 DOM イベントの場合、Blazor ディレクティブでは、@onkeydown
や @onfocus
などの同等の HTML イベントと同じ名前を共有します。 たとえば、Blazor Server アプリを使って生成されるサンプル アプリの Counter.razor ページには、次のコードが含まれています。 このページには、ボタンが表示されます。 ユーザーがボタンを選択すると、ボタンのクリック回数を示すカウンターを増分する IncrementCount
メソッドが @onclick
イベントでトリガーされます。 カウンター変数の値は、ページの <p> 要素によって表示されます。
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
多くのイベント ハンドラー メソッドは、追加のコンテキスト情報を提供するパラメーターを受け取ります。 このパラメーターは、EventArgs
パラメーターと呼ばれます。 たとえば、@onclick
イベントでは、ユーザーがどのボタンをクリックしたか、ボタンのクリックと同時に Ctrl や Alt などのボタンを押したかどうかに関する情報を MouseEventArgs
パラメーターで渡します。 メソッドを呼び出すときに、このパラメーターを指定する必要はありません。Blazor ランタイムによって自動的に追加されます。 このパラメーターは、イベント ハンドラーでクエリを実行できます。 次のコードでは、ボタンのクリックと同時にユーザーが Ctrl キーを押した場合に、前述の例に示したカウンターを 5 つずつ増分します。
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount(MouseEventArgs e)
{
if (e.CtrlKey) // Ctrl key pressed as well
{
currentCount += 5;
}
else
{
currentCount++;
}
}
}
その他のイベントでは、さまざまな EventArgs
パラメーターが用意されています。 たとえば、@onkeypress
イベントでは、ユーザーが押したキーを示す KeyboardEventArgs
パラメーターを渡します。 どの DOM イベントにも、この情報が不要な場合は、イベント処理メソッドから EventArgs
パラメーターを省略できます。
JavaScript でのイベント処理と Blazor を使用したイベント処理について
従来の Web アプリケーションでは、JavaScript を使用してイベントをキャプチャし、処理します。 HTML <script> 要素の一部として関数を作成し、イベントが発生したときにその関数を呼び出すように配置します。 上で示した Blazor の例と比較するため、次のコードでは、ユーザーが [Click me] ボタンを選ぶたびに値を増やして結果を表示する、HTML ページのフラグメントが示されています。 このコードでは、jQuery ライブラリを使用して DOM にアクセスします。
<p id="currentCount">Current count: 0</p>
<button class="btn btn-primary" onclick="incrementCount()">Click me</button>
<!-- Omitted for brevity -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
var currentCount = 0;
function incrementCount() {
currentCount++;
$('#currentCount').html('Current count:' + currentCount);
}
</script>
イベント ハンドラーの 2 つのバージョンの構文上の違いに加えて、次の機能の違いに注意してください。
- JavaScript では、イベントの名前の先頭に
@
記号が付きません。これは Blazor ディレクティブではありません。 - Blazor コードでは、イベント処理メソッドをイベントにアタッチするときに、メソッドの名前を指定します。 JavaScript では、イベント処理メソッドを呼び出すステートメントを記述し、丸かっこと必要なパラメーターを指定します。
- 最も重要なのは、JavaScript イベント ハンドラーがクライアントのブラウザーで実行されることです。 Blazor Server アプリを構築している場合、Blazor のイベント ハンドラーはサーバー上で実行され、イベント ハンドラーが完了すると、UI に加えられた変更のみがブラウザーで更新されます。 また、Blazor のメカニズムでは、イベント ハンドラーは、セッション間で共有される静的データにアクセスできます。JavaScript モデルではアクセスできません。 ただし、
@onmousemove
などの頻繁に発生する一部のイベントの処理では、サーバーへのネットワーク ラウンドトリップが必要になるため、ユーザー インターフェイスの動作が遅くなる可能性があります。 ブラウザーでのこのようなイベントは、JavaScript を使って処理する方がよい場合があります。
重要
イベント ハンドラーから JavaScript コードを使用し、C# Blazor コードを使用して、DOM を操作できます。 ただし、Blazor は DOM の独自のコピーを保持しており、必要に応じてそれを使ってユーザー インターフェイスを更新します。 JavaScript と Blazor コードを使用して DOM 内の同じ要素を変更すると、DOM が破損し、Web アプリのデータのプライバシーとセキュリティが侵害される可能性があります。
イベントを非同期に処理する
既定では、Blazor イベント ハンドラーは同期されます。 イベント ハンドラーが、Web サービスの呼び出しなど、長時間実行される可能性のある操作を実行すると、イベント ハンドラーを実行するスレッドは、操作が完了するまでブロックされます。 これにより、ユーザー インターフェイスの応答が低下する可能性があります。 これに対処するには、イベント ハンドラー メソッドを非同期として指定します。 C# async
キーワードを使用します。 メソッドでは、Task
オブジェクトを返す必要があります。 次に、イベント ハンドラー メソッド内で await
演算子を使用して、実行時間の長いタスクを別のスレッドで開始し、他の作業のために現在のスレッドを解放できます。 実行時間の長いタスクが完了すると、イベント ハンドラーが再開されます。 次のイベント ハンドラーの例では、時間のかかるメソッドを非同期に実行します。
<button @onclick="DoWork">Run time-consuming operation</button>
@code {
private async Task DoWork()
{
// Call a method that takes a long time to run and free the current thread
var data = await timeConsumingOperation();
// Omitted for brevity
}
}
Note
C# での非同期メソッドの作成について詳しくは、「非同期プログラミングのシナリオ」をご覧ください。
イベントを使用してフォーカスを DOM 要素に設定する
HTML ページでは、ユーザーは要素間をタブ移動できます。フォーカスでは、ページに HTML 要素が表示される順序で自然に移動します。 場合によっては、この順序をオーバーライドし、ユーザーに特定の要素へのアクセスを強制する必要があります。
このタスクを実行する最も簡単な方法は、FocusAsync
メソッドを使用します。 これは、ElementReference
オブジェクトのインスタンス メソッドです。 ElementReference
では、フォーカスを設定する項目を参照する必要があります。 @ref
属性で要素参照を指定し、同じ名前がコード内にある C# オブジェクトを作成します。
次の例では、<button> 要素の @onclick
イベント ハンドラーで、フォーカスを <input> 要素に設定しています。 <input> 要素の @onfocus
イベント ハンドラーは、要素がフォーカスを取得すると、"Received focus" (フォーカスを取得した) というメッセージを表示します。 <input> 要素は、コード内の InputField
変数を介して参照されます。
<button class="btn btn-primary" @onclick="ChangeFocus">Click me to change focus</button>
<input @ref=InputField @onfocus="HandleFocus" value="@data"/>
@code {
private ElementReference InputField;
private string data;
private async Task ChangeFocus()
{
await InputField.FocusAsync();
}
private async Task HandleFocus()
{
data = "Received focus";
}
次の図は、ユーザーがボタンを選択した場合の結果を示しています。
Note
アプリでは、エラーの発生後にユーザーに入力の変更を求めるなど、特定の理由で特定のコントロールのみにフォーカスを設定する必要があります。 フォーカスの設定を使って、ページ上の要素間を一定の順序で移動するようユーザーに強制しないでください。こうすると、要素を見直して入力を変更したいユーザーをかなりイライラさせる可能性があります。
インライン イベント ハンドラーを記述する
C# では、ラムダ式がサポートされています。 ラムダ式を使用すると、匿名関数を作成できます。 ラムダ式は、ページまたはコンポーネントの他の場所で再利用する必要がない簡易イベント ハンドラーがある場合に便利です。 このユニットの開始時に示したクリック数の最初の例では、IncrementCount
メソッドを削除し、代わりに、メソッド呼び出しを同じタスクを実行するラムダ式に置き換えることができます。
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="() => currentCount++">Click me</button>
@code {
private int currentCount = 0;
}
Note
ラムダ式のしくみについて詳しくは、「ラムダ式と匿名関数」をご覧ください。
この方法は、イベント処理メソッドに他の引数を指定する場合にも役立ちます。 次の例の HandleClick
メソッドは、通常のクリック イベント ハンドラーと同じ方法で MouseEventArgs
パラメーターを受け取りますが、文字列パラメーターも受け入れます。 メソッドでは、以前の同様にクリック イベントを処理しますが、ユーザーが Ctrl キーを押したというメッセージも表示します。 ラムダ式では、HandleCLick
メソッドを呼び出し、MouseEventArgs
パラメーター (mouseEvent
) と文字列を渡します。
@page "/counter"
@inject IJSRuntime JS
<h1>Counter</h1>
<p id="currentCount">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick='mouseEvent => HandleClick(mouseEvent, "Hello")'>Click me</button>
@code {
private int currentCount = 0;
private async Task HandleClick(MouseEventArgs e, string msg)
{
if (e.CtrlKey) // Ctrl key pressed as well
{
await JS.InvokeVoidAsync("alert", msg);
currentCount += 5;
}
else
{
currentCount++;
}
}
}
Note
この例では、Blazor に同等の関数がないため、JavaScript の alert
関数を使ってメッセージを表示します。 JavaScript 相互運用機能を使用して、Blazor コードから JavaScript を呼び出します。 この手法の詳細については、別のモジュールで扱います。
イベントの既定の DOM アクションをオーバーライドする
いくつかの DOM イベントには、そのイベントに使用できるイベント ハンドラーの有無に関係なく、イベントの発生時に実行される既定のアクションがあります。 たとえば、<input> 要素の @onkeypress
イベントでは、ユーザーが押したキーに対応する文字が常に表示され、キー入力が処理されます。 次の例では、@onkeypress
イベントを使用して、ユーザーの入力を大文字に変換します。 さらに、ユーザーが @
文字を入力すると、イベント ハンドラーによってアラートが表示されます。
<input value=@data @onkeypress="ProcessKeyPress"/>
@code {
private string data;
private async Task ProcessKeyPress(KeyboardEventArgs e)
{
if (e.Key == "@")
{
await JS.InvokeVoidAsync("alert", "You pressed @");
}
else
{
data += e.Key.ToUpper();
}
}
}
このコードを実行して @
キーを押すと、アラートが表示されますが、@
文字も入力に追加されます。 @
文字の追加は、イベントの既定のアクションです。
この文字が入力ボックスに表示されない場合は、次のように、イベントの preventDefault
属性で既定のアクションをオーバーライドできます。
<input value=@data @onkeypress="ProcessKeyPress" @onkeypress:preventDefault />
イベントはそれでも起動しますが、イベント ハンドラーによって定義されたアクションのみが実行されます。
DOM の子要素の一部のイベントは、その親要素のイベントをトリガーできます。 次の例では、<div> 要素に @onclick
イベント ハンドラーが含まれます。 <div> 内の <button> には独自の @onclick
イベント ハンドラーがあります。 さらに、<div> には <input> 要素が含まれています。
<div @onclick="HandleDivClick">
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<input value=@data @onkeypress="ProcessKeyPress" @onkeypress:preventDefault />
</div>
@code {
private async Task HandleDivClick()
{
await JS.InvokeVoidAsync("alert", "Div click");
}
private async Task ProcessKeyPress(KeyboardEventArgs e)
{
// Omitted for brevity
}
private int currentCount = 0;
private void IncrementCount(MouseEventArgs e)
{
// Omitted for brevity
}
}
アプリが実行され、<div> 要素によって占有されている領域の要素 (または空の領域) をユーザーがクリックすると、HandleDivClick
メソッドが実行され、メッセージが表示されます。 ユーザーが Click me
ボタンを選択すると、IncrementCount
メソッドが実行され、その後に HandleDivClick
が続き、@onclick
イベントが DOM ツリーに伝達されます。 <div> が @onclick
イベントも処理する別の要素の一部である場合、そのイベント ハンドラーも DOM ツリーのルートなどに対して実行されます。 次に示すように、イベントの stopPropagation
属性を使って、イベントのこのような上方への拡散を抑制できます。
<div @onclick="HandleDivClick">
<button class="btn btn-primary" @onclick="IncrementCount" @onclick:stopPropagation>Click me</button>
<!-- Omitted for brevity -->
</div>
EventCallback を使用してコンポーネント間でイベントを処理する
Blazor ページには、1 つ以上の Blazor コンポーネントを含めることができます。コンポーネントは親子リレーションシップで入れ子にすることができます。 EventCallback
を使うことにより、子コンポーネントのイベントで、親コンポーネントのイベント ハンドラー メソッドをトリガーできます。 コールバックは、親コンポーネント内のメソッドを参照します。 子コンポーネントでは、コールバックを呼び出してメソッドを実行できます。 このメカニズムは、delegate
を使用して、C# アプリケーションのメソッドを参照する場合と似ています。
コールバックでは、1 つのパラメーターを受け取ることができます。 EventCallback
はジェネリック型です。 型パラメーターでは、コールバックに渡される引数の型を指定します。
たとえば、次のシナリオを考えてみます。 ユーザーが入力文字列を入力し、大文字、小文字、大文字/小文字の混在文字に変換する、そこから文字をフィルター処理する、その他の種類の変換を実行するなど、それらの文字列を何らかの方法で変換できる TextDisplay
という名前のコンポーネントを作成するとします。 ただし、TextDisplay
コンポーネントのコードを記述する時点では、変換プロセスがどのようなものになるかわからないため、代わりに、この操作を別のコンポーネントまで先延ばしにしようと考えます。 次に示すのは、TextDisplay
コンポーネントのコードです。 これは、ユーザーがテキスト値を入力できる <input> 要素の形式で入力文字列を提供します。
@* TextDisplay component *@
@using WebApplication.Data;
<p>Enter text:</p>
<input @onkeypress="HandleKeyPress" value="@data" />
@code {
[Parameter]
public EventCallback<KeyTransformation> OnKeyPressCallback { get; set; }
private string data;
private async Task HandleKeyPress(KeyboardEventArgs e)
{
KeyTransformation t = new KeyTransformation() { Key = e.Key };
await OnKeyPressCallback.InvokeAsync(t);
data += t.TransformedKey;
}
}
TextDisplay
コンポーネントでは、OnKeyPressCallback
という名前の EventCallback
オブジェクトを使用します。 HandleKeypress
メソッドのコードでは、コールバックを呼び出します。 @onkeypress
イベント ハンドラーは、キーが押され、HandleKeypress
メソッドを呼び出すたびに実行されます。 HandleKeypress
メソッドでは、ユーザーが押したキーを使用して KeyTransformation
オブジェクトを作成し、このオブジェクトをパラメーターとしてコールバックに渡します。 KeyTransformation
型は、次の 2 つのフィールドで構成される単純なクラスです。
namespace WebApplication.Data
{
public class KeyTransformation
{
public string Key { get; set; }
public string TransformedKey { get; set; }
}
}
key
フィールドには、ユーザーが入力した値が含まれ、TransformedKey
フィールドでは、処理が完了すると、変換されたキー値を保持します。
この例では、EventCallback
オブジェクトはコンポーネント パラメーターであり、その値はコンポーネントの作成時に指定されます。 このアクションは、TextTransformer
という名前の別のコンポーネントによって実行されます。
@page "/texttransformer"
@using WebApplication.Data;
<h1>Text Transformer - Parent</h1>
<TextDisplay OnKeypressCallback="@TransformText" />
@code {
private void TransformText(KeyTransformation k)
{
k.TransformedKey = k.Key.ToUpper();
}
}
TextTransformer
コンポーネントは、TextDisplay
コンポーネントのインスタンスを作成する Blazor ページです。 ページのコード セクションの TransformText
メソッドへの参照を OnKeypressCallback
パラメーターに設定します。 TransformText
メソッドでは、引数として指定された KeyTransformation
オブジェクトを受け取り、大文字に変換された Key
プロパティで見つかった値を TransformedKey
プロパティに入力します。 次の図は、TextTransformer
ページによって表示される TextDisplay
コンポーネントの <input> フィールドにユーザーが値を入力したときの、制御のフローを示したものです。
この方法の利点は、OnKeypressCallback
パラメーターのコールバックを提供する任意のページで TextDisplay
コンポーネントを使用できることです。 表示と処理は完全に分離されています。 TextDisplay
コンポーネントの EventCallback
パラメーターのシグネチャに一致する他のコールバックで TransformText
メソッドを切り替えることができます。
コールバックが適切な EventArgs
パラメーターで型指定されている場合は、中間メソッドを使用せずに、コールバックをイベント ハンドラーに直接接続できます。 たとえば、子コンポーネントは、次のような @onclick
などのマウス イベントを処理できるコールバックを参照する場合があります。
<button @onclick="OnClickCallback">
Click me!
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }
}
この場合、EventCallback
は MouseEventArgs
型パラメーターを受け取ります。そのため、@onclick
イベントのハンドラーとして指定できます。