Table of Contents
- Chapter 7: SportsStore: A Real Application
- 7.1 Getting Started
- 7.1.1 Creating the Visual Studio Solution and Projects
- 7.1.2 Installing the Tool Packages
- 7.1.3 Adding References Between Projects
- 7.1.4 Setting Up the DI Container
- 7.1.5 Running the Application
- 7.2 Starting the Domain Model
- 7.3 Displaying a List of Products
- 7.3.1 Adding a Controller
- 7.3.2 Adding the Layout, View Start File and View
- 7.3.3 Setting the Default Route
- 7.3.4 Running the Application
- 7.4 Preparing a Database
- 7.4.1 Creating the Database
- 7.4.2 Defining the Database Schema
- 7.4.3 Adding Data to the Database
- 7.4.4 Creating the Entity Framework Context
- 7.4.5 Creating the Product Repository
- 7.5 Adding Pagination
- 7.5.1 Displaying Page Links
- 7.5.1.1 Adding the View Model
- 7.5.1.2 Adding the HTML Helper Method
- 7.5.1.3 Adding the View Model Data
- 7.5.1.4 Displaying the Page Links
- 7.5.2 Improving the URLs
- 7.6 Styling the Content
This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.
Chapter 7: SportsStore: A Real Application
Objective of chap 07, 08, 09, 10, 11, 12:
- Create an online product cataglog that consumers can browse by category and page
- Create a shopping cart where users can add and remove products
- Create a checkout where customoers can enter their shipping details
- Create an administrative area that includes CRUD operations for managing the catalog
- Protect administrator site with loggin feature
Highlight of developing this app:
- Going slow as building up levels of infrastructure
7.1 Getting Started
7.1.1 Creating the Visual Studio Solution and Projects
- Create a new VS solutioni called
SportsStore
- Add three projects:
SportsStore.Domain
(Class Library): Holds- Domain entities and logic;
- Persistency access setup using repository created with EF
SportsStore.WebUI
(ASP.NET MVC Web Application): Holds- controllers and views
- Act as UI for SportsStore app
SportsStore.UnitTest
(Unit Test Project): Holds- Unit tests for other 2 projects
7.1.2 Installing the Tool Packages
Install external packages: Ninject
and Moq
How to install are omitted. Check book of details.
7.1.3 Adding References Between Projects
Dependences among 3 projects are shown below:
SportsStore.Domain
dependencies: NONESportsStore.WebUI
dependencies:SportsStore.Domain
SportsStore.UnitTests
dependences:SportsStore.Domain
&SportsStore.WebUI
Dependencies are added via:
Assemblies -> Framework
Assemblies -> Extensions/Solution
7.1.4 Setting Up the DI Container
Objective: use Ninject to create a custom dependencey resolver for MVC to use to instantiate objects across app.
Steps:
- Add
/Infrastructure
directory underSportsStore.WebUI
- Add
NinjectDependencyResolver.cs
as in Listing 7-1 - Add reference of
NinjectDependencyResolver.cs
in MVC dependency injection mechanism (i.e.App_Start/NinjectWebCommon.cs
file), as shown in Listing 7-2
Listing 7-1 NinjectDependencyResolver.cs
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using Ninject;
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
private IKernel kernel;
public NinjectDependencyResolver(IKernel kernelParam) {
kernel = kernelParam;
AddBindings();
}
public object GetService(Type serviceType) {
return kernel.TryGet(serviceType);
}
public IEnumerable<object> GetServices(Type serviceType) {
return kernel.GetAll(serviceType);
}
private void AddBindings() {
// put bindings here
}
}
}
private static void RegisterServices(IKernel kernel) {
System.Web.Mvc.DependencyResolver.SetResolver(new
SportsStore.WebUI.Infrastructure.NinjectDependencyResolver(kernel));
}
7.1.5 Running the Application
Start Debugging SportsStore.WebUI
display error page as there is no Controller associated with the URL (http://localhost:xxxx/)
7.2 Starting the Domain Model
All MVC Framework projects start with domain model, as everything in an MVC Framework application revolves around it.
Objective: create a domain model "Product"
Steps:
- Create Product entity
Product.cs
in/Entities
inSportsStore.Domain
project
Listing 7-3. The Contents of the Product.cs File
namespace SportsStore.Domain.Entities {
public class Product {
public int ProductID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
}
Notes: separating domain from MVC is not necessary in every projects, but it's useful in large and complex projects.
7.2.1 Creating an Abstract Repository
Objective: Use repository pattern to separate data model entities from storage and retrieval logic
Steps:
- Create interface file
IProductsRepository.cs
in/Abstract
ofSportsStore.Domain
project
Listing 7-4. The Contents of the IProductRepository.cs File
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.Domain.Abstract {
public interface IProductRepository {
IEnumerable<Product> Products { get; }
}
}
where
IEnumerable<T>
is used to allow caller to obtain a sequence ofProduct
objects, w/o saying how or hwere data is stored
7.2.2 Makinig a Mock Repository
Objective: create a mock implementation of the IProductRepository
interface (real implementation is repository class to store/retrieve data to/from db)
Steps:
- Mock implementation & bind to interface in
AddBindings
method ofNinjectDependencyResolver
class in WebUI project
Listing 7-5. Adding the Mock IProductRepository Implementation in the NinjectDependencyResolver.cs File
...
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
...
private void AddBindings() {
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new List<Product> {
new Product { Name = "Football", Price = 25 },
new Product { Name = "Surf board", Price = 179 },
new Product { Name = "Running shoes", Price = 95 }
});
kernel.Bind<IProductRepository>().ToConstant(mock.Object);
}
}
}
where
.ToConstant()
is used to set Ninject scope (Refer to Table 6-3).- Rather than create a new instance of implementation object each time. Ninject will always satisfy requests for the
IProductRepository
interface with same mock object.
- Rather than create a new instance of implementation object each time. Ninject will always satisfy requests for the
7.3 Displaying a List of Products
Objective:
- using MVC;
- add model and repository features
- Create a controller and action method to display details of the products in repository
7.3.1 Adding a Controller
Steps:
- Create Empty MVC5 Controller
ProductController.cs
- Add conostructor that declears dependency on
IProductRepository
interface (Listing 7-6) - Add action method
List
in controller
Listing 7-6. The Initial Contents of the Product Controller.cs File
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
}
}
Listing 7-7 Adding an Action Method to the ProductController.cs File
...
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
...
public ViewResult List() {
return View(repository.Products);
}
}
}
where
View(...)
tells framework to render the default view for the action methodrepository.Products
is aList
ofProduct
objects that passed toView
method. It provides framework with data which populateModel
object in.cshtml
7.3.2 Adding the Layout, View Start File and View
Objective: Add default view for List
action method.
Steps:
- Right-click the controller action method and Add a view. Select
Product
as Model class- If "Use a layout page" option is selected, then
List.cshtml
will be created, along with_ViewStart.cshtml
andShared/_Layout.cshtml
- If "Use a layout page" option is selected, then
Shared/_Layout.cshtml
contain template but we don't need it. Hence edit it as follow
Listing 7-8. Editing the _Layout.cshtml File
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>
7.3.2.1 Rendering the View Data
Objective: modify view for rendering
- In 7.3.2, although we set model type of view to be
Product
, the real type passed in controller action method isIEnumerable<Product>
(List of products). - Hence, need change
@model
@using SportsStore.Domain.Entities
@model IEnumerable<Product>
@{
ViewBag.Title = "Products";
}
@foreach (var p in Model) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
7.3.3 Setting the Default Route
Objective: set MVC Framework that it will send requests that arrive for root URL to List
action method in ProductController
class
Steps:
- Edit
RegisterRoutes
method inApp_Start/RouteConfig.cs
Listing 7-10. Adding the Default Route in the RouteConfig.cs File
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace SportsStore.WebUI {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Product", action = "List",
id = UrlParameter.Optional }
);
}
}
}
where
- We use convention-based route mapping
7.3.4 Running the Application
Refer detail to book
7.4 Preparing a Database
Omit, refer detail to book
7.4.1 Creating the Database
Omit, refer detail book
7.4.2 Defining the Database Schema
Omit, refer detail book
7.4.3 Adding Data to the Database
Omit, refer detail book
7.4.4 Creating the Entity Framework Context
Objective: Create EF Context to connect with established db
Steps:
- Install EF packages to projects
- Create a
context
class. It assoaciate model with db (Listing 7-12) - Add database connection in
Web.config
File ofSportsStore.WebUI
(Listing 7-13)
Install-Package EntityFramework -projectname SportsStore.Domain
Install-Package EntityFramework -projectname SportsStore.WebUI
Listing 7-12. The Content of the EFDbContext.cs File
using SportsStore.Domain.Entities;
using System.Data.Entity;
namespace SportsStore.Domain.Concrete {
public class EFDbContext : DbContext {
public DbSet<Product> Products { get; set; }
}
}
where
EFDbContext
inheritSystem.Data.Entity.DbContext
. Hence it automatically defines a property for each table in db.- Name of property specifies the table (i.e.
Products
) - Type parameter of
DbSet
(i.e.<Product>
) specifies the model type that EF should use to represent rows in the table - e.g. EF should use
Product
model type to represent rows inProducts
table
- Name of property specifies the table (i.e.
Listing 7-13. Adding a Database Connection in the Web.config File
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<!-- Add EFDbContext -->
<add name="EFDbContext" connectionString="Data Source=(localdb)\v11.0;Initial
Catalog=SportsStore;Integrated Security=True"
providerName="System.Data.SqlClient"/>
</connectionStrings>
<appSettings>
<add key="webpages:Version" value="3.0.0.0" />
<add key="webpages:Enabled" value="false" />
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5.1" />
<httpRuntime targetFramework="4.5.1" />
</system.web>
</configuration>
where:
- The added
<connectionString>
tell EF how to connect db,
7.4.5 Creating the Product Repository
Objective: Add a concrete product repository to save/retrieve data to/from db
Steps:
- Add a class
EFProductRepository.cs
in/Concrete
folder ofSportsStore.Domain
project. The repository useEFDbContext
- Edit Ninject binding and replace mock repository with real one
- (optional) run application and check rendered view for listing products
Listing 7-14. The Contents of the EFProductRepostory.cs File
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using System.Collections.Generic;
namespace SportsStore.Domain.Concrete {
public class EFProductRepository : IProductRepository {
private EFDbContext context = new EFDbContext();
public IEnumerable<Product> Products {
get { return context.Products; }
}
}
}
where:
EFProductRepository
is a repository class. It implementsIProductRepository
interface and use instance ofEFDbContext
(we defined) to retrieve data from/to db using EF.
Listing 7-15. Adding the Real Repository Binding in the NinjectDependencyResolver.cs File
...
namespace SportsStore.WebUI.Infrastructure {
public class NinjectDependencyResolver : IDependencyResolver {
...
private void AddBindings() {
kernel.Bind<IProductRepository>().To<EFProductRepository>();
}
}
}
where
- The new binding tells Ninject to create instnace of
EFProductRepository
class to service requests forIProductRepository
interface.
7.5 Adding Pagination
Objective: add support for pagination so the view display a fixed number of products on a page, and user can move from one page to another to view overall catalog
Steps:
- Add a parameter to
List
action method - (optional) create a unit test to verify the pagination
Listing 7-16. Adding Pagination Support to the List Action Method in the ProductController.cs File
...
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public int PageSize = 4;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List(int page = 1) {
return View(repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize));
}
}
}
where:
PageSize
is created to define number of product in one pageList
action method is provided an optional parameter (default value ofpage
is1
)List
use LINQ to perform read
UNIT TEST: PAGINATION in UnitTest1.cs
of SportsStore.UnitTests
project
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using SportsStore.Domain.Abstract;
using SportsStore.Domain.Entities;
using SportsStore.WebUI.Controllers;
namespace SportsStore.UnitTests {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void Can_Paginate() {
// Arrange
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// Act
IEnumerable<Product> result =
(IEnumerable<Product>)controller.List(2).Model; // Use .Model to get data out of View
// Assert
Product[] prodArray = result.ToArray();
Assert.IsTrue(prodArray.Length == 2);
Assert.AreEqual(prodArray[0].Name, "P4");
Assert.AreEqual(prodArray[1].Name, "P5");
}
}
}
where:
- In the UT, we:
- Create a mock repository
- Inject the mock repository into constructor of
ProductController
class - Call
List
action method in controller class to request a specific page - Compare
- Note: To get data returned from a controller method (i.e. from returned
View(...)
). We call.Model
property.- i.e. we use
controller.List(2).Model
to get data returned from controller action method
- i.e. we use
7.5.1 Displaying Page Links
After running application, visit http://localhost:xxxx/?page=2 to navigate through catalog of products.
To let customer to navigate easily, we need to render some page links at the bottom of each list of products so customer can navigate btw pages.
7.5.1.1 Adding the View Model
Objective: create a view model to pass information to the view about number of pages available, the current page, and the total number of products in repository.
Steps:
- Create a view model class
PagingInfo
toModels
folder inSportsStore.WebUI
project- A view model is not part of domain model, it's a convenient class for passing data btw controller and view. Hence it's in
SportsStore.WebUI
project instead of domain project
- A view model is not part of domain model, it's a convenient class for passing data btw controller and view. Hence it's in
Listing 7-17. The Contents of the PagingInfo.cs File
using System;
namespace SportsStore.WebUI.Models {
public class PagingInfo {
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int CurrentPage { get; set; }
public int TotalPages {
get { return (int)Math.Ceiling((decimal)TotalItems / ItemsPerPage); }
}
}
}
7.5.1.2 Adding the HTML Helper Method
Objective: implement the helper method to create page link
Steps:
- Create helper method
PageLinks.cs
in/HtmlHelper
folder underSportsStore.WebUI
project (Listing 7-18) - Create UT to test
PageLinks.cs
- Add helper method namespace into view specific
Web.config
(Listing 7-19)
using System;
using System.Text;
using System.Web.Mvc;
using SportsStore.WebUI.Models;
namespace SportsStore.WebUI.HtmlHelpers {
public static class PagingHelpers {
public static MvcHtmlString PageLinks(this HtmlHelper html,
PagingInfo pagingInfo,
Func<int, string> pageUrl) {
StringBuilder result = new StringBuilder();
for (int i = 1; i <= pagingInfo.TotalPages; i++) {
TagBuilder tag = new TagBuilder("a");
tag.MergeAttribute("href", pageUrl(i));
tag.InnerHtml = i.ToString();
if (i == pagingInfo.CurrentPage) {
tag.AddCssClass("selected");
tag.AddCssClass("btn-primary");
}
tag.AddCssClass("btn btn-default");
result.Append(tag.ToString());
}
return MvcHtmlString.Create(result.ToString());
}
}
}
where:
Func
parameter accepts a delegate that used to generate the links to view other pages- Details of
MergeAttribute
,MvcHtmlString
etc are available later chapters (chap21 Creating Custom Helper Methods)
UNIT TEST: CREATING PAGE LINKS
...
namespace SportsStore.UnitTests {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void Can_Paginate() {
// ...statements removed for brevity...
}
[TestMethod]
public void Can_Generate_Page_Links() {
// Arrange - define an HTML helper - we need to do this
// in order to apply the extension method
HtmlHelper myHelper = null;
// Arrange - create PagingInfo data
PagingInfo pagingInfo = new PagingInfo {
CurrentPage = 2,
TotalItems = 28,
ItemsPerPage = 10
};
// Arrange - set up the delegate using a lambda expression
Func<int, string> pageUrlDelegate = i => "Page" + i;
// Act
MvcHtmlString result = myHelper.PageLinks(pagingInfo, pageUrlDelegate);
// Assert
Assert.AreEqual(@"<a class=""btn btn-default"" href=""Page1"">1</a>"
+ @"<a class=""btn btn-default btn-primary selected"" href=""Page2"">2</a>"
+ @"<a class=""btn btn-default"" href=""Page3"">3</a>",
result.ToString());
}
}
}
where:
- During test, it verify helper method output by using literal string values. C# can work with such strings as long as string is prefixed with
@"
and use 2 sets double quotes (""
) in place where one set double quote is used.
To use extension method, namespace of extension method must be in scope of where it's used.
- For normal C# code,
using
is used to bring extension method into scode - For Razor view:
- Either add configuration entry to view specific
Web.config
file. - Or add
@using
statement to view.
- Either add configuration entry to view specific
- For Razor MVC project, there are 2
Web.config
file, main one in root of project, view-specific one inViews
folder
Listing 7-19. Adding the HTML Helper Method Namespace to the Views/web.config File
...
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="SportsStore.WebUI" />
<add namespace="SportsStore.WebUI.HtmlHelpers"/>
</namespaces>
</pages>
</system.web.webPages.razor>
...
where:
- Every namespace referred by a Razor view needs to be explicitly declared in
web.config
or applied with@using
expression
7.5.1.3 Adding the View Model Data
Objective: provide an instance of PagingInfo
view model class to the view before use HTML helper method
- There are 2 methods:
- Method 1: use ViewBag
- Method 2: wrap everything in view model (We choose this)
Steps:
- Create class
ProductsListViewModel.cs
to/Models
folder ofSportsStore.WebUI
project (Listing 7-20) - Update
List
action method in controller to useProductsListViewModel
class to provide view with details of the products to display page and details of pagination (Listing 7-21) - Update
List.cshtml
to use list ofProduct
objects (Listing 7-22)
using System.Collections.Generic;
using SportsStore.Domain.Entities;
namespace SportsStore.WebUI.Models {
public class ProductsListViewModel {
public IEnumerable<Product> Products { get; set; }
public PagingInfo PagingInfo { get; set; }
}
}
Listing 7-21. Updating the List Method in the ProductController.cs File
...
namespace SportsStore.WebUI.Controllers {
public class ProductController : Controller {
private IProductRepository repository;
public int PageSize = 4;
public ProductController(IProductRepository productRepository) {
this.repository = productRepository;
}
public ViewResult List(int page = 1) {
ProductsListViewModel model = new ProductsListViewModel {
Products = repository.Products
.OrderBy(p => p.ProductID)
.Skip((page - 1) * PageSize)
.Take(PageSize),
PagingInfo = new PagingInfo {
CurrentPage = page,
ItemsPerPage = PageSize,
TotalItems = repository.Products.Count()
}
};
return View(model);
}
}
}
Listing 7-22. Updating the List.cshtml File
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Products";
}
@foreach (var p in Model.Products) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
where:
@model
directive is changed to Razor that this view will work with different data types.
UNIT TEST: PAGE MODEL VIEW DATA
...
[TestMethod]
public void Can_Send_Pagination_View_Model() {
// Arrange
Mock<IProductRepository> mock = new Mock<IProductRepository>();
mock.Setup(m => m.Products).Returns(new Product[] {
new Product {ProductID = 1, Name = "P1"},
new Product {ProductID = 2, Name = "P2"},
new Product {ProductID = 3, Name = "P3"},
new Product {ProductID = 4, Name = "P4"},
new Product {ProductID = 5, Name = "P5"}
});
// Arrange
ProductController controller = new ProductController(mock.Object);
controller.PageSize = 3;
// Act
ProductsListViewModel result = (ProductsListViewModel)controller.List(2).Model;
// Assert
PagingInfo pageInfo = result.PagingInfo;
Assert.AreEqual(pageInfo.CurrentPage, 2);
Assert.AreEqual(pageInfo.ItemsPerPage, 3);
Assert.AreEqual(pageInfo.TotalItems, 5);
Assert.AreEqual(pageInfo.TotalPages, 2);
}
...
Alsoo modifysearlier pagination UT Can_Paginate
method
detail omit, refer to text book (You can correct it yourself)
7.5.1.4 Displaying the Page Links
Objective: Let View List.csthml
to call HTML helper method
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Products";
}
@foreach (var p in Model.Products) {
<div>
<h3>@p.Name</h3>
@p.Description
<h4>@p.Price.ToString("c")</h4>
</div>
}
<!-- Call helper to get pagination -->
<div>
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x }))
</div>
7.5.2 Improving the URLs
Objective: creaate URLs that more appearling by creating scheme that follows pattern of composable URLs
- Composable URLs is one URL pattern that make sense to user
- i.e. we want to replace query string in URL with better expression (http://localhost/?page=2 => http://localhost/Page2)
Steps:
- Add route to
RegisterRoutes
method inRouteConfig.cs
file inApp_Start
folder ofSportsStore.WebUI
project.
Listing 7-24. Adding a New Route to the RouteConfig.cs File
...
namespace SportsStore.WebUI {
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// New added route, prior than default
routes.MapRoute(
name: null,
url: "Page{page}",
defaults: new { Controller = "Product", action = "List" }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Product", action = "List",
id = UrlParameter.Optional }
);
}
}
}
7.6 Styling the Content
Objective: Implement a classic two-column layout with a header.
7.6.1 Installing the Bootstrap Package
In Package Manager Console, install bootstrap package
Install-Package -version 3.0.0 bootstrap –projectname SportsStore.WebUI
7.6.2 Applying Bootstrap Styles to the Layout
Current condition:
- when scaffold
List.cshtml
view forProduct
controller, we check option to use empty layout. It makes VS created/Views/Shared/_Layout.cshtml
. - Using which Layout, is specified
/Views/_ViewStart.cshtml
(shown in Listing 7-25)
Listing 7-25. The Contents of the _ViewStart.cshtml
File
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
Objective: apply bootstrap stylesheet to layout of SportsStore.WebUI
layout
Steps:
- use
link
to apply bootstrap CSS to_Layout.cshmtl
(Listing 7-26) to decorate the layout - Apply bootstrap to style the List.cshtml File to rerender the body
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Add bootstrap stylesheet in _Layout -->
<link href="∼/Content/bootstrap.css" rel="stylesheet" />
<link href="∼/Content/bootstrap-theme.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<!-- Use bootstrap to style navbar and row panel -->
<div class="navbar navbar-inverse" role="navigation">
<a class="navbar-brand" href="#">SPORTS STORE</a>
</div>
<div class="row panel">
<div id="categories" class="col-xs-3">
Put something useful here later
</div>
<div class="col-xs-8">
<!-- render views here -->
@RenderBody()
</div>
</div>
</body>
</html>
Listing 7-27 Applying Bootstrap to Style and List.cshtml File
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Products";
}
@foreach (var p in Model.Products) {
<!-- Use bootstrap to style view-->
<div class="well">
<h3>
<strong>@p.Name</strong>
<span class="pull-right label label-primary">@p.Price.ToString("c")</span>
</h3>
<span class="lead"> @p.Description</span>
</div>
}
<div class="btn-group pull-right">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new { page = x }))
</div>
where:
- As bootstrap has been added in layout file, there is no need to link stylesheet again in each view file
Note: style each view directly is not recommanded. We can improve it by assigning non-Bootstrap classes to element based on their role in app and then use library like jQuery or LESS to map btw custom class and bootstrap ones. (This method is not utilized in this SportsStore projects)
7.6.3 Creating a Partial View
Partial View = a fragment of content that can be embeded into another view.
- Partial views are contained in individual files and are reusable acroos multiple views (partial views resides in
/Views/Shared
folder). It can help to reduce duplication.
Objective: Create partial view for sidebar
Steps:
- Right-click
/Views/Shared
folder inSportsStore.WebUI
project and add partial view, and set toProduct
domain model class - Edit created ProductSummary.cshtml file (Listing 7-28)
- Use partial view in
List.cshtml
file (Listing 7-29)
Listing 7-28. Adding Markup to the ProductSummary.cshtml File
@model SportsStore.Domain.Entities.Product
<!-- Partial view -->
<div class="well">
<h3>
<strong>@Model.Name</strong>
<span class="pull-right label label-primary">@Model.Price.ToString("c")</span>
</h3>
<span class="lead"> @Model.Description</span>
</div>
Listing 7-29. Using a Partial View in the List.cshtml File
@model SportsStore.WebUI.Models.ProductsListViewModel
@{
ViewBag.Title = "Products";
}
<!-- Use partial view -->
@foreach (var p in Model.Products) {
@Html.Partial("ProductSummary", p)
}
<div class="pager">
@Html.PageLinks(Model.PagingInfo, x => Url.Action("List", new {page = x}))
</div>
where:
- Partial view is called using
Html.Partial
helper method. Parameters used for calling are- name of the view (i.e.
"ProductSummary"
) - view model object (i.e.
p
)
- name of the view (i.e.