Advertisement

#3 View Location Expander and View Renderer

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.

Looking for razor files outside of the views folder

  • WebUi
    • Infrastructure
      • ViewLocationExpander.cs

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);
        }
    }
}
(a) Creating a view expander so that we are able to specify and arbitrary location for our razor files.

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()));
        }
        ...
    }
}
(b) Adding our view location expander to the razor view engine options.

Now for an injectable view renderer

  • WebUi
    • Infrastructure
      • IViewRenderer.cs
      • ViewRenderer.cs

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);
    }
}
(c) The interface that we will implement for our view renderer.

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();
        }
    }
}
(d) Our view renderer that we will be able to inject into our controllers and pages when needed.
Advertisement

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>();
        }
        ...
    }
}
(e) Adding the mapping between our interface and the class.

Time to test it out

  • WebUi
    • Features
      • Messaging
        • Email
          • Test.cshtml

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>
(f) Our test razor page which simply takes in a string as the model.

Display our test to the world

  • WebUi
    • Pages
      • Index.cshtml.cs

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");
        }
    }
}
(g) Adjusting our index page to test our view renderer.
  • WebUi
    • Pages
      • Index.cshtml

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
(h) Updating the html of our index page to display our test string.

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.

Right now instead of display html we have a string which contains html.
(i) Right now instead of display html we have a string which contains html.

But I want html

  • WebUi
    • Pages
      • Index.cshtml

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)
(j) Using the raw method will place the html into the page and not a string which contains html.

With that small change and once again refreshing the page we should see what we are expecting to see (k).

Now the html is being displayed the way we intended it to be.
(k) Now the html is being displayed the way we intended it to be.
Exciton Interactive LLC
Advertisement