108

I have the following layout for my mvc project:

  • /Controllers
    • /Demo
    • /Demo/DemoArea1Controller
    • /Demo/DemoArea2Controller
    • etc...
  • /Views
    • /Demo
    • /Demo/DemoArea1/Index.aspx
    • /Demo/DemoArea2/Index.aspx

However, when I have this for DemoArea1Controller:

public class DemoArea1Controller : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

I get the "The view 'index' or its master could not be found" error, with the usual search locations.

How can I specify that controllers in the "Demo" namespace search in the "Demo" view subfolder?

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Daniel Schaffer
  • 56,753
  • 31
  • 116
  • 165
  • Here is another sample of a simple ViewEngine from Rob Connery's MVC Commerce app: [View Engine Code](http://mvcsamples.codeplex.com/SourceControl/changeset/view/17126#286681) And the Global.asax.cs code to set the ViewEngine: [Global.asax.cs](http://mvcsamples.codeplex.com/SourceControl/changeset/view/17126#286569) Hope this helps. – Robert Dean Mar 11 '09 at 02:51

10 Answers10

123

You can easily extend the WebFormViewEngine to specify all the locations you want to look in:

public class CustomViewEngine : WebFormViewEngine
{
    public CustomViewEngine()
    {
        var viewLocations =  new[] {  
            "~/Views/{1}/{0}.aspx",  
            "~/Views/{1}/{0}.ascx",  
            "~/Views/Shared/{0}.aspx",  
            "~/Views/Shared/{0}.ascx",  
            "~/AnotherPath/Views/{0}.ascx"
            // etc
        };

        this.PartialViewLocationFormats = viewLocations;
        this.ViewLocationFormats = viewLocations;
    }
}

Make sure you remember to register the view engine by modifying the Application_Start method in your Global.asax.cs

protected void Application_Start()
{
    ViewEngines.Engines.Clear();
    ViewEngines.Engines.Add(new CustomViewEngine());
}
ahsteele
  • 26,243
  • 28
  • 134
  • 248
Sam Wessel
  • 8,830
  • 8
  • 40
  • 44
  • How can you access the path of a Master Page from a Nested Master page? As in setting the nested master page layout to search within the paths of the CustomViewEngine – Drahcir Dec 18 '12 at 16:27
  • 6
    Is it not better if we skip Clearing the already registered engines and just add the new one and viewLocations shall have only the new ones? – Prasanna Sep 01 '14 at 11:43
  • 4
    Implements without ViewEngines.Engines.Clear(); All work fine. If you want to use *.cshtml you must inherits from RazorViewEngine – KregHEk Feb 19 '15 at 11:40
  • 2
    is there any way we can link the "add view" and "go to view" options from the controllers to the new view locations? i am using visual studio 2012 – Neville Nazerane Nov 19 '16 at 00:39
  • As mentioned by @Prasanna, there is no need to clear the existing engines in order to add new locations, see [this answer](https://stackoverflow.com/questions/632964/can-i-specify-a-custom-location-to-search-for-views-in-asp-net-mvc/59915540#59915540) for more details. – Hooman Bahreini Jan 26 '20 at 04:28
52

Now in MVC 6 you can implement IViewLocationExpander interface without messing around with view engines:

public class MyViewLocationExpander : IViewLocationExpander
{
    public void PopulateValues(ViewLocationExpanderContext context) {}

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        return new[]
        {
            "/AnotherPath/Views/{1}/{0}.cshtml",
            "/AnotherPath/Views/Shared/{0}.cshtml"
        }; // add `.Union(viewLocations)` to add default locations
    }
}

where {0} is target view name, {1} - controller name and {2} - area name.

You can return your own list of locations, merge it with default viewLocations (.Union(viewLocations)) or just change them (viewLocations.Select(path => "/AnotherPath" + path)).

To register your custom view location expander in MVC, add next lines to ConfigureServices method in Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.ViewLocationExpanders.Add(new MyViewLocationExpander());
    });
}
OrangeKing89
  • 694
  • 6
  • 16
whyleee
  • 4,019
  • 1
  • 31
  • 33
  • 3
    I wish I could vote this up 10 votes. Is exactly what is needed in Asp.net 5 / MVC 6. Beautiful. Very useful in my case (and others) when you want to group areas into super areas for either larger sites or logical groupings. – drewid Nov 19 '15 at 07:08
  • The Startup.cs portion should be: services.Configure It goes in this method: public void ConfigureServices(IServiceCollection services) – OrangeKing89 Oct 20 '16 at 13:08
  • Agree with drewid in that I wish I could upvote this 10 times. Worked perfectly in .Net 5 and is much easier to implement and simpler to understand than any of the other solutions involving view engines. – dislexic Jun 22 '21 at 02:26
