Development With A Dot

Blog on development in general, and specifically on .NET

Sponsors

News

My Friends

My Links

Permanent Posts

Portuguese Communities

May 2012 - Posts

ASP.NET MVC CRUD Validation

One thing I didn’t refer on my previous post on ASP.NET MVC CRUD with AJAX was how to retrieve model validation information into the client.

We want to send any model validation errors to the client in the JSON object that contains the ProductId, RowVersion and Success properties, specifically, if there are any errors, we will add an extra Errors collection property. Here’s how:

   1: [HttpPost]
   2: [AjaxOnly]
   3: [Authorize]
   4: public JsonResult Edit(Product product)
   5: {
   6:     if (this.ModelState.IsValid == true)
   7:     {
   8:         using (ProductContext ctx = new ProductContext())
   9:         {
  10:             Boolean success = false;
  11:  
  12:             ctx.Entry(product).State = (product.ProductId == 0) ? EntityState.Added : EntityState.Modified;
  13:  
  14:             try
  15:             {
  16:                 success = (ctx.SaveChanges() == 1);
  17:             }
  18:             catch (DbUpdateConcurrencyException)
  19:             {
  20:                 ctx.Entry(product).Reload();
  21:             }
  22:  
  23:             return (this.Json(new { Success = success, ProductId = product.ProductId, RowVersion = Convert.ToBase64String(product.RowVersion) }));
  24:         }
  25:     }
  26:     else
  27:     {
  28:         Dictionary<String, String> errors = new Dictionary<String, String>();
  29:  
  30:         foreach (KeyValuePair<String, ModelState> keyValue in this.ModelState)
  31:         {
  32:             String key = keyValue.Key;
  33:             ModelState modelState = keyValue.Value;
  34:  
  35:             foreach (ModelError error in modelState.Errors)
  36:             {
  37:                 errors[key] = error.ErrorMessage;
  38:             }
  39:         }
  40:  
  41:         return (this.Json(new { Success = false, ProductId = 0, RowVersion = String.Empty, Errors = errors }));
  42:     }
  43: }

As for the view, we need to change slightly the onSuccess JavaScript handler on the Single view:

   1: function onSuccess(ctx)
   2: {
   3:     if (typeof (ctx.Success) != 'undefined')
   4:     {
   5:         $('input#ProductId').val(ctx.ProductId);
   6:         $('input#RowVersion').val(ctx.RowVersion);
   7:  
   8:         if (ctx.Success == false)
   9:         {
  10:             var errors = '';
  11:  
  12:             if (typeof (ctx.Errors) != 'undefined')
  13:             {
  14:                 for (var key in ctx.Errors)
  15:                 {
  16:                     errors += key + ': ' + ctx.Errors[key] + '\n';
  17:                 }
  18:  
  19:                 window.alert('An error occurred while updating the entity: the model contained the following errors.\n\n' + errors);
  20:             }
  21:             else
  22:             {
  23:                 window.alert('An error occurred while updating the entity: it may have been modified by third parties. Please try again.');
  24:             }
  25:         }
  26:         else
  27:         {
  28:             window.alert('Saved successfully');
  29:         }
  30:     }
  31:     else
  32:     {
  33:         if (window.confirm('Not logged in. Login now?') == true)
  34:         {
  35:             document.location.href = '<%
   1: : FormsAuthentication.LoginUrl 
%>?ReturnURL=' + document.location.pathname;
  36:         }
  37:     }
  38: }

The logic is as this:

  1. If the Edit action method is called for a new entity (the ProductId is 0) and it is valid, the entity is saved, and the JSON results contains a Success flag set to true, a ProductId property with the database-generated primary key and a RowVersion with the server-generated ROWVERSION;
  2. If the model is not valid, the JSON result will contain the Success flag set to false and the Errors collection populated with all the model validation errors;
  3. If the entity already exists in the database (ProductId not 0) and the model is valid, but the stored ROWVERSION is different that the one on the view, the result will set the Success property to false and will return the current (as loaded from the database) value of the ROWVERSION on the RowVersion property.

