web-dev-qa-db-ja.com

AngularJSを使用したASP.NET MVC検証フォーム

MVC 4とAngularJS(+ Twitterブートストラップ)のプロジェクトに参加しています。私は通常、MVCプロジェクトで「jQuery.Validate」、「DataAnnotations」、「Razor」を使用しています。次に、web.configでこれらのキーを有効にして、クライアント上のモデルのプロパティを検証します。

<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />

たとえば、私のモデルにこれがある場合:

[Required]
[Display(Name = "Your name")]
public string Name { get; set; }

このCshtmlの場合:

@Html.LabelFor(model => model.Name)
@Html.TextBoxFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)

Htmlの結果:

<label for="Name">Your name</label>
<input data-val="true" data-val-required="The field Your name is required." id="Name" name="Name" type="text" value="" />
<span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true"></span>

しかし、今、AngularJSを使用するとき、次のようにレンダリングしたいと思います。

<label for="Name">Your name</label>
<input type="text" ng-model="Name" id="Name" name="Name" required />
<div ng-show="form.Name.$invalid">
   <span ng-show="form.Name.$error.required">The field Your name is required</span>
</div>

これを解決するヘルパーまたは「データ注釈」があるかどうかはわかりません。 AngularJSには、次のような多くの機能があることを理解しています。

<div ng-show="form.uEmail.$dirty && form.uEmail.$invalid">Invalid:
    <span ng-show="form.uEmail.$error.required">Tell us your email.</span>
    <span ng-show="form.uEmail.$error.email">This is not a valid email.</span>
</div>

まあ、具体的に。 AngularJSでクライアントに表示するための属性(データ注釈)を解決するには、ヘルパーまたは「データ注釈」が必要です。

それでも存在しない場合は、RazorForAngularJSのように、おそらく実行する時間です。

編集

ASP.NET MVCとAngularJSを操作する最良の方法はおそらくそれを行うことだと思います(front-end)手書き(すべてのHTMLを手書きで書く)

36
andres descalzo

ASP.Net/Angular Webサイトを作成している人として、できる限りRazorを使用してHTMLをレンダリングすることから離れる方がはるかに優れていると言えます。

私のプロジェクトでは、1つのカミソリビューを設定してメインページをレンダリングし(Angularで書かれた単一ページのアプリを使用しています)、Angularのテンプレートとして使用するストレート.htmlファイルのフォルダーがあります。

私の場合、残りはASP.Net Web API呼び出しで行われますが、MVCアクションをJSON結果で使用することもできます。

このアーキテクチャに切り替えるとすぐに、開発は順調に進みました。

29
Ben Lesh

かみそりからの脱却についての考えに賛成ですが、ページをより迅速に作成するためのツールを作成できます。私見では、ツールセットから削除するのではなく、必要な場所でカミソリ機能を使用する方が良いです。

ところで、 ngval を見てください。これは、angularjsバリデーターとしてクライアント側にデータ注釈をもたらします。これにはhtmlヘルパーとangularモジュールがあります。プロジェクトは開発の初期段階にあることに言及する必要があります。

9
alisabzevari

MVCからAngularJsへの移行をスムーズにするためのディレクティブを作成しました。マークアップは次のようになります。

<validated-input name="username" display="User Name" ng-model="model.username" required>

これは、フィールドが変更されるまで検証を遅らせるなど、Razorの規則と同じように動作します。時間が経つにつれて、マークアップを維持することは非常に直感的でシンプルになりました。

主題に関する私の記事

Plinkr

4
Jeff Dunlop

おそらく、あなたがやりたいことをするための方法は半ダースあると思います。おそらく最も簡単なのは、jquery.validationマークアップを認識するAngularディレクティブを使用することです。

そのようなプロジェクトは次のとおりです。 https://github.com/mdekrey/unobtrusive-angular-validation

そしてここに別のものがあります: https://github.com/danicomas/angular-jquery-validate

個人的には、jquery.validation.unobtrusive属性の代わりにMVC出力angular検証属性を作成するコードを作成することでこの問題を解決したため、どちらも試していません。

3番目のオプションは、サーバー側の検証のみに依存することです。これは明らかに遅いですが、より複雑な検証シナリオの場合には、これが唯一の選択肢になる場合があります。この場合、Web APIコントローラーが通常返すModelStateDictionaryオブジェクトを解析するには、javascriptを記述するだけです。その方法と、AngularJSのネイティブ検証モデルに統合する方法については、いくつかの例があります。

ModelStateDictionaryを解析するための不完全なコードを次に示します。

``

