Monday 20 May 2013

Id column of viewmodel not bound on Postback in MVC

This weekend I had an issue with the postback of a simple viewmodel object. For some reason the validation of this object failed even though the Id was not required.
     public class SkillViewModel
    {
        public int Id { get; set; }

        [Required]
        public virtual string Description { get; set; }
        [Required]
        public virtual string Name { get; set; }
    }
      [HttpPost]
        public ActionResult Create(SkillViewModel skill)
            if (ModelState.IsValid)
            {
                SkillRecord oRecord = new SkillRecord()
                {
                    Name = skill,
                    Description = skill.Description
                };

                SkillService.Create(oRecord);

                return RedirectToAction("Index");
            }

            return View(skill);
        }
To solve this issue, I started by adding some debugging code to the controller action to find out what was going wrong.
var errors = ModelState.SelectMany(e => e.Value.Errors);
This gave me the following error: "The Id field is required.". This is strange. Of course changing the name of the Id property works, but this is not what I want. So what is going on. In the create I am underposting, this means I am not posting the Id. This gave me problems in combination with my defined routes.
routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
Since I am not posting the Id from by razor view, the binding takes tries to get the id from the route, which is not there and cause a validation error. There are two solutions, either add '[Bind(Exclude = "Id")]' before the parameter in the Create method or add an additional route (before the existing one) without the id in it:
routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );

routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );