How are Razor Page View Component paths searched? I fail to get them working, .NET Core tries to include irrelevant model - asp.net-core

I cannot get ViewComponent to be included in a page.
I have a folder structure:
- Pages
- Components
- ExternalSystems
- Default.cshtml
- Views
- Shared
- Components
- ExternalSystems
- Default.cshtml
Class file
public class Default : ViewComponent
{
private readonly Models.PermissionRegisterContext _context;
public Default(Models.PermissionRegisterContext context)
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var externalSystems = await _context.ExternalSystem.ToListAsync();
return View("Default", externalSystems);
}
}
within Pages/Index.cshtml i'v tried to include this component by trial & error (i'm learning this stuff after all):
#await Component.InvokeAsync("ExternalSystems");
#await Component.InvokeAsync("/Pages/Components/ExternalSystems");
#await Component.InvokeAsync("/Pages/Components/ExternalSystems/Default");
#await Component.InvokeAsync("/Pages/Components/ExternalSystems/Default.cshtml");
#await Component.InvokeAsync("/Views/Shared/ExternalSystems");
I expected that exception will show places searched like it does for #Html.Partial:
InvalidOperationException: The partial view 'none' was not found. The following locations were searched: /Pages/none.cshtml /Views/Shared/none.cshtml
However for every single call to #await Component.InvokeAsync it spits this exception:
InvalidOperationException: Could not find an 'Invoke' or 'InvokeAsync' method for the view component '.Pages.Identities.IndexModel'.
First of all, it doesn't show me paths searched. Second, wait what? Why are you telling me something about Pages.Identities? Yes, I have that model, but it is nowhere referenced in neither Pages/Index.cshtml or View Component i'm trying to include.
Could someone please provide me with a guidance to determine: How view component paths are searched? For Razor Pages it is not documented, only in 3rd party site.
Anyway it doesn't work that way for me - what would be the debugging steps? Console debug doesn't show anything useful.
dotnet 2.0.7

Let's say you have created a view component as follows :
public class ExternalSystems : ViewComponent
{
public ExternalSystems()
{
//constructor can have dependencies injected.
}
public IViewComponentResult Invoke()
{
//View is a helper method available in classes inherited
//from ViewComponent that returns an instance of
//ViewViewComponentResult. It has multiple overloads as described
//later.
return View(viewName,viewModel);
}
}
View method has multiple overrides :
View() - use default view for view component without any viewmodel
View(viewModel) - use default view for view component with specified viewmodel
View(viewName) - use specified view for view component without any viewmodel
View(viewName,viewModel) - use specified view for view component with specified viewmodel
When you try to render this view component from a Controller, view will be looked up at following locations :
"/Views/{ControllerName}/Components/ExternalSystems/{ViewName}.cshtml" .
So, if you are using HomeController and have specified viewName as ExternalSystemsView in the View(viewName,viewModel) call , your path becomes
/Views/Home/Components/ExternalSystems/ExternalSystemsView.cshtml . This allows each controller to have its own custom view for the view returned by view component.
If the ExternalSystemsView.cshtml is not located at above path , it will be looked up at /Views/Shared/Components/ExternalSystems/ExternalSystemsView.cshtml
You can override the lookup position by passing the complete path of view - View("Views/Shared/Components/Common/YourView.cshtml") while calling View(viewName,viewModel) from your ViewComponent's Invoke method.
Note : If you don't specify a viewName, it defaults to Default.cshtml which is different from Index.html used for controllers
For your case, #await Component.InvokeAsync("ExternalSystems") is the correct call as it expects the viewcomponent name as parameter. ViewName will be picked up from what you have passed as the viewName parameter value to View(viewName,viewModel) call in your ViewComponent's Invoke method and will default to Default.cshtml if no viewname has been specified.

Alright, my mistake was that I didn't see I have decorated ...Pages.Identities.IndexModel with [ViewComponent]. It was unintentional and was just trying to make ViewComponents work by trial & error and failed to see this mistake.
Basically dotnet discovered type that didn't have either Invoke/InvokeAsync function and thus was stuck on that exception.
I validated that these paths are searched when providing "HelloWorld" as view name:
/Pages/Components/HelloWorld/HelloWorld.cshtml
/Views/Shared/Components/HelloWorld/HelloWorld.cshtml

Related

Access Views and Controllers which are under Sub folders for specific scenarios?

