1

A design goal for a website I'm working on is to keep the URL in the browser in a state where the user can copy it, and the link can be used from another browser/user/machine to return to the spot that the url was copied. (The actual changes will happen via AJAX, but the URL will change to reflect where they are.)

Example: If you were on the customer page looking at customer 123, and had details pulled up on their order #456, and full details on line 6 of this order, your url could simply be /customer/123/456/6

The challenge comes with a second feature: Users can add UI columns (analogous to adding a new tab in a tab view, or a new document in an MDI app) Each column can easily generate a routable url, but I need the url to reflect one or more columns. (E.G. User has both /customer/123/456/6 and /customer/333/55/2 in two side by side columns)

In a perfect world, I'd like the url to be /customer/123/456/6/customer/333/55/2 for the above scenario, but I don't know if MVC routing can handle repetitive patterns, or, if so, how it is done.

Can this be done via routing? If not is there a way to get this type of one-or-more functionality from Url?

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
SvdSinner
  • 951
  • 1
  • 11
  • 23

2 Answers2

3

You could create a custom route handler (see my previous answer) or derive from a RouteBase like NightOwl888 suggested. Another approach would be to simply use a model binder and a model binder attribute.

public class CustomerInvoiceLineAttribute : CustomModelBinderAttribute
{
    public override IModelBinder GetBinder()
    {
        return new CustomerInvoiceLineModelBinder();
    }
}

public class CustomerInvoiceLineModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var path = (string)bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue;
        var data = path.Split(new[] { "/customer/" }, StringSplitOptions.RemoveEmptyEntries);

        return data.Select(d =>
        {
            var rawInfo = d.Split('/');
            return new CustomerInvoiceLine
            {
                CustomerId = int.Parse(rawInfo[0]),
                InvoiceId = int.Parse(rawInfo[1]),
                Line = int.Parse(rawInfo[2])
            };
        });
    }
}

You define your route by specifying a star route data. This mean that the route parameter will contains everything following the action

routes.MapRoute(
    name: "CustomerViewer",
    url: "customer/{*customerInfo}",
    defaults: new { controller = "Customer", action = "Index" });

Then in your controller, you bind your parameter with the same name as the star route parameter using the custom model binder defined above:

public ActionResult Index([CustomerInvoiceLine] IEnumerable<CustomerInvoiceLine> customerInfo)
{
    return View();
}

You will need to add validation during the parsing and probably security too, so that a customer cannot read the invoice of other customers.

Also know that URL have a maximum length of 2000 characters.

Community
  • 1
  • 1
Pierre-Alain Vigeant
  • 22,635
  • 8
  • 65
  • 101
  • Never customize `MvcRouteHandler` for this purpose. `MvcRouteHandler` can only provide a one-way mapping from URL to resource, but your `ActionLinks` and `RouteLinks` will not work. It is better to subclass `RouteBase` or `Route` (as in [this example](http://stackoverflow.com/questions/31934144/multiple-levels-in-mvc-custom-routing/31958586#31958586)) so you correctly account for the 2-way nature of routing, especially if you don't want to build your own system for reconstructing the URLs on the views. – NightOwl888 Sep 22 '15 at 21:17
1

You can do this with the built-in routing as long as you don't anticipate that any of your patterns will repeat or have optional parameters that don't appear in the same segment of the URL as other optional parameters.

It is possible to use routing with optional parameters by factoring out all of the permutations, but if you ask me it is much simpler to use the query string for this purpose.

NOTE: By definition, a URL must be unique. So you must manually ensure your URLs don't have any collisions. The simplest way to do this is by matching the page with the path (route) and adding this extra information as query string values. That way you don't have to concern yourself with accidentally making routes that are exactly the same.

However, if you insist on using a route for this purpose, you should probably put your URLs in a database in a field with a unique constraint to ensure they are unique.

For the most advanced customization of routing, subclass RouteBase or Route. This allows you to map any URL to a set of route values and map the route values back to the same URL, which lets you use it in an ActionLink or RouteLink to build the URLs for your views and controllers.

public class CustomPageRoute : RouteBase
{
    // This matches the incoming URL and translates it into RouteData
    // (typically a set of key value pairs in the RouteData.Values dictionary)
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        RouteData result = null;

        // Trim the leading slash
        var path = httpContext.Request.Path.Substring(1);

        if (/* the path matches your route logic */)
        {
            result = new RouteData(this, new MvcRouteHandler());

            result.Values["controller"] = "MyController";
            result.Values["action"] = "MyAction";

            // Any other route values to match your action...
        }

        // IMPORTANT: Always return null if there is no match.
        // This tells .NET routing to check the next route that is registered.
        return result;
    }

    // This builds the URL for ActionLink and RouteLink
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        VirtualPathData result = null;

        if (/* all of the expected route values match the request (the values parameter) */)
        {
            result = new VirtualPathData(this, page.VirtualPath);
        }

        // IMPORTANT: Always return null if there is no match.
        // This tells .NET routing to check the next route that is registered.
        return result;
    }
}

Usage

routes.Add(
    name: "CustomPage", 
    item: new CustomPageRoute());

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212