Layout Support

Aug 4, 2011 at 4:15 PM

Wondering if this has support for layouts. Let's say we have x number of emails that we want to send out both within the mvc web project and standalone jobs (windows service, console, etc). I'd like to define in a separate assembly (class project) all the different templates (.cshtml) and a single layout file as you do in an mvc project. When I generate the templates, I want it to utilize the layout that I specify.

Then, In my windows service or mvc project, I reference the assembly with all the templates and would like to generate or return the html to be sent out as an email in my case but you could have dozens of other uses of what you need the html or the result for.

Is it currently supported? If not, what's a current workaround and any future timelines for full support?

Coordinator
Aug 4, 2011 at 8:07 PM

The generator does support layouts in MVC & WebPages application in the same way that they're supported when not using the generator. However, there is currently no support for this when using  simple templating (i.e. what Haacked blogged).

Note that this is more of a runtime feature than a code generation feature. I think the current generator is just missing one feature, which is the ability to specify the base class of the template. If we add that support, it will then be possible to set it to some base class that supports a Layout mechanism similar to what's available in MVC.

Definitely an interesting direction to look at.

Aug 4, 2011 at 8:17 PM
  1. If I'm reading your response correctly, are you sauing that if one is within the web runtime, then it should work but if one wants to reuse the assembly no matter what the environment is, then it needs future work?
  2. So if I create cshtml templates and mark them @*Generator MvcView instead of @*Generator Template, would that work in a class library project assuming all mvc dll are referenced?

Trying to connect the dots based on your response. May be a few examples would clarify it for me but definitely thank you for the response even though I'm not clear on it yet :)

Coordinator
Aug 5, 2011 at 5:40 AM

Well, not exactly. When using the generator in the full MVC scenario (as in this post), then layout works fine. But in the standalone template scenario (is in this post), it doesn't currently work.

I think we could make it work, but there is some work involved to get there. Can't promise when, but we'll get there!

Aug 5, 2011 at 3:19 PM

Got it. It's a shame but got it. If bribing helps in speeding up the process then I'm in :) Kidding. Will anxiously follow this project. For now, will repeat the layout html in every template.

Thanks.

Aug 5, 2011 at 4:29 PM

A pull request helps speed things up even more. ;)

Coordinator
Aug 6, 2011 at 12:31 AM

Can you give more details about the kind of Layout support that you're after? e.g. do you just need the layout page to have one 'hole' where it calls @RenderBody() and the end page renders? Or fancier scenarios where the layout page renders multiple sections, and the end page defines them using the @section syntax?

Coordinator
Aug 6, 2011 at 12:43 AM

The other interesting question is how pages should be addressed and instantiated. In the MVC way, everything is done by path. But here, it may be simpler to do it with strongly typed objects. e.g. maybe to render a page with a layout page, you'd do something like:

    var myPage = new MyPage() { SomeProp = "Foo" };
    myPage.Layout = new MyLayoutPage();
    string text = myPage.TransformText();

Thoughts?

Aug 6, 2011 at 1:55 AM

Thanks David for following up. I get what you're saying. Regarding the first question, my immediate need is yes, a single entry point via @RenderBody.

The overall goal though is simply that I'd like to define the layout once and also have the option to override it if a specific template needs a specific layout based on some business logic. To be honest, the latter is a cherry on top of the cake.

The real need is to simply have web runtime's ability. Define the layout in some startup/global file and have it be used automatically without any further coding.

The second best option is to simply having ability to define the layout in each view. The alternative would be to generate layout view based on some either abstract class or interface that would give me an ability to use IoC but again, might be over-engineering in this specific case.

The code that you have is fine but in most scenarios, people really want just one layout defined once but optionally have the ability to override it and that's where your code may shine but I would not want to specify it every single time for hundreds of views/templates. Yes, I could abstract it and create some generic method like:

Please.Generate<SOME_TEMPLATE_CLASS, SOME_LAYOUT_CLASS>(model)
but I think your goal is to probably make it seamless whether one is coding in a class library or mvc in terms of razor and hence a global/startup file is your best bet. You already have an awesome product in Razor, it simply needs to be extended onto the non-mvc world. Thank you for listening.

Coordinator
Aug 6, 2011 at 3:34 PM

Thanks for your additional input.

Note that in my code above, you could just as easily take the 'myPage.Layout = new MyLayoutPage()' line and instead set 'Layout = new MyLayoutPage()' from inside the view, much like you do in MVC (except with real objects instead of paths).

As an additional feature, we could allow setting some static Layout factory that creates a layout page for a given page. e.g.

TemplateEngine.LayoutFactory = (page) => new MyLayoutPage();

Here I'm ignoring the passed in page, but a fancier implementation could do different things for different pages.

Well, we can start with something simple and iterate from there :)

Aug 6, 2011 at 8:27 PM

That sounds great. Thanks for following up. I think you will have a slew of new customers with this type of support.

Aug 23, 2011 at 1:38 PM
Edited Aug 23, 2011 at 5:28 PM

Hi, I've been trying to use RazorGenerator in a unit test project.  I can render a view page fine with no problems, but when I try to do the same for a layout page I get a "Stack Empty" exception:

System.InvalidOperationException : Stack empty.
Stack Trace:
  at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
  at System.Collections.Generic.Stack`1.Pop()
  at System.Web.WebPages.WebPageBase.get_PreviousSectionWriters()
  at System.Web.WebPages.WebPageBase.EnsurePageCanBeRequestedDirectly(String methodName)
  at System.Web.WebPages.WebPageBase.RenderBody()
  at {my project}.Views.Shared._Layout.Execute() in {my project}\Views\Shared\_Layout.cshtml:line 12
  at PrecompiledMvcViews.Testing.WebViewPageExtensions.Render[TModel](WebViewPage`1 view, TModel model)

In my layout I'm just doing:           

var layout = new {my project}.Views.Shared._Layout();
Assert.Equal("", layout.Render());

I presume it's something to do with the fact that it isn't associated with a view page for the RenderBody() method to work, is there any way currently I could go about doing that?

Thanks!

P.S. I'm using the latest version of the source (40779b6928a4)

EDIT: removed my project path

Coordinator
Aug 23, 2011 at 4:34 PM

Yes, this is busted. Here is a quick hack which might work. Add this before the view.Execute() call in WebViewPageExtensions.Render:

    var sectionWriterStack = new Dictionary<string, SectionWriter>(StringComparer.OrdinalIgnoreCase);
    sectionWriterStack[""] = () => { };
    view.AsDynamic().SectionWritersStack.Push(sectionWriterStack);
    view.AsDynamic()._body = (Action<TextWriter>)(w => { });

Very hacky with lots of private reflection, but let's first see if it even works for you.

Aug 23, 2011 at 5:25 PM
Edited Aug 23, 2011 at 5:26 PM

Thanks for the quick reply! I now get a different error though:

System.Web.HttpException : The file "" cannot be requested directly because it calls the "RenderBody" method.
Stack Trace:
  at System.Web.WebPages.WebPageBase.EnsurePageCanBeRequestedDirectly(String methodName)
  at System.Web.WebPages.WebPageBase.RenderBody()
  at {my project}.Views.Shared._Layout.Execute() in {my project}\Views\Shared\_Layout.cshtml:line 12
  at PrecompiledMvcViews.Testing.WebViewPageExtensions.Render[TModel](WebViewPage`1 view, TModel model)
Coordinator
Aug 23, 2011 at 5:35 PM

Think you may need to push two items into the SectionWritersStack since WebPageBase look for at least two things in there

var top = SectionWritersStack.Pop();
var previous = SectionWritersStack.Count > 0 ? SectionWritersStack.Peek() : null;
SectionWritersStack.Push(top);
return previous;
Coordinator
Aug 23, 2011 at 5:39 PM

We might want to approach unit testing Layout pages differently because WebPages goes out of its way to try and prevent you from requesting a Layout or Start page. Maybe testing the outputs of an empty cshtml file with only the Layout attribute set?

Aug 23, 2011 at 5:59 PM

Awesome, that's sorted it! Thanks so much.

I've changed the _body line to            

view.AsDynamic()._body = (Action)(w => { w.WriteLine("{Body}"); });

This allows me to replace {Body} with the rendered view content.  I'm sure there's a more elegant way but this works for me for now.

Thanks a lot!

Coordinator
Aug 23, 2011 at 6:19 PM

Yes, sorry, my code was not quite right. I pushed a new build of PrecompiledMvcViews.Testing (1.0.3) which fixes that. But the logic to make it work is nasty, because the WebPages framework is just not mockable enough :(

Anyway, it'd be great if you could try the latest official and make sure it works.

Coordinator
Aug 23, 2011 at 6:22 PM

About your change to the _body line: what are you trying to achieve here? If you just want to unit test the layout page, then I would think you wouldn't want to render anything for the body. Are you trying to compose the full page with both the layout and the view?

Aug 23, 2011 at 11:23 PM

Yes, I'm just playing around with things at the moment but I have a vague objective of being able to dynamically render the complete HTML for a given view, but passing in a fake view model.  I tried mocking out the ControllerContext but that doesn't seem to work outside of ASP.NET due to some internal dependencies, so I thought I could maybe accomplish it using RazorGenerator.

Sep 1, 2011 at 12:07 AM

Just in case you're interested, I've been working on a way to unit test views using headless browser automation (using HTMLUnit), but in a standalone way, so I can dynamically generate the view HTML from the view name and a view model, and run unit tests against the HTML and Javascript from within Visual Studio.  I've managed to get something working, although it's still a bit hacky, some work still to do on it.  I had to modify some code in the PrecompiledMvcViews.Testing.WebViewPageExtensions class to get it to do what I wanted.  I've blogged about it here on the offchance it's useful to somebody else.

Jan 20, 2012 at 4:04 AM

Trying to resurrect the post and see if layouts outside of the httpcontext are still impossible to do?

Coordinator
Jan 20, 2012 at 4:31 PM

Haven't really looked at this as yet. Converting an extant Mvc app to work without HttpContext might not be the easiest thing to do particularly because Layout pages are handled via WebPages which depends on Http globals rather the abstraction types that are passed to Mvc. I guess I could investigate this as part of the next release.