ASP.NET Core MVC View Component search path - c#

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

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());
});

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.

Related

ViewComponent: Do I need both the view and the component?

I have a very simple view component. It is located in my ASP.NET Core project under /Views/Shared/Components/DisclosureCard. It currently consists of the files Default.cshtml and DisclosureCardViewComponent.cs, where the latter looks like this:
public class DisclosureCardViewComponent : ViewComponent
{
public IViewComponentResult Invoke(DisclosureCardViewModel disclosureCardViewModel) =>
View(disclosureCardViewModel);
}
This seems like an unnecessary "reelay" to me. Is there any way I can get rid of DisclosureCardViewComponent.cs and just use Default.cs? When I try to use #await Component.InvokeAsync("DisclosureCard", disclosureCardViewModel) without it I get the following exception:
"System.InvalidOperationException: 'A view component named 'DisclosureCard' could not be found. A view component must be a public non-abstract class, not contain any generic parameters, and either be decorated with 'ViewComponentAttribute' or have a class name ending with the 'ViewComponent' suffix. A view component must not be decorated with 'NonViewComponentAttribute'.'"
I think a Partial View is a better fit for what you're after here, which is an encapsulated view with a model but no logic.
To use a partial view, create a _DisclosureCard.cshtml file in Views/Shared and then invoke like this:
#await Html.PartialAsync("_DisclosureCard", disclosureCardViewModel)
Alternatively, you can invoke the partial view using the tag-helper:
<partial name="_DisclosureCard" model="disclosureCardViewModel" />
Is there any way I can get rid of DisclosureCardViewComponent.cs and
just use Default.cs?
Certainly you can. In that scenario, you would require to change the invocation way at the same time and your component name inside components under shared folder also should be Defult and within it you should have default.cshtml file. Have look on the following screenshot:
ViewComponents Folder:
Note: Remember that, view component never directly handles a request we always need a class in between to invoke it from view.
Shared Folder:
When I try to use #await Component.InvokeAsync("DisclosureCard",
disclosureCardViewModel) without it I get the following exception:
The error you are getting is pretty obvious and expected. Because, as you are now calling your ViewComponent as following:
#await Component.InvokeAsync("DisclosureCard", disclosureCardViewModel)
After Renaming viewComponent:
Once you would rename your viewComponent from DisclosureCardViewComponent to just Default.cs so you have to call it as following:
#await Component.InvokeAsync("Default", disclosureCardViewModel)
Note: You ought to matach your component.cs files name as it is. If your component name is different than you should use component Suffix to avaid ambiguity.
Output:
Note: If you would like to know more details on View components you could check our official document here

Is there a way in a Razor Pages app to change the default starting route to an Area?

When you create an ASP.Net Core Web App with Individual Accounts it adds an Identity Area along with the regular Pages folder.
Since I don't want to be working between the Pages folder along with the Area folder, I want to create a Home Area that my app's default route "/" will route to.
I was able to accomplish this for the Index page by adding this to Program.cs:
builder.Services.AddRazorPages(options =>
{
options.Conventions.AddAreaPageRoute("Home", "/Index", "");
});
Obviously, this just takes care of the single Index page, but if I wanted to have an About or Privacy page, I would have to add AreaPageRoutes for them as well. I tried a couple different things with AddAreaFolderRouteModelConvention that I am too embarrassed to show here, but was ultimately unable to find out how to make it work.
Is it possible and secondly, is it bad practice, to map the default routing of Pages to a specific Area?
You can use a PageRouteModelConvention to change the attribute route for pages in an area:
public class CustomPageRouteModelConvention : IPageRouteModelConvention
{
public void Apply(PageRouteModel model)
{
foreach (var selector in model.Selectors.ToList())
{
selector.AttributeRouteModel.Template = selector.AttributeRouteModel.Template.Replace("Home/", "");
}
}
}
Generally, my advice regarding areas in Razor Page applications is don't use them. They add additional complexity for no real gain at all. You have already found yourself fighting against the framework as soon as you introduced your own area. QED.
The only place areas serve a real purpose is within Razor class libraries, so you have to live with an area when using the Identity UI. But you don't have to use the Identity UI. You can take code inspiration from it and implement your own version in your existing Pages folder.

View Component as a Tag Helper does not get Invoked

Invoking a View Component as a Tag Helper was introduced in ASP.NET Core 1.1. (See “Invoking a view component as a Tag Helper”). But the following only returns the Test for VC part of the view. It seems that <vc:annual-orders>…</vc:annual-orders> part does not get invoked at all.
Views\Shared\Components\AnnualOrders\Default.cshtml:
#{
Layout = "";
}
<div>Test for VC</div>
<div>
<vc:annual-orders>
</vc:annual-orders>
</div>
myProj\ViewComponents\AnnualOrdersViewComponent.cs:
public class AnnualOrdersViewComponent : ViewComponent
{
private readonly OrdersContext _context;
public AnnualOrdersViewComponent(OrdersContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var lastOrders = _context.Where(t => t.orderType == "new");
return View(await lastOrders);
}
}
NOTE:
I'm using ASP.NET Core 1.1 and the View Components without Tag Helpers are working fine.
I also followed MSDN's official tutorial, “Invoking View Components as Tag Helpers” where it explains that PascalCase class names and method parameters for the Tag Helper are translated into their lower kebab-case.
This doesn't address your specific situation since your view component is parameterless, but as it's closely related, I’m leaving it here for anyone that needs to hear it:
Even if the tag helper is correctly registered in e.g. the _ViewStart.cshtml, as per #alan-savage's answer, it will not render unless you include all parameters from the InvokeAsync() method.
This may seem self-evident, but it can be confusing since it doesn't respond with an exception, nor is there any (obvious) design-time validation built into Visual Studio.
Note: There actually is design-time validation, it’s just not obvious. In the code editor, correctly referenced view components will show up with bold property names. But this is easy to miss if you're not looking for it. And there isn't e.g. a warning that shows up in the Error List panel, or as part of the build output.
So, for example, if you instead had:
public class AnnualOrdersViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(string labelName)
{
…
}
}
And then called your tag helper as:
<vc:annual-orders></vc:annual-orders>
Your code will compile without warning, and your page will run without exception—but the view component will not be rendered.
In fact, prior to ASP.NET Core 6, this would even happen if you made the view component parameter optional, as the tag helper syntax didn’t honor optional parameters:
public class AnnualOrdersViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(string labelName = null)
{
…
}
}
Note: As of ASP.NET Core 6 Preview 6, View Components called as Tag Helpers will now honor optional parameters (source).
Obviously, in either of the above examples, this could be fixed by simply including all parameters:
<vc:annual-orders label-name="Contrived Example"></vc:annual-orders>
Again, this doesn't address the specifics of your problem, but I imagine developers running into this issue will likely come across this thread, so I wanted to include this as another troubleshooting step in case the tag helper has already been correctly registered.
I have been struggling with this and finally managed to get tag helpers for view components to work.
The issue I had was that the tag helpers were not working on the views within Areas. To resolve this, I copied the _ViewImports.cshtml and _ViewStart.cshtml pages from the /Views directory into /Areas/<AreaName>/Views directory. The the tag helpers now work and Visual Studio is giving me IntelliSense on my properties.
Don't forget to add to the _ViewStart.cshtml files (where <AssemblyName> is the name of the assembly containing the View Components:
#addTagHelper *, <AssemblyName>
this does Not work.
#addTagHelper *, MyProject.Components
this works
#addTagHelper *, MyAssemblyName
it takes assembly name, not name space.

Proper way to route to controllers in Umbraco ASP.NET / IApplicationEventHander vs ApplicationEventHandler vs RouteConfig.cs, RenderMvcController etc

