【问题标题】:ASP.NET MVC Validation form with AngularJS带有 AngularJS 的 ASP.NET MVC 验证表单
【发布时间】:2013-02-09 09:46:15
【问题描述】:

我在 MVC 4 和 AngularJS(+ twitter bootstrap)中进行了一个项目。我通常在我的 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)

【问题讨论】:

    标签: asp.net-mvc validation razor angularjs data-annotations


    【解决方案1】:

    作为创作了 ASP.Net/Angular 网站的人,我可以告诉您,如果您不再使用 Razor 来呈现您的 HTML,将会更好。

    在我的项目中,我设置了一个 razor 视图来呈现我的主页(我使用的是用 Angular 编写的单页应用程序),然后我有一个包含直接 .html 文件的文件夹,用作我的模板角度。

    在我的例子中,其余部分在 ASP.Net Web API 调用中完成,但您也可以使用带有 JSON 结果的 MVC 操作。

    一旦我切换到这个架构,事情就变得顺利多了,开发方面。

    【讨论】:

    • 我正在考虑沿着这条路线进行一个项目,但是您将如何处理验证?我想重新使用我在 C# 视图模型上设置的验证,并将它们传递给客户端 javascript 验证(例如 jquery.validate 或类似的东西)。另一种方法是在向 WebApi 控制器发送请求后从服务器返回错误并捕获它们以显示给用户。顺便说一句,我对 AngularJs 很陌生,但很想开始。
    • 就个人而言,我发现在客户端使用 Angular 的内置验证,然后在服务器上使用 .NET 的 DataAnnotations 验证非常简单。我认为尝试将 .NET 的东西下放到客户端可能最终会比它真正的价值更令人头疼。
    • +1 我 100% 同意 Blesh。如果您认为 AngularJS 和 MVC 的生活很艰难,请尝试与 Web 表单集成。绝对的噩梦。尝试在RadioButtonList 中添加ng-modelListItems?... 为您提供随机包装跨度标签!我决定完全放弃使用臃肿的 .NET 控件,而只使用标准的 HTML 元素。然后,我在回发时将表单值绑定到 POCO 模型,并让 DataAnnotations 负责服务器端验证 FTW。
    • @blesh,虽然我同意这可能是考虑到约束条件的最佳方法,但您会重复验证,因此可能会面临字段验证方式不一致的可能性。
    • 任何东西都可以发布到 API,这是一种通过网络进行交互的不同方式。提供服务并随心所欲地编写客户。所以无论如何你都会有这种可能性。从服务器生成客户端验证无论如何都是粗略的业务。
    【解决方案2】:

    我同意 blesh 关于远离 razor 的想法,但您可以创建一些工具来更快地创建页面。恕我直言,最好在需要的地方使用剃须刀功能,而不是将其从工具集中删除。

    顺便说一句,看看ngval。它将数据注释作为 angularjs 验证器带到客户端。它有一个 html 助手和一个角度模块。不得不提的是,该项目处于早期开发阶段。

    【讨论】:

    • ngval 看起来像一个选项。我不知道它是否支持 IClientValidatable。网站上的示例仅显示 DataAnnotation 属性。
    【解决方案3】:

    我编写了一个指令来平滑从 MVC 到 AngularJs 的过渡。标记看起来像:

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

    其行为与 Razor 约定相同,包括将验证延迟到修改字段之后。随着时间的推移,我发现维护我的标记非常直观和简单。

    My article on the subject

    Plinkr

    【讨论】:

      【解决方案4】:

      我认为可能有六种方法可以做你想做的事。可能最简单的方法是使用识别 jquery.validation 标记的 Angular 指令。

      这里有这样一个项目:https://github.com/mdekrey/unobtrusive-angular-validation

      还有一个:https://github.com/danicomas/angular-jquery-validate

      我也没有尝试过,因为就个人而言,我通过编写代码使 MVC 输出角度验证属性而不是 jquery.validation.unobtrusive 属性解决了这个问题。

      第三种选择是仅依赖服务器端验证。尽管这显然较慢,但有时对于更复杂的验证场景,它可能是您唯一的选择。在这种情况下,您只需编写 javascript 来解析 Web API 控制器通常返回的 ModelStateDictionary 对象。有一些示例说明如何做到这一点并将其集成到 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);
                          }
                      });
                  }
              };
          }]);
      

      ````

      我对这里提供的其他答案感到相当失望。当您尝试验证比电子邮件地址更困难的东西时,“不要这样做”并不是一个很好的建议。

      【讨论】:

      【解决方案5】:

      我以稍微不同的方式解决了这个问题。我修改了我的 MVC 应用程序以通过过滤器和自定义视图引擎响应 application/json 内容类型,该引擎将 Json 序列化器剃须刀模板注入到要搜索的视图位置。

      这样做是为了允许使用 jQuery UI、Bootstrap 和 Json 响应为相同的控制器/操作对我们的网站进行蒙皮。

      这是一个示例 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,为了代码库的简洁性和安全性,排除了 using 语句。这会尝试三次返回响应。首先是阅读原始的 View{controller}{action}.cshtml,解析出 html 助手并将它们放入表单和字段中。第二次尝试从我们的内置博客系统(下面的 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)
      }
      

      克隆方法见Best way to clone properties of disparate objects

          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;
      }
      

      【讨论】:

      • SafeClone 与提到的其他克隆方法类似,但不会复制标记为虚拟的属性。这有助于防止序列化期间的循环引用。
      猜你喜欢
      • 2010-09-22
      • 1970-01-01
      • 1970-01-01
      • 2014-07-05
      • 1970-01-01
      • 2014-07-29
      • 1970-01-01
      • 2013-11-29
      • 1970-01-01
      相关资源
      最近更新 更多