MVCモデルのメタデータをダイナミックマークアップに出力する

ASP.NET MVCでは、メタデータ(モデルのフィールドを記述する属性)は、マークアップの生成(フィールドの名前、プレースホルダーなどの表示)とデータ検証(検証ルールの表示)の両方で使用されます。 2種類の検証を条件付きで区別できます。



ユーザーがフィールドに入力する際に​​犯した間違いをすぐに確認し、サーバーにデータを送信することなく修正を行うことができるため、クライアント検証は優れています(邪魔にならない検証)。 この場合、このタイプの検証が必要です。



実際に問題は何ですか?
古典的なアプローチを使用してマークアップ生成を行うと、すべてが自動的に機能しますが、ajaxを使用してクライアント上でhtmlマークアップを動的に生成するとどうなりますか? この場合、マークアップには何も自動的に追加されません。 もちろん、必要なものはすべて手動で追加することができ、問題は解決されたように見えますが、同じデータをサーバーとクライアントで2回記述する必要があるため、コードの重複の問題が発生します。これにより、他の問題が発生します。 動的マークアップは非常に便利な場合もありますが、ここでは、クライアント側でモデルのメタデータとデータ検証を導出するという疑問が生じます。 これについては後で説明します。



そのため、クライアント側へのMVCモデルメタデータの自動出力と控えめな検証を実装する必要があります。



主なアイデア:




httpヘッダーを介してメタデータを転送するという考え方は、 この記事から引用されています。

Backbone-validation.jsライブラリの詳細については、 こちらをご覧ください



サーバー側


サーバーに2つのフィルターを作成します。



メタデータフィルター(フィールド名、プレースホルダーなど)
public class MetaToHeader : ActionFilterAttribute { private readonly string header; public MetaToHeader(string header) { this.header = header; } public override void OnActionExecuted(ActionExecutedContext filterContext) { var result = filterContext.Result as JsonResult; if (result != null && result.Data != null) { var meta = GetMeta(result.Data); var jsonMeta = new JavaScriptSerializer().Serialize(meta); var jsonMetaBytes = Encoding.UTF8.GetBytes(jsonMeta); filterContext.HttpContext.Response.Headers.Add(header, Convert.ToBase64String(jsonMetaBytes)); } base.OnActionExecuted(filterContext); } private static IDictionary<string, object> GetMeta(object model) { var meta = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()); return meta.Properties.ToDictionary( p => p.PropertyName, p => new { label = p.GetDisplayName(), title = p.Description, placeholder = p.Watermark, readOnly = p.IsReadOnly } as object); } }
      
      







検証ルールフィルター
 public class ValidationToHeader : ActionFilterAttribute { private readonly string header; private static readonly Dictionary<string, Func<ModelClientValidationRule, List<object>>> Rules; public ValidationToHeader(string header) { this.header = header; } static ValidationToHeader() { Rules = new Dictionary<string, Func<ModelClientValidationRule, List<object>>>() { { "length", r => { var result = new List<object>(); if (r.ValidationParameters.ContainsKey("max")) result.Add(new {maxLength = r.ValidationParameters["max"]}); if (r.ValidationParameters.ContainsKey("min")) result.Add(new {minLength = r.ValidationParameters["min"]}); result.Add(new { msg = r.ErrorMessage }); return result; } }, { "range", r => { var result = new List<object>(); if (r.ValidationParameters.ContainsKey("max")) result.Add(new {max = r.ValidationParameters["max"]}); if (r.ValidationParameters.ContainsKey("min")) result.Add(new {min = r.ValidationParameters["min"]}); result.Add(new {msg = r.ErrorMessage}); return result; } }, { "remote", r => { var result = new Dictionary<string, object>(); if (r.ValidationParameters.ContainsKey("url")) result.Add("url", r.ValidationParameters["url"]); if (r.ValidationParameters.ContainsKey("type")) result.Add("type", r.ValidationParameters["type"]); result.Add("msg", r.ErrorMessage); return new List<object> { new {remote = result} }; } }, { "required", r => new List<object> { new { required = true, msg = r.ErrorMessage } } }, { "number", r => new List<object> { new { pattern = "number", msg = r.ErrorMessage } } } }; } public override void OnActionExecuted(ActionExecutedContext filterContext) { var result = filterContext.Result as JsonResult; if (result != null && result.Data != null) { var meta = GetRules(result.Data, filterContext.Controller.ControllerContext); var jsonMeta = new JavaScriptSerializer().Serialize(meta); var jsonMetaBytes = Encoding.UTF8.GetBytes(jsonMeta); filterContext.HttpContext.Response.Headers.Add(header, Convert.ToBase64String(jsonMetaBytes)); } base.OnActionExecuted(filterContext); } public static IDictionary<string, object> GetRules(object model, ControllerContext context) { var meta = ModelMetadataProviders.Current.GetMetadataForType(() => model, model.GetType()); return meta.Properties.ToDictionary( p => p.PropertyName, p => PropertyRules(p, context) as object); } private static object[] PropertyRules(ModelMetadata meta, ControllerContext controllerContext) { return meta.GetValidators(controllerContext) .SelectMany(v => v.GetClientValidationRules()) .SelectMany(r => Rules[r.ValidationType](r)) .ToArray(); } }
      
      







