ASP.NET MVC - 部分ビューにあるinput要素のname属性にプレフィックスを付ける

探していたことはまさにコレでした。

stackoverflow.com

TemplateInfoもよくわかっていなかったので真似してサンプルを書いてみました。

やりたいこと

まずはやりたいことを整理します。

次のようにAddressクラスのプロパティを2つ持つモデル、SampleInputModelがあるとします。コントローラのアクションでこのモデルをバインドしたいとします。

// 住所氏名
public class Address {
    [Display(Name = "住所")]
    public string Place { get; set; }

    [Display(Name = "名前")]
    public string Name { get; set; }
}

// 入力モデル
// Addressクラスを2つ持つ
public class SampleInputModel {
    [Display(Name = "送付先")]
    public Address ToAddress { get; set; }

    [Display(Name = "送付元")]
    public Address FromAddress { get; set; }
}

そしてAddressクラス用に部分ビューを用意したいとします。(このサンプルだと部分ビューにするまでも・・・という気がしますが、モデルもビューのマークアップももっと複雑だという体でお願いします。)

@* _Address.cshtml *@
@* Addressクラス用の部分ビュー *@

@model Address

<div>@Html.LabelFor(model => model.Place):@Html.TextBoxFor(model => model.Place)</div>
<div>@Html.LabelFor(model => model.Name):@Html.TextBoxFor(model => model.Name)</div>

RenderPartialメソッドを使って部分ビューを呼び出しますが、input要素のname属性が重複してしまいます。

<form action="@Url.Action()" method="post">
    @Html.LabelFor(model => model.ToAddress)
    @{
        Html.RenderPartial("_Address", Model.ToAddress);
    }
    <hr />
    @Html.LabelFor(model => model.FromAddress)
    @{
        Html.RenderPartial("_Address", Model.FromAddress);
    }
    <hr />
    <button type="submit">保存</button>
</form>

<!-- 生成されるhtml -->
<!-- input要素のname属性が重複している -->
<form action="/" method="post">
    <label for="ToAddress">送付先</label>
    <div><label for="Place">住所</label><input id="Place" name="Place" type="text" value="" /></div>
    <div><label for="Name">名前</label><input id="Name" name="Name" type="text" value="" /></div>
    <hr />
    <label for="FromAddress">送付元</label>
    <div><label for="Place">住所</label><input id="Place" name="Place" type="text" value="" /></div>
    <div><label for="Name">名前</label><input id="Name" name="Name" type="text" value="" /></div>
    <hr />
    <button type="submit">保存</button>
</form>

これではモデルにうまくバインドできません。id属性も重複していてダメですし。name属性をToAddress.NameFromAddress.Nameといった感じにする必要があります。

解決するには

モデルにバインドできるようにname属性を設定するには、TemplateInfo.HtmlFieldPrefixプロパティを使います。

このプロパティはTemplateInfo.GetFullHtmlFieldメソッド内で使われています。TemplateInfo.GetFullHtmlFieldメソッドはTextBoxForといったHtmlHelperの拡張メソッド内でname属性を作るのに使われています。

ということでTemplateInfo.HtmlFieldPrefixプロパティを使うRenderPartialFor拡張メソッドを書いてみます。

public static class HtmlHelperExtensions {
    // RenderPartialのラムダ式版
    public static void RenderPartialFor<TModel, TProperty>(
        this HtmlHelper<TModel> htmlHelper,
        string partialViewName,
        Expression<Func<TModel, TProperty>> expression) {

        // ラムダ式からモデルのメタデータを取得して、
        // さらにメタデータからモデルを取得する
        var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        var model = metadata.Model;

        // TemplateInfoは、TextBoxForなどのHtmlHelperメソッドの拡張メソッドで
        // name属性を取得するのに使っているクラス
        var templateInfo = new TemplateInfo {
            // ラムダ式から「プロパティをドットで連結する文字列」を取得して、
            // プレフィックスに指定する
            HtmlFieldPrefix = ExpressionHelper.GetExpressionText(expression),
        };

        // TemplateInfoを差し替えるためにビューデータも作成する
        var viewData = new ViewDataDictionary(htmlHelper.ViewData) {
            TemplateInfo = templateInfo,
        };

        htmlHelper.RenderPartial(partialViewName, model, viewData);
    }
}

