5

I'm looking to override the 'ViewEngine' for MVC5 in a way that first, it find my pages.. which already i failed.

Second, It only operate on a single Area {root}/{area}/{controller}/{action}/{etc.}

as so far as i googled it, i found several topic and answers, but they didn't fit my need. so i rather to ask it here, maybe i'm wrong with something...

public class CustomAreaViewEngine:RazorViewEngine
{
    public CustomAreaViewEngine()
    {
        var viewLocations = new[]
        {
            "~/App/pages/{1}/{0}.cshtml",
            "~/App/pages/{1}/{0}.vbhtml"
        };
        AreaMasterLocationFormats = viewLocations;
        AreaPartialViewLocationFormats = viewLocations;
        AreaViewLocationFormats = viewLocations;
    }

    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {

        var viewEngineResult = base.FindPartialView(controllerContext, partialViewName, useCache);
        return viewEngineResult;
    }

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
        controllerContext.RouteData.Values["controller"] = controllerContext.RouteData.Values["controller"].ToString().ToLower();

        var viewEngineResult = base.FindView(controllerContext, viewName, masterName, useCache);
        return viewEngineResult;
    }
}

The method 'FindView' returns empty [first problem]

Global Config:

AreaRegistration.RegisterAllAreas();
ViewEngines.Engines.Add(new CustomAreaViewEngine()); // Look View Inside App/Pages/{1}/{0}.cshtml/.vbhtml

My Hierarchy