On a future post I will talk about the possibilities that exist for performing model validation, stay tuned!

Yet Another ASP.NET MVC CRUD Tutorial

I know that I have not posted much on MVC, mostly because I don’t use it on my daily life, but since I find it so interesting, and since it is gaining such popularity, I will be talking about it much more. This time, it’s about the most basic of scenarios: CRUD.

Although there are several ASP.NET MVC tutorials out there that cover ordinary CRUD operations, I couldn’t find any that would explain how we can have also AJAX, optimistic concurrency control and validation, using Entity Framework Code First, so I set out to write one! I won’t go into explaining what is MVC, Code First or optimistic concurrency control, or AJAX, I assume you are all familiar with these concepts by now.

Let’s consider an hypothetical use case, products. For simplicity, we only want to be able to either view a single product or edit this product.

First, we need our model:

   1: public class Product
   2: {
   3:     public Product()
   4:     {
   5:         this.Details = new HashSet<OrderDetail>();
   6:     }
   7:  
   8:     [Required]
   9:     [StringLength(50)]
  10:     public String Name
  11:     {
  12:         get;
  13:         set;
  14:     }
  15:  
  16:     [Key]
  17:     [ScaffoldColumn(false)]
  18:     [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
  19:     public Int32 ProductId
  20:     {
  21:         get;
  22:         set;
  23:     }
  24:  
  25:     [Required]
  26:     [Range(1, 100)]
  27:     public Decimal Price
  28:     {
  29:         get;
  30:         set;
  31:     }
  32:  
  33:     public virtual ISet<OrderDetail> Details
  34:     {
  35:         get;
  36:         protected set;
  37:     }
  38:  
  39:     [Timestamp]
  40:     [ScaffoldColumn(false)]
  41:     public Byte[] RowVersion
  42:     {
  43:         get;
  44:         set;
  45:     }
  46: }

Keep in mind that this is a simple scenario. Let’s see what we have:

  • A class Product, that maps to a product record on the database;
  • A product has a required (RequiredAttribute) Name property which can contain up to 50 characters (StringLengthAttribute);
  • The product’s Price must be a decimal value between 1 and 100 (RangeAttribute);
  • It contains a set of order details, for each time that it has been ordered, which we will not talk about (Details);
  • The record’s primary key (mapped to property ProductId) comes from a SQL Server IDENTITY column generated by the database (KeyAttribute, DatabaseGeneratedAttribute);
  • The table uses a SQL Server ROWVERSION (previously known as TIMESTAMP) column for optimistic concurrency control mapped to property RowVersion (TimestampAttribute).

Then we will need a controller for viewing product details, which will located on folder ~/Controllers under the name ProductController:

   1: public class ProductController : Controller
   2: {
   3:     [HttpGet]
   4:     public ViewResult Get(Int32 id = 0)
   5:     {
   6:         if (id != 0)
   7:         {
   8:             using (ProductContext ctx = new ProductContext())
   9:             {
  10:                 return (this.View("Single", ctx.Products.Find(id) ?? new Product()));
  11:             }
  12:         }
  13:         else
  14:         {
  15:             return (this.View("Single", new Product()));
  16:         }
  17:     }
  18: }

If the requested product does not exist, or one was not requested at all, one with default values will be returned. I am using a view named Single to display the product’s details, more on that later.

As you can see, it delegates the loading of products to an Entity Framework context, which is defined as:

   1: public class ProductContext: DbContext
   2: {
   3:     public DbSet<Product> Products
   4:     {
   5:         get;
   6:         set;
   7:     }
   8: }

Like I said before, I’ll keep it simple for now, only aggregate root Product is available.

The controller will use the standard routes defined by the Visual Studio ASP.NET MVC 3 template:

   1: routes.MapRoute(
   2:                 "Default", // Route name
   3:                 "{controller}/{action}/{id}", // URL with parameters
   4:                 new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
   5:             );

Next, we need a view for displaying the product details, let’s call it Single, and have it located under ~/Views/Product:

   1: <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<Product>" %>
   2: <!DOCTYPE html>
   3:  
   4: <html>
   5: <head runat="server">
   6:     <title>Product</title>
   7:     <script src="/Scripts/jquery-1.7.2.js" type="text/javascript"></script>
   1:  
   2:     <script src="/Scripts/jquery-ui-1.8.19.js" type="text/javascript">
   1: </script>
   2:     <script src="/Scripts/jquery.unobtrusive-ajax.js" type="text/javascript">
   1: </script>
   2:     <script src="/Scripts/jquery.validate.js" type="text/javascript">
   1: </script>
   2:     <script src="/Scripts/jquery.validate.unobtrusive.js" type="text/javascript">
   1: </script>
   2:     <script type="text/javascript">
   3:         function onFailure(error)
   4:         {
   5:         }
   6:  
   7:         function onComplete(ctx)
   8:         {
   9:         }
  10:  
  11:     
</script>
   8: </head>
   9: <body>
  10:     <div>
  11:         <%
   1: : this.Html.ValidationSummary(false) 
%>
  12:         <%
   1:  using (this.Ajax.BeginForm("Edit", "Product",  new AjaxOptions{ HttpMethod = FormMethod.Post.ToString(), OnSuccess = "onSuccess", OnFailure = "onFailure" })) { 
%>
  13:         <%
   1: : this.Html.EditorForModel() 
%>
  14:         <input type="submit" name="submit" value="Submit" />
  15:         <%
   1:  } 
%>
  16:     </div>
  17: </body>
  18: </html>

Yes… I am using ASPX syntax… sorry about that! Smile  And I'm using jQuery, not Microsoft's AJAX Library, it is possible to do the same with both, I just prefer jQuery.

I implemented an editor template for the Product class, which must be located on the ~/Views/Shared/EditorTemplates folder as file Product.ascx:

   1: <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Product>" %>
   2: <div>
   3:     <%: this.Html.HiddenFor(model => model.ProductId) %>
   4:     <%: this.Html.HiddenFor(model => model.RowVersion) %>
   5:     <fieldset>
   6:         <legend>Product</legend>
   7:         <div class="editor-label">
   8:             <%: this.Html.LabelFor(model => model.Name) %>
   9:         </div>
  10:         <div class="editor-field">
  11:             <%: this.Html.TextBoxFor(model => model.Name) %>
  12:             <%: this.Html.ValidationMessageFor(model => model.Name) %>
  13:         </div>
  14:         <div class="editor-label">
  15:             <%= this.Html.LabelFor(model => model.Price) %>
  16:         </div>
  17:         <div class="editor-field">
  18:             <%= this.Html.TextBoxFor(model => model.Price) %>
  19:             <%: this.Html.ValidationMessageFor(model => model.Price) %>
  20:         </div>
  21:     </fieldset>
  22: </div>

One thing you’ll notice is, I am including both the ProductId and the RowVersion properties as hidden fields; they will come handy later or, so that we know what product and version we are editing. The other thing is the included JavaScript files: jQuery, jQuery UI and unobtrusive validations. Also, I am not using the Content extension method for translating relative URLs, because that way I would lose JavaScript intellisense for jQuery functions.

OK, so, at this moment, I want to add support for AJAX and optimistic concurrency control. So I write a controller method like this:

   1: [HttpPost]
   2: [AjaxOnly]
   3: [Authorize]
   4: public JsonResult Edit(Product product)
   5: {
   6:     if (this.TryValidateModel(product) == true)
   7:     {
   8:         using (BlogContext ctx = new BlogContext())
   9:         {
  10:             Boolean success = false;
  11:  
  12:             ctx.Entry(product).State = (product.ProductId == 0) ? EntityState.Added : EntityState.Modified;
  13:  
  14:             try
  15:             {
  16:                 success = (ctx.SaveChanges() == 1);
  17:             }
  18:             catch (DbUpdateConcurrencyException)
  19:             {
  20:                 ctx.Entry(product).Reload();
  21:             }
  22:  
  23:             return (this.Json(new { Success = success, ProductId = product.ProductId, RowVersion = Convert.ToBase64String(product.RowVersion) }));
  24:         }
  25:     }
  26:     else
  27:     {
  28:         return (this.Json(new { Success = false, ProductId = 0, RowVersion = String.Empty }));
  29:     }
  30: }

So, this method is only valid for HTTP POST requests (HttpPost), coming from AJAX (AjaxOnly, from MVC Futures), and from authenticated users (Authorize). It returns a JSON object, which is what you would normally use for AJAX requests, containing three properties:

  • Success: a boolean flag;
  • RowVersion: the current version of the ROWVERSION column as a Base-64 string;
  • ProductId: the inserted product id, as coming from the database.

If the product is new, it will be inserted into the database, and its primary key will be returned into the ProductId property. Success will be set to true;

If a DbUpdateConcurrencyException occurs, it means that the value in the RowVersion property does not match the current ROWVERSION column value on the database, so the record must have been modified between the time that the page was loaded and the time we attempted to save the product. In this case, the controller just gets the new value from the database and returns it in the JSON object; Success will be false.

Otherwise, it will be updated, and Success, ProductId and RowVersion will all have their values set accordingly.

So let’s see how we can react to these situations on the client side. Specifically, we want to deal with these situations:

  • The user is not logged in when the update/create request is made, perhaps the cookie expired;
  • The optimistic concurrency check failed;
  • All went well.

So, let’s change our view:

   1: <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<Product>" %>
   2: <%@ Import Namespace="System.Web.Security" %>
   3:  
   4: <!DOCTYPE html>
   5:  
   6: <html>
   7: <head runat="server">
   8:     <title>Product</title>
   9:     <script src="/Scripts/jquery-1.7.2.js" type="text/javascript"></script>
   1:  
   2:     <script src="/Scripts/jquery-ui-1.8.19.js" type="text/javascript">
   1: </script>
   2:     <script src="/Scripts/jquery.unobtrusive-ajax.js" type="text/javascript">
   1: </script>
   2:     <script src="/Scripts/jquery.validate.js" type="text/javascript">
   1: </script>
   2:     <script src="/Scripts/jquery.validate.unobtrusive.js" type="text/javascript">
   1: </script>
   2:     <script type="text/javascript">
   3:         function onFailure(error)
   4:         {
   5:             window.alert('An error occurred: ' + error);
   6:         }
   7:  
   8:         function onSuccess(ctx)
   9:         {
  10:             if (typeof (ctx.Success) != 'undefined')
  11:             {
  12:                 $('input#ProductId').val(ctx.ProductId);
  13:                 $('input#RowVersion').val(ctx.RowVersion);
  14:  
  15:                 if (ctx.Success == false)
  16:                 {
  17:                     window.alert('An error occurred while updating the entity: it may have been modified by third parties. Please try again.');
  18:                 }
  19:                 else
  20:                 {
  21:                     window.alert('Saved successfully');
  22:                 }
  23:             }
  24:             else
  25:             {
  26:                 if (window.confirm('Not logged in. Login now?') == true)
  27:                 {
  28:                     document.location.href = '<%: FormsAuthentication.LoginUrl %>?ReturnURL=' + document.location.pathname;
  29:                 }
  30:             }
  31:         }
  32:  
  33:     
</script>
  10: </head>
  11: <body>
  12:     <div>
  13:         <%
   1: : this.Html.ValidationSummary(false) 
%>
  14:         <%
   1:  using (this.Ajax.BeginForm("Edit", "Product",  new AjaxOptions{ HttpMethod = FormMethod.Post.ToString(), OnSuccess = "onSuccess", OnFailure = "onFailure" })) { 
%>
  15:         <%
   1: : this.Html.EditorForModel() 
%>
  16:         <input type="submit" name="submit" value="Submit" />
  17:         <%
   1:  } 
%>
  18:     </div>
  19: </body>
  20: </html>

The implementation of the onSuccess function first checks if the response contains a Success property, if not, the most likely cause is the request was redirected to the login page (using Forms Authentication), because it wasn’t authenticated, so we navigate there as well, keeping the reference to the current page. It then saves the current values of the ProductId and RowVersion properties to their respective hidden fields. They will be sent on each successive post and will be used in determining if the request is for adding a new product or to updating an existing one.

The only thing missing is the ability to insert a new product, after inserting/editing an existing one, which can be easily achieved using this snippet:

   1: <input type="button" value="New" onclick="$('input#ProductId').val('');$('input#RowVersion').val('');"/>

And that’s it.

Site Map Provider Integrated Authorization

The ASP.NET out of the box included site map provider, XmlSiteMapProvider, allows specifying one or more required roles for each node it knows about. Let’s say you have something like this on your Web.sitemap file:

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
   3:     <siteMapNode url="/Default.aspx" title="Home">
   4:         <siteMapNode url="/About.aspx" title="About" />
   5:         <siteMapNode url="/SomePage.aspx" title="Some Page" roles="Administrator" />
   6:     </siteMapNode>
   7: </siteMap>

What it means is:

  • Page /Default.aspx is publicly accessible, since it has no roles attribute specified;
  • Page /About.aspx is also publicly accessible, for the same reason;
  • Page /SomePage.aspx can only be accessed by users with the Administrator role.

Of course, the site map provider doesn’t actually enforce these permissions; what it does is, if, for example, it is bound to a TreeView, it will only display the nodes that the current user is allowed to navigate to. But if someone attempts to navigate to /SomePage.aspx, unless a restriction is defined on the Web.config file, nothing will prevent it from happening.

Knowing this, I wrote a small module that fills in the gap. It strictly enforces what is specified in the site map provider – either XmlSiteMapProvider or any other –, no other restrictions are applied. If a restriction is added through the Web.config file, the access will also fail.

Here is the module:

   1: public class SiteMapAuthorizationModule : IHttpModule
   2: {
   3:     #region IHttpModule Members
   4:  
   5:     public void Dispose()
   6:     {            
   7:     }
   8:  
   9:     public void Init(HttpApplication context)
  10:     {
  11:         context.PostAcquireRequestState += this.OnPostAcquireRequestState;
  12:     }
  13:     
  14:     #endregion
  15:  
  16:     protected void OnPostAcquireRequestState(Object sender, EventArgs e)
  17:     {
  18:         AuthenticationSection section = WebConfigurationManager.OpenWebConfiguration("~").GetSection("system.web/authentication") as AuthenticationSection;
  19:         HttpApplication app = sender as HttpApplication;
  20:         SiteMapProvider provider = SiteMap.Provider;
  21:         SiteMapNode node = provider.CurrentNode;
  22:  
  23:         if (node != null)
  24:         {
  25:             if (node.Roles.Count != 0)
  26:             {
  27:                 if (node.Roles.OfType<String>().All(role => (role != "*") && (app.Context.User.IsInRole(role) == false)))
  28:                 {
  29:                     app.Context.Response.Buffer = true;
  30:                     app.Context.Response.Redirect(String.Format("{0}?ReturnUrl={1}", section.Forms.LoginUrl, HttpUtility.UrlEncode(app.Context.Request.Url.PathAndQuery)), false);
  31:                     app.Context.ApplicationInstance.CompleteRequest();
  32:                 }
  33:             }
  34:         }
  35:     }
  36: }

As you see, it allows specifying multiple roles, separated by commas, or the * symbol, in which case, all users will be granted access, and is effectively equivalent to not specifying a role at all.

You have to register it on Web.config, and it will use whatever site map provider is configured:

   1: <system.web>
   2:     <httpModules>
   3:         <!-- for IIS 6 -->
   4:         <add name="SiteMapAuthorization" type="MyNamespace.SiteMapAuthorizationModule, MyAssembly"/>
   5:     </httpModules>        
   6: </system.web>
   7: <system.webServer>
   8:     <!-- for IIS 7+ -->
   9:     <modules runAllManagedModulesForAllRequests="true">
  10:         <add name="SiteMapAuthorization" type="MyNamespace.SiteMapAuthorizationModule, MyAssembly"/>
  11:     </modules>
  12: </system.webServer>

As always, looking forward to hearing from you!

More Posts