Improving MVC UI with entity framework validation

In this recipe, we will leverage the data annotations to provide real-time feedback to the user, while those same annotations will validate objects before allowing them to be saved to the database.

Getting ready

We will be using the NuGet Package Manager to install the Entity Framework 4.1 assemblies.

The package installer can be found at http://nuget.codeplex.com/.

We will also be using a database for connecting to the data and updating it.

Open the Improving Custom Property Validation solution in the included source code examples.

How to do it...

Let's get connected to the database using the following steps:

  1. This is a recipe that deals specifically with the UI interaction, and therefore cannot be wrapped in testing. However, we can verify that the UI responds manually to the same responses that we get programmatically. So, we add the following test:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Validation;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using BusinessLogic;
    using DataAccess;
    using DataAccess.Database;
    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Test.Properties;
    using System.Data.Entity;
    
    namespace Test
    {
      [TestClass]
      public class ValidationTest
      {
        [TestMethod]
        [ExpectedException(typeof(DbEntityValidationException))]
        public void ShouldErrorOnTitleToLong()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
          StringBuilder builder = new StringBuilder();
          for (int i = 0; i < 20; i++)
          {
            builder.Append("This is going to be repeated");
          }
          var blog = new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Test",
            Title = builder.ToString()
          };
    
          //Act
          context.Set<Blog>().Add(blog);
          context.SaveChanges();
    
          //Assert
          Assert.Fail("Didn't Error");
        }
    
        [TestMethod]
        [ExpectedException(typeof(DbEntityValidationException))]
        public void ShouldErrorOnDescriptionRequired()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
          StringBuilder builder = new StringBuilder();
          var blog = new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = null,
            Title = "Test"
          };
    
          //Act
          context.Set<Blog>().Add(blog);
          context.SaveChanges();
    
          //Assert
          Assert.Fail("Didn't Error");
        }
    
        [TestMethod]
        [ExpectedException(typeof(DbEntityValidationException))]
        public void ShouldErrorOnDateOutsideAcceptableRange()
        {
          //Arrange
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
          StringBuilder builder = new StringBuilder();
          var blog = new Blog()
          {
            Creationdate = new DateTime(1890,1,1),
            ShortDescription = "Test",
            Title = "Test"
          };
    
          //Act
          context.Set<Blog>().Add(blog);
          context.SaveChanges();
    
          //Assert
          Assert.Fail("Didn't Error");
        }
    
        [TestMethod]
        [ExpectedException(typeof(DbEntityValidationException))]
        public void ShouldErrorOnRatingOutOfRange()
        {
          var init = new Initializer();
          var context = new BlogContext(Settings.Default.BlogConnection);
          init.InitializeDatabase(context);
          var blog = new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Test",
            Title = "Test",
            Rating = 6.0
          };
    
          //Act
          context.Set<Blog>().Add(blog);
          context.SaveChanges();
    
          //Assert
          Assert.Fail("Didn't Error");
    
        }
    
      }
    
    }
  2. Add an initializer to the DataAccess project in the Database folder with the following code to set up the data:
    using System;
    using System.Data.Entity;
    using BusinessLogic;
    
    namespace DataAccess.Database
    {
    
      public class Initializer : DropCreateDatabaseAlways<BlogContext>
      {
        public Initializer()
        {
    
        }
        protected override void Seed(BlogContext context)
        {
          context.Set<Blog>().Add(new Blog()
          {
            Creationdate = DateTime.Now,
            ShortDescription = "Testing",
            Title = "Test Blog"
          });
          context.SaveChanges();
        }
      }
    }
  3. In the BusinessLogic project, add a new C# class named Blog with the following code:
    using System;
    using System.ComponentModel.DataAnnotations;
    using System.Text.RegularExpressions;
    
    namespace BusinessLogic
    {
      public class Blog
      {
        private const string DateBetween1900And2100Pattern = @"^(19|20)dd[- /.](0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$";
    
        public int Id { get; set; }
    
        [RegularExpression(pattern: DateBetween1900And2100Pattern,ErrorMessage = "Date is not valid between 1900 and 2100")]
    
        public DateTime Creationdate { get; set; }
    
        [Required]
        public string ShortDescription { get; set; }
    
        [Required]
        [StringLength(120,ErrorMessage = "Title is To Long")]
        public string Title { get; set; }
    
        [Range(0.0,5.0, ErrorMessage = "Invalid Range, must be between 0 and 5")]
        public double Rating { get; set; }
      }
    }
  4. Add a Mapping folder to the DataAccess project, and add a BlogMapping class to the folder with the following code:
    using System.ComponentModel.DataAnnotations;
    using System.Data.Entity.ModelConfiguration;
    using BusinessLogic;
    
    namespace DataAccess.Mappings
    {
      public class BlogMapping : EntityTypeConfiguration<Blog>
      {
        public BlogMapping()
        {
          this.ToTable("Blogs");
          this.HasKey(x => x.Id);
    
          this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("BlogId");
    
          this.Property(x => x.Title).IsRequired().HasMaxLength(250);
    
          this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired();
    
          this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description");
        }
    
      }
    }
  5. Modify the BlogContext class to contain the new mappings, and a DbSet property for Blog with the following code:
    using System;
    using System.Data.Entity;
    using System.Linq;
    using BusinessLogic;
    using DataAccess.Mappings;
    
    namespace DataAccess
    {
      public class BlogContext : DbContext, IUnitOfWork
      {
        public BlogContext(string connectionString) : base(connectionString)
        {
    
        }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
          modelBuilder.Configurations.Add(new BlogMapping());
          base.OnModelCreating(modelBuilder);
        }
        public IQueryable<T> Find<T>() where T : class
        {
          return this.Set<T>();
        }
    
        public void Refresh()
        {
          this.ChangeTracker.Entries().ToList().ForEach(x=>x.Reload());
        }
    
        public void Commit()
        {
          this.SaveChanges();
        }
      }
    }
  6. Run our test, and see how it works.
  7. Modify the BlogController with the following code:
    using System;
    using System.Data.Entity.Validation;
    using System.Linq;
    using System.Web.Mvc;
    using BusinessLogic;
    using DataAccess;
    using UI.Properties;
    
    namespace UI.Controllers
    {
      public class BlogController : Controller
      {
        private IBlogRepository _blogRepository;
    
        public BlogController() : this(new BlogRepository(new BlogContext(Settings.Default.BlogConnection))) { }
    
        public BlogController(IBlogRepository blogRepository)
        {
          _blogRepository = blogRepository;
        }
    
        // GET: /Blog/
        public ActionResult Display()
        {
          Blog blog = _blogRepository.Set<Blog>().First();
          return View(blog);
        }
    
        public ActionResult Create()
        {
          return View(new Blog());
        }
    
        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")]Blog blog)
        {
          if (!ModelState.IsValid) return View();
          return RedirectToAction("Display");
        }
    
      }
    
    }
  8. Run the application, and we should get some verification on the UI from the same validation that executes from the entity framework side.
    How to do it...

How it works...

We start off by specifying a test that will validate the data annotations on the database side. In this example, we will also have to validate the client-side feedback manually to ensure that we have met our intent.

The Blog object is restricted with several simple validations that will give some error messages to the MVC View when the user changes inputs and the validation runs. The details of these restrictions, for our example, are not drastically important, but they give us a framework to test against.

We have added a couple Create methods to the BlogController, so we can add a Create view method that is strongly tied to Blog, and use the Create template method. This will present the UI, and set up the validation message handlers.

The MVC framework will use the validation attributes to put messages on the screen as an instant feedback to the user.

There's more...

Validations like these are generated amazingly simply in an MVC user experience, but we need to understand how that is accomplished.

Understanding the Html helper

The Html helper that enables our validation message to be displayed for a property, must be used to display the message from our attribute. The editor for the helper will not display on its own. This also requires sending the business objects to the UI in strongly-typed views. One of the key features of Code First is that we can use the objects throughout our code base, because they are not tied to our database structure.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset