Umbraco 8 Contact Form Tutorial - FadiZahhar/umbraco8showandtell GitHub Wiki

Partial View & MacroPartial Methods — Step-by-Step


Step 1: Decide on the Implementation

  • Partial View: You render a contact form directly in your page/template, and handle the form in a controller.

  • MacroPartial: You allow editors to insert a contact form anywhere (like in RTE content), by creating a Macro based on a Partial View.

Both use nearly identical code, just different integration points.


Step 2: Create a Contact Form Partial View

  1. In Umbraco Backoffice, go to Settings → Partial Views.

  2. Click Create, and name it:
    ContactForm.cshtml

  3. Paste this code in your partial (simple version):

@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using (Html.BeginUmbracoForm("HandleContact", "ContactSurface", FormMethod.Post))
{
    <div class="form-group">
        <label>Name</label>
        <input type="text" name="name" class="form-control" required />
    </div>
    <div class="form-group">
        <label>Email</label>
        <input type="email" name="email" class="form-control" required />
    </div>
    <div class="form-group">
        <label>Message</label>
        <textarea name="message" class="form-control" required></textarea>
    </div>
    <button type="submit" class="btn btn-primary">Send</button>
}
@if (TempData["success"] != null)
{
    <div class="alert alert-success">@TempData["success"]</div>
}
@if (TempData["error"] != null)
{
    <div class="alert alert-danger">@TempData["error"]</div>
}

What’s happening:

  • This form submits to a Surface Controller named ContactSurfaceController, method HandleContact.

  • After submission, it can display a success or error message from TempData.


Step 3: Create the Surface Controller

  1. In your solution, add a new C# class in Controllers:

using System.Net.Mail;
using System.Web.Mvc;
using Umbraco.Web.Mvc;

public class ContactSurfaceController : SurfaceController { [HttpPost] public ActionResult HandleContact(string name, string email, string message) { // Simple validation if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(message)) { TempData["error"] = "All fields are required."; return RedirectToCurrentUmbracoPage(); } try { // Send email (customize as needed) var mail = new MailMessage(); mail.To.Add("[email protected]"); // change to your admin/support address mail.Subject = "Contact Form Message"; mail.Body = $"From: {name} ({email})\n\n{message}"; // ... configure SMTP client ... TempData["success"] = "Thank you for contacting us!"; } catch { TempData["error"] = "There was a problem sending your message."; } return RedirectToCurrentUmbracoPage(); } }

  • You can extend this with real SMTP configuration, model validation, etc.


Step 4: Render the Partial View

  • In your page template (e.g., Contact.cshtml or Home.cshtml):

@Html.Partial("ContactForm")

Step 5: Create a MacroPartial (For RTE/Macros)

  1. Copy your ContactForm.cshtml to Views/MacroPartials/ContactForm.cshtml.

  2. In Umbraco Backoffice, Settings → Partial View Macro Files → Create Macro:

    • Choose ContactForm (MacroPartial).

    • Optionally, add parameters if you want editors to customize (e.g., email target).

  3. Now, in the content editor, you can insert the Macro in RTE fields or the grid:

    • Use Insert Macro > select ContactForm.


Step 6: Macro Differences

  • In MacroPartial, @inherits Umbraco.Web.Macros.PartialViewMacroPage can be used, but usually the same logic as a normal partial view.

  • Parameters passed from the Macro can be accessed via Model.MacroParameters["parameterName"].


Bonus: Strongly Typed Form Model

If you want to use model binding and validation, define a model:

public class ContactFormModel
{
    [Required]
    public string Name { get; set; }
    [Required, EmailAddress]
    public string Email { get; set; }
    [Required]
    public string Message { get; set; }
}

In the view, change:

@model YourNamespace.ContactFormModel
@using (Html.BeginUmbracoForm<ContactSurfaceController>("HandleContact"))
{
    @Html.TextBoxFor(m => m.Name)
    @Html.TextBoxFor(m => m.Email)
    @Html.TextAreaFor(m => m.Message)
    <button type="submit">Send</button>
}

Update your controller to accept ContactFormModel model.


Summary Table

Step What to Do Where
Partial View Create ContactForm.cshtml Views/Partials/
Controller Add ContactSurfaceController.cs Controllers/
Render Partial @Html.Partial("ContactForm") Page template
MacroPartial Copy to MacroPartials/ContactForm.cshtml Views/MacroPartials/
Create Macro Register Macro using MacroPartial Backoffice/Settings
Insert Macro Insert via RTE/Macro picker Content editor

Why use Partial View vs MacroPartial?

