top button
Flag Notify
    Connect to us
      Site Registration

Site Registration

Dealing With Varying Number Of Route Segments In ASP.NET MVC

+4 votes
274 views

Most of the times the ASP.NET MVC routes consists of known number of segments. For example, consider the default route pattern defined by ASP.NET MVC:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", 
              action = "Index", 
              id = UrlParameter.Optional }
);

Here, the route consists of three segments namely controller, action and an optional id. This works well for many applications. However, at times you may not have idea about the exact number of route segments involved. Consider the following example.

Suppose you are building an application that shows the population in one or more cities of a country. So, you need to pass the country name and one or more cities as a part of the URLs. Some sample URLs are given below:

/home/getdata/USA/Chicago
/home/getdata/USA/Chicago/Houston
/home/getdata/USA/Seattle/Boston/Tampa

As you can see from the above URLs, one could specify any number of cities in the route segments. Obviously you can't have a route pattern with a known number of segments. Luckily, ASP.NET MVC allows you to use what is often called catch-all route parameter. Let's see how it can be used in this situation.

Create a new ASP.NET MVC application and open its RouteConfig.cs file. Then add the following MapRoute() call after the default route pattern.

routes.MapRoute(
    name: "CatchAll",
    url: "{controller}/{action}/
          {country}/{*cities}",
    defaults: new { controller = "Home", 
              action = "GetData" }
);

Notice that the route pattern consists of four parameters namely controller, action, country and *cities. The *cities does the trick for us. This catch-all parameter indicates that any number of route segments can follow the country segment. Also notice that the route is configured to be handled by the GetData() action of the HomeController. The catch-all parameter must appear as the last segment of a route.

The GetData() action looks like this:

public ActionResult GetData(string country,string cities)
{
    string[] cityArray = cities.Split('/');

    ViewBag.Country = country;
    ViewBag.Cities = cityArray;

    return View();
}

The GetData() action takes two parameters - country and cities. Note that names of these parameters must match with the names of the route parameters defined in the RouteConfig.cs.

Capturing and using the country parameter is quite straightforward. The cities parameter could be a single city or multiple cities in the form city1/city2/city3. So, you need to split this string to get an array. Once you do that you are free to process the country and city values as per the application's logic. In this case you simply pass them to the GetData view through the ViewBag.

The Index view looks like this:

<body>
 <h1>
 <a href="/home/getdata/USA/Chicago">USA - Chicago</a>
 </h1>
 <h1>
 <a href="/home/getdata/USA/Chicago/Houston">
    USA - Chicago and Houston
 </a>
 </h1>
 <h1>
 <a href="/home/getdata/USA/Seattle/Boston/Tampa">
    USA - Seattle, Boston and Tampa
 </a>
 </h1>
</body>

The Index view simply renders a few links with USA as the country and some combination of cities.

The GetData view is shown below:

<body>
    <h1>@ViewBag.Country</h1>
    @foreach(string city in ViewBag.Cities)
    {
        <h2>@city</h2>
    }
</body>

The GetData view simply displays the country received in the controller and also a list of cities received from the route segments.

The following figure shows a sample run of the application.

image

Notice how the URL segments and the output of the view shows the country and cities.

That's it! Keep coding !!

posted Oct 15, 2016 by Shivaranjini

  Promote This Article
Facebook Share Button Twitter Share Button LinkedIn Share Button


Related Articles

ASP.NET model binding is quite powerful and flexible. It caters to most of the scenarios without much configuration from developers. However, at times you may need to intervene in order to achieve the desired model binding effect. One such situation is when you use multiple instance of a partial view on a view. This article shows one possible approach to deal with such situations.

Suppose that you have a web page as shown below:

image

As shown in the above figure the web page captures OrderID, CustomerID, ShippingAddress and BillingAddress from the end user. This information is stored in a model class - Order - that looks like this:

public class Order
{
    public int OrderID { get; set; }
    public int CustomerID { get; set; }

    public Address ShippingAddress { get; set; }
    public Address BillingAddress { get; set; }
}

public class Address
{
    public string Street1{get;set;}
    public string Street2{get;set;}
    public string Country{get;set;}
    public string PostalCode{get;set;}
}

