Episode 10

Umbraco 13 Tutorial - Episode 10 - Contact Form and Meta Data

Code

ContactViewComponent.cs

using Freelancer.Models.ViewModels;

using Microsoft.AspNetCore.Mvc;

namespace Freelancer.Components;

[ViewComponent(Name = "Contact")]
public class ContactViewComponent : ViewComponent
{
    public IViewComponentResult Invoke(ContactViewModel model)
    {
        model ??= new ContactViewModel();

        return View(model);
    }
}

ContactViewComponent

FreelancerConfig.cs

namespace Freelancer.Configuration;

public class FreelancerConfig
{
    public const string SectionName = "Freelancer";
    public EmailSettings? EmailSettings { get; set; }
}

public class EmailSettings
{
    public string? From { get; set; }
    public string? To { get; set; }
}

FreelancerConfig.cs

ContactSurfaceController.cs

using Freelancer.Configuration;
using Freelancer.Models.ViewModels;

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Mail;
using Umbraco.Cms.Core.Models.Email;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Website.Controllers;

namespace Freelancer.Controllers.Surface;

public class ContactSurfaceController : SurfaceController
{
    private readonly IEmailSender _emailSender;
    private readonly ILogger<ContactSurfaceController> _logger;
    private readonly FreelancerConfig _freelancerConfig;

    public ContactSurfaceController(
        IUmbracoContextAccessor umbracoContextAccessor,
        IUmbracoDatabaseFactory databaseFactory,
        ServiceContext services,
        AppCaches appCaches,
        IProfilingLogger profilingLogger,
        IPublishedUrlProvider publishedUrlProvider,
        IEmailSender emailSender,
        ILogger<ContactSurfaceController> logger,
        IOptions<FreelancerConfig> freelancerConfig) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
    {
        _emailSender = emailSender;
        _logger = logger;
        _freelancerConfig = freelancerConfig.Value;
    }

    public async Task<IActionResult> Submit(ContactViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return CurrentUmbracoPage();
        }

        try
        {
            var subject = string.Format("Enquiry from: {0} - {1}", model.Name, model.Email);
            EmailMessage message = new(_freelancerConfig?.EmailSettings?.From,
                _freelancerConfig?.EmailSettings?.To, subject, model.Message, false);
            await _emailSender.SendAsync(message, emailType: "Contact");

            TempData["ContactSuccess"] = true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Contact Form Submission Error");
            TempData["ContactSuccess"] = false;
        }

        return RedirectToCurrentUmbracoPage();
    }
}

ContactSurfaceController.cs

PublishedContentExtensions.cs

namespace Freelancer.Extensions;

public static class PublishedContentExtensions
{
    public static HomePage? GetHomePage(this IPublishedContent publishedContent)
    {
        return publishedContent.AncestorOrSelf<HomePage>();
    }

    public static SiteSettings? GetSiteSettings(this IPublishedContent publishedContent)
    {
        var homePage = GetHomePage(publishedContent);
        return homePage?.FirstChild<SiteSettings>();
    }

    public static string GetMetaTitleOrName(this IPublishedContent publishedContent, string? metaTitle)
    {
        if (!string.IsNullOrWhiteSpace(metaTitle)) return metaTitle;

        return publishedContent.Name;
    }

    public static string? GetSiteName(this IPublishedContent publishedContent)
    {
        var homePage = publishedContent.GetHomePage();
        if (homePage == null) return null;
        var siteSettings = homePage.GetSiteSettings();
        if (siteSettings == null) return null;
        return siteSettings?.SiteName ?? null;
    }
}

PublishedContentExtensions.cs

ContactViewModel.cs

using System.ComponentModel.DataAnnotations;

using Freelancer.Validation;

namespace Freelancer.Models.ViewModels;

public class ContactViewModel
{
    [Display(Name = "Full name")]
    [Required(ErrorMessage = "You must enter your name")]
    public string? Name { get; set; }

    [Display(Name = "Email address")]
    [EmailAddress(ErrorMessage = "You must enter a valid email address")]
    [Required(ErrorMessage = "You must enter your email address")]
    public string? Email { get; set; }

    [Display(Name = "Phone number")]
    public string? Phone { get; set; }

    [Display(Name = "Message")]
    [Required(ErrorMessage = "You must enter your message")]
    public string? Message { get; set; }

    [Display(Name = "Yes, I give permission to store and process my data")]
    [Required(ErrorMessage = "You must give consent to us storing your details before you can send us a message")]
    [MustBeTrue(ErrorMessage = "You must give consent to us storing your details before you can send us a message")]
    public bool Consent { get; set; }
}

ContacViewModel.cs

Program.cs

using Freelancer.Configuration;

using Slimsy.DependencyInjection;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.CreateUmbracoBuilder()
    .AddBackOffice()
    .AddWebsite()
    .AddSlimsy()
    .AddDeliveryApi()
    .AddComposers()
    .Build();

builder.Services.Configure<FreelancerConfig>(
    builder.Configuration.GetSection(FreelancerConfig.SectionName));

WebApplication app = builder.Build();

await app.BootUmbracoAsync();


app.UseUmbraco()
    .WithMiddleware(u =>
    {
        u.UseBackOffice();
        u.UseWebsite();
    })
    .WithEndpoints(u =>
    {
        u.UseInstallerEndpoints();
        u.UseBackOfficeEndpoints();
        u.UseWebsiteEndpoints();
    });

await app.RunAsync();

Program.cs

ValidationAttributes.cs

using System.ComponentModel.DataAnnotations;

namespace Freelancer.Validation;

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class MustBeTrue : ValidationAttribute
{
    public override bool IsValid(object? value)
    {
        return value != null && value is bool v && v;
    }
}

ValidationAttributes.cs

HomePage.cshtml

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.HomePage>

@{
    Layout = "Main.cshtml";
}

@await Html.GetBlockGridHtmlAsync(Model, "headerContent")
@await Html.GetBlockGridHtmlAsync(Model, "mainContent")

HomePage.cshtml

form.cshtml

@inherits UmbracoViewPage<Umbraco.Cms.Core.Models.Blocks.BlockGridItem<ContentModels.Form>>

<!-- Contact Section-->
<section class="page-section" id="contact">
    <div class="container">
        <!-- Contact Section Heading-->
        <h2 class="page-section-heading text-center text-uppercase text-secondary mb-0">Contact Me</h2>
        <!-- Icon Divider-->
        <div class="divider-custom">
            <div class="divider-custom-line"></div>
            <div class="divider-custom-icon"><svg style="width: 1.2em; height: 1.2em;" class="svg-inline--fa fa-star" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="star" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" data-fa-i2svg=""><path fill="currentColor" d="M316.9 18C311.6 7 300.4 0 288.1 0s-23.4 7-28.8 18L195 150.3 51.4 171.5c-12 1.8-22 10.2-25.7 21.7s-.7 24.2 7.9 32.7L137.8 329 113.2 474.7c-2 12 3 24.2 12.9 31.3s23 8 33.8 2.3l128.3-68.5 128.3 68.5c10.8 5.7 23.9 4.9 33.8-2.3s14.9-19.3 12.9-31.3L438.5 329 542.7 225.9c8.6-8.5 11.7-21.2 7.9-32.7s-13.7-19.9-25.7-21.7L381.2 150.3 316.9 18z"></path></svg></div>
            <div class="divider-custom-line"></div>
        </div>
        <!-- Contact Section Form-->
        <div class="row justify-content-center">
            <div class="col-lg-8 col-xl-7">
                @await Component.InvokeAsync("Contact")
            </div>
        </div>
    </div>
</section>

form.cshtml

metaData.cshtml

@inherits UmbracoViewPage<ISEoproperties>

@if (Model == null) return;

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="@Model.MetaDescription" />
<title>@Umbraco.AssignedContentItem.GetMetaTitleOrName(Model.MetaTitle) | @(Umbraco.AssignedContentItem.GetSiteName())</title>
<meta name="robots" content="@(Model.IsFollowable ? "FOLLOW," : "NOFOLLOW,")@(Model.IsIndexable ? "INDEX" : "NOINDEX")" />
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico" />

metaData.cshtml

Default.cshtml

@inherits UmbracoViewPage<ContactViewModel>
@using Freelancer.Controllers.Surface
@using Freelancer.Models.ViewModels

@{
    bool submitted = bool.TryParse(TempData["ContactSuccess"]?.ToString() ?? string.Empty, out var success);
}

@if(submitted)
{
    <div class="row">
        <div class="col-12 text-center">
            @if(success)
            {
                <p>@Umbraco.GetDictionaryValueOrDefault("ContactForm.SuccessMessage", "Thank you for your email. We will be in touch shortly.")</p>
            }
            else
            {
                <p>@Umbraco.GetDictionaryValueOrDefault("ContactForm.ErrorMessage", "There was a problem when submitting the form. Please try again later.")</p>
            }
        </div>
    </div>
}
else
{
    using (Html.BeginUmbracoForm<ContactSurfaceController>("Submit", FormMethod.Post, new { @class = "text-left", role = "form" }))
    {
        <!-- Name input-->
        <div class="form-floating mb-3">
            <input asp-for="@Model.Name" class="form-control" id="name" type="text" placeholder="Full name" aria-label="Name" />
            <label asp-for="@Model.Name"></label>
            <span asp-validation-for="@Model.Name" class="text-danger"></span>
        </div>
        <!-- Email address input-->
        <div class="form-floating mb-3">
            <input asp-for="@Model.Email" class="form-control" id="name" type="text" placeholder="Email address" aria-label="Email" />
            <label asp-for="@Model.Email"></label>
            <span asp-validation-for="@Model.Email" class="text-danger"></span>
        </div>
        <!-- Phone number input-->
        <div class="form-floating mb-3">
            <input asp-for="@Model.Phone" class="form-control" id="name" type="text" placeholder="Phone number" aria-label="Phone" />
            <label asp-for="@Model.Phone"></label>
            <span asp-validation-for="@Model.Phone" class="text-danger"></span>
        </div>
        <!-- Message input-->
        <div class="form-floating mb-3">
            <input asp-for="@Model.Message" class="form-control" id="name" type="text" placeholder="Message" aria-label="Message" />
            <label asp-for="@Model.Message"></label>
            <span asp-validation-for="@Model.Message" class="text-danger"></span>
        </div>
        <div class="form-check form-check-info text-left mb-3">
            <input asp-for="@Model.Consent" class="form-check-input" type="checkbox">
            <label asp-for="@Model.Consent" class="form-check-label">
                I agree to the <a href="/privacy-policy/" class="text-dark font-weight-bolder">Privacy Policy</a>
            </label>
            <span asp-validation-for="@Model.Consent" class="text-danger"></span>
        </div>
        <div class="form-group">
            <button class="btn btn-primary btn-block btn-lg" type="submit">Register</button>
        </div>
    }
}

Default.cshtml

appsettings.json

,
"Freelancer": {
    "EmailSettings": {
        "From": "[email protected]",
        "To":  "[email protected]"
    }
}

appsettings.json