Member Registration in Umbraco 9
In this article, we will look into Member Registration in Umbraco 9.
Umbraco 9 makes use of ASP.NET Core Identity for both backoffice users and website members. With previous versions of Umbraco, we had the MembershipHelper
that was handy for accessing member data as IPublishedContent
. The helper is available in Umbraco 9 as well. It contains a variety of helper methods that can be used in views and controllers. Umbraco 9 also comes with an interface IMemberManager
that can help access member data as MemberIdentityUser
.
Let us start with the view model for registration, the code is as shown below.
public class RegisterViewModel
{
public string Email { get; set; }
public string Password { get; set; }
public bool HasAPetUnicorn { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Now we need a ViewComponent to build up the form. The ViewComponent passes an instance of the RegisterViewModel to my view.
public class RegisterViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View(new RegisterViewModel());
}
}
The View(Default.cshtml
) for this ViewComponent is in the folder ~/Views/Components/Register
. This is a bit of convention followed for ViewComponents. The code for the view is as shown below.
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<RegisterViewModel>
@using (Html.BeginUmbracoForm("Register", "AccountSurface", FormMethod.Post, new { @class = "text-left" }))
{
<div class="form-floating">
<input asp-for="@Model.FirstName" class="form-control" id="firstname" type="text" placeholder="Enter your first name..." autocomplete="off" data-sb-validations="required" />
<label for="Name">First Name</label>
<span asp-validation-for="@Model.FirstName" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="@Model.LastName" class="form-control" id="lastname" type="text" placeholder="Enter your last name..." autocomplete="off" data-sb-validations="required" />
<label for="Name">Last Name</label>
<span asp-validation-for="@Model.LastName" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="@Model.Email" class="form-control" id="email" placeholder="Enter your email..." autocomplete="off" data-sb-validations="required,email"/>
<label for="Email">Email</label>
<span asp-validation-for="@Model.Email" class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="@Model.Password" class="form-control" type="password" id="password" placeholder="Enter your email..." autocomplete="off" data-sb-validations="required"/>
<label for="Email">Password</label>
<span asp-validation-for="@Model.Password" class="text-danger"></span>
</div>
<div class="form-floating">
<input type="checkbox" asp-for="@Model.HasAPetUnicorn" >
<label for="Message">Has a pet unicorn</label>
</div>
<br />
<div class="form-floating">
<button class="btn btn-primary text-uppercase float-end" id="register" type="submit">Register</button>
</div>
}
I have a document type called Register
which has a default template by the same name. It is a very basic document type. I can invoke my ViewComponent in the template as shown below.
@await Component.InvokeAsync("Register")
The view posts back to an action called Register
in the AccountSurfaceController
. So let us have a look at the code for the action.
public class AccountSurfaceController : SurfaceController
{
private readonly IMemberService _memberService;
private readonly IMemberManager _memberManager;
private readonly IMemberSignInManager _memberSignInManager;
private readonly IPublishedContentQuery _publishedContentQuery;
private readonly ILogger<AccountSurfaceController> _logger;
public AccountSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IMemberService memberService, IMemberManager memberManager, IMemberSignInManager memberSignInManager, IPublishedContentQuery publishedContentQuery, ILogger<AccountSurfaceController> logger) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_memberService = memberService;
_memberManager = memberManager;
_memberSignInManager = memberSignInManager;
_publishedContentQuery = publishedContentQuery;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!ModelState.IsValid)
{
return CurrentUmbracoPage();
}
//check whether the user is already registered
if (await _memberManager.FindByEmailAsync(model.Email) != null)
{
TempData["Success"] = false;
TempData["errorMessage"] = "User already registered!";
return CurrentUmbracoPage();
}
//create a new identity member instance without an identity
var identityMember = MemberIdentityUser.CreateNew(model.Email, model.Email, Models.Member.ModelTypeAlias, true, model.FirstName + " " + model.LastName);
//create an identity member
var identityResult = await _memberManager.CreateAsync(identityMember, model.Password);
if (identityResult.Succeeded)
{
TempData["Success"] = true;
if (model.HasAPetUnicorn)
{
//add to role
await _memberManager.AddToRolesAsync(identityMember, new string[] { "unicorn user group" });
//save the additional details using the MemberService
var member = _memberService.GetByKey(identityMember.Key);
member.SetValue("hasAUnicorn", model.HasAPetUnicorn);
_memberService.Save(member);
}
}
else
{
TempData["Success"] = false;
var errors = identityResult.Errors;
var errorString = new StringBuilder();
//added for learning purposes. This could be logged
foreach(var error in errors)
{
errorString.Append($"{error.Code}-{error.Description}");
}
_logger.LogError(errorString.ToString());
}
return CurrentUmbracoPage();
}
}
I am heavily making use of the IMemberManager
interface in my code as you can see. I have also added some TempData
goodness to show any messages to the end user.
Understanding the concepts behind asynchronous messaging
Understanding the concepts behind asynchronous messaging