Marcin On ASP.NET

Keeping my eye on the dot

December, 2010

Posts
  • Marcin On ASP.NET

    Razor, Nested Layouts and Redefined Sections

    • 10 Comments

    In a recent post I introduced a technique for dealing with optional Razor sections and default content. In this post I will expand upon that technique and describe a way of working with sections across nested layout hierarchies. If you are not familiar with sections, layout pages, or my technique then go ahead and read that post to catch up.

    One aspect of the relationship between layout pages and sections in Razor that a fair number of people might find surprising is that a section defined in a content page can only be used in its immediate layout. There is implicit scoping going on that prevents certain use cases. Take the following example:

    <!DOCTYPE html>
    <html>
    <body>
    @RenderSection("SubSection")
    @RenderBody()
    </body>
    </html>
    @{
        Layout = "~/Views/_MasterLayout.cshtml";
    }
    <div>
    @section SubSection {
    <h1>Title</h1>
    }
    @RenderBody()
    @RenderSection("ContentSection")
    </div>
    @{
        Layout = "~/Views/_SubLayout.cshtml";
    }
    <div>
    <p>Main content</p>
    @section ContentSection {
    <div>Footer</div>
    }
    </div>

    In the above example you can certainly call RenderSection("SubSection") in _MasterLayout.cshtml, as well as call RenderSection("ContentSection") in _SubLayout.cshtml. However, it is impossible to call RenderSection("ContentSection") from _MasterLayout.cshtml because the file rendering the section and the file defining the section are not directly related. Essentially sections are limited to the Content-Layout scope and are not accessible to other layout pages outside of that scope.

    Redefining sections

    You can work around this by essentially redefining the section in the intermediate layout. 

    @{
        Layout = "~/Views/_MasterLayout.cshtml";
    }
    <div>
    @section SubSection {
    <h1>Title</h1>
    }
    @RenderBody()
    @section ContentSection {
      @RenderSection("ContentSection", required: false)
    }
    </div>

    Now you are able to reference “ContentSection” from _MasterLayout.cshtml. However you should be aware that you are not overriding “ContentSection" in the same sense as overriding methods in a child class. You are actually defining a new section named “ContentSection” in the SubLayout-MasterLayout scope that renders the section named “ContentSection” from the Content-SubLayout scope. The fact that the names match is incidental. The names certainly do not have to match.

    Things get even more complicated when you want to use the IsSectionDefined method to conditionally provide default content for optional sections. Because it’s necessary to propagate “ContentSection” by redefining the section in _SubLayout.cshtml you can no longer depend on IsSectionDefined returning the expected result.

    Conditionally redefining sections via RedefineSection

    Fortunately not everything is lost. What you want to do is to conditionally redefine a section. Building on the RenderSection helper method from my previous post I created the RedefineSection more helper methods to aid in this scenario:

    public static class SectionExtensions {
        private static readonly object _o = new object();
        public static HelperResult RenderSection(this WebPageBase page,
                                string sectionName,
                                Func<object, HelperResult> defaultContent) {
            if (page.IsSectionDefined(sectionName)) {
                return page.RenderSection(sectionName);
            }
            else {
                return defaultContent(_o);
            }
        }
    
        public static HelperResult RedefineSection(this WebPageBase page,
                                string sectionName) {
            return RedefineSection(page, sectionName, defaultContent: null);
        }
    
        public static HelperResult RedefineSection(this WebPageBase page,
                                string sectionName,
                                Func<object, HelperResult> defaultContent) {
            if (page.IsSectionDefined(sectionName)) {
                page.DefineSection(sectionName,
                                   () => page.Write(page.RenderSection(sectionName)));
            }
            else if (defaultContent != null) {
                page.DefineSection(sectionName,
                                   () => page.Write(defaultContent(_o)));
            }
            return new HelperResult(_ => { });
        }
    }

    The RedefineSection method conditionally redefines a section (that is it redefines in only if a section was already defined in a content page). The second overload also allows you to provide a default content template that will be used if the content page did not define the section. Using this code you can write the following pages:

    <!DOCTYPE html>
    <html>
    <body>
    @RenderSection("TitleSection", required: false)
    @RenderBody()
    </body>
    </html>
    @{
        Layout = "~/Views/_MasterLayout.cshtml";
    }
    <div>
    @this.RedefineSection("TitleSection",
                          @<h1>Default SubLayout title</h1>)
    @RenderBody()
    </div>
    @{
        Layout = "~/Views/_SubLayout.cshtml";
    }
    @section TitleContent {
    <h1>Title</h1>
    }
    <p>Main content</p>

    In the above example Content.cshtml defines the “TitleContent” section. _SubLayout.cshtml redefines that section but also provides some default markup in case the content page does not have the “TitleContent” section defined. Finally, _MasterLayout.cshtml consumes the section indicating that it is optional – this means that the entire site will still work even if the content page does not define the section and the intermediate layout does not provide a default value.

    Hope you find the above technique useful in your complex layout pages. Please let me know if you encounter any issues and whether these methods are valuable enough that they should be added to the framework for the next version.

  • Marcin On ASP.NET

    Optional Razor Sections with Default Content

    • 8 Comments

    Solution quick links:

    1. IsSectionDefined method
    2. Razor inline templates

    RenderSection

    The new ASP.NET page framework built on Razor (which is available in MVC 3) provides a facility for content pages to contribute named fragments of markup to their layout pages which the layout page can then render in an arbitrary location using the RenderSection method.

    For example, the following content page declares an "ExtraContent" section:

    @{
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    
    @section ExtraContent {
        <div>Some extra content</div>
    }
    
    <div>The main content</div>

    And the following layout page renders it:

    <!DOCTYPE html>
    <html>
    <head></head>
    <body>
    @RenderBody()
    
    @RenderSection("ExtraContent")
    @RenderSection("OptionalContent", required: false)
    </body>
    </html>

    You can even declare that a section is not required like the “OptionalContent” section in the example above.

    But what if you want to have some default content for your optional sections?

    Option 1: Use the IsSectionDefined method

    The IsSectionDefined method returns true if a child content page defined a section. You can use that to decide whether to render a section or some other content:

    <!DOCTYPE html>
    <html>
    <body>
    @RenderBody()
    
    @if (IsSectionDefined("OptionalContent")) { 
        @RenderSection("OptionalContent")
    }
    else { 
        <div>Default content</div>
    }
    </body>
    </html>

    Just remember that you need to use the @RenderSection() syntax (you need the @ character) so that the contents of the section is actually printed to the output. Without that character you will get an exception with the following message:

    The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/_Layout.cshtml": "OptionalContent"

    Option 2: Use Razor inline templates

    Razor provides a way to pass inline markup templates into function calls. This is a powerful mechanism that allows you to combine custom markup fragments with general purpose display logic. In order to make the following example work you will need to drop the following code into your project:

    using System.Web.WebPages;
    using System;
    
    public static class SectionExtensions {
        private static readonly object _o = new object();
        public static HelperResult RenderSection(this WebPageBase page,
                            string sectionName,
                            Func<object, HelperResult> defaultContent) {
            if (page.IsSectionDefined(sectionName)) {
                return page.RenderSection(sectionName);
            }
            else {
                return defaultContent(_o);
            }
        }
    }

    This code essentially wraps the previous example in a reusable extension method. All you need now is to provide the default content markup. With it you can write views like the following:

    <!DOCTYPE html>
    <html>
    <body>
    @RenderBody()
    
    @this.RenderSection("OptionalContent",
                        @<div>Default content</div>)
    </body>
    </html>

    For the second parameter you need to use the @ character to trigger the Razor parser to go into markup mode. Once in markup mode you can do anything that you do in the main body of the page.

    Just remember that since this is an extension method you need to call it off the this object. Otherwise the C# compiler will not be able to locate it and you will get a compilation error.

    If you like this technique read the follow up: Razor, Nested Layouts and Redefined Sections

Page 1 of 1 (2 items)