I have a Solution structure like this:
MyApp.Core
--Properties
--References
--bin
--Events
|EventHandlers.cs
--Directory
--Controllers
|DirectoryController.cs
--Helpers
|ContextHelpers.cs
--Models
|DirectoryModel.cs
--AnotherSite
--Controllers
--Helpers
--Models
--Services
--Shared
--Controllers
|HomePageController.cs
--Helpers
|Extensions.cs
|app.config
|packages.config
MyApp.Umbraco
--Properties
--References
--bin
etc........
--Views
--Directory
--Partials
|DirectoryFilters.cshtml
|DirectoryBase.cshtml
|DirectoryHome.cshtml
|FDirectory.cshtml
|SDirectory.cshtml
--Partials
--Shared
|Base.cshtml
|Web.config
etc........
My Umbraco instance uses the models and controllers from my "Core" project. There is nested directory structure, because of multiple websites in one installation, in the "Core", and also in the "Views" directory in the Umbraco instance.
I am still fairly noob to .NET MVC, and I understand route hijacking, but the documentation for Umbraco's routing is slim. I have the following:
EventHandlers.cs
namespace MyApp.Core.Events
{
/// <summary>
/// Registers site specific Umbraco application event handlers
/// </summary>
public class MyAppStartupHandler : IApplicationEventHandler
{
public void OnApplicationInitialized(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
}
public void OnApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
RegisterCustomRoutes();
}
public void OnApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
}
private static void RegisterCustomRoutes()
{
// Custom Routes
RouteTable.Routes.MapUmbracoRoute(
"FDirectory",
"fdirectory/{id}",
new
{
controller = "Directory",
action = "FDirectory",
id = UrlParameter.Optional
},
new PublishedPageRouteHandler(1000));
RouteTable.Routes.MapUmbracoRoute(
"SDirectory",
"sdirectory/{id}",
new
{
controller = "Directory",
action = "SDirectory",
id = UrlParameter.Optional
},
new PublishedPageRouteHandler(1001));
RouteTable.Routes.MapUmbracoRoute(
"HomePage",
"",
new
{
controller = "HomePage",
action = "Index",
id = UrlParameter.Optional
},
new PublishedPageRouteHandler(1002));
}
}
public class PublishedPageRouteHandler : UmbracoVirtualNodeRouteHandler
{
private readonly int _pageId;
public PublishedPageRouteHandler(int pageId)
{
_pageId = pageId;
}
protected override IPublishedContent FindContent(RequestContext requestContext, UmbracoContext umbracoContext)
{
if (umbracoContext != null)
{
umbracoContext = ContextHelpers.EnsureUmbracoContext();
}
var helper = new UmbracoHelper(UmbracoContext.Current);
return helper.TypedContent(_pageId);
}
}
}
DirectoryController.cs
namespace MyApp.Core.Directory.Controllers
{
public class DirectoryController : RenderMvcController
{
public DirectoryController() : this(UmbracoContext.Current) { }
public DirectoryController(UmbracoContext umbracoContext) : base(umbracoContext) { }
public ActionResult FDirectory(RenderModel model)
{
return CurrentTemplate(new DirectoryModel(model.Content));
}
public ActionResult SDirectory(RenderModel model)
{
return CurrentTemplate(new DirectoryModel(model.Content));
}
}
}
So Umbraco does not install with an App_Start folder. I would like to know what the best approach is for a multi-site installation of Umbraco for registering the routes to the controllers. My implementation works, but it seems like I shouldn't have to create actions for every single page I am going to have in a site, in every controller. I know Umbraco has its own routing, so using Umbraco concepts, ASP.NET MVC concepts, and whatever else is available, what is the best way to implement this type of solution structure? Should I even worry about using a RouteConfig.cs and create a App_Start directory? Or is what I am doing the best approach? Should I use IApplicationEventHandler or ApplicationEventHandler?
Also, I have to hard code the node ID's. I've read that there is a way to Dynamically? And example of this would be great.
Examples of the best way to implement a structured multi-site Umbraco MVC solution is what I am asking for I guess, in regards to routing the controllers, with some detail, or links to strong examples. I have searched and researched, and there are bits and pieces out there, but not really a good example like what I am working with. I am going to have to create a RouteMap for every single page I create at this point, and I don't know if this is the most efficient way of doing this. I even tried implementing a DefaultController, but didn't see the point of that when your solution is going to have multiple controllers.
I'm not entirely sure what you are trying to achieve with this, but I'll try to explain how it works and maybe you can clarify afterwards.
I assume you have the basics of Umbraco figured out (creating document types + documents based on the document types). This is how Umbraco is normally used and it will automatically do routing for you for each of these "content nodes" (documents) you create in a site.
So create a document named document1 and it will be automatically routed in your site at URL: http://localhost/document1. By default this document will be served through a default MVC controller and it will all take place behind the scenes without you having to do anything.
Route hijacking allows you to override this default behavior and "shove in" a controller that lets you interfere with how the request is handled. To use hijacking you create a RenderMvcController with the alias of your document type. That could be HomePageController : RenderMvcController.
This controller should have an action with the following signature:
public override ActionResult Index(RenderModel model)
In this action you are able to modify the model being sent to the view in any way you like. That could be - getting some external data to add on to the model or triggering some logic or whatever you need to do.
This is all automatically hooked up by naming convention and you will not have to register any routes manually for this to work.
The other type of Umbraco MVC controller you can create is a SurfaceController. This one is usually used for handling rendering of child actions and form submissions (HttpPost). The SurfaceController is also automatically routed by Umbraco and will be located on a "not so pretty" URL. However since it is usually really not used for anything but rendering child actions and taking form submits, it doesn't really matter what URL it is located at.
Besides these auto-routed controllers you are of course able to register your own MVC controllers like in any standard MVC website. The one difference though is that unlike a normal ASP.NET MVC website, an Umbraco site does not have the automagical default registration of controllers allowing the routing to "just work" when creating a new controller.
So if you want to have a plain old MVC controller render in an Umbraco site without it being related to a document/node in Umbraco, you would have to register a route for it like you would do in any other MVC site. The best way of doing that is to hook in and add it to the Routes using an ApplicationEventHandler class. That will automatically be triggered during application startup - essentially allowing you to do what you would normally do in App_Start.
Just to be clear though - if you plan on using data from Umbraco, you should not be using normal MVC controllers and should not require any manual route registration to be done. You usually want to render a template/view in context of a document/node created in Umbraco (where you can modify data/properties of the document) and then the route hijacking is the way to go.
From what it looks like, it could seem that the correct way to do what you are trying to do is to simply create two document types:
FDirectory and SDirectory
You click to allow both of these to be created in root and then you create documents called FDirectory and SDirectory and they will be automatically routed on these URLs. Creating a RenderMvcController's called FDirectoryController : RenderMvcController will then make sure it is used to hijack the routing whenever that page is requested.
If you're simply trying to set up a multi-site solution I would suggest you create a Website document type and create a node for each site you want, in the root of your Umbraco content tree. Right click each of these nodes and edit the hostname to be whatever you need it to be. This can also be some "child url" like /fdirectory or /sdirectory in case you need to test this on localhost without using multiple hostnames.
Hope this gives you the pointers needed, otherwise try to explain what you are trying to do and I'll see if I can refine my answer a bit!

Where do I place my C# code in MVC4?

I only ever had experience in using webforms but I have started learning MVC4 recently to broaden my knowledge. However as someone with only experience in webforms, It took me awhile go get a grip on MVC4.
I've basically set up the displays for the site with the layouts and views and calling the views with the controller.
However I want to move on and start giving the site a purpose, like registration etc. which requires some heavy c# coding.
However I am lost on where I should place my handlers etc. I figured I should do my data connection and all behind-page processes in the controller?
Please correct me if I am wrong.
Generally, you can put your c# code wherever you want. The only conventions that MVC adheres to by default are the Controllers and Views folder. And technically the only reason for the Controllers folder is because controllers placed in there will default to the MyApp.Controllers namespace. Controllers are resolved by name and namespace, not file location (unlike Views).
What I usually do is put business specific services in a Services folder, and utility/infrastructure in an Infrastructure folder.
However, you're free to arrange your c# code however you like.
MVC (Model View Controller).
Your all data access logic code in "Model".
Your view and UI in "View".
Your all business logic code in "Controller".
In order for them to interact with each other. here is a simple example:
Your Model code:
public static List<string> GetCountryData()
{
List<string> lstCountries = new List<string>();
var countryData = yourDBContext.Database.SqlQuery<string>("SPROC_GetCountryData");
if(countryData.Any())
{
lstCountries = countryData.ToList<string>();
}
return lstCountries;
}
Your Controller code:
public ActionResult CountryView()
{
List<string> lstCountries = YourModel.GetCountryData();
return View(lstCountries);
}
Your View:
#model List<string>
<ul>
#foreach (var country in #Model)
{
<li>country</li>
}
</ul>
Hope it will help some.

Categories

Resources