© Lee Naylor 2016

Lee Naylor, ASP.NET MVC with Entity Framework and CSS , 10.1007/978-1-4842-2137-2_5

5. Sorting, Paging, and Routing

Lee Naylor

(1)Newton-le-Willows, Merseyside, UK

This chapter focuses on adding, sorting, and paging products and adding support for friendly URLs to use the ASP.NET Routing feature.

Note

If you want to follow along with the code in this chapter, you must either have completed Chapter 4 or download Chapter 4’s source code for from www.apress.com as a starting point.

Sorting Products by Price

To demonstrate sorting, I’ll show you a simple example to sort products by price, allowing users to order products by price.

First of all, add a new switch statement to the Index method of the ControllersProductsController.cs file, as highlighted in the following code, so that the products are reordered by price:

// GET: Products
public ActionResult Index(string category, string search, string sortBy)
{
    //instantiate a new view model
    ProductIndexViewModel viewModel = new ProductIndexViewModel();


    //select the products
    var products = db.Products.Include(p => p.Category);


    //perform the search and save the search string to the viewModel
    if (!String.IsNullOrEmpty(search))
    {
        products = products.Where(p => p.Name.Contains(search) ||
        p.Description.Contains(search) ||
        p.Category.Name.Contains(search));
        viewModel.Search = search;
    }


    //group search results into categories and count how many items in each category
    viewModel.CatsWithCount = from matchingProducts in products
                                where
                                matchingProducts.CategoryID != null
                                group matchingProducts by
                                         matchingProducts.Category.Name into
                                catGroup
                                select new CategoryWithCount()
                                {
                                    CategoryName = catGroup.Key,
                                    ProductCount = catGroup.Count()
                                };


    if (!String.IsNullOrEmpty(category))
    {
        products = products.Where(p => p.Category.Name == category);
    }


    //sort the results
    switch (sortBy)
    {
        case "price_lowest":
            products = products.OrderBy(p => p.Price);
            break;
        case "price_highest":
            products = products.OrderByDescending(p => p.Price);
            break;
        default:
            break;
    }


    viewModel.Products = products;
    return View(viewModel);
}

This new code uses the Entity Framework OrderBy and OrderByDescending methods to sort products by ascending and descending price. Run the application without debugging and manually change the URL to test that sorting works as expected, by using the Products?sortBy=price_lowest and Products?sortBy=price_highest URLs. The products should reorder with the lowest priced item at the top and the highest priced item at the top, respectively. Figure 5-1 shows the products being sorted with the highest price first.

A419071_1_En_5_Fig1_HTML.jpg
Figure 5-1. The products list sorted by highest price first

Adding Sorting to the Products Index View

We now need to add some user interface controls for sorting into the web site to allow users to choose how they want to sort. To demonstrate this, add a select list and populate it with values and text from a dictionary type.

First of all, add the following highlighted SortBy and Sorts properties to the ProductIndexViewModel class in the ViewModelsProductIndexViewModel.cs file:

using BabyStore.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;


namespace BabyStore.ViewModels
{
    public class ProductIndexViewModel
    {
        public IQueryable<Product> Products { get; set; }
        public string Search { get; set; }
        public IEnumerable<CategoryWithCount> CatsWithCount { get; set; }
        public string Category { get; set; }
        public string SortBy { get; set; }
        public Dictionary<string, string> Sorts { get; set; }


        public IEnumerable<SelectListItem> CatFilterItems
        {
            get
            {
                var allCats = CatsWithCount.Select(cc => new SelectListItem
                {
                    Value = cc.CategoryName,
                    Text = cc.CatNameWithCount
                });


                return allCats;
            }
        }
    }


    public class CategoryWithCount
    {
        public int ProductCount { get; set; }
        public string CategoryName { get; set; }
        public string CatNameWithCount
        {
            get
            {
                return CategoryName + " (" + ProductCount.ToString() + ")";
            }
        }
    }
}

The SortBy property will be used as the name of the select element in the view and the Sorts property will be used to hold the data to populate the select element.

Now we need to populate the Sorts property from the ProductController class. Modify the ControllersProductsController.cs file to add the following line of code to the end of the Index method prior to returning the View:

