vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【ASP.NET Core】Razorで遊んでみる

はじめに

ブログとしては久しぶりに ASP.NET Core 関連のお話です。

C# のコードを HTML に書けるということで ASP.NET Core に採用されている Razor 。

実をいうとあんまり使われておらず、 Angular や React などの JavaScriptフレームワークが主流なんじゃないの? と思っていました。

が、とある勉強会にて C# 版 WebAssembly である Blazor の話が盛り上がったのですが、その構文がかなり Razor と共通していると感じました。

で、あれ? もしかして結構本気でこの構文を推し進めようとしているのでは?
(もちろんジョークだと思っていたわけではないですが)
と思ったのでした。

Blazor にも興味があるところですが、まずは情報もよりたくさんあるであろう Razor から調べてみることにしました。

というお話です。

なお Microsoft Docs をはじめ、 Razor 構文は紹介されているところがたくさんあるため、網羅的に試すというよりは気になったものをピックアップして取り上げることにします。

試してみる

では早速試してみることにします。

基本的に C# で書くことのできるコードは、頭に@をつけることで何でも書くことができるようです。

また @ が複数行にわたる場合など、{} や () でまとめることも可能です。

@{
    var text = "Hello ";
    var sample = ViewBag.Sample;
}
< div>
    @(text + sample)
< /div>

{} や () の使用は必須ではないものがほとんどですが、List< string> のように <> が含まれる場合は必ず {} や () 内に入れる必要があります。

@functions

関数は @functions の中に書くことができます。

@functions {
    string GetMessage() {
        return "Hello";
    }    
}
< div>@GetMessage()< /div>

@classes のようなものは用意されていないため、インナークラスを定義することはできないようです...と思いきや、 @functions は実は関数が書ける、というものではなくコードブロックが書けるものであるため、下記のように書くことができてしまいます。

@functions {
    
    public class InnerClassSample {
        public string Name { get; set; }
    }
    
    async Task< InnerClassSample> GetInnerClassAsync() {
        await Task.Delay(500);
        return new InnerClassSample{ Name = "Hello" };
    }    
}

ただし当然ながら? コントローラークラスなどからはアクセスできないため、活躍の場は少なそうです。

Controller から値を渡す

Controller 側から値を渡す方法としては ViewData、ViewBagがあります。

いずれも Controller で値を入れておき、 View 側で取り出す、という形をとります。

HomeController.cs

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using RazorSample.Models;

namespace RazorSample.Controllers {
    [Route("")]
    public class HomeController: Controller {
        public IActionResult Index() {
            // ViewData で渡す値をセット.
            ViewData["SampleUsers"] = Enumerable.Range(0, 5)
                .Select(num => new User {
                    ID = num,
                    Name = "User " + num,
                    Description = "Hello World!!!"
                })
                .ToList();
            return View("/Views/Index.cshtml");
        }
    }
}

Index.cshtml

@foreach (User u in (List< User>)ViewData["SampleUsers"]) {
    < table>
        < tr>
            < td>ID: < /td>
            < td>@u.ID< /td>
        < /tr>
        < tr>
            < td>Name: < /td>
            < td>@u.Name< /td>
        < /tr>
        < tr>
            < td>Description: < /td>
            < td>@u.Description< /td>
        < /tr>
    < /table>
}

ViewData で受け取れる値の型は object のため、キャストしてから取り出します。

ViewData と ViewBag の違いはアクセスの方法です。

ViewData[Sample] = "Hello";
ViewBag.Sample = "Hello";

注意点として、 ViewBag の Sample もどこかで定義されているわけではないため、 入力補完や静的解析は効かない、ということです。

ということで使い分けは気分次第、ということになります。 ( ViewBag は NullReferenceException がとか読んだ気もしますが、 Dictionary も Key が見つからなきゃエラーになりますよね)

コメントアウト

@{
    /* DoSomething(); */
    
    // DoSomething();
}

↑でもコメントアウトできることはできます。

が、

@* @DoSomething() *@

がスマートですね。

動的に呼び出せるか

できません。

ページをロードするときに評価され、 HTML に変換されてしまうので、

< button type="button" onclick=@{ Say(); }>Say< /button>

