Question

I want to change view locations at runtime based on current UI culture. How can I achieve this with default Web Form view engine?

Basically I want to know how implement with WebFormViewEngine something what is custom IDescriptorFilter in Spark.

Is there other view engine which gives me runtime control over view locations?


Edit: My URLs should looks following {lang}/{controller}/{action}/{id}. I don't need language dependent controllers and views are localized with resources. However few of the views will be different in some languages. So I need to tell view engine to looks to the language specific folder first.

Was it helpful?

Solution

A simple solution would be to, in your Appication_Start get hold of the appropriate ViewEngine from the ViewEngines.Engines collection and update its ViewLocationFormats array and PartialViewLocationFormats. No hackery: it's read/write by default.

protected void Application_Start()
{
    ...
    // Allow looking up views in ~/Features/ directory
    var razorEngine = ViewEngines.Engines.OfType<RazorViewEngine>().First();
    razorEngine.ViewLocationFormats = razorEngine.ViewLocationFormats.Concat(new string[] 
    { 
        "~/Features/{1}/{0}.cshtml"
    }).ToArray();
    ...
    // also: razorEngine.PartialViewLocationFormats if required
}

The default one for Razor looks like this:

ViewLocationFormats = new string[]
{
    "~/Views/{1}/{0}.cshtml",
    "~/Views/{1}/{0}.vbhtml",
    "~/Views/Shared/{0}.cshtml",
    "~/Views/Shared/{0}.vbhtml"
};

Note that you may want to update PartialViewLocationFormats also.

OTHER TIPS

VirtualPathProviderViewEngine.GetPathFromGeneralName must be changed to allow an additional parameter from the route. Its not public, that's why you have to copy GetPath, GetPathFromGeneralName, IsSpecificPath ...over to your own ViewEngine implementation.

You are right: this looks like a complete rewrite. I wished GetPathFromGeneralName was public.

using System.Web.Mvc;
using System;
using System.Web.Hosting;
using System.Globalization;
using System.Linq;

namespace MvcLocalization
{
    public class LocalizationWebFormViewEngine : WebFormViewEngine
    {
        private const string _cacheKeyFormat = ":ViewCacheEntry:{0}:{1}:{2}:{3}:";
        private const string _cacheKeyPrefix_Master = "Master";
        private const string _cacheKeyPrefix_Partial = "Partial";
        private const string _cacheKeyPrefix_View = "View";
        private static readonly string[] _emptyLocations = new string[0];

        public LocalizationWebFormViewEngine()
        {
            base.ViewLocationFormats = new string[] { 
                    "~/Views/{1}/{2}/{0}.aspx", 
                    "~/Views/{1}/{2}/{0}.ascx", 
                    "~/Views/Shared/{2}/{0}.aspx", 
                    "~/Views/Shared/{2}/{0}.ascx" ,
                     "~/Views/{1}/{0}.aspx", 
                    "~/Views/{1}/{0}.ascx", 
                    "~/Views/Shared/{0}.aspx", 
                    "~/Views/Shared/{0}.ascx" 

            };

        }

        private VirtualPathProvider _vpp;

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException("controllerContext");

            if (String.IsNullOrEmpty(viewName))
                throw new ArgumentException( "viewName");

            string[] viewLocationsSearched;
            string[] masterLocationsSearched;

            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            string viewPath = GetPath(controllerContext, ViewLocationFormats, "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched);
            string masterPath = GetPath(controllerContext, MasterLocationFormats, "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched);

            if (String.IsNullOrEmpty(viewPath) || (String.IsNullOrEmpty(masterPath) && !String.IsNullOrEmpty(masterName)))
            {
                 return new ViewEngineResult(viewLocationsSearched.Union(masterLocationsSearched));
            }

            return new ViewEngineResult(CreateView(controllerContext, viewPath, masterPath), this);
        }

        private string GetPath(ControllerContext controllerContext, string[] locations, string locationsPropertyName, string name, 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();

            bool nameRepresentsPath = IsSpecificPath(name);
            string cacheKey = CreateCacheKey(cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName);

            if (useCache)
            {
                string result = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, cacheKey);
                if (result != null)
                {
                    return result;
                }
            }

            return (nameRepresentsPath) ?
                GetPathFromSpecificName(controllerContext, name, cacheKey, ref searchedLocations) :
                GetPathFromGeneralName(controllerContext, locations, name, controllerName, cacheKey, ref searchedLocations);
        }

        private string GetPathFromGeneralName(ControllerContext controllerContext, string[] locations, string name, string controllerName, string cacheKey, ref string[] searchedLocations)
        {
            string result = String.Empty;
            searchedLocations = new string[locations.Length];
            string language = controllerContext.RouteData.Values["lang"].ToString();

            for (int i = 0; i < locations.Length; i++)
            {
                string virtualPath = String.Format(CultureInfo.InvariantCulture, locations[i], name, controllerName,language);

                if (FileExists(controllerContext, virtualPath))
                {
                    searchedLocations = _emptyLocations;
                    result = virtualPath;
                    ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
                    break;
                }

                searchedLocations[i] = virtualPath;
            }

            return result;
        }