// GET: Products
public ActionResult Index(string category, string search, string sortBy)
{
    //instantiate a new view model
    ProductIndexViewModel viewModel = new ProductIndexViewModel();


    //select the products
    var products = db.Products.Include(p => p.Category);


    //perform the search and save the search string to the viewModel
    if (!String.IsNullOrEmpty(search))
    {
        products = products.Where(p => p.Name.Contains(search) ||
        p.Description.Contains(search) ||
        p.Category.Name.Contains(search));
        viewModel.Search = search;
    }


    //group search results into categories and count how many items in each category
    viewModel.CatsWithCount = from matchingProducts in products
                                where
                                matchingProducts.CategoryID != null
                                group matchingProducts by
                                matchingProducts.Category.Name into
                                catGroup
                                select new CategoryWithCount()
                                {
                                    CategoryName = catGroup.Key,
                                    ProductCount = catGroup.Count()
                                };


    if (!String.IsNullOrEmpty(category))
    {
        products = products.Where(p => p.Category.Name == category);
    }


    //sort the results
    switch (sortBy)
    {
        case "price_lowest":
            products = products.OrderBy(p => p.Price);
            break;
        case "price_highest":
            products = products.OrderByDescending(p => p.Price);
            break;
        default:
            break;
    }


    viewModel.Products = products;
    viewModel.Sorts = new Dictionary<string, string>
    {
        {"Price low to high", "price_lowest" },
        {"Price high to low", "price_highest" }
    };


    return View(viewModel);
}

Finally, we need to add the control to the view so that users can make a selection. To achieve this, add the highlighted code to the ViewsProductsIndex.cshtml file after the filter by category code as follows:

@model BabyStore.ViewModels.ProductIndexViewModel

@{
    ViewBag.Title = "Products";
}


<h2>@ViewBag.Title</h2>

<p>
    @Html.ActionLink("Create New", "Create")
    @using (Html.BeginForm("Index", "Products", FormMethod.Get))
    {
        <label>Filter by category:</label>
        @Html.DropDownListFor(vm => vm.Category, Model.CatFilterItems, "All");
        <label>Sort by:</label>
        @Html.DropDownListFor(vm => vm.SortBy, new SelectList(Model.Sorts, "Value", "Key"),  
        "Default")
        <input type="submit" value="Filter" />
        <input type="hidden" name="Search" id="Search" value="@Model.Search" />
    }
</p>


<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Category)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Products.First().Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Products.First().Description)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Products.First().Price)
        </th>
        <th></th>
    </tr>


    @foreach (var item in Model.Products)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Category.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
                @Html.ActionLink("Details", "Details", new { id = item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.ID })
            </td>
        </tr>
    }
</table>

This new select control uses the SortBy property from the view model as its name. It populates itself with the data from the view model’s Sorts property using the second entry in each line of the dictionary as the value submitted by the control (specified by "Value") and the first entry in each line as the text displayed to the user (specified by "Key"), as shown in Figure 5-2.

A419071_1_En_5_Fig2_HTML.jpg
Figure 5-2. The sort by select control in the Products index page

Start the site without debugging and click on View All Our Products. Next to the category filter, you will now see a select list allowing the users to select to sort by price, as shown in Figure 5-2. You can use this new control to sort products by price.

Adding Paging

In this section, I will show you a way to add paging to allow users to page through the product search results rather than showing them all in one large list. This code will use the popular NuGet package PagedList.Mvc, which is written and maintained by Troy Goode. I have chosen to use this as an introduction to paging because it is easy to set up and use. Later in the book, I will show you how to write your own asynchronous paging code and an HTML helper to display paging controls.

Installing PagedList.Mvc

First of all, we need to install the package. Open the Project menu and then choose Manage NuGet Packages in order to display the NuGet Package Manager window. In this window, select the browse option and then search for pagedlist, as shown in Figure 5-3. Then install the latest version of PagedList.Mvc (currently 4.5.0) by clicking on the Install link. When you install PagedList.Mvc, the PagedList package is also installed.