フィルターがJson形式でクライアントに送信されたデータを処理するという事実に注意する価値があります。



クライアント部


クライアントで、次を作成します。



ベースモデル
 (function () { var models = window.App.Models; models.DataMetaModel = Backbone.Model.extend({ metaHeader: 'data-meta', validationHeader: 'data-validation', urlRoot: '', initialize: function (options) { this.urlRoot = options.url; }, parse: function (response, xhr) { var metaData = xhr.xhr.getResponseHeader(this.metaHeader); var validationData = xhr.xhr.getResponseHeader(this.validationHeader); this.meta = metaData ? $.parseJSON(Base64.decode(metaData)) : undefined; this.validation = validationData ? $.parseJSON(Base64.decode(validationData)) : undefined; return response; } }); })();
      
      







解析メソッドを再定義して、HTTPヘッダーからメタデータと検証ルールをキャプチャし、バックボーンライブラリのbackbone-validation.jsで使用されます

基本的なバックボーンビュー
 (function () { var views = window.App.Views; views.dataMetaView = Backbone.View.extend({ events: { 'submit': 'evSubmit', 'blur input[type=text]': 'evBlur', }, initialize: function (options) { _.extend(Backbone.Validation.callbacks, { valid: this.validCallback, invalid: this.invalidCallback, }); _.extend(Backbone.Validation.validators, { remote: this.remoteValidator }); Backbone.Validation.bind(this, { offFlatten: true //  .    backbone-validation.js : var flatten = function (obj, into, prefix) { }); }, render: function () { this.addMeta(); }, addMeta: function () { _.each(this.model.meta, function (meta, name) { $('label[for=' + name + ']').text(meta.label); $('input[name=' + name + ']').attr({ title: meta.title, placeholder: meta.placeholder, readonly: meta.readOnly }); }); }, evBlur: function (e) { var $el = $(e.target); this.model.set($el.attr('name'), $el.val(), {validate: true, validateAll: false}); }, evSubmit: function (e) { if (!this.model.isValid(true)) return false; }, validCallback: function (view, attr, selector) { var control = view.$('[' + selector + '=' + attr + ']'); var group = control.parents(".control-group"); group.removeClass("error"); if (control.data("error-style") === "tooltip") { // CAUTION: calling tooltip("hide") on an uninitialized tooltip // causes bootstraps tooltips to crash somehow... if (control.data("tooltip")) control.tooltip("hide"); } else if (control.data("error-style") === "inline") { group.find(".help-inline.error-message").remove(); } else { group.find(".help-block.error-message").remove(); } }, invalidCallback: function (view, attr, error, selector) { var control = view.$('[' + selector + '=' + attr + ']'); var group = control.parents(".control-group"); group.addClass("error"); if (control.data("error-style") === "tooltip") { var position = control.data("tooltip-position") || "right"; control.tooltip({ placement: position, trigger: "manual", title: error }); control.tooltip("show"); } else if (control.data("error-style") === "inline") { if (group.find(".help-inline").length === 0) { group.find(".controls").append("<span class=\"help-inline error-message small-text\"></span>"); } var target = group.find(".help-inline"); target.text(error); } else { if (group.find(".help-block").length === 0) { group.find(".controls").append("<p class=\"help-block error-message small-text\"></p>"); } var target = group.find(".help-block"); target.text(error); } }, remoteValidator: function (value, attr, customValue, model) { var result, data = model.toJSON(); data[attr] = value; $.ajax({ type: customValue.type || 'GET', data: data, url: customValue.url, async: false, success: function (state) { if (!state) result = customValue.msg || 'remote validation error'; }, error: function () { result = "remote validation error"; } }); return result; } }); })();
      
      







ここで、Backbone Viewでは、検証ライブラリであるbackbone-validation.jsへのエントリポイントが最初に初期化されます。

 Backbone.Validation.bind(this, { offFlatten: true //  .    backbone-validation.js : var flatten = function (obj, into, prefix) { });
      
      





次に、エラーの強調表示に必要なコールバック関数(有効、無効)が初期化されます。 リモート検証属性もここで初期化されます:

 remoteValidator: function (value, attr, customValue, model) { var result, data = model.toJSON(); data[attr] = value; $.ajax({ type: customValue.type || 'GET', data: data, url: customValue.url, async: false, success: function (state) { if (!state) result = customValue.msg || 'remote validation error'; }, error: function () { result = "remote validation error"; } }); return result; }
      
      





クライアント側では、リモート検証属性はajaxメソッド(メソッドのタイプはサーバー側のモデルの説明で指定できます)であり、応答として検証済みフィールドの状態を示す変数を受け取ります。



検証が不可能な場合、または何らかの理由でクライアント側で行うことが困難な場合、リモート検証が必要です。 サーバー側のコードでは、リモート検証属性は次のように記述されています

 [Remote("RemoteEmailValidation", "Friends", ErrorMessage = "   ")]
      
      







使用する


サーバー側では、Json形式でデータを送信するメソッドを作成する必要があります。 作成したフィルターを適用します:

 [MetaToHeader("data-meta")] [ValidationToHeader("data-validation")] public ActionResult GetData() { return Json(new Friend(), JsonRequestBehavior.AllowGet); }
      
      





HTTPヘッダーの名前は、フィルターにパラメーターとして示されます。 parseメソッドでは、クライアント上で同じ名前が使用されます。



クライアント側で、作成されたDataMetaModel基本バックボーンモデルから継承されたFriendModelバックボーンモデルを作成します。このモデルでは、解析メソッドが再定義されます。

 (function() { var models = window.App.Models; models.FriendModel = models.DataMetaModel.extend({ initialize: function(options) { models.DataMetaModel.prototype.initialize.call(this, options); } }); })();
      
      







また、作成された基本的なバックボーンビューdataMetaViewから継承されたバックボーンビューNewFriendも作成します。

 (function () { var views = window.App.Views; views.NewFriend = views.dataMetaView.extend({ initialize: function (options) { views.dataMetaView.prototype.initialize.call(this); this.model.on('sync', this.render, this); this.template = _.template($(options.template).html()); }, render: function () { this.$el.html(this.template(this.model.toJSON())); views.dataMetaView.prototype.render.call(this); return this; }, load: function () { this.model.fetch(); } }); })();
      
      





ここでrenderメソッドで、すべてのステップを完了した後、ベースのrenderメソッドを呼び出す必要があります
 views.dataMetaView.prototype.render.call(this);
      
      



サーバー側のモデルの説明に従って、レンダリングされたフィールドにメタデータ(名前、プレースホルダーなど)を追加するため。 ただし、DOMでクライアントに渡された検証ルールは追加されません。 これらは、backbone-validation.jsライブラリでのみ使用されます。





友達モデルを作成します。

 public class Friend { public int Id { get; set; } [Display(Name = "", Prompt = " ", Description = " ")] [Required(ErrorMessage = "First name required")] [StringLength(50, MinimumLength = 2)] public string FirstName { get; set; } [Display(Name = "", Prompt = " ", Description = " ")] [Required(ErrorMessage = "Last name required")] [StringLength(50, MinimumLength = 2)] public string LastName { get; set; } [Display(Name = "", Prompt = " ", Description = " ")] [Required(ErrorMessage = "Age required")] [Range(0, 120, ErrorMessage = "Age must be between 0 and 120")] public int? Age { get; set; } [Display(Name = " ", Prompt = "  ", Description = "  ")] [Required(ErrorMessage = "Email required")] [Email(ErrorMessage = "Not a valid email")] [Remote("RemoteEmailValidation", "Friends", ErrorMessage = "   ")] public string Email { get; set; } }
      
      





コントローラーで、モデルを返すメソッドに2つのフィルターを追加します。

 [MetaToHeader("data-meta")] [ValidationToHeader("data-validation")] public ActionResult GetData() { return Json(new Friend(), JsonRequestBehavior.AllowGet); }
      
      





クライアントで、 use句で指定されたモデルとビューを作成し、動的マークアップの生成にビューが使用するテンプレートも定義します。

 <script type='text/template' id='dataMeta-template'> <form action="/Friends/Create" method="post"> <div class="control-group"> <label for="FirstName"></label> <div class="controls"> <input type='text' name="FirstName" value='<%- FirstName %>' /> </div> </div> <div class="control-group"> <label for="LastName"></label> <div class="controls"> <input type='text' name="LastName" value='<%- LastName %>' /> </div> </div> <div class="control-group"> <label for="Age"></label> <div class="controls"> <input type='text' name="Age" value='<%- Age %>' /> </div> </div> <div class="control-group"> <label for="Email"></label> <div class="controls"> <input type='text' name="Email" value='<%- Email %>' /> </div> </div> <p><button class="btn" type="submit">Create</button></p> </form> </script>
      
      







ページのエントリポイントはスクリプトです。

 <script> (function($) { var models = window.App.Models, views = window.App.Views; var dataMetaModel = new models.FriendModel({ urlRoot: '/Friends/GetData' }); var dataMetaView = new views.NewFriend({ el: '#dataMeta', model: dataMetaModel, template: '#dataMeta-template' }); dataMetaView.load(); })(jQuery); </script>
      
      







結果












プロジェクト



All Articles