Razor Themed View Engine for multi-themes site
All sources code for this post can be find at here.
I have just updated the source codes and the post with the suggest from gbitdiff. Thanks for your comment.
Have you ever implemented a multi-themes site using Razor view engine? Yes it is really good to do that. Specially, you can do it and make it work well with Razor. I spent a lot of time to investigate this problem. After searched on Google, I had found some of links very useful about this topic. But all of them also implemented in ASP.NET MVC 2.0 and custom a WebformViewEngine. So I know I must work with it from scratch. But no problem, I decided to analyses some examples at here and here. They are good samples for my work. And I can re-use the ViewEngine from them, only change from WebformViewEngine to RazorViewEngine.
What components do we need to make your site using multi themes? We only need css, images and certainly views for make its working. Basic idea is customize the ViewEngine and when the ASP.NET MVC want to find a image, css, view or partial view, we will point to the valid one in theme folder. That's it. After 4 hours to implemented it, I very happy because it can work very well. And this is my result for default theme:
red theme
and green theme
this is a project structure for multi-themes
As you see, I use the path Content\themes\... is same with the organisation of ASP.NET MVC 3.0, and the path ~\Views\... contain all views. And now I will show you some important line of code to implemented it.
First thing is RazorThemeViewEngine
public class RazorThemeViewEngine : RazorViewEngine
{
public RazorThemeViewEngine()
{
MasterLocationFormats = new[] {
"~/Views/{2}/{1}/{0}.cshtml",
"~/Views/{2}/{1}/{0}.vbhtml",
"~/Views/{2}/Shared/{0}.cshtml",
"~/Views/{2}/Shared/{0}.vbhtml"
};
ViewLocationFormats = new[] {
"~/Views/{2}/{1}/{0}.cshtml",
"~/Views/{2}/{1}/{0}.vbhtml",
"~/Views/{2}/Shared/{0}.cshtml",
"~/Views/{2}/Shared/{0}.vbhtml"
};
PartialViewLocationFormats = new[] {
"~/Views/{2}/{1}/{0}.cshtml",
"~/Views/{2}/{1}/{0}.vbhtml",
"~/Views/{2}/Shared/{0}.cshtml",
"~/Views/{2}/Shared/{0}.vbhtml"
};
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
try
{
return File.Exists(controllerContext.HttpContext.Server.MapPath(virtualPath));
}
catch (HttpException exception)
{
if (exception.GetHttpCode() != 0x194)
{
throw;
}
return false;
}
catch
{
return false;
}
}
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
string[] strArray;
string[] strArray2;
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentException("viewName must be specified.", "viewName");
}
var themeName = GetThemeToUse(controllerContext);
var requiredString = controllerContext.RouteData.GetRequiredString("controller");
var viewPath = GetPath(controllerContext, ViewLocationFormats, "ViewLocationFormats", viewName, themeName, requiredString, "View", useCache, out strArray);
var masterPath = GetPath(controllerContext, MasterLocationFormats, "MasterLocationFormats", masterName, themeName, requiredString, "Master", useCache, out strArray2);
if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
}
return new ViewEngineResult(strArray.Union(strArray2));
}
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
string[] strArray;
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(partialViewName))
{
throw new ArgumentException("partialViewName must be specified.", "partialViewName");
}
var themeName = GetThemeToUse(controllerContext);
var requiredString = controllerContext.RouteData.GetRequiredString("controller");
var partialViewPath = GetPath(controllerContext, PartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, themeName, requiredString, "Partial", useCache, out strArray);
return string.IsNullOrEmpty(partialViewPath) ? new ViewEngineResult(strArray) : new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
}
private static string GetThemeToUse(ControllerContext controllerContext)
{
var themeName = controllerContext.HttpContext.Items["themeName"] as string ?? "Default";
return themeName;
}
private static readonly string[] _emptyLocations;
private string GetPath(ControllerContext controllerContext, string[] locations, string locationsPropertyName, string name, string themeName, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations)
{
searchedLocations = _emptyLocations;
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
if ((locations == null) || (locations.Length == 0))
{
throw new InvalidOperationException("locations must not be null or emtpy.");
}
bool flag = IsSpecificPath(name);
string key = CreateCacheKey(cacheKeyPrefix, name, flag ? string.Empty : controllerName, themeName);
if (useCache)
{
var viewLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
if (viewLocation != null)
{
return viewLocation;
}
}
return !flag ? GetPathFromGeneralName(controllerContext, locations, name, controllerName, themeName, key, ref searchedLocations)
: GetPathFromSpecificName(controllerContext, name, key, ref searchedLocations);
}
private static bool IsSpecificPath(string name)
{
var ch = name[0];
if (ch != '~')
{
return (ch == '/');
}
return true;
}
private string CreateCacheKey(string prefix, string name, string controllerName, string themeName)
{
return string.Format(
CultureInfo.InvariantCulture,
":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}",
new object[] { GetType().AssemblyQualifiedName, prefix, name, controllerName, themeName });
}
private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, string themeName, string cacheKey, ref string[] searchedLocations)
{
var virtualPath = string.Empty;
searchedLocations = new string[locations.Length];
for (var i = 0; i < locations.Length; i++)
{
var str2 = string.Format(CultureInfo.InvariantCulture, locations[i], new object[] { name, controllerName, themeName });
if (FileExists(controllerContext, str2))
{
searchedLocations = _emptyLocations;
virtualPath = str2;
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
searchedLocations[i] = str2;
}
return virtualPath;
}
private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
{
var virtualPath = name;
if (!FileExists(controllerContext, name))
{
virtualPath = string.Empty;
searchedLocations = new[] { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
}
so lucky because RazorViewEngine is also same with WebformViewEngine, so it is not hard to convert to. :D
Second is ThemeControllerBase
public class ThemeControllerBase : Controller
{
protected override void Execute(System.Web.Routing.RequestContext requestContext)
{
var themeName = ConfigurationManager.AppSettings["ThemeName"];
var defaultTheme = ConfigurationManager.AppSettings["DefaultTheme"];
if (requestContext.HttpContext.Items[themeName] == null)
{
//first time load
requestContext.HttpContext.Items[themeName] = requestContext.HttpContext.Request.Cookies.Get("theme").Value;
}
else
{
requestContext.HttpContext.Items[themeName] = defaultTheme;
var previewTheme = requestContext.RouteData.GetRequiredString("theme");
if (!string.IsNullOrEmpty(previewTheme))
{
requestContext.HttpContext.Items[themeName] = previewTheme;
}
}
base.Execute(requestContext);
}
}
and the DropDownList partial view for choose a list of themes:
@{ var list = ThemeSample.Helpers.ThemeHelper.GetSelectedList(ViewContext.RequestContext.HttpContext.Items["themeName"].ToString());
}
@Html.DropDownList("Theme", list)
<script language="javascript" type="text/javascript">
$(function () {
$('#Theme').change(function () {
$.post('@Url.Action("Index", "ThemeDropdown")', { theme: $(this).val() }, function (result) {
$('body').html('');
$('body').html(result);
});
});
});
</script>
controller for it, after user choose the theme on DropDownList, it will submit to the action in this controller.
public class ThemeDropdownController : Controller
{
[HttpPost]
public ActionResult Index(string theme)
{
var themeName = ConfigurationManager.AppSettings["themeName"];
ControllerContext.RequestContext.HttpContext.Items[themeName] = theme;
var themeCookie = new HttpCookie("theme", theme);
HttpContext.Response.Cookies.Add(themeCookie);
const string controller = "Home";
const string action = "Index";
return Redirect(string.Format("~/{0}/{1}/{2}", theme, controller, action));
}
}
theme helper class
public static class ThemeHelper
{
public static IList<SelectListItem> GetSelectedList(string selectedTheme)
{
// TODO: should get from web.config or database
var list = new List<SelectListItem>
{new SelectListItem {Text = "Default", Value = "Default"},
new SelectListItem {Text = "Red", Value = "Red"},
new SelectListItem {Text = "Green", Value = "Green"}};
foreach (var selectListItem in list.Where(selectListItem => selectListItem.Value.Equals(selectedTheme)))
{
selectListItem.Selected = true;
}
return list;
}
public static string GetCssWithTheme(ViewContext viewContext)
{
var themeName = ConfigurationManager.AppSettings["themeName"];
return string.Format("~/Content/themes/{0}/Site.css", viewContext.HttpContext.Items[themeName]);
}
}
some config in Global.asax
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorThemeViewEngine());
Notes: In this post I did not write the helper for find images, but it is really easy to implemented. How about your thinking in this topic? I really want to know about that. Leave some messages and we will get more clearly about that. Happy coding and see you next post!