A419071_1_En_5_Fig3_HTML.jpg
Figure 5-3. The NuGet Package Manager showing PagedList.Mvc as the top result

Updating the View Model and Controller for Paging

Once PagedList.Mvc is installed, the first thing that needs to be modified is ProductIndexViewModel, so that the Products property is changed to the type IPagedList. Modify the ViewModelsProductIndexViewModel.cs file to update the code highlighted here:

using BabyStore.Models;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using PagedList;


namespace BabyStore.ViewModels
{
    public class ProductIndexViewModel
    {
        public IPagedList<Product> Products { get; set; }
        public string Search { get; set; }
        public IEnumerable<CategoryWithCount> CatsWithCount { get; set; }
        ...rest of code omitted for brevity...

We now need to modify the Index method of the ProductsController class so that it returns Products as a PagedList (achieved by using the ToPagedList() method). A default sort order also needs to be set in order to use PagedList. First of all, add the code using PagedList; to the using statements at the top of the file. Then modify the ControllersProductsController.cs file, as highlighted, in order to use the new PagedList package.

public ActionResult Index(string category, string search, string sortBy, int? page)
{
    //instantiate a new view model
    ProductIndexViewModel viewModel = new ProductIndexViewModel();


    //select the products
    var products = db.Products.Include(p => p.Category);


    //perform the search and save the search string to the viewModel
    if (!String.IsNullOrEmpty(search))
    {
        products = products.Where(p => p.Name.Contains(search) ||
        p.Description.Contains(search) ||
        p.Category.Name.Contains(search));
        viewModel.Search = search;
    }


    //group search results into categories and count how many items in each category
    viewModel.CatsWithCount = from matchingProducts in products
                                where
                                matchingProducts.CategoryID != null
                                group matchingProducts by
    matchingProducts.Category.Name into
                                catGroup
                                select new CategoryWithCount()
                                {
                                    CategoryName = catGroup.Key,
                                    ProductCount = catGroup.Count()
                                };


    if (!String.IsNullOrEmpty(category))
    {
        products = products.Where(p => p.Category.Name == category);
        viewModel.Category = category;
    }


    //sort the results
    switch (sortBy)
    {
        case "price_lowest":
            products = products.OrderBy(p => p.Price);
            break;
        case "price_highest":
            products = products.OrderByDescending(p => p.Price);
            break;
        default:
            products = products.OrderBy(p => p.Name);
            break;
    }


    const int PageItems = 3;
      int currentPage = (page ?? 1);
      viewModel.Products = products.ToPagedList(currentPage, PageItems);
      viewModel.SortBy = sortBy;
    viewModel.Sorts = new Dictionary<string, string>
    {
        {"Price low to high", "price_lowest" },
        {"Price high to low", "price_highest" }
    };
    return View(viewModel);
}

The first change adds the parameter int? page, which is a nullable integer and will represent the current page chosen by the user in the view. When the Products index page is first loaded, the user will not have selected any page, hence this parameter can be null.

We also need to ensure that the current category is saved to the view model so we have added this line of code to ensure that you can page within a category: viewModel.Category = category;.

The code products = products.OrderBy(p => p.Name); is then used to set a default order of products because PagedList requires the list it receives to be sorted.

Next, we specify the number of items to appear on each page by adding a constant using the line of code const int PageItems = 3;. We then declare an integer variable int currentPage = (page ?? 1); to hold the current page number and take the value of the page parameter, or 1, if the page variable is null.

The products property of the view model is then assigned a PagedList of products specifying the current page and the number of items per page using the code viewModel.Products = products.ToPagedList(currentPage, PageItems);.

Finally, the sortBy value is now saved to the view model so that the sort order of the products list is preserved when moving from one page to another by the code: viewModel.SortBy = sortBy;.

Updating the Products Index View for Paging

Having implemented the paging code in our view model and controller, we now need to update the ViewsProductsIndex.cshtml file to display a paging control so that the user can move between pages. We’ll also add an indication of how many items were found. To achieve this, modify the file to add a new using statement, add an indication of the total number of products found, and display paging links at the bottom of the page, as highlighted in the following code:

@model BabyStore.ViewModels.ProductIndexViewModel
@using PagedList.Mvc


@{
    ViewBag.Title = "Products";
}


<h2>@ViewBag.Title</h2>
<p>
    @(String.IsNullOrWhiteSpace(Model.Search) ? "Showing all" : "You search for " +
        Model.Search + " found")  @Model.Products.TotalItemCount products
</p>


<p>
    @Html.ActionLink("Create New", "Create")
    @using (Html.BeginForm("Index", "Products", FormMethod.Get))
    {
        <label>Filter by category:</label>
        @Html.DropDownListFor(vm => vm.Category, Model.CatFilterItems, "All");
        <label>Sort by:</label>
        @Html.DropDownListFor(vm => vm.SortBy, new SelectList(Model.Sorts, "Value", "Key"), "Default")
        <input type="submit" value="Filter" />
        <input type="hidden" name="Search" id="Search" value="@Model.Search" />
    }
</p>


<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Category)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Products.First().Name)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Products.First().Description)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Products.First().Price)
        </th>
        <th></th>
    </tr>


    @foreach (var item in Model.Products)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Category.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Description)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
                @Html.ActionLink("Details", "Details", new { id = item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id = item.ID })
            </td>
        </tr>
    }
