I'm developing a large ASP.NET Core 2 web application but I still get confused with URLs.
When fist learning, I thought URL's took on the name of the View, but discovered they come from the Controller method.
Here is an example to convey my issue:
I have a controller method named Daysheet, which returns a view and model with the same name.
In the Daysheet view, I call various controller methods from Javascript to perform specific actions. One of them is called AssignStaff which takes two integer parameters.
In the AssignStaff method I again return the Daysheet view with model, but now my URL is "AssignStaff"!
I can't just do a redirect because the whole Daysheet model is not being passed to the AssignStaff method.
I have many situations like this where after calling an action, I end up with another URL that I don't want.
UPDATE/EDIT
Thanks for assistance and apologies if my explanation is confusing. I simply have a view called Daysheet that uses a model. I want to call various controller methods to perform various actions, but I want to stay on the "Daysheet" view/URL.
As mentioned, I can't just redirect because in the action method I no longer have the whole model from the Daysheet view. Also, if I redirect I can't pass the whole model because that causes an error saying the header is too long. I think my only choice may be to use ajax for the actions so that the URL doesn't change.
When you just do Return View("") name in a Controller Action, the URL will be the name of the Action you are using.
If you want to redirect to some specific Action, that will help to make sure the Url matches to where you are. You might want to read more about it here.
To do so, use:
RedirectToAction()
The URLs your application responds to are called "routes", and they are either created by convention or explicitly. The default is by convention, of course, which is a URL in the form of /{controller=Home}/{action=Index}. Index is the default action if that portion of the route is left off, so a request to /foo will by convention map to FooController.Index. HomeController is the default controller, so an empty path (e.g. http://sample.com) will by convention invoke HomeController.Index.
Razor Pages have their own conventions. These do somewhat follow the file system, but exclude the Pages part of the path. So a Razor Page like Pages/Foo/MyRazorPage.cshtml, will load up under /Foo/MyRazorPage.
There there is the Route attribute, which allows you to specify a totally custom route. This attribute can be applied to a controller class and individual actions in the class. For example:
[Route("foo")]
public class MyAwesomeController
{
[Route("bar")]
public IActionResult MyAwesomeAction()
{
return View();
}
}
With that, a request to /foo/bar will actually invoke MyAwesomeController.MyAwesomeAction.
Related
Question:
Edit: It seems my question is actually not a routing issue but an anchoring issue.
If I have assigned a route:
[Route("~/Envelope/List/AcademicYear/{year}")]
public IActionResult AcademicYear(string year)
{
}
How would I correctly use an asp-action to call this route?
using
<a asp-action="List/AcademicYear/" asp-route-id="#Model.AcademicYear">#Model.AcademicYear</a>
returns a url with a %2f (Envelopes/List%2fAcademicYear/2122) instead of / (Envelopes/List/AcademicYear/2122) and thus results in a 404 error
How do I use Custom URL with asp-action to call a specific Action in my Controller?
or
How do I change the routing so I can call an action from a controller with a non default route mapping?
Context:
I've read https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0
and yet i'm still confused on the whole concept of routing and how to interacts with controllers and actions.
In my application I have a controller called Envelope - it controls everything to do with my Envelopes.
I have a class in the Envelopes controller called
public class EnvelopeController : Controller {
public IActionResult List() {... return View()}
and it returns the List.cshtml view. The current url as set by default route mapping: /Envelope/List
In the List.cshtml I have a link that is intended to filter the List on a year parameter
<a asp-action="AcademicYear" asp-route-academicYear="#Model.AcademicYear"> #Model.AcademicYear</a>
My intention is to pass this into a method in the Envelopes controller called "AcademicYear" that gathers the Envelope data stored in temp data, deseralises it and then returns a filtered version based on the parameter:
public IActionResult AcademicYear(string academicYear) { return View("List", newViewModel)}
The return url after this point is correctly: /Envelope/AcademicYear?academicYear=21%2F22
However I would Like to know how to change this so even though I call the Action
<a asp-action="AcademicYear" asp-route-academicYear="#Model.AcademicYear"/>
the URL returned would look like this /Envelope/List/AcademicYear/2122/
Is there a way of doing this? Am I looking at the problem the wrong way? I have thought about simply passing a parameter in the List action and running some form of control to do either set of operations depending on the parameters existence but realistically the List method and the filtering AcademicYear method aren't really doing the same thing and I'd like to seperate out the code into different methods if possible.
Even if its not the appropriate solution I would still like to know if it is possible to change the URL routing for an action after it has been called.
Edit :
I have tried using HttpGet(List/AcademicYear/{academicYear:[a-zA-Z]} however when I do this I can't actually call List/AcademicYear as an asp-action due to the "/" and how that encodes to %2f
Answer:
With the help of the below solutions I realised I was looking at the problem wrong and was actually having issues creating correct anchors.
Reading: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0#url-generation-and-ambient-values-1
I realised the answer was staring me in the face and I used this alongside the Routes provided for in the answers
<a id="#academicYear.Name" href="#Url.Action("AcademicYear", "Envelope",new {year= academicYear.Name})">
Maybe you just need to decorate your action like that:
[HttpGet("List/AcademicYear/{year:int}")] // or HttpPost it depends on you
public IActionResult AcademicYear(string year) { }
You can add several attribute routes to the action.
[Route("~/Envelope/List/AcademicYear/{year}", Name="ListRoute")]
[Route("~/Envelope/AcademicYear/{year}")]
public IActionResult AcademicYear(string year) { }
and if you need this url
http://localhost:xxxx/Envelope/List/AcademicYear/2021
you can use this html helper, that will create a right anchor tag for you using an attribute route name.
#Html.RouteLink(#Model.AcademicYear, "ListRoute", new { year = #Model.AcademicYear })
I added Name="ListRoute" to the attribute routing (see above)
I am building a cms, on the edit screen for a section you can edit multiple types of pages, the urls need to remain nutral, like this:
foobar.com/edit/section/my-content-page-name
foobar.com/edit/section/my-gallery-page-name
foobar.com/edit/section/my-blog-page-name
In this scenario the Index action is used for both gets and posts.
At the moment I have one massive ViewModel, that encompasses all the data required across all page types.
I feel this is quite wrong and, makes an ugly solution for deciding what type of page update on the post.
How can I keep the Action the same but use it with different strongly type ViewModels?
Is this even possible?
public ActionResult Index(string page)
{
var model = _pageManager.GetSection(page, SelectedSite);
return View(model.PageType, model);
// renders appropriate View based on page type.
}
[Transaction]
[HttpPost]
[ValidateInput(false)]
public ActionResult Index(SectionIndexViewModel model)
{
// all page types post back to same action to update content etc.
// at this point SectionIndexViewModel is getting bloated with properties because it must cater for ALL page types data.
var action = Request["action"] ?? "";
// currently use this to determine what event has been triggered
switch (action.ToLower())
{
// then goes to update the appropriate page, blog or gallery
// etc.
all page types post back to same action to update content etc.
There is your problem. The same action should not handle all post backs. Create one controller per feature (content, gallery, blog). It's how MVC is intended to be used.
Single Responsibility Principle do also apply to controllers.
You can even move the controllers to class libraries to get plugin like architecture for your CMS. I've described how here: http://blog.gauffin.org/2012/05/griffin-mvccontrib-the-plugin-system/
I managed to achieve this with some MVC basics that I had forgotten about.
The routing remains as per the defaults.
For each ViewModel type I delivered an extra hidden field in the form, with the type of the page/content/ViewModel eg: Content Page, or Blog Page etc.
In the Post action, I check the type of the page from this hidden field.
Then use TryUpdateModel using the expected ViewModel type for that page type.
And the rest is straight forward.
Pretty basic stuff really.
I have a fixedURL to which I'd like to post different types of xml message, deserialized using DataContracts. Depending on the type of the deserialized message, I'd like to route to:
overloaded methods, e.g.
void Process(ContractType1 request) {}
void Process(ContractType2 request) {}
So at some point I need to deserialize this message and hopefully allow the default routing rules to match the correct method. Which extensibility point should I use for this? Or even better, can I make this work out of the box?!
If it makes any difference, I'm using MVC 3.
ASP NET MVC does not respect the overload if they are not decorated for different HTTP methods - e.g. one for POST, other for GET.
You need to use [ActionName(Name = "Process2")] to change the route name. And you will have to use different routes to access (if the HTTP methods are the same)
Have a look here.
Apart from the technical workaround, passing different contracts to the same URL is against the REST principles. Data could be in different format (XML, JSON, ...) but it must be the same. The URI defines a unique intention. Now it is possible to have a common dumpster where documents are all dumped to the same URI but then ASP NET MVC default model binder would not be able to help you and you need to create your own model binder.
Contrary to the other answer I say this is possible
Asp.net MVC is a great platform that can be easily extended. And so basically I've written a special action method selector that makes it possible to write overloads that can serve the same HTTP method but defer in parameters. By default you'd get runtime error that action method can't be resolved. But when you use this action method selector you get rid of this error.
Basically if your parameter classes have distinct parameter names, you can actually select methods by that.
Action method selector is called RequiresRouteValuesAttribute and a typical usage scenario would be with default route where id is optional in:
{controller}/{action}/{id}
This means that you either have to write
public ActionResult Index(int? id)
{
if (id.HasValue)
{
// display details view
}
else
{
// display master view
}
}
but by using my action method selector you can easily write two action methods:
public ActionResult Index()
{
// display master view
}
[RequiresRouteValues("id")]
public ActionResult Index(int id)
{
// display details view
}
The same could be applied to your action methods as long as your custom types have distinct property names or methods use different parameter names. So in your case it could be something like:
[RequiresRouteValues("first.Id")] // when you provide prefix with your form
// or
[RequiresRouteValues("Some.ContractType1.Distict.Property.Name")]
public ActionResult Process(ContractType1 first)
{
// do what's appropriate
}
[RequiresRouteValues("second.Id")] // when you provide prefix with your form
// or
[RequiresRouteValues("Some.ContractType2.Distict.Property.Name")]
public ActionResult Process(ContractType2 second)
{
// do what's appropriate
}
Read all the details about this action method selector and get the code as well.
So in order accomplish what I asked in this post I did the following:
[iPhone]
[ActionName("Index")]
public ActionResult IndexIPhone()
{
return new Test.Areas.Mobile.Controllers.HomeController().Index();
}
[ActionName("Index")]
public ActionResult Index()
{
return View();
}
Which still serves the same view as the Index action method in this controller. Even though I can see it executing the Test.Areas.Mobile.Controllers.HomeController().Index() action method just fine. What's going on here? And how do I serve the Index view from Mobile area without changing the request URL (as asked in the original post referenced above)?
You have a few options:
Redirect to the Action you'd like to return: return RedirectToAction("Action-I-Want").
Return the View by name: return View("The-View-I-Want").
Note that with the 2nd approach you'd have to put your view in the "Shared" folder for all controllers to be able to find it and return it. This can get messy if you end up putting all your views there.
As a side note: The reason your work doesn't find the view is because default view engine looks for the view in the folder that "belongs" to the current executing controller context, regardless of what code you're calling.
Edit:
It is possible to group all "mobile" views in the same folder. On your Global.asax (or where ever you're setting up your ViewEngine, just add the path to your mobile View in the AreaViewLocationFormats. Mind you, you'll still have to name your views differently.
You can also write your own view engine. I'd do something like detecting the browser and then serving the right file. You could setup a convention like View.aspx, and View.m.aspx.
Anyhow, just take a look at WebFormViewEngine and you'll figure out what works best for you.
The easiest way to send a request to a view handled by another controller is RedirectToAction("View-Name", "Controller-Name").
There are overloads of View() that take route information that might work as well, but they'd require more effort to set up.
Well actually the easiest way is to make one version of your site programmed on standards instead of browser detection :D -- however in direct response to accomplish what it in a more of a ASP.NET mvc fashion, using:
RedirectToAction("ViewName", "ControllerName");
is a good method however I have found it is more practical if you feel you must program for different browser standards to create a primary view and an alternate "mobile" view under your controllers views. Then instead of writing special code on every controller, instead extend the controller like so.
public class ControllerExtended : Controller
{
private bool IsMobile = false;
private void DetectMobileDevices(){ .... }
}
Then modify your controller classes to instead say ControllerExtended classes and just add the one line to the top of each Action that you have alternate views of like so:
public class ApplicationsController : ControllerExtended
{
// GET: /Applications/Index
public ActionResult Index() {
this.DetectMobileDevices();
if(this.IsMobile){
return RedirectToAction("MobileIndex");
} else {
// actual action code goes here
return View();
}
}
}
Alternately you can use return View("ViewName"); but from my experience you want to actually perform different actions as opposed to just showing the result in a different view as in the case of presenting an HTML table as opposed to a Flex table to help iPhone users since there is no flash support in the iPhone, etc. (as of this writing)
I have two different objects: contracts, and task orders. My requirements specify that in order to view the Details for either object, the Url should be "http://.../Contract/Details" or "http://.../TaskOrder/Details" depending on which type. They are both very similar and the details pages are almost identical, so I made a class that can either be a contract or a task order, and has a variable "objectTypeID" that says which type it is. I wrote the action "Details" in the task order controller, but now I want to call that from the contract controller instead of recopying the code.
So is there any way to have the url still say ".../Contract/Details" but call the action in the TaskOrder controller instead? I tried using
TaskOrderController TOController = new TaskOrderController();
TOController.Details(id);
This would have worked except that I can't use the HttpContext.Session anymore, which I used several times in the action.
Why are you calling a controller from a controller? A controller action should be called via a route and return a view.
If you have common code used by two separate controllers then you should be looking to abstract this code to another class.
RedirectToAction("Details","To");
In addition, add routing parameters if you need to.
Also, maybe you need a BaseController class which these two controllers inherit from and which implement the same Details action, but based on the objectTypeID do slightly different things.
Create a base class for the controller. Like DetailsController
Put your details code in there, and have it accept an typeId.
Then have your two controllers derive from that base class, and have their Details action call the base class passing in the id
Thanks David, I should be calling it from the view.
All I needed was the following line in my Contract/Details.aspx page:
<%= Html.Action("Details", "TaskOrder", new { id = ViewData["id"] })%>