MVC RouteDatas are confused - asp.net-mvc

I am using asp.net mvc for my website project. I think i have wrong things in my routedata but i am not sure it is wrong or ok. i will explain the situation.
I am caching my action results (html outputs) in Cache with a generated key
public static string GetKeyFromActionExecutingContext(ControllerContext filterContext)
{
StringBuilder keyBuilder = new StringBuilder();
if (filterContext.IsChildAction)
keyBuilder.Append("C-");
else
keyBuilder.Append("P-");
foreach (var item in filterContext.RouteData.Values)
{
keyBuilder.AppendFormat("{0}={1}.", item.Key, item.Value);
}
return keyBuilder.ToString();
}
ex: For HomePage , generated cache key is P-Controller=Home.Action=Index and
I have also childactions in my sitemaster like LoginBox(It is in MembershipController/LoginBox)
Its cache key is C-Controller=Membership.Action=LoginBox.
Everything is okey till now.
I have also subcategories in my website like
domain/category1
domain/category1/subcategory1
domain/category1/subcategory2
domain/category2
When i am browsing a sub category from domain/category1
My generated keys are failed because my routedatas are wrong
filterContext.RouteData.Values:
Controller = Membership
Action = LoginBox
ctg1 = category1
ctg2 = ""
ctg3 = ""
Why these are mixed. It is using the "Category" routemapping but I think it must use "Default" routemapping.
My global.asax like below
routes.MapRoute(
"Category",
"{ctg0}/{ctg1}/{ctg2}/{ctg3}",
new
{
controller = "Category",
action = "Index",
ctg0 = "",
ctg1 = "",
ctg2 = "",
ctg3 = ""
},
new
{
ctg0 = new CategoryRouteConstraint(),
}
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" },
new { controller = #"[^\.]*" }
);
Also my CategoryRouteConstraint Method it is checking from db that ctg0 value is a category name
public class CategoryRouteConstraint : IRouteConstraint
{
public Boolean Match(
HttpContextBase httpContext,
Route route,
String sParameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
if ((routeDirection == RouteDirection.IncomingRequest))
{
if (values["ctg0"] != null && !string.IsNullOrEmpty(values["ctg0"].ToString()))
return Category.IsRoutingForCategory(values["ctg0"].ToString());
return false;
}
return false;
}
}

Hopefully this may help you, it will show you which routes a url matches, I was a little confused by the question.
http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx

Related

Route with Two optional parameters in MVC3 not working

I have a following types of url used in my Application.
localhost/admin/userdetail/id
localhost/admin/userdetail/id/true
localhost/admin/userdetail/id/true/success
Here is my Admin Controller
bool inSaveAction, string status are optional
[Authorize]
public ActionResult UserDetail(string Id, bool inSaveAction, string status)
{
}
[HttpPost, Authorize, ValidateAntiForgeryToken]
public ActionResult SaveUserDetail(UserDetailViewModel viewModel)
{
User userToSave = new User();
AdminService.UpdateUser(userToSave);
//This is calling the above function as it sending all 3 params
return RedirectToAction("UserDetail", new { Id = viewModel.Id,
inSaveAction = true, status = "success" });
}
Below case is not working
#Html.ActionLink("DisplayName", "UserDetail", new { id = Model.Id })
In Global.asax
routes.MapRoute("UserDetail",
"UserDetail/{id}",
new
{
controller = "Admin",
action = "UserDetail",
id = UrlParameter.Optional
}
);
I followed http://haacked.com/archive/2011/02/20/routing-regression-with-two-consecutive-optional-url-parameters.aspx
How can i make inSaveAction & status as optional parameter for my UserDetail action?
You're missing the parameters in your route config. In order to make this work with different parameters optional (as in Phil Haack's post), you need to define multiple routes
routes.MapRoute("UserDetail-WithStatus",
"UserDetail/{id}/{inSaveAction}/{status}",
new
{
controller = "Admin",
action = "UserDetail",
// nothing optional
}
);
routes.MapRoute("UserDetail-WithoutStatus",
"UserDetail/{id}/{inSaveAction}",
new
{
controller = "Admin",
action = "UserDetail",
// nothing optional
}
);
routes.MapRoute("UserDetail-WithoutSaveAction",
"UserDetail/{id}",
new
{
controller = "Admin",
action = "UserDetail",
id = UrlParameter.Optional
}
);
And then create links with:
#Html.ActionLink("Link", "Index", "Admin", new { id = 1, inSaveAction = true, success = "success" }, null)
You'll also need to set the optional parameters as nullable, otherwise you'll get exceptions if id or inSaveAction are missing.
public ActionResult UserDetail(int? id, bool? inSaveAction, string status)
{
}