</table>
<div>
    Page @(Model.Products.PageCount < Model.Products.PageNumber ? 0 :  
        Model.Products.PageNumber) of @Model.Products.PageCount
    @Html.PagedListPager(Model.Products, page => Url.Action("Index",
        new { category = @Model.Category,
              Search = @Model.Search,
            sortBy = @Model.SortBy,
            page
    }))
</div>

The indication of how many products were found is displayed using the code:

<p>
    @(String.IsNullOrWhiteSpace(Model.Search) ? "Showing all" : "You search for " +
        Model.Search + " found")  @Model.Products.TotalItemCount products
</p>

This code uses the ?: (also known as ternary) operator to check if the search term is null or made up of whitespace. If this is true, the output of the code will be "Showing all xx products" or else if the user has entered a search term, the output will be "Your search for search term found xx products". In effect, this operates as a shorthand if statement. More information on the ?: operator can be found at https://msdn.microsoft.com/en-gb/library/ty67wk28.aspx .

Finally, the paging links are generated by this new code:

<div>
    Page @(Model.Products.PageCount < Model.Products.PageNumber ? 0 :  
        Model.Products.PageNumber) of @Model.Products.PageCount
    @Html.PagedListPager(Model.Products, page => Url.Action("Index",
        new { category = @Model.Category,Search = @Model.Search,sortBy = @Model.SortBy,
              page
        }))
</div>

This code is wrapped in a div tag for presentation purposes. The first code line uses the ?: operator to decide whether or not there are any pages to display. It displays "Page 0 of 0" or "Page x of y" where x is the current page and y the total number of pages.

The next line of code uses the HTML PagedListPager helper that comes as part of the PagedList.Mvc namespace. This helper takes the list of products and produces a hyperlink to each page. Url.Action is used to generate a hyperlink targeting the Index view containing the page parameter. We have added an anonymous type to the helper method in order to pass the current category, search, and sort order to the helper so that each page link contains these in its querystring. This means that the search term, chosen category, and sort order are all preserved when moving from one page to another. Without them, the list of products would be reset to show all the products.

Figure 5-4 shows the effect of this code. A search has been performed for sleeping and the results are filtered to the sleeping category, sorted by price high to low. The user has moved to page 2 of the results.

A419071_1_En_5_Fig4_HTML.jpg
Figure 5-4. Working paging with search term, sorting, and filtering by categories preserved

Routing

So far we have been using parameters in the querystring portion of the URL to pass data for categories and paging to the Index action method in the ProductController class. These URLs follow the standard format such as /Products?category=Sleeping&page=2, and although functional, these URLs can be improved upon by using the ASP.NET Routing feature. It generates URLs in a more “friendly” format that is more meaningful to users and search engines. ASP.NET routing is not specific to MVC and can also be used with Web Forms and Web API; however, the methods used are slightly different when working with Web Forms.

