I have an application that gets rolled out in multiple countries. There will be a setting in the web.config file, that defines the country.
The country will not be in the URL.
Some of the the views change depending on the country.
My first attempt is to use a folder inside the views folder that contains views, if they differ from the default view:
Default
/questions/ask.aspx
Spain
/questions/ESP/ask.aspx
If there is no view in the country-folder the default view is used. Is there a way to extend the ViewEngine to lookup views in the country folder first?
EDIT:
This is a poc only. To see a full implementation have a look at
http://pietschsoft.com/?tag=/mvc
private static string[] LocalViewFormats =
new string[] {
"~/Views/{1}/ESP/{0}.aspx",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
public LocalizationWebFormViewEngine()
{
ViewLocationFormats = LocalViewFormats;
}
public class MyViewEngine : WebFormViewEngine
{
private static string[] LocalViewFormats = new[] { "~/Views/ESP/{0}.aspx",
"~/Views/ESP/{0}.ascx" };
public MyViewEngine()
{
ViewLocationFormats = LocalViewFormats.Union(ViewLocationFormats).ToArray();
}
}
Obviously, you don't want to hardcode the location, but this should give you the general idea.
Related
I develop massive web application with a lot of views. It is messy to keep desktop/mobile views in same folder. Is it possible to group mobile views (name.Mobile.cshtml) into explicit subfolder and say to DisplayModeProvider to find views there?
For example,
Views/Home/Index.Mobile.cshtml moves to Views/Home/Mobile/Index.Mobile.cshtml
Views/People/List.Mobile.cshtml moves to Views/People/Mobile/List.Mobile.cshtml
Well, i found some ways to resolve this problem.
I can implement custom RazorViewEngine(WebFormViewEngine) and specify my own ViewLocationFormats collection.
I can use Areas to achieve this behaviour.
I can implement custom DefaultDisplayMode and override TransformPath() method to change views location.
I think third way is more easy and simple way. Here is the code:
First i create custom DisplayMode and inherit it from DefaultDisplayMode:
public class NewDisplayMode : DefaultDisplayMode
{
public NewDisplayMode()
: base("Mobile") //any folder name you like, or you can pass it through parameter
{
}
public NewDisplayMode(string folderName)
: base(folderName) //any folder name you like, or you can pass it through parameter
{
}
protected override string TransformPath(string virtualPath, string suffix)
{
string view = Path.GetFileName(virtualPath);
string pathToView = Path.GetDirectoryName(virtualPath);
virtualPath = (pathToView + "\\" + suffix + "\\" + view).Replace("\\", "/");
return virtualPath;
}
}
In the code above i override TransformPath() method and transform virtualPath string to change location to views.
Next all i need is to add this mode to modes collection:
protected void Application_Start()
{
DisplayModeProvider.Instance.Modes.RemoveAt(0);
DisplayModeProvider.Instance.Modes.Insert(0, new NewDisplayMode()
{
ContextCondition = context => context.GetOverriddenBrowser().IsMobileDevice
//ContextCondition = context => context.Request.Url.Host == "www.m.mysite.com"
});
//other code
}
Therefore, i do not need to rename my view files, i use same name for mobile and desktop views. Finally, my folders structure looks like so:
How do you make 1 SiteMap per MVC area and use MvcSiteMapNodeAttribute at the same time?
Please have a look at this answer for help with setting up MvcSiteMapProvider with areas. The routes have to be configured using the correct conventions or it won't work right.
However, that alone isn't going to address this requirement, because there is no default assumption made that you want to have a different SiteMap per area.
The behavior of the internal DI container assumes that there will be 1 SiteMap per domain name, and that all of the SiteMaps in the application will be built using the same configuration. There is no way to change this behavior unless you use an external DI container and follow the instructions in Multiple SiteMaps in One Application to override it.
Option 1
You could continue using the internal DI container and a single SiteMap for the entire website and you could create a custom ISiteMapNodeVisibilityProvider that hides everything that is not in the current area by reading the area from the current request.
public class AreaSiteMapNodeVisibilityProvider
: SiteMapNodeVisibilityProviderBase
{
public AreaSiteMapNodeVisibilityProvider()
{
// NOTE: Accept this as a constructor parameter if using external DI and
// use a guard clause to ensure it is not null.
this.mvcContextFactory = new MvcSiteMapProvider.Web.Mvc.MvcContextFactory();
}
private readonly MvcSiteMapProvider.Web.Mvc.IMvcContextFactory mvcContextFactory;
#region ISiteMapNodeVisibilityProvider Members
public override bool IsVisible(ISiteMapNode node, IDictionary<string, object> sourceMetadata)
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var area = requestContext.RouteData.DataTokens["area"];
var areaName = area == null ? string.Empty : area.ToString();
return string.Equals(node.Area, areaName, StringComparison.OrdinalIgnoreCase);
}
#endregion
}
Then set it up as the default visibility provider.
<add key="MvcSiteMapProvider_DefaultSiteMapNodeVisibiltyProvider" value="MyNameSpace.AreaSiteMapNodeVisibilityProvider, MyAssemblyName" />
Using external DI (StructureMap example shown):
// Visibility Providers
this.For<ISiteMapNodeVisibilityProviderStrategy>().Use<SiteMapNodeVisibilityProviderStrategy>()
.Ctor<string>("defaultProviderName").Is("MyNameSpace.AreaSiteMapNodeVisibilityProvider, MyAssemblyName");
Do note that you will still need to nest your area nodes below the non-area part of the site if you do this, so it might not behave as you would like. You need to ensure you set the parent key of the Admin area to a key of a node in the non-area part - there can only be 1 root node per SiteMap.
Also, if you go this route, be sure to set the MvcSiteMapProvider_VisibilityAffectsDescendants setting to "false" so your area nodes are not affected by the visibility of the non-area nodes.
Option 2
Inject a custom ISiteMapCacheKeyGenerator that is based on area and use the SiteMapCacheKey property of [MvcSiteMapNode] attribute to control which area the node belongs to.
public class AreaSiteMapCacheKeyGenerator
: ISiteMapCacheKeyGenerator
{
public AreaSiteMapCacheKeyGenerator(
IMvcContextFactory mvcContextFactory
)
{
if (mvcContextFactory == null)
throw new ArgumentNullException("mvcContextFactory");
this.mvcContextFactory = mvcContextFactory;
}
protected readonly IMvcContextFactory mvcContextFactory;
#region ISiteMapCacheKeyGenerator Members
public virtual string GenerateKey()
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var area = requestContext.RouteData.DataTokens["area"];
return area == null ? "default" : area.ToString();
}
#endregion
}
You need to inject this using external DI (StructureMap example shown):
this.For<ISiteMapCacheKeyGenerator>().Use<AreaSiteMapCacheKeyGenerator>();
And then configure your [MvcSiteMapNode] attributes:
[MvcSiteMapNode(Title = "title", Description = "desc", Key = "root", ParentKey = null, ImageUrl = "fa-home", Order = 0, SiteMapCacheKey = "Admin")]
[MvcSiteMapNode(Title = "title", Description = "desc", Key = "root", ParentKey = null, ImageUrl = "fa-home", Order = 0, SiteMapCacheKey = "default")]
Option 3
Rather than setting SiteMapCacheKey on every [MvcSiteMapNode] attribute, you could put each area in a separate assembly and configure it to only scan the pertinent area assembly for [MvcSiteMapNode] attribute.
public class AreaSiteMapCacheKeyGenerator
: ISiteMapCacheKeyGenerator
{
public AreaSiteMapCacheKeyGenerator(
IMvcContextFactory mvcContextFactory
)
{
if (mvcContextFactory == null)
throw new ArgumentNullException("mvcContextFactory");
this.mvcContextFactory = mvcContextFactory;
}
protected readonly IMvcContextFactory mvcContextFactory;
#region ISiteMapCacheKeyGenerator Members
public virtual string GenerateKey()
{
var requestContext = this.mvcContextFactory.CreateRequestContext();
var area = requestContext.RouteData.DataTokens["area"];
return area == null ? "default" : area.ToString();
}
#endregion
}
public class OneToOneSiteMapCacheKeyToBuilderSetMapper
: ISiteMapCacheKeyToBuilderSetMapper
{
public virtual string GetBuilderSetName(string cacheKey)
{
return cacheKey;
}
}
In the external DI module (StructureMap example shown):
// Setup the cache
var cacheDependency = this.For<ICacheDependency>().Use<NullCacheDependency>();
var cacheDetails = this.For<ICacheDetails>().Use<CacheDetails>()
.Ctor<TimeSpan>("absoluteCacheExpiration").Is(absoluteCacheExpiration)
.Ctor<TimeSpan>("slidingCacheExpiration").Is(TimeSpan.MinValue)
.Ctor<ICacheDependency>().Is(cacheDependency);
// Register the ISiteMapNodeProvider instances
var defaultNodeProvider = this.For<ISiteMapNodeProvider>().Use<ReflectionSiteMapNodeProvider>()
.Ctor<bool>("includeAssemblies").Is(new string[] { "dllmain" });
var adminNodeProvider = this.For<ISiteMapNodeProvider>().Use<ReflectionSiteMapNodeProvider>()
.Ctor<bool>("includeAssemblies").Is(new string[] { "dll2" });
// Register the ISiteMapBuilder instances
var defaultBuilder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
.Ctor<ISiteMapNodeProvider>().Is(defaultNodeProvider);
var adminBuilder = this.For<ISiteMapBuilder>().Use<SiteMapBuilder>()
.Ctor<ISiteMapNodeProvider>().Is(adminNodeProvider);
// Register the builder sets
this.For<ISiteMapBuilderSetStrategy>().Use<SiteMapBuilderSetStrategy>()
.EnumerableOf<ISiteMapBuilderSet>().Contains(x =>
{
// SiteMap builder for the non-area part of the site
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("default")
.Ctor<bool>("securityTrimmingEnabled").Is(false)
.Ctor<bool>("enableLocalization").Is(false)
.Ctor<bool>("visibilityAffectsDescendants").Is(false)
.Ctor<bool>("useTitleIfDescriptionNotProvided").Is(true)
.Ctor<ISiteMapBuilder>().Is(defaultBuilder)
.Ctor<ICacheDetails>().Is(cacheDetails);
// SiteMap builder for the Admin area of the site
x.Type<SiteMapBuilderSet>()
.Ctor<string>("instanceName").Is("Admin")
.Ctor<bool>("securityTrimmingEnabled").Is(false)
.Ctor<bool>("enableLocalization").Is(false)
.Ctor<bool>("visibilityAffectsDescendants").Is(false)
.Ctor<bool>("useTitleIfDescriptionNotProvided").Is(true)
.Ctor<ISiteMapBuilder>().Is(adminBuilder)
.Ctor<ICacheDetails>().Is(cacheDetails);
});
// Register the custom ISiteMapCacheKeyGenerator and ISiteMapCacheKeyToBuilderSetMapper
this.For<ISiteMapCacheKeyGenerator>().Use<AreaSiteMapCacheKeyGenerator>();
this.For<ISiteMapCacheKeyToBuilderSetMapper>().Use<OneToOneSiteMapCacheKeyToBuilderSetMapper>();
I have a multi-tenant application and I'm trying to determine the simplest means of controlling which CSS files are bundled based on the url of any incoming request.
I'm thinking I can have some conditional logic inside RegisterBundles() that takes the Url as a string, and bundles accordingly:
public static void RegisterBundles(BundleCollection bundles, string tenant = null) {
if (tenant == "contoso"){
bundles.Add(new StyleBundle("~/contoso.css")
}
}
But I don't know how to pass the string into RegisterBundles, nor even if it's possible, or the right solution. Any help here would be awesome.
It is not possible to do it in RegisterBundles right now. Dynamically generating the bundle content per request will prevent ASP.net from caching the minified CSS (it's cached in HttpContext.Cache).
What you can do is create one bundle per tenant in RegisterBundles then select the appropriate bundle in the view.
Example code in the view:
#Styles.Render("~/Content/" + ViewBag.TenantName)
Edit:
As you said, setting the TenantName in a ViewBag is problematic since you have to do it per view. One way to solve this is to create a static function like Styles.Render() that selects the correct bundle name based from the current tenant.
public static class TenantStyles
{
public static IHtmlString Render(params string[] paths)
{
var tenantName = "test"; //get tenant name from where its currently stored
var tenantExtension = "-" + tenantName;
return Styles.Render(paths.Select(i => i + tenantExtension).ToArray());
}
}
Usage
#TenantStyles.Render("~/Content/css")
The bundle names will need to be in the this format {bundle}-{tenant} like ~/Content/css-test. But you can change the format ofcourse.
I think you are after a solution that allows you to dynamically control the BundleCollection. As far as I know this is currently not possible.
The bundles are configured during app start/configured per the application domain.
A future version of ASP.NET may support this feature i,e using VirtualPathProvider.
Here is some discussion.
See also this SO question.
i'm not good in english, but if you mean you need to handle which CSS file load when you run any URL in your page, i can handle css file in a controler.
First, create a controller name : ResourceController
// CREATE PATH TO CSS FOLDER, I store in webconfig <add key="PathToStyles" value="/Content/MyTheme/" />
private static string _pathToStyles = ConfigurationManager.AppSettings["PathToStyles"];
public void Script(string resourceName)
{
if (!String.IsNullOrEmpty(resourceName))
{
var pathToResource = Server.MapPath(Path.Combine(_pathToScripts, resourceName));
TransmitFileWithHttpCachePolicy(pathToResource, ContentType.JavaScript.GetEnumDescription());
}
}
public void Style(string resourceName)
{
if (!String.IsNullOrEmpty(resourceName))
{
var pathToResource = Server.MapPath(Path.Combine(_pathToStyles, resourceName));
TransmitFileWithHttpCachePolicy(pathToResource, ContentType.Css.GetEnumDescription());
}
}
private void TransmitFileWithHttpCachePolicy(string pathToResource, string contentType)
{
//DO WHAT YOU WANT HERE;
Response.ContentType = contentType;
Response.TransmitFile(pathToResource);
}
//You can handle css or js file...
private enum ContentType
{
[EnumDescription("text/css")]
Css,
[EnumDescription("text/javascript")]
JavaScript
}
In file Global.asax.cs, make sure in application start medthod, in contain the route config
protected void Application_Start()
{
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
Go to routeConfig, add below map to this file (must be add in top of this file) :
routes.MapRoute(
name: "Resource",
url: "resource/{action}/{resourceName}",
defaults: new { controller = "Resource" }
);
Now, create a UrlHelperExtensions class, same path with webconfig file
public static class UrlHelperExtensions
{
public static string Style(this UrlHelper urlHelper, string resourceName)
{
return urlHelper.Content(String.Format("~/resource/style/{0}", resourceName));
}
}
And from now, you can define css file in your view like :
..."<"link href="#Url.Style("yourcss.css")" rel="stylesheet" type="text/css"
/>
Hope this help
First a little context. When you call Html.RenderPartial you send the View name, that view will be searched at locations specified by RazorViewEngine.PartialViewLocationFormats:
Html.RenderPartial("Post", item);
When you set the Layout property at Razor page, you canĀ“t just say the name, you need to specify the path. How can I just specify the name?
//Layout = "_Layout.cshtml";
Layout = "_Layout"; //Dont work
I need this because I overrided the RazorViewEngine.MasterLocationFormats.
Currently I am specifying the Master at controller:
return View("Index", "_Layout", model);
This works, but I prefer to do this at View.
There is no direct way to do it,
But we can write an HtmlExtension like "RenderPartial()" which will give complete layout path at runtime.
public static class HtmlExtensions
{
public static string ReadLayoutPath<T>(this HtmlHelper<T> html,string layoutName)
{
string[] layoutLocationFormats = new string[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
foreach (var item in layoutLocationFormats)
{
var controllerName= html.ViewContext.RouteData.Values["Controller"].ToString();
var resolveLayoutUrl = string.Format(item, layoutName, controllerName);
string fullLayoutPath = HostingEnvironment.IsHosted ? HostingEnvironment.MapPath(resolveLayoutUrl) : System.IO.Path.GetFullPath(resolveLayoutUrl);
if (File.Exists(fullLayoutPath))
return resolveLayoutUrl;
}
throw new Exception("Page not found.");
}
}
In the view we can use it as,
#{
Layout = Html.ReadLayoutPath("_Layout");
}
Can I ask why you are doing this or more specifically why are you returning a layout page from a controller? You are missing the point of master pages it seems.
You can't specify just the "name", you need to specify the path of the layout view so that it can in turn be applied to the view are rendering.
Layout = "~/SomeCustomLocation/SomeFolder/_Layout.cshtml"
I tried to make the ViewEngine use an additional path using:
base.MasterLocationFormats = new string[] {
"~/Views/AddedMaster.Master"
};
in the constructor of the ViewEngine. It works well for aspx and ascx(PartialViewLocationFormats, ViewLocationFormats).
I still have to supply the MasterPage in web.config or in the page declaration. But if I do, then this declaration is used, not the one in the ViewEngine.
If I use am empty MasterLocationFormats, no error is thrown. Is this not implemeted in RC1?
EDIT:
using:
return View("Index", "AddedMaster");
instead of
return View("Index");
in the Controller worked.
Your example isn't really complete, but I am going to guess that your block of code exists at the class level and not inside of a constructor method. The problem with that is that the base class (WebFormViewEngine) initializes the "location format" properties in a constructor, hence overriding your declaration;
public CustomViewEngine()
{
MasterLocationFormats = new string[] {
"~/Views/AddedMaster.Master"
};
}
If you want the hard-coded master to only kick in as a sort of last effort default, you can do something like this:
public CustomViewEngine()
{
MasterLocationFormats = new List<string>(MasterLocationFormats) {
"~/Views/AddedMaster.Master"
}.ToArray();
}