The Order class consists of four public properties namely OrderID, CustomerID, ShippingAddress and BillingAddress. Notice that OrderID and CustomerID are integer properties whereas ShippingAddress and BillingAddress properties are of type Address. The Address class is also shown and consists of four string properties - Street1, Street2, Country and PostalCode.

Now let's assume that the whole page is rendered using two ASP.NET MVC Partial Pages. The OrderID and CustomerID is captured using _BasicDetails.cshtml as shown below:

@model Demo.Models.Order

<table>
    <tr>
        <td>@Html.LabelFor(m=>m.OrderID)</td>
        <td>@Html.TextBoxFor(m=>m.OrderID)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.CustomerID)</td>
        <td>@Html.TextBoxFor(m=>m.CustomerID)</td>
    </tr>
</table>

Note that _BasicDetails partial page has its model set to Order class. The partial page then uses LabelFor() and TextBoxFor() helpers to display a label and textbox for the OrderID and CustomerID model properties respectively.

The address information is captured using _Address.cshtml as shown below:

@model Demo.Models.Address

<table>
    <tr>
        <td>@Html.LabelFor(m=>m.Street1)</td>
        <td>@Html.TextBoxFor(m=>m.Street1)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.Street2)</td>
        <td>@Html.TextBoxFor(m=>m.Street2)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.Country)</td>
        <td>@Html.TextBoxFor(m=>m.Country)</td>
    </tr>
    <tr>
        <td>@Html.LabelFor(m=>m.PostalCode)</td>
        <td>@Html.TextBoxFor(m=>m.PostalCode)</td>
    </tr>
</table>

The _Address partial page has Address class as its model and uses LabelFor() and TextBoxFor() helpers to display model properties.

The Index view that makes use of _BasicDetails and _Address partial pages to form the complete page is shown below:

@model Demo.Models.Order

...
@using(Html.BeginForm("ProcessForm","Home",FormMethod.Post))
{
  <h3>Basic Details</h3>
  @Html.Partial("_BasicDetails")

  <h3>Shipping Address</h3>
  @Html.Partial("_Address",Model.ShippingAddress)
        
  <h3>Billing Address</h3>
  @Html.Partial("_Address",Model.BillingAddress)
        
  <input type="submit" value="Submit" />
}
</body>
</html>

The Index view renders the _BasicDetails partial page using Partial() helper. Since the model for Index view is Order class, the same is available to the _BasicDetails partial page. Then two instances of _Address partial page are rendered on the page to capture ShippingAddress and BillingAddress respectively. Recollect that _Address has Address class as its model. So, Model.ShippingAddress and Model.BillingAddress are passed to the Partial() helper.

The above form submits to ProcessForm action method that looks like this:

public ActionResult ProcessForm(Order ord)
{
    //do something with Order object here
    return View("Index");
}

And the Index() action method looks like this:

public ActionResult Index()
{
    Order ord = new Order();
    ord.BillingAddress = new Address();
    ord.ShippingAddress = new Address();
    return View(ord);
}

Both of these methods are quite straightforward and need no explanation.

Now comes the important and tricky part. If you run the application at this stage, you will get the following HTML markup in the browser (unwanted markup has been removed for the sake of clarity):

<form action="/Home/ProcessForm" method="post">        
<h3>Basic Details</h3>
<table>
    <tr>
        <td><label for="OrderID">OrderID</label></td>
        <td><input id="OrderID" name="OrderID" type="text" /></td>
    </tr>
    <tr>
        <td><label for="CustomerID">CustomerID</label></td>
        <td><input id="CustomerID" name="CustomerID" type="text" /></td>
    </tr>
</table>
<h3>Shipping Address</h3>
<table>
    <tr>
        <td><label for="Street1">Street1</label></td>
        <td><input id="Street1" name="Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Street2">Street2</label></td>
        <td><input id="Street2" name="Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Country">Country</label></td>
        <td><input id="Country" name="Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="PostalCode">PostalCode</label></td>
        <td><input id="PostalCode" name="PostalCode" type="text" value="" /></td>
    </tr>
