WebMatrix - Consistent Look And Feel With Razor

Most web development frameworks provide features that assist with creating a consistent look and feel across pages, or make provision for managing reusable partial content, such as headers and footers. The Web Pages framework is no exception. This article examines the core features offered by its Razor view engine for handling templated layouts and common blocks of content.

I'm going to enhance the web application I started in my previous WebMatrix article: WebMatrix - A First Application. At the moment, the site is just a single page, but it will grow, so now is the time to start thinking about establishing a structure for the page designs and content. In ASP.NET Web Forms and MVC, Master Pages is the mechanism by which this is managed. Web Pages introduces layout pages, which borrow heavily from some of the open source MVC view engines on offer, such a NVelocity and Spark. So we'll add one to the existing site that was begun in the last article, and can be downloaded here.

Once you have downloaded the folder, unzipped the site and saved it somewhere convenient, open WebMatrix and use the Site From Folder option to navigate to the saved folder and open the site.

Making sure you have the Files menu item selected, right click on the Books icon in the left pane, and choose the New Folder option. Name the folder "Shared". Now, either right click on the Shared folder and choose New File, or click New in the top Files menu and choose File. Select the option to add a cshtml file, and call it _layout.cshtml. Prefixing the name of the file with an underscore ensures that if it is browsed to, IIS will not serve the request. Copy the reference to the style sheet from Default.cshtml into the appropriate position within the head tag. Now switch back to Default.cshtml and remove the following code:

along with the closing body and html tags at the bottom. Default.cshtml needs to be associated with the layout page. This is achieved by adding the highlighted line of code to the top of the page:

@{
    Layout = "/Shared/_layout.cshtml";
}

At the moment, if you launch Default.cshtml, you will receive an error because no content has been provided to it. First, amend the code block in Default.cshtml to inlcude the second line:

@{
    Layout = "/Shared/_layout.cshtml";
    Page.Title = "My Book Site";
}

Then amend layout.cshtml to look like the following:

<!DOCTYPE html>
<html>
    <head>
        <title>@Page.Title</title>
        <link href="@Href("/Styles/StyleSheet.css")" rel="stylesheet" type="text/css" />
    </head>
    <body> 
        @RenderBody()

    </body>
</html>

Page is a container for variables that can be shared between content blocks or pages (which Default.cshtml has now become) and layout pages. It is capable of having dynamic properties applied to it so that arbitrary values can become strongly typed properties of the Page object. A call to the RenderBody() method in the layout page specifies where the body of the content page should appear within the layout page. It's the equivalent of a ContentPlaceHolder control in traditional Master Pages, with Default.cshtml acting in the same way as an ASP.NET Content control. When the page is launched now, you won't notice much difference except that the page now has a title in the browser title bar.

Time for a header, methinks. This will be treated as piece of partial content. The RenderPage() method is repsonsible for locating the file name that's passed in as a parameter, and rendering it within the layout page. Add a new cshtml file to the shared folder and call it _header.cshtml. Then remove all the default markup and replace it with the following:

<div>
    <h1>@Page.Header</h1>
</div>    

Now turning back to _layout.cshtml, amend it like so, adding the call to RenderPage():

<!DOCTYPE html>
<html>
    <head>
        <title>@Page.Title</title>
        <link href="@Href("/Styles/StyleSheet.css")" rel="stylesheet" type="text/css" />
    </head>
    <body> 
        @RenderPage("/Shared/_header.cshtml")
    
        @RenderBody()

    </body>
</html>

Finally, a value needs to be supplied by the content page (Default.cshtml) to the _header.cshtml file for Page.Header so add the highlighted line to Default.cshtml:

@{
    Page.Header = "View Books";
    Layout = "/Shared/_layout.cshtml";
    Page.Title = "My Book Site";
}

Now when you launch Default.cshtml, you can see that the layout page has been applied, in turn picking up the header page, and the PageData dictionary works to provide the text for the header control's H1 tag:

Great! But there is a small problem. Eventually, this site will grow, and acquire more and more content pages. At the moment, we are defining the source file for the site's layout in each individual content page, which is tiresome and error prone. What we really need is one place to set this value, and Web Pages provides that in the form of _PageStart pages. These files contain code that runs for all the pages in a particular folder. Since we want all the pages in the root folder to share the same layout page, we will add a new cshtml file to the root called _PageStart.cshtml, and remove all the content. Then we need to replace that with this:

@{
    Layout = "/Shared/_layout.cshtml";
}

Finally we need to remove the Layout page declaration in Default.cshtml. When the page runs, the layout page is still applied, and will be for any other content pages added to the root folder of the site. If additional content pages are added to new folders as they are created, each folder will need its own _PageStart.cshtml file so that it can locate its layout page if needed.

Another way to manage partial content is to use the RenderSection() method. This works through specifying sections within the content page itself, and using the method call in the layout page to render them. For example, we can specify a footer section within Default.cshtml like this:

@section footer{
    <div id="footer">&copy @DateTime.Now.Year All rights reserved</div>
}

Then in the layout page, we use the RenderSection() method call:

<!DOCTYPE html>
<html>
    <head>
        <title>@Page.Title</title>
        <link href="@Href("/Styles/StyleSheet.css")" rel="stylesheet" type="text/css" />
    </head>
    <body> 
        @RenderPage("/Shared/_header.cshtml")
    
        @RenderBody()
        
        @RenderSection("footer")
        
    </body>
</html>

Notice that you simply pass in the name of the section declared in the content page. However, sometimes you may only want this section to appear on certain content pages, but all of the pages will make use of the same layout file. You can manage this easily by making the section optional by passing in a second parameter to the call:

<!DOCTYPE html>
<html>
    <head>
        <title>@Page.Title</title>
        <link href="@Href("/Styles/StyleSheet.css")" rel="stylesheet" type="text/css" />
    </head>
    <body> 
        @RenderPage("/Shared/_header.cshtml")
    
        @RenderBody()
        
        @RenderSection("footer", required: false)
        
    </body>
</html>

An alternative method to achieve this is to use the IsSectionDefined() method:

@if(IsSectionDefined("footer")){
    @RenderSection("footer")
  }

This article looked at a number of Razor view engine features for managing content. We covered layout pages, which, along with a call to RenderBody() are used for defining page templates. We also looked at a couple of methods for managing partial content - RenderPage() and RenderSection(). At this stage, I am not providing an updated download. One of the great things about the concepts presented here is that they are so simple, you can apply them to the existing download available at the previous article yourself.