Using view engines

When ASP.NET Core 3 uses server-side code for rendering HTML, it uses a view engine. By default, when building standard views with their associated .cshtml files, we use the Razor view engine with the Razor syntax, for example.

By convention, this engine is able to work with views, which are located within the Views folder. Since it is built-in and it is the default engine, it is automatically bound to the HTTP request pipeline without us needing to do anything for it to work.

If we need to use Razor to render files that are located outside of the Views folder and don't come directly from the HTTP request pipeline, such as an email template, we cannot use the default Razor view engine. Instead, we need to define our own view engine and make it responsible for generating the HTML code.

In the following example, we will explain how you can use Razor to render an email based on an email template that isn't coming from the HTTP request pipeline:

  1. Open the Solution Explorer and create a new folder called ViewEngines. Then, add a new class called EmailViewEngine.cs that has the following constructor: 
public class EmailViewEngine
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public EmailViewEngine( IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider, IServiceProvider
serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
...
}

Within the same EmailViewEngine, let's create a FindView method, as follows: 

 private IView FindView(ActionContext actionContext, string viewName)
{
var getViewResult = _viewEngine.GetView(executingFilePath:
null, viewPath: viewName, isMainPage: true);
if (getViewResult.Success)
return getViewResult.View;
var findViewResult = _viewEngine.FindView(actionContext,
viewName, isMainPage: true);
if (findViewResult.Success)
return findViewResult.View;
var searchedLocations = getViewResult.
SearchedLocations.Concat(findViewResult.SearchedLocations);
var errorMessage = string.Join
( Environment.NewLine, new[] { $"Unable to
find view '{viewName}'. The following locations
were searched:" }.Concat(searchedLocations));
throw new InvalidOperationException(errorMessage);
}

Let's create a GetActionContext method in the same EmailViewEngine class:

private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(),
new ActionDescriptor());
}

We will use the preceding method in the following RenderEmailToString method, as follows: 