  • Partial View: For fixed form placement (e.g., always on Contact page).

  • MacroPartial: For flexible placement anywhere, by editors.


Let’s extract and examine the core code now.

Here’s what I found in your core code that’s directly related to the contact form:

  • ViewModel/ContactFormViewModel.cs

  • Controllers/ContactController.cs

Now, I’ll read and explain both files, then show you exactly how to wire them into your Umbraco 8 solution (both as a partial and as a macro), giving you a robust and modern contact form workflow.


Let’s check ContactFormViewModel.cs first.

Your ContactFormViewModel is a strongly-typed view model with server-side validation using DataAnnotations:

  • Name: required, max 80 chars

  • EmailAddress: required, valid email

  • Comment: required, max 500 chars

  • Subject: optional, max 255 chars

  • RecaptchaSiteKey: (for reCAPTCHA integration, if needed)


Now, let’s check ContactController.cs.

Your code already demonstrates a robust, enterprise-level contact form workflow for Umbraco 8. Here’s a summary of what your core logic does:

  • ContactFormViewModel: Strongly-typed model with DataAnnotations for validation.

  • ContactController (SurfaceController):

    • RenderContactForm: Renders the form with optional reCAPTCHA.

    • HandleContactForm: Handles POST, validates model, checks reCAPTCHA, saves submission to Umbraco (under "Contact Forms"), and sends an admin email.

    • SendContactFormReceivedEmail: Reads site settings for email addresses and uses SmtpClient.

    • IsCaptchaValid: Calls Google reCAPTCHA API for validation.


Final, Narrative Step-by-Step Tutorial

"How to Implement a Modern Contact Form in Umbraco 8"

(Based on your HighlyDeveloped.Core code)


Step 1: Build the Contact Form ViewModel

Create ContactFormViewModel.cs in your Core project:

public class ContactFormViewModel
{
    [Required]
    [MaxLength(80, ErrorMessage = "Please try and limit to 80 characters")]
    public string Name { get; set; }
    [Required]
    [EmailAddress(ErrorMessage = "Please enter a valid email address")]
    public string EmailAddress { get; set; }
    [Required]
    [MaxLength(500, ErrorMessage = "Please try and limit your comments to 500 characters")]
    public string Comment { get; set; }
    [MaxLength(255, ErrorMessage = "Please try and limit to 255 characters")]
    public string Subject { get; set; }
    public string RecaptchaSiteKey { get; set; }
}

This ViewModel handles all form data and provides server-side validation.


Step 2: Create the Contact Surface Controller

Add ContactController.cs to your Core/Controllers folder:

  • RenderContactForm action prepares the view model, injects the reCAPTCHA site key from Site Settings, and returns the Partial View for the form.

  • HandleContactForm POST action validates input, verifies reCAPTCHA, saves data as Umbraco content, and sends a notification email.

The controller should be registered as a SurfaceController so Umbraco recognizes it for form posting.


Step 3: Create the Contact Form Partial View

In Views/Partials/Contact Form.cshtml:

@model HighlyDeveloped.Core.ViewModel.ContactFormViewModel

@using (Html.BeginUmbracoForm("HandleContactForm", "Contact")) { @Html.AntiForgeryToken() <div class="form-group"> @Html.LabelFor(m => m.Name) @Html.TextBoxFor(m => m.Name, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.Name) </div> <div class="form-group"> @Html.LabelFor(m => m.EmailAddress) @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.EmailAddress) </div> <div class="form-group"> @Html.LabelFor(m => m.Subject) @Html.TextBoxFor(m => m.Subject, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.Subject) </div> <div class="form-group"> @Html.LabelFor(m => m.Comment) @Html.TextAreaFor(m => m.Comment, new { @class = "form-control" }) @Html.ValidationMessageFor(m => m.Comment) </div>

@* reCAPTCHA, if configured *@
@if (!string.IsNullOrWhiteSpace(Model.RecaptchaSiteKey))
{
    &lt;div class="g-recaptcha" data-sitekey="@Model.RecaptchaSiteKey"&gt;&lt;/div&gt;
    &lt;script src="https://www.google.com/recaptcha/api.js" async defer&gt;&lt;/script&gt;
}

&lt;button type="submit" class="btn btn-primary"&gt;Send&lt;/button&gt;

}

@if (TempData["status"] != null && TempData["status"].ToString() == "OK") { <div class="alert alert-success">Thank you for your message. We will be in touch soon.</div> } @Html.ValidationSummary(true)


Step 4: Render the Contact Form