ビューでRenderPartialメソッドの代わりにRenderPartialForメソッドを使ってみると。

<form action="@Url.Action("Edit")" method="post">
    @Html.LabelFor(model => model.ToAddress)
    @{
        Html.RenderPartialFor("_Address", model => model.ToAddress);
    }
    <hr />
    @Html.LabelFor(model => model.FromAddress)
    @{
        Html.RenderPartialFor("_Address", model => model.FromAddress);
    }
    <hr />
    <button type="submit">保存</button>
</form>

<!-- 生成されるhtml -->
<form action="/" method="post">
    <label for="ToAddress">送付先</label>
    <div><label for="ToAddress_Place">住所</label><input id="ToAddress_Place" name="ToAddress.Place" type="text" value="" /></div>
    <div><label for="ToAddress_Name">名前</label><input id="ToAddress_Name" name="ToAddress.Name" type="text" value="" /></div>
    <hr />
    <label for="FromAddress">送付元</label>
    <div><label for="FromAddress_Place">住所</label><input id="FromAddress_Place" name="FromAddress.Place" type="text" value="" /></div>
    <div><label for="FromAddress_Name">名前</label><input id="FromAddress_Name" name="FromAddress.Name" type="text" value="" /></div>
    <hr />
    <button type="submit">保存</button>
</form>

name属性がいい感じに出力されています。これでモデルへのバインドもうまくできます。

さすがMVC

.NETでUDPを使って通信する(UdpClientを使う)

前回はTCPを使って通信してみたので今回はUDPを使って通信するサンプルを書いてみました。

ichiroku11.hatenablog.jp

.NETでUDPを使って通信するにはUdpClientを使います。Socketを使う方法はまたそのうち…というフラグ。

UdpClient クラス (System.Net.Sockets)

UDPコネクションレスTCPよりシンプルなプロトコルなので、UdpClientを使ったコードもTCPに比べるとシンプルでわかりやすいかなと思います。TcpListenerのようなクラスもないですし、NetworkStreamも使いません。言ってしまえばバイト配列を送信する、受信するだけです。

クライアント

UDPのサンプルもHTTPのようにクライアントとサーバでリクエストとレスポンスのメッセージをやりとりする動きにしました。まずはクライアントから見ていきましょう。

クライアントの動き

  1. サーバにリクエストを送信
  2. サーバからレスポンスを受信

書くまでもない感じですね。

クライアントのサンプルコード

SendAsyncで一方的にリクエストを送信して、ReceiveAsyncでレスポンスを受信しています。

// クライアント
public class Client<TRequest, TResponse> {
    // 送信先のエンドポイント
    private readonly IPEndPoint _endpoint;

    public Client(IPEndPoint endpoint) {
        _endpoint = endpoint;
    }

    // クライアントを実行する
    // リクエストを送信してレスポンスを受信する
    public async Task<TResponse> SendAsync(TRequest request) {
        // UdpClientを作成するときにはエンドポイントを指定しない
        using (var client = new UdpClient()) {
            // 1. リクエストを送信
            // (送信先のエンドポイントを指定して)
            Console.WriteLine($"Client send {nameof(request)}: {request}");
            var requestBytes = new ObjectConverter<TRequest>().ToByteArray(request);
            await client.SendAsync(requestBytes, requestBytes.Length, _endpoint);

            // 2. レスポンスを受信
            var result = await client.ReceiveAsync();

            // ここで受信した結果のリモートエンドポイントが送信先かをチェックした方がいい気がする

            var responseBytes = result.Buffer;
            var response = new ObjectConverter<TResponse>().FromByteArray(responseBytes);
            Console.WriteLine($"Client receive {nameof(response)}: {response}");

            return response;
        }
    }
}

