WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

アクションメソッドと構造不定の JSON (MVC5)

by WebSurfer 25. August 2021 11:33

ASP.NET MVC5 / Web API 2 のアクションメソッドで、構造が不定の JSON 文字列をどのように受け取って処理できるかという話を書きます。先の Core 版の記事「アクションメソッドと構造不定の JSON (CORE)」の .NET Framework 版です。

JSON 文字列から指定した name の value を取得

.NET Framework / Core 2.x 以前と Core 3.x 以降の ASP.NET MVC / Web API では JSON デシリアライズに使うライブラリが異なり、前者はサードパーティ製の Newtonsoft.JSON、後者は System.Text.Json 名前空間のものになります。

デシリアライザが違っても、先の記事でアクションメソッドの引数に使った System.Text.Json 名前空間の JsonElement 構造体を Newtonsoft.JSON の JToken クラスに変えれば、モデルバインダが自動的にデシリアライズしてくれるのではないかと期待して試したのですが MVC はダメでした。

MVC のアクションメソッドの場合、先の記事のように Partial(Rootobject postedObject) とすると postedObject に渡される前の処理でエラーになります。Full(JToken postedObject) としても同じくエラーになります。

一方、Web API のアクションメソッドの場合は、先の Core 版の記事と同様にして期待通りの結果になりました。コードの違いはこの記事の下の方に書きましたので見てください。

MVC と Web API で結果が異なるのは、想像ですが、モデルバインドの仕組みが異なるためと思われます。先の記事「ASP.NET Web API のバインディング」に書きましたが、Web API の場合は Model Binding または Formatter を利用するという 2 つの方法があって、クエリ文字列からパラメータを取得する場合は Model Binding、ボディから取得する場合は Formatter を使うそうです。

なので、MVC の場合は、プリミティブな方法ですが、アクションメソッドの引数には JSON 文字列のまま渡して、アクションメソッドの中でデシリアライズするのが良さそうです。(他にカスタムモデルバインダを使うとかの手段があるかもしれませんが、余計に複雑になりそうな気がしましたので、途中で考えるのをやめました(笑))

検証に使った MVC の Controller / Action Method のコードは以下の通りです。

using System.Collections.Generic;
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Mvc5WithWebAPI.Models;

namespace Mvc5WithWebAPI.Controllers
{
    public class JsonModelbindingController : Controller
    {
        // Partial(Rootobject postedObject) ではエラー
        [HttpPost]
        public ActionResult Partial(string jsonText)
        {
            var postedObject = JsonConvert.DeserializeObject<Rootobject>(jsonText);
            JToken element = postedObject.Response.Result.ObjectArray;
            return Content(element.ToString());
        }


        // Full(JToken postedObject) ではエラー
        [HttpPost]
        public ActionResult Full(string jsonText)
        {
            JToken jtoken = JsonConvert.DeserializeObject<JToken>(jsonText);

            JToken element = FindJTokenByName(jtoken, "ObjectArray");

            string returnValue = (element != null) ?
                element.ToString() : "ObjectArray は取得できません";

            return Content(returnValue);
        }


        private static JToken FindJTokenByName(JToken jtoken, string name)
        {
            if (jtoken is JObject)
            {
                foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
                {
                    if (kvp.Key == name)
                    {
                        return kvp.Value;
                    }
                    else
                    {
                        JToken retVal = FindJTokenByName(kvp.Value, name);
                        if (retVal != null)
                        {
                            return retVal;
                        }
                    }
                }
            }
            else if (jtoken is JArray)
            {
                foreach (JToken jtokenInArray in (JArray)jtoken)
                {
                    JToken retVal = FindJTokenByName(jtokenInArray, name);
                    if (retVal != null)
                    {
                        return retVal;
                    }
                }
            }
            else
            {
                return null;
            }
            return null;
        }
    }
}

検証に使った JSON 文字列とそれをベースに作成した C# のクラス定義は、先の記事「アクションメソッドと構造不定の JSON (CORE)」のものと同じですので、そちらを見てください。

クライアント側は jQuery ajax を使って {"JsonText":"JSON 文字列"}という形で MVC のアクションメソッドに送信して検証しました。その応答をブラウザ上に表示したのが上の画像です。

検証に使った View のコードを以下に記載しておきます。コメントで「MVC 向け」とした部分を見てください。

@{
    ViewBag.Title = "JsonModelbinding";
}

<h2>JsonModelbinding</h2>