のように書くと、ページロード時に Say() が実行され、 onclick は空の状態になります(ボタンを押しても反応しない)。

加えて

< !--button type="button" onclick=@{ Say(); }>< /button-->

としてもコメントアウトとみなされないため、注意が必要そうです。

async/await

@functions {    
    string message = "default";

    async Task< string> GenerateMessage() {
        await Task.Delay(1000);
        return "hello";
    }
}
< div>
    @{ message = await GenerateMessage(); }
    @message

    @* これでもOK *@
    @await GenerateMessage()
< /div>

上記のように async/await を使うこと自体はできますが、非同期で表示の内容を切り替えられるわけではないため、 ページのロード完了は await などで待つ必要があります。

非同期での実行がしたい場合は Blazor?

@:

@: を頭につけると、その行は < text>< /text> で囲まれたのと同じ扱いとなります。

@for (var i = 0; i < 3; i++) {
    var person = "person " + i;
    @:Name: @person この行の @: 以降はすべて HTML として出力される
}

HtmlTagHelper

Razor では直接 HTML タグを書く他に、より C# 的に書けるようヘルパーが用意されています。

Index.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RazorSample.Models.User

@* 
    using を使うことでスコープの終わりに EndForm を自動で実行してくれる 
    BeginForm の引数は 1. アクション( Submit で呼び出す関数)、
        2. コントローラー名、 3. 送信方法( Get 、 Post)、 4. HTML の属性( class とか)
*@

@using(Html.BeginForm(
          "SendMessage",
          "Home",
          FormMethod.Post,
          new { @class = "myclass"})){
              < table>
                  < tr>
                      < td>@Html.LabelFor(m => m.Name)< /td>
                      < td>@Html.TextBoxFor(m => m.Name)< /td>
                  < /tr>
                  < tr>
                      < td>@Html.LabelFor(m => m.Description)< /td>
                      < td>@Html.TextAreaFor(m => m.Description)< /td>
                  < /tr>
              < /table>
              < input type="submit" value="Submit" />
}

User.cs

using System;

namespace RazorSample.Models {
    public class User {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime LastUpdateDate { get; set; }
    }
}

HomeController.cs

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using RazorSample.Models;

namespace RazorSample.Controllers {
    [Route("")]
    public class HomeController: Controller {
        ~省略~

        [HttpPost]
        public void SendMessage(User sampleUser) {           
            Console.WriteLine("Send");
        }
    }
}

Formなどに対してusing を使えるのは便利そうですね。

JavaScript での C# の変数

例えばこんな処理

Index.cshtml

@functions {
    string message = "default message";
    async Task< string> GetMessageAsync() {
        await Task.Delay(500);
        return "hello";
    }
}
< script type="text/javascript">
    var clickSample = function() {
        // '' や "" で囲まないとエラー
        console.log('@message');
    }
< /script>
< div>
    @{ message = await GetMessageAsync(); }
< /div>
< button onclick="clickSample()">Click< /button>

まず地味にハマったのは、 JavaScript の console.log で C# の変数を渡すときに '' や "" で囲んでおらず、「 default 」が定義されていない、とエラーになったことです。

「 default 」ってなんぞ?と思ったら、変数の「 default message 」が半角スペースで区切られたものを変数として扱おうとしていたためのようでした。

やはり動的に型を持つ言語は扱いが違うので慣れていないとびっくりしますね(;'∀')

あと、 JavaScript 内での C# の変数の値は JavaScript のコードがロードされた時に確定されるため、上記のように「 @{ message = await GetMessageAsync(); } 」より前に定義すると値は「default message」に、後に定義すると「 hello 」になります。

async/await などを使う場合は注意が必要ですね。

おわりに

取り留めもなく書いていきましたが、かなり C# 的に HTML が書ける、というのはなかなか面白いです。

とはいえ非同期の処理は使えなかったり(実行はされるが表示に反映されない)、どのように HTML に変換されるかを理解する必要がある、という意味ではやはり HTML をちゃんとわかっておく必要があったりします。

今回取り上げた内容もまだ Razor の一部でしかないと思うので、引き続き追いかけてみるとともに Blazor も試してみたいと思います。

参照