加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

Flexible theming for ASP.NET MVC

发布时间:2020-12-15 01:08:51 所属栏目:百科 来源:网络整理
导读:? http://blogs.planetcloud.co.uk/mygreatdiscovery/post/Flexible-theming-for-ASPNET-MVC.aspx I’m sure many people have experienced frustration when creating custom themes for various apps. Now if it was just a case of changing CSS then tha

?http://blogs.planetcloud.co.uk/mygreatdiscovery/post/Flexible-theming-for-ASPNET-MVC.aspx

I’m sure many people have experienced frustration when creating custom themes for various apps. Now if it was just a case of changing CSS then that would be fine. But often there is a need to change master pages and pages for either layout purposes or perhaps to add additional functionality or UI components.

Well since I’m loving MVC so much at the moment I thought I would look into how we could do theming. The great thing about ASP.NET MVC is that since you are just calling a controller action,you get so much control over the entire request. For example,you can return a view and override the master page that should be used:

public?ActionResult Index()?
{?
??? ViewData["Message"] =?"Welcome to ASP.NET MVC!";?
????return?View("Index",?"CustomMaster");?
}

For my ideal theming solution I would want to:

1) Easily add my own CSS?
2) Change site layout (master page)?
3) Override specific views (perhaps I want to change some of the elements on a product detail page)?
4) Do any of the above,or none at all – and everything just works.

ASP.NET MVC is built on a lot of convention. For example,the default ViewEngine looks in specific areas for the View names passed by controller actions. The great thing is,you can add your own custom ViewEngine and override how Views are located.

This is exactly what Chris Pietschmann did in his article??ASP.NET MVC: Implement Theme Folders using a Custom ViewEngine.

In order to meet my theming requirements I made a few changes since I wanted it to be optional to override the default views.

So if?http://www.mysite.com/catalog/product/groovy-shoes?was requested,the ViewEngine would first look for a view called “Product” in the currently selected themes folder and if it did not exist then it would use the one in the /Views/Catalog/ directory.

I like this approach because it means that if you release a new version of your application and some of the views change then this won’t effect the custom views made by other people. In fact,if your styles no longer worked against a new version of the view,you could grab the view from the existing release (providing your model hadn’t changed) and just drop this inside your theme folder.

So to better explain,how about some visuals. The image below displays the default structure of my MVC project:

image

The “Default” views are in their default location. These are the ones we will develop with since things like the “New View” action in Visual Studio will pop the views in your /Views directory.

The site master page sits in the default location of /Views/Shared and CSS sits in /Content/css. So if I run my project and I get the default MVC site look and feel:

image

So now I decide that everything looks good,but I want to change the logo on my site and add in another menu item. To do this I just take a copy of the existing master page and drop it in my themes folder (maintaining the same directory structure):

image

Then I make my changes and run the site.

image

So now we have a custom master page but still using the default CSS. So let’s add our own custom stylesheet. Grab a copy of the default CSS stylesheet and paste into your themes folder (again,maintaining the same directory structure):

image

Currently the master page is referencing the default CSS so we need to change the CSS reference to point to our theme CSS. To do this I have written a few helper methods. The default method is shown below:


< link? href ="<%=AppHelper.CssUrl(" site. css ")%>"? rel ="stylesheet"? type ="text/css"? />

Simply change to:


< link? href ="<%=AppHelper.ThemeCssUrl(" site. css "," Default ")%>"? rel ="stylesheet"? type ="text/css"? />??

?

?

?

?

Here we are passing the name of the theme in order to grab the correct stylesheet.

Note: Since I am preserving the same directory structure then a relative reference should have worked but it didn’t (and I don’t know why). If there is a better way of doing this then leave a comment. This approach seems clean enough and does get rid of any hardcoded paths.

Now when I run my site I pick up my custom stylesheet:

image

The final requirement was that I should be able to override any of the existing views. Sure I can change the styles using CSS but often the HTML markup isn’t quite what you need or perhaps you want to add some additional elements.

Let’s say I want to add another heading and add a widget that displays some product categories. Again,simply copy the index view into your theme directory:

image

Make your changes and launch the site:

image

The source code:

public?CommerceViewEngine()?
{?
??? MasterLocationFormats =?new?string[] {?
????????"~/Themes/{2}/Views/{1}/{0}.master",?
????????"~/Themes/{2}/Views/Shared/{0}.master",?
????????"~/Views/Shared/{0}.master"?
??? };?
??? ViewLocationFormats =?new?string[] {?
????????"~/Themes/{2}/Views/{1}/{0}.aspx",?
????????"~/Themes/{2}/Views/{1}/{0}.ascx",?
????????"~/Themes/{2}/Views/Shared/{0}.aspx",?
????????"~/Themes/{2}/Views/Shared/{0}.ascx",?
????????"~/Views/{1}/{0}.aspx",?
????????"~/Views/{1}/{0}.ascx",?
????????"~/Views/Shared/{0}.aspx",?
????????"~/Views/Shared/{0}.ascx"?
??? };?
??? PartialViewLocationFormats = ViewLocationFormats;?
}

#region Overrides

public?override?ViewEngineResult FindView(ControllerContext controllerContext,?string?viewName,?stringmasterName,?bool?useCache)?
{?
????if?(controllerContext ==?null) {?
????????throw?new?ArgumentNullException("controllerContext");?
??? }?
????if?(string.IsNullOrEmpty(viewName)) {?
????????throw?new?ArgumentException("viewName must be specified.",?"viewName");?
??? }

????if?(controllerContext.Controller.GetType().BaseType ==?typeof(CommerceController)) {?
??????? var commerceController = controllerContext.Controller?as?CommerceController;?
????????string?themeName = controllerContext.HttpContext.Items["theme"]?as?string;

????????string?controllerName = controllerContext.RouteData.GetRequiredString("controller");

????????string[] strArray,strArray2;

??????? masterName = (string.IsNullOrEmpty(masterName)) ??"site"?: masterName;

????????string?viewPath =?this.GetPath(controllerContext,?this.ViewLocationFormats,?"ViewLocationFormats",?
??????????????? viewName,themeName,controllerName,?"View",useCache,?out?strArray);

????????string?masterPath =?this.GetPath(controllerContext,?this.MasterLocationFormats,?"MasterLocationFormats",?
??????????? masterName,?"Master",?out?strArray2);

????????if?(!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) ||string.IsNullOrEmpty(masterName)))?
??????? {?
????????????return?new?ViewEngineResult(this.CreateView(controllerContext,viewPath,masterPath),?this);?
??????? }?
????????return?new?ViewEngineResult(strArray.Union<string>(strArray2));?
??? }?
????else?
??? {?
????????return?base.FindView(controllerContext,viewName,masterName,useCache);?
??? }?
}

public?override?ViewEngineResult FindPartialView(ControllerContext controllerContext,?string?partialViewName,?booluseCache)?
{?
????if?(controllerContext ==?null) {?
????????throw?new?ArgumentNullException("controllerContext");?
??? }

????if?(string.IsNullOrEmpty(partialViewName)) {?
????????throw?new?ArgumentException("viewName must be specified.",?"viewName");?
??? }

????if?(controllerContext.Controller.GetType().BaseType ==?typeof(CommerceController))?
??? {?
??????? var commerceController = controllerContext.Controller?as?CommerceController;?
????????string?themeName = controllerContext.HttpContext.Items["theme"]?as?string;?
????????string?controllerName = controllerContext.RouteData.GetRequiredString("controller");

????????string[] strArray;?
????????string?partialViewPath =?this.GetPath(controllerContext,?this.PartialViewLocationFormats,?
????????????"PartialViewLocationFormats",partialViewName,?"Partial",?outstrArray);

????????if?(string.IsNullOrEmpty(partialViewPath)) {?
????????????return?new?ViewEngineResult(strArray);?
??????? }?
????????return?new?ViewEngineResult(this.CreatePartialView(controllerContext,partialViewPath),?this);?
??? }?else?{?
????????return?base.FindPartialView(controllerContext,useCache);?
??? }?
}

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;?
??? }?
}