レスポンスを受信したタイミングで、送信したエンドポイントから返ってきたレスポンスかどうか、UdpReceiveResult.RemoteEndPointを確認した方がいい気がします。上記のサンプルでは書いていませんが。

サーバ

続いてサーバ。

サーバの動き

  1. クライアントからリクエストを受信する
  2. リクエストを処理してレスポンスを作る
  3. クライアントにレスポンスを送信する

サーバのサンプルコード

2と3は複数のクライアントからのリクエストを並列に処理することを考えてTaskを使いました。ただサンプルではTaskを管理していません。投げっぱなしは良くないので管理する必要があるとは思いますが、どうするのがいいのかなと。

// サーバ
public class Server<TRequest, TResponse> {
    // リクエストを受信するエンドポイント
    private readonly IPEndPoint _endpoint;

    public Server(IPEndPoint endpoint) {
        _endpoint = endpoint;
    }

    // サーバを実行する
    public async Task RunAsync(Func<TRequest, TResponse> processor) {
        // リクエストを受信するエンドポイントを指定してUdpClientを作成
        using (var client = new UdpClient(_endpoint)) {
            while (true) {
                // 1. リクエストを受信
                var result = await client.ReceiveAsync();
                var requestBytes = result.Buffer;
                var request = new ObjectConverter<TRequest>().FromByteArray(requestBytes);
                Console.WriteLine($"Server receive {nameof(request)}: {request}");

                var sender = Task.Run(async () => {
                    // 2. リクエストからレスポンスを作成
                    var response = processor(request);

                    // 3. リクエストの送信元にレスポンスを送信
                    Console.WriteLine($"Server send {nameof(response)}: {response}");
                    var responseBytes = new ObjectConverter<TResponse>().ToByteArray(response);
                    await client.SendAsync(responseBytes, responseBytes.Length, result.RemoteEndPoint);
                });

                // Taskの管理やエラー処理は省略
            }
        }
    }
}

クライアントとサーバを実行してみる

そして実行してみると。

class Program {
    // サーバの作成
    public static Server<string, string> Server(IPEndPoint endpoint)
        => new Server<string, string>(endpoint);

    // クライアントの作成
    public static Client<string, string> Client(IPEndPoint endpoint)
        => new Client<string, string>(endpoint);

    // 文字列を並びを反対にする
    private static string Reverse(string original)
        => new string(original.Reverse().ToArray());

    static void Main(string[] args) {
        var endpoint = new IPEndPoint(IPAddress.Loopback, 54321);

        // サーバを実行
        var server = Task.Run(() => Server(endpoint).RunAsync(Reverse));

        // クライアントを実行
        Task.WaitAll(
            Client(endpoint).SendAsync("あいうえお"),
            Client(endpoint).SendAsync("かきくけこ"));

        // サーバのTaskをきれいに終了するにはどうしたらいいのか...
    }
}
// 実行結果
/*
Client send request: あいうえお
Server receive request: あいうえお
Client send request: かきくけこ
Server receive request: かきくけこ
Server send response: おえういあ
Server send response: こけくきか
Client receive response: おえういあ
Client receive response: こけくきか
*/

TCPのサンプルを書いたときもそうでしたが、サーバの終了処理(キャンセル処理?)はどう書くのがいいかまだよく分からずです。

ソース全体はこちら。

.NETでUDP(UdpClient) · GitHub

.NETでTCPを使って通信する(TcpClientとTcpListenerを使う)

.NETで通信するプログラムに興味が出てきたので調べたりしています。ネットワークプログラミングと言うんですかね。まずはやっぱりTCPかなと思うので、TCPを使って通信する簡単なクライアントとサーバのサンプルコードを書いてみました。

