Как указать местоположение представления в asp.net core mvc при использовании пользовательских местоположений?

Скажем, у меня есть контроллер, который использует маршрутизацию, основанную на атрибутах, для обработки запрошенного URL-адреса/admin/product следующим образом:

[Route("admin/[controller]")]        
public class ProductController: Controller {

    // GET: /admin/product
    [Route("")]
    public IActionResult Index() {

        return View();
    }
}

Теперь позвольте сказать, что я хочу, чтобы мои представления были организованы в структуре папок, которая приблизительно отражает пути URL, к которым они относятся. Поэтому я хотел бы, чтобы представление для этого контроллера было расположено здесь:

/Views/Admin/Product.cshtml

Чтобы идти дальше, если бы у меня был такой контроллер:

[Route("admin/marketing/[controller]")]        
public class PromoCodeListController: Controller {

    // GET: /admin/marketing/promocodelist
    [Route("")]
    public IActionResult Index() {

        return View();
    }
}

Я хотел бы, чтобы фреймворк автоматически просматривал его здесь:

Views/Admin/Marketing/PromoCodeList.cshtml

В идеале подход для информирования о структуре местоположения представления будет работать в общем режиме на основе информации о маршрутах, основанной на атрибутах, независимо от того, сколько сегментов URL-адресов задействовано (т.е. насколько глубоко оно вложенное).

Как я могу инструктировать структуру Core MVC (я в настоящее время использую RC1), чтобы искать представление контроллера в таком местоположении?

Ответ 1

Вы можете расширить местоположения, где механизм представления ищет представления, внедрив расширитель местоположения представления. Вот пример кода для демонстрации подхода:

public class ViewLocationExpander: IViewLocationExpander {

    /// <summary>
    /// Used to specify the locations that the view engine should search to 
    /// locate views.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="viewLocations"></param>
    /// <returns></returns>
    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
        //{2} is area, {1} is controller,{0} is the action
        string[] locations = new string[] { "/Views/{2}/{1}/{0}.cshtml"};
        return locations.Union(viewLocations);          //Add mvc default locations after ours
    }


    public void PopulateValues(ViewLocationExpanderContext context) {
        context.Values["customviewlocation"] = nameof(ViewLocationExpander);
    }
}

Затем в ConfigureServices(IServiceCollection services) в файле startup.cs добавьте следующий код, чтобы зарегистрировать его в контейнере IoC. Сделайте это сразу после services.AddMvc();

services.Configure<RazorViewEngineOptions>(options => {
        options.ViewLocationExpanders.Add(new ViewLocationExpander());
    });

Теперь у вас есть способ добавить любую пользовательскую структуру каталогов, которую вы хотите, в список мест, которые механизм просмотра ищет для представлений и частичных представлений. Просто добавьте его в string[] locations string[]. Кроме того, вы можете поместить файл _ViewImports.cshtml в тот же каталог или любой родительский каталог, и он будет найден и объединен с вашими представлениями, расположенными в этой новой структуре каталогов.

Обновить:
Хорошая особенность этого подхода заключается в том, что он обеспечивает большую гибкость, чем подход, который был позже представлен в ASP.NET Core 2 (спасибо @BrianMacKay за документирование нового подхода). Так, например, этот подход ViewLocationExpander позволяет не только указывать иерархию путей для поиска видов и областей, но также для макетов и компонентов видов. Также у вас есть доступ к полному ActionContext чтобы определить, каким может быть соответствующий маршрут. Это обеспечивает большую гибкость и мощность. Так, например, если вы хотите определить подходящее местоположение представления, оценивая путь текущего запроса, вы можете получить доступ к пути текущего запроса через context.ActionContext.HttpContext.Request.Path.

Ответ 2

Хорошие новости... В ASP.NET Core 2. * вам больше не нужен пользовательский ViewEngine или даже ExpandViewLocations.

Использование пакета OdeToCode.AddFeatureFolders

Это самый простой способ... У К. Скотта Аллена есть пакет nuget для вас в OdeToCode.AddFeatureFolders, который является чистым и включает дополнительную поддержку областей. Github: https://github.com/OdeToCode/AddFeatureFolders

Установите пакет, и это так просто, как:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
                .AddFeatureFolders();

        ...
    }

    ...
}  

DIY

