Too many params in URL - Routing ASP.NET MVC - asp.net-mvc

Im searching the best way for manage long urls in routing. I have many actions which looks like this:
/a/b/c/d/e
the route:
routes.MapRoute(
"xxx",
"{a}/{b}/{c}/{d}/{e}",
new { controller = "Xxx", action="Xxx"});
the controller:
public ActionResult Xxx(int a, int b, int c, int d, int e) { ... }
any change in params gives multi-change in every route/action, and that is the problem. Its not elastic. Is there any possibility to map params to some object? Something that would look like:
public ActionResult Xxx(RouteParams rp) { ... }
Hmm... eventually I think that I could use the Action Filter to solve this:
private RouteParams rp;
public override void OnActionExecuting(FilterExecutingContext filterContext) {
rp = new RouteParams(...);
}
but I dont like this solution
Best regards

Create an object like you did and use ModelBinder to construct it instead of filter. The Default Model binder should work, if not then create a custom one.

Keep your route settings the same, just create a new model with properties matching the parameters in the route settings:
public class XxxModel
{
public int a { get; set; }
public int b { get; set; }
public int c { get; set; }
public int d { get; set; }
public int e { get; set; }
}
Then use XxxModel as your parameter in the action:
public ActionResult Xxx( XxxModel model )
{
...
}
a, b, c, d and e will be mapped to the properties in the model.

Will this work for you?
routes.MapRoute(
"xxx",
"{*params}",
new { controller = "Xxx", action="Xxx"});
and
public ActionResult Xxx(string params) { ... }
params will be one string (eg. 1/2/3/4/5)
you'll have to do a params.Split("/") and Convert.ToInt32() to the parameters.

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.

Asp.Net mvc 5 - How can I pass a complex object as route value in Html.ActionLink() so default model binder can map it?

I have an object containing searching, sorting and paging parameters as well as an id of a record to be edited.
I'd like to pass this object into Html.ActionLink() as a route value object, so that the resulting query string will be correctly mapped by the default model binder into the Edit action's parameter, which is an EditViewModel.
The idea is that after the Edit action completes, it can redirect back to the Index and maintain the same paging/sorting position, in the same data set, and filtered by the same search string.
Edit View Model:
public class EditViewModel
{
public SearchSortPageViewModel SearchSortPageParams { get; set; }
public int Id { get; set; }
public EditViewModel()
{
SearchSortPageParams = new SearchSortPageViewModel();
Id = 0;
}
public EditViewModel(SearchSortPageViewModel searchSortPageParams, int id)
{
SearchSortPageParams = searchSortPageParams;
Id = id;
}
}
public class SearchSortPageViewModel
{
public string SearchString { get; set; }
public string SortCol { get; set; }
public string SortOrder { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
}
Edit action:
public ActionResult Edit(EditViewModel evm)
{
/* ... */
}
When I do this in the view:
#model MyApp.Areas.Books.ViewModels.Books.IndexViewModel
...
#{EditViewModel evm = new EditViewModel(Model.SearchSortPageParams, item.ID);}
#Html.ActionLink("Edit", "Edit", evm)
I get this:
http://localhost:63816/Books/Books/Edit/4?SearchSortPageParams=MyApp.Areas.Base.ViewModels.SearchSortPageViewModel
But I want this:
http://localhost:63816/Books/Books/Edit/4?SearchSortPageParams.SearchString=abc&SearchSortPageParams.SortCol=name&SearchSortPageParams.SortOrder=asc&SearchSortPageParams.Page=1&SearchSortPageParams.PageSize=3
The only way I have been able to pass the object so far has been to manually prepare the query string, like this:
#{string theQueryString = "?SearchSortPageParams.SearchString=" + #evm.SearchSortPageParams.SearchString + "&SearchSortPageParams.SortCol=" + #evm.SearchSortPageParams.SortCol + "&SearchSortPageParams.SortOrder=" + #evm.SearchSortPageParams.SortOrder + "&SearchSortPageParams.Page=" + #evm.SearchSortPageParams.Page + "&SearchSortPageParams.PageSize=" + #evm.SearchSortPageParams.PageSize;}
Edit
I thought of writing a custom model binder, but it seems silly given that the default model binder already handles nested objects if formatted as a query string in the way it expects.
I also thought of writing a custom object serializer which outputs a serial format which the default model binder expects, but haven't yet gone down that route.
Finally, I thought of flattening out the EditViewModel so there is nothing nested, just have all the properties listed out flatly. But, it's not ideal.
So, what is the best way to go?
As far as I know, you can't pass the complex object directly, but you can avoid having to build the query string yourself by passing a RouteValueDictionary:
#Html.ActionLink("Edit", "Edit", new RouteValueDictionary {
{"SearchSortPageParams.SortOrder", evm.SearchSortPageParams.SortOrder },
{ /* etc... */ }
})
This should generate the query string as you need it.
The only other alternative would be use reflection to iterate over the properties of the model and generate this dictionary that way but that would, in my opinion, be over-engineered.
Of course, I would generally suggest in this situation that you just have your action method take separate parameters:
public ActionResult Search(string searchString, SortOrder sortOrder, ...)
I'd generally consider this to be a more appropriate way to pass GET parameters to a method (of course, this could get unwieldy if you have a lot of parameters). Then you can just do the following, which is much tidier:
#Html.ActionLink("Edit", "Edit",
new { sortOrder = evm.SearchSortPageParams.SortOrder, ... })

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));
}

What approach to take for testing AutoMapper configuration in an ASP.NET MVC application?

We are using AutoMapper extensively in our ASP.NET MVC web applications with the AutoMapViewResult approach set out in this question. So we have actions that look like this:
public ActionResult Edit(User item)
{
return AutoMapView<UserEditModel>(View(item));
}
This creates hidden failure points in the application if the requested mapping has not been configured - in that this is not a compile time fail.
I'm looking at putting something in place to test these mappings. As this needs to test the actual AutoMapper configuration I presume this should be done as part of integration testing? Should these tests be structured per controller or per entity? What about the possibility of automatically parsing all calls to AutoMapView?
Note that we are already testing that the AutoMapper configuration is valid using AssertConfigurationIsValid, it is missing mappings that I want to deal with.
If your controller action looked like this:
public AutoMapView<UserEditModel> Edit(User item)
{
return AutoMapView<UserEditModel>(View(item));
}
Then you can pretty easily, using reflection, look for all controller actions in your project. You then examine the action parameter types and the generic type parameter of your AutoMapView action result. Finally, you ask AutoMapper if it has a type map for those input/output models. AutoMapper doesn't have a "CanMap" method, but you can use the IConfigurationProvider methods of FindTypeMapFor:
((IConfigurationProvider) Mapper.Configuration).FindTypeMapFor(null, typeof(User), typeof(UserEditModel);
Just make sure that's not null.
[Test]
public void MapperConfiguration()
{
var mapper = Web.Dto.Mapper.Instance;
AutoMapper.Mapper.AssertConfigurationIsValid();
}
You can use the AssertConfigurationIsValid method. Details are on the automapper codeplex site (http://automapper.codeplex.com/wikipage?title=Configuration%20Validation)
Strictly speaking you should be writing a test to validate the mapping before you write a controller action that depends on the mapping configuration being present.
Either way, you can use the Test Helpers in the MvcContrib project to check the action method returns the expected ViewResult and Model.
Here's an example:
pageController.Page("test-page")
.AssertViewRendered()
.WithViewData<PortfolioViewData>()
.Page
.ShouldNotBeNull()
.Title.ShouldEqual("Test Page");
I do something like this.
using System.Linq;
using System.Reflection;
using AutoMapper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
public class SampleDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Sample
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string LoginId { get; set; }
}
public class AutomapperConfig
{
public static void Configure()
{
Mapper.Initialize(cfg => cfg.AddProfile<ViewModelProfile>());
}
}
public class ViewModelProfile : Profile
{
protected override void Configure()
{
CreateMap<SampleDto, Sample>();
}
}
[TestClass]
public class AutoMapperTestsSample
{
public AutoMapperTestsSample()
{
AutomapperConfig.Configure();
}
[TestMethod]
public void TestSampleDtoFirstName()
{
#region Arrange
var source = new SampleDto();
source.FirstName = "Jim";
//source.LastName = "Bob";
var dest = new Sample();
dest.FirstName = "FirstName";
dest.LastName = "LastName";
dest.LoginId = "LoginId";
#endregion Arrange
#region Act
AutoMapper.Mapper.Map(source, dest);
#endregion Act
#region Assert
Assert.AreEqual("Jim", dest.FirstName);
Assert.AreEqual(null, dest.LastName);
Assert.AreEqual("LoginId", dest.LoginId);
#endregion Assert
}
[TestMethod]
public void TestSampleDtoLastName()
{
#region Arrange
var source = new SampleDto();
//source.FirstName = "Jim";
source.LastName = "Bob";
var dest = new Sample();
dest.FirstName = "FirstName";
dest.LastName = "LastName";
dest.LoginId = "LoginId";
#endregion Arrange
#region Act
AutoMapper.Mapper.Map(source, dest);
#endregion Act
#region Assert
Assert.AreEqual(null, dest.FirstName);
Assert.AreEqual("Bob", dest.LastName);
Assert.AreEqual("LoginId", dest.LoginId);
#endregion Assert
}
/// <summary>
/// This lets me know if something changed in the Dto object so I know to adjust my tests
/// </summary>
[TestMethod]
public void TestSampleDtoReflection()
{
#region Arrange
var xxx = typeof(SampleDto);
#endregion Arrange
#region Act
#endregion Act
#region Assert
Assert.AreEqual(2, xxx.GetRuntimeFields().Count());
Assert.AreEqual("System.String", xxx.GetRuntimeFields().Single(a => a.Name.Contains("FirstName")).FieldType.ToString());
Assert.AreEqual("System.String", xxx.GetRuntimeFields().Single(a => a.Name.Contains("LastName")).FieldType.ToString());
#endregion Assert
}
}

Search page MVC routing (hidden action, no slashes, like SO)

I want my searches like those in Stack Overflow (i.e. no action, no slashes):
mydomain.com/search --> goes to a general search page
mydomain.com/search?type=1&q=search+text --> goes to actual search results
My routes:
routes.MapRoute(
"SearchResults",
"Search/{*searchType}", --> what goes here???
new { action = "Results" }
);
routes.MapRoute(
"SearchIndex",
"Search",
new { action = "Index" }
);
My SearchController has these actions:
public ActionResult Index() { ... }
public ActionResult Results(int searchType, string searchText) { ... }
The search results route does not work. I don't want to use the ".../..." approach that everyone seems to be using, because a search query is not a resource, so I want the data in a query string as I've indicated, without slashes--exactly like SO does.
TIA!Matt
You don't need two routes because you're providing search parameters as query string. Just have one search route:
routes.MapRoute(
"Search",
"Search",
new { controller = "Search", action = "Search" }
);
Then write this controller action
public ActionResult Search(int? type, string q)
{
var defaultType = type.HasValue ? type.Value : 1;
if (string.IsNullOrEmpty(q))
{
// perform search
}
// do other stuff
}
The body of this method greatly depends on the search condition whether you require both parameters when you search for stuff or just q and you have some default for type. Remember that page indexing can be done just the same way.
Using strong type parameters (validation wise)
You could of course create a class that could be validated, but property names should reflect that of the query string. So you'd either have a class:
public class SearchTerms
{
public int? type { get; set; }
public string q { get; set; }
}
And use the same request with equally named query variables as you do now, or have a clean class and adjust your request:
public class SearchTerms
{
public int? Type { get; set; }
public string Query { get; set; }
}
http://domain.com/Search?Type=1&Query=search+text

Resources