#3 View Location Expander and View Renderer
Friday, November 9, 2018
In this article we will expand the locations that the razor view engine will search to find razor views and we will create a razor view renderer that we can inject into our controllers and pages. We are doing this for a few different reasons but the two we will be exploring in just a little while is sending emails and indexing the content of our application for full text searching.
Parts
- Part 29: Offset Pager Urls
- Part 28: Offset Pager Start
- Part 27: Mock Context Builder
- Part 26: Mock Repository
- Part 25: Mock Async
- Part 24: Picture Tag Helper
- Part 23: Img DPR Tag Helper
- Part 22: Img Responsive Tag Helper
- Part 21: Img Optimized Display
- Part 20: Img Optimization
- Part 19: Img Lazy Loading
- Part 18: Img Responsive
- Part 17: Bottom Nav
- Part 16: Main Nav Cookie
- Part 15: Main Nav Mobile
- Part 14: Main Nav Search
- Part 13: Main Nav Auth
- Part 12: Main Nav Anchors
- Part 11: Main Nav Logo
- Part 10: Search Results
- Part 9: Search Manager
- Part 8: Search Start
- Part 7: Seeding the Database
- Part 6: Domain Database
- Part 5: Emailing Exceptions
- Part 4: Mailkit
- Part 3: View Renderer
- Part 2: Upgrade to 2.1
- Part 1: Quick Start
Looking for razor files outside of the views folder
- WebUi
- Infrastructure
- ViewLocationExpander.cs
- Infrastructure
By default our view renderer, which we will create in a little bit, will look for views in a few specific locations. In
particular it will try to find razor files located in a folder called 'Views' in the root of our project. For our purpose
we want to specify the location of razor files that will be outside of the View folder. To do this we need to start by
creating a view location expander (a). The important part of the expander
is the ExpandViewLocations
method which takes in a context and view locations that
have already been defined. For our purposes we are going to specify the location of each view that we want to use ourselves
so we will simply union a new template string with the locations already defined.
ViewLocationExpander.cs
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor;
namespace WebUi.Infrastructure
{
public class ViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
var locations = new[] {"{0}.cshtml"};
return locations.Union(viewLocations);
}
public void PopulateValues(ViewLocationExpanderContext context)
{
context.Values["customviewlocation"] = nameof(ViewLocationExpander);
}
}
}
We need to register our expander
- WebUi
- Startup.cs
With our expander created we have to tell our application that we would like to use it. To do this we return to our startup file and add the change shown in (b).
Startup.cs
...
using WebUi.Infrastructure;
namespace WebUi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RazorViewEngineOptions>(x => x.ViewLocationExpanders.Add(new ViewLocationExpander()));
}
...
}
}
Now for an injectable view renderer
- WebUi
- Infrastructure
- IViewRenderer.cs
- ViewRenderer.cs
- Infrastructure
With the changes we have made we are now able to look for razor pages in any location that we specify. To make use of this ability we need a renderer that can convert the razor syntax to a string. We also want to be good developers so we need to be able to test things easily which means we will start by creating an interface for our renderer as shown in (c).
IViewRenderer.cs
namespace WebUi.Infrastructure
{
public interface IViewRenderer
{
string Render<TModel>(string name, TModel model);
}
}
With our interface created it is time to create our renderer (d).
ViewRenderer.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
using System;
using System.IO;
namespace WebUi.Infrastructure
{
public class ViewRenderer : IViewRenderer
{
private readonly IServiceProvider _serviceProvider;
private readonly ITempDataProvider _tempDataProvider;
private readonly IRazorViewEngine _viewEngine;
public ViewRenderer(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public string Render<TModel>(string name, TModel model)
{
var actionContext = GetActionContext();
var viewEngineResult = _viewEngine.FindView(actionContext, name, false);
if (viewEngineResult.Success == false)
{
throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
}
var view = viewEngineResult.View;
using (var output = new StringWriter())
{
WriteToOutput(model, actionContext, view, output);
return output.ToString();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext
{
RequestServices = _serviceProvider
};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
private void WriteToOutput<TModel>(TModel model, ActionContext actionContext, IView view, TextWriter output)
{
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions());
view.RenderAsync(viewContext).GetAwaiter().GetResult();
}
}
}
Adding a service
- WebUi
- Startup.cs
As I have mentioned a few times before we would like to be able to inject our view renderer
when we need it. To do this we find ourselves back in our startup file. This time we need
to add a transient service that maps our IViewRenderer
interface to an instance of our ViewRenderer
class. We
do this by adding the line shown in (e).
Startup.cs
...
using WebUi.Infrastructure;
namespace WebUi
{
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<IViewRenderer, ViewRenderer>();
}
...
}
}
Time to test it out
- WebUi
- Features
- Messaging
- Email
- Test.cshtml
- Email
- Messaging
- Features
Since the direction we are heading is to use what we have created to send emails from our application we will do a little setup for that purpose while testing things out. To that end we need to create a new test razor page (f), in the location shown. Our test page is very simple and only takes in a string for the model.
Test.cshtml
@model string
<h1>@Model</h1>
<p>This is a test</p>
Display our test to the world
- WebUi
- Pages
- Index.cshtml.cs
- Pages
Time to modify our index page so that we can test out our renderer. We of course start by injecting our view renderer in the constructor and then use it to render the content of our test razor page to a string stored on our page model (g).
Index.cshtml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Infrastructure;
namespace WebUi.Pages
{
public class IndexModel : PageModel
{
private readonly IViewRenderer _vr;
public IndexModel(IViewRenderer vr)
{
_vr = vr;
}
public string ViewRendererTest { get; set; }
public void OnGet()
{
ViewRendererTest = _vr.Render("Features/Messaging/Email/Test", "Hello World");
}
}
}
- WebUi
- Pages
- Index.cshtml
- Pages
Next up we need to change the html on our index page to something like what is shown in (h).
Index.cshtml
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
@Model.ViewRendererTest
Once that is done and we refresh the page we should see the output shown in (i). This is of course not exactly what we wanted. The solution, fortunately for us, is very simple.
But I want html
- WebUi
- Pages
- Index.cshtml
- Pages
In order for us to display the html instead of having a string wrapping our html we just need to use the
Raw
method of the Html helper (j).
Index.cshtml
...
@Html.Raw(Model.ViewRendererTest)
With that small change and once again refreshing the page we should see what we are expecting to see (k).