In this recipe, we will add a couple of Razor views to the application built in the previous recipes. We will see how our module code controls which view to use and how to set up tests for the views.
You need the code built until the Content negotiation and more model binding (Advanced) recipe. If you haven't coded along, you can start from the code for the Content negotiation and more model binding (Advanced) recipe in the code download.
I will assume you have a working knowledge of Razor, so if you are new to Razor, you should probably brush up on Razor before diving into this recipe.
The following steps will help you add views to your application:
As always, we start by adding a test. This test is added to
TodosModuleTests
and does two new things: first, it sets theAccept
header on theGET
request to"text/html"
, which colloquially means "give me back some HTML", and second, it asserts on the contents of the body of the response. Theactual
variable has theBrowserResponse
type. TheBody
property onBrowserResponse
objects represents the body of the response from the handler code in the Nancy module. When this body contains HTML, theBrowserResponse
type supports looking up parts of this HTML using CSS selectors:[Fact] public void Should_be_able_to_get_view_with_posted_todo() { var actual = sut.Post("/todos/", with => { with.JsonBody(aTodo); with.Accept("application/json"); }) .Then .Get("/todos/", with => with.Accept("text/html")); actual.Body["title"].ShouldContain("Todos"); actual.Body["tr#1 td:first-child"] .ShouldExistOnce() .And .ShouldContain(aTodo.title); }
The test posts a
Todo
object to our application and then gets it back in the form of HTML. The tests assert that the body of the response to theGET
request should contain atitle
tag that contains the"Todos"
text and that the firsttd
tag inside the firsttr
tag found in the body should contain the title of thetodo
posted right before.At this point this test fails. Run and see for yourself. Take a look at the error message you get from Nancy. It's a bit long, but it should contain a part similar to the following code:
Unable to locate view 'Todo[]' Currently available view engine extensions: sshtml,html,htm Locations inspected: views/todos/Todos/Todo[]-da- DK,views/todos/Todos/Todo[],todos/Todos/Todo[]-da- DK,todos/Todos/Todo[],views/todos/Todo[]-da- DK,views/todos/Todo[],todos/Todo[]-da- DK,todos/Todo[],views/Todos/Todo[]-da- DK,views/Todos/Todo[],Todos/Todo[]-da- DK,Todos/Todo[],views/Todo[]-da-DK,views/Todo[],Todo[]-da- DK,Todo[]Root path: C:\projects\nancy-quick- start\src\recipe-6\TodoNancy\TodoNancyTests\bin\Debug
This actually is very useful information. First, it tells us that Nancy is trying to locate a view called
Todo[]
. Second, it tells us which file extensions Nancy expects views to have:sshtml
,html
, orhtm
. Third, it tells us all the places Nancy looked for a view in a file calledTodo[]
with one of the listed extensions.The first problem to fix is that we want the view to be called
Todos
and notTodo[]
. In order to do this, we change theGet
handler in theTodosModule
class to this:Get["/"] = _ => Negotiate .WithModel(todoStore.GetAll()) .WithView("Todos");
The previous code tells Nancy to use content negotiation to control the format of the response and to use
todoStore.GetAll()
as the model; that is, if the request indicates that it accepts JSON, the result oftodoStore.GetAll()
will be serialized to JSON in the body of the response. If the incoming request accepts HTML, Nancy will now look for the"Todos"
view in all the places indicated in the previous error message, and pass the result oftodoStore.GetAll()
into the view as the model object.As stated in the beginning of the recipe, we want our views to be Razor views, so at this point we install the
Nancy.ViewEngines.Razor
NuGet package in theTodoNancy
project. This will add references to theRazor
assembly and the Nancy adaptor for Razor, but in order for Nancy to start using the Razor View Engine, we need to make sure the assembly is loaded. This is done most conveniently on the bootstrapper where we add the following line of code:private RazorViewEngine ensureRazorIsLoaded;
Now we are ready to add the new
Razor
view. To do so, create aViews
folder in theTodoNancy
project, and add an HTML file under theViews
folder called asTodos.cshtml
. Then, go to the properties of this new file (either right-click on it in Solution Explorer or press Alt + Enter while it is selected in Solution Explorer) and set the Copy to Output Directory property to Copy always. Now replace any content in the newTodos.cshtml
file with the following code snippet:@inherits Nancy.ViewEngines.Razor.NancyRazorViewBase<TodoNancy.Todo[]> <html> <head><title>Todos</title></head> <body> <h1>All todos</h1> <table> <th>Title</th><th>Order</th><th>Completed</th> @foreach (var todo in @Model) { <tr id="
@
todo.id"><td>@todo.title</td><td>@todo.order</td><td>@todo.completed</td></tr> } </table > <h1>Add todo</h1> <form action="/todos/" method="post"> <input type="text" name="title" value="title" /> <input type="number" name="order" value="0" /> <input type="checkbox" name="completed" /> <input type="submit" value="add" /> </form> </body> </html>This is a strongly typed Razor view that works on an array of
Todo
objects. It contains a table with a row for eachTodo
object. This is the table we assert on in our test, which should now pass.The
Todos
view also contains aform
tag that is set up topost
in/todos/
. In the code download for this recipe, you will find a test for this form, but I will leave it out here because it is similar to the tests we added previously.Try to run the application and go to
/todos
in your browser. You should see theTodos
view showing whatever views you have in your implementation oftodoStore
. You should also see the form, but if you fill in the form and click on add, you get an Internal Server Error. If you look at the details of the error, you will see that Nancy cannot locate an appropriate view to use. To fix this, add a test that does aPOST
and acceptstext/html
and then change the handler forPost
inTodosModule
as shown in the following code snippet:Post["/"] = _ => { var newTodo = this.Bind<Todo>(); if (newTodo.id == 0) newTodo.id = todoStore.Count + 1; if (!todoStore.TryAdd(newTodo)) return HttpStatusCode.NotAcceptable; return Negotiate.WithModel(newTodo) .WithStatusCode(HttpStatusCode.Created) .WithView("Created"); };
For this to work, we need to add another view called
Created.cshtml
. I will leave it up to you to implement theCreated
view (or you can look in the code download).
Nancy doesn't support Razor views alone; out of the box, Nancy comes with Super Simple View Engine, which—as the name indicates—is a simple (yet fairly powerful) view engine with a syntax akin to Razor. The Super Simple View Engine expects files to have the .sshtml
extension, which is why this particular extension is listed when Nancy can't find a view.
Furthermore, there are NuGets for various other view engines such as Spark, Nustache, and others.
As is always the case with Nancy, you can also roll on your own if you cannot find support for your favorite view engine. In order to do this, you basically implement the IViewEngine
interface against your favorite view engine and let Nancy automatically wire your new view engine up with the rest of the framework.