Umbraco MVC Forms using a Surface Controller

Setting up a form in a standard MVC way can be tricky in Umbraco so I created this sample code so that I can easily get a new form page built.


The main problem you find when creating a form in Umbraco is how to pass the view model from the controller through to the view. The approach I have developed allows you to pass the model from your controller through to your view and then by loading the form using @Html.Action allows the model to then be passed to the form partial view.

First off we need to go into Umbraco and create a new document type: ContactPage, and create a new template for the document Type also called ContactPage. Then create a new content item for our contact page.

 

The View Model

We need a model to hold our form data which has to derive from RenderModel.

The constructors allow us to pass in an Umbraco content item, or if nothing is passed in it will attempt to get the content from the UmbracoContext.

Data annotations are used to provide validation and error messages.

[AllowHtml] has been added to the message property so that if any markup is entered into the message field of the form it will still post successfully.

using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Web.Mvc;
using Umbraco.Core.Models;
using Umbraco.Web;
using Umbraco.Web.Models;

namespace UmbracoMvcForms.ViewModels
{
    public class ContactFormViewModel : RenderModel
    {
        public ContactFormViewModel() : base(new UmbracoHelper(UmbracoContext.Current)
            .TypedContent(UmbracoContext.Current == null ? 0 : UmbracoContext.Current.PageId)) { }
        public ContactFormViewModel(IPublishedContent content) : base(content, CultureInfo.CurrentCulture) { }

        [Required(ErrorMessage = "Please enter your email address")]
        [EmailAddress(ErrorMessage = "This is not a valid address")]
        [DataType(DataType.EmailAddress)]        
        public string Email { get; set; }

        [Required(ErrorMessage = "Please enter your name")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Please enter a message")]
        [AllowHtml]
        public string Message { get; set; }

        public bool MessageSent { get; set; }
        public string ErrorMessage { get; set; }
    }
}

The Page Controller

The page controller has to be named to match the document type. I created the document type with an alias of ContactPage so the controller needs to be named ContactPageController.

For Umbraco to recognise the controller it needs to derive from RenderMvcController.

This controller isn't doing a lot, there's just a single Index action that create's a new instance of the view model and passes it to the current template view.

using System.Web.Mvc;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
using UmbracoMvcForms.ViewModels;

namespace UmbracoMvcForms.Controllers
{
    public class ContactPageController : RenderMvcController
    {
        public override ActionResult Index(RenderModel model)
        {
            var viewModel = new ContactFormViewModel(model.Content);
            return CurrentTemplate(viewModel);
        }
    }
}

The Contact Form Controller

The contact form controller is doing all the work. It has three methods. The first is ShowContactForm[Httpget] which is called when the page is first loaded with HttpGet. No additional processing is needed, we just need to pass the model through to the partial view.

The next is ShowContactForm[HttpPost] which is called after the form has been posted, all the processing has already been done at this point so it just passes the model through to the relevant partial view - _ContactForm or _MessageSent.

The PostContactForm method is called directly when the form is posted back to the server. It process the form, sends an email and then returns the model to the current template view which in turn passes the model to the ShowContactFormPost method.

The process flow is as follows:

Request Page

  1. ContactPageController.Index()
  2. ContactPage View
  3. ContactFormController.ShowContactForm(model) [HttpGet]
  4. _ContactForm Partial View 

 Post Form

  1. ContactFormController.PostContactForm(model)
  2. ContactPageView
  3. ContactFormController.ShowContactForm [HttpPost]
  4. _ContactForm or _MessageSent Partial View
using System;
using System.Net.Mail;
using System.Web.Mvc;
using Umbraco.Web.Mvc;
using UmbracoMvcForms.ViewModels;

namespace UmbracoMvcForms.SurfaceControllers
{
    public class ContactFormController : SurfaceController
    {
        private const string EmailTo = "somebody@your-website.com";
        private const string EmailFrom = "somebody@your-website.com";
        private const string Subject = "Message from your-website.com";
        private const string ErrorMessage = "We were unable to send your message. Have a cup of tea and try again later.";
        private const string ContactFormView = "_ContactForm";
        private const string MessageSentView = "_MessageSent";
        private const string EmailBody = "<html><body><p>From {0}</p>Email {1}<p></p>Message<br>{2}<p></p><p></p></body></html>";
        
