QA@IT

C# ASP.NET MVC コントローラーのパラメータを可変にしたい

8389 PV

VS2013 Pro、C#にてASP.NETの開発をしております。
今回、あるフォームが日本語と英語サイトで用意されており、このフォームに入力された内容を、
Ajax経由でPOSTし、指定のコントローラーのアクションメソッドのパラメータとして渡したいという事を実現する方法につき質問させて頂きます。

フォームの項目は、日本語/英語それぞれに、別名でモデルとして定義されております。
(FormJpModel,FormEnModel)

現状、コントローラーのアクションメソッドは、
[HttpPost]
public ActionResult FormRegistJp(FormJpModel model)
{
そして、
[HttpPost]
public ActionResult FormRegistEn(FormEnModel model)
{
と別々に作成し、似たような処理を行っております。
このパラメーターを共通化し、アクションメソッドの処理で、別で取得した言語コードにより、
modelにFormJpModelやFormEnModelをセットさせたいのです。

もし可能な方法がございましたら、ご教示下さいますよう、お願い致します。

  • 質問者さんのやりたいことが分からないのですが、今は Model, View, Controller すべて日本語・英語で別々に作られているのですか? 「このパラメーターを共通化」というのがやりたいことのようですが「パラメータ」とは具体的に何でしょうか? -
  • すみません、もう一つ質問させれください。今は期待通り動いているのでしたら、それにもかかわらず「共有化」したいという理由は何でしょう? 「動いてるものを直そうとするな」という格言みたいなものもあるのですが・・・ 期待通りに動いてないとすると、具体的に何が問題なのでしょう? -
  • 今回、機能追加したい部分がコントローラーのアクションメソッドです。
    日本語用→FormRegistJp(FormJpModel model)、英語用→FormRegistEn(FormEnModel model)と別々に定義する以外FormRegistAll(XXXX model)と共通化して書けないかと。
    -
  • この場合パラメータ(XXXX model)にどういう型を書いて、言語が日本語の時はmodelにFormJpModelをセットして、
    model.InputUserNameとか、model.InputUserAddressのように使えたらと考えております。
    -
  • 2 つ目の質問には答えていただけないのでしょうか? -
  • とりあえず、アクションメソッドを共通化するのではなく、リソースファイルを使う案を回答欄に書いておきます。 -
  • 申し訳ございません、2つめのコメントに関して失念しておりました。現状、期待通りに動いているという意味ではなく、今の環境・資産を利用して共有化するには?という、安易な発想でございます。 -
  • すみませんが意味がわかりません。「現状、期待通りに動いているという意味ではなく」ということは期待通り動いてないということですか? であれば「今の環境・資産を利用」はできないと思うのですか。 -

回答

Takacさんの回答にもありますがカスタムModelBinderを作成すれば、できるとは思います。
ちょっと試しに書いてみた程度のものなので実用に耐えうるかは不明です。

InputUserName, InputUserAddress を親クラスに持たせ、継承したモデルFormEnModel、FormJpModel用のビューを一つずつ持たせ、
どちらからのPOSTも一つのActionで受け取るようにしています。

コントローラ

Controllers/FormController.cs

using qait9816.Models;
using System.Web;
using System.Web.Mvc;

namespace qait9816.Controllers {    
    public class FormController : Controller {

        public ActionResult Index() {
            return View(); // 初期ページ表示
        }
        [AcceptVerbs("GET")] 
        public ActionResult CreateEn() {
            return View(); // 入力ページ表示
        }
        [AcceptVerbs("GET")] 
        public ActionResult CreateJp() {
            return View(); // 入力ページ表示
        }

        [AcceptVerbs("POST")] // Jp,EnいずれのPOSTも受け付ける
        public ActionResult Create(FormModelBase item) {
            // 親クラスで受けとる
            if (Request.ContentType.StartsWith("application/json")) {
                // jQuery $.post用
                var r = new JsonResult();
                if (item is FormEnModel) r.Data = (FormEnModel)item;
                if (item is FormJpModel) r.Data = (FormJpModel)item;
                return r;
            }
            else {
                // form submit 用
                return View(item);
            }
        }
    }
}

ビュー

Views/Form/Index.cshtml

@{
    Layout = null;
}
<!DOCTYPE html>
<html><head>
    <title>Index</title>
</head><body>
    <div> 
        @Html.ActionLink("Add English member", "CreateEn") |
        @Html.ActionLink("Add Japanese member", "CreateJp")
    </div>
</body></html>

Views/Form/CreateEn.cshtml

@{
    Layout = null;
}
<!DOCTYPE html>
<html><head>
    <title>Input page</title>
    <script src="//code.jquery.com/jquery-1.12.0.min.js"></script>
    <script>
        function postData(){
            var postdata = {
                TypeString: $('#typeString').val(),
                EnPostalCode: $('#postalcode').val(),
                // ■ CreateJp.cshtmlはこちら
                // JpPostalCode: $('#postalcode').val(),
                InputUserName: $('#username').val(),
                InputUserAddress: $('#useraddress').val()

            };

            $.post({
                url: '/Form/Create',
                type: 'POST',
                data: JSON.stringify(postdata),
                contentType: 'application/json; charset=utf-8',
                success: function (data_success) {
                    console.log(data_success)
                },
                error: function () {
                    console.log('error');
                }
            });
        }
        $(document).ready(function () {
            $('#postButton').on('click', postData);
        });
    </script>
</head><body>
    <div>
        <form action="/Form/Create" method="post">
            @Html.Hidden("TypeString", typeof(qait9816.Models.FormEnModel).ToString()
            , new { id = "typeString" })
            <!-- ■ CreateJp.cshtmlはこちら -->
            @*@Html.Hidden("TypeString", typeof(qait9816.Models.FormJpModel).ToString()
            , new { id = "typeString" })*@  
            <input type="text" name="EnPostalCode" id="postalcode" value="55416" />
            <input type="text" name="InputUserName" id="username" value="John Smith" />
            <input type="text" name="InputUserAddress" id="useraddress" value="Nowhere" />
            <input type="button" id="postButton" value="Submit" />
            <button>submit sync</button>
        </form>
    </div>    
    <div> @Html.ActionLink("Back to Top", "Index") </div>
</body></html>

Views/Form/CreateJp.cshtml

省略
CreateEn.cshtmlのコメント参照

Views/Form/Create.cshtml (Form Submitの結果表示用)

@model qait9816.Models.FormModelBase
@{
    Layout = null;
}
<!DOCTYPE html>
<html><head>
    <title>Create</title>
</head><body>
    <div>
        Type Name You Post is : @Model.GetType().ToString() <br />
        <br />
        Address: @Model.InputUserAddress <br />
        Name: @Model.InputUserName <br />
        @if (Model is qait9816.Models.FormEnModel) {
            @:Postal: @(((qait9816.Models.FormEnModel)Model).EnPostalCode)
        }
        else {
            @:Postal: @(((qait9816.Models.FormJpModel)Model).JpPostalCode)
        }
        <br />
    </div>
    <div> @Html.ActionLink("Back to Top", "Index") </div>
</body></html>

モデル

Models/FormEnModel.cs

namespace qait9816.Models {
    public class FormEnModel : FormModelBase {
        public string EnPostalCode { get; set; }
    }
}

Models/FormJpModel.cs

namespace qait9816.Models {
    public class FormJpModel : FormModelBase {
        public string JpPostalCode { get; set; }
    }
}

で、本題のカスタムModelBinderです。
もともとのmodel生成処理を上書きして、
ビューのhiddenに置いておいた型文字列をもとにモデルを復元させています。
スペースの都合もあって利用するFormModelBaseのファイルの中に書いてしまっています。
Application_Startで登録するやり方もありますが今回は属性で指定しています。

Models/FormModelBase.cs

using System;
using System.IO;
using System.Web;
using System.Web.Helpers;
using System.Web.Mvc;

namespace qait9816.Models {
    [ModelBinder(typeof(MultiLangFormModelBinder))]
    public abstract class FormModelBase {
        public string InputUserName { get; set; }
        public string InputUserAddress { get; set; }
    }

    public class MultiLangFormModelBinder : DefaultModelBinder {
        protected override object CreateModel(
            ControllerContext controllerContext, 
            ModelBindingContext bindingContext, Type modelType) {

            // 一応 コントローラとアクションを制限
            if (controllerContext.RouteData.Values["controller"].ToString() != "Form" ||
                controllerContext.RouteData.Values["action"].ToString() != "Create") {
                return base.CreateModel(controllerContext, bindingContext, modelType);
            }

            string typeString = (string)bindingContext
                                            .ValueProvider
                                            .GetValue("TypeString")
                                            .ConvertTo(typeof(string));
            Type type = Type.GetType((string)typeString, true);

            // 対象外の型なら例外
            if (!typeof(FormJpModel).IsAssignableFrom(type) 
                && !typeof(FormEnModel).IsAssignableFrom(type)) {

                throw new InvalidCastException("Unsupported type posted.");
            }

            // 本当の型でmodelを復元する。
            var model = Activator.CreateInstance(type);
            bindingContext.ModelMetadata = 
                ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
            return model;
        }
    }
}

構成にもよりますが、共通でない部分の処理は結局書くのでしょうから
共通の親がいるなら別々のアクションメソッドで受け取った後で共通処理を呼ぶとか、
ジェネリッククラス作って共通部分以外はジェネリックメソッド受け取るとかの方が
すっきりしそうな気はしますね。

編集 履歴 (0)

単にFormJpModel,FormEnModelの全てのプロパティを(必要ならNull許容型で)持つモデルを用意してそれを使うだけでよい気がしますが...
どうしても共通の引数で独自のクラスとして受け取りたいというなら、独自のモデルバンダーを作り、モデルに共通の親クラスかインターフェイスを定義し、それを受け取るアクションメソッドに適用させればできるとは思います
ASP.NET MVC モデル バインディングの特長と問題点
の抽象モデル バインダーあたりが参考になるかもしれません

編集 履歴 (1)

今回、機能追加したい部分がコントローラーのアクションメソッドです。
日本語用→FormRegistJp(FormJpModel model)、英語用→FormRegistEn(FormEnModel model)と別々に定義する以外FormRegistAll(XXXX model)と共通化して書けないかと。

FormRegistAll(XXXX model) を日本語・英語共通に使えるようにするとすれば、それ用の View も、アクションメソッドの引数に使ってモデルバインディングする XXXX Model も共通で使えるものを新たに作らないとダメだと思いますが。(現在の質問者さんのコード、DB 等がどうなっているか不明ですが、普通の構成を考えるとそうせざるを得ないかと)

登録用のフォームのラベルや説明などがカルチャによって切り替われば良いのであればリソースファイルを利用してはいかがですか?

aspx ページでのリソースの利用
http://surferonwww.info/BlogEngine/post/2015/04/21/use-of-resources-in-aspx-page.aspx

上の記事は ASP.NET Web Forms アプリの Web サイトプロジェクトの話ですが、MVC でも同様にリソースファイルを使えます。(MVC は Web アプリケーションプロジェクトなのでリソースファイルの置き場所の制約が減って、Windows アプリのように任意の場所にリソースファイルを置けます)

Model に付与するデータアノテーションによって、ラベルやエラーメッセージもリソースファイルを使って多言語化できます。

データアノテーション検証の多言語対応
http://surferonwww.info/BlogEngine/post/2014/09/11/multi-languages-message-for-data-annotation.aspx

そうすれば、ブラウザが要求ヘッダに含めて送信してくる Accept-Language の設定によって ASP.NET が自動的に日本語・英語を切り替えてくれます。(Accept-Language の設定方法の例は上の記事の最後の方の画像を見てください)

編集 履歴 (1)
ウォッチ

この質問への回答やコメントをメールでお知らせします。