  • In your contact page template (Contact.cshtml or similar), render the form using:

@Html.Action("RenderContactForm", "Contact")

Or, if you wish to insert the partial directly:

@Html.Partial("Contact Form", new HighlyDeveloped.Core.ViewModel.ContactFormViewModel())

But using the controller is preferred as it injects the reCAPTCHA key.


Step 5: Enable Macro Usage (for RTE/Editors)

  1. Copy your Contact Form.cshtml to Views/MacroPartials/Contact Form.cshtml.

  2. In Umbraco Backoffice > Settings > Partial View Macro Files, create a Macro and link it to the partial.

  3. Editors can now insert the Contact Form anywhere using "Insert Macro" in RTE/Grid.

If you need to pass parameters, access them via
Model.MacroParameters["parameterName"]
in your macro partial.


Step 6: Configure Site Settings for Emails and reCAPTCHA

  • In your Site Settings document type, add properties:

    • recaptchaSiteKey (Textstring)

    • recaptchaSecretKey (Textstring)

    • emailSettingsFromAddress (Textstring)

    • emailSettingsAdminAccounts (Textbox Multiple, comma-separated for multiple emails)

Fill these out in the Content section under your Site Settings node.


Step 7: Validation, Storage, and Email

  • The controller stores each submission as a "contactForm" under a "contactForms" content node.

  • The admin receives an email with form details.

  • reCAPTCHA is verified if configured.


Summary Table

Step File/Location Purpose
ViewModel Core/ViewModel/ContactFormViewModel.cs Data & validation for the contact form
Controller Core/Controllers/ContactController.cs Handles form logic, reCAPTCHA, storage, emailing
Partial View Views/Partials/Contact Form.cshtml Razor form, renders UI, does model binding
MacroPartial Views/MacroPartials/Contact Form.cshtml (optional) Enables editors to insert via Macro in Umbraco
Site Settings Site Settings DocType Stores keys/addresses for email & reCAPTCHA

How it Works End-to-End

  1. Visitor sees the contact form (with optional reCAPTCHA).

  2. Submits the form, which posts to ContactController.HandleContactForm.

  3. Validation runs. If valid, submission is:

    • Saved as content in Umbraco (so you always have a record)

    • Emailed to admin addresses

  4. Success/Failure feedback is shown to user.

  5. reCAPTCHA blocks spam if enabled.


This is a professional, scalable way to implement contact forms in Umbraco 8. Your code is already set up for best practices!

Here is a complete code pack for your Umbraco 8 contact form, using your HighlyDeveloped.Core logic and following best practices. This includes:

  • ViewModel
  • Surface Controller
  • Partial View
  • MacroPartial
  • Site Settings Doc Type Properties
  • How to render on a page

🗂️ Contact Form Complete Code Pack for Umbraco 8


1. ViewModel

File: HighlyDeveloped.Core/ViewModel/ContactFormViewModel.cs

using System.ComponentModel.DataAnnotations;

namespace HighlyDeveloped.Core.ViewModel
{
    public class ContactFormViewModel
    {
        [Required]
        [MaxLength(80, ErrorMessage = "Please try and limit to 80 characters")]
        public string Name { get; set; }

