ASP.NET MVC Validation form with AngularJS
je suis avec un projet en MVC 4 et AngularJS (+ bootstrap twitter). J'utilise habituellement dans mes projets MVC "jQuery.Validate", "DataAnnotations"et " Razor". Puis j'active ces clés dans mon web.config pour valider les propriétés de modèle sur le client:
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
Par exemple, si j'ai dans mon modèle:
[Required]
[Display(Name = "Your name")]
public string Name { get; set; }
avec ce Cshtml:
@Html.LabelFor(model => model.Name)
@Html.TextBoxFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
le résultat html serait:
<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>
mais maintenant quand J'utilise AngularJS, je veux rendre peut-être comme ceci:
<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>
Je ne sais pas s'il existe un helper ou une" Annotation de données " pour résoudre ce problème. Je comprends que AngularJS a beaucoup plus de caractéristiques comme:
<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>
en particulier. J'ai besoin d'aide ou "Annotation de données" pour résoudre les attributs (Annotation de données) pour l'affichage sur le client avec AngularJS.
si elle n'existe toujours pas, peut-être est-il temps de le faire, comme RazorForAngularJS
Modifier
je pense que peut-être la meilleure façon de travailler avec ASP.NET MVC et AngularJS est de le faire ( front-end
) par part (écrit tout le code HTML à la main)
6 réponses
comme quelqu'un qui a écrit un ASP.Net/Angular site web, Je peux vous dire que vous allez être beaucoup mieux de se retirer de L'utilisation de Razor pour rendre votre HTML où vous le pouvez.
dans mes projets j'ai mis en place une vue de rasoir pour rendre ma page principale (j'utilise une application d'une page simple écrite en angulaire), puis j'ai un dossier de droite .les fichiers html que j'utilise comme mes modèles Angulaire.
le reste est fait en ASP.Net appels D'API Web dans mon cas, mais vous pouvez également utiliser L'action MVC avec les résultats JSON.
dès que je suis passé à cette architecture, les choses se sont beaucoup mieux passées pour moi, du point de vue du développement.
je suis d'accord avec l'idée de blesh de s'éloigner de razor, mais vous pouvez créer quelques outils pour créer des pages plus rapide. IMHO il est préférable d'utiliser des caractéristiques de rasoir où ils ont besoin au lieu de le retirer de l'ensemble d'outils.
BTW ont un coup d'oeil à ngval . Il apporte des annotations de données côté client comme validateurs angularjs. Il a un helper html et un module angulaire. Je dois mentionner que le projet est dans les premiers stades de développement.
j'ai écrit une directive pour lisser la transition de MVC à AngularJs. Le markup ressemble à:
<validated-input name="username" display="User Name" ng-model="model.username" required>
qui se comporte de façon identique aux conventions de Razor, y compris retarder la validation jusqu'à ce qu'un champ soit modifié. Avec le temps, j'ai trouvé le maintien de ma marge assez intuitif et simple.
je pense qu'il y a probablement une demi-douzaine de façons de faire ce que vous voulez. Probablement le plus facile est d'utiliser une directive angulaire qui reconnaît jquery.marque de validation.
voici un tel projet: https://github.com/mdekrey/unobtrusive-angular-validation
et en voici un autre: https://github.com/danicomas/angular-jquery-validate
Je n'ai pas essayé non plus parce que personnellement, j'ai résolu ce problème en écrivant du code pour faire des attributs de validation angulaire de sortie MVC au lieu de jquery.validation.discrète attributs.
une troisième option consiste à ne s'appuyer que sur la validation côté serveur. Bien que cela soit évidemment plus lent, cela peut parfois être votre seule option pour des scénarios de validation plus complexes. Dans ce cas, il vous suffit d'écrire javascript pour analyser L'objet ModelStateDictionary que les contrôleurs D'API Web renvoient habituellement. Il y a quelques des exemples sur la façon de le faire et de l'intégrer dans le modèle de validation natif D'AngularJS.
voici un code incomplet pour analyser le 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);
}
});
}
};
}]);
` "
je suis plutôt déçu par les autres réponses fournies ici. "Ne le faites pas" n'est pas une si bonne suggestion quand vous essayez de valider quelque chose un peu plus difficile qu'une adresse e-mail.
j'ai résolu cela d'une manière légèrement différente. J'ai modifié mon application MVC pour répondre au type de contenu application/json via un filtre et un moteur de vue personnalisé qui injecte un modèle de rasoir serializer Json dans les emplacements de vue pour la recherche.
cela a été fait pour permettre le dépouillement de notre site Web avec les réponses jQuery UI, Bootstrap & Json pour les mêmes contrôleurs/actions.
voici un exemple de résultat 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"
}
]
}
}
le filtre est utilisé pour traduire les résultats de redirection dans un objet json qui passe l'url suivante sur le programme appelant:
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)
};
}
Voici les vues\JSON\Serializer.cshml, avec utilisation de déclarations exclues pour la brièveté et la sécurité de notre base de données. Cela fait trois tentatives de retourner une réponse. La première est de lire la vue d'origine{controller}{action}.cshtml, en analysant les helpers html et en les plaçant dans les formulaires et les champs. La deuxième tentative recherche et les éléments de notre système de blogging intégré (PostContent ci-dessous) et à défaut, nous n'utilisons que le modèle.
@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)
}
Pour le clone méthodes, voir le Meilleur moyen de cloner les propriétés des différents objets
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;
}
et voici une mise en œuvre du codage de valeur clé pour rendre le traitement des chaînes de propriété un peu plus facile:
/// <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;
}
je pense que c'est une question Angular beginners will put (this is how I found it :)), et c'est pourquoi je pense qu'il mérite une réponse qui explique peut-être les auteurs éditent et l'espoir aide ceux qui se posent la même question et je sorte de reformuler: Comment puis-je garder la cohérence entre la validation angulaire et la validation du modèle mvc?.
le champ model à valider va de la vue angulaire - > vers un contrôleur angulaire - > vers un service angulaire - > vers ASP.méthode webapi ou asp.MVC controller action, qui, à la fin des correspond -> à un modèle mvc
Cela signifie que, sur tous ceux (au moins 4 "projecteurs"), vous devez être sûr de transférer le modèle exact et le champ que vous consultez avec le rasoir.
Donc, ce que je veux dire, c'est qu'il y a beaucoup de choses qui peuvent souffler votre cohérence sur le chemin.
donc je suis d'accord avec: les réécrire manuellement pour le côté client, et utiliser des tests automatisés pour assurer la cohérence