stackoverflow URL rewrite

How does SO perform the URL rewrite if we only put in the question ID?
questions/{id}/{whatever}
to
questions/{id}/{question-slug}
I've been working for some time with MVC and I have it working (routes, action, everything) so that it picks up the right content based on the provided ID.
However, the part after the {id} (the slug part) stays the same as typed in. So if someone typed in content/5/foobar it will display the right content but will leave the foobar in there.
In the controller (or somewhere else, please suggest where) I would need to go into the DB and pull out the right slug, put it in the route data and then perform a redirect to the same action with this correct data, I guess?
This is a try with Execute Result override. It works but does not redirect or replace/display the correct URL in browser:
protected override void Execute(System.Web.Routing.RequestContext requestContext) {
if (requestContext.RouteData.Values["id"] != null) {
string currentSlug = _repository.Find(int.Parse(requestContext.RouteData.Values["id"].ToString())).Slug;
if (requestContext.RouteData.Values["slug"] != null) {
requestContext.RouteData.Values.Remove("slug");
}
requestContext.RouteData.Values.Add("slug", currentSlug);
}
base.Execute(requestContext);
}
This is another, nicely working, version of a Display action, so you can see what it does and get an idea what I want:
//
// GET: {culture}/directory/5/{slug}
public virtual ActionResult Display(int id, string slug)
{
var model = _repository.Find(id);
if (model != null) {
if (!model.Slug.Equals(slug, System.StringComparison.OrdinalIgnoreCase)) {
return RedirectToActionPermanent(pndng.DirectoryEntry.ActionNames.Display, pndng.DirectoryEntry.Name, new { id = model.Id, slug = model.Slug });
}
return View(model);
}
// no model found
return InvokeHttp404(HttpContext);
}
This one performs permanent redirect (it does what I want) but is it right?
I guess I need a redirect to refresh the browser URL, don't I?
public ActionResult Details(int id, string slug)
{
var session = MvcApplication.CurrentRavenSession;
var blogPostRelations = session
.Query<BlogPost, BlogPosts_WithRelatedData>()
.Where(x => x.IntId == id)
.As<BlogPostRelations>()
.FirstOrDefault()
;
if (blogPostRelations == null)
return HttpNotFound();
if (blogPostRelations.BlogPost.DisplayData.Slug.Value != slug)
return RedirectToActionPermanent("Details", new { id = id, slug = blogPostRelations.BlogPost.DisplayData.Slug.Value });
return View(blogPostRelations);
}
Notice the:
if (blogPostRelations.BlogPost.DisplayData.Slug.Value != slug)
return RedirectToActionPermanent("Details", new { id = id, slug = blogPostRelations.BlogPost.DisplayData.Slug.Value });
So your #2 approach is the right one.
You could write a custom route for this:
public class QuestionsRoute : Route
{
public QuestionsRoute()
: base(
"questions/{id}/{slug}",
new RouteValueDictionary(new
{
controller = "questions",
action = "index",
slug = UrlParameter.Optional
}),
new RouteValueDictionary(new
{
id = #"\d+"
}),
new MvcRouteHandler()
)
{ }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var id = rd.GetRequiredString("id");
var slug = rd.Values["slug"] as string;
if (string.IsNullOrEmpty(slug))
{
slug = GoFetchSlugFromDb(id);
if (string.IsNullOrEmpty(slug))
{
return null;
}
httpContext.Response.RedirectToRoutePermanent(new
{
action = "index",
controller = "questions",
id = id,
slug = slug
});
return null;
}
return rd;
}
private string GoFetchSlugFromDb(string id)
{
// TODO: you know what to do here
throw new NotImplementedException();
}
}
which will be registered in Application_Start:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("questions", new QuestionsRoute());
}
Now your QuestionsController will be pretty simple:
public class QuestionsController: Controller
{
public ActionResult Index(int id, string slug)
{
...
}
}

Custom routes with ASP.NET MVC for faceted search [ from QueryString to Route ]