angular.module('app')
    .directive('joshServerValidate', ['$http', function ($http) {
        return {
            require: 'ngModel',
            link: function (scope, ele, attrs, c) {
                console.info('wiring up ' + attrs.ngModel + ' to controller ' + c.$name);
                scope.$watch('modelState', function () {
                    if (scope.modelState == null) return;
                    var modelStateKey = attrs.joshServerValidate || attrs.ngModel;
                    modelStateKey = modelStateKey.replace(attrs.joshServerValidatePrefix, '');
                    modelStateKey = modelStateKey.replace('$index', scope.$index);
                    modelStateKey = modelStateKey.replace('model.', '');
                    console.info('validation for ' + modelStateKey);
                    if (scope.modelState[modelStateKey]) {
                        c.$setValidity('server', false);
                        c.$error.server = scope.modelState[modelStateKey];
                    } else {
                        c.$setValidity('server', true);
                    }
                });
            }
        };
    }]);

``

ここで提供されている他の回答にはかなり失望しています。電子メールアドレスよりも少し難しいことを検証しようとしているとき、「それをしないでください」というのはそれほど良い提案ではありません。

2
Josh Mouch

私はこれをわずかに異なる方法で解決しました。 MVCアプリケーションを変更して、フィルターと、Jsonシリアライザーカミソリテンプレートをビューの場所に挿入して検索するカスタムビューエンジンを介してapplication/jsonコンテンツタイプに応答するようにしました。

これは、jQuery UI、Bootstrap&同じコントローラー/アクションのJson応答)を使用してWebサイトのスキニングを可能にするために行われました。

JSON結果のサンプルは次のとおりです。

{
  "sid": "33b336e5-733a-435d-ad11-a79fdc1e25df",
  "form": {
    "id": 293021,
    "disableValidation": false,
    "phone": null,
    "zipCode": "60610",
    "firstName": null,
    "lastName": null,
    "address": null,
    "unit": null,
    "state": "IL",
    "email": null,
    "yearsAtAddress": null,
    "monthsAtAddress": null,
    "howHeard": null
  },
  "errors": [
    "The first name is required",
    "The last name is required",
    "Please enter a phone number",
    "Please enter an email address"
  ],
  "viewdata": {
    "cities": [
      {
        "selected": false,
        "text": "CHICAGO",
        "value": "CHICAGO"
      }
    ],
    "counties": [
      {
        "selected": false,
        "text": "COOK"
      }
    ]
  }
}

このフィルターは、リダイレクト結果をjsonオブジェクトに変換し、次のURLを呼び出しプログラムに渡します。

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);

        // if the request was application.json and the response is not json, return the current data session.
        if (filterContext.HttpContext.Request.ContentType.StartsWith("application/json") && 
            !(filterContext.Result is JsonResult || filterContext.Result is ContentResult))
        {
            if (!(filterContext.Controller is BaseController controller)) return;

            string url = filterContext.HttpContext.Request.RawUrl ?? "";
            if (filterContext.Result is RedirectResult redirectResult)
            {
                // It was a RedirectResult => we need to calculate the url
                url = UrlHelper.GenerateContentUrl(redirectResult.Url, filterContext.HttpContext);
            }
            else if (filterContext.Result is RedirectToRouteResult routeResult)
            {
                // It was a RedirectToRouteResult => we need to calculate
                // the target url
                url = UrlHelper.GenerateUrl(routeResult.RouteName, null, null, routeResult.RouteValues, RouteTable.Routes,
                    filterContext.RequestContext, false);
            }
            else
            {
                return;
            }
            var absolute = url;
            var currentUri = filterContext.HttpContext.Request.Url;
            if (url != null && currentUri != null && url.StartsWith("/"))
            {
                absolute = currentUri.Scheme + "://" + currentUri.Host + url;
            }

            var data = new {
                nextUrl =  absolute,
                uid = controller.UniqueSessionId(),
                errors = GetFlashMessage(filterContext.HttpContext.Session)
            };

            var settings = new JsonSerializerSettings
            {
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
                PreserveReferencesHandling = PreserveReferencesHandling.Objects,
                Formatting = Formatting.Indented,
                NullValueHandling = NullValueHandling.Ignore
            };
            filterContext.Result = new ContentResult
            {
                ContentType = "application/json",
                Content = JsonConvert.SerializeObject(data,settings)
            };
        }

以下はViews\Json\Serializer.cshmlで、コードベースの簡潔さとセキュリティのために使用されているステートメントは含まれていません。これは、応答を返すときに3回試行します。 1つ目は、元のView {controller} {action} .cshtmlを読み、htmlヘルパーを解析して、フォームとフィールドに配置することです。 2番目の試みは、組み込みのブログシステム(以下のPostContent)から要素を探し、モデルを使用するだけでは失敗します。

@model dynamic
@{
    Response.ContentType = "application/json";

    Layout = "";
    var session = new Object(); // removed for security purposes

    var messages = ViewBag.Messages as List<string>() ?? new List<string>();
    var className = "";
    if (!ViewData.ModelState.IsValid)
    {
        messages.AddRange(ViewData.ModelState.Values.SelectMany(val => val.Errors).Select(error => error.ErrorMessage));
    }


    dynamic result;
    string serial;

    try
    {
        Type tModel = Model == null ? typeof(Object) : Model.GetType();
        dynamic form = new ExpandoObject();
        dynamic fields = new ExpandoObject();

        var controller = ViewContext.RouteData.Values["controller"] as string ?? "";
        var action = ViewContext.RouteData.Values["action"] as string;

        var viewPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Views", controller, action + ".cshtml");
        if (File.Exists(viewPath))
        {
            string contents = File.ReadAllText(viewPath);
            var extracted = false;
            var patterns = new[]
            {
                @"@Html\.\w+For\(\w+ => \w+\.(.*?)[,\)]",
                @"@Html\.(\w+)For\(\w+ => \w+\.([\w\.]+)[, ]*(\(SelectList\))*(ViewBag\.\w+)*[^\)]*",
                "name=\"(.*?)\""
            };

            for (var i = 0; i < 3 && !extracted; i++)
            {
                switch (i)
                {
                    case 0:
                        form = contents.ExtractFields(patterns[0], Model as object, out extracted);
                        fields = contents.ExtractElements(patterns[1], Model as object, out extracted, ViewData);
                        break;
                    case 1:
                        form = Model as mvcApp.Models.Blog == null ? null : (Model.PostContent as string).ExtractFields(patterns[2], Model as object, out extracted);
                        break;
                    default:
                        form = Model;
                        break;
                }
            }
        }
        else if (Model == null)
        {
            // nothing to do here - safeModel will serialize to an empty object
        }
        else if (Model is IEnumerable)
        {
            form = new List<object>();

            foreach (var element in ((IEnumerable) Model).AsQueryable()
                    .Cast<dynamic>())
            {
                form.Add(CustomExtensions.SafeClone(element));
            }

        } else {
            form = Activator.CreateInstance(tModel);
            CustomExtensions.CloneMatching(form, Model);
        }

        // remove any data models from the viewbag to prevent
        // recursive serialization
        foreach (var key in ViewData.Keys.ToArray())
        {
            var value = ViewData[key];
            if (value is IEnumerable)
            {
                var enumerator = (value as IEnumerable).GetEnumerator();
                value = enumerator.MoveNext() ? enumerator.Current : null;
            }
            if (value != null)
            {
                var vtype = value.GetType();
                if (vtype.Namespace != null && (vtype.Namespace == "System.Data.Entity.DynamicProxies" || vtype.Namespace.EndsWith("Models")))
                {
                    ViewData[key] = null;
                }
            }
        }

        result = new
        {
            uid = session.UniqueId,
            form,
            fields,
            errors = messages.Count == 0 ? null : messages,
            viewdata = ViewBag
        };
        var setting = new JsonSerializerSettings
        {
            PreserveReferencesHandling = PreserveReferencesHandling.None,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            Formatting = Formatting.Indented
        };
        if (form is IEnumerable)
        {
            setting.NullValueHandling = NullValueHandling.Ignore;
        }
        serial = JsonConvert.SerializeObject(result, setting);
    }
    catch (Exception e)
    {
        result = new {
            uid = session.UniqueId,
            error = e.Message.Split('|')
        };
        serial = JsonConvert.SerializeObject(result);
    }
    @Html.Raw(serial)
}

Cloneメソッドについては、 異種オブジェクトのプロパティを複製する最良の方法 を参照してください。

    public static dynamic ExtractFields(this string html, string pattern, object model, out bool extracted)
    {
        if (html == null || model == null)
        {
            extracted = false;
            return null;
        }
        dynamic safeModel = new ExpandoObject();
        var safeDict = (IDictionary<string, Object>)safeModel;

        var matches = new Regex(pattern).Matches(html);
        extracted = matches.Count > 0;

        if ( extracted )
        {
            foreach (Match match in matches)
            {
                var name = match.Groups[1].Value;
                var value = CustomExtensions.ValueForKey(model, name);
                var segments = name.Split('.');
                var obj = safeDict;
                for (var i = 0; i < segments.Length; i++)
                {
                    name = segments[i];
                    if (i == segments.Length - 1)
                    {
                        if (obj.ContainsKey(name))
                        {
                            obj[name] = value;
                        }
                        else
                        {
                            obj.Add(name, value);
                        }
                        continue;
                    }
                    if (!obj.ContainsKey(name))
                    {
                        obj.Add(name, new ExpandoObject());
                    }
                    obj = (IDictionary<string, Object>)obj[name];
                }
            }
        }
        return safeModel;
    }

次に、プロパティチェーンの処理を少し簡単にするためのキー値コーディングの実装を示します。

/// <summary>
/// This borrows KeyValueCoding from Objective-C and makes working with long chains of properties more convenient. 
/// KeyValueCoding is null tolerant, and will stop if any element in the chain returns null instead of throwing a NullReferenceException. 
/// Additionally, the following Linq methods are supported: First, Last, Sum &amp; Average.
/// <br/>
/// KeyValueCoding flattens nested enumerable types, but will only aggregate the last element: "children.grandchildren.first" will return 
/// the first grandchild for each child. If you want to return a single grandchild, use "first.children.grandchildren". The same applies to
/// Sum and Average.
/// </summary>
/// <param name="source">any object</param>
/// <param name="keyPath">the path to a descendant property or method "child.grandchild.greatgrandchild".</param>
/// <param name="throwErrors">optional - defaults to supressing errors</param>
/// <returns>returns the specified descendant. If intermediate properties are IEnumerable (Lists, Arrays, Collections), the result *should be* IEnumerable</returns>
public static object ValueForKey(this object source, string keyPath, bool throwErrors = false)
{
    try
    {
        while (true)
        {
            if (source == null || keyPath == null) return null;
            if (keyPath == "") return source;

            var segments = keyPath.Split('.');
            var type = source.GetType();
            var first = segments.First();
            var property = type.GetProperty(first);
            object value = null;
            if (property == null)
            {
                var method = type.GetMethod(first);
                if (method != null)
                {
                    value = method.Invoke(source, null);
                }
            }
            else
            {
                value = property.GetValue(source, null);
            }

            if (segments.Length == 1) return value;


            var children = string.Join(".", segments.Skip(1));
            if (value is IEnumerable || "First|Last|Sum|Average".IndexOf(first, StringComparison.OrdinalIgnoreCase) > -1)
            {
                var firstChild = children.Split('.').First();
                var grandchildren = string.Join(".", children.Split('.').Skip(1));
                if (value == null) {
                    var childValue = source.ValueForKey(children);
                    value = childValue as IEnumerable<object>;
                    switch (first.Proper())
                    {
                        case "First":
                            return value == null ? childValue : ((IEnumerable<object>)value).FirstOrDefault();
                        case "Last":
                            return value == null ? childValue : ((IEnumerable<object>)value).LastOrDefault();
                        case "Count":
                            return value == null ? (childValue == null ? 0 : 1) : (int?)((IEnumerable<object>)value).Count();
                        case "Sum":
                            return value == null
                                ? Convert.ToDecimal(childValue ?? "0")
                                : ((IEnumerable<object>) value).Sum(obj => Convert.ToDecimal(obj ?? "0"));
                        case "Average":
                            return value == null
                                ? Convert.ToDecimal(childValue ?? "0")
                                : ((IEnumerable<object>) value).Average(obj => Convert.ToDecimal(obj ?? "0"));
                    }
                } else {
                    switch (firstChild.Proper())
                    {
                        case "First":
                            return ((IEnumerable<object>)value).FirstOrDefault().ValueForKey(grandchildren);
                        case "Last":
                            return ((IEnumerable<object>)value).LastOrDefault().ValueForKey(grandchildren);
                        case "Count":
                            if (!string.IsNullOrWhiteSpace(grandchildren))
                            {
                                value = value.ValueForKey(grandchildren);
                                if (value != null && ! (value is IEnumerable<object>))
                                {
                                    return 1;
                                }
                            }
                            return value == null ? 0 : ((IEnumerable<object>)value).Count();
                        case "Sum":
                            return ((IEnumerable<object>)value).Sum(obj => Convert.ToDecimal(obj.ValueForKey(grandchildren)??"0"));
                        case "Average":
                            return ((IEnumerable<object>)value).Average(obj => Convert.ToDecimal(obj.ValueForKey(grandchildren) ?? "0"));
                    }
                }
                if (value == null) return null;
                var flat = new List<object>();
                foreach (var element in (IEnumerable<object>)value)
                {
                    var child = element.ValueForKey(children);
                    if (child == null)
                    {
                        continue;
                    }
                    if (child is IEnumerable && !(child is string))
                    {
                        flat.AddRange((IEnumerable<object>) child);
                    }
                    else
                    {
                        flat.Add(child);
                    }
                }
                return flat.Count == 0? null: flat;
            }
            source = value;
            keyPath = children;
        }
    }
    catch (Exception)
    {
        if (throwErrors) throw;
    }
    return null;
}
1
B2K