11

So I have a case where the layout has evolved to become more complicated. There's the usual things like @section styleIncludes{ ... }, then other sections that define all kinds of the things that each page can optionally(but almost always) specify like the structure of the current pages breadcrumb. The reason all these things are sections is because they are embedded in the structure of the layout.

I find myself making copies of previous pages, because there's 8 or so different sections, rather than try to remember the exact spelling of them, or piece meal copy/paste.

I am thinking it'd be better to create a fluent API for these so that I have some object that has 8 functions, each one returning the object itself, so you can do something like Sections.Style(some MVC text template or razor delgate?).Breadcrumb(etc.)

The main purpose is to be able to code these sections in a guided way and strongly type the names instead of relying on perfect typing or copy/paste.

However, extensions/helpers in razor return MvcHtmlString, and I imagine a @section is represented by something completely different.

Not asking you to write a complete solution for me, but just some ideas on how to precede.

What object should a helper return to represent a @section declaration? I.e. the analogy of a MvcHtmlString.

What would you suggest the parameter type for the fluent methods, like Style or Breadcrumb? I would like the razor passed to be similar in capability to writing razor in the curly braces of the section declaration. For example, ability to access local variables declared on the razor page, just as you can do with a regular section declaration. I don't want something like string concatenation like .SomeSection("<div...>Bunch of html stuffed in a string</div>")

In other words, if many of my cshtml pages begin something like

@{
  string title = "Edit Person"
  ViewBag.Title = title;
}
@section styles{
  .someOneOffPageSpecificStyle { width:59px }
}
@section javascript{
  //javascript includes which the layout will place at the bottom...
}
@section breadcrumb{
  <a ...>Parent Page</a> &gt; <a ...>Sub Page</a> &gt; @title
}

I'd rather have seom sort of fluent API like this, not really for the resulting style of code, but rather because it will be easier to write the code and not have problems with typos etc. since intellisense will assist:

@{
  string title = "Edit Person"
  ViewBag.Title = title;
}
@Sections
.Styles(@<text>
  .someOneOffPageSpecificStyle { width:59px }
</text>)
.Javascript(@<text>
  //javascript includes which the layout will place at the bottom...
</text>)
.Breadcrumb(@<text>
  <a ...>Parent Page</a> &gt; <a ...>Sub Page</a> &gt; @title
</text>)
AaronLS
  • 37,329
  • 20
  • 143
  • 202
  • Why do you have so many sections that aren't assembled in a common layout? (And I don't think you can, you'll just have to declare the sections and then call your extension methods inside each one) – Kirk Woll May 10 '13 at 20:12
  • They are assembled in a common layout. You can have many sections defined in different parts of a layout. The breadcrumb appears in one place, the styles in another, etc. but they are all indeed in the same layout. – AaronLS May 10 '13 at 20:24
  • Then why would you want a helper method to generate all these sections for you since you would only be doing it once in the common layout? – Kirk Woll May 10 '13 at 20:29
  • You will be doing it more than once, you will do it for each cshtml that utilizes that layout. The sections in the layout have a counterpart declaration in each cshtml page. So even though you are using a common layout, each cshtml will define their own sections, but the section names must match up with the names in the layout. It is a way for the layout to expose customizable sections of the page. Most MVC projects come default with some sort of style and javascript section, since each *.cshtml can't include a head tag and the javascript should be at the end, the layout provides a section – AaronLS May 10 '13 at 20:40

3 Answers3

2

Most likely this isn't possible (using sections).

First we have to understand how MVC works under the hood. We write cshtml files, but these files will eventually be compiled into a .Net class that is instantiated and then methods are executed (at a bare minimum Execute()) which (most of the time) write to the response buffer for IIS to return (very similar if not exactly the same as how RenderAction works - Invokes a child action method and renders the result inline in the parent view). HtmlHelpers (or any custom helpers) are simply called from the instaniated class (as delegates), so they can only execute once the code in cshtml files are compiled.

The System.Web.Razor.Generator.SectionCodeGenerator requires a string definition to create Sections. So when you define a Section the string must exist in the cshtml file before the file is compiled, since HtmlHelper or/and Custom helpers won't be executed until the file is compiled there isn't a way to write a class or object that can update the cshtml file before it's compiled.