.NETでTCPを使って通信するにはTcpClientとTcpListenerを使います。

より細かく制御できるSocketを使う方法もあるようです。いい勉強になりそうだとは思うのですがなかなか難しそうなのでソケットはまた別で試したいと思います。というフラグ立て。

クライアント

クライアントもサーバもHTTPをイメージしてリクエストとレスポンスのメッセージをやりとりする動きにしました。まずはクライアントの動きです。

クライアントの動き

  1. サーバに接続
  2. サーバにリクエストを送信する
  3. サーバからレスポンスを受信する

クライアントのサンプルコード

上記の動きをするクライアントのサンプルコードです。クライアント側ではTcpClientを使います。エンドポイントとはIPアドレスとポート番号のセットのことです。

// クライアント
public class Client<TRequest, TResponse> {
    // 接続先のエンドポイント
    private readonly IPEndPoint _endpoint;

    public Client(IPEndPoint endpoint) {
        _endpoint = endpoint;
    }

    // サーバにリクエストを送信してレスポンスを受信する
    public async Task<TResponse> Send(TRequest request) {
        using (var client = new TcpClient()) {
            // 1. サーバに接続
            await client.ConnectAsync(_endpoint.Address, _endpoint.Port);

            using (var stream = client.GetStream()) {
                // 2. サーバにリクエストを送信する
                Console.WriteLine($"Client send: {request}");
                stream.WriteObject(request);

                // 3. サーバからレスポンスを受信する
                var response = stream.ReadObject<TResponse>();
                Console.WriteLine($"Client received: {response}");

                return response;
            }
        }
    }
}

Stream.WriteObject、Stream.ReadObjectは自作した拡張メソッドです。WriteObjectはオブジェクトをバイト配列に変換してストリームに書き込みます。ReadObjectはストリームから読み込んだバイト配列をオブジェクトとして取得します。

サーバ

続いてサーバです。クライアントと比べるとちょっと複雑です。

サーバの動き

  1. クライアントからの接続を待つ
  2. クライアントからの接続を受け入れる
  3. クライアントからリクエストを受信する
  4. リクエストを処理してレスポンスを作る
  5. クライアントにレスポンスを送信する

3から5は複数のクライアントからのリクエストを並列に処理できるようにしています。

サーバのサンプルコード

サーバではまずTcpListenerを使ってクライアントからの接続を待ちます。クライアントから接続されると今度はTcpClientを使ってメッセージを読み書きすると言った感じになります。

// サーバ
public class Server<TRequest, TResponse> {
    // 接続を待つエンドポイント
    private readonly IPEndPoint _endpoint;

    // リクエストからレスポンスを作成する処理
    private readonly Func<TRequest, TResponse> _processor;

    // TCPリスナー
    private readonly TcpListener _listener;

    public Server(IPEndPoint endpoint, Func<TRequest, TResponse> processor) {
        _endpoint = endpoint;
        _processor = processor;
        _listener = new TcpListener(_endpoint);
    }

    // クライアントからリクエストを受信してレスポンスを送信する
    private void Receive(TcpClient client) {
        using (client)
        using (var stream = client.GetStream()) {
            // 3. クライアントからリクエストを受信する
            var request = stream.ReadObject<TRequest>();

            // 4. リクエストを処理してレスポンスを作る
            var response = _processor(request);

            // 5. クライアントにレスポンスを送信する
            stream.WriteObject(response);
        }
    }

    // 接続を待つ
    public async Task Listen() {
        Console.WriteLine($"Server listen:");
        // 1. クライアントからの接続を待つ
        _listener.Start();

        while (true) {
            // 2. クライアントからの接続を受け入れる
            var client = await _listener.AcceptTcpClientAsync();
            Console.WriteLine($"Server accepted:");

            var task = Task.Run(() => Receive(client));

            // Taskの管理やエラー処理は省略
        }
    }

    // 終了する
    public void Close() {
        _listener.Stop();
    }
}

