On ASP.NET 5 a Component view must be in one of two places:
Views/NameOfControllerUsingComponent/Components/ComponentName/Default.cshtml
Views/Shared/Components/ComponentName/Default.cshtml
Is there a way to change this to:
Views/NameOfControllerUsingComponent/Components/ComponentName.cshtml
Views/Shared/Components/ComponentName.cshtml
So basically, remove the folder ComponentName and change the view name from Default.cshtml to ComponentName.cshtml.
For me it makes more sense ... Is it possible?
That convention is only applied if you create a view component that derives from the base ViewComponent
provided by the framework.
That class defines the View
helpers, which return a ViewViewComponentResult
:
public ViewViewComponentResult View<TModel>(string viewName, TModel model)
{
var viewData = new ViewDataDictionary<TModel>(ViewData, model);
return new ViewViewComponentResult
{
ViewEngine = ViewEngine,
ViewName = viewName,
ViewData = viewData
};
}
The ViewViewComponentResult
is where the conventions are defined:
private const string ViewPathFormat = "Components/{0}/{1}";
private const string DefaultViewName = "Default";
public async Task ExecuteAsync(ViewComponentContext context)
{
...
string qualifiedViewName;
if (!isNullOrEmptyViewName &&
(ViewName[0] == '~' || ViewName[0] == '/'))
{
// View name that was passed in is already a rooted path, the view engine will handle this.
qualifiedViewName = ViewName;
}
else
{
// This will produce a string like:
//
// Components/Cart/Default
//
// The view engine will combine this with other path info to search paths like:
//
// Views/Shared/Components/Cart/Default.cshtml
// Views/Home/Components/Cart/Default.cshtml
// Areas/Blog/Views/Shared/Components/Cart/Default.cshtml
//
// This supports a controller or area providing an override for component views.
var viewName = isNullOrEmptyViewName ? DefaultViewName : ViewName;
qualifiedViewName = string.Format(
CultureInfo.InvariantCulture,
ViewPathFormat,
context.ViewComponentDescriptor.ShortName,
viewName);
}
...
}
Notice that if you return from your view component the full path to a view as the view name, then the view component will use the specified view. Something like:
return View("~/Views/Shared/Components/ComponentName.cshtml")
Since there is no way to modify the conventions in ViewViewComponentResult
and your approach would only work for view components with a single view, you could build something using the root view paths approach:
ViewComponent
class extending the existing one.Add new helper methods or hide the existing View
methods to return a view using a full path:
public ViewViewComponentResult MyView<TModel>(TModel model)
{
var viewName = string.Format(
"~/Views/Shared/Components/{0}.cshtml",
this.ViewComponentContext.ViewComponentDescriptor.ShortName)
return View(viewName, model);
}
ViewComponent
instead of having to create your own class.Another alternative would be creating a class SingleViewViewComponent
copying the code for ViewComponent
but replacing the implementation of ViewViewComponentResult View<TModel>(string viewName, TModel model)
. Then when creating your view components, you would inherit from SingleViewViewComponent
instead of ViewComponent
.
Took me a weekend to finally find a way around this that didn't involve writing a custom ViewComponentResult.
in MVC .Net Core, you can add your own IViewLocationExpander
to the RazorViewEngineOptions in your startup.cs's ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
...
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new CustomLocationExpander());
});
}
This allows you to add custom Paths that are used in conjuction with the ViewLocationExpanderContext
ViewName ({0}), ControllerName ({1}).
The main issue is that you can't alter the context's values, which makes it seemingly impossible to change the default View Component's ViewName of Component/ComponentName/Default
Seemingly impossible
Here's the trick, the ExpandViewLocations is called with each View(), each time it doesn't have a fully qualified view path. Which means you can add custom logic. What I did was add a catch to detect ViewComponents in the PopulateValues
method, then added to the context.Values
dictionary, and then if that dictionary has those custom values, it will prepend to the Paths the list of paths that use my generated view name instead of the context.
It's fully reverse compatible, and shouldn't impact performance one bit.
public class CustomLocationExpander : IViewLocationExpander
{
private const string _CustomViewPath = "CustomViewPath";
private const string _CustomController = "CustomController";
public void PopulateValues(ViewLocationExpanderContext context)
{
Regex DefaultComponentDetector = new Regex(@"^((?:[Cc]omponents))+\/+([\w\.]+)\/+(.*)");
/*
* If successful,
* Group 0 = FullMatch (ex "Components/MyComponent/Default")
* Group 1 = Components (ex "Component")
* Group 2 = Component Name (ex "MyComponent")
* Group 3 = View Name (ex "Default")
* */
var DefaultComponentMatch = DefaultComponentDetector.Match(context.ViewName);
if (DefaultComponentMatch.Success)
{
// Will render Components/ComponentName as the new view name
context.Values.Add(_CustomViewPath, string.Format("{0}/{1}", DefaultComponentMatch.Groups[1].Value, DefaultComponentMatch.Groups[2].Value));
context.Values.Add(_CustomController, context.ControllerName);
}
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
/* Parameters:
* {2} - Area Name
* {1} - Controller Name
* {0} - View Name
* */
List<string> Paths = new List<string> {
// Default View Locations to support imported / legacy paths
"/Views/{1}/{0}.cshtml",
"/Views/Shared/{0}.cshtml",
// Adds Feature Folder Rendering
"/Features/{1}/{0}.cshtml",
"/Features/Shared/{0}.cshtml",
// Handles My Custom rendered views
"/{0}.cshtml"
};
// Add "Hard Coded" custom view paths to checks, along with the normal default view paths for backward compatibility
if (context.Values.ContainsKey(_CustomViewPath))
{
// Generate full View Paths with my custom View Name and Controller Name
var CombinedPaths = new List<string>(Paths.Select(x => string.Format(x, context.Values[_CustomViewPath], context.Values[_CustomController], "")));
// Add in original paths for backward compatibility
CombinedPaths.AddRange(Paths);
return CombinedPaths;
}
// Returns the normal view paths
return Paths;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With