To keep things manageable, we’re going to generate routes only for categories and paging. There won’t be a route for searching or sorting due to the fact that routing requires a route for each expected combination that can appear and each parameter needs some way of identifying itself. For example, we use the word “page” to prefix each page number in the routes that use it. It is possible to make routing overly complex by trying to add a route for everything. It’s also worth noting that any values submitted by the HTML form for filtering by category will still generate URLs in the “old” format because that is how HTML forms work by design.

One of the most important things about routing is that routes have to be added in the order of most specific first, with more general routes further down the list. The routing system searches down the routes until it finds anything that matches the current URL and then it stops. If there is a general route that matches a URL and a more specific route that also matches, but it is defined below the more general route then the more specific route will never be used.

Adding Routes

We are going to take the same approach to adding routes as used by the scaffolding process when the project was created and add them to the App_StartRouteConfig.cs file in this format:

routes.MapRoute(
    name: "Name",
    url: "Rule",
    defaults: DefaultValues
);

The name parameter represents the name of the route and can be left blank; however, we’ll be using them in this book to differentiate between routes.

The url parameter contains a rule for matching the route to a URL format. This can contain several formats and arguments, as follows:

  • The url parameter is divided into segments, with each segment matching sections of the URL.

  • A URL has to have the same number of segments as the url parameter in order to match it, unless either defaults or a wildcard is specified (see the following bullet points for an explanation of each of these).

  • Each segment can be:

    • A static element URL, such as “Products”. This will simply match the URL /Products and will call the relevant controller and action method.

    • A variable element that is able to match anything. For example, "Products/{category}" will match anything after Products in a URL and assign it to {category} and {category} can then be used in the action method targeted by the route.

    • A combination of static and variable elements that will match anything in the same URL segment matching the format specified. For example, "Products/Page{page}" will match URLs such as Products/Page2 or Products/Page99 and assign the value of the part of the URL following Page to {page}.

    • A catch-all wildcard element, for example "Products/{*everything}, will map everything following the Products section of the URL into the everything variable segment. This is done regardless of whether it contains slashes or not. We won’t use wildcard matches in this project.

  • Each segment can also be specified as being optional or having a default value if the corresponding element of the URL is blank. A good example of this is the default route specified when the project was created. This route uses the following code to specify default values for the controller and action method to be used. It also defines the id element as being optional:

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

To start with routes, add a route for allowing URLs in the format /Products/Category (such as /Products/Sleeping to display just the products in the Sleeping category). Add the following highlighted code to the RegisterRoutes method in the App_StartRouteConfig.cs file, above the Default route:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;


namespace BabyStore
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


            routes.MapRoute(
                name: "ProductsbyCategory",
                url: "Products/{category}",
                defaults: new { controller = "Products", action = "Index" }
            );


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

Start the web site without debugging and click on Shop by Category, then click on Sleeping. The link will now open the /Products/Sleeping URL, as shown in Figure 5-5, due to the new ProductsbyCategory route.

A419071_1_En_5_Fig5_HTML.jpg
Figure 5-5. The Products/Category URL in action

So far so good; everything looks like it’s working okay and you can now use the URLs in the format Products/Category. However, there is a problem. Try clicking on the Create New link. The product create page no longer appears and instead a blank list of categories is displayed. The reason for this is that the new route treats everything following Products in the URL as a category. There is no category named Create, so no products are returned, as shown in Figure 5-6.

A419071_1_En_5_Fig6_HTML.jpg
Figure 5-6. Broken Create New link

Now click the back button to go back to the products list with some products displayed. Try clicking on the Edit, Details, and Delete links. They still work! You may be wondering why this is; well, the answer lies in the fact that the working links all have an ID parameter on the end of them. For example, the edit links take the format /Products/Edit/6 and this format matches the original Default route ("{controller}/{action}/{id}") rather than the new ProductsbyCategory route ("Products/{category}").

