View engine

In the previous article, we implemented the Session and provided a higher level of encapsulation for HttpListenerContext in the process. In the results returned by the Controller we can see the results dynamically executed by the server, but for now they exist as raw strings. From a basic point of view, there is nothing wrong with returning strings ———— The early CGI/ servlets of the Internet did this. The problem is that the interface is too low-level. Designers want to see HTML pages, not concatenate strings themselves. That’s why a View Engine exists.

View engines have been around for a long time. In the jargon of other frameworks, it is sometimes called a Template Engine or something like that, because it can theoretically be used to generate anything in text form, not just HTML files. However, it has always been the most widely used for generating pages. Recently, Nodejs has seen an explosion of template engines, with dozens of view engines available in Nodejs. Another interesting thing is that view engines have evolved from an early “parasitic” form that was tied to a specific Web framework to a standalone one that can be used in many different languages and frameworks. For example, Razor, which grew out of ASP.NET MVC, has been transformed by hobbyists into vash, which can be used in many Web frameworks for Nodejs. Jinja2, well known in the Python world, has also been ported to Nunjucks and is also available for Nodejs. The well-known Handlebars (whose core language is Mustache) can be used in a variety of situations.

It is possible to implement a template engine yourself from scratch. Several early programming books discussed this topic, but the implementation takes several chapters. That’s a little too big for this series. We know that the Razor engine already has a separate version of RazorEngine, so in this article, we will integrate it directly into our Own Web application.

code

The sample code of this article has been put on Github, and the code associated with each article is placed in a separate branch for readers’ reference. Therefore, to get the sample code for this article, use the following command:

git clone https://github.com/shuhari/web-server-succinctly-example.git
git checkout -b  05-view-engine origin/05-view-engine 
Copy the code

implementation

So far, our program has been a very simple console application and doesn’t reference any external resources other than BCL. Now, in order to use RazorEngine, we have to include it. The easiest way, of course, is through Nuget. Enter the following command on the Nuget console:

install-package RazorEngine
Copy the code

It then waits for Nuget to download and reference the necessary library files.

Note: according to the original book, in the program directly reference RazorEngine DLL file will lead to version conflict, the author suggested that the practice is to download the source code to compile. My own experiments have shown that when I run an application, I see a warning message, but it still works. The difference may be due to the version of the library (the book has been out for some time). I suggest you use Nuget first, and then follow the author’s advice if you have problems with it.

In the previous article, to avoid implementing a large number of subclasses, we made the Controller return string and output the HTML content directly. Now, however, with view engines, the problem can no longer be avoided. To emulate the ASP.NET MVC interface, define the base class first:

public abstract class ActionResult
{
    public abstract void Execute(HttpServerContext context);
}
Copy the code

Subclasses of ActionResult decide for themselves what results to return ———— This is one of the flexibility of the framework and is more unit testing friendly.

In some cases you may not need to go through the view engine. For example, some Web apis expect you to return an OK or even null. The output is the same whether you use a view engine or not, so let’s add another helper method:

public static class HttpUtil { ... public static HttpListenerResponse Content(this HttpListenerResponse response, string content, string mimeType) { var contentBytes = Encoding.UTF8.GetBytes(content); response.ContentType = mimeType; response.StatusCode = 200; response.ContentLength64 = contentBytes.Length; response.OutputStream.Write(contentBytes, 0, contentBytes.Length); response.OutputStream.Close(); return response; }}Copy the code

For scenarios that do not use a view engine, simply return the content:

public class ContentResult : ActionResult { public ContentResult(string content, string mimeType = null) { _content = content ?? ""; _mimeType = mimeType ?? "text/html"; } private readonly string _content; private readonly string _mimeType; public override void Execute(HttpServerContext context) { context.Response.Content(_content, _mimeType); }}Copy the code

For scenarios using a view engine, we need to decide where the view files are located. For those familiar with ASP.NET MVC, the Razor view engine has a complex set of rules for finding views, but we don’t want to make things too complicated, so we follow the normal project structure. Find files named Views/{controller}/{action}.cshtml directly.

public class ViewResult : ActionResult { public ViewResult(string controllerName, string viewName, object model) { _viewLocation = FindViewLocation(controllerName, viewName); _model = model; } private string _viewLocation; private object _model; private string FindViewLocation(string controllerName, string viewName) { var baseDir = Path.GetDirectoryName(GetType().Assembly.Location); string filePath = Path.Combine(baseDir, "Views", controllerName, viewName + ".cshtml"); return filePath; } public override void Execute(HttpServerContext context) { var tpl = File.ReadAllText(_viewLocation, System.Text.Encoding.UTF8); var result = Razor.Parse(tpl, _model); context.Response.Content(result, "text/html"); }}Copy the code

Also remember to change the output mode of the.cshtml file to Copy if newer (or Copy always) in the file properties so that the executable can find the location of the view file.

ActionResult and subclasses have been added. Next we need to adjust the execution logic of the route to accept ActionResult returns:

public class Routing : IMiddleware { ... public MiddlewareResult Execute(HttpServerContext context) { foreach (var entry in _entries) { var routeValues = entry.Match(context.Request); if (routeValues ! = null) { ... var result = GetActionResult(controller, actionMethod, routeValues); result.Execute(context); return MiddlewareResult.Processed; } } return MiddlewareResult.Continue; } private ActionResult GetActionResult(IController controller, MethodInfo method, RouteValueDictionary routeValues) { ... var result = method.Invoke(controller, paramValues); var actionResult = result as ActionResult; if (actionResult ! = null) return actionResult; else return new ContentResult(Convert.ToString(result), "text/html"); }}Copy the code

The parts that don’t need to be modified are omitted to avoid the code getting too long.

The controller base class adds a helper method that allows you to return the view result:

public abstract class Controller : IController { ... protected ViewResult View(string viewName, object model) { var controllerName = GetType().Name; if (controllerName.EndsWith("Controller")) controllerName = controllerName.Substring(0, controllerName.Length - 10); return new ViewResult(controllerName, viewName, model); }}Copy the code

Now we can return the controller to the view:

public class HomeController : Controller { public ActionResult Index() { ... var model = new { title = "Homepage", counter = counter}; return View("Index", model); }}Copy the code

Finally, create Views/Home/ index. CSHTML to display the data model from the controller:

@model dynamic <! DOCTYPE html> <html> <head> <title>@Model.title</title> </head> <body> <h1>Counter = @Model.counter</h1> </body> </html>Copy the code

To implement a truly complete view engine like ASP.NET MVC, we need to implement a number of things, including ViewBag, ViewData, TempData, and a number of other properties. However, these are already implementation level content, interested students can do it themselves. Our template engine also does not cache the compiled results of the view. Recompiling the view every time it is accessed is definitely a performance problem, so I’ll leave that up to interested students.

So far, we have implemented enough functionality to support a simple Web application. However, for most business systems, you also need to log system users and allow them to perform normal login/logout operations. The Session implemented earlier provides a good starting point for supporting users, but it doesn’t actually record any user information yet. In the next article, we’ll add background features and pages that allow users to log in and out of the system.

series

  • Write your own Web server (index) in C#
  • Write a Web server in C#, part 1 – basics
  • Write a Web server in C#, part 2 – middleware
  • Write a Web server in C#, part 3 – routing
  • Write your own Web server in C#, part 4 – Session
  • Write your own Web server in C#, part 5 – view engine
  • Writing your own Web server in C#, part 6 – user authentication