One of the lesser known, but very powerful template options of the DotNetNuke DDR Menu, originally created by Mark Allan, is Razor. A while back, Joe Brinkman had a very interesting blog post, combining razor templating with taxonomy to come to very advanced menu layouts.
Sadly, DotNetNuke 7.0 introduced a breaking change in the way Razor was supported that specifically impacted the DDR Menu implementation of it. Although the issue had been on our radar for some time, it was not a very easy one to fix… Well.. the wait is over, in DotNetNuke 7.0.4, this is finally fixed.
Also, this fix has been released to the CodePlex DDRMenu project, as version 2.0.3
Symptoms and causes
How would you know you are using a skin that depends on a Razor menu template? Typically, you would get any of these 3 errors:
- The "li@GetClasses(node)" element was not closed. All elements must be either self-closing or have a matching end tag.
- Could not load file or assembly 'System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' or one of its dependencies.
- error CS0103: The name 'Model' does not exist in the current context
Two different issues are were involved with these error. First, DotNetNuke 7 changed from referencing System.Web.Pages 1.0 to version 2.0, where small changes in the Razor parser caused this to no longer work:
<ul@topLevelId class="level-@level">
Correct new markup would be:
<ul @topLevelId class="level-@level">
(space between ul and @topLevelId).
Whilst this is an easy one to fix, the second issue was not. As it turned out, DDRMenu had a hard dependency on System.Web.Pages 1.0, and did not use the built in DotNetNuke wrapper methods for Razor.
This is the way the old code instantiated a WebPage:
This code was called by the Render method of the RazorTemplateProcessor:
In this case, the razor object is instantiated using reflection:
The problem with this code is that it uses MVC directly and is not using the DotNetNuke Razor engine, so when we upgraded to .NET 4, WebAPI and removed MVC from the installation, this code broke. The easiest way to fix this is to use the core Razor engine, which would make this more robust for potential future changes as well.
New Code
The old Razor class is now obsolete and everything is now done inside the RazorTemplateProcessor
The new Render method now looks like this:
This directly calls the DotNetNuke Razor Engine using the Render method that supports Generics (razorEngine.Render<dynamic>(writer, model);). This allows us to directly pass in the Model which will subsequently be passed into the WebPage.
This is the DotNetNuke.Web.Razor.RazorEngine.Render<T> method:
As you can see, the Model is passed in and also to the WebPage.
Impact on templates
Sadly all of this means that the DDRMenu Razor templates need to be updated as well. In order to make the Model available inside the Razor script properly, this needs to be added at the top of the script:
@using System.Dynamic;
@inherits DotNetNuke.Web.Razor.DotNetNukeWebPage<dynamic>
We are simply telling the script that it inherits from the generic DotNetNukeWebpage.
The other breaking change is not caused by us, but by the switch from Webpages 1.0 to Webpages 2.0. Apparently, under webpages 1.0 this was valid markup:
<ul@topLevelId class="level-@level">
In webpages 2.0 that is not valid anymore, and should be this:
<ul @topLevelId class="level-@level">
Sample Template
This is a sample to show the impact. This template is taken from the DDRMenu Template examples project The old template (pre-DNN 7) lines are highlighted to show problems:
@using DotNetNuke.Web.DDRMenu;
@{ var root = Model.Source.root; }
@helper RenderNodes(IList<MenuNode> nodes) {
if (nodes.Count > 0) {
< ul>
@foreach (var node in nodes) {
var cssClasses = new List<string>();
if (node.First) { cssClasses.Add("first"); }
if (node.Last) { cssClasses.Add("last"); }
if (node.Selected) { cssClasses.Add("selected"); }
var classString = new HtmlString((cssClasses.Count == 0) ? "" : (" class=\"" + String.Join(" ", cssClasses.ToArray()) + "\""));
<li@classString>
@if (node.Enabled) {
<a href="@node.Url">@node.Text</a>
} else {
@node.Text
}
@RenderNodes(node.Children)
</li>
}
</ul>
}
}
@RenderNodes(root.Children)
Updated template for DotNetNuke 7 (highlighted lines are the lines with additions / changes):
@using DotNetNuke.Web.DDRMenu;
@using System.Dynamic;
@inherits DotNetNuke.Web.Razor.DotNetNukeWebPage<dynamic>
@{ var root = Model.Source.root; }
@helper RenderNodes(IList<MenuNode> nodes) {
if (nodes.Count > 0) {
< ul>
@foreach (var node in nodes) {
var cssClasses = new List<string>();
if (node.First) { cssClasses.Add("first"); }
if (node.Last) { cssClasses.Add("last"); }
if (node.Selected) { cssClasses.Add("selected"); }
var classString = new HtmlString((cssClasses.Count == 0) ? "" : (" class=\"" + String.Join(" ", cssClasses.ToArray()) + "\""));
<li @classString>
@if (node.Enabled) {
<a href="@node.Url">@node.Text</a>
} else {
@node.Text
}
@RenderNodes(node.Children)
</li>
}
</ul>
}
}
@RenderNodes(root.Children)