#endregion

#region Utilities

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 =?this.CreateCacheKey(cacheKeyPrefix,name,flag ??string.Empty : controllerName,themeName);?
????if?(useCache) {?
????????string?viewLocation =?this.ViewLocationCache.GetViewLocation(controllerContext.HttpContext,key);?
????????if?(viewLocation !=?null) {?
????????????return?viewLocation;?
??????? }?
??? }?
????if?(!flag) {?
????????return?this.GetPathFromGeneralName(controllerContext,locations,key,ref?searchedLocations);?
??? }?
????return?this.GetPathFromSpecificName(controllerContext,?ref?searchedLocations);?
}

private?static?bool?IsSpecificPath(string?name)?
{?
????char?ch = name[0];?
????if?(ch !=?'~')?
????????return?(ch ==?'/');?
????return?true;?
}

private?string?CreateCacheKey(string?prefix,?string?name,?string?themeName)?
{?
????return?string.Format(CultureInfo.InvariantCulture,?":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}",?
????????new?object[] {?base.GetType().AssemblyQualifiedName,prefix,themeName });?
}

private?string?GetPathFromGeneralName(ControllerContext controllerContext,?
????string?controllerName,?string?cacheKey,?ref?string[] searchedLocations)?
{?
????string?virtualPath =?string.Empty;?
??? searchedLocations =?new?string[locations.Length];?
????for?(int?i = 0; i < locations.Length; i++) {?
????????string?str2 =?string.Format(CultureInfo.InvariantCulture,locations[i],?new?object[] { name,themeName });

????????if?(this.FileExists(controllerContext,str2)) {?
??????????? searchedLocations = _emptyLocations;?
??????????? virtualPath = str2;?
????????????this.ViewLocationCache.InsertViewLocation(controllerContext.HttpContext,cacheKey,virtualPath);?
????????????return?virtualPath;?
??????? }?
??????? searchedLocations[i] = str2;?
??? }?
????return?virtualPath;?
}

private?string?GetPathFromSpecificName(ControllerContext controllerContext,?refstring[] searchedLocations)?
{?
????string?virtualPath = name;?
????if?(!this.FileExists(controllerContext,name)) {?
??????? virtualPath =?string.Empty;?
??????? searchedLocations =?new?string[] { name };?
??? }?
????this.ViewLocationCache.InsertViewLocation(controllerContext.HttpContext,virtualPath);?
????return?virtualPath;?
}??????

#endregion

?

?

We are also setting the theme on our custom controller (currently hardcoded):

public?class?CommerceController : Controller?
{?
????string?_themeName;?
????public?string?ThemeName?
??? {?
??????? get {?
????????????if?(string.IsNullOrEmpty(_themeName))?
??????????????? _themeName =?"Default";?
????????????return?_themeName;?
??????? }?
??????? set {?
??????????? _themeName =?value;?
??????? }?
??? }

????protected?override?void?Execute(System.Web.Routing.RequestContext requestContext)?
??? {?
??????? requestContext.HttpContext.Items["theme"] =?this.ThemeName;?
????????base.Execute(requestContext);?
??? }?
}

One thing that does not seem to be working currently is the caching of the view locations. Whilst ViewLocationCache.InsertViewLocation certainly fires,it does not appear to store anything. I have emailed Chris and posted to StackOverflow to try and find out why (perhaps a difference in MVC 2.0 RC?).

Enjoy!

[Update]

The ViewLocationCache is disabled by default when running in debug mode. If you want to enable it for testing purposes,?see my post here.

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读