ASP.NET MVC3 Beta + Entity Framework Code First + Razor View Engine (Sample Project)
Hi everyone, I was digging the new ASP.NET MVC3 Beta and decided to share a sample project with you. This guide is for people that already know the basics of the ASP.NET MVC framework (Models/Controllers/Views/Routes and how it works).
I'll guide you through the key parts of the project, i suggest you to clone it and follow up the code in the project by reading my post, because i won't write here the entire code to create it from the ground up, but feel free to create a new project to try out the features that i will explain to you (it's the best approach)
Downloads
First things first: to use all of the features i'm going to introduce, you'll need to grab and install the ASP.NET MVC3 Beta and the Entity Framework CTP4 release:
Introduction
The project is based on the ASP.NET MVC3 framework, and it uses the new Code First approach of the Entity Framework that let you create the model classes for your entities, and it will automatically create the database for you.
With the new MVC3 installed, Visual Studio 2010 will have new tooling features that will let you choose to create a project and the views based on a particular view engine. For this project i'm using the new Razor View Engine, for a preview about the syntax you can read this blog entry by Scott Gu.
Now we're ready to start.
Creating the MVC3 Project
To create a new ASP.NET MVC 3 project just open the New Project dialog, with the beta installed you can proceed by clicking on ASP.NET MVC3 Web Application. Pick Razor as the default view engine.
I've chosen to create a test project too, i'm not covering unit testing in this post though, but if you want you can browse the Home and Account controller tests that VS will create automatically to understand how it works, it's fairly easy, you can add your own classes, create your own test methods, and the Test menu of VS2010 will let you run your tests directly.
Remember to add a reference to your project to the Microsoft.Data.Entities.CTP.dll (the Entity Framework CTP4 dll you've installed at the beginning).
Areas
MVC2 introduced a new feature called Areas. In an MVC project you can create areas that let you separate the code (and the web application) in various areas. In Visual Studio, you can create new areas just right clicking on the web application project, and choosing Add Area.
In my project i created an Accounting area, it will contain the pages and the business logic to handle Expenses and Incomes.
Warning: You will need to modify the Layout.cshtml master page into the shared view folders, to specify an area for the ActionLink html helpers:
<ul id="menu">
<li>@Html.ActionLink("Home", "Index", "Home", new { area=UrlParameter.Optional }, null)</li>
<li>@Html.ActionLink("Expenses", "Index", "Expenses", new { area="Accounting" }, null)</li>
<li>@Html.ActionLink("Incomes", "Index", "Incomes", new { area="Accounting" }, null)</li>
<li>@Html.ActionLink("About", "About", "Home", new { area=UrlParameter.Optional }, null)</li>
</ul>
The Models
Before starting to create the models, be sure to have an instance of SQLEXPRESS up and running, because Code First will use the instance to create and manage the database for your application.
Under the new Accounting folder in Areas, we start by creating our model class, create a new class named AccountingModels (in the Models directory, of course).
The expense and income models are the following:
public class Expense
{
public int ExpenseId { get; set; }
public string Description { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public virtual ExpenseType ExpenseType { get; set; }
}
public class ExpenseType
{
public int ExpenseTypeId { get; set; }
public string Description { get; set; }
public virtual ICollection<Expense> Expenses { get; set; }
}
public class Income
{
public int IncomeId { get; set; }
public string Description { get; set; }
public DateTime Date { get; set; }
public decimal Amount { get; set; }
public virtual IncomeType IncomeType { get; set; }
}
public class IncomeType
{
public int IncomeTypeId { get; set; }
public string Description { get; set; }
public virtual ICollection<Income> Incomes { get; set; }
}
The virtual properties of the models, are the conventions that the Entity Framework follow to identify relationships through the entities, with these properties it will "understand" that an Expense has one Expense Type, and the Expense Type model, can be associated with multiple Expenses, and will create the database accordingly.
For a list of conventions used by the Entity Framework Code First, go here.
Creating the Database Context
The next thing to do is creating a Context for our model, subclassing the DbContext class.
public class AccountingContext : DbContext
{
public AccountingContext() : base("AccountingDB") { }
public DbSet<Expense> Expenses { get; set; }
public DbSet<ExpenseType> ExpenseTypes { get; set; }
public DbSet<Income> Incomes { get; set; }
public DbSet<IncomeType> IncomeTypes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ExpenseType>().Property(e => e.Description).IsRequired();
modelBuilder.Entity<IncomeType>().Property(i => i.Description).IsRequired();
modelBuilder.Entity<Expense>().MapSingleType().ToTable("Expenses");
modelBuilder.Entity<ExpenseType>().MapSingleType().ToTable("ExpenseTypes");
modelBuilder.Entity<Income>().MapSingleType().ToTable("Incomes");
modelBuilder.Entity<IncomeType>().MapSingleType().ToTable("IncomeTypes");
modelBuilder.Entity<Expense>().HasRequired(e => e.ExpenseType).WithMany(e => e.Expenses);
modelBuilder.Entity<Income>().HasRequired(i => i.IncomeType).WithMany(i => i.Incomes);
}
}
With this approach you can specify the entities that will be part of your Database context, and you can override the model creation, through the fluent API provided by the EF, declaring the mapping of the models to your database tables.
You can specify a database name specifing a string into the base class of the constructor, in this case, the database name will be AccountingDB.
In OnModelCreate, with the fluent API of the EF, i specified that the "Description" fields in the database are required, i also specified the table names for the entities, and i specified the the Expense and Income tables cannot have a NULL value for their relationship with the ExpenseType and the IncomeType.
For further examples about the fluent API, this is a good blog post (there are many others, just search, an official reference doesn't yet exists).
Setting Up an Initializer
Another thing we can do for our models is specifying an Initializer, with an initializer you can tell the Entity Framework when to recreate the database tables and you can also populate the tables with some test data.
public class AccountingInitializer : RecreateDatabaseIfModelChanges<AccountingContext>
{
protected override void Seed(AccountingContext context)
{
var expenses = new List<Expense>
{
new Expense { Description = "Chinese Lunch", Amount = 10.00M, Date = new DateTime(2010,07,4),
ExpenseType = new ExpenseType { Description = "Lunch" } },
new Expense { Description = "Car Repair", Amount = 320.00M, Date = new DateTime(2010,07,23),
ExpenseType = new ExpenseType { Description = "Car" } }
};
expenses.ForEach(e => context.Expenses.Add(e));
var incomes = new List<Income>
{
new Income { Description = "July Remuneration", Amount = 1800.00M, Date = new DateTime(2010,07,15),
IncomeType = new IncomeType { Description = "Remuneration" } }
};
incomes.ForEach(i => context.Incomes.Add(i));
}
}
With RecreateDatabaseIfModelChanges, we're telling EF to recreate the tables whenever the model class code changes, and by overriding the Seed method, we can specify some test data that will be inserted into the database tables.
You can also use AlwaysRecreateDatabase, or CreateDatabaseOnlyIfNotExists, instead of RecreateDatabaseIfModelChanges, these properties are self-explaining ;).
Be sure to add the initializer into the Global.asax Application_Start method, in this way:
Database.SetInitializer<AccountingContext>(new AccountingInitializer());
Data Annotations
Now, let's add some Data Annotation to our models, Data Annotations are used to customize the appearence/behaviour/validation rules for the field of our entities.
This is the code for the models of my example project:
public class Expense
{
[ScaffoldColumn(false)]
public int ExpenseId { get; set; }
[Required(ErrorMessage="The Description field is required.")]
public string Description { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
public DateTime Date { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:F}")]
public decimal Amount { get; set; }
public virtual ExpenseType ExpenseType { get; set; }
[DisplayName("Expense Type")]
[Required(ErrorMessage="The ExpenseType field is required.")]
public int ExpenseTypeId { get; set; }
}
public class ExpenseType
{
[ScaffoldColumn(false)]
public int ExpenseTypeId { get; set; }
[Required(ErrorMessage = "The Description field is required.")]
public string Description { get; set; }
public virtual ICollection<Expense> Expenses { get; set; }
}
public class Income
{
[ScaffoldColumn(false)]
public int IncomeId { get; set; }
[Required(ErrorMessage = "The Description field is required.")]
public string Description { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
public DateTime Date { get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:F}")]
public decimal Amount { get; set; }
public virtual IncomeType IncomeType { get; set; }
[DisplayName("Income Type")]
[Required(ErrorMessage = "The IncomeType field is required.")]
public int IncomeTypeId { get; set; }
}
public class IncomeType
{
[ScaffoldColumn(false)]
public int IncomeTypeId { get; set; }
[Required(ErrorMessage = "The Description field is required.")]
public string Description { get; set; }
public virtual ICollection<Income> Incomes { get; set; }
}
I'll explain some of those.
ScaffoldColumn: enable/disable the visualization of the field into the display or editor views (more details later on)
Required: specify a validation rule, the field cannot be left unspecified, you can set an error message for the validation
DisplayFormat: you can specify the format of your property, DisplayFor and EditorFor helpers in the views will use this format
DisplayName: you can specify a display name for the field, LabelFor helper will print out the specified name.
I've also added a reference to the ExpenseTypeId and the IncomeTypeId of the main models.
You can read more about data annotations here
Accounting Repository
Next step, let's create a repository class to talk to the database through our entity framework DbContext.
public class AccountingRepository
{
AccountingContext context = new AccountingContext();
public IQueryable<Expense> GetExpenses()
{
var expenses = from e in context.Expenses
select e;
return expenses;
}
public Expense GetExpense(int id)
{
return context.Expenses.SingleOrDefault(e => e.ExpenseId == id);
}
internal IQueryable<ExpenseType> GetExpenseTypes()
{
var expenseTypes = from e in context.ExpenseTypes
select e;
return expenseTypes;
}
public IQueryable<Income> GetIncomes()
{
var incomes = from i in context.Incomes
select i;
return incomes;
}
public Income GetIncome(int id)
{
return context.Incomes.SingleOrDefault(i => i.IncomeId == id);
}
internal IQueryable<IncomeType> GetIncomeTypes()
{
var incomeTypes = from i in context.IncomeTypes
select i;
return incomeTypes;
}
internal void Add(Expense expense)
{
context.Expenses.Add(expense);
}
internal void Add(Income income)
{
context.Incomes.Add(income);
}
internal void Remove(Expense expense)
{
context.Expenses.Remove(expense);
}
internal void Remove(Income income)
{
context.Incomes.Remove(income);
}
public void Save()
{
context.SaveChanges();
}
}
The repository simplify the the way we can query our database, avoiding us to write common linq select by calling a generic method, it's often a good pattern approach.
Creating the basic Controllers
Now let's head to the Controllers, for brevity i'm not showing the code, you can checkout the sample code at the end of the post to check it out.
To create a controller, right-click on the Controllers folder and choose Add Controller, in the window, name it ExpenseController, for example, and check the option to enable the creation of the methods for CRUD operations.
The code for the controller is pretty basic, you can add some functionalities to it, a "Not Found" view for example, or the Authorize filters and other stuff like that, but for this sample, it wasn't strictly necessary.
Let's jump to the views for a moment.
Razor Views
As i said at the beginning, i used the new Razor View Engine, i've got a bad news for you, Visual Studio doesn't still provide the editing of the Razor views with Syntax Highlighting and Intellisense, so it's a little unconfortable to work with it now, but the final MVC3 release will have those features for sure, so we don't have to worry =)
To create a Razor View in MVC3, it's simple, just right click on the views folders, or onto the methods of the controller and click on Add View. The MVC3 lets you choose, from a dropdownlist, which type of view you want to create (ASPX, Razor, etc..). If you have other view engines installed (Nhaml, Spark, etc..), you should see them in the combo box.
The MVC3 introduced the new @model directive, a more cleaner and concise way to reference the strongly-typed models from the views
In MVC/MVC2 we did this in our view files:
@inherits System.Web.Mvc.WebViewPage<Accounting.Areas.Accounting.Models.Expense>
In MVC3 we can simply write this:
@model Accounting.Areas.Accounting.Models.Expense
Here's the Index view of the expenses for this project sampe:
@model IEnumerable<Accounting.Areas.Accounting.Models.Expense>
@{
View.Title = "Expenses";
Layout = "~/Views/Shared/_Layout.cshtml";
}
Expenses
@Html.ActionLink("Create New", "Create")
<table>
<tr>
<th></th>
<th>
Description
</th>
<th>
Date
</th>
<th>
Amount
</th>
<th>
Expense Type
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.ExpenseId }) |
@Html.ActionLink("Details", "Details", new { id=item.ExpenseId }) |
@Html.ActionLink("Delete", "Delete", new { id=item.ExpenseId })
</td>
<td>
@item.Description
</td>
<td>
@String.Format("{0:d}", item.Date)
</td>
<td>
@String.Format("{0:F}", item.Amount)
</td>
<td>
@item.ExpenseType.Description
</td>
</tr>
}
</table>
And this is the Edit view:
@model Accounting.Areas.Accounting.Models.Expense
@{
View.Title = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
Edit
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Fields</legend>
@Html.EditorFor(model => model)
<input type="submit" value="Save" />
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
T4 Templates
For those who still doesn't know about T4 templates, the time has come to find out what they are, and how to use them. They specify the code created for our views when we use the "Add View" dialog.
Be sure to read this guide.
I've included the T4 templates in my project, i modified the basic ones, to let them use the new EditorFor and DisplayFor MVC2 html helpers, and added a couple other templates for generating Display and Editor Partial Views.
Here's an example, this is the Create.tt T4 template, that outputs a Create view:
<#@ template language="C#" HostSpecific="True" #>
<#@ output extension=".cshtml" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#
MvcTextTemplateHost mvcHost = (MvcTextTemplateHost)(Host);
#>
@model <#= mvcHost.ViewDataTypeName #>
<#
// The following chained if-statement outputs the file header code and markup for a partial view, a content page, or a regular view.
if(mvcHost.IsPartialView) {
#>
<#
} else if(mvcHost.IsContentPage) {
#>
@{
View.Title = "<#= mvcHost.ViewName#>";
Layout = "<#= mvcHost.MasterPageFile#>";
}
## <#= mvcHost.ViewName#>
<#
} else {
#>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><#= mvcHost.ViewName #></title>
</head>
<body>
<#
}
#>
<#
Dictionary<string, string> properties = new Dictionary<string, string>();
FilterProperties(mvcHost.ViewDataType, properties);
#>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Fields</legend>
@Html.EditorFor(model => model)
<input type="submit" value="Create" />
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
<#
// The following code closes the asp:Content tag used in the case of a master page and the body and html tags in the case of a regular view page
#>
<#
if(!mvcHost.IsPartialView && !mvcHost.IsContentPage) {
#>
</body>
</html>
<#
}
#>
<#
public void FilterProperties(Type type, Dictionary<string, string> properties) {
if(type != null) {
PropertyInfo[] publicProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
foreach (PropertyInfo pi in publicProperties)
{
if (pi.GetIndexParameters().Length > 0)
{
continue;
}
Type currentPropertyType = pi.PropertyType;
Type currentUnderlyingType = System.Nullable.GetUnderlyingType(currentPropertyType);
if(currentUnderlyingType != null) {
currentPropertyType = currentUnderlyingType;
}
if (IsBindableType(currentPropertyType) && pi.CanWrite)
{
if(currentPropertyType.Equals(typeof(double)) || currentPropertyType.Equals(typeof(decimal))) {
properties.Add(pi.Name, "String.Format(\"{0:F}\", Model." + pi.Name + ")");
} else if(currentPropertyType.Equals(typeof(DateTime))) {
properties.Add(pi.Name, "String.Format(\"{0:g}\", Model." + pi.Name + ")");
} else {
properties.Add(pi.Name, "Model." + pi.Name);
}
}
}
}
}
public bool IsBindableType(Type type)
{
bool isBindable = false;
if (type.IsPrimitive || type.Equals(typeof(string)) || type.Equals(typeof(DateTime)) || type.Equals(typeof(decimal)) || type.Equals(typeof(Guid)) || type.Equals(typeof(DateTimeOffset)) || type.Equals(typeof(TimeSpan)))
{
isBindable = true;
}
return isBindable;
}
#>
DisplayFor and EditorFor HTML Helpers
This 2 helpers, output the appropriate html to display a model or field, or the editor of a model or field. You can specify a Display template or an Editor template by creating the views in the subfolders EditorTemplates placed into the Shared views folder
For example, if we want to specify an editor for our Expense model, we create a new folder under the Shared Views of the Accounting area called: EditorTemplates, and we create in it a file named Expense.cshtml.
Now, when we use the helper in another view: Html.EditorFor(Model), ASP.NET MVC will search for an editor under the EditorTemplates named
For our example, if we pass an Expense object into the EditorFor helper, it will search for an Expense.cshtml view under EditorTemplates, and if it isn't specified, it will output the editor specified for each field, or a standard textbox.
The Edit view in the project uses the EditorFor helper, it will output the contente of the following Expense.cshtml view:
@model Accounting.Areas.Accounting.Models.Expense
<div class="editor-label">
@Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Date)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Date)
@Html.ValidationMessageFor(model => model.Date)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Amount)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Amount)
@Html.ValidationMessageFor(model => model.Amount)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.ExpenseTypeId)
</div>
<div class="editor-field">
@Html.DropDownListFor(model => model.ExpenseTypeId, (SelectList) ViewData["ExpenseTypes"])
@Html.ValidationMessageFor(model => model.ExpenseTypeId)
</div>
It works for every object or value type, if you want to specify the html for all the DateTime editor fields, you can create a DateTime.cshtml under the EditorTemplates folder.
DropDownLists For Related Entities
For the Expense and Income editor templates, the views provide a DropDownList for selecting the type of expenses and the type of incomes.
I used ViewData to "pass" a SelectList to the views (it would be better using View Models though, but for this basic example it was good enough).
Sample controller code:
var expenseTypes = repository.GetExpenseTypes();
ViewData["ExpenseTypes"] = new SelectList(expenseTypes, "ExpenseTypeId", "Description");
Sample html helper code:
@Html.DropDownListFor(model => model.ExpenseTypeId, (SelectList) ViewData["ExpenseTypes"])
If I found some more time, i'll write another entry with an updated version of the project to cover some other aspects of ASP.NET MVC, like View Models pattern, Ajax, Unit Testing, and other tips.
Now, grab the source of my project and start browsing it, feel free to ask if something isn't clear, because the code is not commented.