To fix this issue, we need to add a more specific route for the Products/Create URL. Add a new route the RegisterRoutes method in the App_StartRouteConfig.cs file above the ProductsbyCategory route, as highlighted:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


    routes.MapRoute(
        name: "ProductsCreate",
        url: "Products/Create",
        defaults: new { controller = "Products", action = "Create" }
    );


    routes.MapRoute(
        name: "ProductsbyCategory",
        url: "Products/{category}",
        defaults: new { controller = "Products", action = "Index" }
    );


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

Start the web site without debugging and click on the Create Product link. It now works again because of the ProductsCreate route. It’s very important to add the ProductsCreate route above the ProductsByCategory route; otherwise, it will never be used. If it was below the ProductsByCategory route, the routing system would find a match for the "Products/{category}" URL first and then stop searching for a matching route.

Next we’re going to add a route for paging so that the web site can use URLs in the format /Products/Page2. Update the RegisterRoutes method of the App_StartRouteConfig.cs file to add a new route to the file above the ProductsbyCategory route, as follows:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


    routes.MapRoute(
        name: "ProductsCreate",
        url: "Products/Create",
        defaults: new { controller = "Products", action = "Create" }
    );


    routes.MapRoute(
        name: "ProductsbyPage",
        url: "Products/Page{page}",
        defaults: new
        { controller = "Products", action = "Index" }
    );


    routes.MapRoute(
        name: "ProductsbyCategory",
        url: "Products/{category}",
        defaults: new { controller = "Products", action = "Index" }
    );


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

This new ProductsByPage route will match any URLs with the format Products/PageX, where X is the page number. Again this route must appear before the ProductsbyCategory route; otherwise, it will never get used. Try the new ProductsbyPage route by starting the web site without debugging and clicking View All Our Products. Then click on a page number in the paging control at the bottom of the page. The URL should now appear in the format Products/PageX. For example, Figure 5-7 shows the result of clicking on number 4 in the paging control, which generates the URL Products/Page4.

A419071_1_En_5_Fig7_HTML.jpg
Figure 5-7. The updated Products/PageX route in action

So far we have added routes for Products/Category and Product/PageX, but we have nothing for Product/Category/PageX. To add a new route that allows this, add the following code above the ProductsByPage route in the RegisterRoutes method of the App_StartRouteConfig.cs file:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


    routes.MapRoute(
        name: "ProductsCreate",
        url: "Products/Create",
        defaults: new { controller = "Products", action = "Create" }
    );


    routes.MapRoute(
        name: "ProductsbyCategorybyPage",
        url: "Products/{category}/Page{page}",
        defaults: new { controller = "Products", action = "Index" }
    );


    routes.MapRoute(
        name: "ProductsbyPage",
        url: "Products/Page{page}",
        defaults: new
        { controller = "Products", action = "Index" }
    );


    routes.MapRoute(
        name: "ProductsbyCategory",
        url: "Products/{category}",
        defaults: new { controller = "Products", action = "Index" }
    );


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

Start the web site without debugging and click on Shop by Category and then click on Sleeping. Then click on page 2 in the paging control. The URL now generated is Products/Sleeping/Page2 because of the new ProductsbyCategorybyPage route. This is shown in Figure 5-8.

A419071_1_En_5_Fig8_HTML.jpg
Figure 5-8. The ProductsbyCategorybyPage route in action

We now appear to have added all the new routes, but there are still some issues with how the new routes affect the site. To see the first remaining issue, start with the web site filtered as shown in Figure 5-8. Then try to choose another category from the drop-down and clicking the filter button. The results are not filtered and remain on the Sleeping category. This is because the HTML form no longer targets the ProductsController index method correctly. To resolve this issue, we need to add one final route to the /App_Start/RouteConfig.cs file and then configure the HTML form to use it.

First of all, add a new route above the default route in the RegisterRoutes method of the App_StartRoutesConfig.cs file:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


    routes.MapRoute(
        name: "ProductsCreate",
        url: "Products/Create",
        defaults: new { controller = "Products", action = "Create" }
    );


    routes.MapRoute(
        name: "ProductsbyCategorybyPage",
        url: "Products/{category}/Page{page}",
        defaults: new { controller = "Products", action = "Index" }
    );


    routes.MapRoute(
        name: "ProductsbyPage",
        url: "Products/Page{page}",
        defaults: new
        { controller = "Products", action = "Index" }
    );


    routes.MapRoute(
        name: "ProductsbyCategory",
        url: "Products/{category}",
        defaults: new { controller = "Products", action = "Index" }
    );


    routes.MapRoute(
        name: "ProductsIndex",
        url: "Products",
        defaults: new { controller = "Products", action = "Index" }
    );


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