<input id="button1" type="button" value="Web API" />
<input id="button2" type="button" value="MVC" />
<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[
        var json =
            '{ "Title": "This is my title",' +
            '"Response": { "Version": 1, "StatusCode": "OK",' +
            '"Result": { "Profile": { "UserName": "SampleUser", "IsMobileNumberVerified": false, "MobilePhoneNumber": null },' +
            '"ObjectArray" : [{"Code": 2000,"Description": "Fail"},{"Code": 3000,"Description": "Success"}],' +
            '"lstEnrollment": "2021-2-5"},' +
            '"Message": {"Code": 1000, "Description": "OK"}},' +
            '"StringArray" : ["abc", "def", "ghi"] }';

        // Web API 向け
        $("#button1").on("click", function () {
            $.ajax({
                type: "POST",
                url: "/WebApi/Partial",
                data: json,
                contentType: "application/json; charset=utf-8",
            }).done(function (data) {
                $("#result").empty();
                $("#result").append(data);
            }).fail(function (jqXHR, status, error) {
                $('#result').text('Status: ' + status +
                    ', Error: ' + error);
            });
        });

        // MVC 向け
        $("#button2").on("click", function () {
            var j = { JsonText: json };
            var text = JSON.stringify(j);
            $.ajax({
                type: "POST",
                url: "/JsonModelbinding/Partial",
                data: text,
                contentType: "application/json; charset=utf-8",
            }).done(function (data) {
                $("#result").empty();
                $("#result").append(data);
            }).fail(function (jqXHR, status, error) {
                $('#result').text('Status: ' + status +
                    ', Error: ' + error);
            });
        });
        //]]>
    </script>
}

Web API の場合は、上にも書きましたが、組み込みの Formatter が JSON 文字列をデシリアライズしてアクションメソッドの引数に渡してくれるからか、先の Core 版の記事と同様にして期待通りの結果が得られました。

検証に使った Web API の Controller / Action Method のコードは以下の通りです。

using System.Collections.Generic;
using System.Web.Http;
using Newtonsoft.Json.Linq;
using Mvc5WithWebAPI.Models;

namespace Mvc5WithWebAPI.Controllers
{
    public class ValuesController : ApiController
    {
        [HttpPost]
        [Route("WebApi/Partial")]
        public string Partial([FromBody] Rootobject postedObject)
        {
            JToken element = postedObject.Response.Result.ObjectArray;
            return element.ToString();
        }

        [HttpPost]
        [Route("WebApi/Full")]
        public string Full([FromBody] JToken postedObject)
        {
            JToken element = FindJTokenByName(postedObject, "ObjectArray");

            string returnValue = (element != null) ?
                element.ToString() : "ObjectArray は取得できません";

            return returnValue;
        }


        private static JToken FindJTokenByName(JToken jtoken, string name)
        {
            if (jtoken is JObject)
            {
                foreach (KeyValuePair<string, JToken> kvp in (JObject)jtoken)
                {
                    if (kvp.Key == name)
                    {
                        return kvp.Value;
                    }
                    else
                    {
                        JToken retVal = FindJTokenByName(kvp.Value, name);
                        if (retVal != null)
                        {
                            return retVal;
                        }
                    }
                }
            }
            else if (jtoken is JArray)
            {
                foreach (JToken jtokenInArray in (JArray)jtoken)
                {
                    JToken retVal = FindJTokenByName(jtokenInArray, name);
                    if (retVal != null)
                    {
                        return retVal;
                    }
                }
            }
            else
            {
                return null;
            }
            return null;
        }
    }
}

クライアント側は、上の View のコードのコメントで「Web API 向け」とした部分を見てください。先の Core 版の記事と同様に、サンプル JSON 文字列をそのまま送信しています。

Tags: , , , ,

MVC

ASP.NET MVC5 で boorstrap datepicker 利用

by WebSurfer 16. August 2021 14:14

ASP.NET MVC5 で Controller で Model に日付を設定して View に渡し、Html ヘルパーの EditorFor を使ってテキストボックスにその日付を表示したとします。そのテキストボックスに bootstrap datepicker を適用してカレンダーを表示した際、 Model に設定した日付がカレンダー上で選択されるようにする方法を備忘録として残しておきます。

boorstrap datepicker

自動的に Model に設定した日付が bootstrap datepicker のカレンダー上で選択されて表示されると思っていたのですが、何もしないと Model に設定した日付ではなくてブラウザに表示された当日の日付になってしまいます。