Используйте это, если вам нужен очень точный контроль над структурой вашей папки, или если вам не разрешено/вы не хотите получать зависимость по какой-либо причине. Это также довольно легко, хотя, возможно, более загромождено, чем пакет nuget выше:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
         ...

         services.Configure<RazorViewEngineOptions>(o =>
         {
             // {2} is area, {1} is controller,{0} is the action    
             o.ViewLocationFormats.Clear(); 
             o.ViewLocationFormats.Add("/Controllers/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
             o.ViewLocationFormats.Add("/Controllers/Shared/Views/{0}" + RazorViewEngine.ViewExtension);

             // Untested. You could remove this if you don't care about areas.
             o.AreaViewLocationFormats.Clear();
             o.AreaViewLocationFormats.Add("/Areas/{2}/Controllers/{1}/Views/{0}" + RazorViewEngine.ViewExtension);
             o.AreaViewLocationFormats.Add("/Areas/{2}/Controllers/Shared/Views/{0}" + RazorViewEngine.ViewExtension);
             o.AreaViewLocationFormats.Add("/Areas/Shared/Views/{0}" + RazorViewEngine.ViewExtension);
        });

        ...         
    }

...
}

И это оно! Никаких специальных классов не требуется.

Работа с Решарпер/Райдер

Бонусный совет: если вы используете ReSharper, вы можете заметить, что в некоторых местах ReSharper не может найти ваши взгляды и выдает раздражающие предупреждения. Чтобы обойти это, вытяните пакет Resharper.Annotations и в свой файл startup.cs (или где-либо еще) добавьте один из этих атрибутов для каждого из ваших мест просмотра:

[assembly: AspMvcViewLocationFormat("/Controllers/{1}/Views/{0}.cshtml")]
[assembly: AspMvcViewLocationFormat("/Controllers/Shared/Views/{0}.cshtml")]

[assembly: AspMvcViewLocationFormat("/Areas/{2}/Controllers/{1}/Views/{0}.cshtml")]
[assembly: AspMvcViewLocationFormat("/Controllers/Shared/Views/{0}.cshtml")]

Надеюсь, это сэкономит некоторым людям часы разочарования, которые я только что пережил. :)

Ответ 3

В ядре .net вы можете указать весь путь к представлению.

return View("~/Views/booking/checkout.cshtml", checkoutRequest);

Ответ 4

Для этого вам понадобится пользовательский RazorviewEngine.

Во-первых, двигатель:

public class CustomEngine : RazorViewEngine
{
    private readonly string[] _customAreaFormats = new string[]
    {
        "/Views/{2}/{1}/{0}.cshtml"
    };

    public CustomEngine(
        IRazorPageFactory pageFactory,
        IRazorViewFactory viewFactory,
        IOptions<RazorViewEngineOptions> optionsAccessor,
        IViewLocationCache viewLocationCache)
        : base(pageFactory, viewFactory, optionsAccessor, viewLocationCache)
    {
    }

    public override IEnumerable<string> AreaViewLocationFormats =>
        _customAreaFormats.Concat(base.AreaViewLocationFormats);
}

Это создаст дополнительный формат области, который соответствует варианту использования {areaName}/{controller}/{view}.

Во-вторых, зарегистрируйте двигатель в методе ConfigureServices класса Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Add custom engine (must be BEFORE services.AddMvc() call)
    services.AddSingleton<IRazorViewEngine, CustomEngine>();

    // Add framework services.
    services.AddMvc();
}

В-третьих, добавьте маршрутизацию области к вашим маршрутам MVC в методе Configure:

app.UseMvc(routes =>
{
    // add area routes
    routes.MapRoute(name: "areaRoute",
        template: "{area:exists}/{controller}/{action}",
        defaults: new { controller = "Home", action = "Index" });

    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Наконец, измените класс ProductController на использование AreaAttribute:

[Area("admin")]
public class ProductController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

Теперь ваша структура приложения может выглядеть так:

образец структуры проекта

Ответ 5

Так что после копания, я думаю, я нашел проблему в другом стеке потока. У меня была та же проблема, и после копирования в файл ViewImports из раздела, не относящегося к области, ссылки начали функционировать, как и ожидалось.
Как видно здесь: Asp.Net Core 2.0 MVC помощник тега привязки не работает
Другое решение было копировать на уровне представления:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers