ARCHIVED
This chapter has not been updated for the current version of Orchard, and has been ARCHIVED.
This design proposal outlines enhancements to the Themes feature to support the following:
This design proposal outlines enhancements to the Themes feature to support the following:
- The ability for the application to function independently of the Themes feature, by having default
Views
,Content
,Scripts
,Packages
andWidgets
folders - The ability for an applied Theme to override the default files in the application for
Views
,Content
,Scripts
,Packages
andWidgets
- The ability for an applied Theme to “fall back” to default files when they are not overridden by the Theme
- An include-style helper method syntax for composition of View-related files (either in the applied Theme or in the default Views folder)
Theme Overrides
The application defines top-level Content
, Views
, Scripts
, Packages
and Widgets
folders, which are not related to the applied Theme and allow the application to function independently of the Themes feature.
In the absence of the Themes feature (or an applied Theme), the application will use the files in these directories to serve the UI for the application.
Themes override the default files in the application by specifying files for Content, Views, Scripts and Widgets under the Theme folder. For example, if the application contains a ~/Views/Login.aspx
page, the Blue Theme may override the rendering for this page by specifying a custom ~/Themes/Blue/Views/Login.aspx
page.
The View Engine will look to the currently applied Theme to resolve files before ”falling back” to the default Views folder. Overrides allow a theme to only specify the files that require customization by the Theme, instead of requiring Themes to duplicate every file in the application.
The override feature can dramatically simplify some theme definitions - a simple Theme may only need to override the header and style sheet for the application, so it would only need to specify ~/Views/Header.aspx
and ~/Styles/Site.css
.
The override feature can also simplify upgrading the application to a newer version, since it allows the default files of the application to be independently updated, while preserving Theme customizations.
Note: Serving static files needs to be as fast as possible and the overhead of running any code on top of the web server's tends to be prohibitively high in comparison to the benefits. For this reason, the helper APIs presented here will directly generate URLs that directly map to the physical location of the resource files instead of, for example, generating a route-based URL that could be dynamically resolved later. Where that becomes problematic is that there are a few places, such as stylesheets, that are themselves static resources but that must reference other static resources (typically background images). Because the stylesheet is itself a static resource (it is possible to serve an
aspx
as the stylesheet but this is confusing and breaks IntelliSense), it cannot call into the helpers and must reference its dependencies using URLs that are relative to itself. This means in turn that the dependencies in question must be physically at the place that the stylesheet points to. This of course puts a limitation on resource fallback: you cannot override just the stylesheet, you also need to copy everything it depends on.Issue: In cases where the page is output-cached, adding or removing a file in the theme might not have immediate effect as the cached page is still pointing at the previously resolved resource. This could be fixed by "touching" the page or by doing more elaborate management of cache dependencies.
Display template overrides
Display and editor templates are typically defined in a module under ~/Packages/[PackageName]/Views/DisplayTemplates/[items|parts]/[PackageName].[ItemOrPartName].ascx
.
Overriding such a specialized display template is possible and sometimes useful, but it is discouraged, because the theme author can't possibly handle all existing modules. Whenever possible, the markup in the module view should be generic enough to be efficiently styled through CSS.
For those cases where the theme author or the person who customizes the application needs to override one of the display templates from a core or extension module, he should do so in the current theme under ~/Themes/[ThemeName]/Views/DisplayTemplates/[items|parts]/[PackageName].[ItemOrPartName].ascx
. Notice that the only change in the path was to replace ~/Packages/[PackageName]
with ~/Themes/[ThemeName]
.
Widget Overrides
Preliminary: widgets are not yet implemented. In order to allow a Theme to override the complete rendering for the application, it is necessary to support the ability for a Theme to override an individual widget's rendering.
This assumes that the Theme author has foreknowledge of which widgets are installed to the application, but in many cases an application will include a default set of widgets that can be assumed to exist.
An override for a widget is specified by adding a Widgets folder underneath the named folder for the Theme, creating a subfolder for the Widget to override, and copying the widget's view file (.ascx
) to the folder.
This file can be customized any way the Theme author sees fit, and the widget engine will use this file instead of the default one supplied with the widget.
Include Methods
To enable the override feature to work properly, it is necessary to introduce an indirection for composing View files and including dependent references such as scripts, images, and style sheets.
This allows the Theme to reference files in a way that allows the application to resolve dependent file references without depending on a hard-coded physical path.
This is achieved using helper methods to generate URLs, as described below. It should be noted that an include-style model for composition is decidedly different from the built-in ASP.NET Master Page feature, but is more tailored to the desired audience for Theme developers, namely HTML designers that may not be familiar with the intricacies of the ASP.NET programming model. The assumption is that includes are likely to be more immediately understandable by this audience.
View composition relies on an Html.Zone
helper method that defines named zones in the views. Several providers can take advantage of the named zones, for example an include provider that resolves the appropriate View file (either local to the Theme or one of the default application files) and includes it, or a widget provider that injects widgets into zones.
Zones can include named subsections that enable the insertion of positioned contents. By default, all zones come with two subsections, before
and after
, that enable the insertion of contents at the beginning and end of the zone. An example of a zone with named subsections is:
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><%=Html.Title() %></title><%
Html.Zone("head", ":metas :styles :scripts"); %>
</head>
In this example, a head zone is injected into the head tag, and it defines subsections for metas, stylesheets and scripts. This is used to inject meta-tags, registered styles and scripts.
Example document and layout files follow:
document.aspx
:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<BaseViewModel>" %>
<%@ Import Namespace="Orchard.Mvc.ViewModels"%>
<%@ Import Namespace="Orchard.Mvc.Html"
%><!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><%=Html.Title() %></title><%
Html.Zone("head", ":metas :styles :scripts"); %>
</head>
<body><%
Html.ZoneBody("body"); %>
</body>
</html>
Note: The document file is almost never overridden by the theme.
layout.ascx
:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<BaseViewModel>" %>
<%@ Import Namespace="Orchard.Mvc.ViewModels"%>
<%--
name: Sample layout template
zones: Head, Header, Content, Right sidebar, Footer
--%><%
Html.RegisterStyle("site.css");
Html.RegisterScript("MyScript.js");
Model.Zones.AddRenderPartial("header");
Model.Zones.AddRenderPartial("footer");
%>
<div class="page">
<img src="<%= Html.ContentFolderUrl("images/banner.jpg") %>" alt="Banner"/>
<img src="<%= Html.Theme("Logo") %>" alt="Logo"/>
<div id="header"><%
Html.Zone("header");
Html.Zone("menu"); %>
</div>
<div id="main"><%
Html.ZoneBody("content");
%></div>
<div id="rightSidebar"><%
Html.Zone("right sidebar", "UnorderedListLayout")
%></div>
<div id="footer"><%
Html.Zone("footer");
%></div>
</div>
Note
in this example, some contents are inlined but in a real template those would really be in partial views.
In this example, the calls to Html.Zone
declare zones on the page where components will be able to inject contents and widgets.
For the header and footer zones, calls to AddRenderPartial
will try to find a partial view with the same name (without its extension) as the zone (first in the theme, then in the top views) and will include it if it's found.
The included files can themselves contain calls to Html.Zone()
for nested composition.
Issue: when new zones are being added by included partial view, how will the admin UI discover them? The page also contains specialized calls to other APIs that will be detailed below.
Open Issue: Do we need a version of
Html.Zone
that accepts a model?
How many overloads of RenderPartial
do we want to repeat?
Question: includes will be so common that we could consider having a shortcut like we have for localization.
Zone("Content")
orZ("Content")
? The contents of each include file should be semantically complete HTML. No separation of opening and closing tags into separate file should exist. When this looks necessary, the surrounding markup should be moved to the parent file.
Html.Title
A special helper that injects the page title.
Html.RegisterScript
Registers a JavaScript file for inclusion by Html.Zone("head", ":metas :styles :scripts")
. This helper method accepts as input a path that is relative to the root Scripts directory (of either the theme or root application).
It registers the script to include but does not render anything immediately. The actual rendering of the script tag will happen when the view calls Html.Zone("head", ":metas :styles :scripts")
.
The actual URL that will be rendered into the page will be mapped by the web server directly to either the theme or root application Scripts
directory without having to run application code.
The system will search for the correct physical file to serve in the following order:
~/Themes/ThemeName/Scripts
~/Scripts
Example:
Html.RegisterScript("myscript.js")
...registers a URL to the first physical file that exists in these locations (in order):
/ApplicationName/Themes/ThemeName/Scripts/myscript.js
/ApplicationName/Scripts/myscript.js
Parameters:
scriptPath
[String] - the path to the script file, relative to the root of the Scripts
directory.
Return Value:
None
Html.RegisterStyleSheet
Registers a CSS file for inclusion by Html.Zone("head", ":metas :styles :scripts")
.
This helper method accepts as input a path that is relative to the root Content
directory (of either the theme or root application).
It registers the stylesheet to include but does not render anything immediately. The actual rendering of the link tag will happen when the view calls Html.Zone("head", ":metas :styles :scripts")
.
The actual URL that will be rendered into the page will be mapped by the web server directly to either the theme or root application Scripts
directory without having to run application code.
The system will search for the correct physical file to serve in the following order:
~/Themes/ThemeName
~/Themes/ThemeName/Content
~/
~/Content
Note: It is not correct (and might result in an exception) to include stylesheets from widgets using this API, since these stylesheets belong in the <head>
of the document. Refer to the section entitled “Widget Scripts and Stylesheets” below.
Example:
Html.RegisterStyleSheet("mystylesheet.css")
...registers a URL to the first physical file that exists in these locations (in order):
/ApplicationName/Themes/ThemeName/mystylesheet.css
/ApplicationName/Themes/ThemeName/Content/mystylesheet.css
/ApplicationName/mystylesheet.css
/ApplicationName/Content/mystylesheet.css
Parameters:
styleSheetPath
[String] - the path to the CSS file, relative to the root of the Content or theme directory.
Return Value:
None
Html.Zone("head", ":metas :styles :scripts")
We'll have a special provider that recognizes the "Head" zone and generates the script, style and link tags resulting from previous registration by widgets and views. In particular, Html.RegisterScript
and Html.RegisterStyleSheet
calls result in Html.Include("Head")
generating the relevant tags.
This special provider is also the one that will inject style overrides.
The widgets can also participate in what gets rendered by this API by exposing their list of scripts and stylesheets (see Widgets).
The list of stylesheets and scripts to be included is first processed to remove duplicates, and then the helpers proceeds to rendering the relevant tags.
Parameters:
name
[string] - the name of the zone. May correspond to the name of a partial view, minus the file extension.
subsections
[string] - a space-separated list of subsections for the zone. Each subsection name begins with a colon. The ":before
" and ":after
" subsections always exist no matter what is specified in this parameter.
layout
[String] - Optional: the name of the view file that defines the chrome to render between the different items of the zone (to surround individual contents). The chrome template file looks like this:
Preliminary
UnorderedListLayout.ascx
:
<@Control language="C#" inherits="System.Web.Mvc.ViewUserControl<List<object>>" %>
<% if (Model.Length > 0) { %>
<% if (Model.Length == 1) { %>
<%= Html.DisplayFor(Model[0]) %>
<% } else {
<ul class="layoutList">
<% foreach(var item in Model) { %>
<li><%= Html.DisplayFor(item) %></li>
<% } %>
</ul>
<% } %>
<% } %>
If no layout file with that name is found, a default layout is used, that is looked for in the template directory, then in the fallback theme.
Html.Theme
Includes a theme setting.
Parameters:
settingName
[String] - the name of the setting to include.
Return Value:
A string that contains the value of the setting, or an empty string if it not found.
Default Includes
By convention, the pages of the Orchard Commerce application are divided into the following includes.
Specific Themes may override and/or define additional include files.
Head.ascx
Contains meta tags, styles, and scripts that should be included in every page of the application. In the example above, the contents of the head file has been inlined so that the file contains examples of each include method. In a real template, this contents would be in head
, and the template itself would have a simple Html.Include("head")
.
Header.ascx
Contains header content that is common all pages (site name banner image, navigation menus, login/logout link, etc).
Note: in the example above, the banner has been put in the template itself but it shouldn't be in a real template.
Footer.ascx
Contains footer content that is common to all pages. The overall composition of views might result in something like this:
Content and Script Includes
Supporting Theme overrides for Views, Scripts, and Content enables Theme authors to simply copy files from the default application's top-level folders into the Theme's folder structure.
However, a given View page might have references to other files - in particular, content such as images and stylesheets, and scripts.
In order to keep these references intact when copying files to the Theme, it is necessary to support helper methods for referencing files from the Content
and Scripts
directories, namely Html.ContentFolderUrl
, Html.RegisterStylesheet
and Html.RegisterScript
.
These methods take as input a path relative to the local Content or Scripts folder for the Theme (or Widget) and return a resolved URL.
For example, in a Theme, a particular View page might have code as follows:
<a href="<%=Url.Action("Show", "Cart") %>"><img
src="<%=Html.ContentFolderUrl("images/cart.gif") %>"
alt="Cart" /></a>
The Html.ContentFolderUrl
returns a path to /ApplicationName/Themes/MyCurrentTheme/images/cart.gif
, /ApplicationName/Themes/MyCurrentTheme/Content/images/cart.gif
, /ApplicationName/images/cart.gif
or /ApplicationName/Content/images/cart.gif
, whichever is found first when the API is called. This relies on IIS static file handling to serve the content, for performance reasons.
<a href="/Cart/Show"><img src="/ContentFiles/images/cart.gif" alt="Your cart" /></a>
A similar Html.RegisterScriptUrl
is supported for script file references, which searches under the appropriate Scripts folder (first in the Theme, then in the top-level Scripts folder for the application).
In general, if a content or script file is meant to be Theme-overridable, these Include
methods should be used to reference the file path.
In the example above, the View page might be in the top-level Views
folder and the Theme overrides the cart.gif
image in the Theme's Content/images
folder.
Conversely, the View page might be overridden by the Theme, but the cart.gif
image remains in the top-level Content/images
folder.
In either case, the references will resolve correctly at runtime.