我正在使用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)
答案 0 :(得分:29)
作为一名撰写ASP.Net/Angular网站的人,我可以告诉你,如果你不想使用Razor来渲染你的HTML,那么你会更好。
在我的项目中,我设置了一个剃刀视图来渲染我的主页面(我使用的是一个用Angular编写的单页面应用程序),然后我有一个直接.html文件的文件夹,我用作我的模板角。
其余部分在我的情况下在ASP.Net Web API调用中完成,但您也可以使用带有JSON结果的MVC操作。
一旦我转向这种架构,对我来说事情就更顺利了,发展明智。
答案 1 :(得分:9)
我同意关于离开剃刀的肉体想法,但你可以创建一些工具来更快地创建页面。恕我直言,最好在他们需要的地方使用剃刀功能,而不是从工具集中删除它。
BTW看看ngval。它将数据注释作为angularjs验证器提供给客户端。它有一个html帮助器和一个角度模块。我必须提到该项目处于早期开发阶段。
答案 2 :(得分:4)
我写了一个指令来平滑从MVC到AngularJs的过渡。标记看起来像:
<validated-input name="username" display="User Name" ng-model="model.username" required>
其行为与Razor约定相同,包括在修改字段之后延迟验证。随着时间的推移,我发现保持我的标记非常直观和简单。
答案 3 :(得分:2)
我认为可能有六种方法可以做你想做的事。可能最简单的方法是使用一个识别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);
}
});
}
};
}]);
````
我对此处提供的其他答案感到非常失望。 &#34;不要这样做&#34;当您尝试验证比电子邮件地址更难的内容时,这不是一个很好的建议。
答案 4 :(得分:1)
我以稍微不同的方式解决了这个问题。我通过过滤器和自定义视图引擎修改了我的MVC应用程序以响应application / json内容类型,该引擎将Json序列化器剃刀模板注入视图位置进行搜索。
这样做是为了允许我们的网站使用jQuery UI,Bootstrap&amp; amp; 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 & 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;
}