        [HttpGet]
        public ActionResult ShowContactForm()
        {
            var model = new ContactFormViewModel(CurrentPage);
            return PartialView(ContactFormView, model);
        }
        
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult ShowContactForm(ContactFormViewModel model)
        {
            return PartialView(model.MessageSent ? MessageSentView : ContactFormView, model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult PostContactForm(ContactFormViewModel model)
        {
            var template = UmbracoContext.Application.Services.FileService.GetTemplate(CurrentPage.TemplateId);
            var path = template.VirtualPath;

            if (!ModelState.IsValid) return View(path, model);

            var mailMessage = new MailMessage
            {
                IsBodyHtml = true,
                Subject = Subject,
                Body = string.Format(EmailBody, model.Name, model.Email, model.Message),
                From = new MailAddress(EmailFrom, model.Name)
            };
            mailMessage.To.Add(new MailAddress(EmailTo));

            var client = new SmtpClient();

            try
            {
                client.Send(mailMessage);
                model.MessageSent = true;
            }
            catch (SmtpException)
            {
                model.ErrorMessage = ErrorMessage;
                model.MessageSent = false;
            }

            return View(path, model);
        }
    }
}

The Master View

The master view is using Bootstrap/Bootswatch to create a simple page layout.

All css and javascript are being referenced using a CDN so you don't need any local assets.

@inherits UmbracoTemplatePage
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Umbraco MVC Contact Form</title>

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/superhero/bootstrap.css">

    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
      <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
    <![endif]-->    
</head>
<body>

    <div class="container">
        @RenderBody()
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
    @RenderSection("Footer", false)

</body>
</html>

The Contact Page View

The contact page view calls a method on the ContactForm surface controller to display the form.

The page also adds references to the neccessary jquery files so that we can validate the form dynamically.

@inherits UmbracoViewPage<UmbracoMvcForms.ViewModels.ContactFormViewModel>
@{ Layout = "Master.cshtml"; }

@Html.Action("ShowContactForm", "ContactForm", Model)

@section Footer
{
    <script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js"></script>
    <script src="http://ajax.aspnetcdn.com/ajax/mvc/4.0/jquery.validate.unobtrusive.min.js"></script>
}

The Contact Form Partial View

The Contact Form partial view display's the actual contact form, using the model to create the relevant fields.

The form is created using the Html.BeginUmbracoForm method which creates a normal HTML form but adds a hidden field ufprt. Then when the form is posted to the server, Umbraco can route the call directly to the method referenced in the BeginUmbracoForm method, in this case, the PostContactForm method of the ContactForm controller.

I have added the Html.AntiForgeryToken() method which generates a hidden form field (anti-forgery token) that is validated when the form is submitted, using the ValidateAntiForgeryToken attribute in the ContactForm controller.

@inherits UmbracoViewPage<UmbracoMvcForms.ViewModels.ContactFormViewModel>

<h2 class="title">Contact Form<span class="line"></span></h2>

@if (!string.IsNullOrEmpty(Model.ErrorMessage))
{
    <div class="alert alert-danger">@Model.ErrorMessage</div>   
}

@using (Html.BeginUmbracoForm("PostContactForm", "ContactForm", FormMethod.Post))
{
    @Html.AntiForgeryToken()

    <div class="form-group">
        @Html.LabelFor(x => x.Name)
        @Html.TextBoxFor(x => x.Name, new {@class = "form-control", placeholder="Name"})
    </div>

    <p>@Html.ValidationMessageFor(x => x.Name, null, null, "strong")</p>
                   
    <div class="form-group">
        @Html.LabelFor(x => x.Email)
        @Html.TextBoxFor(x => x.Email, new {@class = "form-control", placeholder = "Email", type = "email"})
    </div>

    <p>@Html.ValidationMessageFor(x => x.Email, null, null, "strong")</p>

    <div class="form-group">
        @Html.LabelFor(x => x.Message)
        @Html.TextAreaFor(x => x.Message, new {@class = "form-control", rows = "5", placeholder = "Message"})            
    </div>

    <p>@Html.ValidationMessageFor(x => x.Message, null, null, "strong")</p>

    <button type="submit" class="btn btn-primary">Send Message</button>
}

Message Sent Partial View

The _MessageSent partial view just displays a success message after the email has been sent.

I could have combined this with the _ContactForm partial view but by allowing the controller to decide which view to display it reduces the amount of logic in the partial views ensuring that they can use just enough razor to display the view. 

@inherits UmbracoViewPage<UmbracoMvcForms.ViewModels.ContactFormViewModel>

<h2>Your message has been sent</h2>

Web.Config

We need to add a few app settings to the web.config to get the form validation working and if you want to get the email send working then you will need to update the mail settings with the settings from your email account.

<appSettings>
  ...
  ...
  <add key="ClientValidationEnabled" value="true"/>
  <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
  <add key="ValidationSettings:UnobtrusiveValidationMode" value="None"/>
</appSettings>
...
...
<system.net>
  <mailSettings>
    <smtp>
      <network host="mail.your-website.com" port="8889" userName="your-username" password="your-password"/>
    </smtp>
  </mailSettings>
</system.net>

Comments

Pete's Code Library

Peter Edney at a wedding

My code library is where I keep all my useful bits of code that I refer to over and again. They are generally incomplete and are a quick tool to remind me of how to resolve an issue.

Categories