Sunday, January 17, 2016

Model bind to a more verbose child in the MVC paradigm before upcasting to the parent.

Our parent:

using System;
using System.ComponentModel;
namespace CatchingExperiment.Models
{
   public class Person
   {
      public string Name { get; set; }
      
      [DisplayName("Place of Birth")]
      public string PlaceOfBirth { get; set; }
      
      public DateTime? Birthday { get; set; }
   }
}

 
 

Our child:

using System.ComponentModel;
namespace CatchingExperiment.Models
{
   public class PersonAdditionHelper : Person
   {
      [DisplayName("New Infant?")]
      public bool IsNewInfant { get; set; }
      
      [DisplayName("Use Existing Place of Birth?")]
      public string ExistingPlaceOfBirth { get; set; }
   }
}

 
 

What will we do with these? Person is a domain object commonly used in our fictional application, but PersonAdditionHelper is a DTO of sorts for just helping us add a new Person. The reason for the child to begin with might be to accommodate a form which does not merely have fields which map one-to-one with the parent, a from with extra fields. An example:

@{
   Layout = null;
}
@using CatchingExperiment.Models
@model PersonAdditionHelper
<html>
   <head><title>Whatever</title></head>
   <body>
      @using (Html.BeginForm("Catch", "Home", FormMethod.Post))
      {
         <div>
            @Html.LabelFor(m => m.Name)
            @Html.TextBoxFor(m => m.Name)
         </div>
         <div>
            @Html.LabelFor(m => m.Birthday)
            @Html.TextBoxFor(m => m.Birthday, new { type = "date"})
         </div>
         <div>
            @Html.LabelFor(m => m.PlaceOfBirth)
            @Html.TextBoxFor(m => m.PlaceOfBirth)
         </div>
         <div>
            @Html.LabelFor(m => m.ExistingPlaceOfBirth)
            @{
               List<SelectListItem> existingItems = ViewBag.ExistingItems;
            }
            @Html.DropDownListFor(m => m.ExistingPlaceOfBirth, existingItems)
         </div>
         <div>
            @Html.LabelFor(m => m.IsNewInfant)
            @Html.CheckBoxFor(m => m.IsNewInfant)
         </div>
         <input type="submit" value="Go" />
      }
   </body>
</html>

 
 

Alright, we have a way to just define the person as "brand new" in lieu of setting a birthday and we have a way of picking from a list of existing locales for PlaceOfBirth. The simple form gives us more than three controls for configuring what is ultimately three fields.

 
 

As you can see, we cannot just map the fields shown to the Person object so we will map it to PersonAdditionHelper instead like so:

using System;
using System.Collections.Generic;
using System.Web.Mvc;
using CatchingExperiment.Models;
using CatchingExperiment.Utilities;
namespace CatchingExperiment.Controllers
{
   public class HomeController : Controller
   {
      public ActionResult Index()
      {
         List<SelectListItem> existingPlaces = new List<SelectListItem>();
         existingPlaces.Add(new SelectListItem()
         {
            Text = "",
            Value = ""
         });
         foreach (string existingPlace in Repository.GetExistingPlaces())
         {
            existingPlaces.Add(new SelectListItem()
            {
               Text = existingPlace,
               Value = existingPlace
            });
         }
         ViewBag.ExistingItems = existingPlaces;
         return View(new PersonAdditionHelper());
      }
      
      public ActionResult Catch(PersonAdditionHelper personAdditionHelper)
      {
         Person person = (Person)personAdditionHelper;
         if (personAdditionHelper.IsNewInfant)
         {
            person.Birthday = DateTime.Now;
         }
         if (!String.IsNullOrWhiteSpace(personAdditionHelper.ExistingPlaceOfBirth))
         {
            person.PlaceOfBirth = personAdditionHelper.ExistingPlaceOfBirth;
         }
         Repository.SavePerson(person);
         return View(person);
      }
   }
}

 
 

At our controller we catch the child at the Catch action (where our form posts to) and then, after upcasting the child to the parent type, we massage the parent a little bit. Make sense? We can get this under test like so:

using System;
using System.Web.Mvc;
using CatchingExperiment.Controllers;
using CatchingExperiment.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace CatchingExperiment.Tests.Controllers
{
   [TestClass]
   public class HomeControllerTests
   {
      private HomeController _controller;
      private PersonAdditionHelper _person;
      
      [TestInitialize()]
      public void HomeControllerTestsInitialize()
      {
         _controller = new HomeController();
         _person = new PersonAdditionHelper()
         {
            Name = "Tom",
            PlaceOfBirth = "Florida",
            Birthday = new DateTime(1974,8,24)
         };
      }
      
      [TestMethod]
      public void happy_pass()
      {
         var actionResult = _controller.Catch(_person);
         var viewResult = (ViewResult)actionResult;
         var person = ((Person)viewResult.ViewData.Model);
         Assert.AreEqual(person.Name, "Tom");
         Assert.AreEqual(person.PlaceOfBirth, "Florida");
         Assert.AreEqual(person.Birthday, new DateTime(1974, 8, 24));
      }
      
      [TestMethod]
      public void use_of_existing_locale_behaves_as_expected()
      {
         _person.ExistingPlaceOfBirth = "Texas";
         var actionResult = _controller.Catch(_person);
         var viewResult = (ViewResult)actionResult;
         var person = ((Person)viewResult.ViewData.Model);
         Assert.AreEqual(person.Name, "Tom");
         Assert.AreEqual(person.PlaceOfBirth, "Texas");
         Assert.AreEqual(person.Birthday, new DateTime(1974, 8, 24));
      }
      
      [TestMethod]
      public void new_infant_feature_behaves_as_expected()
      {
         _person.IsNewInfant = true;
         var actionResult = _controller.Catch(_person);
         var viewResult = (ViewResult)actionResult;
         var person = ((Person)viewResult.ViewData.Model);
         Assert.AreEqual(person.Name, "Tom");
         Assert.AreEqual(person.PlaceOfBirth, "Florida");
         Assert.AreNotEqual(person.Birthday, new DateTime(1974, 8, 24));
         Assert.IsTrue(person.Birthday > DateTime.Now.AddHours(-1));
         Assert.IsTrue(person.Birthday < DateTime.Now.AddHours(1));
      }
   }
}

 
 

Things to keep in mind about the tests:

  1. It's hard to test around DateTime.Now as this should really be an external dependency that is mockable which is looped in from elsewhere.
  2. I had to add the following references to the test project to get it to work:
    • the UI project
    • System.Web.Mvc
    • System.Web.WebPages

No comments:

Post a Comment