Root
--/App
--/--/pages
--/--/shared (but not that shared)
--/...
--/Area
--/--/View
--/--/--/Controller
--/--/--/View(MVC BASED HIERARCHY, I Don't want to use in most case, and redirect to App)
--/--/--/...
--/--/...
--/Controller
--/View
--/--/...(MVC BASED HIERARCHY)
--/...

EDITS:


EDIT1:


Changes i did due to @James Ellis-Jones answers:

Images: My Route Config: My Route Config Area Provided Route Config: Area Provided Route Config Global Config: Global Config My View Engine: My View Engine

still when i use http://localhost:1422/view/home/index i receive an error, which exist in my other home (View, related to the main controller, not the area controller.) it bring a wrong file.

Another Issue I Figured Out, Which Didn't Worked Too

My namespaces was wrong in last edit, and i changed them, but it didn't worked out too.

namespaces: new[] { "RavisHSB.Areas.View.Controllers" }

EDIT2:


Changes i did due to @CarlosFernández answers:

I add ViewEngines.Engines.Clear(); it somehow went one step ahead. but still doesn't work.

New Global.aspx: My Global.aspx And i face this new error: The new Error

Hassan Faghihi
  • 1,888
  • 1
  • 37
  • 55
  • Hi, one thing about your custom area view engine is that it only alters the view paths for area requests: therefore from what I remember the controller looking for the view has to be have been reached via a route registered in an AreaRegistrationContext or you have to manually add a DataToken to the RouteData with the index "area" and the name of the area (any one in this case) as a value – James Ellis-Jones Apr 23 '16 at 17:57
  • @JamesEllis-Jones i don't know how exactly it work, and i don't know the dataToken, BTW msdn doesn't say much too...https://msdn.microsoft.com/en-us/library/system.web.routing.routedata.datatokens(v=vs.110).aspx . ignoring my code, how should i implement it. i want everything that address `/View (it's my Area name, for my angular based app)/controller(same to my page folder)/Action`, to be parsed like this: `~/App/pages/{same folder as controller name(start with small cap)}/{action name(small cap again)}.cshtml` . – Hassan Faghihi Apr 24 '16 at 04:50
  • 1
    Have you tried clearing the viewEngines before add your CustomAreaViewEngine? Just put this before adding it: ViewEngines.Engines.Clear(); – Carlos Fernández Apr 26 '16 at 14:00
  • @CarlosFernández No, because i want my main controller to search the main application too, but thinking about @@JamesEllis-Jones in his last comment on his own post, i think i give it a try – Hassan Faghihi Apr 27 '16 at 07:47
  • @deadManN how many areas do you have? – Mike Apr 29 '16 at 11:35

2 Answers2

2

Try to customize this code for your engine

Update #1

 private static readonly List<string> EmptyLocations;

    public MyEngine()
    {


        ViewLocationFormats = new[]
            {
                "~/App/Pages/{1}/{0}.cshtml",
                "~/App/Pages/{1}/{0}.vbhtml"
            };


    }
    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        try
        {
            return System.IO.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)
    {
        var strArray = new List<string>();
        var strArray2 = new List<string>();


        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }
        if (string.IsNullOrEmpty(viewName))
        {
            throw new ArgumentException("viewName must be specified.", "viewName");
        }


        var controllerName = controllerContext.RouteData.GetRequiredString("controller");
        var viewPath = "";

        var viewLocation = ViewLocationFormats;
        var masterLocation = MasterLocationFormats;
        var area = "";
        if (controllerContext.RouteData.DataTokens.Keys.Any(x => x.ToLower() == "area"))
        {
            area = controllerContext.RouteData.DataTokens.FirstOrDefault(x => x.Key.ToLower() == "area").Value.ToString();

            viewLocation = AreaViewLocationFormats;
            masterLocation = AreaMasterLocationFormats;
        }

        viewPath = GetPath(controllerContext, viewLocation, area, viewName, controllerName, "TubaSite_View", useCache, strArray);



        var masterPath = GetPath(controllerContext, masterLocation, area, masterName, controllerName, "TubaSite_Master", useCache, strArray2);

        if (!string.IsNullOrEmpty(viewPath) && (!string.IsNullOrEmpty(masterPath) || string.IsNullOrEmpty(masterName)))
        {
            return new ViewEngineResult(this.CreateView(controllerContext, viewPath, masterPath), this);
        }
        if (string.IsNullOrEmpty(viewPath))
        {
            throw new Exception(String.Format("Page Not Found - {0} {1}", masterName, masterPath));
        }
        return new ViewEngineResult(strArray.Union<string>(strArray2));
    }
    public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
    {
        var strArray = new List<string>();
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }
        if (string.IsNullOrEmpty(partialViewName))
        {
            throw new ArgumentException("partialViewName must be specified.", "partialViewName");
        }



        var requiredString = controllerContext.RouteData.GetRequiredString("controller");

        var partialViewLocation = PartialViewLocationFormats;
        var area = "";
        if (controllerContext.RouteData.DataTokens.Keys.Any(x => x.ToLower() == "area"))
        {
            area = controllerContext.RouteData.DataTokens.FirstOrDefault(x => x.Key.ToLower() == "area").Value.ToString();
            partialViewLocation = AreaPartialViewLocationFormats.Union(PartialViewLocationFormats).ToArray();
        }

        var partialViewPath = "";

        partialViewPath = GetPath(controllerContext, partialViewLocation, area, partialViewName, requiredString, "TubaSite_Partial", false, strArray);

        return string.IsNullOrEmpty(partialViewPath) ? new ViewEngineResult(strArray) : new ViewEngineResult(CreatePartialView(controllerContext, partialViewPath), this);
    }


    private string GetPath(ControllerContext controllerContext, string[] locations, string area,
            string name, string controllerName,
            string cacheKeyPrefix, bool useCache, List<string> searchedLocations)
    {
        searchedLocations = EmptyLocations;
        if (string.IsNullOrEmpty(name))
        {
            return string.Empty;
        }

        if ((locations == null) || (locations.Length == 0))
        {
            throw new InvalidOperationException("Path not found.");
        }

        var flag = IsSpecificPath(name);
        var key = CreateCacheKey(cacheKeyPrefix, name, flag ? string.Empty : controllerName);
        if (useCache)
        {
            var viewLocation = ViewLocationCache.GetViewLocation(controllerContext.HttpContext, key);
            if (viewLocation != null)
            {
                return viewLocation;
            }
        }
        if (!flag)
        {
            return
                GetPathFromGeneralName(controllerContext, locations, area, name, controllerName, key,
                    searchedLocations);


        }
        return GetPathFromSpecificName(controllerContext, name, key, 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)
    {
        return string.Format(CultureInfo.InvariantCulture,
                ":ViewCacheEntry:{0}:{1}:{2}:{3}", GetType().AssemblyQualifiedName, prefix, name, controllerName);
    }

    private string GetPathFromGeneralName(ControllerContext controllerContext, IEnumerable<string> locations, string area, string name,
            string controllerName, string cacheKey, List<string> searchedLocations)
    {
        if (locations == null) throw new ArgumentNullException("locations");
        if (searchedLocations == null) searchedLocations = new List<string>();

        var virtualPath = string.Empty;
        var locationData =
            locations.Select(
                t =>
                string.Format(CultureInfo.InvariantCulture, t, new object[] { name, controllerName })).ToList();
        foreach (var str2 in locationData)
        {
            if (FileExists(controllerContext, str2))
            {
                virtualPath = str2;
                ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
                return virtualPath;
            }
            searchedLocations.Add(str2);
        }
        return virtualPath;
    }

    private string GetPathFromSpecificName(ControllerContext controllerContext, string name, string cacheKey, List<string> searchedLocations)
    {
        var virtualPath = name;
        if (!FileExists(controllerContext, name))
        {
            virtualPath = string.Empty;
            searchedLocations = new List<string>() { name };
        }
        ViewLocationCache.InsertViewLocation(controllerContext.HttpContext, cacheKey, virtualPath);
        return virtualPath;
    }
}

and also for the last error you've get you can use this config

<configSections>

    <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
      <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
      <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
    </sectionGroup>

  </configSections>

  <system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Routing" />
        <add namespace="System.Web.Optimization" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>
Mahdi Farhani
  • 964
  • 1
  • 9
  • 22
  • too complex... BTW, FYI, if by any chance you can simplify it, i only have standard parameters, like Area, Controller, and Action. and only for these three: AreaViewLocationFormats, AreaMasterLocationFormats, AreaPartialViewLocationFormats, i want to parse `~/View/{controller}/{action}` to `~/App/pages/{controller(FirstLetterSmallCap)}/{action(FirstLetterSmallCap)}`, View is a specific area name, i mean `when {area} == "View" then use this path, else don't. BTW today is last day for bounty – Hassan Faghihi Apr 30 '16 at 05:03
  • Let me add, I already found a way to redirect my pages to the page as you can see in the error at the end of my page, but i kinda get the error that says my view must drived from WebViewPage. which is configured in area/view/views/web.config file `` – Hassan Faghihi Apr 30 '16 at 05:15
  • Please Check my update, I've simplified the engine, and post a configuration for the last error. I hope it will be useful for your – Mahdi Farhani Apr 30 '16 at 06:17
  • BTW, I set the configuration in Main web.config – Mahdi Farhani Apr 30 '16 at 06:19
  • Since you provide complete answer i think you deserve the award. BTW, i made it to work, by moving the web.config file from Views folder, to App folder, as both of them act as {controller} part of the path, or at least that's what i thought. – Hassan Faghihi Apr 30 '16 at 06:58
