How to make MVC Routing handle url with dashes - asp.net-mvc

I have a route defined as follows in MVC:
routes.MapRoute(
name: "ContentNavigation",
url: "{viewType}/{category}-{subCategory}",
defaults: new { controller = "Home", action = "GetMenuAndContent", viewType = String.Empty, category = String.Empty, subCategory = String.Empty });
If I navigate to http://example.com/something/category-and-this-is-a-subcategory
It fills the variables as:
viewType: "something"
category: "category-and-this-is-a"
subCategory: "subcategory".
What I want is for the word before the first dash to always go into category, and the remaining into subcategory. So it would produce:
viewType: "something"
category: "category"
subCategory: "and-this-is-a-subcategory"
How can I achieve this?

One possibility is to write a custom route to handle the proper parsing of the route segments:
public class MyRoute : Route
{
public MyRoute()
: base(
"{viewType}/{*catchAll}",
new RouteValueDictionary(new
{
controller = "Home",
action = "GetMenuAndContent",
}),
new MvcRouteHandler()
)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var rd = base.GetRouteData(httpContext);
if (rd == null)
{
return null;
}
var catchAll = rd.Values["catchAll"] as string;
if (!string.IsNullOrEmpty(catchAll))
{
var parts = catchAll.Split(new[] { '-' }, 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 1)
{
rd.Values["category"] = parts[0];
rd.Values["subCategory"] = parts[1];
return rd;
}
}
return null;
}
}
that you will register like that:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add("ContentNavigation", new MyRoute());
...
}
Now assuming that the client requests /something/category-and-this-is-a-subcategory, then the following controller action will be invoked:
public class HomeController : Controller
{
public ActionResult GetMenuAndContent(string viewType, string category, string subCategory)
{
// viewType = "something"
// category = "category"
// subCategory = "and-this-is-a-subcategory"
...
}
}

Related

The controller for path " " was not found or does not implement IController. error

I pull the tags in the news details section. The corresponding code block is below.
NewsDetail:
foreach (var item in etiketler.Take(1))
{
<span>#item</span>
}
foreach (var item in etiketler.Skip(1))
{
<span>#item</span>
}
Controller :
public ActionResult Tag(string tag, int? pageSize)
{
string id = this.RouteData.Values["tag"].ToString();
SectionServices _sectionServices = new SectionServices();
if (!pageSize.HasValue) pageSize = 1;
ViewBag.Current = pageSize;
Models.TagModel model = new Models.TagModel();
var dat = _sectionServices.getNewsByTag((int)pageSize, tag);
ViewData["etiket"] = tag;
if (dat != null)
{
ViewBag.Tag = tag;
model.getNews = dat;
return View(model);
}
return View();
}
Route Config :
routes.MapRoute(
name: "TagPage",
url: "{tag}-haberleri/{pageSize}",
defaults: new { controller = "Page", action = "Tag", pageSize = UrlParameter.Optional }
);
I get errors like "The controller for path '/Mert Hakan_haberleri / 2' was not found or does not implement IController" in log records. what is the cause of this error, clicking on tags works correctly, but I see this error in log records.
I also had this error. When I embedded the classes into a namespace, everything started working for me.
namespace PageControllers { // added this line!
public class PageController {
public ActionResult Tag() {
//code logic
return View();
}
}
}

ASP.NET MVC5 Change Language using Route

I'm new using MVC5 and I try to do a website with different language.
So far I do some configuration and when I click a different language it changes the url but it doesn't change the text in view.
Here is my code if anyone have idea:
In App_Data folder I have 2 files: CultureConstraint and CultureFilter
public class CultureFilter : IAuthorizationFilter
{
private readonly string defaultCulture;
public CultureFilter(string defaultCulture)
{
this.defaultCulture = defaultCulture;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var values = filterContext.RouteData.Values;
string culture = (string)values["culture"] ?? this.defaultCulture;
CultureInfo ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
public class CultureConstraint : IRouteConstraint
{
private readonly string defaultCulture;
private readonly string pattern;
public CultureConstraint(string defaultCulture, string pattern)
{
this.defaultCulture = defaultCulture;
this.pattern = pattern;
}
public bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration &&
this.defaultCulture.Equals(values[parameterName]))
{
return false;
}
else
{
return Regex.IsMatch((string)values[parameterName], "^" + pattern + "$");
}
}
}
In RouteConfig I add:
routes.MapRoute(
name: "DefaultWithCulture",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { culture = new CultureConstraint(defaultCulture: "en", pattern: "[a-z]{2}") }
);
In filterConfig:
filters.Add(new CultureFilter(defaultCulture: "en"));
In the _Layout View Page:
#using Test.App_LocalResources;
#{
var routeValues = this.ViewContext.RouteData.Values;
var controller = routeValues["controller"] as string;
var action = routeValues["action"] as string;
}
<ul>
<li>#Html.ActionLink("Russia", #action, #controller, new { culture = "ru" }, new { rel = "alternate", hreflang = "ru" })</li>
<li>#Html.ActionLink("English", #action, #controller, new { culture = "en" }, new { rel = "alternate", hreflang = "en" })</li>
</ul>
<div style="background: silver">
#GlobalRes.Money: <br />
#GlobalRes.DateAndTime: <br />
</div>
And Also in the View Page:
#using Test.App_LocalResources;
#{
ViewBag.Title = GlobalRes.Title;
}
I put the language file in App_LocalResources with the name Global.resx and Global.ru.resx.
So Title, Money and DateTime always get the english text never the russia one.
Did I miss anything?

Url.routeUrl returns null (I've tried same questions)

Hi everyone I wanna get url like {Controller}/{action}/{postid}-{address}
but routUrl returns null please help me to solve it.(I'm newbie in MVC)
my route config is
routes.MapRoute(
name: "Post",
url: "Posts/Show/{postid}-{address}",
defaults: new { controller = "Posts", action = "Index", postid = "", address = "" }
);
and index.cshtml
#item.PostTitle
the url that generate is
http://localhost:59066/Posts/Show/1-Post-with-Featured-Image
but in PostsController
public ActionResult Show(string add)
{ return View();}
"string add" is null !
I wouldn't change the Routes...
try this...
#item.PostTitle
This will send PostId and Address as parameters, so you can get them in the controller like:
public ActionResult AwesomeThings(int PostId, String Address)
{
var foo = PostId;
var bar = Address;
return View(model);
}
No changes in routing,
Index.cshtml:
#item.PostTitle
Controller:
public ActionResult Show(string postid, string address)
{ return View();}
I changed the route to
routes.MapRoute("Post", "post/{postid}-{address}", new { controller = "Posts", action = "Show" ,postid="",address=""}, namespaces);
and added a route with same controller and action
routes.MapRoute("PostAddress", "post/{IdAndAdd}", new { controller = "Posts", action = "Show" }, namespaces);
routes.MapRoute("Post", "post/{postid}-{address}", new { controller = "Posts", action = "Show" ,postid="",address=""}, namespaces);
then action received "idAndAdd" correctly
public ActionResult Show(string idAndAdd)
{
var parts = SeperateAddress(idAndAdd);
if (parts == null)
return HttpNotFound();
var post = db.Posts.Find(parts.Item1);
if (post == null)
return HttpNotFound();
if (!post.Address.Equals(parts.Item2, StringComparison.CurrentCultureIgnoreCase))
return RedirectToRoutePermanent("Post", new { postid = parts.Item1, address = post.Address });
return View(post);
}
and it's worked .

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 ?

Resources