I'm implementing a faceted search functionality where the user can filter and drill down on 4 properties of my model: City, Type, Purpose and Value.
I have a view section with the facets like this:
Each line displayed in the above image is clickable so that the user can drill down and do the filtering...
The way I'm doing it is with query strings that I pass using a custom ActionLink helper method:
#Html.ActionLinkWithQueryString(linkText, "Filter",
new { facet2 = Model.Types.Key, value2 = fv.Range });
This custom helper keeps the previous filters (query string parameters) and merges them with new route values present in other action links. I get a result like this when the user has applied 3 filters:
http://leniel-pc:8083/realty/filter?facet1=City&value1=Volta%20Redonda&
facet2=Type&value2=6&facet3=Purpose&value3=3
It's working but I'd like to know about a better/cleaner way of doing this using routes. The order of the parameters can change depending on the filters the user has applied. I have something like this in mind:
http://leniel-pc:8083/realty/filter // returns ALL rows
http://leniel-pc:8083/realty/filter/city/rio-de-janeiro/type/6/value/50000-100000
http://leniel-pc:8083/realty/filter/city/volta-redonda/type/6/purpose/3
http://leniel-pc:8083/realty/filter/type/7/purpose/1
http://leniel-pc:8083/realty/filter/purpose/3/type/4
http://leniel-pc:8083/realty/filter/type/8/city/carangola
Is this possible? Any ideas?
Is this possible? Any ideas?
I would keep the query string parameters for filtering.
But if you wanted to achieve the urls you have asked for in your question I will cover 2 possible techniques.
For both approaches that I will present here I assume that you already have a view model:
public class FilterViewModel
{
public string Key { get; set; }
public string Value { get; set; }
}
and a controller:
public class RealtyController : Controller
{
public ActionResult Filter(IEnumerable<FilterViewModel> filters)
{
... do the filtering ...
}
}
The first option is to write a custom model binder that will be associated with the IEnumerable<FilterViewModel> type:
public class FilterViewModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var filtersValue = bindingContext.ValueProvider.GetValue("pathInfo");
if (filtersValue == null || string.IsNullOrEmpty(filtersValue.AttemptedValue))
{
return Enumerable.Empty<FilterViewModel>();
}
var filters = filtersValue.AttemptedValue;
var tokens = filters.Split('/');
if (tokens.Length % 2 != 0)
{
throw new Exception("Invalid filter format");
}
var result = new List<FilterViewModel>();
for (int i = 0; i < tokens.Length - 1; i += 2)
{
var key = tokens[i];
var value = tokens[i + 1];
result.Add(new FilterViewModel
{
Key = tokens[i],
Value = tokens[i + 1]
});
}
return result;
}
}
which will be registered in Application_Start:
ModelBinders.Binders.Add(typeof(IEnumerable<FilterViewModel>), new FilterViewModelBinder());
and you will also have a filter route:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Filter",
"realty/filter/{*pathInfo}",
new { controller = "Realty", action = "Filter" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
A second possibility is to write a custom route
public class FilterRoute : Route
{
public FilterRoute()
: base(
"realty/filter/{*pathInfo}",
new RouteValueDictionary(new
{
controller = "realty", action = "filter"
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var filters = rd.Values["pathInfo"] as string;
if (string.IsNullOrEmpty(filters))
{
return rd;
}
var tokens = filters.Split('/');
if (tokens.Length % 2 != 0)
{
throw new Exception("Invalid filter format");
}
var index = 0;
for (int i = 0; i < tokens.Length - 1; i += 2)
{
var key = tokens[i];
var value = tokens[i + 1];
rd.Values[string.Format("filters[{0}].key", index)] = key;
rd.Values[string.Format("filters[{0}].value", index)] = value;
index++;
}
return rd;
}
}
which will be registered in your RegisterRoutes method:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("Filter", new FilterRoute());
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
In my opinion (and this is pretty subjective) your initial approach seems fine. I think search criteria belong on the querystring as they represent a subset of the resources you're trying to retrieve.
Your urls don't make much sense from a logical resource hierarchy point of view.
I would probably rename the "filter" method "search" however, with the filters being the querystring variables. Also, is it necessary to define facets in the querystring - can't you achieve the same result by naming the facet explicity, like ?city=Volta&type=6&purpose=3 ?

asp .net mvc routing url with custom literal

Is it possible to make url with custom literal separator that can have default parameters ?
context.MapRoute(
"Forums_links",
"Forum/{forumId}-{name}",
new { area = "Forums", action = "Index", controller = "Forum" },
new[] { "Jami.Web.Areas.Forums.Controllers" }
);
I have this as you see im using to dash to separate id from name so I can have url like:
/Forum/1-forum-name
Instead of:
/Forum/1/forum-name
I see the problem is I'm using multiple dashes. And routing engine don't know which one to separate. But overalll it doesn't change my question because I want to use multiple dashes anyway.
Very interesting question.
The only way I could come up with is much like Daniel's, with one extra feature.
context.MapRoute(
"Forums_links",
"Forum/{forumIdAndName}",
new { area = "Forums", action = "Index", controller = "Forum" },
new { item = #"^\d+-(([a-zA-Z0-9]+)-)*([a-zA-Z0-9]+)$" } //constraint
new[] { "Jami.Web.Areas.Forums.Controllers" }
);
That way, the only items that will get matched to this route are ones formatted in the pattern of:
[one or more digit]-[zero or more repeating groups of string separated by dashes]-[final string]
From here you would use the method Daniel posted to parse the data you need from the forumIdAndName parameter.
One way to achieve this could be by combining id and name into the same route value:
context.MapRoute(
"Forums_links",
"Forum/{forumIdAndName}",
new { area = "Forums", action = "Index", controller = "Forum" },
new[] { "Jami.Web.Areas.Forums.Controllers" }
);
And then extract the Id from it:
private static int? GetForumId(string forumIdAndName)
{
int i = forumIdAndName.IndexOf("-");
if (i < 1) return null;
string s = forumIdAndName.Substring(0, i);
int id;
if (!int.TryParse(s, out id)) return null;
return id;
}

GetVirtualPath method in mvc

Can anyone explain how the virtual path is being calculating?
According to the RouteData.Values or according to the url pattern?
I'm trying to remove some routedata values but still the virtual path is not changes.
I have a problem that the virtual path return with redundant slash at the beginning of the URL like : /he/controller/action the slash before culture is redundant...
I'm using custom routes like the following
routes.Add("Default",
new CustomRoute("{culture}/{controller}/{action}/{id}",
new
{
controller = "Desktop",
action = "Index",
culture = "he-IL",
guid = "",
id = UrlParameter.Optional
}));
routes.Add("Wizard_" + wizard,
new CustomRoute("{guid}/{culture}/" + wizardName + "/{action}/{id}",
new
{
controller = wizard,
action = "Index",
culture = "he-IL",
guid = "",
id = UrlParameter.Optional
}));
the problem is when using Url.Action(action, controller) method and the action is in the wizard controller, so the URL for the action is wizard format like {guid}/{culture}/" + wizard + "/{action}/{id}
bu the guid value is empty and the returned URL is //he-il/controller/action
instead of /he-il/controller/action
The CustomRoute class:
public class CustomRoute : Route
{
private List<string> _wizards;
public CustomRoute(string uri, object defaults)
: base(uri, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
_wizards = new List<string>();
FillWizards(ref _wizards);
DataTokens = new RouteValueDictionary();
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
bool hasGuid = httpContext.Request.RequestContext.RouteData != null
&& httpContext.Request.RequestContext.RouteData.Values != null
&& httpContext.Request.RequestContext.RouteData.Values.ContainsKey("guid")
&& !httpContext.Request.RequestContext.RouteData.Values["guid"].ToString().Equals(Guid.Empty);
var routeData = base.GetRouteData(httpContext);
if (routeData == null)
return null;
bool isWizard = _wizards.Contains(routeData.Values["controller"].ToString());
Debug.WriteLine("Controller: " + routeData.Values["controller"] + " action: " + routeData.Values["action"] + " Is wizard: " + isWizard + " has guid: " + hasGuid);
if (isWizard && !hasGuid)
{
if (string.IsNullOrEmpty(routeData.Values["guid"].ToString()))
{
routeData.Values["guid"] = Guid.NewGuid().ToString("N");
}
}
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
VirtualPathData path;
path = base.GetVirtualPath(requestContext, values);
return path;
}
private void FillWizards(ref List<string> items)
{
var _configuration = ObjectFactory.GetInstance<IConfiguration>();
List<string> wizards = _configuration.GetParamValue<string>("SessionUniqueWizards", "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
items = wizards;
}
}
The reason why your guid parameter is missing is because
There is no guid parameter in your call to Url.Action(action, controller).
There is (apparently) no guid parameter in the current request. That is, the current route that is being hit has no guid route value.
You have specified the default value for the guid (guid = ""). Since empty string is what you specified as the default, empty string is what you are getting by default.
For the URL to build correctly, the guid has to come from somewhere. MVC always passes matching route values from the current request when building outgoing URLs, but since not all of your URLs have a guid you need to specify it for the pages where it does not exist in the context:
Url.Action("Index", "Search", new { guid = "a565f84f9152495792d433f5bd26000f")
This is the normal way to do it. Typically, if you are building the link for a CRUD operation you are doing so within a list of entities.
For example, for a Product entity, you would normally have links for Edit Product and Delete Product for each Product in a list that would look something like this:
<tr>
<td>model.ProductName</td>
<td>#Html.ActionLink("Product", "Edit", new { guid = model.ProductId })</td>
<td>#Html.ActionLink("Product", "Delete", new { guid = model.ProductId })</td>
</tr>
There would also typically be a link to add a new entity that has no identifier.
#Html.ActionLink("Product", "Add")
But it is unclear from your example how a "wizard" would be created. Creating a new identifier should normally be a function of the Add method, not that of a route.
But there seems to be an issue where you are randomly generating a GUID within your route, so it is unclear how you expect this value to be maintained from one request to the next.

Resources