I am reading an MVC book and following the examples from it to create a music store project.
In one of the example, it creates a controller, calls an action method with a parameter in the URL. I found something interesting. Here is the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace MvcMusicStore.Controllers
{
public class StoreController : Controller
{
//
// GET: /Store/
public string Index()
{
return "Hello from Store.Index()";
}
// GET: /Store/Browse?genre=?Disco
public string Browse(string genre)
{
string message =
HttpUtility.HtmlEncode("Store.Browse, Genre = " + genre);
return message;
}
//
// GET: /Store/Details/5
public string Details(int id)
{
string s = "Store.Details, ID = " + id;
return s;
}
}
}
In the last method "Details(int id)", if I call it using a URL like
http://localhost:4961/store/details/6
It's alright. But if I change the name of the parameter from "id" to "i", the compiler doesn't complain but when I ran it I would get an error message that I am unable to interpret.
Part of the error message is like this:
The parameters dictionary contains a null entry for parameter 'i' of non-nullable type 'System.Int32' for method 'System.String Details(Int32)' in 'MvcMusicStore.Controllers.StoreController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
So what is wrong if I just use "i" for integer?
It's because in your route definition you used {id}. Because Int32 is a value type it means that you have to pass a value for this parameter when invoking the action.
For example you could call it like this and still keep your default route definition with {id}
http://localhost:4961/store/details?i=6
You literally have to use the name of the variable (seriously). I ran into this a while back and was.... let's say, surprised. The entry in the url must match the method parameter.
The problem is that when you change
public string Details(int id)
to
public string Details(int i)
then you introduce a breaking change. The code which called Details by passing parameter id is now passing a parameter which does not match. As a result, Details is called and i does not match anything. When calling and omitting a parameter, the parameter must be marked as optional with this syntax:
public string Details(int i = 0)
But since it is not, you get the error. Either change it back to id, or change the caller to use i (as #Darin points out, the binding is coming from your default route definition).
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
You would need to change these to be
"{controller}/{action}/{i}", // URL with parameters
new { controller = "Home", action = "Index", i = UrlParameter.Optional }, // Parameter defaults
Related
When I want to call a new page in .net, say the "About.cshtml" page, I use the following code in the HomeController:
public ActionResult About()
{
ViewBag.Title = "About";
return View();
}
To call it I'd use a link to "/Home/About". And if I wanted to create a new page called "Contact.cshtml", for example, I'd copy the above and replace About with Contact.
I know that the route in "/Home" calls the HomeController. But how, exactly, does that controller know to return the About.cshtml page? I assume it's based on the name of the function. But this doesn't sound right to me. About() isn't an HTTP verb like Get() or Post(), and the name of the function normally shouldn't define what it does, unless it already existed.
Also, when exactly is View() defined, and when is it assigned to the About.cshtml page?
Finally, is there an attribute that would allow me to return the About.cshtml page with a different function name (as I can set a function to respond to Get with the [HttpGet] attribute)?
But how, exactly, does that controller know to return the About.cshtml page?
Because the action method name is About:
public ActionResult About()
The route found that method by the URL:
/Home/About
If the URL didn't include the action:
/Home
Then it would look for a default action. Normally this is Index(), as configured by the default route mapping:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
Note how a default value is defined for both controller and action if none is provided on the URL.
the name of the function normally shouldn't define what it does
Why on Earth not? A function name should exactly define what that function does.
Also, when exactly is View() defined
It's in the base controller class.
Finally, is there an attribute that would allow me to return the About.cshtml page with a different function name
Not an attribute per se, but you can specify the view name when calling View():
return View("SomeOtherView");
only to explain a few more (the David's response is so good), View() is an object of type ViewResultBase, in class Controller;
protected internal ViewResult View()
{
return View(viewName: null, masterName: null, model: null);
}
ViewResultBase has a method ExecuteResult() that receives a parameter of type ControllerContext (this parameter has the info about the request) and inside this method, if the name of the view is null, the view name is established based on the url (read the explain of David about the routing) that is called accesing to the RouteData:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if (String.IsNullOrEmpty(ViewName))
{
ViewName = context.RouteData.GetRequiredString("action");
}
....
}
Here, if you watch the RouteData you can view that the called action is Index, and this value is set to the ViewName property:
Regards,
What component(s) do I need to implement and how can I hook it into the framework in order to have urls where the query parameters with names containing 2 or more words separated by hyphens?
For example:
I would have this url:
www.mysite.com/resource/i-am-looking-for?parameterOne=foo¶meterTwo=bar
I would like to have it like this:
www.mysite.com/resource/i-am-looking-for?parameter-one=foo¶meter-two=bar
My action would be something like this:
ActionResult DoSomething(string parameterOne, string parameterTwo)
The reason: user friendly urls and consistency
I need to have:
the component to integrate seamlessly with the framework URL helpers (Url.Action, Url.RouteUrl etc)
the incoming data to be bound to the action parameters (or model)
Is there a diagram where I can see the framework extensibility point in this regard?
Thank you!
public ActionResult SomeAction( string Some-Var ) {} is invalid because in C# variable names can not contain hyphens. Underscores ARE allowed, however so this IS valid public ActionResult SomeAction( string Some_Var ) {}
Now, if you relax your need to bind to strongly typed input vars to the action, you can accomplish your goal with Request.QueryString["some-var"] but you will have to handle the type conversion and error handling associated with it.
You can add Custom Value Provider Factory as shown below,
public class MyQueryStringValueProvider : NameValuePairsValueProvider
{
public QueryStringValueProvider(
HttpActionContext actionContext,
CultureInfo culture)
: base(
() =>{
var request = actionContext.ControllerContext;
foreach(var pair in request
.GetQueryNameValuePairs()){
yield return new KeyValuePair<String,String)(
Transform(pair.Key), pair.Value
);
}, culture)
{
}
private String Transform(String key){
// just removing all - , as it is case insensitive
//
return key.Replace("-","");
}
}
And you have to register your provider as shown below,
ValueProviderFactories.Factories.Add(
new MyQueryStringValueProvider());
For safe side, you can remove existing QueryStringValueProvider to avoid name conflicts for keys that does not have dash.
For dashes in Action Name, you can refer https://github.com/AtaS/lowercase-dashed-route
How about url encoding on client's side?
This way you can call controllers using general way. Please have a look one of the answers regarding that: Encode URL in JavaScript?
for example: this is your action.
public ActionResult GetNew(int id)
{
return View(news.Where(i => i.Id == id).SingleOrDefault());
}
First Step
Open App_Start>RouteConfig.cs open in your project.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "GetNew",
url: "news/new-detail/{id}",
defaults: new { controller = "News", action = "GetNew", id = ""}
);
}
}
Now run your project and write your browser http://localhost:….(different for you)/news/new-detail/1
New ID No. 1 will open.
I hope it's been helpful
As I read explanation here, I found that Spring can automatically bind GET request parameter to a type. Below is the sample code from the link.
#Controller
#RequestMapping("/person")
public class PersonController {
...
#RequestMapping("/create")
public String create(Person p) {
//TODO: add Person to DAO
return "person/show";
}
}
Can someone tell me how spring do this? What bean that contains the logic to convert the parameter onto command type (Person type)?
The trick is done here: org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument()
This is the excerpt of code where it actually binds the class to the values:
String name = ModelFactory.getNameForParameter(parameter);
//Here it determines the type of the parameter and creates an instance
Object attribute = (mavContainer.containsAttribute(name)) ?
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
//Then it binds the parameters from the servlet to the previously created instance
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
if (binder.getTarget() != null) {
bindRequestParameters(binder, request);
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors()) {
if (isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
}
I just read this post by Dave Ward (http://encosia.com/using-jquery-to-post-frombody-parameters-to-web-api/), and I'm trying to throw together a simple web api controller that will accept a viewmodel, and something just isn't clicking for me.
I want my viewmodel to be an object with a couple DateTime properties:
public class DateRange
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
Without changing anything in the stock web api project, I edit my values controller to this:
public IEnumerable<float> Get()
{
DateRange range = new DateRange()
{
Start = DateTime.Now.AddDays(-1),
End = DateTime.Now
};
return Repo.Get(range);
}
// GET api/values/5
public IEnumerable<float> Get(DateRange id)
{
return Repo.Get(range);
}
However, when I try to use this controller, I get this error:
Multiple actions were found that match the request:
System.Collections.Generic.IEnumerable1[System.Single] Get() on type FEPIWebService.Controllers.ValuesController
System.Collections.Generic.IEnumerable1[System.Single] Get(FEPIWebService.Models.DateRange) on type FEPIWebService.Controllers.ValuesController
This message appears when I hit
/api/values
or
/api/values?start=01/01/2013&end=02/02/2013
How can I solve the ambiguity between the first and second get actions?
For further credit, if I had this action
public void Post(DateRange value)
{
}
how could I post the Start and End properties to that object using jQuery so that modelbinding would build up the DateRange parameter?
Thanks!
Chris
The answer is in detail described here: Routing and Action Selection. The Extract
With that background, here is the action selection algorithm.
Create a list of all actions on the controller that match the HTTP request method.
If the route dictionary has an "action" entry, remove actions whose name does not match this value.
Try to match action parameters to the URI, as follows:
For each action, get a list of the parameters that are a simple type, where the binding gets the parameter from the URI. Exclude
optional parameters.
From this list, try to find a match for each parameter name, either in the route dictionary or in the URI query string. Matches are
case insensitive and do not depend on the parameter order.
Select an action where every parameter in the list has a match in the URI.
If more that one action meets these criteria, pick the one with the most parameter matches.
4.Ignore actions with the [NonAction] attribute.
Other words, The ID parameter you are using, is not SimpleType, so it does not help to decide which of your Get methods to use. Usually the Id is integer or guid..., then both methods could live side by side
If both of them would return IList<float>, solution could be to omit one of them:
public IEnumerable<float> Get([FromUri]DateRange id)
{
range = range ?? new DateRange()
{
Start = DateTime.Now.AddDays(-1),
End = DateTime.Now
};
return Repo.Get(range);
}
And now both will work
/api/values
or
/api/values?Start=2011-01-01&End=2014-01-01
I want to return a HTTP status 404 if invalid arguments are passed to my controller. For example if I have a controller that looks like:
public ActionResult GetAccount(int id)
{
...
}
Then I want to return a 404 if say urls such as these are encountered:
/GetAccount
/GetAccount/notanumber
i.e. I want to trap the ArgumentException that is thrown.
I know I could use a nullable type:
public ActionResult GetAccount(int? id)
{
if(id == null) throw new HttpException(404, "Not found");
}
But that's pretty icky and repetitious.
I was hoping I could add this to my controllers where necessary:
[HandleError(View="Error404", ExceptionType = typeof(ArgumentException))]
public class AccountsController : Controller
{
public ActionResult GetAccount(int id)
{
...
}
}
But that doesn't appear to work well.
I saw this post and this answer which nearly solves my problem:
In that answer an abstract BaseController is created from which you derive all your other controllers from:
public abstract class MyController : Controller
{
#region Http404 handling
protected override void HandleUnknownAction(string actionName)
{
// If controller is ErrorController dont 'nest' exceptions
if (this.GetType() != typeof(ErrorController))
this.InvokeHttp404(HttpContext);
}
public ActionResult InvokeHttp404(HttpContextBase httpContext)
{
IController errorController = ObjectFactory.GetInstance<ErrorController>();
var errorRoute = new RouteData();
errorRoute.Values.Add("controller", "Error");
errorRoute.Values.Add("action", "Http404");
errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString);
errorController.Execute(new RequestContext(
httpContext, errorRoute));
return new EmptyResult();
}
#endregion
}
This works great at handling unknown actions with a 404 but doesn't allow me to handle invalid data as a 404.
Can I safely override Controller.OnException(ExceptionContext filterContext) like this:
protected override void OnException(ExceptionContext filterContext)
{
if(filterContext.Exception.GetType() == typeof(ArgumentException))
{
filterContext.ExceptionHandled = true;
this.InvokeHttp404(filterContext.HttpContext);
}
else
{
base.OnException(filterContext);
}
}
On the surface it seems to work, but am I storing up any problems by doing this?
Is this semantically correct thing to do?
Best way? Action method selector attribute!
To actually avoid nullable method arguments I suggest that you write an Action Method Selector attribute that will actually only match your action method when id is supplied. It won't say that argument wasn't supplied but that it couldn't match any action methods for the given request.
I would call this action selector RequireRouteValuesAttribute and would work this way:
[RequireRouteValues("id")]
public ActionResult GetAccount(int id)
{
...
}
Why is this the best solution for your problem?
If you look at your code you'd like to return a 404 on actions that match name but parameter binding failed (either because it wasn't supplied or any other reason). Your action so to speak requires particular action parameter otherwise a 404 is returned.
So when adding action selector attribute adds the requirement on the action so it has to match name (this is given by MVC) and also require particular action parameters. Whenever id is not supplied this action is not matched. If there's another action that does match is not the issue here because that particular action will get executed. The main thing is accomplished. Action doesn't match for invalid route request and a 404 is returned instead.
There's an app code for that!
Check my blog post that implements this kind of attribute that you can use out of the box. It does exactly what you're after: it won't match your action method if route data provided doesn't have all required values.
Disclaimer: this does not cover all the cases
For urls in your examples, returning 404 can be done in single line. Just add route constraint for id parameter.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index" }, // Parameter defaults
new { id = #"\d+" } // restrict id to be required and numeric
);
And that's all. Now any matching url that has no id or id is not numeric, autimatically triggers not found error (for which there are plenty of ways to handle, one in your example, another by using custom HandleErrorAttribute, etc). And you can use non-nullable int parameters on your actions.
I managed to get this working by adding this route at the end of all routes:
routes.MapRoute("CatchAllErrors", "{*url}",
new { controller = "Error", action = "NotFound" }
);
Note: First I followed this: How can I properly handle 404 in ASP.NET MVC?