        [Required]
        [EmailAddress(ErrorMessage = "Please enter a valid email address")]
        public string EmailAddress { get; set; }

        [Required]
        [MaxLength(500, ErrorMessage = "Please try and limit your comments to 500 characters")]
        public string Comment { get; set; }

        [MaxLength(255, ErrorMessage = "Please try and limit to 255 characters")]
        public string Subject { get; set; }

        public string RecaptchaSiteKey { get; set; }
    }
}

2. Surface Controller

File: HighlyDeveloped.Core/Controllers/ContactController.cs

using HighlyDeveloped.Core.ViewModel;
using System;
using System.Linq;
using System.Web.Mvc;
using Umbraco.Web.Mvc;
using System.Net.Mail;
using System.Net.Http;
using Newtonsoft.Json.Linq;

namespace HighlyDeveloped.Core.Controllers
{
    public class ContactController : SurfaceController
    {
        public ActionResult RenderContactForm()
        {
            var vm = new ContactFormViewModel();

            // Set reCAPTCHA site key from Site Settings
            var siteSettings = Umbraco.ContentAtRoot().DescendantsOrSelfOfType("siteSettings").FirstOrDefault();
            if (siteSettings != null)
            {
                vm.RecaptchaSiteKey = siteSettings.Value<string>("recaptchaSiteKey");
            }
            return PartialView("~/Views/Partials/Contact Form.cshtml", vm);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult HandleContactForm(ContactFormViewModel vm)
        {
            if (!ModelState.IsValid)
            {
                ModelState.AddModelError("Error", "Please check the form.");
                return CurrentUmbracoPage();
            }
            var siteSettings = Umbraco.ContentAtRoot().DescendantsOrSelfOfType("siteSettings").FirstOrDefault();
            if (siteSettings != null)
            {
                var secretKey = siteSettings.Value<string>("recaptchaSecretKey");
                if (!string.IsNullOrEmpty(secretKey) && !string.IsNullOrEmpty(Request.Form["g-recaptcha-response"]))
                {
                    var isCaptchaValid = IsCaptchaValid(Request.Form["g-recaptcha-response"], secretKey);
                    if (!isCaptchaValid)
                    {
                        ModelState.AddModelError("Captcha", "The captcha is not valid. Are you human?");
                        return CurrentUmbracoPage();
                    }
                }
            }

            try
            {
                // Store in Umbraco as a "contactForm" under "contactForms"
                var contactForms = Umbraco.ContentAtRoot().DescendantsOrSelfOfType("contactForms").FirstOrDefault();
                if (contactForms != null)
                {
                    var newContact = Services.ContentService.Create("Contact", contactForms.Id, "contactForm");
                    newContact.SetValue("contactName", vm.Name);
                    newContact.SetValue("contactEmail", vm.EmailAddress);
                    newContact.SetValue("contactSubject", vm.Subject);
                    newContact.SetValue("contactComments", vm.Comment);
                    Services.ContentService.SaveAndPublish(newContact);
                }

                SendContactFormReceivedEmail(vm);

                TempData["status"] = "OK";
                return RedirectToCurrentUmbracoPage();
            }
            catch (Exception exc)
            {
                // Log error (optionally use Umbraco's logger)
                ModelState.AddModelError("Error", "Sorry there was a problem noting your details. Would you please try again later?");
            }
            return CurrentUmbracoPage();
        }

        private bool IsCaptchaValid(string token, string secretKey)
        {
            using (var httpClient = new HttpClient())
            {
                var res = httpClient
                    .GetAsync($"https://www.google.com/recaptcha/api/siteverify?secret={secretKey}&response={token}")
                    .Result;
                if (res.StatusCode != System.Net.HttpStatusCode.OK)
                    return false;

                string jsonRes = res.Content.ReadAsStringAsync().Result;
                dynamic jsonData = JObject.Parse(jsonRes);
                return jsonData.success == true;
            }
        }

        private void SendContactFormReceivedEmail(ContactFormViewModel vm)
        {
            var siteSettings = Umbraco.ContentAtRoot().DescendantsOrSelfOfType("siteSettings").FirstOrDefault();
            if (siteSettings == null)
            {
                throw new Exception("There are no site settings");
            }

            var fromAddress = siteSettings.Value<string>("emailSettingsFromAddress");
            var toAddresses = siteSettings.Value<string>("emailSettingsAdminAccounts");

            if (string.IsNullOrEmpty(fromAddress))
                throw new Exception("There needs to be a from address in site settings");
            if (string.IsNullOrEmpty(toAddresses))
                throw new Exception("There needs to be a to address in site settings");

            var emailSubject = "There has been a contact form submitted";
            var emailBody = $"A new contact form has been received from {vm.Name} ({vm.EmailAddress}).\nSubject: {vm.Subject}\n\nTheir comments were:\n{vm.Comment}";

            var smtpMessage = new MailMessage();
            smtpMessage.Subject = emailSubject;
            smtpMessage.Body = emailBody;
            smtpMessage.From = new MailAddress(fromAddress);

            var toList = toAddresses.Split(',');
            foreach (var item in toList)
            {
                if (!string.IsNullOrEmpty(item))
                    smtpMessage.To.Add(item.Trim());
            }

            using (var smtp = new SmtpClient())
            {
                smtp.Send(smtpMessage);
            }
        }
    }
}

3. Partial View

File: /Views/Partials/Contact Form.cshtml

@model HighlyDeveloped.Core.ViewModel.ContactFormViewModel

@using (Html.BeginUmbracoForm("HandleContactForm", "Contact"))
{
    @Html.AntiForgeryToken()
    <div class="form-group">
        @Html.LabelFor(m => m.Name)
        @Html.TextBoxFor(m => m.Name, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Name)
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.EmailAddress)
        @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.EmailAddress)
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Subject)
        @Html.TextBoxFor(m => m.Subject, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Subject)
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Comment)
        @Html.TextAreaFor(m => m.Comment, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Comment)
    </div>

    @* Render reCAPTCHA if configured *@
    @if (!string.IsNullOrWhiteSpace(Model.RecaptchaSiteKey))
    {
        <div class="g-recaptcha" data-sitekey="@Model.RecaptchaSiteKey"></div>
        <script src="https://www.google.com/recaptcha/api.js" async defer></script>
    }

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