What you can do is write your own HtmlHelper or other Custom Helpers to do something similar to what Sections provide (without actually using any sections). For example I wrote this because I needed to write Javascript from partial views and/or templates (which you can't do with sections). If you are required to use sections, then this may not help.

The follow code is example only it is not how razor actually works (at all, after looking at some code). But to make this example make sense, I'll use razor-ish like code and naming conventions.

layout.cshtml

<html>
<body>
@RenderSection("MySectionName")

@RenderBody();
</body>
</html>

Index.cshtml

@{
  _layout = "layout";
}

@section MySection {
  <div>MySection</div>
}

<div>My Body</div>

Maybe be compiled into a class that resembles:

public class app_aspnet_layout : System.Web.Mvc.WebViewPage
{

  public Execute()
  {
    throw new NotImplementedException();
  }

  public void ExecutePageHierarchy(WebPageContext pageContext, 
                                   TextWriter writer)
  {
    writer.Write("<html>")
    writer.Write("<body>")

    var section = pageContext.SectionWriters["MySectionName"];
    section();

    pageContext.View.ExecutePageHierarchy(null, writer)

    writer.Write("</body>")
    writer.Write("</html>")
  }
}

public class app_aspnet_index : System.Web.Mvc.WebViewPage
{ 
  // generated from the _layout Definition
  private WebViewPage startPage = new app_aspnet_layout();

  public Execute()
  {
     WebPageContext pageContext = new WebPageContext();
     pageContext.View = this;

     pageContext.SectionWriters.Add("MySectionName", 
                                    this.Section_MySectionName);

     var writer = HttpContext.Current.Response.Stream.AsTextWriter();

     if (startPage != null)
     {
       startPage.ExecutePageHierarchy(pageContext, writer);
     }
     else
     {
       this.ExecutePageHierarchy(pageContext, writer);
     }
  }

  // html generated from non-section html
  public void ExecutePageHierarchy(WebPageContext pageContext, 
                                   TextWriter writer)
  {
    writer.Write("<div>My Body</div>");
  }

  public void Section_MySectionName(TextWriter writer)
  {
    writer.Write("<div>MySection</div>");
  }
}
Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • I'll take a look at that. Sounds like the generally that will work. Also would solve the other unrelated problem you speak of in being unable to define sections in partials. – AaronLS Jul 25 '13 at 01:09
  • **but these files will eventually be compiled into a full .Net class** Can you tell the meaning of full .net class ? – Imad Alazani Jul 26 '13 at 06:15
  • `Public Class MyClass { }` – Erik Philips Jul 26 '13 at 07:42
  • "when you define a Section the string must exist in the cshtml file before the file is compiled", I'm confused on this. How are sections that contain calls to helpers and reference variables handled? Such as in my example the `@title` variable is referenced. If helpers aren't called yet, and the section code generator requires a string, how does it resolve these to a string? In source of SectionCodeGenerator, it seems it is deffered, as it seems to treat the body section as a delegate: `WriteStartLambdaDelegate`. Either way, you are right, execution seems deferred, so helpers won't work. – AaronLS Jul 31 '13 at 19:38
  • Just to make sure I'm clear, SectionCodeGenerator is called during compilation? Or before when the file is parsed? And when you say "HtmlHelper or/and Custom helpers won't be executed until the file is compiled" I assume you meant "...**after** the file is compiled" ? (which is sort of implied by the past tense but at first it sounded like you were saying they are executed during compilation) – AaronLS Jul 31 '13 at 19:41
  • Re:*Your First Comment* - Don't mistake the defining of a section `@Section ` (say in a layout) with the code the provided for a section to use (in the view). The is a string, compiled to be used to location a *section code* in a view (compiled as well). This string has to exist before the layout gets compiled. Helpers are called at *run-time * to execute code to return values so a Helper cannot define a section name based on when the code runs. – Erik Philips Jul 31 '13 at 19:46
  • Re: *SectionCodeGenerator is called during compilation? Or before when the file is parsed?* I would assume it is parsed then compiled, but it doesn't matter because it *has* to be successfully compiled into a class. The `SectionCodeGenerator` is internal to MVC, so unless you plan to do some heavy lifting to change how it works, there is no difference between parsed/compiled. – Erik Philips Jul 31 '13 at 19:51
  • re:*HtmlHelper or/and Custom helpers won't be executed until the file is compiled" I assume you meant "...**after** the file is compiled"* - I should have said HtmlHelper or/and Custom helpers cannot be executed until the file is compiled. HtmlHelper and Customer Helpers will not be executed (only referenced) when the view/layout is compiled. – Erik Philips Jul 31 '13 at 19:52
1

If you have resharper then I'd suggest a live code template (a powerful snippet).

You can create a template with a single parameter. The macro source of this parameter can be a comma delimited list of values. When you use the template/snippet it will present you with an intellisense box with your section names available for selection.

Assuming the names of the sections you use won't change that much then you shouldn't have to edit the template very often to include new section names.

christophano
  • 915
  • 2
  • 20
  • 29
0

You could tackle the problem from a different direction - using t4 templates.

If the structure of your layout is now reasonably stable (ie. your not adding and removing sections frequently) and all you want is to stop having to copy and paste stuff when creating new views - this would probably work quite well. It will create the views for you to your specifications - you can do some quite clever things in them with reflection and other logic

There is a nuget to load the code templates - search for "codetemplates" - and they are pretty straightfoward to use.

StanK
  • 4,750
  • 2
  • 22
  • 46
  • This would work, but to make it strongly typed, the types would have to be compiled into a class library for the T4 templates to use. That would be complex. – Erik Philips Jul 30 '13 at 04:18
  • I'm not sure that I follow you there - what part of the t4 template would you need to make "strongly typed"? – StanK Jul 31 '13 at 03:44
  • 1
    OP Question stated `The main purpose is to be able to code these sections in a guided way and strongly type the names instead of relying on perfect typing or copy/paste.` In order for the T4 template to use a strongly typed type, it needs to be defined in a compiled class library so that it can also be used outside the T4 template in the views (otherwise the views defining sections won't be strongly typed). – Erik Philips Jul 31 '13 at 07:41