In my previous post in this series on the new WebMatrix suite of technologies from Microsoft, I described how we are supporting the use of Razor scripts in DotNetNuke, by providing a “Razor Host” module. In this article I will dive deeper into how we host the Razor Parser.
Razor can actually be thought of as a templating engine and if you are planning on building a module that requires templates you might want to consider Razor. Hopefully, this article will show how you might do that.
The WebPage Class
The WebPage Class (actually WebPageBase) is the basis of the new WebPages Framework, in the same way that the “Page” class is the basis of the WebForms Framework.
Essentially the approach we use in our “Razor Host” module is as follows.
- Identify the Razor script file (e.g. Twitter.cshtml)
- Call BuildManager.GetType(scriptFile) to create an instance of WebPage. Note that the System.Web.WebPages assembly contains a special class – PreApplicationStartCode – that ensures that the extensions cshtml and vbhtml are registered with the BuildManager
- Call the ExecutePageHierarchy method of the WebPage class and capture the rendered content.
- Create a LiteralControl with the rendered content and add it to the control tree.
In order to achieve this there are two new classes in a new assembly (DotNetNuke.Web.Razor.dll) which is distributed as part of the “Razor Host” Module.
- RazorModuleBase – this class implements IModuleControl by sub-classing ModuleUserControlBase
- DotNetNukeWebPage – this class subclasses WebPageBase and provides some DNN specific enhancements.
The RazorModuleBase Class
The RazorModuleBase class is where everything comes together. All DNN Modules need to implement the IModuleControl Interface – this how the module injection logic knows what to inject. RazorModuleBase is a new base class that implements IModuleControl and is used as the base class for the Razor Host Module.
In the OnPreRender method of the class we first check if our script - RazorScriptFile – exists. If it does we call the CreateWebPageInstance method to get an instance of the DotNetNukeWebPage (lines 7-16)
Figure 1 – The OnPreRender method of the RazorModuleBase class
|
1: Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
2: MyBase.OnPreRender(e)
3:
4: Try
5: If File.Exists(Server.MapPath(RazorScriptFile)) Then
6:
7: Dim instance As Object = CreateWebPageInstance()
8: If instance Is Nothing Then
9: Throw New InvalidOperationException( _
10: String.Format( _
11: CultureInfo.CurrentCulture, _
12: "The webpage found at '{0}' was not created.", _
13: RazorScriptFile))
14: End If
15:
16: Dim webPage As DotNetNukeWebPage = TryCast(instance, DotNetNukeWebPage)
17:
18: If webPage Is Nothing Then
19: Throw New InvalidOperationException( _
20: String.Format( _
21: CultureInfo.CurrentCulture,
22: "The webpage at '{0}' must derive from DotNetNukeWebPage.",
23: RazorScriptFile))
24: End If
25:
26: webPage.SetContext(Me.ModuleContext)
27: InitHelpers(webPage)
28:
29: Dim writer As New StringWriter
30: webPage.ExecutePageHierarchy(New WebPageContext(HttpContext), writer, webPage)
31:
32: Controls.Add(New LiteralControl(Server.HtmlDecode(writer.ToString())))
33: End If
34: Catch ex As Exception
35: ProcessModuleLoadException(Me, ex)
36: End Try
37:
38:
39: End Sub
|
We then set the WebPage’s module context by calling its SetContext method (line 26). This allows us to access the ModuleContext in Razor script. Finally we call the WebPage’s ExecutePageHierarchy method to get the rendered content, which we add as a LiteralControl to the Controls collection of the module.
In the above description I glossed over two important points: the RazorScriptFile property and the CreateWebPageInstance property.
The RazorScriptFile property (Figure 2) returns the virtual path of a “default” script file. In the Razor Host module, we override this property and return the currently selected script, but the default behaviour in the base class is to return “MyScript.cshtml” if the current control is “MyScript.ascx”.
Figure 2 - The RazorScriptFile property
|
1: Protected Overridable ReadOnly Property RazorScriptFile As String
2: Get
3: Return Me.AppRelativeVirtualPath.Replace("ascx", "cshtml")
4: End Get
5: End Property
|
The CreateWebPageInstance method (Figure 3) uses the RazorScritpFile property to create an instance of a DotNetNukeWebPage (Figure 3), calling BuildManager.GetCompiledType (line 2), which returns a Type and then calling Activator.CreateInstance (line6) to return an instance of the DotNetNukeWebPage.
Figure 3 – CreateWebPageInstance
|
1: Private Function CreateWebPageInstance() As Object
2: Dim type As Type = BuildManager.GetCompiledType(RazorScriptFile)
3: Dim instance As Object = Nothing
4:
5: If type IsNot Nothing Then
6: instance = Activator.CreateInstance(type)
7: End If
8:
9: Return instance
10: End Function
|
Earlier in this article I mentioned that BuildManager knew to “build” a WebPage instance from a cshtml or vbhtml because of some code that exists in the WebPages Framework that registers the extension. So why do we get an instance of DotNetNukeWebPage rather than an instance of WebPage?
The answer lies in the included web.config file that sits in the same folder as the Razor Host Module. By default the Razor Engine will return an instance of WebPage, but this can be modified in the new system.web.webPages.razor section of web.config, where we have indicated that an instance of DotNetNukeWebPage should be returned.
Figure 4 – Razor Host Module’s web.config file
|
1: <system.web.webPages.razor>
2: <pages pageBaseType="DotNetNuke.Web.Razor.DotNetNukeWebPage">
3: <namespaces>
4: <add namespace="Microsoft.Web.Helpers" />
5: <add namespace="WebMatrix.Data" />
6: namespaces>
7: pages>
8: system.web.webPages.razor>
|
At the moment we are just including this web.config file as part of the module, but in the final release we will probably update the web.config file in the website root to include the information.
So that was for all the Architect types who want to know how it was done. In the next blog I will show how you can “package” up a script you have created and distribute it to other DNN users.
This article is cross-posted from my personal blog.