37
votes

I'm with a project in MVC 4 and AngularJS (+ twitter bootstrap). I usually use in my MVC projects "jQuery.Validate", "DataAnnotations" and "Razor". Then I enable these keys in my web.config to validate properties of model on the client:

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

For example if I have in my model this:

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

With this Cshtml:

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

The html result would:

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

But now when I use AngularJS, I want to render maybe like this:

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

I do not know if there are any helper or "Data Annotation" to resolve this. I understand that AngularJS has many more features like:

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

Well, specifically. I need some helper or "Data Annotation" to resolve the attributes (Data Annotation) for display on the client with AngularJS.

If it still does not exist, perhaps it is time to do, like RazorForAngularJS

Edit

I think perhaps the best way to work with ASP.NET MVC and AngularJS is do it (front-end) by hand (writing all the HTML by hand)

5

5 Answers

29
votes

As someone that's authored an ASP.Net/Angular website, I can tell you that you're going to be way better off stepping away from using Razor to render your HTML where you can.

In my projects I've set up one razor view to render my main page (I'm using a single page app written in Angular), then I have a folder of straight .html files that I use as my templates for Angular.

The rest is done in ASP.Net Web API calls in my case, but you can also use MVC action with JSON results.

As soon as I switched to this architecture, things went a lot more smoothly for me, development wise.

9
votes

I agree with blesh idea about stepping away from razor, but you can create some tools for creating pages more rapid. IMHO it is better to use razor features where they needed instead of removing it from out toolset.

BTW have a look at ngval. It brings data annotations to client side as angularjs validators. It has an html helper and an angular module. I have to mention that project is in early development stages.

4
votes

I wrote a directive to smooth out the transition from MVC to AngularJs. The markup looks like:

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

Which behaves identically to Razor conventions, including delaying validation until after a field is modified. Over time, I've found maintaining my markup pretty intuitive and simple.

My article on the subject

Plinkr

2
votes

I think there are probably half a dozen ways to do what you want. Probably the easiest is to use an Angular directive that recognizes jquery.validation markup.

Here is such a project: https://github.com/mdekrey/unobtrusive-angular-validation

And here is another: https://github.com/danicomas/angular-jquery-validate

I haven't tried either because personally, I solved this problem by writing code to make MVC output angular validation attributes instead of jquery.validation.unobtrusive attributes.

A 3rd option is to rely only on server side validation. Though this is obviously slower, it may be your only option sometimes for more complex validation scenarios. In this case, you just have to write javascript to parse the ModelStateDictionary object that Web API controllers usually return. There are some examples out there on how to do that and integrate it into AngularJS's native validation model.

Here is some incomplete code to parse the 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);
                    }
                });
            }
        };
    }]);

````

I'm rather disappointed with the other answers provided here. "Don't do it" isn't such a great suggestion when you're trying to validate something a little more difficult than an email address.

1
votes

I solved this in a slightly different way. I modified my MVC application to response to the application/json content type via a filter and a custom view engine which injects a Json serializer razor template into the view locations to search.

This was done to allow skinning of our website with jQuery UI, Bootstrap & Json responses for the same controllers/actions.

Here is a sample json result:

{
  "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"
      }
    ]
  }
}

The filter is used to translate redirect results into a json object which passes the next url onto the calling program:

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

Here is the Views\Json\Serializer.cshml, with using statements excluded for brevity and security of our codebase. This does three attempts at returning a response. The first is to read the original View{controller}{action}.cshtml, parsing out the html helpers and placing those into forms and fields. The second attempt looks for and elements from our built-in blogging system (PostContent below) and failing that we just use the Model.

@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)
}

For the clone methods see 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;
    }

And here is an implementation of key value coding to make dealing with property chains a bit easier:

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