In this chapter, I continue to build out the SportsStore example app. I add support for navigating around the application and start building a shopping cart.
You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/apress/pro-asp.net-core-3. See Chapter 1 for how to get help if you have problems running the examples.
Adding Navigation Controls
Enhance the Index action method in the HomeController class so that it can filter the Product objects in the repository
Revisit and enhance the URL scheme
Create a category list that will go into the sidebar of the site, highlighting the current category and linking to others
Filtering the Product List
Modifying the ProductsListViewModel.cs File in the SportsStore/Models/ViewModels Folder
Adding Category Support in the HomeController.cs File in the SportsStore/Controllers Folder
I made three changes to the action method. First, I added a parameter called category. This category parameter is used by the second change in the listing, which is an enhancement to the LINQ query: if category is not null, only those Product objects with a matching Category property are selected. The last change is to set the value of the CurrentCategory property I added to the ProductsListViewModel class. However, these changes mean that the value of PagingInfo.TotalItems is incorrectly calculated because it doesn’t take the category filter into account. I will fix this in a while.
Keeping your unit tests synchronized with your code changes quickly becomes second nature when you get into the testing mindset.
Obviously, users won’t want to navigate to categories using URLs, but you can see how small changes can have a big impact once the basic structure of an ASP.NET Core application is in place.
This test creates a mock repository containing Product objects that belong to a range of categories. One specific category is requested using the action method, and the results are checked to ensure that the results are the right objects in the right order.
Refining the URL Scheme
No one wants to see or use ugly URLs such as /?category=Soccer. To address this, I am going to change the routing configuration in the Configure method of the Startup class to create a more useful set of URLs, as shown in Listing 8-3.
It is important to add the new routes in Listing 8-3 in the order they are shown. Routes are applied in the order in which they are defined, and you will get some odd effects if you change the order.
Changing the Routing Schema in the Startup.cs File in the SportsStore Folder
Route Summary
URL | Leads To |
---|---|
/ | Lists the first page of products from all categories |
/Page2 | Lists the specified page (in this case, page 2), showing items from all categories |
/Soccer | Shows the first page of items from a specific category (in this case, the Soccer category) |
/Soccer/Page2 | Shows the specified page (in this case, page 2) of items from the specified category (in this case, Soccer) |
The ASP.NET Core routing system handles incoming requests from clients, but it also generates outgoing URLs that conform to the URL scheme and that can be embedded in web pages. By using the routing system both to handle incoming requests and to generate outgoing URLs, I can ensure that all the URLs in the application are consistent.
Prefixed Values in the PageLinkTagHelper.cs File in the SportsStore/Infrastructure Folder
Decorating a tag helper property with the HtmlAttributeName attribute allows me to specify a prefix for attribute names on the element, which in this case will be page-url-. The value of any attribute whose name begins with this prefix will be added to the dictionary that is assigned to the PageUrlValues property, which is then passed to the IUrlHelper.Action method to generate the URL for the href attribute of the a elements that the tag helper produces.
Adding a New Attribute in the Index.cshtml File in the SportsStore/Views/Home Folder
Building a Category Navigation Menu
I need to provide users with a way to select a category that does not involve typing in URLs. This means presenting a list of the available categories and indicating which, if any, is currently selected.
ASP.NET Core has the concept of view components, which are perfect for creating items such as reusable navigation controls. A view component is a C# class that provides a small amount of reusable application logic with the ability to select and display Razor partial views. I describe view components in detail in Chapter 24.
In this case, I will create a view component that renders the navigation menu and integrate it into the application by invoking the component from the shared layout. This approach gives me a regular C# class that can contain whatever application logic I need and that can be unit tested like any other class.
Creating the Navigation View Component
The Contents of the NavigationMenuViewComponent.cs File in the SportsStore/Components Folder
The view component’s Invoke method is called when the component is used in a Razor view, and the result of the Invoke method is inserted into the HTML sent to the browser. I have started with a simple view component that returns a string, but I’ll replace this with HTML shortly.
Using a View Component in the _Layout.cshtml File in the SportsStore/Views/Shared Folder
I removed the placeholder text and replaced it with the vc:navigation-menu element, which inserts the view component. The element omits the ViewComponent part of the class name and hyphenates it, such that vc:navigation-menu specifies the NavigationMenuViewComponent class.
Generating Category Lists
Adding Categories in the NavigationMenuViewComponent.cs File in the SportsStore/Components Folder
The constructor defined in Listing 8-8 defines an IStoreRepository parameter. When ASP.NET Core needs to create an instance of the view component class, it will note the need to provide a value for this parameter and inspect the configuration in the Startup class to determine which implementation object should be used. This is the same dependency injection feature that I used in the controller in Chapter 7, and it has the same effect, which is to allow the view component to access data without knowing which repository implementation will be used, a feature I describe in detail in Chapter 14.
In the Invoke method, I use LINQ to select and order the set of categories in the repository and pass them as the argument to the View method, which renders the default Razor partial view, details of which are returned from the method using an IViewComponentResult object, a process I describe in more detail in Chapter 24.
I created a mock repository implementation that contains repeating categories and categories that are not in order. I assert that the duplicates are removed and that alphabetical ordering is imposed.
Creating the View
The Contents of the Default.cshtml File in the SportsStore/Views/Shared/Components/NavigationMenu Folder
This view uses one of the built-in tag helpers, which I describe in Chapters 25–27, to create anchor elements whose href attribute contains a URL that selects a different product category.
Highlighting the Current Category
There is no feedback to the user to indicate which category has been selected. It might be possible to infer the category from the items in the list, but some clear visual feedback seems like a good idea. ASP.NET Core components such as controllers and view components can receive information about the current request by asking for a context object. Most of the time, you can rely on the base classes that you use to create components to take care of getting the context object for you, such as when you use the Controller base class to create controllers.
The ViewComponent base class is no exception and provides access to context objects through a set of properties. One of the properties is called RouteData, which provides information about how the request URL was handled by the routing system.
Passing the Selected Category in the NavigationMenuViewComponent.cs File in the SportsStore/Components Folder
Inside the Invoke method, I have dynamically assigned a SelectedCategory property to the ViewBag object and set its value to be the current category, which is obtained through the context object returned by the RouteData property. The ViewBag is a dynamic object that allows me to define new properties simply by assigning values to them.
This unit test provides the view component with routing data through the ViewComponentContext property, which is how view components receive all their context data. The ViewComponentContext property provides access to view-specific context data through its ViewContext property, which in turn provides access to the routing information through its RouteData property. Most of the code in the unit test goes into creating the context objects that will provide the selected category in the same way that it would be presented when the application is running and the context data is provided by ASP.NET Core MVC.
Highlighting in the Default.cshtml File in the SportsStore/Views/Shared/Components/NavigationMenu Folder
Correcting the Page Count
Creating Category Pagination Data in the HomeController.cs File in the SportsStore/Controllers Folder
Notice that I also call the Index method, specifying no category, to make sure I get the correct total count as well.
Building the Shopping Cart
An Add To Cart button will be displayed alongside each of the products in the catalog. Clicking this button will show a summary of the products the customer has selected so far, including the total cost. At this point, the user can click the Continue Shopping button to return to the product catalog or click the Checkout Now button to complete the order and finish the shopping session.
Configuring Razor Pages
Enabling Razor Pages in the Startup.cs File in the SportsStore Folder
The AddRazorPages method sets up the services used by Razor Pages, and the MapRazorPages method registers Razor Pages as endpoints that the URL routing system can use to handle requests.
The Contents of the _ViewImports.cshtml File in the SportsStore/Pages Folder
The Contents of the _ViewStart.cshtml File in the SportsStore/Pages Folder
The Contents of the _CartLayout.cshtml File in the SportsStore/Pages Folder
Creating a Razor Page
The Contents of the Cart.cshtml File in the SportsStore/Pages Folder
Creating the Add To Cart Buttons
The Contents of the UrlExtensions.cs File in the SportsStore/Infrastructure Folder
The PathAndQuery extension method operates on the HttpRequest class, which ASP.NET Core uses to describe an HTTP request. The extension method generates a URL that the browser will be returned to after the cart has been updated, taking into account the query string, if there is one. In Listing 8-19, I have added the namespace that contains the extension method to the view imports file so that I can use it in the partial view.
This is the view imports file in the Views folder and not the one added to the Pages folder.
Adding a Namespace in the _ViewImports.cshtml File in the SportsStore/Views Folder
Adding the Buttons to the ProductSummary.cshtml File View in the SportsStore/Views/Shared Folder
I have added a form element that contains hidden input elements specifying the ProductID value from the view model and the URL that the browser should be returned to after the cart has been updated. The form element and one of the input elements are configured using built-in tag helpers, which are a useful way of generating forms that contain model values and that target controllers or Razor Pages, as described in Chapter 27. The other input element uses the extension method I created to set the return URL. I also added a button element that will submit the form to the application.
Notice that I have set the method attribute on the form element to post, which instructs the browser to submit the form data using an HTTP POST request. You can change this so that forms use the GET method, but you should think carefully about doing so. The HTTP specification requires that GET requests must be idempotent, meaning that they must not cause changes, and adding a product to a cart is definitely a change.
Enabling Sessions
Enabling Sessions in the Startup.cs File in the SportsStore Folder
The AddDistributedMemoryCache method call sets up the in-memory data store. The AddSession method registers the services used to access session data, and the UseSession method allows the session system to automatically associate requests with sessions when they arrive from the client.
Implementing the Cart Feature
The Contents of the Cart.cs File in the SportsStore/Models Folder
The Cart class uses the CartLine class, defined in the same file, to represent a product selected by the customer and the quantity the user wants to buy. I defined methods to add an item to the cart, remove a previously added item from the cart, calculate the total cost of the items in the cart, and reset the cart by removing all the items.
The Cart class is relatively simple, but it has a range of important behaviors that must work properly. A poorly functioning cart would undermine the entire SportsStore application. I have broken down the features and tested them individually. I created a new unit test file called CartTests.cs in the SportsStore.Tests project to contain these tests.
Sometimes, as in this case, the code required to test the functionality of a class is longer and more complex than the class itself. Do not let that put you off writing the unit tests. Defects in simple classes can have huge impacts, especially ones that play such an important role as Cart does in the example application.
Defining Session State Extension Methods
The Contents of the SessionExtensions.cs File in the SportsStore/Infrastructure Folder
These methods serialize objects into the JavaScript Object Notation format, making it easy to store and retrieve Cart objects.
Completing the Razor Page
Handling Requests in the Cart.cshtml File in the SportsStore/Pages Folder
The Contents of the Cart.cshtml.cs File in the SportsStore/Pages Folder
The class associated with a Razor Page is known as its page model class, and it defines handler methods that are invoked for different types of HTTP requests, which update state before rendering the view. The page model class in Listing 8-25, which is named CartModel, defines an OnPost hander method, which is invoked to handle HTTP POST requests. It does this by retrieving a Product from the database, retrieving the user’s Cart from the session data, and updating its content using the Product. The modified Cart is stored, and the browser is redirected to the same Razor Page, which it will do using a GET request (which prevents reloading the browser from triggering a duplicate POST request).
The GET request is handled by the OnGet handler method, which sets the values of the ReturnUrl and Cart properties, after which the Razor content section of the page is rendered. The expressions in the HTML content are evaluated using the CartModel as the view model object, which means that the values assigned to the ReturnUrl and Cart properties can be accessed within the expressions. The content generated by the Razor Page details the products added to the user’s cart and provides a button to navigate back to the point where the product was added to the cart.
The handler methods use parameter names that match the input elements in the HTML forms produced by the ProductSummary.cshtml view. This allows ASP.NET Core to associate incoming form POST variables with those parameters, meaning I do not need to process the form directly. This is known as model binding and is a powerful tool for simplifying development, as I explain in detail in Chapter 28.
Razor Pages can feel a little odd when you first start using them, especially if you have previous experience with the MVC Framework features provided by ASP.NET Core. But Razor Pages are complementary to the MVC Framework, and I find myself using them alongside controllers and views because they are well-suited to self-contained features that don’t require the complexity of the MVC Framework. I describe Razor Pages in Chapter 23 and show their use alongside controllers throughout Part 3 and Part 4 of this book.
I am not going to describe these unit tests in detail because there is a simpler way to perform these tests, which I explain in the next chapter. The complexity in this test is mocking the ISession interface so that the page model class can use extension methods to retrieve a JSON representation of a Cart object. The ISession interface only stored byte arrays, and getting and deserializing a string is performed by extension methods. Once the mock objects are defined, they can be wrapped in context objects and used to configure an instance of the page model class, which can be subjected to tests.
Patience and a little experimentation are required to write effective unit tests, especially when the feature you are testing operates on the context objects that ASP.NET Core provides.
Summary
In this chapter, I started to flesh out the customer-facing parts of the SportsStore app. I provided the means by which the user can navigate by category and put the basic building blocks in place for adding items to a shopping cart. I have more work to do, and I continue the development of the application in the next chapter.