How to map model data in URL route? - c#

I have an index page which displays Categories (which have attributes: id and name) with the URL request: http://localhost:62745/home/index.
When I click a category, I am taken to http://localhost:62745/Home/Products/6.
I want to make the URL more detailed and replace the Category Id property 6 in the previous URL with the name property of the Category which was just clicked.
My route config look like this:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Categories",
url: "{controller}/{action}/{categoryName}",
defaults: new { controller = "Category", action = "Index", categoryName = UrlParameter.Optional }
);
}
}
The first MapRoute() method was already implemented. I added the second one hoping to solve my problem, but it didn't.
This is my controller action for products:
// GET: Product
public async Task<ActionResult> Index(int? id, string categoryName)
{
var products = (await db.Categories.Where(c => c.Id == id)
.SelectMany(p => p.Products.Select(x => new ProductViewModel { Id = x.Id, Name = x.Name, ByteImage = x.Image, Price = x.Price}))
.ToListAsync());
categoryName = db.Categories.Where(c => c.Id == id).Select(c => c.Name).ToString();
if (products == null)
{
return HttpNotFound();
}
return View(new ProductIndexViewModel{ Products = products, CategoryId = id });
}

Both your routes are identical in terms how they are interpreted - they accept 2 segments and an optional 3rd segment, so Home/Products/6 and Home/Products/CategoryName both match the first Default route (there is nothing unique to distinguish the 2nd Categories route so it will never be hit.
Your question refers to a Products() method in the 2nd url, but you have not shown that method, so its not clear if your referring to a product name or a category name, but the principal is the same.
Assuming you want the url to be Home/Products/ProductName to display a specific product, then in the view
#foreach(var item in Model.Products)
{
// link for each product
#Html.ActionLink("Details", "Products", "Home", new { id = item.Name }, null)
Then you can simply make the method
public ActionResult Products(string id)
{
// the value of id is the the the product name
....
}
Alternatively, if you want the parameter to be string name rather that string id, then you can define a specific route and place it before the Default route.
routes.MapRoute(
name: "ProductDetails",
url: "Home/Products/{name}",
defaults: new { controller = "Home", action = "Products", name = UrlParameter.Optional }
);
so that the method becomes
public ActionResult Products(string name)
Side note: The fact you have a query in the Index() method to get the category name suggests you might also be wanting a 'slug' route, in which case, refer how to implement url rewriting similar to SO.

Related

Setting custom MVC routes with fixed parts

I have 2 controllers:
public class HomeController : Controller
{
public ActionResult Index(string id) //id is category slug
{
if(string.IsNullOrEmpty(id))
id = "MainCategory";
var model = getCategoryPageModel(id);
return View(model);
}
}
public class PostController : Controller
{
public ActionResult Index(string id) //id is post slug
{
var model = getPostModel(id);
return View(model);
}
}
And this is my route config:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//angular route for admin section
routes.MapRoute(
name: "AngularCatchAllRoute",
url: "Admin/{*any}",
defaults: new { controller = "Admin", action = "Index"}
);
//route which includes language
routes.MapRoute(
name: "DefaultLocalized",
url: "{lang}/{controller}/{action}/{id}",
constraints: new { lang = #"(\w{2})|(\w{2}-\w{2})" }, // en or en-US
defaults: new { controller = "Home", action = "Index", id = "" }
);
//do I need this one??
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
I want to have two types of custom routes:
www.mysite.xyz/categoryName - calls Home/Index/id with default language ('sr')
www.mysite.xyz/en/categoryName - calls Home/Index/id with language 'en'
www.mysite.xyz/Post/postID - calls Post/Index/id with default language('sr')
www.mysite.xyz/en/Post/postID - calls Post/Index/id with language 'en'
My 'DefaultLocalized' route already works fine with default and custom language route part, but my url has to contain all route parts: controller/action/id. I just want to simplyfy urls to be more readable to users.
Actually I made it work for post with 'lang' to be mandatory:
www.mysite.xyz/sr/Post/postID - but I want 'sr' to be default like in 'DefaultLocalized' route, there I don't have to set lang to be 'sr'...
I already have tried some answers from other similar questions but I did not make it work.
You can use attribute routing as below:
[Route("~/{cate}")]
[Route("{lang}/{cate}")]
public IActionResult Index(string lang, string cate)
{
return View();
}
That work for me with urls:
http://[host]/Mobile
http://[host]/en/Mobile
Hope this help!

Routing in ASP.NET MVC, showing username in URL

I'm trying to make a route so I can show the username in the URL like this:
http://localhost1234/john
Here Is my routeconfig:
routes.MapRoute(
name: "users", // Route name
url: "{username}", // URL with parameters
defaults: new { controller = "Home", action = "Index", username = "" } // Parameter defaults
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Here is my HomeController:
public ActionResult Index(string username = "Test")
{
return View();
}
First of all, the URL Is not changed. When I set username = "Test" inside my route-config, the URL is not changed.
Second, I can't navigate to my other controllers. If I change the URL to http://localhost123/Welcome, nothing happens. It should redirect me to a new page.
What am I doing wrong here?
If I change the order of the routes, I can navigate to other pages, but the username Is not displayed In the URL.
I have googled and all of the answers on this subject says that I should use a route like the one above.
On its own, your routing will not work because if the url was .../Product meaning that you wanted to navigate to the Index() method of ProductController, it would match your first route (and assume "Product" is the username. You need to add a route constraint to your roue definitions that returns true if the username is valid and false if not (in which case it will try the following routes to find a match).
Assuming you have a UserController with the following methods
// match http://..../Bryan
public ActionResult Index(string username)
{
// displays the home page for a user
}
// match http://..../Bryan/Photos
public ActionResult Photos(string username)
{
// displays a users photos
}
Then you route definitions need to be
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "User",
url: "{username}",
defaults: new { controller = "User", action = "Index" },
constraints: new { username = new UserNameConstraint() }
);
routes.MapRoute(
name: "UserPhotos",
url: "{username}/Photos",
defaults: new { controller = "User", action = "Photos" },
constraints: new { username = new UserNameConstraint() }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Test", action = "Index", id = UrlParameter.Optional }
);
}
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> users = new List<string>() { "Bryan", "Stephen" };
// Get the username from the url
var username = values["username"].ToString().ToLower();
// Check for a match (assumes case insensitive)
return users.Any(x => x.ToLower() == username);
}
}
}
If the url is .../Bryan, it will match the User route and you will execute the Index() method in UserController (and the value of username will be "Bryan")
If the url is .../Stephen/Photos, it will match the UserPhotos route and you will execute the Photos() method in UserController (and the value of username will be "Stephen")
If the url is .../Product/Details/4, then the route constraint will return false for the first 2 route definitions and you will execute the Details() method of ProductController
If the url is .../Peter or .../Peter/Photos and there is no user with username = "Peter" then it will return 404 Not Found
Note that the the sample code above hard codes the users, but in reality you will call a service that returns a collection containing the valid user names. To avoid hitting the database each request, you should consider using MemoryCache to cache the collection. The code would first check if it exists, and if not populate it, then check if the collection contains the username. You would also need to ensure that the cache was invalidated if a new user was added.
You need to categorize the url for different section of your website so that url pattern matching mechanism go smooth. For example in your case put a category 'profile' or anything other. Now your request url look like http://localhost1234/profile/john and route will be
routes.MapRoute(
name: "users", // Route name
url: "Profile/{username}", // URL with parameters
defaults: new { controller = "Home", action = "Index" } // Parameter defaults
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
For more information follow link Routing in MVC

Passing a non numeric value for default "id" parameter in MVC Controller

I have a fairly simple setup where I am creating a MVC website to display the details of a customer. For customers the unique id is their email address which is non-numeric. So in my ROuteConfig.cs I have the default route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
In my controller called "CustomerController" I have this action
public ActionResult Details(string id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
CustomerModel customerModel = _customerManager.GetCustomer(id);
if (customerModel == null)
{
return HttpNotFound();
}
return View(customerModel);
}
Now when I navigate to this url
http://localhost:45826/customer/Details/someemail%40web.com
the routing infrastructure does not invoke the Action "Details" on my controller, however if I navigate to this url
http://localhost:45826/customer/Details/5
then the action is invoke passing in the value 5 for the id parameter.
If I change the URL a bit and use this syntax
http://localhost:45826/customer/Details?id=fromweb%40web.com
Again the action is invoked properly passing the email address to the id parameter.
Can someone help understand why non-numeric values aren't mapping to the action as expected?
I have also tried adding this route before the default route but that doesn't work too and I get same results
routes.MapRoute(
name: "ViewCustomer",
url: "customer/details/id",
defaults: new { controller = "Customer", action = "Details", id = "" }
);
Add this in web.config:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
Read:
https://stackoverflow.com/a/49831093/10299573

ASP.NET MVC ActionLink not selecting correct controller

Question background:
I'm trying to pass an variable - in this case an int 'productId' variable' in the url to a controller and action method specified in the ActionLink method.
The issue:
My routes are set as follows:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Login", action = "Login", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "ProductDetailHandler",
url: "{controller}/{action}/{productId}",
defaults: new { controller = "Product", action = "ProductDetail", productId = UrlParameter.Optional }
);
My 'ActionLink' in my 'Products.cshtml' view - which is returned by a controller called 'HomePageController' is set as follows:
#Html.ActionLink("Product", "ProductDetail", new { productId = (ViewBag.data as List<LoginTest.Models.HomePageItem>).First().Id })
The controller that receives the passed 'productId' along with its action method is set as follows:
public class ProductController : Controller
{
public ActionResult ProductDetail(int productId)
{
//logic
return View();
}
}
This is the issue, when looking at the URL it is shown to be redirecting to the 'HomePage' controller:
If someone could tell me why my ActionLink is not going to the Product controller that would be great.
EDIT:
This is the 'Homepage' view that I click a button to redirect me to 'product/productDetail/productId'
My route now just features the 'Default example':
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Login", action = "Login", id = UrlParameter.Optional }
);
The Action Link is now:
#Html.ActionLink("Product", "ProductDetail", "Product", new { id = (ViewBag.data as List<LoginTest.Models.HomePageItem>).First().Id })
The 'Product/ProductDetail' controller and action method now looks like:
public class ProductController : Controller
{
public ActionResult ProductDetail(int id)
{
string hold;
return View();
}
}
This still is giving me the the incorrect URL, as shown, note the 'productId' is now showing as 'length'?
Since the link is on a page rendered by HomePageController the default is to use that controller in the route. You need to use the overload that accepts a controller name
#Html.ActionLink("Your link text", "ProductDetail", "Product", new { id = 1 }, null)
As a side note, your original route table would have created /Product/ProductDetail?productId =1 with this overload because it matches the default route which is the first route in your table (the order of routes is important). In order to have /Product/ProductDetail/1, either reverse the order of the routes or just change the parameter of ProductDetail to int id rather than int productId
Make sure you are using an overload that has controllerName in it, as shown in the following screenshot.
When I remove routeValues: null, it uses a different overlaod which has routeValue as third paramater.
Try this:
routes.MapRoute(
name: "ProductDetailHandler",
url: "Product/{action}/{productId}",
defaults: new { controller = "Product", action = "ProductDetail", productId = UrlParameter.Optional }
);
routes.MapRoute
(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Not sure what you are trying to do with the Login controller. Maybe you can put log-in on your Home page or redirect the Home/Index to Login.
Also you can specify the default namespace if it doesn't find your controller:
routes.MapRoute(
name: "ProductDetailHandler",
url: "Product/{action}/{productId}",
defaults: new { controller = "Product", action = "ProductDetail", productId = UrlParameter.Optional },
new string[] { "MyProject.Controllers" }
);
routes.MapRoute
(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new string[] { "MyProject.Controllers" }
);

Routing to index with id in ASP.NET MVC 4

I'd like to maintain ASP.NET MVC 4's existing controller/action/id routing with default controller = Home and default action = Index, but also enable controller/id to route to the controller's index method as long as the second item is not a known action.
For example, given a controller Home with actions Index and Send:
/Home/Send -> controller's Send method
/Home -> controller's Index method
/Home/Send/xyz -> controller's Send method with id = xyz
/Home/abc -> controller's Index method with id = abc
However, if I define either route first, it hides the other one. How would I do this?
Do the specific one first before the default generic one. The order matters.
routes.MapRoute(name: "single", url: "{controller}/{id}",
defaults: new { controller = "Home", action = "Index" },
constraints: new { id = #"^[0-9]+$" });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home",
action = "Index",
id = UrlParameter.Optional }
);
In case, that the list of your actions (e.g. Send) is well known, and their (action) names cannot be the same as some ID value, we can use our custom ConstraintImplementation:
public class MyRouteConstraint : IRouteConstraint
{
public readonly IList<string> KnownActions = new List<string>
{ "Send", "Find", ... }; // explicit action names
public bool Match(System.Web.HttpContextBase httpContext, Route route
, string parameterName, RouteValueDictionary values
, RouteDirection routeDirection)
{
// for now skip the Url generation
if (routeDirection.Equals(RouteDirection.UrlGeneration))
{
return false; // leave it on default
}
// try to find out our parameters
string action = values["action"].ToString();
string id = values["id"].ToString();
// id and action were provided?
var bothProvided = !(string.IsNullOrEmpty(action) || string.IsNullOrEmpty(id));
if (bothProvided)
{
return false; // leave it on default
}
var isKnownAction = KnownActions.Contains(action
, StringComparer.InvariantCultureIgnoreCase);
// action is known
if (isKnownAction)
{
return false; // leave it on default
}
// action is not known, id was found
values["action"] = "Index"; // change action
values["id"] = action; // use the id
return true;
}
And the route map (before the default one - both must be provided), should look like this:
routes.MapRoute(
name: "DefaultMap",
url: "{controller}/{action}/{id}",
defaults: new { controller = string.Empty, action = "Index", id = string.Empty },
constraints: new { lang = new MyRouteConstraint() }
);
Summary: In this case, we are evaluating the value of the "action" parameter.
if both 1) action and 2) id are provided, we won't handle it here.
nor if this is known action (in the list, or reflected...).
only if the action name is unknown, let's change the route values: set action to "Index" and action value to ID.
NOTE: action names and id values need to be unique... then this will work
The easiest way is to simply create two Action methods in your Controller. One for Index and one for Send and place your string id parameter on both. Since you cannot have duplicate or overloaded action methods, that solves that problem. Your Index method will now handle both index or blank paths where the id is present or not (null) and process your views that way. Your Send method will do the exact same as Index. You can then route, process, or redirect how you like, depending on if id is null. This should work with no changes to RouteConfig.cs:
public ActionResult Index(string id) {if (id == null) Do A else Do B}
public ActionResult Send(string id) {if (id == null) Do A else Do B}
I had this struggle for a long time, and this was the simplest solution.
I don't think there is a solution for your requirements, as you have two competing routes. Maybe you can define something specific (in case you don't have any other controllers).
routes.MapRoute(
name: "Send",
url: "{controller}/Send/{id}",
defaults: new { controller = "Home", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Home",
url: "Home",
defaults: new { controller = "Home", action = "Index" }
routes.MapRoute(
name: "Index",
url: "{controller}/{id}",
defaults: new { controller = "Home", action= "Index",
id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index",
id = UrlParameter.Optional }
);
This what worked for me:
1) In RouteConfig, I put this to be the first line:
routes.MapRoute(name: "single", url: "{controller}/{lastName}",
defaults: new { controller = "LeaveRequests", action = "Index" });
2) In my controller:
public ViewResult Index(string lastName)
{
if (lastName == null)
{
return //return somthing
}
else
{
return //return something
}
}
3) when you call the controller
http://localhost:34333/leaveRequests/Simpsons
it will give you all requests for Simpsons.
if you call http://localhost:34333/leaveRequests/
it will give all requests
this is working for me
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default1",
url: "{id}",
defaults: new { controller ="Home", action ="Index" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller ="Home", action ="Index", id = UrlParameter.Optional }
);

Categories