With this post I’m continuing my series of articles on Code-First development and workflow. In previous articles I developed a complete domain model for a project management application. This model includes POCO classes, that define each of the entities in the project management domain, along with a database context model defined in Entity Framework. Now I’m ready to build a web application based on this domain model, utilizing the Model View Controller (MVC) design pattern.
At first I decided to build the website using the ASP.NET Web Forms Application template in Visual studio 2012. I figured, since I already developed the domain model, and had Entity Framework generating the object model, I could simply hand craft the controller and view components and add them to the website in separate folders or class libraries. After all, the domain model consists of only nine entities;
- Customer
- Industry
- Person
- Project
- Rate
- Task
- Timesheet
- Timesheet Entry
- Title
However, it didn’t take long to realize, that developing and unit testing all of the classes and methods, required to support the controllers and views for each entity, would be an overwhelming task for one person. Consider this, each entity requires one controller class. Each controller class requires eight action methods to support the Create, Read, Update, Delete (CRUD) operations, which are necessary to maintain each entity of the domain. Each CRUD operation must be supported by a separate view in the user interface. Here is the minimal inventory of components required to support the MVC design pattern with a model consisting of 9 entities;
- 9 Controller Classes
- 72 Action Methods
- 45 View Classes
- 126 Total components
The minimal number of components required to support an MVC design pattern over a domain model with 9 entities is 126. I estimate this amounts to over 3,000 lines of code. This is an overwhelming coding task for one person. However, MVC is a standardized, repeatable pattern. I decided to take some time to look into automating this development effort.
ASP.NET MVC 4
I changed my strategy, and restarted development of the project management website, using the ASP.NET MVC 4 Web Application template in Visual Studio 2012. I’m not going to get into all of the details of how to build the website using the MVC 4 website template. Instead, I will describe how I incorporated the project management domain model, which I had previously developed, into the MVC 4 website template. However, if you are new to ASP.NET MVC 4, here is an excellent step-by-step tutorial on building an ASP.NET MVC 4 web application called Intro to ASP.NET MVC 4.
Integration Of The Project Management Domain Model Into ASP.NET MVC 4
The ASP.NET MVC 4 Web Application template, generates a fully functional web application based on the MVC design pattern. The website project generated by the template provides a folder called Models. I simply added a C# file to the Models folder and named it ProjectManagementModels.cs. I also declared a name space of Lexicon.ActionManager.Models in the C# file. Then I copied all the source code, of the project management domain model POCO classes, into this one C# file. The result is illustrated below. Notice how the solution explorer in Visual Studio 2012 makes navigation of this rather large source code file very easy.
I also copied the database context class into this same C# file, as illustrated here.
Here is the entire source code file ProjectManagementModels.cs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 |
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity; using System.Data.Entity.ModelConfiguration; using System.Globalization; using System.Web.Mvc; using System.Linq; using System.Web; namespace Lexicon.ActionManager.Models { public class Customer { public int CustomerId { get; set; } [Required(ErrorMessage = "Customer name is required")] [StringLength(50, ErrorMessage = "The customer name is too long")] public string Name { get; set; } [Required(ErrorMessage = "Contact person is required")] [StringLength(50, ErrorMessage = " The contact person name is too long")] public string ContactPerson { get; set; } [Required(ErrorMessage = "Phone number is required")] [DataType(DataType.PhoneNumber, ErrorMessage = "Phone number is not in the correct format")] public string Phone { get; set; } [DataType(DataType.PhoneNumber, ErrorMessage = "Fax number is not in the correct format")] public string Fax { get; set; } [Required(ErrorMessage = "Email address is required")] [DataType(DataType.EmailAddress, ErrorMessage = "The email address is not in the correct format")] public string EmailAddress { get; set; } public int Industry_Id { get; set; } [ForeignKey("Industry_Id")] public virtual Industry Industry { get; set; } public virtual List<Project> Projects { get; set; } } public class Industry { public int IndustryId { get; set; } [Required(ErrorMessage = "Industry name is required")] public string Name { get; set; } public virtual List<Customer> Customers { get; set; } } public class Person { public int PersonId { get; set; } [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } public string MiddleName { get; set; } public int Title_Id { get; set; } [ForeignKey("Title_Id")] public virtual Title Title { get; set; } public virtual List<TimeSheet> Timesheets { get; set; } } public class Project { public int ProjectId { get; set; } [Required] public string Name { get; set; } public string Description { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public double BudgetedCost { get; set; } public double ActualCost { get; set; } public double EstimatedHours { get; set; } public double ActualHours { get; set; } public double BillableHours { get; set; } public double PercentEstHours { get; set; } public double PercentBudgetCost { get; set; } public int Customer_Id { get; set; } [ForeignKey("Customer_Id")] public virtual Customer Customer { get; set; } public virtual List<Task> Tasks { get; set; } public virtual List<Rate> Rates { get; set; } public virtual List<TimeSheetEntry> TimeSheetEntries { get; set; } } public class Rate { public int RateId { get; set; } [Required] public decimal HourlyRate { get; set; } public int Title_Id { get; set; } [ForeignKey("Title_Id")] public virtual Title Title { get; set; } public int Project_Id { get; set; } [ForeignKey("Project_Id")] public virtual Project Project { get; set; } } public class Task { public virtual int TaskId { get; set; } [Required] public virtual string Name { get; set; } public virtual string Description { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public double BudgetedCost { get; set; } public double ActualCost { get; set; } public double EstimatedHours { get; set; } public double ActualHours { get; set; } public double BillableHours { get; set; } public double PercentEstHours { get; set; } public double PercentBudgetCost { get; set; } public virtual List<Project> Projects { get; set; } } public class TimeSheet { public int TimeSheetId { get; set; } [Required] public string Description { get; set; } public DateTime FromDate { get; set; } public DateTime ToDate { get; set; } public int Person_Id { get; set; } [ForeignKey("Person_Id")] public virtual Person Person { get; set; } public virtual List<TimeSheetEntry> TimeSheetEntries { get; set; } } public class TimeSheetEntry { public int TimeSheetEntryId { get; set; } [Required] public DateTime Date { get; set; } [Required] public decimal Hours { get; set; } public int Timesheet_Id { get; set; } [ForeignKey("Timesheet_Id")] public virtual TimeSheet TimeSheet { get; set; } public int Project_Id { get; set; } [ForeignKey("Project_Id")] public virtual Project Project { get; set; } public int Task_Id { get; set; } [ForeignKey("Task_Id")] public virtual Task Task { get; set; } } public class Title { public int TitleId { get; set; } [Required] public string JobTitle { get; set; } public string JobDescription { get; set; } public virtual List<Rate> Rates { get; set; } } public class ProjectManagementContext : DbContext { protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); #region Make foreign key relationships required (not nullable) modelBuilder.Entity<Project>().HasRequired(c => c.Customer); modelBuilder.Entity<Customer>().HasRequired(i => i.Industry); modelBuilder.Entity<Person>().HasRequired(t => t.Title); modelBuilder.Entity<Rate>().HasRequired(t => t.Title); modelBuilder.Entity<Rate>().HasRequired(p => p.Project); modelBuilder.Entity<TimeSheetEntry>().HasRequired(ts => ts.TimeSheet); modelBuilder.Entity<TimeSheetEntry>().HasRequired(p => p.Project); modelBuilder.Entity<TimeSheetEntry>().HasRequired(tk => tk.Task); modelBuilder.Entity<TimeSheet>().HasRequired(pr => pr.Person); #endregion #region Defining all entity relationships in Fluent API //modelBuilder.Entity<Industry>().HasMany<Customer>(c => c.Customers) // .WithRequired() // .WillCascadeOnDelete(); //modelBuilder.Entity<Customer>().HasMany<Project>(p => p.Projects) // .WithRequired() // .WillCascadeOnDelete(); //modelBuilder.Entity<Person>().HasMany<TimeSheet>(tsh => tsh.Timesheets) // .WithRequired() // .WillCascadeOnDelete(); //modelBuilder.Entity<Project>().HasMany<TimeSheetEntry>(ptse => ptse.TimeSheetEntries) // .WithRequired() // .WillCascadeOnDelete(); //modelBuilder.Entity<TimeSheet>().HasMany<TimeSheetEntry>(tse => tse.TimeSheetEntries) //.WithRequired() //.WillCascadeOnDelete(); //modelBuilder.Entity<Title>().HasMany<Rate>(tr => tr.Rates) // .WithRequired() // .WillCascadeOnDelete(); //modelBuilder.Entity<Project>().HasMany<Rate>(pr => pr.Rates) // .WithRequired() // .WillCascadeOnDelete(); #endregion } public DbSet<Customer> Customers { get; set; } public DbSet<Person> Persons { get; set; } public DbSet<Project> Projects { get; set; } public DbSet<Task> Tasks { get; set; } public DbSet<Rate> Rates { get; set; } public DbSet<TimeSheet> TimeSheets { get; set; } public DbSet<TimeSheetEntry> TimeSheetEntries { get; set; } public DbSet<Title> Titles { get; set; } public DbSet<Industry> Industries { get; set; } } public class ProjectConfiguration : EntityTypeConfiguration<Project> { // Establish the many to many relationship between projects and tasks internal ProjectConfiguration() { this.HasMany(p => p.Tasks) .WithMany(t => t.Projects) .Map(mc => { mc.MapLeftKey("ProjectId"); mc.MapRightKey("TaskId"); mc.ToTable("ProjectTask"); }); } } } |
Adding Controllers And Views For The Project Management Domain Model Into ASP.NET MVC 4
Integrating the project management domain model into ASP.NET MVC 4 was easy enough. Adding the necessary controller and view components was even easier. To add a controller class, simply right-click on the Controllers folder in the Solution Explorer of Visual Studio. This will present a context menu. Hover the mouse pointer over the add menu option to display the sub-context menu, and then click on the controller option. This will display the Add Controller dialogue box, as illustrated in Figure 3.
In the Add Controller dialogue box, first give the new controller class a name. To follow the configuration over convention style of developing, which is supported in ASP.NET MVC 4, the controller name should be the same as the model entity it controls, with a suffix of Controller. In this case, I’m generating a controller class for the project domain entity of the model. Therefore, I simply named the new controller class “ProjectController”.
The Add Controller dialogue box offers numerous Scaffolding options – see Figure 3. This gives the developer total control over the amount of automation and scaffolding generated by ASP.NET MVC 4. The Scaffolding Options are presented in three drop down list boxes;
- Template – a list of scaffolding template options
- Model Class – a list of all the entity classes defined in the model
- Data Context Class – a list of all classes in the model, including the Entity Framework database context class, which defines the object model
- For Template, I want the highest level of automation and generated scaffolding in my project, so I selected the “MVC controller with read/write actions and views, using Entity Framework”.
- For Model Class, I selected the “Project” class option from the model
- For Data Context Class, I selected the “ProjectManagementContext” class option from the model
Finally, after selecting the three scaffolding options, simply click the add button and the ProjectController class is automatically generated, with the eight action methods implementing the CRUD operations to maintain Project objects. The ProjectController class is automatically added to the Controllers folder. The five view classes, which provide the user interface support for the CRUD operations, are also automatically generated and added to the Views folder in the solution explorer. Everything was automatically generated according to specifications I selected in the Add Controller dialogue box.
The results of the generated scaffolding are impressive. The ProjectController class has eight action methods and 128 lines of code. Five views were automatically generated with 553 lines of code. All of this code is fully functional right out of the box.
Here is the generated code of the ProjectController class;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
public class ProjectController : Controller { private ProjectManagementContext db = new ProjectManagementContext(); // // GET: /Project/ public ActionResult Index() { var projects = db.Projects.Include(p => p.Customer); return View(projects.ToList()); } // // GET: /Project/Details/5 public ActionResult Details(int id = 0) { Project project = db.Projects.Find(id); if (project == null) { return HttpNotFound(); } return View(project); } // // GET: /Project/Create public ActionResult Create() { ViewBag.Customer_Id = new SelectList(db.Customers, "CustomerId", "Name"); return View(); } // // POST: /Project/Create [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Project project) { if (ModelState.IsValid) { db.Projects.Add(project); db.SaveChanges(); return RedirectToAction("Index"); } ViewBag.Customer_Id = new SelectList(db.Customers, "CustomerId", "Name", project.Customer_Id); return View(project); } // // GET: /Project/Edit/5 public ActionResult Edit(int id = 0) { Project project = db.Projects.Find(id); if (project == null) { return HttpNotFound(); } ViewBag.Customer_Id = new SelectList(db.Customers, "CustomerId", "Name", project.Customer_Id); return View(project); } // // POST: /Project/Edit/5 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(Project project) { if (ModelState.IsValid) { db.Entry(project).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } ViewBag.Customer_Id = new SelectList(db.Customers, "CustomerId", "Name", project.Customer_Id); return View(project); } // // GET: /Project/Delete/5 public ActionResult Delete(int id = 0) { Project project = db.Projects.Find(id); if (project == null) { return HttpNotFound(); } return View(project); } // // POST: /Project/Delete/5 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { Project project = db.Projects.Find(id); db.Projects.Remove(project); db.SaveChanges(); return RedirectToAction("Index"); } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } } } |
Conclusions
The development approach I outline in this article probably isn’t for everyone. Some may argue that a certain level of control and flexibility, in structuring an application, is sacrificed with this approach. However, small shops and one person operations, can definitely benefit from the efficiency gained by the automatically generated application scaffolding. There is also a significant improvement in quality, and testing efficiency, gained from tapping into the know how, and capabilities, of a much large community of developers, who are continuously making contributions to the ASP.NET MVC model.