Episode Links
Papercut
https://github.com/ChangemakerStudios/Papercut-SMTP
GitHub Repo
🐙 GitHub
https://github.com/prjseal/Umbraco-13-Series/
📝 Guest Book
https://github.com/prjseal/Umbraco-13-Series/issues/1
Get Help
💬 Discord
🗣️ Facebook Group
https://www.facebook.com/groups/umbracowebdevs
🐘 Mastodon
https://umbracocommunity.social/
🗨️ Umbraco Forum
https://our.umbraco.com/forum/
☕Buy me a coffee
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);
}
}
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; }
}
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();
}
}
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;
}
}
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; }
}
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();
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;
}
}
HomePage.cshtml
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.HomePage>
@{
Layout = "Main.cshtml";
}
@await Html.GetBlockGridHtmlAsync(Model, "headerContent")
@await Html.GetBlockGridHtmlAsync(Model, "mainContent")
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>
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" />
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>
}
}
appsettings.json
,
"Freelancer": {
"EmailSettings": {
"From": "[email protected]",
"To": "[email protected]"
}
}