On my all actions methods I use this route attribute to use - sign.
[HttpPost]
[Route("lower-case-converter")]
public IActionResult Index(BassModel model)
Since I have many case converters I want to put them inside a folder called "CaseConverters". I made a folder in views folder called ""CaseConverters". Note that there are other tools too.
So I changed the route attribute like this
[Route("~/CaseConverters/lower-case-converter")]
Above not worked. So I changed it to
[Route("/CaseConverters/lower-case-converter")]
Still its not working. Note that i want to add this folder to Controllers folder too. How to acehve this?
To specify a route for lower-case-converter action inside your CaseConverters controller you can specify the route as follows, (I'm assuming CaseConverters is your controller. If yes It's better to use the correct naming convention. Ex: CaseConvertersController)
[Route("CaseConverters/lower-case-converter")]
You can also specify a route template for your controller class. as follows,
[Route("CaseConverters")]
public class CaseConvertersController : Controller
{
[Route("lower-case-converter")]
public IActionResult LowerCaseConverter()
{
//you implementation
}
}
For the above code route template for the action is CaseConverters/lower-case-converter. And also make sure use have used app.UseMvc() and MapRoute() methods inside Configure() method inside the startup.cs

Umbraco custom controller not using declared model type

Umbraco 9 - I've created a page type called SiteSearch and a controller to hijack requests to pages of that page type. This all works correctly.
The controller gets an IEnumerable from a very simple search service and sets it on a ViewModel, which is then passed to the view.
However, I simply cannot get the view to respect the model declaration. I am getting the error:
ModelBindingException: Cannot bind source type
Maysteel_Web.ViewModels.SiteSearchViewModel to model type
Umbraco.Cms.Core.Models.PublishedContent.IPublishedContent.
It seems to be forcing my view to use an instance IPublishedContent (singular), even though I'm declaring the model as my custom object. I've tried changing the model declaration to string just to see what would happen:
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type
'Maysteel_Web.ViewModels.SiteSearchViewModel', but this
ViewDataDictionary instance requires a model item of type
'System.String'.
There it recognized the model, but when I declare my custom object as a model, it goes back to trying to bind to IPublishedContent (singular). I've verified that the model I'm passing is not null and actually has results. I'm not sure what else to try. Can anyone help me undertand why this is happening?
SiteSearchController action:
public override IActionResult Index()
{
var searchPage = new SiteSearch(CurrentPage, _publishedValueFallback);
var results = _searchService.QueryUmbraco("about");
var viewModel = new ViewModels.SiteSearchViewModel();
viewModel.Results = results;
return View("~/Views/SiteSearch/index.cshtml", viewModel);
}
View:
#model Maysteel_Web.ViewModels.SiteSearchViewModel
#{
Layout = "/Views/Main.cshtml";
}
<h1>Site Search Page</h1>
ViewModel:
public class SiteSearchViewModel
{
public IEnumerable<IPublishedContent> Results { get; set; }
}
I just figured it out. My layout view had the following line in it:
#inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
Once I removed that, I no longer get the error. It must have been forcing the ViewModel to be IPublishedContent.

Using attribute routing on a controller is forcing me to manage all routes

I am just getting to grips with Asp.net Core and I'm trying to set up a basic site.
I want to build an admin panel that is under a subdirectory.
I have a simple controller which was scaffolded by the EF crud feature.
So it seems that from the examples I should just be able to add a [Route()] attribute to the controller and it will prefix everything. Something like this:
[Route("Admin/Subfolder/[controller]")]
public class EventsController : Controller
{
}
But when I do that I just get an error page saying "multiple actions matched" and it lists index, details, create, etc.
I can get it working if I then go through every method and put a [Route()] attribute on it but this doesn't seem to be in line with the documentation.
It feels like I should be able to just add a prefix to the controller route without having to take over management of every route within the controller. Case in point, the POSTS are not working now and I'm not sure what the format of the route attribute should be for them.
What am I doing wrong?
You are doing it correctly. Default route attribute can be applied at the controller level. “Placing a route attribute on the controller makes all actions in the controller use attribute routing.”
Can you post complete code of your controller? There must be something else going on in there. Make sure you use HttpPost/HttpGet attribute for actions with the same name, like so:
[Route("Admin/Subfolder/[controller]")]
public class EventsController : Controller
{
[HttpGet]
public IActionResult NewEvent()
{ }
[HttpPost]
public IActionResult NewEvent()
{ }
}
Good explanation on routing can be found here

InvalidOperationException rendering ViewComponent in Strongly-Typed View

Recently updated dotnet core 1.0.1 to 1.1 and ViewComponent in MVC starts failing with the below exception:
InvalidOperationException: One or more errors occurred. (The model item passed into the ViewDataDictionary is of type 'App.Models.HomeViewModel', but this ViewDataDictionary instance requires a model item of type 'App.Components.LoginViewComponent'.)
The Index.cshtml renders LoginViewComponent:
#model App.Models.HomeViewModel
<html>
#Component.InvokeAsync("LoginViewComponent")
</html>
Views/Home/Index.cshtml
The ViewComponent LoginViewComponent/Default.cshtml is just a class that displays it's model's info:
#model App.Components.LoginViewCompoent
<body>
<div>#Html.DisplayFor(v=>v.LoginName)</div>
</body>
Views/Shared/Components/LoginViewComponent/Default.cshtml
It renders fine when the #model directives is removed from Default.cshtml.
Isn't ViewComponent suppose to be separated and agnostic from the parent view that is wrapping it? From the exception it seems that it would be require to declare the LoginViewComponent ViewComponent in HomeViewModel class in order to render it.
Couldn't find any change note on this on asp.net core github.
Comment and help on this would be greatly appreciated.
I've came across the same error, someone in my team updated Microsoft.AspNetCore.Mvc version from 1.0.0 to 1.1.0 and some of the components I had in Strongly-Type views started throwing
InvalidOperationException: The model item passed into the ViewDataDictionary is of type 'My.StronglyView.ObjectType', but this ViewDataDictionary instance requires a model item of type 'My.ViewComponent.ObjectType'.
I am not sure if this change was intentional, but it is definitely not what we would expect.
I haven't got the time to research about the reasons of this 'breaking' change but I came with a solution.
Thing is, if you pass a null object to your ViewComponent.View() method, we get this exception. Any non-null object passed through it, would update the ViewData.ModelExplorer and the correct object-type would have been registered, avoiding this exception.
Using Tester-Doer pattern, same pattern used in some classes of .Net Framework, We can now pass a non-null object to the ViewComponent and use it and its wrapped object as We need.
What I did was, I created a interface IViewComponentModel<T> as class ViewComponentModel<T> as below:
// Interface for ViewComponentModel
public interface IViewComponentModel<T>
where T : class
{
T Data { get; }
bool HasData();
}
// ViewComponentModel class for the Strongly-Type View Component
public class ViewComponentModel<T> : IViewComponentModel<T>
where T : class
{
public T Data { get; private set; }
public ViewComponentModel(T data)
{
Data = data;
}
public bool HasData() => Data != null;
}
In my ViewComponent class implementation, I return View(new ViewComponentModel<AnyReferenceType>(myModel));
public async Task<IViewComponentResult> InvokeAsync()
{
var myViewData = _myService.GetSomeObjectForMyViewComponent();
var model = new ViewComponentModel<MyViewDataType>(myViewData);
return await Task.FromResult(View(model));
}
And finally, in my Component View (.cshtml) I have the code below:
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
#model ViewComponentModel<MyViewDataType>
Now you can do whatever you want with this object inside of the view and to use your real model, just call #Model.Data and voilà.
In this way, We will never pass a null object to the ViewComponent and it won't 'inherit' the object type from the View.
Hopefully it helps!
asp.net-core asp.net-core-mvc dotnet-core asp.net-core-1.1
Simple solution is that, in ViewComponent class check null for view model, and if view model is null, then return empty Content result as following:
public async Task<IViewComponentResult> InvokeAsync()
{
var vm = some query for ViewModel;
if(vm != null)
{
return View(vm);
}
return Content("");
}
By default it will try to pass in the model of the "Parent View" ,not sure what you would call that, try updating your code like this
#Component.InvokeAsync("LoginViewComponent", new App.Components.LoginViewComponent())
so that partial view can have what it needs, in order to serve up it's content.

ChildTag appears outside parentTag in TagHelper

I have Parent and Child TagHelper.. I have set attribute "parentTag" for Child Tag and yet it displays outside that parent Tag. How to restrict it from appearing outside parent Tag
[HtmlTargetElement("TestParent")]
public class Parent: TagHelper{
}
[HtmlTargetElement("TestChild",ParentTag = "TestParent")]
public class Child : TagHelper
{
}
What it need is that "TestChild" should not appear outside "TestParent"
For example:
<TestChild value=2></TestChild>
Above code must throw error or should not be shown in intellisense as it not enclosed by parent Tag "Testparent"
I tried to recreate your scenario with parent tag helper
[HtmlTargetElement("TestParent")]
public class ParentTagHelper : TagHelper
{
}
and a child tag helper:
[HtmlTargetElement("TestChild", ParentTag = "TestParent")]
public class ChildTagHelper : TagHelper
{
public int Value { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Content.SetContent(Value.ToString());
}
}
I added those two in _ViewImports.cshtml, then build the project. Then i called the tag helpers in a view:
<TestParent>
<TestChild value="101"></TestChild>
</TestParent>
<TestChild value="202"></TestChild>
I saw expected IntelliSense on first tag and didn't see one on the other one:
Finally, I executed the web app on ASP.NET Core 2.2. The HTML code of resulted page was:
<TestParent>
<TestChild>101</TestChild>
</TestParent>
<TestChild value="202"></TestChild>
It looks perfectly fine. Just as expected. The first tag was processed, the second one wasn't.
Conclusion
It looks like you had other problems aside from those described in original question. Most likely the problem wasn't in the sole tag helpers functionality and their restrictions on parent tags.