1

Try this: in RouteConfig.cs where you are setting up the route which links to a controller which you want to find the custom view location, add this:

var route = routes.MapRoute(<route params here>);
route.DataTokens["area"] = "AreaName";

This will tell MVC that when you follow this route, you are going into area 'AreaName'. Then it will subsequently look for views in that area. Your custom ViewEngine will only affect view locations when MVC is looking for them in some area. Otherwise it won't have any effect, as the location format lists for areas are the only ones it overrides.

James Ellis-Jones
  • 3,042
  • 21
  • 13
  • i don't want to just register an area, i want to tell it to look for for files in a specific location. otherwise, these are provided: `AreaRegistration.RegisterAllAreas();` and then each area came with : `public class ViewAreaRegistration : AreaRegistration { public override string AreaName {get{return "View";}} public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "View_default", "View/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional }, namespaces: new []{"RavisHSB.Areas.View.Controllers"} );}}` – Hassan Faghihi Apr 24 '16 at 10:52
  • BTW, i don't want to route a URL to a MVC controller, but to Route to a directory after the controller action called. like when you say, `public ActionResult Index() { return View(); }`, this return View bring the file from the pattern that i defined, not from `~/{Area}/{Controller}/{Action}.cshtml` – Hassan Faghihi Apr 24 '16 at 11:15
  • I know you don't want to register an area, but you need to do what I said for your view engine to do anything different from the standard view engine as at the moment it only changes the view search paths for when MVC is looking for a view in an area – James Ellis-Jones Apr 26 '16 at 07:43
  • what about this one that is defined side by Area? `public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "View_default", "View/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional }, namespaces: new []{"RavisHSB.Areas.View.Controllers"} ); }` – Hassan Faghihi Apr 26 '16 at 12:30
  • look my post in a minutes, i will write done what i did, and it still failed. – Hassan Faghihi Apr 26 '16 at 12:36