        private string CreateCacheKey(string prefix, string name, string controllerName)
        {
            return String.Format(CultureInfo.InvariantCulture, _cacheKeyFormat,
                GetType().AssemblyQualifiedName, prefix, name, controllerName);
        }

        private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations)
        {
            string result = name;

            if (!FileExists(controllerContext, name))
            {
                result = String.Empty;
                searchedLocations = new[] { name };
            }

            ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, result);
            return result;
        }

        private static bool IsSpecificPath(string name)
        {
            char c = name[0];
            return (c == '~' || c == '/');
        }

    }
}

1) Extend the class from razor view engine

public class LocalizationWebFormViewEngine : RazorViewEngine

2) Add the partial location formats

public LocalizationWebFormViewEngine() 
{
    base.PartialViewLocationFormats = new string[] {
        "~/Views/{2}/{1}/{0}.cshtml", 
        "~/Views/{2}/{1}/{0}.aspx", 
        "~/Views/{2}/Shared/{0}.cshtml", 
        "~/Views/{2}/Shared/{0}.aspx"
    };

    base.ViewLocationFormats = new string[] {
        "~/Views/{2}/{1}/{0}.cshtml", 
        "~/Views/{2}/{1}/{0}.aspx", 
        "~/Views/{2}/Shared/{0}.cshtml", 
        "~/Views/{2}/Shared/{0}.aspx"
    };
}

3) Create the override method for partial view render

public override ViewEngineResult FindPartialView(ControllerContext controllerContext, String partialViewName, Boolean useCache)
{
    if (controllerContext == null)
    {
        throw new ArgumentNullException("controllerContext");
    }
    if (String.IsNullOrEmpty(partialViewName))
    {
        throw new ArgumentException("partialViewName");
    }

    string[] partialViewLocationsSearched;

    string controllerName = controllerContext.RouteData.GetRequiredString("controller");
    string partialPath = GetPath(controllerContext, PartialViewLocationFormats, "PartialViewLocationFormats", partialViewName, controllerName, _cacheKeyPrefix_Partial, useCache, out partialViewLocationsSearched);

    return new ViewEngineResult(CreatePartialView(controllerContext, partialPath), this);}
}

I believe that solution would be to create your own ViewEngine which inherits from WebFormViewEngine. In constructor, it should check current UI culture from current thread and add appropriate locations. Just don't forget to add it to your view engines.

This should look something like this:

public class ViewEngine : WebFormViewEngine
{
    public ViewEngine()
    {
        if (CultureIsX())
            ViewLocationFormats = new string[]{"route1/controller.aspx"};
        if (CultureIsY())
            ViewLocationFormats = new string[]{"route2/controller.aspx"};
    }
}

in global.asax:

ViewEngines.Engines.Add(new ViewEngine());

Below is a localized view engine without the rewrite.

In a nutshell, the engine will insert new locations into the view locations everytime a view is looked up. The engine will use the two character language to find the view. So if the current language is es (Spanish), it'll look for ~/Views/Home/Index.es.cshtml.

See code comments for more details.

A better approach would be to override the way view locations are parsed, but the methods are not overridable; maybe in ASP.NET MVC 5?

public class LocalizedViewEngine : RazorViewEngine
{
    private string[] _defaultViewLocationFormats;

    public LocalizedViewEngine()
        : base()
    {
        // Store the default locations which will be used to append
        // the localized view locations based on the thread Culture
        _defaultViewLocationFormats = base.ViewLocationFormats;
    }

    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        AppendLocalizedLocations();
        return base.FindPartialView(controllerContext, partialViewName, useCache:fase);
    }

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        AppendLocalizedLocations();
        returnbase.FindView(controllerContext, viewName, masterName, useCache:false);
    }

    private void AppendLocalizedLocations()
    {
        // Use language two letter name to identify the localized view
        string lang = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;

        // Localized views will be in the format "{action}.{lang}.cshtml"
        string localizedExtension = string.Format(".{0}.cshtml", lang);

        // Create an entry for views and layouts using localized extension
        string view =  "~/Views/{1}/{0}.cshtml".Replace(".cshtml", localizedExtension);
        string shared =  "~/Views/{1}/Shared/{0}".Replace(".cshtml", localizedExtension);

        // Create a copy of the default view locations to modify
        var list = _defaultViewLocationFormats.ToList();

        // Insert the new locations at the top of the list of locations
        // so they're used before non-localized views.
        list.Insert(0, shared);
        list.Insert(0, view);
        base.ViewLocationFormats = list.ToArray();
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top