public async Task<string> RenderEmailToString<TModel>(string viewName, TModel model)
{
var actionContext = GetActionContext();
var view = FindView(actionContext, viewName);
if (view == null)
throw new InvalidOperationException(string.Format
("Couldn't find view '{0}'", viewName));
using var output = new StringWriter();
var viewContext = new ViewContext(actionContext,
view,
new ViewDataDictionary<TModel>(metadataProvider:
new
EmptyModelMetadataProvider(), modelState: new
ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(actionContext.HttpContext,
_tempDataProvider), output, new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return output.ToString();
}

After creating the EmailViewEngine class, extract its interface, IEmailViewEngine, as follows: 

public interface IEmailViewEngine
{
Task<string> RenderEmailToString<TModel>(string
viewName, TModel model);
}

  1. Create a new folder called Helpers and add a new class to it called EmailViewRenderHelper.cs
public class EmailViewRenderHelper
{
IWebHostEnvironment _hostingEnvironment;
IConfiguration _configurationRoot;
IHttpContextAccessor _httpContextAccessor;
public async Task<string> RenderTemplate<T>(string
template, IWebHostEnvironment hostingEnvironment,
IConfiguration configurationRoot,
IHttpContextAccessor httpContextAccessor, T model
) where T : class
{
_hostingEnvironment = hostingEnvironment;
_configurationRoot = configurationRoot;
_httpContextAccessor = httpContextAccessor;
var renderer = httpContextAccessor.HttpContext.
RequestServices
.GetRequiredService<IEmailViewEngine>();
return await renderer.RenderEmailToString<T>(template,
model);
}
}
  1. Add a new service called EmailTemplateRenderService in the Services folder. It will have the following constructor:
public class EmailTemplateRenderService
{
private IWebHostEnvironment _hostingEnvironment;
private IConfiguration _configuration;
private IHttpContextAccessor _httpContextAccessor;
public EmailTemplateRenderService(IWebHostEnvironment
hostingEnvironment, IConfiguration configuration,
IHttpContextAccessor httpContextAccessor)
{
_hostingEnvironment = hostingEnvironment;
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;
}
}

Now, create a RenderTemplate method, as follows: 

public async Task<string> RenderTemplate<T>(string templateName, T model, string host) where T : class
{
var html = await new EmailViewRenderHelper().
RenderTemplate(templateName, _hostingEnvironment,
_configuration, _httpContextAccessor, model);
var targetDir = Path.Combine(Directory.
GetCurrentDirectory(), "wwwroot", "Emails");
if (!Directory.Exists(targetDir))
Directory.CreateDirectory(targetDir);
string dateTime = DateTime.Now.
ToString("ddMMHHyyHHmmss");
var targetFileName = Path.Combine(targetDir,
templateName.Replace("/", "_").Replace("\", "_")
+ "." + dateTime + ".html");
html = html.Replace("{ViewOnLine}", $"
{host.TrimEnd('/')}/Emails/{Path.GetFileName
(targetFileName)}");
html = html.Replace("{ServerUrl}", host);
File.WriteAllText(targetFileName, html);
return html;
}

 Extract its interface and name it IEmailTemplateRenderService.

  1. Register EmailViewEngine and EmailTemplateRenderService in the Startup class: 
services.AddTransient<IEmailTemplateRenderService,  
EmailTemplateRenderService>();
services.AddTransient<IEmailViewEngine,
EmailViewEngine>
();
 Note that you need to register EmailViewEngine and EmailTemplateRenderService as transient because of HTTPContextAccessor injection.
  1. Add a new layout page in the Views/Shared folder called _LayoutEmail.cshtml. First, we'll create the head section, as follows: 
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-
scale=1.0" />
<title>@ViewData["Title"] - TicTacToe</title>
<environment include="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7
/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css
/bootstrap.min.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css"
asp-append-version="true" />
</environment>
</head>

Now, we'll create the body section, as follows: 

<body>
<div class="container body-content">
@RenderBody()
<hr />
<footer> <p>&copy; 2019 - TicTacToe</p> </footer>
</div>
<environment include="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js">
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>

@RenderSection("Scripts", required: false)
</body>
  1. Add a new model called UserRegistrationEmailModel to the Models folder: 
public class UserRegistrationEmailModel
{
public string Email { get; set; }
public string DisplayName { get; set; }
public string ActionUrl { get; set; }
}
  1. Create a new subfolder called EmailTemplates in the Views folder and add a new view called UserRegistrationEmail
@model TicTacToe.Models.UserRegistrationEmailModel
@{
ViewData["Title"] = "View";
Layout = "_LayoutEmail";
}
<h1>Welcome @Model.DisplayName</h1>
Thank you for registering on our website. Please click <a href="@Model.ActionUrl">here</a> to confirm your email.
  1. Update the EmailConfirmation method within UserRegistrationController so that we can use the new email view engine before sending any emails:
var userRegistrationEmail = new UserRegistrationEmailModel
{
DisplayName = $"{user.FirstName} {user.LastName}",
Email = email,
ActionUrl = Url.Action(urlAction)
};

var emailRenderService = HttpContext.RequestServices.
GetService<IEmailTemplateRenderService>();
var message = await emailRenderService.RenderTemplate
("EmailTemplates/UserRegistrationEmail",
userRegistrationEmail, Request.Host.ToString());
  1. Start the application and register a new user. Open UserRegistrationEmail and analyze its content (look in the wwwroot/Emails folder):

If you see the InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Http.IHttpContextAccessor error, you will need to register IHttpContextAccessor manually in the Startup class by adding services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); in the ConfigureServices method or by adding the built-in services.AddHttpContextAccessor(); method.

You have looked at a variety of concepts and code examples throughout this book, but we still haven't talked about how to ensure excellent quality and maintainability for our applications. The next section is going to shed some light on this subject, which is dedicated to application testing.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.192.59