その原因は、例えば new DateTime(2021, 3, 23) としてそれを Model に設定すると、テキストボックスに 2021/03/23 0:00:00 と表示される(当該 html input 要素で value="2021/06/24 0:00:00" となる)、即ち 0:00:00 部分が bootstrap datepicker にとって余計で日付を判定できなかったからのようです。

なので、当該 html input 要素で value="2021/03/23" となるようにすれば bootstrap datepicker のカレンダー上もその日付を選択して表示するようになります。

そのためには、Model の当該プロパティに DisplayFormatAttribute 属性を付与してやります。具体的には以下のようにします。その結果が上の画像です。

(1) Model

using System;
using System.ComponentModel.DataAnnotations;

namespace Mvc5App.Models
{
    public class BootstrapDatepickerModel
    {
        [Display(Name = "日付")]
        [DisplayFormat(ApplyFormatInEditMode = true,
            DataFormatString = "{0:yyyy/MM/dd}")]
        public DateTime? OrderDate { get; set; }
    }
}

Visual Studio 2019 のテンプレートを使って ASP.NET Web アプリを作ると、Bootstrap.js と Bootstrap.css が自動的にプロジェクトに含まれますが、bootstrap datepicker はその中には含まれていませんので注意してください。別途 github のサイト uxsolutions/bootstrap-datepicker 等からダウンロードする必要があります。この記事では、この記事を書いている時点での最新版 1.9.0 を使用しました。

また、bootstrap datepicker は Boorstrap 本体のバージョン 4 以上には対応してないそうです (Bootstrap 4 および 5 でも使う方法があるそうですが未検証・未確認です)。この記事で使った Bootstrap のバージョンは、MVC5 アプリのテンプレートに含まれている v3.4.1 です。

ご参考に View と Controller / Action Metod のコードも以下に載せておきます。

(2) View

@section Scripts { ... } 内の link 要素、script 要素による外部 bootstrap datepicker の js ファイル、css ファイルへの参照に注目してください。

@model Mvc5App.Models.BootstrapDatepickerModel

@{
    ViewBag.Title = "Datepicker";
}

<h2>Datepicker</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>BootstrapDatepickerModel</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.OrderDate,
                htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.OrderDate,
                    new { htmlAttributes = new { @class = "form-control datepicker" } })
                @Html.ValidationMessageFor(model => model.OrderDate, "",
                    new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    <link href="~/Content/bootstrap-datepicker.css" rel="stylesheet" />
    <script src="~/Scripts/bootstrap-datepicker.js"></script>
    <script src="~/Scripts/locales/bootstrap-datepicker.ja.min.js"></script>

    @Scripts.Render("~/bundles/jqueryval")

    <script type="text/javascript">
        //<![CDATA[
        $(function () {
            $('.datepicker').datepicker({
                format: 'yyyy/mm/dd',
                language: 'ja'
            });
        })
        //]]>
    </script>
}

(3) Controller / Action Method

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc5App.DAL;
using System.Data.Entity;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using Mvc5App.Models;
using System;

namespace Mvc5App.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Datepicker()
        {
            var model = new BootstrapDatepickerModel();
            model.OrderDate = new DateTime(2021, 3, 23);

            // model.OrderDate が null なら今日の日付を表示する
            if (model.OrderDate == null)
            {
                model.OrderDate = DateTime.Now;
            }

            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Datepicker(BootstrapDatepickerModel model)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("Index");

            }

            return View(model);
        }
    }
}

Tags: , , ,

MVC

ASP.NET Identity にエンティティ追加

by WebSurfer 23. June 2021 14:54

Visual Studio のテンプレートで認証を「個別のユーザーアカウント」としてプロジェクトを作成すると ASP.NET Identity を使った認証システムが実装されます。それにエンティティを追加する方法を書きます。この記事の例では下の画像の左上の Post エンティティがそれです。

ASP.NET Identity に Post エンティティを追加

Post というのは投稿という意味です。複数のユーザーが複数の投稿をするアプリで、投稿内容を SQL Server データベースで保持し、Post エンティティクラスを定義して Entity Framework 経由で書き込み、読み出し、編集などを行うという想定です。

ユーザーの誰が投稿を書いたかを識別できるように、上の画像のように Post と ApplicationUser をナビゲーションプロパティで紐づけます。(結果、EF Code First で生成されるデータベースでは、ApplicationUser に該当する dbo.AspNetUsers テーブルの Id 列に、dbo.Posts テーブルの ApplicationUserId 列から FK 制約が張られます)

