MVC controller method for View with optional Guid Parameter - c#

I am trying to make a MVC view accessible from either directly going to that View from a menu or by clicking on a link that'll take me to that same view but with a parameter and with that particular links information instead of seeing a general page if I went straight to it.
public ActionResult Roles(Guid applicationId)
{
if (applicationId == Guid.Empty)
{
return View();
}
var application = new ApplicationStore().ReadForId(applicationId);
return View(application);
}
I know for optional parameters you I'd do something like Guid? in the parameters but visual studios doesn't like that and I can't do Guid application = null either. Any Ideas?

As you already mentioned, make the parameter optional.
public ActionResult Roles(Guid? id) {
if (id == null || id.Value == Guid.Empty) {
return View();
}
var application = new ApplicationStore().ReadForId(id.Value);
return View(application);
}
This also assumes the default convention-based route
"{controller}/{action}/{id}"
Where the id is optional in the route template.
id = UrlParameter.Optional
For example
routes.MapRoute(
name: "SomeName",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Account", action = "Index", id = UrlParameter.Optional }
);

You could potentially just change the Guid parameter to string.
Guid.Empty = 00000000-0000-0000-0000-000000000000 which may cause issues when trying to pass in a null value.
if you switch it to something like this (but still use a Guid):
public ActionResult Roles(string applicationId)
{
if (string.IsNullOrEmpty(applicationId))
{
return View();
}
var application = new ApplicationStore().ReadForId(applicationId);
return View(application);
}
it may side-step the errors you're encountering.

Related

Getting 404 for HttpPost Action

I have a table of records being displayed in a partial view and some of them have no ID values. So I am trying to use an alternative field as an ID when clicking the Edit link for a particular record. I'm not sure if I can legally have two Post action methods, even though I am using different methods names and params.
Currently, if I click on a record with an ID the correct action method gets called. If I select a record with no ID (instead using an "account" string ID which is unique), I get a 404.
RouteConfig:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
Partial View:
...
<td>
#if (item.ID != null)
{
#Html.ActionLink("Edit", "EditBudget", new { id = item.ID })
}
else if (item.Account != null)
{
#Html.ActionLink("Edit", "EditAccountBudget", new { account = item.Account })
}
</td>
BudgetsController:
// POST: Budgets/Edit/5
[Route("edit/{id?}")]
[HttpPost]
public ActionResult EditBudget(int? id = null, FormCollection collection = null)
{
...
// Responding correctly for URL: http://localhost:4007/edit/19
}
[Route("editaccountbudget/{account}")]
[HttpPost]
public ActionResult EditAccountBudget(string account)
{
...
// Getting 404 for URL: http://localhost:4007/editaccountbudget/6000..130
}
ActionLink renders a regular anchor (<a />) tag, so it only does GET not POST. If you want to POST values, you need to use an actual form (either building your own tag, or using Html.BeginForm() ) and then include inside that form's scope a submit button.
Assuming that EditBudget is your controller name , you can change
your route to this to avoid confusing ( or leave as it is since the attribute route will be ignored) and remove [POST] from your action too:
[Route("~EditBudget/EditAccountBudget/{account}")]
Also change:
#Html.ActionLink("Edit", "EditAccountBudget", new new { account = item.Account })
To:
#Html.ActionLink("EditAccountBudget", "EditBudget", new { account = item.Account })
If you use razor pages template controls you need to have both controller and action parts of the route according your route mapping. If you use ajax or httpclient you can have any syntax of route.
Your BudgetsController should look like below without HttpPost Attribute and without Route Attribute as you are using method names in ActionLink. You can use HttpGet attribute if you wish.
Also no need of FormCollection collection parameter in EditBudget method. You will not get anything as its Get not Post.
public ActionResult EditBudget(int? id = null)
{
}
public ActionResult EditAccountBudget(string account)
{
}
As some have pointed out, this is a GET request. If the ID was null, I had to pass the model because I needed more than the account ID to construct the database query.
Partial View:
#if (item.ID != null)
{
#Html.ActionLink("Edit", "EditBudget", new { id = item.ID })
}
else if (item.Account != null)
{
#Html.ActionLink("Edit", "EditBudget", new { account = item.Account,
SelectedDepartment = item.SelectedDepartment, SelectedYear = item.SelectedYear })
}
BudgetsController:
// GET
public ActionResult EditBudget(int? id, BudgetsViewModel model)
{
repo = new BudgetDemoRepository();
if (id != null)
{
// Pass id to repository class
}
else
{
// Pass account and other params to repository class
}
return View(...);
}

UrlHelper in Controller Action does not build correct URL

I have an issue, when I'm trying to build the url for action in controller, the value of vm after assigning is "/". If I try to create url with other action name then everything works fine, like Url.Action("Edit", "Contact").
public class ContactController : Controller
{
public ActionResult List()
{
string vm = Url.Action("Create", "Contact"); // equals "/"
string editUrl = Url.Action("Edit", "Contact"); // all is fine
return View("List", vm);
}
public ActionResult Create()
{
return HttpNotFound();
}
public ActionResult Edit()
{
return HttpNotFound();
}
}
What's wrong with that code?
It is because your route specifies them as defaults.
Your route is:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Contact", action = "Create", id = String.Empty }, null);
Essentially, it is because you specify the default values controller = "Contact", action = "Create". When you specify these as default you are saying if the value is not provided in the URL then use these.
For examples all these URLs are the same: /, /Contact & /Contact/Create. By default MVC generates you the shortest URL.
You could either change the default values or remove them like this:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { id = String.Empty }, null);

Route is matching on parameter, not action name / View isn't returning properly

I want to create a url like that below:
www.mywebapp.com/Users/Profile/john
I have the UsersController controller and the Profile action, which returns a ViewResult to the Profile page.
I've created a route to manage it:
routes.MapRoute(
name: "ProfileRoute",
url: "Users/Profile/{username}",
defaults: new { controller = "Users", action = "Profile", username = UrlParameter.Optional }
);
The first question is: If i change {username} by {id}, it works. When I put {username} like parameter, the action gets NULL in the parameter. Why this?
Here's my action called Profile:
[HttpGet]
public ActionResult Profile(string id) {
if (UsersRepository.GetUserByUsername(id) == null) {
return PartialView("~/Views/Partials/_UsernameNotFound.cshtml", id);
}
return View(id);
}
I've added a View page to show the user profile. However, when the method ends its execution, I got another error:
The view 'john' or its master was not found or no view engine supports the searched locations.
The following locations were searched:
~/Views/Users/john.aspx
~/Views/Users/john.ascx
...
The second question is: The page I have to show is Profile, not a page with the username's name. Why is it happening?
You are getting this error because you are passing a string (id) to the View function, this overload searches for a view with the name passed in the string (in this case the username).
if you are simply trying to pass the username directly to the view you can use something like a ViewBag, so your code should look like this:
public ActionResult Profile(string id) {
if (UsersRepository.GetUserByUsername(id) == null) {
return PartialView("~/Views/Partials/_UsernameNotFound.cshtml", id);
}
ViewBag.Username=id;
return View();
}
I might be reading this incorrectly, but if you change the name of the required parameter from
id to username, it shouldn't return null
[HttpGet]
public ActionResult Profile(string username) {
if (UsersRepository.GetUserByUsername(username) == null) {
return PartialView("~/Views/Partials/_UsernameNotFound.cshtml", id);
}
return View(username);
}

Must the first view must always be called index.aspx?

I have created a controller called loginController.cs and i have created a view called login.aspx
How do I call that view from loginController.cs?
The ActionResult is always set to index and for neatness, I want to specify what view the controller uses when called rather than it always calling its default index?
Hope that makes sense.
You can customize pretty much everything in MVC routing - there is no particular restriction on how routes look like (only ordering is important), you can name actions differently from method names (via ActionName attribute), your can name views whatever you want (i.e. by returning particular view by name).
return View("login");
In the interest of actually answering the question.. you can add a route ABOVE your default route in Global.asax:
routes.MapRoute(
"SpecialLoginRoute",
"login/",
new { controller = "Login", action = "Login", id = UrlParameter.Optional }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
..although, without properly thinking through what you're trying to achieve (that being.. changing what MVC does by default) you're bound to end up with lots and lots of messy routes.
Your return the view from your controller via your Action methods.
public class LoginController:Controller
{
public ActionResult Index()
{
return View();
//this method will return `~/Views/Login/Index.csthml/aspx` file
}
public ActionResult RecoverPassword()
{
return View();
//this method will return `~/Views/Login/RecoverPassword.csthml/aspx` file
}
}
If you need to return a different view (other than the action method name, you can explicitly mention it
public ActionResult FakeLogin()
{
return View("Login");
//this method will return `~/Views/Login/Login.csthml/aspx` file
}
If you want to return a view which exist in another controller folder, in ~/Views, you can use the full path
public ActionResult FakeLogin2()
{
return View("~/Views/Account/Signin");
//this method will return `~/Views/Account/Signin.csthml/aspx` file
}

ASP.NET MVC: Routing custom slugs without affecting performance

I would like to create custom slugs for pages in my CMS, so users can create their own SEO-urls (like Wordpress).
I used to do this in Ruby on Rails and PHP frameworks by "abusing" the 404 route. This route was called when the requested controller could not be found, enabling me te route the user to my dynamic pages controller to parse the slug (From where I redirected them to the real 404 if no page was found). This way the database was only queried to check the requested slug.
However, in MVC the catch-all route is only called when the route does not fit the default route of /{controller}/{action}/{id}.
To still be able to parse custom slugs I modified the RouteConfig.cs file:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
RegisterCustomRoutes(routes);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { Controller = "Pages", Action = "Index", id = UrlParameter.Optional }
);
}
public static void RegisterCustomRoutes(RouteCollection routes)
{
CMSContext db = new CMSContext();
List<Page> pages = db.Pages.ToList();
foreach (Page p in pages)
{
routes.MapRoute(
name: p.Title,
url: p.Slug,
defaults: new { Controller = "Pages", Action = "Show", id = p.ID }
);
}
db.Dispose();
}
}
This solves my problem, but requires the Pages table to be fully queried for every request. Because a overloaded show method (public ViewResult Show(Page p)) did not work I also have to retrieve the page a second time because I can only pass the page ID.
Is there a better way to solve my problem?
Is it possible to pass the Page object to my Show method instead of the page ID?
Even if your route registration code works as is, the problem will be that the routes are registered statically only on startup. What happens when a new post is added - would you have to restart the app pool?
You could register a route that contains the SEO slug part of your URL, and then use the slug in a lookup.
RouteConfig.cs
routes.MapRoute(
name: "SeoSlugPageLookup",
url: "Page/{slug}",
defaults: new { controller = "Page",
action = "SlugLookup",
});
PageController.cs
public ActionResult SlugLookup (string slug)
{
// TODO: Check for null/empty slug here.
int? id = GetPageId (slug);
if (id != null) {
return View ("Show", new { id });
}
// TODO: The fallback should help the user by searching your site for the slug.
throw new HttpException (404, "NotFound");
}
private int? GetPageId (string slug)
{
int? id = GetPageIdFromCache (slug);
if (id == null) {
id = GetPageIdFromDatabase (slug);
if (id != null) {
SetPageIdInCache (slug, id);
}
}
return id;
}
private int? GetPageIdFromCache (string slug)
{
// There are many caching techniques for example:
// http://msdn.microsoft.com/en-us/library/dd287191.aspx
// http://alandjackson.wordpress.com/2012/04/17/key-based-cache-in-mvc3-5/
// Depending on how advanced you want your CMS to be,
// caching could be done in a service layer.
return slugToPageIdCache.ContainsKey (slug) ? slugToPageIdCache [slug] : null;
}
private int? SetPageIdInCache (string slug, int id)
{
return slugToPageIdCache.GetOrAdd (slug, id);
}
private int? GetPageIdFromDatabase (string slug)
{
using (CMSContext db = new CMSContext()) {
// Assumes unique slugs.
Page page = db.Pages.Where (p => p.Slug == requestContext.Url).SingleOrDefault ();
if (page != null) {
return page.Id;
}
}
return null;
}
public ActionResult Show (int id)
{
// Your existing implementation.
}
(FYI: Code not compiled nor tested - haven't got my dev environment available right now. Treat it as pseudocode ;)
This implementation will have one search for the slug per server restart. You could also pre-populate the key-value slug-to-id cache at startup, so all existing page lookups will be cheap.

Categories