I've noticed what I believe to be some odd behavior with T4MVC. Specifically, I'm attempting to build an ActionLink (using the HtmlHelper) for an action where the optional parameter value is null. This works fine most of the time. However, if the current route is of the same for which the ActionLink is being built AND the OptionalParameter has a non-null value, the resulting ActionLink will specify the value of the optional parameter from the current route context.
That's a wordy explanation, I think code will help clarify.
Controller
public virtual ActionResult Today(int? lineNumber = null)
{
return Index(DateTime.Today, DateTime.Today, lineNumber);
}
Route
context.MapRoute(
"TodaysProductionSchedules",
"Production/{Controller}/Today/{lineNumber}",
new
{
area = AreaName,
controller = MVC.Production.ProductionSchedules.Name,
action = MVC.Production.ProductionSchedules.ActionNames.Today,
lineNumber = UrlParameter.Optional
});
Razor
#Html.ActionLink("Show Today", MVC.Production.ProductionSchedules.Today(null))
As I mentioned earlier, if I am not currently on a view which is mapped to this route, the link will be generated correctly. However, if the current view does map the this route AND I either omit the value or supply null (as seen in the razor snippet), the lineNumber parameter will take its value from the current route value.
I think this might be a bug in T4MVC so I'll post a link to this topic on the T4MVC codeplex site as well. Thanks in advance!
Update 7/30/2012: This is fixed in T4MVC 2.10.1!
This was actually a recent regression from the model unbinder change. In t4mvc.tt around line 639, can you try changing AddRouteValues to the following:
public static void AddRouteValues(RouteValueDictionary routeValueDictionary, string routeName, object routeValue) {
IModelUnbinder unbinder;
if (routeValue == null)
{
unbinder = DefaultModelUnbinder;
}
else
{
unbinder = ModelUnbinders.FindUnbinderFor(routeValue.GetType()) ?? DefaultModelUnbinder;
}
unbinder.UnbindModel(routeValueDictionary, routeName, routeValue);
}
Original answer:
I think generally in MVC, in many scenarios when a value is omitted from the new route, it gets its value from the current route, assuming that the high level values are the same (hence the two different cases you see).
So now the question is whether T4MVC can/should do something to avoid this behavior. I haven't checked the exact logic, but maybe if it always set this value in the route, that would disable this unwanted behavior.
But I think the first step is to fully understand the MVC behavior that's at play here before tackling the T4MVC case.
Feel free to take the investigation further and send a PR with the fix! :)
Related
I have an actionresult with two parameter:
public ActionResult Index(int a,string b)
{
//some code
return View(b);
}
it creates this url automatically:
mysite.com/a=1&b=http://site.com/b=1
I just need to show first parameter "a" in my url:
mysite.com/a=1
I use the default route of MVC that creates in global.ascx:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
what should i do?
Thanks...
If you are seeing the "b" parameter bleed through from the current request, you can set the "b" parameter explicitly to empty string to avoid this behavior:
#Html.ActionLink("Home", "Index", "Home", new { a = 1, b = "" }, null)
I reported this "feature" as a bug, but the developers at Microsoft seem to think this behavior is supposed to make your URLs easier to configure, and they don't intend to fix it.
What you're seeing here is a feature of routing where "ambient" values (i.e. values that were detected in the incoming request's URL) are used to simplify (sometimes...) the generation of routes to other pages.
You can check our my answer (under the name "Eilon") in this StackOverflow post, where I explain the behavior in a bit more detail:
How Can I Stop ASP.Net MVC Html.ActionLink From Using Existing Route Values?
Ultimately if you want the most control over what gets generated for a URL there are a few options to consider:
Use named routes to ensure that only the route you want will get used to generate the URL (this is often a good practice, though it won't help in this particular scenario)
Specify all route parameters explicitly - even the values that you want to be empty. That is one way to solve this particular problem.
Instead of using Routing to generate the URLs, you can use Razor's ~/ syntax or call Url.Content("~/someurl") to ensure that no extra (or unexpected) processing will happen to the URL you're trying to generate.
Thanks,
Eilon
I have been developing in classic ASP for about 2.5 years and I am trying to update my skill set to include ASP.NET MVC.
What is the MVC way of executing SSIs? IE: How can I include a database drawn navigation list in a sidebar? I have been looking into partial views, but they seem to get their content from the controller. As far as I can tell this means that I would need to write each controller to pass the navigation list.
Am I thinking along the right lines?
As an alternative to using RenderAction() (which has some drawbacks because it must run the entire ASP.NET request pipeline to get its output), you can use a BaseController type that all of your controllers inherit which overrides OnActionExecuted() to insert values into the ViewData collection. With this approach you will then have the benefit of using a single request, and still not having to worry about manually adding cross cutting data to your model every time you handle a request.
To keep things simple I like to use a public const string SomeDataItemViewDataKey = "Controller.DataName"; in the controller class definition to key the ViewData entry added by the controller and then in the view when I need to render that output I can use templated helpers to pull the value from the ViewData: <%=Html.DisplayFor(ControllerType.SomeDataItemViewDataKey, "PartialViewUsedToRenderTheData") %>.
Update
Because there's been some confusion about the validity of my statement, here is the original source of the performance claims against RenderAction():
Yes, there is a signficant difference
in performance of RenderAction
(slower) vs. RenderPartial (faster).
RenderAction, by definition, has to
run the whole ASP.NET pipeline to
handle what appears to the system to
be a new HTTP request, whereas
RenderPartial is just adding extra
content to an existing view.
-Brad Wilson, Senior developer on the ASP.NET MVC team
Quote source: http://forums.asp.net/p/1502235/3556774.aspx#3556590
Brad Wilson's blog: http://bradwilson.typepad.com/
Update 2
Here's the code from RenderAction() in the MVC2 RTM sources where we can see that although there is a new request being fired, it actually isn't going through the whole ASP.NET pipeline anymore. That being said, there are still some minor although mostly negligible drawbacks to using it over the alternative of PartialViews and ViewModel/ViewData. From what I understand (now) there was an overhaul on the implementation of RenderAction() prior to being moved from the MvcFutures assembly into the core framework; so it may hold that the statement above from Brad Wilson was more valid when he made it 6 months ago than it is now.
internal static void ActionHelper(HtmlHelper htmlHelper, string actionName, string controllerName, RouteValueDictionary routeValues, TextWriter textWriter) {
if (htmlHelper == null) {
throw new ArgumentNullException("htmlHelper");
}
if (String.IsNullOrEmpty(actionName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
}
routeValues = MergeDictionaries(routeValues, htmlHelper.ViewContext.RouteData.Values);
routeValues["action"] = actionName;
if (!String.IsNullOrEmpty(controllerName)) {
routeValues["controller"] = controllerName;
}
bool usingAreas;
VirtualPathData vpd = htmlHelper.RouteCollection.GetVirtualPathForArea(htmlHelper.ViewContext.RequestContext, null /* name */, routeValues, out usingAreas);
if (vpd == null) {
throw new InvalidOperationException(MvcResources.Common_NoRouteMatched);
}
if (usingAreas) {
routeValues.Remove("area");
}
RouteData routeData = CreateRouteData(vpd.Route, routeValues, vpd.DataTokens, htmlHelper.ViewContext);
HttpContextBase httpContext = htmlHelper.ViewContext.HttpContext;
RequestContext requestContext = new RequestContext(httpContext, routeData);
ChildActionMvcHandler handler = new ChildActionMvcHandler(requestContext);
httpContext.Server.Execute(HttpHandlerUtil.WrapForServerExecute(handler), textWriter, true /* preserveForm */);
}
You have two options that are similar to SSI
Use either RenderAction or Action (explanation of the difference between the two)
If you're using MVC2 there's also DisplayFor helper method which would allow you to define a custom display template for a particular object.
In your case Option 1 is probably the better one since you probably don't want to contain the items for the menu in your view model/view data.
Yes, partial views are the way to go. The controller should be handling the presentation logic. Similar to the code behind of web forms and web controls.
I would also utilize the master page for things like navigation.
If I understand your question right and this is something you want to have an every page, RenderAction could be your friend.
I have a problem with getting my pager-function to work. For some reason it doesnt like when I try to pass the current pageindex +1 to the same page for it to display next one.
<% if (Model.Users.HasNextPage) { %>
<%= Html.RouteLink(">>>", "Users", new { page = (Model.Users.PageIndex +1) })%>
<% } %>
If I only use: ”>>>”, ”Users) it works, although the next page function doesn’t work since it doesn’t assign next value.
If I debug Model.Users.PageIndex it has the value 0 when it loads the page (which it should have).
Somehow it doesn’t like the “new”-thingy at the end
I have swedish errors on, but it complains something about not finding the path/route/reference to the location of User, or how its set.
The actionresult looks like:
public ActionResult Users(int? page){
const int pagesize = 10;
var pagnatedUsers = new PaginatedList<User>(_us.GetUsers(), page ?? 0, pagesize);
return View("Users", new UserAdminEditViewModel { Users = pagnatedUsers });
}
Thanks in advance
/M
I'm going to guess "Users" in your second parameter to Html.RouteLink is supposed to refer to your controller action name. RouteLink actually doesn't have an overload of (string linkText, string actionName, object routeValues) which is what it appears you are trying to provide.
The overload you are calling is actually asking for the routeName in the second parameter, and you don't have such a route defined!
Try this
Html.RouteLink(">>>", new { controller="Home", action="Users", page = (Model.Users.PageIndex +1) })%>
substituting for your actual controller name.
Update/response: I was trying to explain why your code wasn't working as expected. Indeed, if you use ActionLink instead with your original parameters that is also a solution - and probably the better one since it seems to be what you want.
RouteLink and ActionLink are essentially the same under the covers (they both end up calling the same underlying code that actually generates the link). The difference is only in context of use - RouteLink is there to help you generate links based on your routing configuration (eg. by a route name) and ActionLink is there for links based on your controller actions (eg. by an action name). And there is plenty of overlap where you could use both of them in the exact same way.
I got that RouteLink code from the Nerddinner-example. And now when I changed to ActionLink instead of RouteLink it worked.
Not quite sure what the difference is between having ActionLink or the way Kurt describes.
What's the best way to handle a visitor constructing their own URL and replacing what we expect to be an ID with anything they like?
For example:
ASP.Net MVC - handling bad URL parameters
But the user could just as easily replace the URL with:
https://stackoverflow.com/questions/foo
I've thought of making every Controller Function parameter a String, and using Integer.TryParse() on them - if that passes then I have an ID and can continue, otherwise I can redirect the user to an Unknown / not-found or index View.
Stack Overflow handles it nicely, and I'd like to too - how do you do it, or what would you suggest?
Here's an example of a route like yours, with a constraint on the number:
routes.MapRoute(
"Question",
"questions/{questionID}",
new { controller = "StackOverflow", action = "Question" },
new { questionID = #"\d+" } //Regex constraint specifying that it must be a number.
);
Here we set the questionID to have at least one number. This will also block out any urls containing anything but an integer, and also prevents the need for a nullable int.
Note: This does not take into account numbers that larger than the range of Int32 (-2147483647 - +2147483647). I leave this as an exercise to the user to resolve. :)
If the user enters the url "questions/foo", they will not hit the Question action, and fall through it, because it fails the parameter constraint. You can handle it further down in a catchall/default route if you want:
routes.MapRoute(
"Catchall",
"{*catchall}", // This is a wildcard routes
new { controller = "Home", action = "Lost" }
);
This will send the user to the Lost action in the Home controller. More information on the wildcard can be found here.
NB: The Catchall should reside as the LAST route. Placing it further up the chain will mean that this will handle all others below it, given the lazy nature of routes in ASP.NET MVC.
Here is some useful infromation that might help.
If you have a action method
public ActionResult Edit(int? id)
{}
then if someone types in
/Home/Edit/23
the parameter id will be 23.
however if someone types in
/Home/Edit/Junk
then id will be null which is pretty cool. I thought it would throw a cast error or something. It means that if id is not a null value then it is a valid integer and can be passed to your services etc. for db interaction.
Hope this provides you with some info that I have found whilst testing.
In ASP.NET MVC, you can define a filter implementing IActionFilter interface. You will be able to decorate your action with this attribute so that it will be executed on, before or after your action.
In your case, you will define it to be executed "before" your action. So that, you will be able to cancel it if there is an error in the passed parameters. The key benefit here that you only write the code which checking the passed paramaters once (i.e you define it in your filter) and use it wherever you want in your controller actions.
Read more about MVC filters here: http://haacked.com/archive/2008/08/14/aspnetmvc-filters.aspx
You can specify constraints as regular expressions or define custom constraints. Have a look at this blog post for more information:
http://weblogs.asp.net/stephenwalther/archive/2008/08/06/asp-net-mvc-tip-30-create-custom-route-constraints.aspx
You will still need to deal with the situation where id 43243 doesn't map to anything which could be dealt with as an IActionFilter or in your controller directly.
The problem with that approach is that they still might pass an integer which doesn't map to a page. Just return a 404 if they do that, just as you would with "foo". It's not something to worry about unless you have clear security implications.
Up until now I've been able to get away with using the default routing that came with ASP.NET MVC. Unfortunately, now that I'm branching out into more complex routes, I'm struggling to wrap my head around how to get this to work.
A simple example I'm trying to get is to have the path /User/{UserID}/Items to map to the User controller's Items function. Can anyone tell me what I'm doing wrong with my routing here?
routes.MapRoute("UserItems", "User/{UserID}/Items",
new {controller = "User", action = "Items"});
And on my aspx page
Html.ActionLink("Items", "UserItems", new { UserID = 1 })
Going by the MVC Preview 4 code I have in front of me the overload for Html.ActionLink() you are using is this one:
public string ActionLink(string linkText, string actionName, object values);
Note how the second parameter is the actionName not the routeName.
As such, try:
Html.ActionLink("Items", "Items", new { UserID = 1 })
Alternatively, try:
Items
Can you post more information? What URL is the aspx page generating in the link? It could be because of the order of your routes definition. I think you need your route to be declared before the default route.
Firstly start with looking at what URL it generates and checking it with Phil Haack's route debug library. It will clear lots of things up.
If you're having a bunch of routes you might want to consider naming your routes and using named routing. It will make your intent more clear when you re-visit your code and it can potentially improve parsing speed.
Furthermore (and this is purely a personal opinion) I like to generate my links somewhere at the start of the page in strings and then put those strings in my HTML. It's a tiny overhead but makes the code much more readable in my opinion. Furthermore if you have or repeated links, you have to generate them only once.
I prefer to put
<% string action = Url.RouteUrl("NamedRoute", new
{ controller="User",
action="Items",
UserID=1});%>
and later on write
link
Html.ActionLink("Items", "User", new { UserID = 1 })