上の画像の Post エンティティ以外の基本のエンティティクラスとコンテキストクラスは ASP.NET Identity の中で定義済みです。(.NET Framework 版の場合は Models/IdentityModels.cs に ApplicationUser クラスと ApplicationDbContext クラスが含まれていて、その継承元で定義されています)

それに Post エンティティを追加してナビゲーションプロパティを張るには Post クラスを追加するだけではダメで、既存の基本のエンティティクラスとコンテキストクラスにも手を加える必要があります。それをどのようにするかというのがこの記事の話です。

.NET Framework 版の場合は自動生成された Models/IdentityModels.cs にコンテキストクラスとエンティティクラスが定義されていますので、それに手を加えて Migration を実行することになります。

手を加えた Models/IdentityModels.cs のサンプルコードは以下の通りです。コードの中で「追加」とコメントした部分を追加しているだけです。(Core 版は場所が違うので注意。この記事の下の方の説明と画像を見てください)

using System.Data.Entity;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;

// 追加
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Mvc5App3.Models
{
    // ApplicationUser クラスにさらにプロパティを追加すると、ユーザーの
    // プロファイル データを追加できます。詳細については、
    // https://go.microsoft.com/fwlink/?LinkID=317594 を参照してください。
    public class ApplicationUser : IdentityUser
    {
        public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
                               UserManager<ApplicationUser> manager)
        {
            // authenticationType が CookieAuthenticationOptions
            // .AuthenticationType で定義されているものと一致して
            // いる必要があります
            var userIdentity = await manager.CreateIdentityAsync(
                 this, DefaultAuthenticationTypes.ApplicationCookie);
            // ここにカスタム ユーザー クレームを追加します
            return userIdentity;
        }

        // 追加
        public virtual IList<Post> Posts { get; set; }
    }

    // 追加
    public class Post
    {
        [Key, Required]
        public int PostId { get; set; }

        [Required, MaxLength(128)]
        public string Title { get; set; }

        [Required, MaxLength(1024)]
        public string Content { get; set; }

        [Required, ForeignKey(nameof(User))]
        public string ApplicationUserId { get; set; }

        public virtual ApplicationUser User { get; set; }
    }

    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }

        // 追加
        public DbSet<Post> Posts { get; set; }
    }
}

上のコードの通りコンテキストクラスとエンティティクラスに手を加えてから Migration 操作 (Add-Migration, Update-Database) を行うと以下のテーブルが SQL Server データベースに生成されます。

Migration 操作で追加された Posts テーブル

dbo.Posts テーブルの ApplicationUserId 列から ApplicationUser に相当するテーブル dbo.AspNetUsers の Id 列に FK 制約が張られます。もちろん Models/IdentityModels.cs のコードに定義したナビゲーションプロパティも期待通り働きます。


以上は .NET Framework 版の話です。Core 版の場合も上記とほぼ同じようにして既存のデータベースに Posts テーブルを追加し、既存の AspNetUsers テーブルの Id 列に FK 制約を張ることができます。

ただし、Core 版では、Visual Studio のテンプレートで「個別のユーザーアカウント」を選んでプロジェクトを作成しても、ASP.NET Core Identity 関係のソースコードは含まれないことに注意してください。Razor Class Library (RCL) として実装されますのでコンテキストクラスとエンティティクラスに手を加えることができません。

なので、Core 版でこの記事のようなことを行う場合は、認証なしでプロジェクトを作成した後でスキャフォールディング機能を使ってソースコードと共に ASP.NET Core Identity を実装するのが良さそうです。

そうした場合、コンテキストクラスとエンティティクラスは下の画像の場所に生成されます。

コンテキストクラスとエンティティクラス

上の画像ではクラス名が ApplicationDbContext, ApplicationUser となっていますが、デフォルトの設定のまま進めると Application の部分がプロジェクト名になります。スキャフォールディングを行う際に任意に設定できますので、この記事では .NET Framework 版と同じ名前に設定しています。

上の画像の ApplicationDbContext, ApplicationUser クラスに、上のサンプルコードで書いたようにナビゲーションプロパティを追加します。

Post クラスはプロジェクトの既存の Models フォルダにクラスファイルを追加してそれに定義するのが良いと思います。

Tags: , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  September 2021  >>
MoTuWeThFrSaSu
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

View posts in large calendar