Mvc: Absolute Paths for Views not working

Created on 30 Jun 2016  路  14Comments  路  Source: aspnet/Mvc

I had a running RC1 project in which I use the following code to load a view

 public IViewComponentResult Invoke(DataTablesTagHelper model)
        {
            return View("~/ViewComponents/DataTables/View", model);
        }

I updated the project to Core 1 now an use net451 as Framework

"frameworks": {
    "net451": {}

After my update the view isnt found anymore any i get the following error:

The view 'Components/DataTablesVC/~/ViewComponents/DataTables/View' was not found. The following locations were searched:
 ~/ViewComponents/DataTables/View
 /Areas/Core/Views/Benutzer/Components/DataTablesVC/~/ViewComponents/DataTables/View.cshtml
 /Areas/Core/Views/Shared/Components/DataTablesVC/~/ViewComponents/DataTables/View.cshtml
 /Views/Shared/Components/DataTablesVC/~/ViewComponents/DataTables/View.cshtml

Have there been any changes to absolute paths of views?

by design

Most helpful comment

@markusvt see https://github.com/aspnet/Mvc/issues/3307#issuecomment-150004428. You'd have to change your view path to include the extension when using absolute paths i.e. ~/ViewComponents/DataTables/View.cshtml

All 14 comments

@markusvt see https://github.com/aspnet/Mvc/issues/3307#issuecomment-150004428. You'd have to change your view path to include the extension when using absolute paths i.e. ~/ViewComponents/DataTables/View.cshtml

Thank you very much! That saved my day :)

Another Problem just arrised:

i got the following class to render a view programmatically (I call that via ajax to change parts of a view based on certain selections of a user)

public class RazorViewToStringRenderer
    {
        private IRazorViewEngine _viewEngine;
        private ITempDataProvider _tempDataProvider;
        private IServiceProvider _serviceProvider;

        public RazorViewToStringRenderer(
            IRazorViewEngine viewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _viewEngine = viewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }

        public string RenderViewToString<TModel>(string name, TModel model, String prefix = "")
        {
            var actionContext = GetActionContext();
            var viewEngineResult = _viewEngine.FindView(actionContext, name, false);

            if (!viewEngineResult.Success)
            {
                throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
            }

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            {
                var viewDataDictionary = new ViewDataDictionary<TModel>(
                        metadataProvider: new EmptyModelMetadataProvider(),
                        modelState: new ModelStateDictionary())
                {
                    Model = model,
                };
                viewDataDictionary.TemplateInfo.HtmlFieldPrefix = prefix;

                var viewContext = new ViewContext(
                    actionContext,
                    view,
                    viewDataDictionary,
                    new TempDataDictionary(
                        actionContext.HttpContext,
                        _tempDataProvider),
                    output,
                    new HtmlHelperOptions());

                view.RenderAsync(viewContext).GetAwaiter().GetResult();

                return output.ToString();
            }
        }

        private ActionContext GetActionContext()
        {
            var httpContext = new DefaultHttpContext();
            httpContext.RequestServices = _serviceProvider;
            return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        }
    }

Before I updated to Core 1.0 it worked. Now it says

Couldn't find view '~/Areas/Company/Views/Adress/_MainAdressFormular.cshtml'

I already tried to remove the ~ or the .cshtml and made sure that view actually exists in that location.

If you print the SearchedLocations from viewEngineResult it might say what locations its trying to look up.

Alternatively, your project.json might be missing the Areas folder. Look for

  "publishOptions": {
    "include": [
      "Areas",
      "..."
    ]
  },

I did print the SearchedLocations and it was empty. I did the following:

 var viewEngineResult = _viewEngine.FindView(actionContext, name, false);
 if (!viewEngineResult.Success)
 {
       throw new InvalidOperationException(string.Format("Couldn't find view '{0}'. Searched: {1}", name, String.Join(",", viewEngineResult.SearchedLocations)));
 }

for the project.json file. Mine looks like this:

  "publishOptions": {
    "include": [
      "wwwroot",
      "Views",
      "Areas/**/Views",
      "appsettings.json",
      "web.config"
    ]
  },

If you haven't modified RazorViewEngineOptions.FileProviders, the view engine is going to look for the view at Path.Combine(IHostingEnvironment.ContentRoot, "Areas/Company/Views/Adress/_MainAdressFormular.cshtml"). Could you check if the view exists at this location?

No I didn't modify it. I just double-checked the location of that view and it is where it is supposed to be
image

Can I somehow configure the SearchedLocations of the view engine as a workaround? As at the moment it seems that it doesnt search anything (output of those locations was empty)

By the way: if i load the view normal in another Razor View, it works:

  @Html.Partial("~/Areas/Company/Views/Adress/_MainAdressFormular.cshtml", Model)

So it seems I miss something with the viewEngine in my RazorViewToStringRenderer

Edit: i spent some time looking into https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Razor/RazorViewEngine.cs
It seems that the getViewmethod explicitly says "return not fount if you get an absolute path" (line 222)

 if (!(IsApplicationRelativePath(viewPath) || IsRelativePath(viewPath)))
            {
                // Not a path this method can handle.
                return ViewEngineResult.NotFound(viewPath, Enumerable.Empty<string>());
            }

So if i for example switch my viewName to `Areas/Company/Views/Adress/_MainAdressFormular' there are some searched locations:

Searched: /Views//Areas/Company/Views/Adress/_MainAdressFormular.cshtml,/Views/Shared/Areas/Company/Views/Adress/_MainAdressFormular.cshtml

So I think there is actually something missing here for those absolute paths.

Okay after some trying out with a custom view engine and looking in existing code and tests i found the solution to my problem:
In my RenderViewToString I need to call

_viewEngine.GetView(executingFilePath: viewPath, viewPath: viewPath, isMainPage: false);

where viewPath contains the absolute path (with ~/...) to the view. So the "FindView" method iis not working for that. Also if i user anything but the absolute path for the viewPath parameter it is not working.
Actually I dont know why it works, as i saw in the RazerViewEngine that there is also a test, that the given path must not be absolute / app-relative.

@markusvt have a look at the doc comments for IViewEngine. That makes the distinction between GetView() and FindView() pretty clear. In short, GetView() starts with an absolute or relative path while FindView() starts with a view name and uses view locations, et cetera.

Thank you for your help :) With getView it works pretty well.

@dougbu thank you, using getview fixed it for me!

@dougbu
Would you rather recommend to use GetView() when dealing with Areas?
That would mean that FindView() doesn't work for Areas.

Thanks
Sven

@sven5 no, I would recommend using both GetView(...) and FindView(...) in all cases. The methods do different things but both are relevant (and work) when dealing with areas. GetView(...) handles paths relative to the current location while FindView(...) searches in the view locations.

The following fallback pattern is what we do within MVC:
c# var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, partialName, isMainPage: false); var getViewLocations = viewEngineResult.SearchedLocations; if (!viewEngineResult.Success) { viewEngineResult = _viewEngine.FindView(ViewContext, partialName, isMainPage: false); }

Was this page helpful?
0 / 5 - 0 ratings