</table>
<h3>Billing Address</h3>
<table>
    <tr>
        <td><label for="Street1">Street1</label></td>
        <td><input id="Street1" name="Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Street2">Street2</label></td>
        <td><input id="Street2" name="Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="Country">Country</label></td>
        <td><input id="Country" name="Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="PostalCode">PostalCode</label></td>
        <td><input id="PostalCode" name="PostalCode" type="text" value="" /></td>
    </tr>
</table>
<input type="submit" value="Submit" />
</form>

Notice the markup in bold letters. Can you see HTML elements with duplicate id and name attributes? That's because you are rendering two instance of the _Address partial page. The model binding framework requires that the HTML fields follow this naming convention for the model binding to work as expected:

<input id="ShippingAddress_Street1" 
       name="ShippingAddress.Street1" type="text" value="" />
<input id="BillingAddress_Street1" 
       name="BillingAddress.Street1" type="text" value="" />

As you can see from the above markup the id and name attributes must fully quality the model property being bound. In the absence of such a naming pattern the Order instance won't be bound as expected as confirmed by the following figure:

image

As shown above the ShippingAddress and BillingAddress properties are null whereas OrderID and CustomerID are captured successfully.

The above problem can be solved by using a variation of the Partial() helper while rendering the _Address partial page. The following code shows how this is done:

<h3>Basic Details</h3>
@Html.Partial("_BasicDetails")

<h3>Shipping Address</h3>
@Html.Partial("_Address", 
  new ViewDataDictionary() 
  { 
    TemplateInfo = new TemplateInfo() 
      { HtmlFieldPrefix = "ShippingAddress" } })

<h3>Billing Address</h3>
@Html.Partial("_Address", 
  new ViewDataDictionary() 
    { TemplateInfo = new TemplateInfo() 
      { HtmlFieldPrefix = "BillingAddress" } })

The variation of Partial() helper used above uses ViewDataDictionary parameter to specify TemplateInfo. The HtmlFieldPrefix property of the TemplateInfo is set to ShippingAddress for the first instance and to the BillingAddress for the second instance.

If you run the application now, you will find the following markup in the browser:

<form action="/Home/ProcessForm" method="post">
<h3>Basic Details</h3>
<table>
    <tr>
        <td><label for="OrderID">OrderID</label></td>
        <td><input id="OrderID" name="OrderID" type="text" value="0" /></td>
    </tr>
    <tr>
        <td><label for="CustomerID">CustomerID</label></td>
        <td><input id="CustomerID" name="CustomerID" type="text" value="0" /></td>
    </tr>
</table>
<h3>Shipping Address</h3>
<table>
    <tr>
        <td><label for="ShippingAddress_Street1">Street1</label></td>
        <td><input id="ShippingAddress_Street1" 
                   name="ShippingAddress.Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="ShippingAddress_Street2">Street2</label></td>
        <td><input id="ShippingAddress_Street2" 
                   name="ShippingAddress.Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="ShippingAddress_Country">Country</label></td>
        <td><input id="ShippingAddress_Country" 
                   name="ShippingAddress.Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="ShippingAddress_PostalCode">PostalCode</label></td>
        <td><input id="ShippingAddress_PostalCode" 
                   name="ShippingAddress.PostalCode" type="text" value="" /></td>
    </tr>
</table>
<h3>Billing Address</h3>
<table>
    <tr>
        <td><label for="BillingAddress_Street1">Street1</label></td>
        <td><input id="BillingAddress_Street1" 
                   name="BillingAddress.Street1" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="BillingAddress_Street2">Street2</label></td>
        <td><input id="BillingAddress_Street2" 
                   name="BillingAddress.Street2" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="BillingAddress_Country">Country</label></td>
        <td><input id="BillingAddress_Country" 
                   name="BillingAddress.Country" type="text" value="" /></td>
    </tr>
    <tr>
        <td><label for="BillingAddress_PostalCode">PostalCode</label></td>
        <td><input id="BillingAddress_PostalCode" 
                   name="BillingAddress.PostalCode" type="text" value="" /></td>
    </tr>
</table>
<input type="submit" value="Submit" />
</form>

As expected the id and name attributes are now fully qualified and hence the model binding will happen as expected as shown below:

image

The model binding now correctly captures ShippingAddress as well as BillingAddress information.

READ MORE
...