ASP.NET Core MVC View Component search path
Asked Answered
A

2

12

In the documentation here: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-2.2

The runtime searches for the view in the following paths:

/Views/{Controller Name}/Components/{View Component Name}/{View Name}
/Views/Shared/Components/{View Component Name}/{View Name}
/Pages/Shared/Components/{View Component Name}/{View Name}

How can I add another path here?

I'd like to have my view components with their respective controllers in one project folder named components like this.

/Components/{View Component Name}/{View Name}

My motivation:

I find out my view components have their own JS and CSS files. I have all JS bundled and minimized in one site.min.js and all CSS bundled and minimized in their site.min.css. The JS are always something like $(function() { ... }) and the CSS are always written in a way that order does not matter so bundling all without knowing the order is not a problem.

Some of those view components have javascripts which change their state on server e.g. AJAX call to a controller's action which returns some JSON or the whole view component's HTML.

Since Controllers are just a C# classes they can be in any folder but it feels stupid to move the controller with the relevant AJAX action to the "Views" folder.

In the end I'd like to have a "component" (not really a "view component" only) like this:

/Components/SomeViewComponent/Default.cshtml
/Components/SomeViewComponent/SomeViewComponentController.cs
/Components/SomeViewComponent/SomeViewComponent.cs
/Components/SomeViewComponent/SomeViewComponent.css
/Components/SomeViewComponent/SomeViewComponent.js
/Components/SomeViewComponent/SomeViewComponent.en.resx
/Components/SomeViewComponent/SomeViewComponent.cs-CZ.resx
Alber answered 17/4, 2019 at 6:24 Comment(0)
A
15

So after an hour digging into the aspnetcore repository, I found the component's search path is hardcoded and then combined with normal view search paths.

// {0} is the component name, {1} is the view name.
private const string ViewPathFormat = "Components/{0}/{1}";

This path is then sent into the view engine

result = viewEngine.FindView(viewContext, qualifiedViewName, isMainPage: false);

The view engine then produces the full path, using the configurable view paths.

  • Views/Shared/Components/Cart/Default.cshtml
  • Views/Home/Components/Cart/Default.cshtml
  • Areas/Blog/Views/Shared/Components/Cart/Default.cshtml

If you want to place your view components into a root folder named "Components" as I wanted, you can do something like this.

services.Configure<RazorViewEngineOptions>(o =>
{
    // {2} is area, {1} is controller,{0} is the action
    // the component's path "Components/{ViewComponentName}/{ViewComponentViewName}" is in the action {0}
    o.ViewLocationFormats.Add("/{0}" + RazorViewEngine.ViewExtension);        
});

That's kind of ugly in my opinion. But it works.

You can also write your own expander like this.

namespace TestMvc
{
    using Microsoft.AspNetCore.Mvc.Razor;
    using System.Collections.Generic;

    public class ComponentViewLocationExpander : IViewLocationExpander
    {
        public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
        {
            // this also feels ugly
            // I could not find another way to detect
            // whether the view name is related to a component
            // but it's somewhat better than adding the path globally
            if (context.ViewName.StartsWith("Components"))
                return new string[] { "/{0}" + RazorViewEngine.ViewExtension };

            return viewLocations;
        }

        public void PopulateValues(ViewLocationExpanderContext context) {}
    }
}

And in Startup.cs

services.Configure<RazorViewEngineOptions>(o =>
{
    o.ViewLocationExpanders.Add(new ComponentViewLocationExpander());   
});
Alber answered 17/4, 2019 at 7:46 Comment(1)
I was facing the same problem, i have used your solution, but it didint work with areas, take a look at my question : #66783105Eulaheulalee
C
3

You can add additional view location formats to RazorViewEngineOptions. As an example, to add a path that satisfies your requirement, you can use something like this:

services
    .AddMvc()
    .AddRazorOptions(o =>
    {
        // /Components/{View Component Name}/{View Name}.cshtml
        o.ViewLocationFormats.Add("/{0}.cshtml");
        o.PageViewLocationFormats.Add("/{0}.cshtml");
    });

As can be seen above, there are different properties for views (when using controllers and actions) and page views (when using Razor Pages). There's also a property for areas, but I've left that out in this example to keep it marginally shorter.

The downside to this approach is that the view location formats do not apply only to view components. For example, when looking for the Index view inside of Home, Razor will now also look for Index.cshtml sitting at the root of the project. This might be fine because it's the last searched location and I expect you're not going to have any views sitting at the root of your project, but it's certainly worth being aware of.

Corsiglia answered 17/4, 2019 at 7:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.