42

There's actually a lot easier method than hardcoding the paths into your constructor. Below is an example of extending the Razor engine to add new paths. One thing I'm not entirely sure about is whether the paths you add here will be cached:

public class ExtendedRazorViewEngine : RazorViewEngine
{
    public void AddViewLocationFormat(string paths)
    {
        List<string> existingPaths = new List<string>(ViewLocationFormats);
        existingPaths.Add(paths);

        ViewLocationFormats = existingPaths.ToArray();
    }

    public void AddPartialViewLocationFormat(string paths)
    {
        List<string> existingPaths = new List<string>(PartialViewLocationFormats);
        existingPaths.Add(paths);

        PartialViewLocationFormats = existingPaths.ToArray();
    }
}

And your Global.asax.cs

protected void Application_Start()
{
    ViewEngines.Engines.Clear();

    ExtendedRazorViewEngine engine = new ExtendedRazorViewEngine();
    engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.cshtml");
    engine.AddViewLocationFormat("~/MyThemes/{1}/{0}.vbhtml");

    // Add a shared location too, as the lines above are controller specific
    engine.AddPartialViewLocationFormat("~/MyThemes/{0}.cshtml");
    engine.AddPartialViewLocationFormat("~/MyThemes/{0}.vbhtml");

    ViewEngines.Engines.Add(engine);

    AreaRegistration.RegisterAllAreas();
    RegisterRoutes(RouteTable.Routes);
}

One thing to note: your custom location will need the ViewStart.cshtml file in its root.

Chris S
  • 64,770
  • 52
  • 221
  • 239
23

If you want just add new paths, you can add to the default view engines and spare some lines of code:

ViewEngines.Engines.Clear();
var razorEngine = new RazorViewEngine();
razorEngine.MasterLocationFormats = razorEngine.MasterLocationFormats
      .Concat(new[] { 
          "~/custom/path/{0}.cshtml" 
      }).ToArray();

razorEngine.PartialViewLocationFormats = razorEngine.PartialViewLocationFormats
      .Concat(new[] { 
          "~/custom/path/{1}/{0}.cshtml",   // {1} = controller name
          "~/custom/path/Shared/{0}.cshtml" 
      }).ToArray();

ViewEngines.Engines.Add(razorEngine);

The same applies to WebFormEngine

Marcelo De Zen
  • 9,439
  • 3
  • 37
  • 50
13

Instead of subclassing the RazorViewEngine, or replacing it outright, you can just alter existing RazorViewEngine's PartialViewLocationFormats property. This code goes in Application_Start:

System.Web.Mvc.RazorViewEngine rve = (RazorViewEngine)ViewEngines.Engines
  .Where(e=>e.GetType()==typeof(RazorViewEngine))
  .FirstOrDefault();

string[] additionalPartialViewLocations = new[] { 
  "~/Views/[YourCustomPathHere]"
};

if(rve!=null)
{
  rve.PartialViewLocationFormats = rve.PartialViewLocationFormats
    .Union( additionalPartialViewLocations )
    .ToArray();
}
Simon Giles
  • 776
  • 9
  • 10
  • 2
    This worked for me, with the exception that the razor engine type was 'FixedRazorViewEngine' instead of 'RazorViewEngine'. Also I throw an exception if the engine was not found since it prevents my application from succesfully initializing. – Rob Apr 05 '16 at 06:00
3

Note: for ASP.NET MVC 2 they have additional location paths you will need to set for views in 'Areas'.

 AreaViewLocationFormats
 AreaPartialViewLocationFormats
 AreaMasterLocationFormats

Creating a view engine for an Area is described on Phil's blog.

Note: This is for preview release 1 so is subject to change.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
3

Most of the answers here, clear the existing locations by calling ViewEngines.Engines.Clear() and then add them back in again... there is no need to do this.

We can simply add the new locations to the existing ones, as shown below:

// note that the base class is RazorViewEngine, NOT WebFormViewEngine
public class ExpandedViewEngine : RazorViewEngine
{
    public ExpandedViewEngine()
    {
        var customViewSubfolders = new[] 
        {
            // {1} is conroller name, {0} is action name
            "~/Areas/AreaName/Views/Subfolder1/{1}/{0}.cshtml",
            "~/Areas/AreaName/Views/Subfolder1/Shared/{0}.cshtml"
        };

        var customPartialViewSubfolders = new[] 
        {
            "~/Areas/MyAreaName/Views/Subfolder1/{1}/Partials/{0}.cshtml",
            "~/Areas/MyAreaName/Views/Subfolder1/Shared/Partials/{0}.cshtml"
        };

        ViewLocationFormats = ViewLocationFormats.Union(customViewSubfolders).ToArray();
        PartialViewLocationFormats = PartialViewLocationFormats.Union(customPartialViewSubfolders).ToArray();

        // use the following if you want to extend the master locations
        // MasterLocationFormats = MasterLocationFormats.Union(new[] { "new master location" }).ToArray();   
    }
}

Now you can configure your project to use the above RazorViewEngine in Global.asax:

protected void Application_Start()
{
    ViewEngines.Engines.Add(new ExpandedViewEngine());
    // more configurations
}

See this tutoral for more info.

Hooman Bahreini
  • 14,480
  • 11
  • 70
  • 137
3

Last I checked, this requires you to build your own ViewEngine. I don't know if they made it easier in RC1 though.

The basic approach I used before the first RC was, in my own ViewEngine, to split the namespace of the controller and look for folders which matched the parts.

EDIT:

Went back and found the code. Here's the general idea.

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
{
    string ns = controllerContext.Controller.GetType().Namespace;
    string controller = controllerContext.Controller.GetType().Name.Replace("Controller", "");

    //try to find the view
    string rel = "~/Views/" +
        (
            ns == baseControllerNamespace ? "" :
            ns.Substring(baseControllerNamespace.Length + 1).Replace(".", "/") + "/"
        )
        + controller;
    string[] pathsToSearch = new string[]{
        rel+"/"+viewName+".aspx",
        rel+"/"+viewName+".ascx"
    };

    string viewPath = null;
    foreach (var path in pathsToSearch)
    {
        if (this.VirtualPathProvider.FileExists(path))
        {
            viewPath = path;
            break;
        }
    }

    if (viewPath != null)
    {
        string masterPath = null;

        //try find the master
        if (!string.IsNullOrEmpty(masterName))
        {

            string[] masterPathsToSearch = new string[]{
                rel+"/"+masterName+".master",
                "~/Views/"+ controller +"/"+ masterName+".master",
                "~/Views/Shared/"+ masterName+".master"
            };


            foreach (var path in masterPathsToSearch)
            {
                if (this.VirtualPathProvider.FileExists(path))
                {
                    masterPath = path;
                    break;
                }
            }
        }

        if (string.IsNullOrEmpty(masterName) || masterPath != null)
        {
            return new ViewEngineResult(
                this.CreateView(controllerContext, viewPath, masterPath), this);
        }
    }

    //try default implementation
    var result = base.FindView(controllerContext, viewName, masterName);
    if (result.View == null)
    {
        //add the location searched
        return new ViewEngineResult(pathsToSearch);
    }
    return result;
}
Joel
  • 19,175
  • 2
  • 63
  • 83
  • 1
    It's actually much easier. Subclass WebFormsViewEngine and then just add to the array of paths it already searches in your constructor. – Craig Stuntz Mar 11 '09 at 02:52
  • Good to know. The last time I needed to modify that collection, it wasn't possible in that manner. – Joel Mar 11 '09 at 13:57
  • Worth mentioning that you need to set the "baseControllerNamespace" variable to your base controller namespace (e.g. "Project.Controllers"), but otherwise did exactly what I needed, 7 years after being posted. – prototype14 Oct 31 '16 at 22:38
3

Try something like this:

private static void RegisterViewEngines(ICollection<IViewEngine> engines)
{
    engines.Add(new WebFormViewEngine
    {
        MasterLocationFormats = new[] {"~/App/Views/Admin/{0}.master"},
        PartialViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.ascx"},
        ViewLocationFormats = new[] {"~/App/Views/Admin//{1}/{0}.aspx"}
    });
}

protected void Application_Start()
{
    RegisterViewEngines(ViewEngines.Engines);
}
Vitaliy Ulantikov
  • 10,157
  • 3
  • 61
  • 54
1

I did it this way in MVC 5. I didn't want to clear the default locations.

Helper Class:

namespace ConKit.Helpers
{
    public static class AppStartHelper
    {
        public static void AddConKitViewLocations()
        {
            // get engine
            RazorViewEngine engine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault();
            if (engine == null)
            {
                return;
            }

            // extend view locations
            engine.ViewLocationFormats =
                engine.ViewLocationFormats.Concat(new string[] {
                    "~/Views/ConKit/{1}/{0}.cshtml",
                    "~/Views/ConKit/{0}.cshtml"
                }).ToArray();

            // extend partial view locations
            engine.PartialViewLocationFormats =
                engine.PartialViewLocationFormats.Concat(new string[] {
                    "~/Views/ConKit/{0}.cshtml"
                }).ToArray();
        }
    }
}

And then in Application_Start:

// Add ConKit View locations
ConKit.Helpers.AppStartHelper.AddConKitViewLocations();