9 chap07
Jason Zhu edited this page 2021-09-14 23:48:15 +10:00
This file contains ambiguous Unicode characters!

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

  1. Create a new VS solutioni called SportsStore
  2. Add three projects:
    1. SportsStore.Domain (Class Library): Holds
      1. Domain entities and logic;
      2. Persistency access setup using repository created with EF
    2. SportsStore.WebUI (ASP.NET MVC Web Application): Holds
      1. controllers and views
      2. Act as UI for SportsStore app
    3. SportsStore.UnitTest (Unit Test Project): Holds
      1. 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: NONE
  • SportsStore.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:

  1. Add /Infrastructure directory under SportsStore.WebUI
  2. Add NinjectDependencyResolver.cs as in Listing 7-1
  3. 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:

  1. Create Product entity Product.cs in /Entities in SportsStore.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:

  1. Create interface file IProductsRepository.cs in /Abstract of SportsStore.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 of Product 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:

  1. Mock implementation & bind to interface in AddBindings method of NinjectDependencyResolver 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.

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:

  1. Create Empty MVC5 Controller ProductController.cs
  2. Add conostructor that declears dependency on IProductRepository interface (Listing 7-6)
  3. 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 method
  • repository.Products is a List of Product objects that passed to View method. It provides framework with data which populate Model object in .cshtml

7.3.2 Adding the Layout, View Start File and View

Objective: Add default view for List action method.

Steps:

  1. Right-click the controller action method and Add a view. Select Product as Model class
    1. If "Use a layout page" option is selected, then List.cshtml will be created, along with _ViewStart.cshtml and Shared/_Layout.cshtml
  2. 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 is IEnumerable<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:

  1. Edit RegisterRoutes method in App_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:

  1. Install EF packages to projects
  2. Create a context class. It assoaciate model with db (Listing 7-12)
  3. Add database connection in Web.config File of SportsStore.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 inherit System.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 in Products table

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:

  1. Add a class EFProductRepository.cs in /Concrete folder of SportsStore.Domain project. The repository use EFDbContext
  2. Edit Ninject binding and replace mock repository with real one
  3. (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 implements IProductRepository interface and use instance of EFDbContext (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 for IProductRepository 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:

  1. Add a parameter to List action method
  2. (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 page
  • List action method is provided an optional parameter (default value of page is 1)
  • 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

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:

  1. Create a view model class PagingInfo to Models folder in SportsStore.WebUI project
    1. 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

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:

  1. Create helper method PageLinks.cs in /HtmlHelper folder under SportsStore.WebUI project (Listing 7-18)
  2. Create UT to test PageLinks.cs
  3. 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.
  • For Razor MVC project, there are 2 Web.config file, main one in root of project, view-specific one in Views 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:

  1. Create class ProductsListViewModel.cs to /Models folder of SportsStore.WebUI project (Listing 7-20)
  2. Update List action method in controller to use ProductsListViewModel class to provide view with details of the products to display page and details of pagination (Listing 7-21)
  3. Update List.cshtml to use list of Product 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)

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

Steps:

  1. Add route to RegisterRoutes method in RouteConfig.cs file in App_Start folder of SportsStore.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.

Fig7-16 Design goal for the SportsStore app

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 for Product 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:

  1. use link to apply bootstrap CSS to _Layout.cshmtl (Listing 7-26) to decorate the layout
  2. 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:

  1. Right-click /Views/Shared folder in SportsStore.WebUI project and add partial view, and set to Product domain model class
  2. Edit created ProductSummary.cshtml file (Listing 7-28)
  3. 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)