This new route is named ProductsIndex and it creates a route that targets the Index action method of the ProductsController class. We’ve created this to give the web site a way to target this method when using URL links and forms.

Using Routes in Forms

Currently the form that filters by category in the Products index page works incorrectly, as it is still configured to try to target the Index method of the ProductsController class. For example, submitting the form to filter to Feeding after it has previously been set to Sleeping generates the following URL: http://localhost:58735/Products/Sleeping?Category=Feeding&SortBy=&Search =.

This URL contains two category arguments—Sleeping and Feeding—so the web site simply filters by the first one it encounters and the products list remains filtered to the Sleeping category. The Sleeping category must be removed when the form is submitted. In order to fix this issue, the HTML form containing the category filter needs to use the ProductsIndex route, which will enable it to remove all the parameters currently prefixed to the URL and only target the Index action method with the parameters submitted by the form.

To change the form to use the ProductsIndex route, change the following line of code in the ViewsProductsIndex.cshtml file from:

@using (Html.BeginForm("Index", "Products", FormMethod.Get))

to:

@using (Html.BeginRouteForm("ProductsIndex", FormMethod.Get))

Now start the web site without debugging and filter products to page 2 of the Sleeping category, as shown in Figure 5-8. Then change the category to Feeding and click the Filter button. The search result will now be filtered to just the products in the Feeding category.

Note

HTML forms can only be configured to submit to routes. They will not submit values in the format of routing; for example, the filter form still submits URLs in the format Products?Category=Feeding&SortBy=&Search= rather than Products/Feeding. This is because the default behavior of HTML forms is to submit URLs in this format, with input elements appended to the querystring element of the URL.

The search form also has the same issue as the filter form, so update the search form in the /Views/Shared/_Layout.cshtml file by changing the line:

@using (Html.BeginForm("Index", "Products", FormMethod.Get, new { @class = "navbar-form navbar-left" }))

to:

@using (Html.BeginRouteForm("ProductsIndex", FormMethod.Get, new { @class = "navbar-form navbar-left" }))

Using a Route in a Hyperlink

The final issue that needs to be resolved is that the View All Our Products link does not show all the products if the user has filtered by category. This is a very similar issue to the one that affected the HTML forms in that the link does not remove any parameters already assigned to the URL. To fix this issue, the outgoing URL link needs to be updated to point to a route rather than an action method. Update the ViewsShared\_Layout.cshtml file to change this line of code:

<li>@Html.ActionLink("View all our Products", "Index", "Products")</li>

to:

<li>@Html.RouteLink("View all our Products", "ProductsIndex")</li>

To pass extra parameters via a URL that targets a route, you simply add them into the link as an anonymous object in the same way that HTML attributes are passed. For example, to make a link that targets just the clothing category, you could use this code: <li>@Html.RouteLink("View all Clothes", "ProductsbyCategory", new { category = "Clothes" })</li>.

Setting a Project Start URL

Now that we’ve added some routes, it makes sense to stop Visual Studio from automatically loading the view that you are editing when you start the project. In Visual Studio, open the Project menu and choose BabyStore Properties in order to open the project properties window (or right-click the project in Solution Explorer and choose Properties). Then choose the Web section and set the Start Action to Specific Page, as shown in Figure 5-9. Don’t enter a value; simply setting this option will make the project load the home page.

A419071_1_En_5_Fig9_HTML.jpg
Figure 5-9. Setting the project start URL to the Specific Page option

Summary

This chapter started by adding sorting to the search results using Entity Framework and adding a select list to the web page so that users can sort products by price. We then added paging to the Products index page using the PagedList.Mvc package and finally used ASP.NET routing so that the web site now displays friendly URLs for categories and paging.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset