RouteValueDictionary initialization and MapRoute issue - asp.net-mvc

Are these two pieces of codes the same?
RouteValueDictionary dic=new RouteValueDictionary();
dic.Add("controller", "Home");
dic.Add("action", "Index");
RouteTable.Routes.MapRoute("Test", "Test/Something", dic);
and
RouteTable.Routes.MapRoute("Test", "Test/Something", new{controller="Home", action="Index"});
I am not getting the same route in the route table. When I use the first option, the keys "controller" and "action" arent in RouteTable.Routes[0].Defaults.Keys but are added on the RouteTable.Routes[0].Defaults.Values
Do you know what I am doing wrong in the first option?

you can pass object of any type for third parameter but the passed object must have included keys defined in you'r url pattern as it's properties.for example
public class test
{
public string controller { get; set; }
public string action { get; set; }
public string id { get; set; }
}
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new test { action = "Index", controller = "Home", id = "" }
);
in this case you'r object must include controller,action,id properties.

Related

Binding parameter to complex type

I have a navigation bar, with several links, like this:
MenuItem1
This request would hit my action method:
public ActionResult Browse(int departmentId)
{
var complexVM = MyCache.GetComplexVM(departmentId);
return View(complexVM);
}
This is my ComplexVM:
public class ComplexVM
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
MyCache, is a static list of departments, which I am keeping in memory, so when user passes in DepartmentId, I wouldn't need to get the corresponding DepartmentName from DB.
This is working fine... but it would be nice if I could somehow initialize ComplexVM in custom model binder, instead of initializing it in the Controller... so I still want to use a link (menu item), but this time, a CustomModelBinder binds my parameter, 2, to ComplexVM: it needs to look up the name of department with id = 2 from MyCache and initialize ComplexVM, then ComplexVM would be passed to this action method:
public ActionResult Browse(ComplexVM complexVM)
{
return View(complexVM);
}
I want to hit the above controller without doing a post-back, as I have a lot of menu item links in my navigation bar... not sure if this is possible? Or if this is even a good idea?
I have seen this link, which sort of describes what I want... but I am not sure how the routing would work... i.e. routing id:2 => ComplexVM
Alternatively would it be possible to do this in RouteConfig, something like this:
routes.MapRoute(
name: "Browse",
url: "{controller}/Browse/{departmentId}",
// this does not compile, just want to explain what I want...
defaults: new { action = "Browse", new ComplexVM(departmentId) });
I can achieve this with little change and with one trick
MenuItem1
Controller action
public ActionResult Browse(ComplexVM complexVM)
{
return View(complexVM);
}
View model
public class ComplexVM
{
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
public ComplexVM()
{
this.DepartmentId = System.Convert.ToInt32(HttpContext.Current.Request("id").ToString);
this.DepartmentName = "Your name from cache"; // Get name from your cache
}
}
This is without using model binder. Trick may help.
That is possible. It is also a good idea :) Off-loading parts of the shared responsibility to models / action filters is great. The only problem is because they are using some special classes to inherit from, testing them sometimes might be slightly harder then just testing the controller. Once you get the hang of it - it's better.
Your complex model should look like
// Your model class
[ModelBinder(typeof(ComplexVMModelBinder)]
public class ComplexVMModel
{
[Required]
public int DepartmentId { get; set; }
public string DepartmentName { get; set; }
}
// Your binder class
public class ComplexVMModelBinder : IModelBinder
{
// Returns false if you can't bind.
public bool BindModel(HttpActionContext actionContext, ModelBindingContext modelContext)
{
if (modelContext.ModelType != typeof(ComplexVMModel))
{
return false;
}
// Somehow get the depid from the request - this might not work.
int depId = HttpContext.Current.Request.Params["DepID"];
// Create and assign the model.
bindingContext.Model = new ComplexVMModel() { DepartmentName = CacheLookup(), DepId = depId };
return true;
}
}
Then at the beginning of your action method, you check the ModelState to see if it's valid or not. There are a few things which can make the model state non-valid (like not having a [Required] parameter.)
public ActionResult Browse(ComplexVM complexVM)
{
if (!ModelState.IsValid)
{
//If not valid - return some error view.
}
}
Now you just need to register this Model Binder.
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
ModelBinders.Binders.Add(typeof(ComplexVMModel), new ComplexVMModelBinder());
}
Your should be able to use the route config that you've provided.

Server Error in '/' Application in ActionLink (but submit works fine)

Execution of the following lines
#Html.ActionLink("Open",actionName: "DownloadExcelFile",
controllerName: "Excel",
routeValues: new { model = new ExcelModel(5, "JobName", "item.UserName") },
htmlAttributes: null)
returns Server Error in '/' Application, could you help me to fix them?
Note that when I change the name of the parameter, model -> id, I get an Error 404 instead of Server Error in '/' Application.
The model is
public class ExcelModel
{
public int InputA { get; set; }
public string InputB { get; set; }
public string UserName { get; set; }
public ExcelModel(int inputA, string inputB, string userName)
{
InputA = inputA;
InputB = inputB;
UserName = userName;
}
public ExcelModel()
{
InputA = 1;
InputB = "B1";
UserName = "NiceUser";
}
...
}
Controller is
public class ExcelController : Controller
{
public ActionResult Index()
{
var model = new ExcelModel(1, "B1", User.Identity.Name);
return View(model);
}
[HttpPost]
public ActionResult DownloadExcelFile(ExcelModel id)
{
// assume we create an an excel stream...
MemoryStream excelStream = id.BuildSpreadsheet();
return new FileStreamResult(excelStream, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = "myfile.xslx"
};
}
}
RouteConfig is the standard one
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 }
);
}
}
Finally, as mentioned earlier, the method itself is fine, since it works perfectly with submit, as below:
#using (Html.BeginForm("DownloadExcelFile", "Excel"))
{
<fieldset>
// fields names and values
<p>
<input type="submit" value="Open Excel"/>
</p>
</fieldset>
}
1) You can't pass an entire class as a route value param. The helper has to be able to put whatever you pass into a URL, which means it has to be something that can be converted to a string value. It might be possible to JSON encode the model and then pass the JSON string for the param, but the helper isn't going to make such assumptions for you, nor would it necessarily know how to JSON encode it for you, if it did.
2) When you just pass the id, you get a 404 because your action doesn't not accept an int for id, but rather expects ExcelModel, which as we discussed in #1, is not possible to pass via URL.
Your controller method is marked with the HttpPost attribute. This means that it only accepts POST-requests and not GET-requests. Normal link visits are GET-requests, so that is probably the problem. (Read more here)
Remove the HttpPost attribute and see if that fixes the problem.

How to post one of four text boxes value to controller from view using FormMethod.Post instead from query string?

I have a search page with four text boxes (ID, batchID, EmployeeNumber, RefNumber)all are numbers. I don't want to use query string to send any of these values to controller. So I am using Form.Post method like below:
#using (Html.BeginForm("Details", "Search", FormMethod.Post, new { #id = Model.id }))
But I want to make it global, so that based on which text box user uses to search, that value should be send to the controller and it's type also if possible(Like they entered ID or batchID or....) so that it will be easy for me to search the database accordingly. Please somebody help.
FYI: my route looks like this in global.asax
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
I am actually thinking to send value from a javascript method where i do all the conditions check.
You could define a view model:
public class SearchViewModel
{
public int? Id { get; set; }
public int? BatchID { get; set; }
public int? EmployeeNumber { get; set; }
public int? RefNumber { get; set; }
}
and then have the controller action you are posting to take this view model as parameter and you will be able to retrieve which values did the user entered in the textboxes:
[HttpPost]
public ActionResult Details(SearchViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
if (model.Id != null)
{
// the user entered something into the textbox containing the id
}
if (model.BatchId != null)
{
// the user entered something into the textbox containing the batch id
}
...
}

Minimal way to handle static content routes/controllers/views from data driven menus?

I have a ListItem class that is used to represent menu items in my application:
public class ListItem : Entity
{
public virtual List List { get; set; }
public virtual ListItem ParentItem { get; set; }
public virtual ICollection<ListItem> ChildItems { get; set; }
public int SortOrder { get; set; }
public string Text { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string Area { get; set; }
public string Url { get; set; }
}
I use this data to construct the routes for the application, but I was wondering if there was a clean way to handle controller/views for static content? Basically any page that doesn't use any data but just views. Right now I have one controller called StaticContentController, which contains a unique action for each static page that returns the appropriate view like so:
public class StaticContentController : Controller
{
public ActionResult Books()
{
return View("~/Views/Books/Index.cshtml");
}
public ActionResult BookCategories()
{
return View("~/Views/Books/Categories.cshtml");
}
public ActionResult BookCategoriesSearch()
{
return View("~/Views/Books/Categories/Search.cshtml");
}
}
Is there some way I could minimize this so I don't have to have so many controllers/actions for static content? It seems like when creating my ListItem data I could set the Controller to a specific controller that handles static content, like I have done, but is there anyway to use one function to calculate what View to return? It seems like I still need separate actions otherwise I won't know what page the user was trying to get to.
The ListItem.Url contains the full URL path from the application root used in creating the route. The location of the View in the project would correspond to the URL location to keep the organization structure parallel.
Any suggestions? Thanks.
Edit: My Route registration looks like so:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("Shared/{*pathInfo}");
routes.MapRoute("Access Denied", "AccessDenied", new { controller = "Shared", action = "AccessDenied", area = "" });
List<ListItem> listItems = EntityServiceFactory.GetService<ListItemService>().GetAllListItmes();
foreach (ListItem item in listItems.Where(item => item.Text != null && item.Url != null && item.Controller != null).OrderBy(x => x.Url))
{
RouteTable.Routes.MapRoute(item.Text + listItems.FindIndex(x => x == item), item.Url.StartsWith("/") ? item.Url.Remove(0, 1) : item.Url, new { controller = item.Controller, action = item.Action ?? "index" });
}
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
You can use a single Action with one parameter (the View name) which will return all the static pages
public class StaticContentController : Controller
{
public ActionResult Page(string viewName)
{
return View(viewName);
}
}
You will also need to create a custom route for serving these views, for example:
routes.MapRoute(
"StaticContent", // Route name
"page/{viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" } // Parameter defaults
);
I see in your example that you specify different folders for your views. This solution will force you to put all static views in the Views folder of the StaticContentController.
If you must have custom folder structure, then you can change the route to accept / by adding * to the {viewName} like this {*viewname}. Now you can use this route: /page/Books/Categories. In the viewName input parameter you will receive "Books/Categories" which you can then return it as you like: return View(string.Format("~/Views/{0}.cshtml", viewName));
UPDATE (Avoiding the page/ prefix)
The idea is to have a custom constraint to check whether or not a file exists. Every file that exists for a given URL will be treated as static page.
public class StaticPageConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string viewPath = httpContext.Server.MapPath(string.Format("~/Views/{0}.cshtml", values[parameterName]));
return File.Exists(viewPath);
}
}
Update the route:
routes.MapRoute(
"StaticContent", // Route name
"{*viewName}", // URL with parameters
new { controller = "StaticContent", action = "Page" }, // Parameter defaults
new { viewName = new StaticPageConstraint() } // Custom route constraint
);
Update the action:
public ActionResult Page(string viewName)
{
return View(string.Format("~/Views/{0}.cshtml", viewName));
}

How do I route a URL with a querystring in ASP.NET MVC?

I'm trying to setup a custom route in MVC to take a URL from another system in the following format:
../ABC/ABC01?Key=123&Group=456
The 01 after the second ABC is a step number this will change and the Key and Group parameters will change. I need to route this to one action in a controller with the step number key and group as paramters. I've attempted the following code however it throws an exception:
Code:
routes.MapRoute(
"OpenCase",
"ABC/ABC{stepNo}?Key={key}&Group={group}",
new {controller = "ABC1", action = "OpenCase"}
);
Exception:
`The route URL cannot start with a '/' or '~' character and it cannot contain a '?' character.`
You cannot include the query string in the route. Try with a route like this:
routes.MapRoute("OpenCase", "ABC/ABC{stepNo}",
new { controller = "ABC1", action = "OpenCase" });
Then, on your controller add a method like this:
public class ABC1 : Controller
{
public ActionResult OpenCase(string stepno, string key, string group)
{
// do stuff here
return View();
}
}
ASP.NET MVC will automatically map the query string parameters to the parameters in the method in the controller.
When defining routes, you cannot use a / at the beginning of the route:
routes.MapRoute("OpenCase",
"/ABC/{controller}/{key}/{group}", // Bad. Uses a / at the beginning
new { controller = "", action = "OpenCase" },
new { key = #"\d+", group = #"\d+" }
);
routes.MapRoute("OpenCase",
"ABC/{controller}/{key}/{group}", // Good. No / at the beginning
new { controller = "", action = "OpenCase" },
new { key = #"\d+", group = #"\d+" }
);
Try this:
routes.MapRoute("OpenCase",
"ABC/{controller}/{key}/{group}",
new { controller = "", action = "OpenCase" },
new { key = #"\d+", group = #"\d+" }
);
Then your action should look as follows:
public ActionResult OpenCase(int key, int group)
{
//do stuff here
}
It looks like you're putting together the stepNo and the "ABC" to get a controller that is ABC1. That's why I replaced that section of the URL with {controller}.
Since you also have a route that defines the 'key', and 'group', the above route will also catch your initial URL and send it to the action.
There is no reason to use routing based in querystring in new ASP.NET MVC project. It can be useful for old project that has been converted from classic ASP.NET project and you want to preserve URLs.
One solution can be attribute routing.
Another solution can be in writting custom routing by deriving from RouteBase:
public class MyOldClassicAspRouting : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
if (httpContext.Request.Headers == null) //for unittest
return null;
var queryString = httpContext.Request.QueryString;
//add your logic here based on querystring
RouteData routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", "...");
routeData.Values.Add("action", "...");
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Implement your formating Url formating here
return null;
}
}
And register your custom routing class
public static void RegisterRoutes(RouteCollection routes)
{
...
routes.Add(new MyOldClassicAspRouting ());
}
The query string arguments generally are specific of that controller and of that specific application logic.
So it will better if this isn't written in route rules, that are general.
You can embed detection of query string on action argument in the following way.
I think that is better to have one Controller for handling StepNo.
public class ABC : Controller
{
public ActionResult OpenCase(OpenCaseArguments arg)
{
// do stuff here
// use arg.StepNo, arg.Key and arg.Group as You need
return View();
}
}
public class OpenCaseArguments
{
private string _id;
public string id
{
get
{
return _id;
}
set
{
_id = value; // keep original value;
ParseQueryString(value);
}
}
public string StepNo { get; set; }
public string Key { get; set; }
public string Group { get; set; }
private void ParseQueryString(string qs)
{
var n = qs.IndexOf('?');
if (n < 0) return;
StepNo = qs.Substring(0, n); // extract the first part eg. {stepNo}
NameValueCollection parms = HttpUtility.ParseQueryString(qs.Substring(n + 1));
if (parms.Get("Key") != null) Key = parms.Get("Key");
if (parms.Get("Group") != null) Group = parms.Get("Group");
}
}
ModelBinder assign {id} value to the id field of OpenCaseArguments. The set method handle querystring split logic.
And keep routing this way. Note routing get your querystring in id argument.
routes.MapRoute(
"OpenCase",
"ABC/OpenCase/{id}",
new {controller = "ABC", action = "OpenCase"}
);
I have used this method for getting multiple fields key value on controller action.

Resources