なるべく簡単なサンプルにするためにエラー処理やTaskの管理など省いていますが、ベースとしてはこんな感じかなと。実際作り込むには色々考えることがありそうに思ったり。

クライアントとサーバを実行してみる

サーバを動かしてクライアントも実行してみます。

class Program {
    static void Main(string[] args) {
        // サーバが接続を待つエンドポイント
        // であり
        // クライアントが接続するサーバのエンドポイント
        var endpoint = new IPEndPoint(IPAddress.Loopback, 54321);

        // サーバ
        var server = new Server<Message, Message>(
            endpoint,
            // リクエストからレスポンスを作る処理
            request => new Message {
                Id = request.Id,
                // メッセージの文字列を逆順にする
                Content = new string(request.Content.Reverse().ToArray()),
            });
        // 接続を待機
        var task = Task.Run(() => server.Listen());

        // クライアント
        Task.WaitAll(
            // リクエストを送信してレスポンスを受信
            new Client<Message, Message>(endpoint).Send(new Message { Id = 10, Content = "あいうえお" }),
            new Client<Message, Message>(endpoint).Send(new Message { Id = 20, Content = "かきくけこ" }),
            new Client<Message, Message>(endpoint).Send(new Message { Id = 30, Content = "さしすせそ" })
        );

        // サーバを終了
        server.Close();
        // サーバの終了処理、Taskの管理、エラー処理あたりが微妙
    }
}

// 実行結果(実行するたびに変わる)
/*
Server listen:
Server accepted:
Server accepted:
Client send: { Id = 10, Content = "あいうえお" }
Client send: { Id = 30, Content = "さしすせそ" }
Server accepted:
Client send: { Id = 20, Content = "かきくけこ" }
Client received: { Id = 30, Content = "そせすしさ" }
Client received: { Id = 10, Content = "おえういあ" }
Client received: { Id = 20, Content = "こけくきか" }
*/

実行結果やデバッグ実行で分かりやすくするために、同じプロセス内でクライアントもサーバも動かしています。あとサーバ(リスナー)の終了の仕方とかTaskの管理とか微妙かもです。

ソース全体はこちらに。

SQL Server - OPENROWSETでJSONファイルを読み込んでOPENJSON

前回、OPENJSONを使ってJSON文字列を結果セットに変換してみました。

ichiroku11.hatenablog.jp

そのあとにそういえば「JSONファイル」を読み込んでOPENJSONを使う方法を知らないなと思いました。検索してみると次の記事が見つかります。

Importing JSON files into SQL Server using OPENROWSET (BULK) | SQL Server Database Engine Blog

この記事の二番煎じもいいところですが、OPENROWSETもCROSS APPLYもたぶんあまり使ったことがない気がするので試してみました。

まずJSONファイルを用意します。

{
    "entries": [
        { "id": 10, "name": "" },
        { "id": 30, "name": "" },
        { "id": 20, "name": "" }
    ]
}

JSONファイルはC:\Temp\sample.jsonにあるとします。このファイルを読み込んで結果セットに変換するクエリは次のように書きます。

select Id, Name
from
    -- OPENROWSETでJSONファイルを読み込む
    -- SINGLE_NCLOBを指定して1行1列の結果セットを取得する
    -- 結果セットにJson(Text)という名前を付ける
    openrowset(bulk N'C:\Temp\sample.json', SINGLE_NCLOB) as Json(Text)
    -- 結果セットのJson.Textに対してCROSS APPLYでOPENJSONを呼び出す
    cross apply
        openjson(Json.Text, N'$.entries')
        with(
            Id int N'$.id',
            Name nvarchar(10) N'$.name'
        )
order by Id;

-- 実行結果
/*
Id          Name
----------- ----------
10          あ
20          う
30          い
*/