@if (TempData["status"] != null && TempData["status"].ToString() == "OK")
{
    <div class="alert alert-success">Thank you for your message. We will be in touch soon.</div>
}
@Html.ValidationSummary(true)

4. MacroPartial (for Rich Text Editor or Grid Editor usage)

File: /Views/MacroPartials/Contact Form.cshtml Copy the same code as above!

Backoffice Setup:

  1. Go to Settings → Partial View Macro Files.
  2. Create a new Macro called "Contact Form".
  3. Link it to /Views/MacroPartials/Contact Form.cshtml.
  4. Now editors can insert the contact form as a macro in RTEs or grids.

5. Site Settings Document Type

(In Umbraco Backoffice > Settings > Document Types, add these fields to your "Site Settings" doc type:)

  • recaptchaSiteKey (Textstring)
  • recaptchaSecretKey (Textstring)
  • emailSettingsFromAddress (Textstring, e.g. [email protected])
  • emailSettingsAdminAccounts (Textbox Multiple, comma-separated email addresses)

6. How to Render the Form

A. On a Contact Page (Template or View)

@Html.Action("RenderContactForm", "Contact")

This ensures the form is always initialized with the reCAPTCHA key and model.


B. As a Macro (RTE, Grid, etc.)

  • Use the Umbraco content editor to insert the Macro "Contact Form" wherever needed.

7. Ensure Email and SMTP is Configured

  • Set up your SMTP settings in web.config or with your hosting provider.
  • Make sure your site’s "from" and "to" addresses are set in Site Settings.

Recap: What Happens

  • The form is shown to users, with validation and optional reCAPTCHA.
  • On submit, Umbraco validates and stores the submission, then emails your admins.
  • Works as a partial view, or as a Macro anywhere on the site.
  • All error and status feedback is handled and shown to the user.
⚠️ **GitHub.com Fallback** ⚠️