MVC 3 - first look
My first thought:
MVC3 already??? ScottGu, slow down pal, give us some time to breath!
Alrighty, so I installed the bits for MVC3. I fired-up VS2010 and I see some new guys in the neighborhood.
When you select the ASP.NET MVC 3 Empty Web Application option, no controllers and no views (not even the master page) are created. I’m guessing that’s why they put the word ‘Empty’ in there, but how would I know… English is my second language!!
As I’m still not very comfortable with Razor, I created the solution with the non-Razor flavor. The first thing I did was to add a Razor view and check it’s compatibility. Just wanted to see how two different view engines lived under the same project in harmony! While doing this I also tested the ‘dynamic’ ViewModel property on the controller.
1: public ActionResult Test()
2: {
3: ViewModel.Message = "Hello MVC 3";
4: return View();
5: }
I’m used to creating views by right-clicking in the action method and choosing ‘Add View…’ and here’s what I see:
Look at that – the two view engines. So I select Razor and add it to my project. All I had to do was to make a call to @View.Message and this is what the Razor view looked like:
1: @inherits System.Web.Mvc.WebViewPage<dynamic>
2:
3: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4:
5: <html xmlns="http://www.w3.org/1999/xhtml">
6: <head>
7: <title>Test</title>
8: </head>
9: <body>
10: <div>
11: @View.Message
12: </div>
13: </body>
14: </html>
I run the application and when I change to url to /home/test, I get the following output:
I loved the simplicity. But just before we move on to the other features, had a couple of concerns. The “@” is similar to <%: %> (the encoded version). So to display something like “ASP.NET <br /> MVC 3” in two different lines you can do:
@(MvcHtmlString.Create(View.Message))
The second concern is that when I selected Razor in the Add View window for my (ASPX)-style solution and click on ‘Add’, I get the following error message:
I would’ve preferred if it had asked me ‘Want the master page to be created?’ But I’m guessing this is already in the works. So for my above test, I had to uncheck the master page checkbox and continue.
In his blog, ScottGu mentions about permanent redirects. Let’s take this to a spin. I modified my Test action method:
1: public ActionResult Test()
2: {
3: return RedirectToActionPermanent("Index");
4: }
Now when I go to ‘/home/test’, I get back to the Index page and Fiddler tells me what happened in the backend:
A 301 indeed. ASP.NET 4.0 also has this feature – details here.
Now let’s do something more ‘real-world’ish. I created my model:
1: public class Product
2: {
3: public string Name { get; set; }
4: public string Category { get; set; }
5: public List<Component> Components { get; set; }
6: }
7:
8: public class Component
9: {
10: public string Name { get; set; }
11: public string ManufacturerCompany { get; set; }
12: }
My Index action ends up like this:
1: public ActionResult Index()
2: {
3: Product product = new Product();
4: product.Name = "Flash Light";
5: product.Category = "Hiking Gear";
6: product.Components = new List<Component>();
7: product.Components.Add(new Component { Name = "Battery", ManufacturerCompany = "Everready" });
8: product.Components.Add(new Component { Name = "Bulb", ManufacturerCompany = "Philips" });
9:
10: return View(product);
11: }
I create my view for Product as:
1: <% using (Html.BeginForm())
2: {%>
3: <%: Html.ValidationSummary(true) %>
4: <div class="display-label">Product Name</div>
5: <div class="display-field">
6: <%: Html.TextBoxFor(m=>m.Name)%>
7: <%: Html.ValidationMessageFor(m => m.Name) %>
8: </div>
9: <div class="display-label">Category</div>
10: <div class="display-field">
11: <%: Html.TextBoxFor(m=>m.Category)%>
12: <%: Html.ValidationMessageFor(m => m.Category)%>
13: </div>
14:
15: <%: Html.EditorFor(m => m.Components, "Component") %>
16:
17: <input type="submit" name="submit" value="Submit" />
18: <% } %>
And to display the Components, I created the ‘Component.cshtml’ Razor partial view.
1: @inherits System.Web.Mvc.WebViewPage<List<Mvc3.Models.Component>>
2:
3: <table>
4: <tr>
5: <th>
6: Name
7: </th>
8: <th>
9: ManufacturerCompany
10: </th>
11: </tr>
12:
13: @foreach (var item in Model) {
14:
15: <tr>
16: <td>
17: @item.Name
18: </td>
19: <td>
20: @item.ManufacturerCompany
21: </td>
22: </tr>
23: }
24: </table>
With this mix, I ran the application and got a beautifully rendered page (another proof of (ASPX) and Razor’s harmonious co-existence).
The curious side of me said ‘What if both Razor and (ASPX) partial views exist in the same folder? Which one will be picked up?’ I added an (ASPX) partial view with the same name – Component.ascx to the EditorTemplate folder. I made this view slightly different – the components were editable. Running the application gave me this:
Turns out ascx has a higher priority than cshtml. Seems like the View Engine looks for .ascx files first and then searches for the .cshtml files. MVC team, please let me know if my understanding is wrong.
At this point, I’d like to raise a feature-request. In the Add View window, it’ll be really helpful if there are two more options next to the ‘Create a partial view’ checkbox – Create partial view as Display Template and Create partial view as Editor Template. These would be grayed out initially and enabled only when user checks the partial view checkbox. Based on what template has been selected, VS2010 should create the appropriate folder (EditorTemplates / DisplayTemplates) and add the partial view file in the appropriate folder. This is a good thing to have even for MVC2 projects.
It’s time to do some validation on our Product class. With MVC2, you could add validation attributes on the properties directly. MVC3 gives another way of doing this (it’s too early for me to say which one’s better). Here’s what my Product class became after adding the validation checks.
1: public class Product : IValidatableObject
2: {
3: public string Name { get; set; }
4: public string Category { get; set; }
5: public List<Component> Components { get; set; }
6:
7: public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
8: {
9: if (string.IsNullOrEmpty(Name))
10: {
11: yield return new ValidationResult("Name cannot be empty;");
12: }
13:
14: if (string.IsNullOrEmpty(Category))
15: {
16: yield return new ValidationResult("Category cannot be empty;");
17: }
18:
19: if (Name == "Flash light" && Category != "Hiking Gear")
20: {
21: yield return new ValidationResult("Product is placed in an incorrect category");
22: }
23: }
24: }
During model binding, if any of these conditions is true, the associated validation message gets displayed on the screen.
I was unable to add HandleLogging attribute to my controller. VS2010 complained of missing some references for this attribute. And when I looked at the Object Browser for System.Web.Mvc, there’s no HandleLoggingAttribute member listed:
May be ‘The Gu’ has special bits for this!
Now here’s my main concern – Model Binding for complex view models. I had this issue with MVC2 as well, but then I thought they’ll fix this issue in the next release.
I’ll use the above Model and the following View to go detail on this issue:
1: <% using (Html.BeginForm())
2: {%>
3: <%: Html.ValidationSummary(true) %>
4: <div class="display-label">Product Name</div>
5: <div class="display-field">
6: <%: Html.TextBoxFor(m=>m.Name)%>
7: <%: Html.ValidationMessageFor(m => m.Name) %>
8: </div>
9: <div class="display-label">Category</div>
10: <div class="display-field">
11: <%: Html.TextBoxFor(m=>m.Category)%>
12: <%: Html.ValidationMessageFor(m => m.Category)%>
13: </div>
14:
15: <%: Html.EditorFor(m => m.Components, "Component") %>
16:
17: <input type="submit" name="submit" value="Submit" />
18: <% } %>
The partial view ‘Component’ looks like this:
1: <h2>Component</h2>
2: <table>
3: <tr>
4: <th>
5: Name
6: </th>
7: <th>
8: ManufacturerCompany
9: </th>
10: </tr>
11: <%1: foreach (var item in Model)2: {
%>
12: <tr>
13: <td>
14: <% 1: : Html.TextBoxFor(m=>item.Name)
%>
15: </td>
16: <td>
17: <% 1: : Html.TextBoxFor(m => item.ManufacturerCompany)
%>
18: </td>
19: </tr>
20: <% 1: }
%>
21: </table>
This gets me the output as:
Now when I click on the submit button, only the product name and category get bound to my product instance, but not the Components list.
The reason this happens is due to the way the html is rendered. When I do a View Source on the page, I see the components as:
If only the name attribute of the input tag gets rendered as ‘Components[0].Name’, ‘Components[0].ManufacturerCompany’ and MS tweaks their Model binding logic a little bit, this will get a lot easier for users. Currently for one of our projects, we’re having to use Html.CustomTextBoxFor() extensions that emits the name attribute correctly and our own Model binding algorithm to get the object mapped correctly.
I’ll stop at this; will update more later. Please download the code here.
Verdict: There’s a lot more to learn and play with, but this sure is a good start for MVC3.