OPENROWSETの引数にSINGLE_NCLOBを指定すると、ファイルの内容をnvarchar(max)の1行1列の結果セットとして取得できます。CROSS APPLYは結果セットの行ごとに関数を呼び出します。今回の結果セットは1行なのでOPENJSONは1回だけ呼ばれているはずです。

参考

SQL Server - OPENJSONは役に立つ

SQL Server 2016から手軽にJSONを扱えるようになっています。

MSDNなどを読みながら勉強している最中で全体を把握しているわけではないですが、SQL ServerJSONサポートは次の2つの機能がメインなのかなと思います。

機能 概要
FOR JSON 結果セットJSON文字列に変換する
OPENJSON関数 JSON文字列結果セットに変換する

ここではSELECT文で取得できる行と列の表形式のデータを「結果セット」と言っています。たぶん言いますよね。行セットとも言うのかもしれません。

そしてOPENJSONの使い方を知っているとJSON文字列を解析したり、集計や抽出するときなどになかなか役に立つのかなと思うので、クエリを書きながら使い方を覚えていきたいと思います。

まずOPENJSONには

の2つがあります。

OPENJSON (TRANSACT-SQL)

今回は明示的なスキーマを指定する方法を試します。正直なところ既定のスキーマはあまり使わないのかな?と。

明示的なスキーマを指定してOPENJSON

明示的なスキーマを指定するとは、結果セットで取得したいスキーマをWITH句で指定することです。WITH句を使わないと既定のスキーマになります。

早速クエリを。

-- JSON文字列
declare @json nvarchar(max) = N'[
  { "id": 10, "name": "Aaa" },
  { "id": 30, "name": "Ccc" },
  { "id": 20, "name": "Bbb" },
  { "id": 99, "name": "Xxx" }
]';

-- OPENJSONを使ってJSON文字列を結果セットに
select *
from openjson(@json)
with(   -- 結果セットのスキーマ(Id, Nameカラムを持つスキーマ)を指定する
    Id int N'$.id',    -- "id"プロパティをIdカラムとする
    Name nvarchar(10) N'$.name'   -- "name"プロパティをNameカラムとする
)
order by Id;

-- 実行結果
/*
Id          Name
----------- ----------
10          Aaa
20          Bbb
30          Ccc
99          Xxx
*/

OPENJSONの引数にはJSON文字列を渡します。

WITH句では結果セットのスキーマをカラムごとにカラム名、データ型、JSONパス式の順で指定します。CREATE TABLEのカラム定義に似ていますね。指定したカラムは普通のテーブルのカラムのようにORDER BY句などで利用できます。

この中で見慣れないのはJSONパス式かなと思いますが、ざっくり言うとJavaScriptオブジェクトのプロパティアクセスのような文字列です。

JSON パス式 (SQL Server)

ドル記号($)については以下の記述がありますが、コンテキストアイテムは「JSON文字列をパースして取得できたオブジェクト」という解釈でいいのかなと思います。

ドル記号 ($) はコンテキスト アイテムを表します。

パス式を指定してOPENJSON

OPENJSONは2つ目の引数にJSONパス式を渡すこともできます。JSONパス式を渡すと取得するオブジェクト(コンテキストアイテム)を指定できます。

declare @json nvarchar(max) = N'{
  "entries": [
      { "id": 10, "name": "Aaa" },
      { "id": 30, "name": "Ccc" },
      { "id": 20, "name": "Bbb" },
      { "id": 99, "name": "Xxx" }
  ]
}';

-- "entries"プロパティが指す配列を取得したいとして
select *
-- OPENJSONの2つ目の引数に取得したいオブジェクトへのJSONパスを指定
from openjson(@json, N'$.entries')
with(
    Id int N'$.id',
    Name nvarchar(10) N'$.name'
);

-- 実行結果
/*
Id          Name
----------- ----------
10          Aaa
30          Ccc
20          Bbb
99          Xxx
*/

とりあえずこんなところで。

OPENJSONはJSONをごにょごにょしたいときに役に立つかなと思いました。

その他の参考