Sunday, July 14, 2013

Moving from WebForms to MVC


So you've heard about this MVC thing and are still a little scared of it, maybe you want to try it out a little bit but you're not sure where to start?

Just so you know, I'm not an ASP.NET MVC expert and am still learning more about how it works every day. But I know enough to be dangerous.. and do some pretty cool stuff with it, easily. I really HATE the WebForms/UserControl way of adding stuff in my "view" (the ascx file) to do validation and form field rendering. I think the way it's done in ASP.NET MVC is much cleaner, easier to use and easier to debug. Also, Model Binding is fantastic (although I hear that's possible in WebForms now as well).

In this post I'll show you how to create a simple contact form that, optionally, allows people to write testimonials. The testimonials will be stored as nodes in Umbraco using the ContentService (new in Umbraco 6). I start out with a WebForms site and will move everything over to MVC after that.

Bonus: the full site is available for download at the bottom of this post!

MVC - Not too scary

A few months ago, I finally figured out why MVC shouldn't be scary for WebForms developers because in their basis, they are very similar.

Think about it, the ascx file could be considered to be the View, the ascx.cs file looks a lot like a Controller and the ascx.designer.cs file looks an aweful lot like a Model:

Usercontrol -mvc

The only difference is that Visual Studio will generate the designer file for you and you can visually move items from your toolbox on your usercontrol's surface. This will then produce some HTML over which you have little control.

With MVC you take back that control, which leads to a little more manual work but in my opinion that's completely worth it. If you're lazy like me though, you can always use the Scaffolding NuGet package to automate a lot of this work.

Let's get started

Enough talking, show me the code! Okay, relax, it's coming.

So let's start with the UserControl by producing this tagsoup thing (yech!).

2013-07-14_145647

In the codebehind I manually fill the "Type of inquiry" dropdown with some values but this data could easily come from some data source like a database table. I have a mail helper class to help me send an e-mail and I need to give it some of the values from the form.

Note that when the type of inquiry is a testimonial, I create a new node under the Testimonials node in Umbraco. Also notice that I'm not using the old Document API so only a few lines of code is needed to create a new document, set the values and then you can just say: SaveAndPublish, awesome!

public partial class ContactUs : UmbracoUserControl { protected void Page_Load(object sender, EventArgs e) { ContactType.Items.Add(new ListItem("Contact", "contact")); ContactType.Items.Add(new ListItem("Request for quote", "quote")); ContactType.Items.Add(new ListItem("Testimonial", "testimonial")); } protected void SendMail(object sender, EventArgs e) { if (Page.IsValid) { var mail = new Mail.MailVariables { From = Email.Text, FromName = Name.Text, Content = string.Format("Contact type: {0} <br /> {1}", ContactType.Text, Message.Text), Subject = "Contact mail from site", To = "contact@example.com", ToName = "Site Owner Name" }; if (Mail.SendMail(mail)) { if (ContactType.SelectedValue == "testimonial") CreateTestimonial(Name.Text, Message.Text); Form.Visible = false; Thanks.Visible = true; } } } private void CreateTestimonial(string name, string body) { var contentService = Services.ContentService; // This should be a macro parameter of course :-) const int testimonialPageId = 1068; var content = contentService.CreateContent(name, testimonialPageId, "umbTextPage"); content.SetValue("bodyText", body); contentService.SaveAndPublish(content); } } 

So I wrapped this UserControl in a macro and add that macro to the Contact.master template.

2013-07-14_150240

Wonderful, I have a working form now that optionally creates testimonials if I select that from the "type" dropdown. Just what I wanted... Almost.

Convert Masterpages to MVC Views

I really wanted to explore MVC and make a clean implementation. In order to do that, I decided to convert all my templates to MVC Views. Which really is surprisingly easy.

Let's start with a few configuration options:

  • In umbracoSettings.config set the defaultRenderingEngine to Mvc (instead of WebForms)
  • In the web.config, add two new keys in the appSettings section that are going to help us later with client-side validation:
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />

Okay, with that out of the way, create a new file in the Views folder called umbMaster.cshtml. This is the exact same filename as the masterpage had. Into umbMaster.cshtml I copy everything that is already in umbMaster.master.

Then I can make a few simple changes to turn it into a full MVC view. For this, it's easiest to use the Masterpages2Views cheatsheet and just find everything that says runat="server" and replace it with the MVC View equivalent.

Here is all I had to do for the masterpage I had, 10 changes, 1 of which is adding "MVC Template" in the text somewhere to make sure everything worked as expected (click for a larger version):

2013-07-14_150807

Even fewer changes are needed for the umbTextPage template. Again, I'll do the same thing: make a umbTextPage.cshtml file, copy the contents of umbTextPage.master into it and make a few changes to turn it into a View.

2013-07-14_152136

From UserControl to SurfaceController

Now, on to the meat of the matter: moving our form to MVC. Let's start with a model. Remember, it's a lot like the designer.ascx.cs file, it holds the fields that we'll be using on our form:

public class ContactModel { [Required] public string Name { get; set; } [Required] [RegularExpression(@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")] public string Email { get; set; } [Required] public string Message { get; set; } public List ContactTypes { get; set; } public string ContactType { get; set; } } 

Nice, the validations that I previously had to put in the tagsoup on the ascx file are now added as simple attributes on the fields here.

Next up, I can make the Controller and you'll notice that the code is almost completely the same as the codebehind I had on the UserControl earlier. For the most part I only changed Email.Text, Name.Text and Message.Text to model.Email, model.Name and model.Message. Easy:

 public class ContactUsController : SurfaceController { [HttpPost] public ActionResult HandleContactSubmit(ContactModel model) { //model not valid, do not save, but return current umbraco page if (ModelState.IsValid == false) { return CurrentUmbracoPage(); } var mail = new Mail.MailVariables { From = model.Email, FromName = model.Name, Content = string.Format("Contact type: {0}<br />{1}", model.ContactType, model.Message), Subject = "Contact mail from site", To = "contact@example.com", ToName = "Site Owner Name" }; if (Mail.SendMail(mail)) { if (model.ContactType == "testimonial") CreateTestimonial(model.Name, model.Message); TempData.Add("Success", true); } return RedirectToCurrentUmbracoPage(); } private void CreateTestimonial(string name, string body) { var contentService = Services.ContentService; // This should be a macro parameter of course :-) const int testimonialPageId = 1068; var content = contentService.CreateContent(name, testimonialPageId, "umbTextPage"); content.SetValue("bodyText", body); contentService.SaveAndPublish(content); } } 

Of note are these two calls:

  • return CurrentUmbracoPage();
    This will return the contact page with everything in the form still filled in, if client side validation failed and we notice an error on the server side, I want return to the filled in form to show the errors and make it easy for the user to correct them.
  • return RedirectToCurrentUmbracoPage();
    This does a full redirect to the contact page, meaning that when you then hit refresh in your browser you won't get asked if you want to post the form again, instead it just loads the empty form again.

Now for the final pieces of the puzzle: the View. All of my views, including the Contact.cshtml have to inherit from UmbracoTemplatePage so that the relevant Umbraco bits are added into it. Inheriting from this class allows me to use things like Umbraco.Field and Umbraco.RenderMacro. The model for these views is the "current page".

However, to be able to render the form and our validation, I need a view that uses my previously made ContactModel as a model, not the current page.
So what I can do to achieve this is render a partial view from my UmbracoTemplatePage. Hence, my Contact View looks like this (note the Html.RenderPartial and me passing the values for the dropdownlist in here, which is not best practice but easier for this example):

@inherits UmbracoTemplatePage @{ Layout = "~/Views/umbMaster.cshtml"; } <div id="page-bgtop"> <div id="content"> <div class="post"> <h2 class="title"><span>@Umbraco.Field("pageName")</span></h2> <div class="entry"> @Umbraco.Field("bodyText") @{ var contactTypes = new List<SelectListItem> { new SelectListItem {Selected = false, Text = "Contact", Value = "contact"}, new SelectListItem {Selected = false, Text = "Request for quote", Value = "quote"}, new SelectListItem {Selected = false, Text = "Testimonial", Value = "testimonial"}, }; Html.RenderPartial("~/Views/Partials/Contact.cshtml", new OneContact.Models.ContactModel { ContactTypes = contactTypes }); } </div> </div> </div> <!-- end div#content --> <div id="sidebar"> @Umbraco.RenderMacro("umb2ndLevelNavigation") </div> <!-- end div#sidebar --> <div style="clear: both; height: 1px"></div> </div>

So now all that's left to do is create a form in the Partial View, which looks like this:

@using OneContact.Controllers @model OneContact.Models.ContactModel @{ var formSent = false; var success = TempData["Success"]; if(success != null) { bool.TryParse(success.ToString(), out formSent); } } <script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> @if (formSent) { <p>Thanks, we'll get back to you soon!</p> } else { using (Html.BeginUmbracoForm<ContactUsController>("HandleContactSubmit")) { @Html.LabelFor(model => model.ContactType)<br /> @Html.DropDownListFor(model => model.ContactType, Model.ContactTypes)<br /> @Html.LabelFor(model => model.Name)<br /> @Html.EditorFor(model => model.Name)<br /> @Html.ValidationMessageFor(model => model.Name)<br /> @Html.LabelFor(model => model.Email)<br /> @Html.EditorFor(model => model.Email)<br /> @Html.ValidationMessageFor(model => model.Email)<br /> @Html.LabelFor(model => model.Message)<br /> @Html.TextAreaFor(model => model.Message)<br /> @Html.ValidationMessageFor(model => model.Message)<br /> <p> <input type="submit" value="Submit" /> </p> } } 

A few things to note here:

  • In the ContactController I created earlier, I added some TempData when the form had been successfully posted. I use that here to either show the form or the success message.
  • I've added a few javascript libraries that will help with client side validation.
  • If you're used to "regular" MVC, you'll notice that I'm not using Html.BeginForm but Html.BeginUmbracoForm. This is so that Umbraco knows how to do the routing correctly.
  • The HTML produced by Html.EditorFor is already very clean and simple, we could also just say something like <input type="text" id="Name" /> and that would work perfectly fine as well and allows me to have full control over the HTML.

Of course I could've added any HTML that my frontend developer gave me to get the exact form styling that they've defined. My frontend developer was late though, so I just put in some line breaks.. ;-)

Summary

So in short what have we done:

  • Copy our complete masterpages to views with the same name and tweaked them a little
  • Created a Model to hold our form fields
  • Created a SurfaceController and copied all of the businesslogic from the UserControl's codebehind in it, with a few easy tweaks
  • Created a View and a Partial View to show the form and be able to post it, again, using most of the HTML that we already had in our masterpage earlier

Once you wrap your head around it, you'll realize that making templates and forms the MVC way is not so hard and you can move on to more advanced scenario's, taking advantage of AJAX forms for example, and Umbraco Partial View Macro's to pass in parameters, or MVC's powerful EditorTemplates and DisplayTemplates. The world is your oyster!

The complete site, including the source code is available so you can run it play with it yourself and copy the code for your own purposes. To log in to the backoffice, username: admin password: test

If you don't know Umbraco, here are some numbers behind the world's friendliest CMS

One of the biggest benefits of using Umbraco is that the community is incredibly pro-active, extremely friendly and helpful.

Chances are that if you get an idea for something you would like to build in Umbraco, someone has already built it. So it is very likely that you can get good and friendly advice from someone from the Umbraco community on Our- just ask.

Number of active installs
443.450
Number of active members in the community
221.745
Known free Umbraco packages available
1.211

Want to be updated on everything Umbraco?

Be one of the first to know about special offers on our products and services. Get invitations to Umbraco events and festivals sent directly to your inbox.

All you need to do is get on our mailing list and soon you'll become a true Umbraco-know-it-all.

Sign up for Umbraco newsletters and offers

Are you sure, that's your real e-mail?