Umbraco 8 Contact Form Tutorial - FadiZahhar/umbraco8showandtell GitHub Wiki
-
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.
-
In Umbraco Backoffice, go to Settings → Partial Views.
-
Click Create, and name it:
ContactForm.cshtml
-
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
, methodHandleContact
. -
After submission, it can display a success or error message from TempData.
-
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.
-
In your page template (e.g.,
Contact.cshtml
orHome.cshtml
):
@Html.Partial("ContactForm")
-
Copy your
ContactForm.cshtml
toViews/MacroPartials/ContactForm.cshtml
. -
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).
-
-
Now, in the content editor, you can insert the Macro in RTE fields or the grid:
-
Use Insert Macro > select
ContactForm
.
-
-
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"]
.
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
.
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 |
-
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.
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)
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.
-
(Based on your HighlyDeveloped.Core code)
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.
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.
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))
{
<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)
-
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.
-
Copy your
Contact Form.cshtml
toViews/MacroPartials/Contact Form.cshtml
. -
In Umbraco Backoffice > Settings > Partial View Macro Files, create a Macro and link it to the partial.
-
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.
-
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.
-
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.
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 |
-
Visitor sees the contact form (with optional reCAPTCHA).
-
Submits the form, which posts to
ContactController.HandleContactForm
. -
Validation runs. If valid, submission is:
-
Saved as content in Umbraco (so you always have a record)
-
Emailed to admin addresses
-
-
Success/Failure feedback is shown to user.
-
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
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; }
}
}
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);
}
}
}
}
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)
File: /Views/MacroPartials/Contact Form.cshtml
Copy the same code as above!
Backoffice Setup:
- Go to Settings → Partial View Macro Files.
- Create a new Macro called "Contact Form".
- Link it to
/Views/MacroPartials/Contact Form.cshtml
. - Now editors can insert the contact form as a macro in RTEs or grids.
(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)
@Html.Action("RenderContactForm", "Contact")
This ensures the form is always initialized with the reCAPTCHA key and model.
- Use the Umbraco content editor to insert the Macro "Contact Form" wherever needed.
- 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.
- 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.