0% found this document useful (0 votes)
218 views

Dot Net Merged

This document provides an introduction to web programming with ASP.NET Core MVC. It outlines objectives to understand components of web applications and key ASP.NET Core concepts. It describes static and dynamic web pages, the MVC pattern, and differences between .NET Framework and .NET Core. It also summarizes Visual Studio and Visual Studio Code features for ASP.NET Core development.

Uploaded by

Cecil Joseph
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
218 views

Dot Net Merged

This document provides an introduction to web programming with ASP.NET Core MVC. It outlines objectives to understand components of web applications and key ASP.NET Core concepts. It describes static and dynamic web pages, the MVC pattern, and differences between .NET Framework and .NET Core. It also summarizes Visual Studio and Visual Studio Code features for ASP.NET Core development.

Uploaded by

Cecil Joseph
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 980

Chapter 1

An introduction
to web programming
with ASP.NET Core
MVC

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 1
Objectives (part 1)
Knowledge
1. Describe the components of a web app.
2. Describe the four components of a URL.
3. Distinguish between static and dynamic web pages, with the focus
on the web server, application server, and database server.
4. Distinguish between the Internet and an intranet.
5. Describe these terms: HTTP request, HTTP response, and round trip.
6. Describe the model, view, and controller of the MVC pattern.
7. Explain how using the MVC pattern can improve app development.
8. Describe three programming models that can be used for developing
ASP.NET apps.
9. Distinguish between .NET Framework and .NET Core.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 2
Objectives (part 2)
10. Describe how an ASP.NET Core app allows a developer to
configure the middleware components in the HTTP request and
response pipeline.
11. Define state and describe why it’s hard to track in a web app.
12. Distinguish between the Visual Studio IDE and the code editor
known as Visual Studio Code.
13. Describe how coding by convention works and how it can benefit
developers.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 3
The components of a web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 4
The components of an HTTP URL

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 5
How a web server processes a static web page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 6
A simple HTTP request
GET / HTTP/1.1
Host: www.example.com

A simple HTTP response


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 136
Server: Apache/2.2.3

<html>
<head>
<title>Example Web Page</title>
</head>
<body>
<p>This is a sample web page</p>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 7
Three protocols that web apps depend upon
 Hypertext Transfer Protocol (HTTP) is the protocol that web
browsers and web servers use to communicate. It sets the
specifications for HTTP requests and responses.
 Hypertext Transfer Protocol Secure (HTTPS) is an extension of
the Hypertext Transfer Protocol (HTTP). It is used for secure
communication over a network.
 Transmission Control Protocol/Internet Protocol (TCP/IP) is a
suite of protocols that let two computers communicate over a
network.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 8
How a web server processes a dynamic web page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 9
The MVC (Model-View-Controller) pattern

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 10
Components of the MVC pattern
 The model consists of the code that provides the data access and
business logic.
 The view consists of the code that generates the user interface
and presents it to the user.
 The controller consists of the code that receives requests from
users, gets the appropriate data and stores it in the model, and
passes the model to the appropriate view.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 11
Benefits of the MVC pattern
 Makes it easier to have different members of a team work on
different components.
 Makes it possible to automate testing of individual components.
 Makes it possible to swap out one component for another
component.
 Makes the app easier to maintain.

Drawbacks of the MVC pattern


 Requires more work to set up.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 12
ASP.NET Web Forms
 Released in 2002.
 Provides for RAD (Rapid Application Development) by letting
developers build web pages by working with a design surface in
a way that’s similar to Windows Forms.
 Has many problems including poor performance, inadequate
separation of concerns, lack of support for automated testing, and
limited control over the HTML/CSS/JavaScript that’s returned to
the browser.
 Uses the ASP.NET Framework, which is proprietary and only
runs on Windows.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 13
ASP.NET MVC
 Released in 2007.
 Uses the MVC pattern that’s used by many other web
development platforms.
 Fixes many of the perceived problems with web forms to provide
better performance, separation of concerns, support for
automated testing, and a high degree of control over the
HTML/CSS/JavaScript that’s returned to the browser.
 Uses the same proprietary, Windows-only ASP.NET Framework
as Web Forms.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 14
ASP.NET Core MVC
 Released in 2015.
 Uses a service to implement the MVC pattern that’s used by
many other web development platforms.
 Provides all of the functionality of ASP.NET MVC but with
better performance, more modularity, and cleaner code.
 Is built on the open-source ASP.NET Core platform that can run
on multiple platforms including Windows, macOS, and Linux.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 15
Some web components of .NET and .NET Core

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 16
A request that makes it through all middleware
in the pipeline

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 17
A request that’s short circuited
by a middleware component in the pipeline

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 18
Middleware can…
 Generate the content for a response
 Edit the content of a request
 Edit the content of a response
 Short circuit a request

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 19
Why state is difficult to track in a web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 20
State concepts
 State refers to the current status of the properties, variables, and
other data maintained by an app for a single user.
 HTTP is a stateless protocol. That means that it doesn’t keep
track of state between round trips. Once a browser makes a
request and receives a response, the app terminates and its state is
lost.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 21
Visual Studio with an ASP.NET Core MVC app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 22
Features of Visual Studio
 IntelliSense code completion makes it easy to enter code.
 Automatic compilation allows you to compile and run an app
with a single keystroke.
 Integrated debugger makes it easy to find and fix bugs.
 Runs on Windows and macOS.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 23
VS Code with an ASP.NET Core MVC app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 24
Features of Visual Studio Code
 IntelliSense code completion makes it easy to enter code.
 Automatic compilation allows you to compile and run an app
with a single keystroke.
 Integrated debugger makes it easy to find and fix bugs.
 Runs everywhere (Windows, macOS, and Linux).

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 25
Some of the folders and files for a web app
GuitarShop
/Controllers
/HomeController.cs
/ProductController.cs
/Models
/Product.cs
/Views
/Home
/About.cshthml
/Index.cshthml
/Product
/Detail.cshthml
/List.cshthml
/wwwroot
/css
custom.css
/images
/js
custom.js
/lib
/boostrap
/jquery
/Startup.cs

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 26
Some naming conventions
for an ASP.NET Core MVC app
 All controller classes should be stored in a folder named
Controllers or one of its subfolders.
 All model classes should be stored in a folder named Models or
one of its subfolders.
 All view files should be stored in a folder named Views or one of
its subfolders.
 All static files such as image files, CSS files, and JavaScript files
shold be stored in a folder named wwwroot or one of its
subfolders.
 All controller classes should have a suffix of “Controller”.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 27
The code for a model class named Product
namespace GuitarShop.Models
{
public class Product
{
public int ProductID { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Slug => Name.Replace(' ', '-');
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 28
The code for the ProductController class
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using GuitarShop.Models;

namespace GuitarShop.Controllers
{
public class ProductController : Controller
{
public IActionResult Detail(string id)
{
Product product = DB.GetProduct(id);
return View(product);
}

public IActionResult List()


{
List<Product> products = DB.GetProducts();
return View(products);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 29
The code for the Product/Detail.cshtml view
@model Product
@{
ViewBag.Title = "Product Detail";
}
<h1>Product Detail</h1>
<table class="table table-bordered table-striped">
<tr>
<td>ID</td><td>@Model.ProductID</td>
</tr>
<tr>
<td>Name</td><td>@Model.Name</td>
</tr>
<tr>
<td>Price</td><td>@Model.Price.ToString("C2")</td>
</tr>
</table>
<a asp-controller="Home" asp-action="Index"
class="btn btn-primary">Home</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 30
The view displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 31
The Startup.cs file
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace GuitarShop
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}

public void Configure(IApplicationBuilder app)


{
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 32
How request URLs map to controllers and actions
by default
Request URL Controller Action
http://localhost Home Index
http://localhost/Home Home Index
http://localhost/Home/About Home About
http://localhost/Product/List Product List
http://localhost/Product/Detail Product Detail

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C1, Slide 33
Chapter 2

How to develop
a single-page
web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 1
Objectives (part 1)
Applied
1. Given the specifications for a single-page MVC web app, write the
C# code for the model and controller classes and write the C# code
and HTML for the Razor view.
2. Given an ASP.NET Core MVC web app, run it on your own
computer.
Knowledge
1. Describe how to use Visual Studio to create an ASP.NET Core
project and add the folders and files necessary for an MVC web
app.
2. List the names of six folders that are included in an MVC web app
by convention.
3. Describe how a controller and its action methods work.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 2
Objectives (part 2)
4. Describe how you can use the ViewBag property to transfer data
from a controller to a view.
5. Distinguish between a Razor code block and a Razor expression.
6. In general terms, describe how to use the Startup.cs file to
configure the HTTP request and response pipeline for a simple
ASP.NET Core MVC web app.
7. Distinguish between a model class and a controller class.
8. Describe the purpose of a Razor view imports page.
9. Describe how you can use the @model directive and asp-for
tag helpers to create a strongly-typed view.
10. Describe how you can use the asp-controller and asp-action tag
helpers to specify the controller and action method for a form
or a link.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 3
Objectives (part 3)
11. Describe how the HttpGet and HttpPost attributes allow you to
code action methods so they can handle HTTP GET or POST
requests.
12. Describe the purpose of a CSS style sheet for an app.
13. Distinguish between a Razor layout and a Razor view.
14. Describe the purpose of a Razor view start.
15. In general terms, describe how ASP.NET Core MVC provides
for validating the data that’s entered by a user.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 4
The dialog for starting a new web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 5
How to start a new ASP.NET Core MVC web app
1. Start Visual Studio.
2. From the menu system, select FileNewProject.
3. Select the ASP.NET Core Web Application item and click the
Next button.
4. Enter a project name.
5. Specify the location (folder). To do that, you can click the
Browse button.
6. If necessary, edit the solution name and click the Create button.
7. Use the resulting dialog to select the Web Application (Model-
View-Controller) template or the Empty template.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 6
The dialog that displays the project templates

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 7
The templates presented in this book
Template Contains…
MVC Starting folders and files for an
ASP.NET Core MVC web app.
Empty Two starting files for an
ASP.NET Core app.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 8
Visual Studio with the folders for an MVC web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 9
How to delete files from the MVC template
1. Expand the Controllers folder and delete all files in that folder.
2. Expand the Models folder and delete all files in that folder.
3. Expand the Views folder and its subfolders and delete all files
in those folders, but don’t delete the folders.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 10
How to add folders to the Empty template
1. Add the Controllers, Models, and Views folders.
2. Within the Views folder, add the Home and Shared folders.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 11
The dialogs for adding a controller

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 12
How to add a file for a controller
1. Right-click the Controllers folder and select AddController.
2. In the Add Scaffold dialog, select “MVC Controller – Empty”
and click Add.
3. In the Add Empty MVC Controller dialog, name the controller
and click Add.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 13
The HomeController.cs file
using Microsoft.AspNetCore.Mvc;

namespace FutureValue.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
ViewBag.Name = "Mary";
ViewBag.FV = 99999.99;
return View();
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 14
The dialog for adding a Razor view

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 15
How to add a view to the Views/Home folder
1. In the Solution Explorer, right-click the Views/Home folder and
select AddView.
2. In the resulting dialog, enter Index as the name of the view.
3. If necessary, select the “Empty (without model)” template.
4. Deselect the “Use a layout page” checkbox.
5. Click the Add button.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 16
The Home/Index.cshtml view
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Home Page</title>
</head>
<body>
<h1>Future Value Calculator</h1>
<p>Customer Name: @ViewBag.Name</p>
<p>Future Value: @ViewBag.FV.ToString("C2")</p>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 17
The Startup.cs file after it has been edited (part 1)
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace FutureValue
{
public class Startup
{
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews();
}

// Use this method to configure the HTTP request pipeline.


public void Configure(IApplicationBuilder app,
IWebHostEnvironment env) {
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 18
The Startup.cs file after it has been edited (part 2)
app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 19
The Start button drop-down list in Visual Studio

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 20
The Future Value app in the Chrome browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 21
The Error List window in Visual Studio

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 22
The dialog for adding a class

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 23
How to add a file for a model class
1. In the Solution Explorer, right-click the Models folder and
select AddClass.
2. In the resulting dialog, enter the name of the class, and click the
Add button.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 24
The FutureValueModel class
namespace FutureValue.Models
{
public class FutureValueModel
{
public decimal MonthlyInvestment { get; set; }
public decimal YearlyInterestRate { get; set; }
public int Years { get; set; }
public decimal CalculateFutureValue() {
int months = Years * 12;
decimal monthlyInterestRate =
YearlyInterestRate / 12 / 100;
decimal futureValue = 0;
for (int i = 0; i < months; i++)
{
futureValue = (futureValue + MonthlyInvestment)
* (1 + monthlyInterestRate);
}
return futureValue;
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 25
The dialog for adding a Razor view imports page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 26
How to add a Razor view imports page
1. In the Solution Explorer, right-click the Views folder and select
AddNew Item.
2. In the resulting dialog, select the InstalledASP.NET
CoreWeb category, select the Razor View Imports item, and
click the Add button.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 27
The Views/_ViewImports.cshtml file
for the Future Value app
@using FutureValue.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

A Razor view imports page makes it easier


to work with…
 Model classes.
 Tag helpers.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 28
Common tag helpers for forms
Tag helper HTML tags
asp-for <label> <input>
asp-action <form> <a>
asp-controller <form> <a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 29
A strongly-typed Index view with tag helpers
(part 1)
@model FutureValueModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Future Value Calculator</title>
<style>
/* all of the CSS styles from figure 2-14 go here */
</style>
</head>
<body>
<h1>Future Value Calculator</h1>
<form asp-action="Index" method="post">
<div>
<label asp-for="MonthlyInvestment">
Monthly Investment:</label>
<input asp-for="MonthlyInvestment" />
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 30
A strongly-typed Index view with tag helpers
(part 2)
<div>
<label asp-for="YearlyInterestRate">
Yearly Interest Rate:</label>
<input asp-for="YearlyInterestRate" />
</div>
<div>
<label asp-for="Years">Number of Years:</label>
<input asp-for="Years" />
</div>
<div>
<label>Future Value:</label>
<input value="@ViewBag.FV.ToString("C2")" readonly>
</div>
<button type="submit">Calculate</button>
<a asp-action="Index">Clear</a>
</form>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 31
Attributes that indicate the HTTP verb
an action method handles
HttpGet
HttpPost

Methods for returning a view from a controller


View()
View(model)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 32
An overloaded Index() action method
using Microsoft.AspNetCore.Mvc;
using FutureValue.Models;

public class HomeController : Controller


{
[HttpGet]
public IActionResult Index()
{
ViewBag.FV = 0;
return View();
}

[HttpPost]
public IActionResult Index(FutureValueModel model)
{
ViewBag.FV = model.CalculateFutureValue();
return View(model);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 33
The Future Value app after a GET request

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 34
The Future Value app after a POST request

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 35
The dialog for adding a CSS style sheet

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 36
How to add a CSS style sheet
1. If the wwwroot/css folder doesn’t exist, create it.
2. Right-click the wwwroot/css folder and select AddNew Item.
3. Select the ASP.NET CoreWeb category, select the Style
Sheet item, enter a name for the CSS file, and click the Add
button.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 37
The custom.css file for the Future Value app
body {
padding: 1em;
font-family: Arial, Helvetica, sans-serif;
}

h1 {
margin-top: 0;
color: navy;
}

label {
display: inline-block;
width: 10em;
padding-right: 1em;
}

div {
margin-bottom: .5em;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 38
The dialog for adding a Razor layout, view start,
or view

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 39
How to add a Razor layout
1. Right-click the Views/Shared folder and select AddNew
Item.
2. Select the ASP.NET CoreWeb category, select the Razor
Layout item, and click the Add button.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 40
How to add a Razor view start
1. Right-click the Views folder (not the Views/Shared folder) and
select AddNew Item.
2. Select the ASP.NET CoreWeb category, select the Razor
View Start item, and click the Add button.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 41
How to add a Razor view
1. Right-click the folder for the view (Views/Home, for example)
and select AddView.
2. Use the dialog from figure 2-5 to specify the name for the view.
3. If you’re using a layout that has a view start, select the “Use a
layout page” item but don’t specify a name for the layout page.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 42
The Views/Shared/_Layout.cshtml file
<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link rel="stylesheet" href="~/css/custom.css" />
</head>
<body>
<div>
@RenderBody()
</div>
</body>
</html>

The Views/_ViewStart.cshtml file


@{
Layout = "_Layout";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 43
The Views/Home/Index.cshtml file (part 1)
@model FutureValueModel
@{
ViewBag.Title = "Future Value Calculator";
}
<h1>Future Value Calculator</h1>
<form asp-action="Index" method="post">
<div>
<label asp-for="MonthlyInvestment">
Monthly Investment:</label>
<input asp-for="MonthlyInvestment" />
</div>
<div>
<label asp-for="YearlyInterestRate">
Yearly Interest Rate:</label>
<input asp-for="YearlyInterestRate" />
</div>
<div>
<label asp-for="Years">Number of Years:</label>
<input asp-for="Years" />
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 44
The Views/Home/Index.cshtml file (part 2)
<div>
<label>Future Value:</label>
<label>@ViewBag.FV.ToString("c2")</label>
</div>
<button type="submit">Calculate</button>
<a asp-action="Index">Clear</a>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 45
How to import the DataAnnotations namespace
using System.ComponentModel.DataAnnotations;

Two common validation attributes


Required
Range(min, max)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 46
A model property with a validation attribute
[Required]
public decimal? MonthlyInvestment { get; set; }

The default error message if the property isn’t set


The field MonthlyInvestment is required.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 47
A model property with two validation attributes
[Required]
[Range(1, 500)]
public decimal? MonthlyInvestment { get; set; }

The default error message if the property


isn’t in a valid range
The field MonthlyInvestment must be between 1 and 500.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 48
A model property with user-friendly error
messages
[Required(ErrorMessage =
"Please enter a monthly investment amount.")]
[Range(1, 500, ErrorMessage =
"Monthly investment amount must be between 1 and 500.")]
public decimal? MonthlyInvestment { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 49
The model class with data validation attributes
(part 1)
using System.ComponentModel.DataAnnotations;

namespace FutureValue.Models
{
public class FutureValueModel
{
[Required(ErrorMessage =
"Please enter a monthly investment.")]
[Range(1, 500, ErrorMessage =
"Monthly investment amount must be between 1 and 500.")]
public decimal? MonthlyInvestment { get; set; }

[Required(ErrorMessage =
"Please enter a yearly interest rate.")]
[Range(0.1, 10.0, ErrorMessage =
"Yearly interest rate must be between 0.1 and 10.0.")]
public decimal? YearlyInterestRate { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 50
The model class (part 2)
[Required(ErrorMessage = "Please enter a number of years.")]
[Range(1, 50, ErrorMessage =
"Number of years must be between 1 and 50.")]
public int? Years { get; set; }

public decimal? CalculateFutureValue()


{
int? months = Years * 12;
decimal? monthlyInterestRate =
YearlyInterestRate / 12 / 100;
decimal? futureValue = 0;
for (int i = 0; i < months; i++)
{
futureValue = (futureValue + MonthlyInvestment) *
(1 + monthlyInterestRate);
}
return futureValue;
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 51
An action method that checks for invalid data
[HttpPost]
public IActionResult Index(FutureValueModel model)
{
if (ModelState.IsValid)
{
ViewBag.FV = model.CalculateFutureValue();
}
else
{
ViewBag.FV = 0;
}
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 52
A view that displays a summary
of validation messages
<form asp-action="Index" method="post">
<div asp-validation-summary="All"></div>
<div>
<label asp-for="MonthlyInvestment">
Monthly Investment:</label>
<input asp-for="MonthlyInvestment" />
</div>
<!-- rest of input form -->
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 53
The Future Value app with invalid data

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C2, Slide 54
Chapter 3

How to make
a web app responsive
with Bootstrap

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 1
Objectives (part 1)
Applied
1. Use any of the Bootstrap CSS classes and components presented in
this chapter in your web apps.
2. Use LibMan to add the NuGet packages for client-side libraries
such as Bootstrap and jQuery to a project.

Knowledge
1. Explain what responsive web design is and how it’s implemented
using Bootstrap.
2. Describe how the Bootstrap grid system works.
3. Describe how to use the Bootstrap CSS classes to format and align
labels, text boxes, and buttons within a form.
4. Describe how to use the Bootstrap CSS classes to format images,
HTML tables, and text.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 2
Objectives (part 2)
5. Describe how to use Bootstrap components such as jumbotrons,
button groups, icons, badges, button dropdowns, list groups, alerts,
breadcrumbs, navs, and navbars.
6. Describe how to use Bootstrap CSS classes to provide context.
7. Describe how to use Bootstrap CSS classes to adjust margins and
padding.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 3
The Future Value app in a desktop browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 4
The Future Value app in a tablet browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 5
The Future Value app in a phone browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 6
The dialog box for adding the jQuery library

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 7
The dialog box for adding the Bootstrap library

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 8
How to add the Bootstrap and jQuery libraries
1. Start Visual Studio and open a project. In the Solution Explorer,
expand the wwwroot/lib folder and delete any old Bootstrap or
jQuery libraries.
2. In the Solution Explorer, right-click on the project name and
select the AddClient-Side Library item.
3. In the dialog box that appears, type “jquery@”, select “3.3.1”
from the list that appears, and click the Install button.
4. Repeat steps 2 and 3 for the library named “twitter-
[email protected]”, but change the target location to
“www/lib/bootstrap”.
5. Repeat steps 2 and 3 for the library named “[email protected]”.
6. Repeat steps 2 and 3 for the library named “jquery-
[email protected]”.
7. Repeat steps 2 and 3 for the library named “jquery-validation-
[email protected]”.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 9
The libman.json file for the client-side libraries

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 10
A Razor layout that enables client-side libraries
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewData["Title"]</title>
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/custom.css" />

<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/popper.js/popper.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.min.js"></script>

<script src="~/lib/jquery-validate/
jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js"></script>
</head>
<body>
@RenderBody()
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 11
An extra directory that may be included
by the MVC template
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.min.css" />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 12
The URL for the Bootstrap documentation
https://getbootstrap.com/docs/

Valid Bootstrap class values


container
container-fluid
row
col
col-size-count
offset-size-count

Valid size values


lg
md
sm
(none)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 13
Two columns that are automatically sized
<div class="col">Column 1</div>
<div class="col">Column 2</div>

An element that spans four columns


on medium and large screens
<div class="col-md-4">This element spans four columns</div>

An element that spans 4 columns


on medium screens and 3 on large
<div class="col-md-4 col-lg-3">
This element spans three or four columns
</div>

An element that is moved one column to the right


on medium screens
<div class="col-md-4 offset-md-1">
This element is offset by one column
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 14
The HTML for a grid
<main class="container">
<div class="row">
<div class="col-12">Column 1</div>
<div class="col-12">Column 2</div>
</div>
<div class="row">
<div class="col-md-4">Column 1</div>
<div class="col-md-8">Column 2</div>
</div>
<div class="row">
<div class="col-md-4 col-sm-6">Column 1</div>
<div class="col-md-8 col-sm-6">Column 2</div>
</div>
</main>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 15
Custom CSS classes that override
the Bootstrap CSS classes
.row { /* custom row styles */
margin-bottom: 0.5rem;
}
.row div { /* custom column styles */
border: 1px solid black;
padding: 0.5rem;
background-color: aliceblue;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 16
The Bootstrap grid on a medium screen

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 17
The Bootstrap grid on a small screen

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 18
The Bootstrap grid on an extra small screen

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 19
Two Bootstrap CSS classes for forms
form-group
form-control

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 20
A form with two text boxes in vertical layout

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 21
The HTML for the form in vertical layout
<form asp-action="Login" method="post">
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" class="form-control" />
</div>
<div class="form-group">
<label for="pwd">Password:</label>
<input id="pwd" type="password" class="form-control" />
</div>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 22
A form with two text boxes in horizontal layout

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 23
The HTML for the form in horizontal layout
<form asp-action="Login" method="post">
<div class="form-group row">
<label for="email" class="col-sm-2">Email:</label>
<div class="col-sm-10">
<input type="email" id="email" class="form-control" />
</div>
</div>
<div class="form-group row">
<label for="pwd" class="col-sm-2">Password:</label>
<div class="col-sm-10">
<input type="password" id="pwd" class="form-control" />
</div>
</div>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 24
Bootstrap CSS classes for working with buttons
btn
btn-primary
btn-secondary
btn-outline-primary
btn-outline-secondary

Bootstrap CSS classes for working with images


img-fluid
rounded

A Bootstrap CSS class for creating a jumbotron


jumbotron

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 25
A jumbotron, an image, and two buttons

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 26
The HTML for the jumbotron, image, and buttons
<div class="container">
<header class="jumbotron">
<img id="logo" alt="Murach logo"
src="~/images/MurachLogo.jpg"
class="img-fluid rounded" />
</header>
<main>
<form asp-action="Index" method="post">
<span class="mr-2">I accept the terms for this site:
</span>
<button type="submit" class="btn btn-primary">
Yes
</button>
<button id="btnNo" class="btn btn-outline-secondary">
No
</button>
</form>
</main>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 27
Bootstrap CSS classes for working with margins
mt-size
mr-size
mb-size
ml-size
m-size

Bootstrap CSS classes for working with padding


pt-size
pr-size
pb-size
pl-size
p-size

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 28
Some elements that use margins and padding

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 29
The HTML for the elements
<div class="container">
<header class="jumbotron mt-2">
<img id="logo" alt="Murach logo"
src="~/images/MurachLogo.jpg"
class="img-fluid rounded" />
</header>
<main>
<form asp-action="Index" method="post">
<span class="mr-2">
I accept the terms for this site:
</span>
<button type="submit"
class="btn btn-primary p-3 mr-2">Yes
</button>
<button id="btnNo"
class="btn btn-outline-secondary p-3">No
</button>
</form>
</main>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 30
The code for the Index view (part 1)
@model FutureValueModel
@{
ViewBag.Title = "Future Value Calculator";
}
<div class="container">
<header class="jumbotron">
<img id="logo" alt="Murach logo"
src="~/images/MurachLogo.jpg"
class="img-responsive rounded" />
<h1 class="mt-3">@ViewBag.Title</h1>
</header>
<main>
<form asp-action="Index" method="post">
<div class="row form-group">
<label asp-for="MonthlyInvestment"
class="col-sm-3">Monthly Investment: </label>
<div class="col-sm-3">
<input asp-for="MonthlyInvestment"
class="form-control" />
</div>
<span asp-validation-for="MonthlyInvestment"
class="col text-danger"></span>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 31
The code for the Index view (part 2)
<div class="row form-group">
<label asp-for="YearlyInterestRate"
class="col-sm-3">Yearly Interest Rate:
</label>
<div class="col-sm-3">
<input asp-for="YearlyInterestRate"
class="form-control" />
</div>
<span asp-validation-for="YearlyInterestRate"
class="col text-danger"></span>
</div>
<div class="row form-group">
<label asp-for="Years" class="col-sm-3">
Number of Years: </label>
<div class="col-sm-3">
<input asp-for="Years" class="form-control" />
</div>
<span asp-validation-for="Years"
class="col text-danger"></span>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 32
The code for the Index view (part 3)
<div class="row form-group">
<label class="col-sm-3">Future Value:</label>
<div class="col-sm-3">
<input value="@ViewBag.FutureValue" readonly
class="form-control" />
</div>
</div>
<div class="row form-group">
<div class="col offset-sm-3">
<button type="submit" class="btn btn-primary">
Calculate</button>
<a asp-action="Index"
class="btn btn-outline-secondary">Clear</a>
</div>
</div>
</form>
</main>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 33
CSS classes for working with HTML tables
table
table-bordered
table-striped
table-hover
table-responsive
w-size

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 34
A table with default styling

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 35
The HTML for the table
<table class="table">
<thead>
<tr>
<th>Department</th>
<th>Phone Number</th>
<th>Extension</th></tr>
</thead>
<tbody>
<tr>
<td>General</td>
<td>555-555-5555</td>
<td>1</td></tr>
<tr>
<td>Customer Service</td>
<td>555-555-5556</td>
<td>2</td></tr>
<tr>
<td>Billing and Accounts</td>
<td>555-555-5557</td>
<td>3</td></tr>
</tbody>
</table>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 36
A table with alternating stripes and borders

The HTML for the table


<table class="table table-striped table-bordered table-hover">...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 37
Common CSS classes for text
text-left
text-right
text-center
text-lowercase
text-uppercase
text-capitalize

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 38
Some examples of the text CSS classes

The HTML for the text


<p class="text-right">
This text is <span class="text-uppercase">
right-aligned</span>.
</p>
<p class="text-center">
<span class="text-capitalize">This text is centered.
</span>
</p>
<p class="text-left">
This text is <span class="text-lowercase">
LEFT-ALIGNED</span>.
</p>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 39
The context classes available to most elements
Class Default color
primary Dark blue
secondary Gray
success Green
info Light blue
warning Orange
danger Red
light White
dark Gray

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 40
Some of the context classes applied to buttons

The HTML for the buttons


<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-info">Info</button>
<button class="btn btn-warning">Warning</button>
<button class="btn btn-danger">Danger</button>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 41
The success class applied to the text
of an element

The HTML for the element


<p class="text-success">
Congratulations! You are now registered.
</p>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 42
The warning class applied to the background
of an element

The HTML for the element


<p class="bg-warning p-2 rounded">
Warning! Some required fields are empty.
</p>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 43
Common CSS classes for creating button groups
btn-group
btn-toolbar
btn-group-size
btn-group-vertical

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 44
A basic button group

The HTML for the button group


<div class="btn-group" role="group"
aria-label="Button group">
<a href="/" class="btn btn-outline-primary">Home</a>
<a href="/cart" class="btn btn-outline-primary">
Cart</a>
<a href="/products" class="btn btn-outline-primary">
Products</a>
<a href="/contact-us" class="btn btn-outline-primary">
Contact Us</a>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 45
A toolbar with two button groups

The HTML for the button groups


<div class="btn-toolbar" role="toolbar"
aria-label="Toolbar with groups">
<div class="btn-group mr-2" role="group"
aria-label="First group">
<a href="/" class="btn btn-outline-primary">Home</a>
<a href="/Cart" class="btn btn-outline-primary">Cart</a>
</div>
<div class="btn-group" role="group" aria-label="Second group">
<a href="/products" class="btn btn-outline-primary">
Products</a>
<a href="/contact-us" class="btn btn-outline-primary">
Contact Us</a>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 46
The URL for the Font Awesome website
https://fontawesome.com/

A typical <link> element that enables


Font Awesome icons
<link rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.8.1/css/all.css"
integrity="sha-long-hash_code" crossorigin="anonymous">

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 47
A button group that includes icons for its buttons

The HTML for the button group


<div class="btn-group" role="group"
aria-label="Button group">
<a href="/" class="btn btn-outline-primary">
<span class="fas fa-home"></span>&nbsp;Home&nbsp;
</a>
<a href="/cart" class="btn btn-outline-primary">
<span class="fas fa-shopping-cart"></span>
&nbsp;Cart&nbsp;
</a>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 48
A button with a badge

The HTML for the button


<a href="/cart" class="btn btn-outline-primary">
&nbsp;Cart&nbsp;
<span class="badge badge-primary">2</span>
</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 49
A button with an icon and a badge

The HTML for the button


<a href="/cart" class="btn btn-outline-primary">
<span class="fas fa-shopping-cart">
</span>&nbsp;Cart&nbsp;
<span class="badge badge-primary">2</span>
</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 50
CSS classes for creating button dropdowns
dropdown
dropdown-toggle
dropdown-menu
dropdown-item
dropup

An HTML5 data attribute


for creating button dropdowns
data-toggle

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 51
A button dropdown

The HTML for the button dropdown


<div class="dropdown">
<button type="button" class="btn btn-primary dropdown-toggle"
id="productsDropdown" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">Products
</button>
<div class="dropdown-menu" aria-labelledby="productsDropdown">
<a class="dropdown-item"
href="/product/list/guitars">Guitars</a>
<a class="dropdown-item"
href="/product/list/drums">Drums</a>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 52
Common CSS classes for creating list groups
list-group
list-group-item
active
disabled

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 53
A basic list group

The HTML for the list group


<ul class="list-group">
<li class="list-group-item">Guitars</li>
<li class="list-group-item">Basses</li>
<li class="list-group-item">Drums</li>
</ul>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 54
Another basic list group with an active item

The HTML for the list group


<div class="list-group">
<a href="/guitars" class="list-group-item active">
Guitars</a>
<a href="/basses" class="list-group-item">Basses</a>
<a href="/drums" class="list-group-item">Drums</a>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 55
Common CSS classes for creating alerts
alert
alert-context
alert-dismissible
alert-link
close

An HTML5 data attribute for creating alerts


data-dismiss

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 56
A dismissible alert with a link

The HTML for the alert


<div class="alert alert-success alert-dismissible">
<button class="close" data-dismiss="alert">
&times;
</button>
Success! <a href="#" class="alert-link">
Learn more</a>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 57
Common CSS classes for creating breadcrumbs
breadcrumb
breadcrumb-item
active

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 58
A breadcrumb with three segments

The HTML for the breadcrumb


<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/">Home</a>
</li>
<li class="breadcrumb-item">
<a href="/Products">Products</a>
</li>
<li class="breadcrumb-item active"
aria-current="page">Guitars
</li>
</ol>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 59
Common CSS classes for creating navs
nav
nav-tabs
nav-pills
nav-item
nav-link
active

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 60
Nav links styled as tabs

The HTML for the nav links


<nav class="nav nav-tabs">
<a class="nav-item nav-link active" href="/">Home</a>
<a class="nav-item nav-link" href="/products">
Products</a>
<a class="nav-item nav-link" href="/cart">Cart</a>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 61
The same nav links styled as pills

The HTML for the nav links


<nav class="nav nav-pills">
<a class="nav-item nav-link active" href="/">Home</a>
<a class="nav-item nav-link" href="/products">
Products</a>
<a class="nav-item nav-link" href="/cart">Cart</a>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 62
A more verbose way of coding the same nav links
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link active" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/products">Products</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/cart">Cart</a>
</li>
</ul>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 63
Common CSS classes for creating navbars
navbar
navbar-expand-size
navbar-light-or-dark
navbar-brand
navbar-toggler
navbar-collapse
collapse
navbar-nav
navbar-alignment

HTML5 data attributes for creating navbars


data-toggle
data-target

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 64
A navbar expanded on a wide screen

A navbar expanded on a small screen

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 65
The HTML for the navbar (part 1)
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
<a class="navbar-brand" href="/">My Guitar Shop</a>
<button class="navbar-toggler" type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<nav class="collapse navbar-collapse"
id="navbarSupportedContent">
<div class="navbar-nav mr-auto">
<a class="nav-item nav-link active" href="/">Home</a>
<a class="nav-item nav-link"
href="/products">Products</a>
<a class="nav-item nav-link" href="/about">About</a>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 66
The HTML for the navbar (part 2)
<div class="navbar-nav navbar-right">
<a class="nav-item nav-link" href="/cart">
<span class="fas fa-shopping-cart"></span>
&nbsp;Cart&nbsp;
<span class="badge badge-primary">2</span>
</a>
</div>
</nav>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 67
More CSS classes for positioning navbars
fixed-top
fixed-bottom

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 68
A navbar that’s fixed at the top of the screen

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 69
The HTML that displays the navbar
<body>
<nav class="navbar navbar-expand-md navbar-dark
bg-primary fixed-top">
<!-- navbar items go here -->
</nav>
<div class="container">
<!-- container items go here -->
</div>
</body>

The CSS that sets the top margin


body {
margin-top: 70px;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 70
A navbar that’s fixed at the bottom of the screen

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C3, Slide 71
Chapter 4

How to develop
a data-driven
MVC web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 1
Objectives (part 1)
Applied
1. Given the specifications for a multi-page web app that stores data in
a database, code and test the app.
2. Use Visual Studio to add the NuGet packages for EF Core and its
tools to a project.

Knowledge
1. Describe how you can code a primary key for an entity by
convention and describe when the value for that primary key will be
automatically generated.
2. Describe how a DB context class maps related entity classes to a
database and seeds the database with initial data.
3. Name the file that’s typically used to store a connection string for a
database.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 2
Objectives (part 2)
4. Describe how a Startup.cs file can read a connection string from a
file.
5. Describe how ASP.NET Core uses dependency injection to pass
DbContext objects to the controllers that need them.
6. Describe how to use the Package Manager Console to create
migration files and use them to create and update the database of a
web app.
7. Describe how to use LINQ to query the data that’s available from
the DbSet properties of a DbContext object.
8. Describe how to use the methods of the DbSet and DbContext
classes to add, update, or delete entities in the database.
9. Describe how you can code a foreign key for an entity by
convention.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 3
Objectives (part 3)
10. Describe how to use the Startup.cs file to modify the HTTP
request and response pipeline to provide for user-friendly URLs.
11. Describe how slugs can make URLs more user friendly.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 4
The Movie List page of the Movie List app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 5
The Add Movie page of the Movie List app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 6
The Edit Movie page of the Movie List app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 7
The Confirm Deletion page of the Movie List app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 8
The Solution Explorer for the Movie List app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 9
Folders in the Movie List app
 Models
 Views
 Controllers
 Migrations

Files in the Movie List app


 appsettings.json
 Startup.cs

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 10
How to open the NuGet Package Manager
 Select ToolsNuget Package ManagerManage NuGet
Packages for Solution.

The NuGet Package Manager

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 11
How to install the NuGet packages for EF Core
1. Click the Browse link in the upper left of the window.
2. Type “Microsoft.EntityFrameworkCore.SqlServer” in the search
box.
3. Click on the appropriate package from the list that appears in the
left-hand panel.
4. In the right-hand panel, check the project name, select the version
that matches the version of .NET Core you’re running, and click
Install.
5. Review the Preview Changes dialog that comes up and click OK.
6. Review the License Acceptance dialog that comes up and click I
Accept.
7. Type “Microsoft.EntityFrameworkCore.Tools” in the search box.
8. Repeat steps 3 through 6.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 12
Three classes provided by EF Core
DbContext
DbContextOptions
DbSet<Entity>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 13
A MovieContext class that inherits
the DbContext class
using Microsoft.EntityFrameworkCore;

namespace MovieList.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{ }

public DbSet<Movie> Movies { get; set; }


}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 14
A Movie class with a property whose value
is generated by the database
using System.ComponentModel.DataAnnotations;

namespace MovieList.Models
{
public class Movie
{
// EF Core will configure the database to generate this value
public int MovieId { get; set; }

[Required(ErrorMessage = "Please enter a name.")]


public string Name { get; set; }

[Required(ErrorMessage = "Please enter a year.")]


[Range(1889, 2999, ErrorMessage = "Year must be after 1889.")]
public int? Year { get; set; }

[Required(ErrorMessage = "Please enter a rating.")]


[Range(1, 5, ErrorMessage =
"Rating must be between 1 and 5.")]
public int? Rating { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 15
One method of the DbContext class
OnModelCreating(mb)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 16
A MovieContext class that seeds initial Movie data
(part 1)
namespace MovieList.Models
{
public class MovieContext : DbContext
{
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{ }

public DbSet<Movie> Movies { get; set; }

protected override void OnModelCreating(


ModelBuilder modelBuilder)
{
modelBuilder.Entity<Movie>().HasData(
new Movie {
MovieId = 1,
Name = "Casablanca",
Year = 1942,
Rating = 5
},

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 17
A MovieContext class that seeds initial Movie data
(part 2)
new Movie {
MovieId = 2,
Name = "Wonder Woman",
Year = 2017,
Rating = 3
},
new Movie {
MovieId = 3,
Name = "Moonstruck",
Year = 1988,
Rating = 4
}
);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 18
A connection string in the appsettings.json file
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MovieContext": "Server=(localdb)\\mssqllocaldb;Database=Movies;
Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 19
Code that enables dependency injection
for DbContext objects
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using MovieList.Models;
...
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
...
services.AddDbContext<MovieContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("MovieContext")));
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 20
The Package Manager Console (PMC) window

How to open the PMC window


 Select the ToolsNuGet Package ManagerPackage Manager
Console command.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 21
How to create the Movies database from your code
1. Make sure the connection string and dependency injection are set
up.
2. Type “Add-Migration Initial” in the PMC at the command prompt
and press Enter.
3. Type “Update-Database” at the command prompt and press Enter.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 22
The Up() method of the Initial migration file
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Movies",
columns: table => new {
MovieId = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy",
SqlServerValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: false),
Year = table.Column<int>(nullable: false),
Rating = table.Column<int>(nullable: false)
},
constraints: table => {
table.PrimaryKey("PK_Movies", x => x.MovieId);
});

migrationBuilder.InsertData(
table: "Movies",
columns: new[] { "MovieId", "Name", "Rating", "Year" },
values: new object[] { 1, "Casablanca", 5, 1942 });

// code that inserts the other two movies


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 23
How to view the database once it’s created
1. Choose the ViewSQL Server Object Explorer command in
Visual Studio.
2. Expand the (localdb)\MSSQLLocalDB node, then expand the
Databases node.
3. Expand the Movies node, then expand the Tables node.
4. To view the table columns, expand a table node and then its
Columns node.
5. To view the table data, right-click a table and select the ViewData
command.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 24
LINQ methods that build or execute
a query expression
Where(lambda)
OrderBy(lambda)
FirstOrDefault(lambda)
ToList()

A method of the DbSet<Entity> class


that gets an entity by its id
Find(id)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 25
A using directive that imports the LINQ namespace
using System.Linq;

A DbContext property that’s used in the examples


private MovieContext context { get; set; }

Code that builds a query expression


IQueryable<Movie> query =
context.Movies.OrderBy(m => m.Name);

Code that executes a query expression


List<Movie> movies = query.ToList();

Code that builds and executes a query expression


var movies =
context.Movies.OrderBy(m => m.Name).ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 26
Code that builds a query expression
by chaining LINQ methods
var query = context.Movies.Where(m => m.Rating > 3)
.OrderBy(m => m.Name);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 27
Code that builds a query expression
on multiple lines
IQueryable<Movie> query = context.Movies;
query = query.Where(m => m.Year > 1970);
query = query.Where(m => m.Rating > 3);
query = query.OrderBy(m => m.Name);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 28
Three ways to select a movie by its id
int id = 1;
var movie = context.Movies.Where(
m => m.MovieId == id).FirstOrDefault();
var movie = context.Movies.FirstOrDefault(m => m.MovieId == id);
var movie = context.Movies.Find(id);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 29
Three methods of the DbSet class
Add(entity)
Update(entity)
Remove(entity)

One method of the DbContext class


SaveChanges()

A using directive for the EF Core namespace


using Microsoft.EntityFrameworkCore;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 30
Code that adds a new movie to the database
var movie = new Movie { Name = "Taxi Driver",
Year = 1976,
Rating = 4
};
context.Movies.Add(movie);
context.SaveChanges();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 31
An appsettings.json file that displays
the generated SQL statements
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Debug"
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 32
Code that selects movies from the database
var movies = context.Movies.OrderBy(m => m.Name).ToList();

The generated SQL statement


SELECT [m].[MovieId], [m].[Name], [m].[Rating], [m].[Year]
FROM [Movies] AS [m]
ORDER BY [m].[Name]

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 33
The Home controller
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using MovieList.Models;

namespace MovieList.Controllers
{
public class HomeController : Controller
{
private MovieContext context { get; set; }

public HomeController(MovieContext ctx) {


context = ctx;
}

public IActionResult Index() {


var movies = context.Movies.OrderBy(
m => m.Name).ToList();
return View(movies);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 34
The Home/Index view (part 1)
@model List<Movie>
@{
ViewBag.Title = "My Movies";
}

<h2>Movie List</h2>

<a asp-controller="Movie" asp-action="Add">Add New Movie</a>


<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Year</th>
<th>Rating</th>
<th></th>
</tr>
</thead>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 35
The Home/Index view (part 2)
<tbody>
@foreach (var movie in Model) {
<tr>
<td>@movie.Name</td>
<td>@movie.Year</td>
<td>@movie.Rating</td>
<td>
<a asp-controller="Movie" asp-action="Edit"
asp-route-id="@movie.MovieId">Edit</a>
<a asp-controller="Movie" asp-action="Delete"
asp-route-id="@movie.MovieId">Delete</a>
</td>
</tr>
}
</tbody>
</table>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 36
The Movie controller (part 1)
namespace MovieList.Controllers
{
public class MovieController : Controller
{
private MovieContext context { get; set; }

public MovieController(MovieContext ctx) {


context = ctx;
}

[HttpGet]
public IActionResult Add() {
ViewBag.Action = "Add";
return View("Edit", new Movie());
}

[HttpGet]
public IActionResult Edit(int id) {
ViewBag.Action = "Edit";
var movie = context.Movies.Find(id);
return View(movie);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 37
The Movie controller (part 2)
[HttpPost]
public IActionResult Edit(Movie movie) {
if (ModelState.IsValid) {
if (movie.MovieId == 0)
context.Movies.Add(movie);
else
context.Movies.Update(movie);
context.SaveChanges();
return RedirectToAction("Index", "Home");
} else {
ViewBag.Action =
(movie.MovieId == 0) ? "Add": "Edit";
return View(movie);
}
}

[HttpGet]
public IActionResult Delete(int id) {
var movie = context.Movies.Find(id);
return View(movie);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 38
The Movie controller (part 3)
[HttpPost]
public IActionResult Delete(Movie movie) {
context.Movies.Remove(movie);
context.SaveChanges();
return RedirectToAction("Index", "Home");
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 39
The Movie/Edit view (part 1)
@model Movie
@{
string title = ViewBag.Action + " Movie";
ViewBag.Title = title;
}

<h2>@ViewBag.Title</h2>

<form asp-action="Edit" method="post">


<div asp-validation-summary="All"
class="text-danger">
</div>

<div class="form-group">
<label asp-for="Name">Name</label>
<input asp-for="Name" class="form-control">
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 40
The Movie/Edit view (part 2)
<div class="form-group">
<label asp-for="Year">Year</label>
<input asp-for="Year" class="form-control">
</div>

<div class="form-group">
<label asp-for="Rating">Rating</label>
<input asp-for="Rating" class="form-control">
</div>

<input type="hidden" asp-for="MovieId" />

<button type="submit" class="btn btn-primary">


@ViewBag.Action</button>
<a asp-controller="Home" asp-action="Index"
class="btn btn-primary">Cancel</a>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 41
The Movie/Delete view
@model Movie
@{
ViewBag.Title = "Delete Movie";
}

<h2>Confirm Deletion</h2>
<h3>@Model.Name (@Model.Year)</h3>

<form asp-action="Delete" method="post">


<input type="hidden" asp-for="MovieId" />

<button type="submit" class="btn btn-primary">


Delete</button>
<a asp-controller="Home" asp-action="Index"
class="btn btn-primary">Cancel</a>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 42
The Genre class
public class Genre
{
public string GenreId { get; set; }
public string Name { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 43
How to add a Genre property to the Movie class
public class Movie
{
/* MovieId, Name, Year, and Rating properties
same as before */

[Required(ErrorMessage = "Please enter a genre.")]


public Genre Genre { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 44
How to specify a foreign key property
when adding a Genre property
public class Movie
{
/* MovieId, Name, Year, and Rating properties
same as before */

[Required(ErrorMessage = "Please enter a genre.")]


public string GenreId { get; set; }
public Genre Genre { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 45
A MovieContext class that adds the Genre model
with initial data (part 1)
public class MovieContext : DbContext {
public MovieContext(DbContextOptions<MovieContext> options)
: base(options)
{ }

public DbSet<Movie> Movies { get; set; }


public DbSet<Genre> Genres { get; set; }

protected override void OnModelCreating(


ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<Genre>().HasData(
new Genre { GenreId = "A", Name = "Action" },
new Genre { GenreId = "C", Name = "Comedy" },
new Genre { GenreId = "D", Name = "Drama" },
new Genre { GenreId = "H", Name = "Horror" },
new Genre { GenreId = "M", Name = "Musical" },
new Genre { GenreId = "R", Name = "RomCom" },
new Genre { GenreId = "S", Name = "SciFi" }
);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 46
A MovieContext class that adds the Genre model
with initial data (part 2)
modelBuilder.Entity<Movie>().HasData(
new Movie { MovieId = 1, Name = "Casablanca",
Year = 1942, Rating = 5, GenreId = "D"
},
new Movie { MovieId = 2, Name = "Wonder Woman",
Year = 2017, Rating = 3, GenreId = "A"
},
new Movie { MovieId = 3, Name = "Moonstruck",
Year = 1988, Rating = 4, GenreId = "R"
}
);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 47
How to update the database
with the new Genre model and seed data
1. Select ToolsNuGet Package ManagerPackage Manager
Console to open the Package Manager Console window.
2. Type “Add-Migration Genre” at the command prompt and
press Enter.
3. Type “Update-Database” at the command prompt and press
Enter.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 48
Some of the code in the Up() method
of the Genre migration file (part 1)
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "GenreId",
table: "Movies",
nullable: false,
defaultValue: "");

migrationBuilder.CreateTable(
name: "Genres",
columns: table => new {
GenreId = table.Column<string>(nullable: false),
Name = table.Column<string>(nullable: true)
}, constraints: table => {
table.PrimaryKey("PK_Genres", x => x.GenreId);
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 49
Some of the code in the Up() method (part 2)
migrationBuilder.InsertData(
table: "Genres",
columns: new[] { "GenreId", "Name" },
values: new object[,] {
{ "A", "Action" },
{ "C", "Comedy" },
{ "D", "Drama" },
{ "H", "Horror" },
{ "M", "Musical" },
{ "R", "RomCom" },
{ "S", "SciFi" }
});

migrationBuilder.UpdateData(
table: "Movies",
keyColumn: "MovieId",
keyValue: 1,
column: "GenreId",
value: "D");

// code that updates the other two movies

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 50
Some of the code in the Up() method (part 3)
migrationBuilder.AddForeignKey(
name: "FK_Movies_Genres_GenreId",
table: "Movies",
column: "GenreId",
principalTable: "Genres",
principalColumn: "GenreId",
onDelete: ReferentialAction.Cascade);
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 51
Another LINQ method that builds
a query expression
Include(lambda)

The Index() action method of the Home controller


using Microsoft.EntityFrameworkCore;
...
public class HomeController : Controller {
...
public IActionResult Index() {
var movies = context.Movies.Include(m => m.Genre)
.OrderBy(m => m.Name).ToList();
return View(movies);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 52
The <table> element of the Home/Index view
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Year</th>
<th>Genre</th>
<th>Rating</th><th></th>
</tr>
</thead>
<tbody>
@foreach (var movie in Model) {
<tr>
<td>@movie.Name</td>
<td>@movie.Year</td>
<td>@movie.Genre.Name</td>
<td>@movie.Rating</td>
<td><!-- Edit/Delete links same as before --></td>
</tr>
}
</tbody>
</table>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 53
The Add() action method of the Movie controller
[HttpGet]
public IActionResult Add()
{
ViewBag.Action = "Add";
ViewBag.Genres
= context.Genres.OrderBy(g => g.Name).ToList();
return View("Edit", new Movie());
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 54
The Edit() action method of the Movie controller
for GET requests
[HttpGet]
public IActionResult Edit(int id) {
ViewBag.Action = "Edit";
ViewBag.Genres
= context.Genres.OrderBy(g => g.Name).ToList();
var movie = context.Movies.Find(id);
return View(movie);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 55
The Edit() action method of the Movie controller
for POST requests
[HttpPost]
public IActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
if (movie.MovieId == 0)
context.Movies.Add(movie);
else
context.Movies.Update(movie);
context.SaveChanges();
return RedirectToAction("Index", "Home");
}
else
{
ViewBag.Action = (movie.MovieId == 0) ? "Add": "Edit";
ViewBag.Genres
= context.Genres.OrderBy(g => g.Name).ToList();
return View(movie);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 56
The form tag of the Movie/Edit view
<form asp-action="Edit" method="post">
...
<div class="form-group">
<label asp-for="GenreId">Genre</label>
<select asp-for="GenreId" class="form-control">
<option value="">select a genre</option>
@foreach (Genre g in ViewBag.Genres)
{
<option value="@g.GenreId">
@g.Name
</option>
}
</select>
</div>
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 57
The default URLs of an MVC app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 58
The ConfigureServices() method to make URLs
lowercase with a trailing slash
public void ConfigureServices(IServiceCollection
services) {
...
services.AddRouting(options => {
options.LowercaseUrls = true;
options.AppendTrailingSlash = true;
});
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 59
The reconfigured URLs

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 60
The Edit page with numeric ID values only
in the URL

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 61
The default route updated to include
a second optional parameter
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern:
"{controller=Home}/{action=Index}/{id?}/{slug?}");
});

A read-only property named Slug


in the Movie class
public string Slug =>
Name?.Replace(' ', '-').ToLower() + '-' +
Year?.ToString();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 62
The Edit/Delete links in the Home/Index view
<a asp-controller="Movie" asp-action="Edit"
asp-route-id="@movie.MovieId"
asp-route-slug="@movie.Slug">Edit</a>
<a asp-controller="Movie" asp-action="Delete"
asp-route-id="@movie.MovieId"
asp-route-slug="@movie.Slug">Delete</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 63
The Edit page with a slug in the URL

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C4, Slide 64
Chapter 5

How to manually
test and debug
an ASP.NET Core
web app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 1
Objectives (part 1)
Applied
1. Manually test a web app in multiple browsers to find errors.
2. Use the debugging techniques presented in this chapter to
determine the cause of errors so you can fix them.

Knowledge
1. Describe how to use Visual Studio to change the default browser.
2. Describe some of the debugging features provided by the
developer tools of the major browsers.
3. Describe the conditions under which the Internal Server Error
page is displayed.
4. Describe the conditions under which the Exception Helper dialog
is displayed.
5. Distinguish between a breakpoint and a tracepoint.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 2
Objectives (part 2)
6. Describe the three Step commands that you can use to control the
execution of an app when it reaches a breakpoint: Step Into, Step
Over, and Step Out.
7. Describe the use of the Locals and Watch windows.
8. Describe the use of the Immediate window.
9. Describe how to use tracepoints to log messages to the Output
window.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 3
The Start drop-down menu
and the Web Browser menu

How to change the default browser


 Display the Start drop-down menu by clicking on its down
arrow. Then, display the Web Browser menu and select a
browser from it.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 4
How to run an app without debugging
 Press Ctrl+F5.
 Select DebugStart Without Debugging.

How to run an app with debugging


 Press F5.
 Click the Start button in the Standard toolbar.
 Select DebugStart Debugging.

How to stop debugging


 Press Shift+F5.
 Click the Stop Debugging button in the Debug toolbar.
 Select DebugStop Debugging.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 5
How to run an app in multiple browsers
 To run an app in two or more browsers, select Browse With from
the Start drop-down menu. Then, in the resulting dialog, hold
down the Ctrl key, select the browsers you want to use, and click
the Browse button.
 When you run an app in two or more browsers, the app is run
without debugging.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 6
Chrome with the Elements tab
of the developer tools open

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 7
How to open and close the developer tools

In Chrome, Firefox, and Edge


 To open, press F12. Or, right-click an element in the page and
select Inspect.
 To close, press F12. Or, click the X in the upper right corner of
the tools panel.

In Opera and Safari


 To open, right-click an element in the page and select Inspect
Element.
 To close, click the X in the upper right corner of the tools panel.
 In Safari, you must enable the developer tools before you can use
them. To do that, select Preferences, click the Advanced tab, and
select the “Show Develop menu” item.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 8
How to view the rendered HTML and CSS styles
 Open the appropriate panel by clicking on its tab. In Firefox, it’s
called the Inspector tab. In Chrome, Safari, and Opera, it’s called
the Elements tab.
 Expand the nodes to navigate to the element you want. Then,
click that element.
 The HTML elements for a page are typically shown in the top of
the panel, and the CSS styles for the selected element are
typically shown below the HTML elements.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 9
The Internal Server Error page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 10
The Internal Server Error page can display…
 The name and description of the exception.
 A stack trace that you can use to find the line of code that caused
the exception.
 Query strings, cookies, headers, and routing data for the current
request.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 11
The Exception Helper

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 12
The Movie controller with a breakpoint

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 13
How to set and remove breakpoints
 To set a breakpoint, click in the margin indicator bar to the left of
the line number for a statement. This highlights the statement and
adds a breakpoint indicator (a red dot) in the margin.
 To remove a breakpoint, click the breakpoint indicator.
 To remove all breakpoints, select DebugDelete All
Breakpoints.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 14
How to enable and disable breakpoints
 To enable or disable a breakpoint, point to the breakpoint
indicator and select Enable/Disable Breakpoint from the resulting
menu.
 To disable all breakpoints, select DebugDisable All
Breakpoints.
 To enable all breakpoints, select DebugEnable All
Breakpoints.
 To display the Breakpoints window, select
DebugWindowsBreakpoints. This window is most useful
for enabling and disabling existing breakpoints.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 15
The Movie List app in break mode

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 16
Commands in the Debug menu and toolbar
Command Keyboard
Start/Continue F5
Break All Ctrl+Alt+Break
Stop Debugging Shift+F5
Restart Ctrl+Shift+F5
Step Into F11
Step Over F10
Step Out Shift+F11

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 17
The Locals and Watch windows

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 18
How to use the Locals, Autos,
and Watch windows
 To display one of these windows, click its tab or select it from
the DebugWindows menu.
 The Locals window displays information about the variables
within the scope of the current method.
 The Autos window works like the Locals window, but it only
displays information about variables used by the current
statement and the previous statement.
 The Watch windows let you view the values of variables and
expressions that you specify, called watch expressions. To add a
watch expression, type a variable name or expression into the
Name column. To delete a row from a Watch window, right-click
the row and select Delete Watch.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 19
The Immediate window

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 20
How to use the Immediate window
 To display this window, click its tab or select it from the
DebugWindows menu.
 To display the current value of a variable or expression, type a
question mark followed by a variable name or expression. Then,
press Enter.
 To execute a statement, type the statement. Then, press Enter.
 To execute an existing command, press the Up or Down arrow
until you have displayed the command. Then, press Enter.
 To remove all commands and output, right-click the window and
select Clear All.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 21
The Movie controller with a tracepoint
that logs a message

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 22
The Output window that displays the message

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 23
How to set a tracepoint
 To set a new tracepoint, right-click a statement and select
BreakpointInsert Tracepoint. Then, complete the Breakpoint
Settings window.
 To convert an existing breakpoint to a tracepoint, point to the
breakpoint icon, click the Settings icon that looks like a gear, and
complete the Breakpoint Settings window.
 For a tracepoint, the Breakpoint Settings window should have the
Continue Execution option selected. That way, it doesn’t enter
break mode like a breakpoint.
 When logging a message to the Output window, you can include
the value of a variable or other expression by placing the variable
or expression inside curly braces, and you can include special
keywords such as FUNCTION by coding a dollar sign ($)
followed by the keyword.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C5, Slide 24
Chapter 6

How to work
with controllers
and routing

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 1
Objectives (part 1)
Applied
1. Configure the routing for an ASP.NET Core MVC web app and
develop controllers that work with the URLs of the app.
2. Use areas to organize the folders and files of an app.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 2
Objectives (part 2)
Knowledge
1. Describe the three segments of the default route.
2. Describe how the segments of the default route map to a controller,
its action methods, and their parameters.
3. For a custom route, describe the difference between specifying
static and dynamic data.
4. When an app has multiple routes, describe the sequence in which
they must be coded.
5. Distinguish between attribute routing and regular routing.
6. List three best practices for creating URLs.
7. List two benefits of well-designed URLs.
8. Describe how you can use areas to organize the folders and files of
an app.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 3
The methods for adding the MVC service

Method Available with


AddControllersWithViews() ASP.NET Core 2.2 and later

AddMvc() Versions prior to ASP.NET.Core 2.2

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 4
Two methods for enabling and configuring routing
UseRouting()
UseEndpoints(endpoints)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 5
Methods that configure the default route (part 1)
// Use this method to add services to the project.
public void ConfigureServices(IServiceCollection services) {
services.AddControllersWithViews(); // add MVC services

// add other services here


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 6
Methods that configure the default route (part 2)
// Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env) {
app.UseDeveloperExceptionPage();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); // mark where routing decisions are made

// configure middleware that runs after routing decisions


// have been made

app.UseEndpoints(endpoints => // map the endpoints


{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

// configure other middleware here


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 7
Another way to map a controller
to the default route
endpoints.MapDefaultControllerRoute();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 8
A URL that has three segments

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 9
The pattern for the default route
{controller=Home}/{action=Index}/{id?}

How the default route works


 The first segment specifies the controller. Since the pattern sets
the Home controller as the default controller, this segment is
optional.
 The second segment specifies the action method within the
controller. Since the pattern sets the Index() method as the
default action, this segment is optional.
 The third segment specifies an argument for the id parameter of
the action method. The pattern uses a question mark (?) to
specify that this segment is optional.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 10
Request URL mappings for the default route
Request URL Controller Action Id
http://localhost Home Index null
http://localhost/Home Home Index null
http://localhost/Home/Index Home Index null

http://localhost/Home/About Home About null

http://localhost/Product Product Index null


http://localhost/Product/List Product List null
http://localhost/Product/List/Guitars Product List Guitars

http://localhost/Product/Detail Product Detail 0


http://localhost/Product/Detail/3 Product Detail 3

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 11
A method that a controller can use
to return plain text to the browser
Content(string)

The Home controller


using Microsoft.AspNetCore.Mvc;

namespace GuitarShop.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
return Content("Home controller, Index action");
}

public IActionResult About()


{
return Content("Home controller, About action");
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 12
A browser after requesting the default page

A browser after requesting the Home/About page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 13
The Product controller
using Microsoft.AspNetCore.Mvc;

namespace GuitarShop.Controllers
{
public class ProductController : Controller
{
public IActionResult List(string id = "All")
{
return Content("Product controller, List action, id: "
+ id);
}

public IActionResult Detail(int id)


{
return Content("Product controller, Detail action, id: "
+ id);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 14
A browser after requesting the Product/List action
with no id segment

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 15
A browser after requesting the Product/Detail
action with an id segment

A browser after requesting the Product/Detail


action with no id segment

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 16
A pattern that mixes static and dynamic data
for a segment
{controller}/{action}/{cat}/Page{num} // 4 segments

Example URLs with static and dynamic data


Request URL Controller Action Parameters
/Product/List/All/Page1 Product List cat=All, num=1
/Product/List/All/Page2 Product List cat=All, num=2

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 17
The List() method of the Product controller
with static and dynamic data
public IActionResult List(string cat, int num)
{
return Content("Product controller, List action, " +
"Category " + cat + ", Page " + num);
}

A URL that requests the Product/List action


for page 1 of all categories

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 18
A pattern with one completely static segment
{controller}/{action}/{cat}/Page/{num} // 5 segments

Example URLs with a completely static segment


Request URL Controller Action Parameters
/Product/List/All/Page/1 Product List cat=All, num=1
/Product/List/All/Page/2 Product List cat=All, num=2

A URL that requests the Product/List action


for page 1 of all categories

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 19
Three routing patterns mapped to controllers
app.UseEndpoints(endpoints =>
{
// most specific route – 5 required segments
endpoints.MapControllerRoute(
name: "paging_and_sorting",
pattern: "{controller}/{action}/{id}/page{page}/sort-by-{sortby}");

// specific route – 4 required segments


endpoints.MapControllerRoute(
name: "paging",
pattern: "{controller}/{action}/{id}/page{page}");

// least specific route – 0 required segments


endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 20
The List() method of the Product controller
with multiple routing patterns
public IActionResult List(string id = "All",
int page = 1, string sortby = "Price")
{
return Content("id=" + id + ", page=" + page +
", sortby=" + sortby);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 21
URLs that use the default route
Request URL Controller/Action Parameters
/ Home/Index id=null
/Home/About Home/About id=null
/Product/Detail/4 Product/Detail id=4
/Product/List/Guitars Product/List id=Guitars

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 22
URLs that uses the paging route
Request URL Parameters
/Product/List/All/Page3 id=All, page=3
/Product/List/Guitars/Page2 id=Guitars, page=2
/Product/List/Guitars/Pg2 Not found because "Pg"
doesn't match static "Page".

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 23
URLs that use the paging_and_sorting route
Request URL Parameters
/Product/List/Guitars/Page2/Sort-By-Name id=Guitars,
page=2,
sortby=Name
/Product/List/Guitars/Page2/By-Name Not found because
"By-" doesn't
match "Sort-By-".

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 24
The Home controller with attribute routing
for both actions
public class HomeController : Controller
{
[Route("/")]
public IActionResult Index()
{
return Content("Home controller, Index action");
}

[Route("About")]
public IActionResult About()
{
return Content("Home controller, About action");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 25
Request URL mappings that use attribute routing
Request URL Maps to
/ the Home/Index action
/About the Home/About action

Two default routes that are overridden


by the attribute routing
/Home/Index
/Home/About

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 26
Two tokens for inserting variable data into a route
[controller]
[action]

A more flexible way to code the attribute


for the Home/About action
[Route("[action]")]

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 27
How to map all controllers
that use attribute routing
app.UseEndpoints(endpoints =>
{
// map controllers that use attribute routing -
// often not necessary
endpoints.MapControllers();

// map pattern for default route


endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 28
The Product controller with attribute routing
that specifies segments
public class ProductController : Controller
{
[Route("Products/{cat?}")]
public IActionResult List(string cat = "All")
{
return Content("Product controller, List action, Category: "
+ cat);
}

[Route("Product/{id}")]
public IActionResult Detail(int id)
{
return Content("Product controller, Detail action, ID: "
+ id);
}

[NonAction]
public string GetSlug(string name)
{
return name.Replace(' ', '-').ToLower();
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 29
Request URL mappings with attribute routing
that specifies segments
Request URL Description
/Products Maps to the Product/List action and uses
the default parameter value of “All”.
/Products/Guitars Maps to the Product/List action and passes
an argument of “Guitars”.
/Product/3 Maps to the Product/Detail action and
supplies a valid int argument of 3.
/Product This URL is not found. It does not map to
the Product/Detail action because it does
not supply the required id segment.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 30
Two default routes that are overridden
by the attribute routing with segments
/Product/List
/Product/Detail

A more flexible way to code the attribute


for the Product/List action
[Route("[controller]s/{cat?}")]

A more flexible way to code the attribute


for the Product/Detail action
[Route("[controller]/{id}")]

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 31
The code for the Product controller
[Route("Retail/[controller]/[action]/{id?}")]
public class ProductController : Controller
{
public IActionResult List(string id = "All")
{
return Content("Product controller, List action, Category: "
+ id);
}

public IActionResult Detail(int id)


{
return Content("Product controller, Detail action, ID: "
+ id);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 32
Request URL mappings with attribute routing
for all actions of a controller
Request URL Description
/Retail/Product/List Maps to the Product/List action and
uses the default parameter value of
“All”.
/Retail/Product/List/Guitars Maps to the Product/List action and
passes an argument of “Guitars”.
/Retail/Product/Detail Maps to the Product/Detail action
and uses the default int value of 0.
/Retail/Product/Detail/3 Maps to the Product/Detail action
and passes a valid int argument of
3.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 33
Two default routes that are overridden
by the attribute routing
/Product/List
/Product/Detail

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 34
Best practices for URLs
 Keep the URL as short as possible while still being descriptive
and user-friendly.
 Use keywords to describe the content of a page, not
implementation details.
 Make your URLs easy for humans to understand and type.
 Use hyphens to separate words, not other characters, especially
spaces.
 Prefer names as identifiers over numbers.
 Create an intuitive hierarchy.
 Be consistent.
 Avoid the use of query string parameters if possible.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 35
A URL that identifies a product…
With a number
https://www.domain.com/product/1307

With a name
https://www.domain.com/product/fender-special-edition-
standard-stratocaster

With a number and name


(to keep it descriptive but short)
https://www.domain.com/product/1307/fender-special

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 36
Four URLs that use query strings to pass data
(not recommended)
https://www.murach.com/p/List?
/p/List?catId=1
/p/List?catId=1&pg=1
/p/Detail?id=1307

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 37
Four URLs that follow best practices
https://www.murach.com/product/list
/product/list/guitars
/product/list/guitars/page-1
/product/detail/1307/fender-stratocaster

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 38
Four shorter URLs that follow best practices
https://www.murach.com/products
/products/guitars
/products/guitars/page-1
/product/1307/fender-stratocaster

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 39
The starting folders for an app
with an Admin area (part 1)
GuitarShop
/Areas
/Admin
/Controllers
/HomeController.cs
/ProductController.cs
/Views
/Home
/Index.cshthml
/Product
/List.cshthml
/AddUpdate.cshthml
/Delete.cshthml
/Shared
/_AdminLayout.cshtml
_ViewImports.cshtml
_ViewStart.cshtml

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 40
The starting folders for an app
with an Admin area (part 2)
/Controllers
/HomeController.cs
/ProductController.cs
/Models
/Product.cs
/Category.cs
/Views
/Home
/Index.cshthml
/About.cshthml
/Product
/List.cshthml
/Detail.cshthml
/Shared
/_Layout.cshtml
_ViewImports.cshtml
_ViewStart.cshtml
Program.cs
Startup.cs

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 41
A route that works with an area
app.UseEndpoints(endpoints =>
{
endpoints.MapAreaControllerRoute(
name: "admin",
areaName: "Admin",
pattern: "Admin/{controller=Home}/{action=Index}/{id?}");

endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 42
The Home controller for the Admin area
namespace GuitarShop.Areas.Admin.Controllers
{
[Area("Admin")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View(); // maps to /Areas/Admin/Views/Home/
// Index.cshtml
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 43
A token you can use to insert the area into a route
[area]

The Product controller for the Admin area


namespace GuitarShop.Areas.Admin.Controllers
{
[Area("Admin")]
public class ProductController : Controller
{
[Route("[area]/[controller]s/{id?}")]
public IActionResult List(string id = "All") {...};

public IActionResult Add() {...};


public IActionResult Update(int id) {...};
public IActionResult Delete(int id) {...};
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 44
Request URL mappings associated with areas
Request URL Description
/Admin/Products Maps to the Product/List action and uses
the default parameter value of “All”.
/Admin/Products/Guitars Maps to the Product/List action and
passes an argument of “Guitars”.
/Admin/Product/Add Maps to the Product/Add action.
/Admin/Product/Update/3 Maps to the Product/Detail action and
passes an id argument of 3.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C6, Slide 45
Chapter 7

How to work
with Razor views

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 1
Objectives (part 1)
Applied
1. Develop strongly-typed Razor views that display the elements that
are unique for a web page.
2. Develop Razor layouts that provide the elements that are the same
for multiple pages.

Knowledge
1. Distinguish between a Razor code block and inline expressions,
loops, and if statements.
2. Distinguish between an inline conditional statement and an inline
conditional expression.
3. Describe how an MVC web app typically maps its views to the
action methods of its controllers.
4. Describe the use of tag helpers to generate a URL.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 2
Objectives (part 2)
5. Distinguish between a relative URL and an absolute URL.
6. Describe how to pass a model to a view.
7. Distinguish between the @model directive and the @Model
property.
8. Describe how to bind an HTML element to a property of a view’s
model.
9. Describe how to bind a <select> element to a list of items.
10. Describe how to display a list of items in a <table> element.
11. Describe how to create and apply a layout.
12. Describe how to build layouts by nesting one layout within
another.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 3
Objectives (part 3)
13. Describe how a layout can use the ViewContext property to get
data about the route of the current view.
14. Describe how to code a section in a view and render that section
in a layout.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 4
The syntax for a Razor code block
@{
// one or more C# statements
}

The syntax for an inline expression


@(csharp_expression)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 5
The Index() action method of the Home controller
public IActionResult Index()
{
ViewBag.CustomerName = "John";
return View(); // returns Views/Home/Index.cshtml
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 6
The Views/Home/Index.cshtml file
@{
string message = "Welcome!";
if (ViewBag.CustomerName != null)
{
message = "Welcome back!";
}
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Home</title>
</head>
<body>
<h1>@message</h1>
<p>Customer Name: @ViewBag.CustomerName</p>
<p>2 + 2 = @(2 + 2)</p>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 7
The view displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 8
A for loop that displays a drop-down list
of month numbers
<label for="month">Month:</label>
<select name="month" id="month">
@for (int month = 1; month <= 12; month++)
{
<option value="@month">@month</option>
}
</select>

The result displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 9
Code in a controller that creates a list of strings
public IActionResult Index()
{
ViewBag.Categories = new List<string>
{
"Guitars", "Basses", "Drums"
};
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 10
A foreach loop that displays a list of links
@foreach (string category in ViewBag.Categories)
{
<div>
<a href="/Product/List/@category/">@category</a>
</div>
}

The result displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 11
An if-else statement in a view
@if (ViewBag.ProductID == 1)
{
<p>Fender Stratocaster</p>
}
else if (ViewBag.ProductID == 2)
{
<p>Gibson Les Paul</p>
}
else
{
<p>Product Not Found</p>
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 12
A switch statement in a view
@switch (ViewBag.ProductID)
{
case 1:
<p>Fender Stratocaster</p>
break;
case 2:
<p>Gibson Les Paul</p>
break;
default:
<p>Product Not Found</p>
break;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 13
An if statement that adds a Bootstrap CSS class
if true
<a asp-controller="Product" asp-action="List"
asp-route-id="All"
class="list-group-item
@if (ViewBag.SelectedCategoryName == "All") {
<text>active</text>
}">
All
</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 14
A conditional expression that adds a Bootstrap
CSS class if true
<a asp-controller="Product" asp-action="List" asp-route-id="All"
class="list-group-item
@(ViewBag.SelectedCategoryName == "All" ? "active" : "")">
All
</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 15
The starting folders and files
for the Guitar Shop app
GuitarShop
/Controllers
/HomeController.cs
/ProductController.cs
/Models
/Category.cs
/Product.cs
/Views
/Home
/Index.cshthml -- the view for the Home/Index action
/About.cshthml -- the view for the Home/About action
/Product
/List.cshthml -- the view for the Product/List action
/Details.cshthml -- the view for the Product/Details action
/Update.cshthml -- the view for the Product/Update action
/Shared
/_Layout.cshtml -- a layout that can be shared by views
_ViewImports.cshtml -- imports models and tag helpers for views
_ViewStart.cshtml -- specifies the default layout for views
/wwwroot
/css
/custom.css
/lib
/bootstrap/css/bootstrap.min.css
Startup.cs -- configures middleware that may impact views
Program.cs -- sets up the app

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 16
The routing that’s specified in the Startup.cs file
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 17
A method that a controller can use
to return a view result to the browser
View()
View(name)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 18
The Home controller
public class HomeController : Controller
{
public IActionResult Index()
{
return View(); // Views/Home/Index.cshtml
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 19
The Product controller
public class ProductController : Controller
{
public IActionResult Index()
{
return View("List"); // Views/Product/List.cshtml
}

public IActionResult List(string id = "All")


{
ViewBag.Category = id;
return View(); // Views/Product/List.cshtml
}

public IActionResult Details(string id)


{
ViewBag.ProductSlug = id;
return View(); // Views/Product/Details.cshtml
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 20
The _Layout.cshtml file in the Views/Shared folder
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/custom.css" />
</head>
<body>
@RenderBody()
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 21
A _ViewStart.cshtml file
that sets the default layout
@{
Layout = "_Layout";
}

A _ViewImports.cshtml file
that enables all ASP.NET Core MVC tag helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 22
How to add a Razor layout, view start,
or view imports file
1. Right-click on the folder where you want to add the file, and
select the AddNew Item item.
2. In the resulting dialog, select the ASP.NET CoreWeb
category.
3. Select the Razor item you want to add and respond to the
resulting dialog boxes.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 23
Three tag helpers for generating URLs
asp-controller
asp-action
asp-route-param_name

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 24
Two ways to code a link
Use HTML to hard code the URL in the href attribute
<a href="/Product/List/Guitars">View guitars</a>

Use ASP.NET tag helpers to generate the URL


<a asp-controller="Product" asp-action="List"
asp-route-id="Guitars">View guitars</a>

The URL for both links


/Product/List/Guitars

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 25
How to code a link to an action method
in the same controller
<a asp-action="About">About Us</a>

The URL that’s generated


/Home/About

How to code a link to an action method


in a different controller
<a asp-controller="Product" asp-action="List">
View all products</a>

The URL that’s generated


/Product/List

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 26
How to code a link that includes a parameter
that’s in a route
<a asp-controller="Product" asp-action="List"
asp-route-id="Guitars">View guitars</a>

The URL that’s generated


/Product/List/Guitars

A link that specifies a route parameter


that doesn’t exist
<a asp-controller="Product" asp-action="List"
asp-route-page="1" asp-route-sort_by="price">
Products - Page 1</a>

The URL that’s generated


/Product/List?page=1&sort_by=price

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 27
The Home/Index view
@{
ViewBag.Title = "Home";
}
<h1>Home</h1>
<div class="list-group">
<a asp-controller="Product" asp-action="List">
View all products</a>

<a asp-controller="Product" asp-action="List"


asp-route-id="Guitars">View guitars</a>

<a asp-controller="Product" asp-action="Details"


asp-route-id="Fender-Stratocaster">
View Fender Stratocaster</a>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 28
The Product/List view
@{
ViewBag.Title = "Product List";
}
<h1>Product List</h1>
<div class="list-group">
<p>Category: @ViewBag.Category</p>

<a asp-action="Details"
asp-route-id="Fender-Stratocaster">
View Fender Stratocaster</a>

<a asp-controller="Home" asp-action="Index">Home</a>


</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 29
The Product/Details view
@{
ViewBag.Title = "Product Details";
}
<h1>Product Details</h1>
<div class="list-group">
<p>Slug: @ViewBag.ProductSlug</p>

<a asp-controller="Home" asp-action="Index">Home</a>


</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 30
A browser displaying the Home page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 31
A browser after clicking the View Guitars link

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 32
A browser after requesting
the View Fender Stratocaster link

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 33
More tag helpers for generating URLs
asp-area
asp-fragment
asp-protocol
asp-host

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 34
How to code a link to an area
<a asp-area="Admin" asp-controller="Product"
asp-action="List">Admin - Product Manager</a>

The URL that’s generated


/Admin/Product/List

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 35
How to code an HTML placeholder
<h2 id="Fender">Fender Guitars</h2>

How to code a URL that jumps


to an HTML placeholder on the same page
<a asp-fragment="Fender">View Fender Guitars</a>

The URL that’s generated


/#Fender

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 36
How to code an absolute URL
<a asp-protocol="https"
asp-host="murach.com"
asp-controller="Shop"
asp-action="Details"
asp-route-id="html5-and-css3"
asp-fragment="reviews">Murach's HTML5 and CSS3 - Reviews</a>

The URL that’s generated


https://www.murach.com/Shop/Details/html5-and-css3#reviews

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 37
Format specifiers you can use to format numbers
Specifier Name
C Currency
N Number
P Percent

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 38
Code that stores numbers in the ViewBag
ViewBag.Price = 12345.67;
ViewBag.DiscountPercent = .045;
ViewBag.Quantity = 1234;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 39
Razor expressions that format the numbers
Expression Result
@ViewBag.Price.ToString("C") $12,345.67
@ViewBag.Price.ToString("C1") $12,345.7
@ViewBag.Price.ToString("C0") $12,346
@ViewBag.Price.ToString("N") 12,345.67
@ViewBag.Price.ToString("N1") 12,345.7
@ViewBag.Price.ToString("N0") 12,346
@ViewBag.DiscountPercent.ToString("P") 4.50%
@ViewBag.DiscountPercent.ToString("P1") 4.5%
@ViewBag.DiscountPercent.ToString("P0") 5%

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 40
Code in a view that displays the numbers
<div>Price: @ViewBag.Price.ToString("C")</div>
<div>Discount Percent: @ViewBag.DiscountPercent.ToString("P1")</div>
<div>Quantity: @ViewBag.Quantity.ToString("N0")</div>

The view displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 41
The Product model
namespace GuitarShop.Models
{
public class Product
{
public int ProductID { get; set; }
public Category Category { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Slug => Name.Replace(' ', '-');
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 42
The Category model
namespace GuitarShop.Models
{
public class Category
{
public int CategoryID { get; set; }
public string Name { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 43
A method that a controller can use
to pass a model to a view
View(model)
View(name, model)

The Product/Details() action method


that passes a model to a view
public IActionResult Details(int id) {
Product product = context.Products.Find(id);
return View(product);
}

A Product/Add() action method


that passes a model to the specified view
[HttpGet]
public IActionResult Add() {
return View("Update", new Product());
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 44
A directive for displaying model properties
in a view
@model

A property for displaying model properties


in a view
@Model

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 45
A _ViewImports file that imports the Models
namespace for all views
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using GuitarShop.Models

The code for the Product/Details view


@model Product
@{
ViewBag.Title = "Product Details";
}
<h1>Product Details</h1>
<div>ID: @Model.ProductID</div>
<div>Name: @Model.Name</div>
<div>Category: @Model.Category.Name</div>
<div>Price: @Model.Price.ToString("C2")</div>
<a asp-controller="Home" asp-action="Index">Home</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 46
The view with model properties in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 47
A tag helper for binding HTML elements
to model properties
asp-for

A view with asp-for tag helpers (part 1)


@model Product
@{
ViewBag.Title = "Update Product";
}
<h1>Update Product</h1>
<form asp-action="Update" method="post">
<div class="form-group">
<label asp-for="Name"></label>
<input asp-for="Name" class="form-control">
</div>

<div class="form-group">
<label asp-for="Price"></label>
<input asp-for="Price" class="form-control">
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 48
A view with asp-for tag helpers (part 2)
<input type="hidden" asp-for="ProductID" />
<input type="hidden" asp-for="Category.Name" />

<button asp-action="Update" type="submit"


class="btn btn-primary">Update
</button>
<a asp-action="List" class="btn btn-primary">
Cancel</a>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 49
The view with bound properties in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 50
The HTML that’s generated
for the Price <label> and <input> elements
<label for="Price">Price</label>
<input class="form-control" type="text"
id="Price" name="Price" value="699.00">

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 51
A tag helper for adding options
to a <select> element
asp-items

The constructor of the SelectList class


SelectList(list, value, text, selectedValue)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 52
An Update action method that creates the model
and passes it to the view
public IActionResult Update(int id)
{
ViewBag.Categories = context.Categories.ToList();
Product product = context.Products.Find(id);
return View(product);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 53
The code that binds items to a <select> element
<div class="form-group">
<label asp-for="CategoryID">Category</label>
<select asp-for="CategoryID"
asp-items="@(new SelectList(ViewBag.Categories,
"CategoryID", "Name",
Model.CategoryID.ToString()))"
class="form-control"></select>
</div>

The <select> element and its label displayed


in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 54
HTML that’s generated for the <select> element
<select class="form-control" id="Category_CategoryID"
name="Category.CategoryID">
<option selected="selected" value="1">
Guitars</option>
<option value="2">Basses</option>
<option value="3">Drums</option>
</select>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 55
A List action method that creates the model
and passes it to the view
public IActionResult List(string id = "All")
{
ViewBag.Categories = context.Categories.ToList();
ViewBag.SelectedCategory = id;
List<Product> products = context.Products
.Where(p => p.Category.Name == id).ToList();
return View(products);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 56
The code for the view (part 1)
@model List<Product>
@{
ViewBag.Title = "Product List";
}

<h1>Product List</h1>
@if (ViewBag.Categories != null)
{
foreach (Category c in ViewBag.Categories)
{
<a asp-controller="Product" asp-action="List"
asp-route-id="@c.Name">@c.Name</a>
<text> | </text>
}
}
<a asp-controller="Product" asp-action="List"
asp-route-id="All">All</a>
<hr />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 57
The code for the view (part 2)
@if (@ViewBag.SelectedCategory == "All")
{
<h2>All Products</h2>
}
else
{
<h2>@ViewBag.SelectedCategory</h2>
}
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th></th>
<th></th>
</tr>
</thead>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 58
The code for the view (part 3)
<tbody>
@foreach (var product in @Model)
{
<tr>
<td>@product.Name</td>
<td>@product.Price.ToString("C")</td>
<td>
<a asp-controller="Product"
asp-action="Details"
asp-route-id="@product.ProductID">
View</a>
</td>
<td>
<a asp-controller="Product"
asp-action="Update"
asp-route-id="@product.ProductID">
Update</a>
</td>
</tr>
}
</tbody>
</table>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 59
The Product objects view in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 60
The Views/Shared/_MainLayout.cshtml file (part 1)
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/custom.css" />
</head>
<body>
<header>
<a asp-controller="Home" asp-action="Index">
Home</a> |
<a asp-controller="Product" asp-action="List">
Products</a> |
<a asp-controller="Home" asp-action="About">
About</a>
<hr />
</header>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 61
The Views/Shared/_MainLayout.cshtml file (part 2)
@RenderBody()

<footer>
<hr />
<p>&copy; @DateTime.Now.Year - Guitar Shop</p>
</footer>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 62
The code for a _ViewStart.cshtml file
that sets the default layout
@{
Layout = "_Layout";
}

The code for a Home/Index view


that explicitly specifies a layout
@{
Layout = "_MainLayout";
ViewBag.Title = "Home";
}
<p>Welcome to the Guitar Shop website!</p>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 63
The Home page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 64
The _Layout file
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/custom.css" />
</head>
<body>
@RenderBody()
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 65
The _MainLayout file
@{
Layout = "_Layout";
}
<header>
<a asp-controller="Home" asp-action="Index">Home</a> |
<a asp-controller="Product" asp-action="List"
asp-route-id="All">Products</a> |
<a asp-controller="Home" asp-action="About">About</a>
<hr />
</header>

@RenderBody()

<footer>
<hr />
<p>&copy; @DateTime.Now.Year - Guitar Shop</p>
</footer>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 66
The _ProductLayout file
@{
Layout = "_MainLayout";
}

<h1>Product Manager</h1>
@if (ViewBag.Categories != null)
{
foreach (Category c in ViewBag.Categories)
{
<a asp-controller="Product" asp-action="List"
asp-route-id="@c.Name">@c.Name</a><text> | </text>
}
<a asp-controller="Product" asp-action="List"
asp-route-id="All">All</a>
}
<hr />

@RenderBody()

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 67
The code for a view that uses
the _ProductLayout view
@model List<Product>
@{
Layout = "_ProductLayout";
ViewBag.Title = "Product List";
}

@if (@ViewBag.SelectedCategory == "All")


{
<h2>All Products</h2>
}
else
{
<h2>@ViewBag.SelectedCategory</h2>
}

<!-- The code for the <table> element -->

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 68
The view with nested layouts in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 69
A _MainLayout file that uses view context (part 1)
@{
Layout = "_Layout";
string controller =
ViewContext.RouteData.Values["controller"].ToString();
string action =
ViewContext.RouteData.Values["action"].ToString();
}
<nav class="navbar navbar-dark bg-primary fixed-top">
<a class="navbar-brand" href="/">My Guitar Shop</a>
<div class="navbar-nav">
<a class="nav-link
@(controller == "Home" && action == "Index" ?
"active" : "")"
asp-controller="Home" asp-action="Index">Home</a>
<a class="nav-link
@(controller == "Product" ? "active" : "")"
asp-controller="Product" asp-action="List">
Products</a>
<a class="nav-link
@(controller == "Home" && action == "About" ?
"active" : "")"
asp-controller="Home" asp-action="About">About</a>
</div>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 70
A _MainLayout file that uses view context (part 2)
@RenderBody()

<footer>
<hr />
<p>&copy; @DateTime.Now.Year - Guitar Shop</p>
</footer>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 71
A view that uses this layout

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 72
Code in a view file that specifies a section
@model Product
@{
ViewBag.Title = "Update Product";
}

@section scripts {
<script src="~/lib/jquery-validate/jquery.validate.min.js">
</script>
<script src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js">
</script>
}

<h2>Update</h2>

// the HTML elements for the rest of the view body go here

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 73
A method that can insert content
from a section into a layout
RenderSection(name, isRequired)

A layout file that specifies an optional section


<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/custom.css" />

<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/popper.js/popper.min.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.min.js"></script>
@RenderSection("scripts", false)
</head>
<body>
@RenderBody()
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 74
The Product List page for customers

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 75
The Product Details page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 76
The Product Manager page for administrators

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 77
The Update Product page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 78
The Add Product page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 79
The Delete Product page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C7, Slide 80
Chapter 8

How to transfer
data from
controllers

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 1
Objectives (part 1)
Applied
1. Use the ViewBag and ViewData properties to transfer data from a
controller to a view.
2. Use a view model to transfer data from a controller to a strongly-
typed view.
3. Develop web apps that redirect and use the TempData property to
transfer data from a controller to a controller.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 2
Objectives (part 2)
Knowledge
1. Describe some important interfaces and classes of the ActionResult
hierarchy.
2. Distinguish between the ViewBag and ViewData properties.
3. Describe the use of a view model to transfer data from a controller
to a view.
4. Describe how one action method can redirect to another action
method.
5. Describe the use of the PRG pattern to prevent resubmission of
POST data.
6. Distinguish between the ViewData and TempData properties.
7. Describe the purpose of the Keep() and Peek() methods of the
TempData class.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 3
The ActionResult hierarchy

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 4
Common ActionResult subtypes
ViewResult
RedirectResult
RedirectToActionResult
JsonResult
FileResult
StatusCodeResult
ContentResult
EmptyResult

A URL for a full list of the ActionResult subtypes


https://docs.microsoft.com/en-us/dotnet/api/
microsoft.aspnetcore.mvc.actionresult

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 5
Some methods of the Controller class
that return an ActionResult object
View()
Redirect()
RedirectToAction()
File()
Json()

Some of the overloads of the View() method


View()
View(model)
View(name)
View(name, model)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 6
An action method that returns a ViewResult object
public ViewResult List() {
var names = new List<string> { "Grace", "Ada", "Charles" };
return View(names);
}

An action method that returns


a RedirectToActionResult object
public RedirectToActionResult Index() =>
RedirectToAction("List");

An action method that may return different types


of result objects
[HttpPost]
public IActionResult Edit(string id) {
if (ModelState.IsValid)
return RedirectToAction("List");
else
return View((object)id); // cast string model to object
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 7
Controller code that adds two items
to the ViewData property
public ViewResult Index() {
ViewData["Book"] = "Alice in Wonderland";
ViewData["Price"] = 9.99;
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 8
Some properties of the ViewDataDictionary class
Count
Keys
Values

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 9
Razor code that displays all the items
in the ViewData object
<h2>@ViewData.Count items in ViewData</h2>
@foreach (KeyValuePair<string, object> item in ViewData)
{
<div>@item.Key - @item.Value</div>
}

The view in the browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 10
Razor code that casts a ViewData object
to the double type
<h4>Price:
@(((double)ViewData["Price"]).ToString("c"))</h4>

The view in the browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 11
Razor code that checks ViewData values for null
Non-nullable type (double)
<h4>Price: @((ViewData["Price"] == null) ? "" :
((double)ViewData["Price"]).ToString("c"))</h4>

Nullable type (string)


<h4>Book: @ViewData["Book"]?.ToString()</h4>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 12
Controller code that adds two dynamic properties
to the ViewBag property
public ViewResult Index() {
ViewBag.Book = "Alice in Wonderland";
ViewBag.Price = 9.99;
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 13
Razor code that uses ViewData
to display ViewBag properties
<h2>@ViewData.Count ViewBag properties</h2>
@foreach (KeyValuePair<string, object> item
in ViewData) {
<div>@item.Key - @item.Value</div>
}

The view in the browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 14
Razor code that works with a ViewBag property
without casting
<h4>Price: @ViewBag.Price.ToString("c")</h4>

The view in the browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 15
Razor code that checks ViewBag properties
for null
Non-nullable type (double)
<h4>Price: @ViewBag.Price?.ToString("c")</h4>

Nullable type (string)


<h4>Book: @ViewBag.Book?.ToString()</h4>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 16
Use ViewData instead of ViewBag
when you need to…
 Use a key name that isn’t valid in C#, such as a key that contains
spaces.
 Call properties and methods of the ViewDataDictionary class,
such as its Count property or its Clear() method.
 Loop through all items in the ViewData dictionary.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 17
How the teams of the NFL are organized
 There are two conferences, the NFC and the AFC.
 Each conference contains four divisions named North, South,
East, and West.
 Each division contains four teams.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 18
The NFL Teams app on first load

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 19
The NFL Teams app after a conference
and division are selected

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 20
The Conference class
public class Conference
{
public string ConferenceID { get; set; }
public string Name { get; set; }
}

The Division class


public class Division
{
public string DivisionID { get; set; }
public string Name { get; set; }
}

The Team class


public class Team
{
public string TeamID { get; set; }
public string Name { get; set; }
public Conference Conference { get; set; }
public Division Division { get; set; }
public string LogoImage { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 21
The TeamContext class (part 1)
public class TeamContext : DbContext
{
public TeamContext(DbContextOptions<TeamContext> options)
: base(options) { }

public DbSet<Team> Teams { get; set; }


public DbSet<Conference> Conferences { get; set; }
public DbSet<Division> Divisions { get; set; }

protected override void OnModelCreating(


ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<Conference>().HasData(
new Conference { ConferenceID = "afc", Name = "AFC"},
...
);

modelBuilder.Entity<Division>().HasData(
new Division { DivisionID = "north", Name = "North" },
...
);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 22
The TeamContext class (part 2)
modelBuilder.Entity<Team>().HasData(
new {TeamID = "ari", Name = "Arizona Cardinals",
ConferenceID = "nfc", DivisionID = "west",
LogoImage = "ARI.png"},
...
);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 23
The Home controller (part 1)
public class HomeController : Controller
{
private TeamContext context;

public HomeController(TeamContext ctx)


{
context = ctx;
}

public ViewResult Index(string activeConf = "all",


string activeDiv = "all")
{
// store selected conference and division IDs in view bag
ViewBag.ActiveConf = activeConf;
ViewBag.ActiveDiv = activeDiv;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 24
The Home controller (part 2)
// get list of conferences and divisions from database
List<Conference> conferences = context.Conferences.ToList();
List<Division> divisions = context.Divisions.ToList();

// insert default "All" value at front of each list


conferences.Insert(0, new Conference { ConferenceID = "all",
Name = "All" });
divisions.Insert(0, new Division { DivisionID = "all",
Name = "All" });

// store lists in view bag


ViewBag.Conferences = conferences;
ViewBag.Divisions = divisions;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 25
The Home controller (part 3)
// get teams - filter by conference and division
IQueryable<Team> query = context.Teams;
if (activeConf != "all")
query = query.Where(
t => t.Conference.ConferenceID.ToLower() ==
activeConf.ToLower());
if (activeDiv != "all")
query = query.Where(
t => t.Division.DivisionID.ToLower() ==
activeDiv.ToLower());

// pass teams to view as model


var teams = query.ToList();
return View(teams);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 26
The custom route in the Startup.cs file
endpoints.MapControllerRoute(
name: "custom",
pattern:
"{controller}/{action}/conf/{activeConf}/div/{activeDiv}");

endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 27
The layout
<!DOCTYPE html>

<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/lib/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="container">
<header class="text-center text-white">
<h1 class="bg-primary mt-3 p-3">NFL Teams</h1>
</header>
<main>
@RenderBody()
</main>
</div>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 28
The Home/Index view (NFL Teams 1.0) (part 1)
@model IEnumerable<Team>
@{
ViewData["Title"] = "NFL Teams";
string Active(string filter, string selected)
{
return (filter.ToLower() == selected.ToLower()) ?
"active" : "";
}
}
<div class="row">
<div class="col-sm-3">
<h3 class="mt-3">Conference</h3>
<div class="list-group">
@foreach (Conference conf in ViewBag.Conferences)
{
<a asp-action="Index"
asp-route-activeConf="@conf.ConferenceID"
asp-route-activeDiv="@ViewBag.ActiveDiv"
class="list-group-item @Active(conf.ConferenceID,
ViewBag.ActiveConf)">
@conf.Name
</a>
}
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 29
The Home/Index view (NFL Teams 1.0) (part 2)
<h3 class="mt-3">Division</h3>
<div class="list-group">
@foreach (Division div in ViewBag.Divisions)
{
<a asp-action="Index"
asp-route-activeConf="@ViewBag.ActiveConf"
asp-route-activeDiv="@div.DivisionID"
class="list-group-item @Active(div.DivisionID,
ViewBag.ActiveDiv)">
@div.Name
</a>
}
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 30
The Home/Index view (NFL Teams 1.0) (part 3)
<div class="col-sm-9">
<ul class="list-inline">
@foreach (Team team in Model)
{
<li class="list-inline-item">
<img src="~/images/@team.LogoImage"
alt="@team.Name"
title="@team.Name |
@team.Conference.Name @team.Division.Name" />
</li>
}
</ul>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 31
A view model for the NFL Teams app (part 1)
public class TeamListViewModel
{
public List<Team> Teams { get; set; }
public string ActiveConf { get; set; }
public string ActiveDiv { get; set; }

// make next two properties standard properties so the setter


// can make the first item in each list "All"
private List<Conference> conferences;
public List<Conference> Conferences {
get => conferences;
set {
conferences = value;
conferences.Insert(0,
new Conference { ConferenceID = "all",
Name = "All" });
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 32
A view model for the NFL Teams app (part 2)
private List<Division> divisions;
public List<Division> Divisions {
get => divisions;
set {
divisions = value;
divisions.Insert(0,
new Division { DivisionID = "all",
Name = "All" });
}
}

// methods to help view determine active link


public string CheckActiveConf(string c) =>
c.ToLower() == ActiveConf.ToLower() ? "active" : "";

public string CheckActiveDiv(string d) =>


d.ToLower() == ActiveDiv.ToLower() ? "active" : "";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 33
The updated Index() action method
of the Home controller
public ViewResult Index(string activeConf = "all",
string activeDiv = "all")
{
var model = new TeamListViewModel {
ActiveConf = activeConf,
ActiveDiv = activeDiv,
Conferences = context.Conferences.ToList(),
Divisions = context.Divisions.ToList()
};
IQueryable<Team> query = context.Teams;
if (activeConf != "all")
query = query.Where(t =>
t.Conference.ConferenceID.ToLower() ==
activeConf.ToLower());
if (activeDiv != "all")
query = query.Where(t =>
t.Division.DivisionID.ToLower() ==
activeDiv.ToLower());
model.Teams = query.ToList();
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 34
The updated Home/Index view (part 1)
@model TeamListViewModel
@{
ViewData["Title"] = "NFL Teams"; // helper function not needed
}
<div class="row">
<div class="col-sm-3">
<h3>Conference</h3>
<div class="list-group">
@foreach (Conference conf in Model.Conferences) {
<a asp-action="Index"
asp-route-conference="@conf.ConferenceID"
asp-route-division="@Model.ActiveDiv"
class="list-group-item
@Model.CheckActiveConf(conf.ConferenceID)">
@conf.Name</a>
}
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 35
The updated Home/Index view (part 2)
<h3>Division</h3>
<div class="list-group">
@foreach (Division div in Model.Divisions) {
<a asp-action="Index"
asp-route-conference="@Model.ActiveConf"
asp-route-division="@div.DivisionID"
class="list-group-item
@Model.CheckActiveDiv(div.DivisionID)">
@div.Name</a>
}
</div>
</div>
<div class="col-sm-9">
<ul class="list-inline">
@foreach (Team team in Model.Teams) {
<!— same as figure 8-9 -->
}
</ul>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 36
Two of the HTTP status codes for redirection
302 Found
301 Moved Permanently

The ActionResult subtypes for redirection


301 Moved Permanently
Subtype 302 Found method method
RedirectResult Redirect() RedirectPermanent()
LocalRedirectResult LocalRedirect() LocalRedirectPermanent()
RedirectToActionResult RedirectToAction() RedirectToActionPermanent()
RedirectToRouteResult RedirectToRoute() RedirectToRoutePermanent()

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 37
How to know which subtype to use for redirection
Subtype Use when…
RedirectResult Redirecting to an external URL, such as
https://google.com.
LocalRedirectResult Making sure you redirect to a URL
within the current app.
RedirectToActionResult Redirecting to an action method within
the current app.
RedirectToRouteResult Redirecting within the current app by
using a named route.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 38
Some of the overloads available
for the RedirectToAction() method
Arguments Redirect to…
(a) The specified action method in the current
controller.
(a, c) The specified action method in the
specified controller.
(a, routes) The specified action method in the current
controller with route parameters.
(a, c, routes) The specified action method in the
specified controller with route parameters.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 39
Code that redirects to another action method
(part 1)
The List() action method in the current controller
public RedirectToActionResult Index() =>
RedirectToAction("List");

The List() action method in the Team controller


public RedirectToActionResult Index() =>
RedirectToAction("List", "Team");

The Details() action method in the current controller


with a parameter
public RedirectToActionResult Index(string id) =>
RedirectToAction("Details", new { ID = id });

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 40
Code that redirects to another action method
(part 2)
Use a shortcut when variable name
and route segment name match
public RedirectToActionResult Index(string id) =>
RedirectToAction("Details", new { id });

Use a string-string dictionary to supply a parameter


public RedirectToActionResult Index(string id) =>
RedirectToAction("Details",
new Dictionary<string, string>(){ { "ID", id } }
);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 41
A browser message that’s displayed when you
refresh a page displayed by a POST request

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 42
Action methods that use the PRG pattern (part 1)
A Delete() action method for a POST request
[HttpPost]
// Post
public IActionResult Delete(Movie movie)
{
context.Movies.Remove(movie);
context.SaveChanges();
return RedirectToAction("Index", "Home");
// Redirect
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 43
Action methods that use the PRG pattern (part 2)
The Index() action method of the Home controller
for a GET request
[HttpGet]
// Get
public IActionResult Index()
{
var movies = context.Movies
.Include(m => m.Genre)
.OrderBy(m => m.Name)
.ToList();
return View(movies);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 44
An action method that uses TempData
with the PRG pattern
[HttpPost]
public IActionResult Delete(Movie movie)
{
context.Movies.Remove(movie);
context.SaveChanges();
TempData["message"] =
$"{movie.Name} deleted from database.";
return RedirectToAction("Index", "Home");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 45
Code that reads a TempData value
in a Layout view
...
<header class="jumbotron">
<h1>My Movies</h1>
</header>...
@if (TempData.Keys.Contains("message"))
{
<h4 class="bg-info text-center text-white p-2">
@TempData["message"]
</h4>
}
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 46
The temporary message displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 47
Two methods of the TempDataDictionary class
Keep()
Keep(key)
Peek(key)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 48
The overloaded Details() action method
of the Home controller (part 1)
[HttpPost]
public RedirectToActionResult Details(TeamViewModel model)
{
Utility.LogTeamClick(model.Team.TeamID);
TempData["ActiveConf"] = model.ActiveConf;
TempData["ActiveDiv"] = model.ActiveDiv;
return RedirectToAction("Details",
new { ID = model.Team.TeamID });
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 49
The overloaded Details() action method
of the Home controller (part 2)
[HttpGet]
public ViewResult Details(string id)
{
Utility.LogTeamClick(model.Team.TeamID);

var model = new TeamViewModel {


Team = context.Teams
.Include(t => t.Conference)
.Include(t => t.Division)
.FirstOrDefault(t => t.TeamID == id),
ActiveConf = TempData.Peek("ActiveConf").ToString(),
ActiveDiv = TempData.Peek("ActiveDiv").ToString()
};
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 50
When to use the Keep() and Peek() methods
 Use Peek() when you know you want the value to stay marked as
unread.
 Use a normal read and Keep() when you want to use a condition
to determine whether to mark the value as unread.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 51
The Home page after selecting a conference
and division

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 52
The Details page after clicking on a team

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 53
The Home page after clicking
on the “Return to Home Page” link

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 54
The TeamViewModel class
public class TeamViewModel
{
public Team Team { get; set; }
public string ActiveConf { get; set; } = "all";
public string ActiveDiv { get; set; } = "all";
}

The updated TeamListViewModel class


public class TeamListViewModel : TeamViewModel
{
// Team, ActiveConf, and ActiveDiv properties are inherited
// Teams, Conferences, and Divisions properties same as
// figure 8-10
// CheckActiveConf() and CheckActiveDiv() methods same as
// figure 8-10
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 55
The Details() action method
of the Home controller (part 1)
[HttpPost]
public RedirectToActionResult Details(TeamViewModel model)
{
Utility.LogTeamClick(model.Team.TeamID);

TempData["ActiveConf"] = model.ActiveConf;
TempData["ActiveDiv"] = model.ActiveDiv;
return RedirectToAction("Details",
new { ID = model.Team.TeamID });
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 56
The Details() action method
of the Home controller (part 2)
[HttpGet]
public ViewResult Details(string id)
{
var model = new TeamViewModel
{
Team = context.Teams
.Include(t => t.Conference)
.Include(t => t.Division)
.FirstOrDefault(t => t.TeamID == id),
ActiveDiv = TempData?["ActiveDiv"]?.ToString() ?? "all",
ActiveConf = TempData?["ActiveConf"]?.ToString() ?? "all"
};
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 57
The Home/Index view (NFL Teams 2.0) (part 1)
@model TeamListViewModel
@{
ViewData["Title"] = "NFL Teams";
}

<div class="row">
<div class="col-sm-3">
<!-- Conference and Division ul elements -->
</div>
<div class="col-sm-9">
<ul class="list-inline">
@foreach (Team team in Model.Teams)
{
<li class="list-inline-item">
<form asp-action="Details" method="post">
<button class="bg-white border-0“
type="submit">
<img src="~/images/@team.LogoImage"
alt="@team.Name"
title="@team.Name |
@team.Conference.Name
@team.Division.Name" />
</button>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 58
The Home/Index view (NFL Teams 2.0) (part 2)
<input type="hidden" asp-for="@team.TeamID" />
<input type="hidden" asp-for="ActiveConf" />
<input type="hidden" asp-for="ActiveDiv" />
</form>
</li>
}
</ul>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 59
The Home/Details view (part 1)
@model TeamViewModel
@{
ViewData["Title"] = "Team Details";
}

<h2>Team Details</h2>

<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<ul class="list-group text-center">
<li class="list-group-item">
<img src="~/images/@Model.Team.LogoImage" alt="" />
<h3>@Model.Team.Name</h3>
</li>
<li class="list-group-item">
<h4>Conference: @Model.Team.Conference.Name</h4>
</li>
<li class="list-group-item">
<h4>Division: @Model.Team.Division.Name</h4>
</li>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 60
The Home/Details view (part 2)
<li class="list-group-item">
<a asp-action="Index"
asp-route-conference="@Model.ActiveConf"
asp-route-division="@Model.ActiveDiv">
Return to Home Page
</a>
</li>
</ul>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C8, Slide 61
Chapter 9

How to work with


session state
and cookies

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 1
Objectives (part 1)
Applied
1. Use session state to store user data that should persist across
multiple requests in a session.
2. Use cookies to store user data that should persist across multiple
sessions.

Knowledge
1. List the six ways that you can handle state with ASP.NET Core
MVC.
2. Describe how session state works with the focus on the session ID.
3. Describe how to enable session state for an ASP.NET Core MVC
web app, including how to change its default settings.
4. Describe the use of session state in controllers and views.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 2
Objectives (part 2)
5. Describe how to use JSON to store .NET objects in session state.
6. Describe how adding extension methods to the ISession interface
can make it easier to store objects in session state.
7. Describe how a wrapper class can make it easier to work with
session state.
8. Distinguish between a session cookie and a persistent cookie.
9. Describe the use of cookies, including how to set a persistent
cookie.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 3
Common web programming features
for maintaining state
 Hidden field
 Query string
 Cookie
 Session state

Two ASP.NET Core MVC features


for maintaining state
 Routes
 TempData

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 4
How ASP.NET Core MVC keeps track of a session

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 5
How to configure an app to use session state
(part 1)
The ConfigureServices() method in the Startup.cs file
public void ConfigureServices(IServiceCollection services)
{
...
// must be called before AddControllersWithViews()
services.AddMemoryCache();
services.AddSession();

services.AddControllersWithViews();
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 6
How to configure an app to use session state
(part 2)
The Configure() method in the Startup.cs file
public void Configure(IAppBuilder app, IHostingEnvironment env)
{
...

// must be called before UseEndpoints()


app.UseSession();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 7
How to change the default session state settings
services.AddSession(options =>
{
// change idle timeout to 5 minutes - default is 20 minutes
options.IdleTimeout = TimeSpan.FromSeconds(60 * 5);
options.Cookie.HttpOnly = false; // default is true
options.Cookie.IsEssential = true; // default is false
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 8
Methods of the ISession interface that set, get,
and remove items
SetInt32(key, value)
SetString(key, value)
GetInt32(key)
GetString(key)
Remove(key)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 9
A using directive for session state in a controller
using Microsoft.AspNetCore.Http;

An action method that gets and sets


a session state value
public ViewResult Index() {
int num = HttpContext.Session.GetInt32("num");
num += 1;
HttpContext.Session.SetInt32("num", num);
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 10
A using directive for session state in a view
@using Microsoft.AspNetCore.Http

A Razor code block that gets


a session state value
@{
int num = Context.Session.GetInt32("num");
}

A Razor expression that gets


a session state value
<div>@Context.Session.GetInt32("num")</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 11
How to add the Newtonsoft JSON NuGet package
1. Use the ToolsNuGet Package ManagerManage NuGet
Packages for Solution command to open the NuGet Package
Manager.
2. Click the Browse link.
3. Type “Microsoft.AspNetCore.Mvc.NewtonsoftJson” in the
search box.
4. Click on the appropriate package from the list that appears in the
left-hand panel.
5. In the right-hand panel, check the project name, select the
version that matches the version of .NET Core you’re running,
and click Install.
6. Review the Preview Changes dialog that comes up and click OK.
7. Review the License Acceptance dialog that comes up and click I
Accept.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 12
How to configure your app
to use the Newtonsoft JSON library
public void ConfigureServices(IServiceCollection services) {
...
services.AddMemoryCache();
services.AddSession();

services.AddControllersWithViews().AddNewtonsoftJson();
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 13
How to import the Newtonsoft JSON library
using Newtonsoft.Json;

Two static methods of the JsonConvert class


SerializeObject(object)
DeserializeObject<T>(string)

Code that sets a Team object in session state


Team team =
new Team { TeamID = "sea", Name = "Seattle Seahawks"
};
string teamJson = JsonConvert.SerializeObject(team);
HttpContext.Session.SetString("team", teamJSON);

Code that gets a Team object from session state


string teamJson = HttpContext.Session.GetString("team");
Team team =
JsonConvert.DeserializeObject<Team>(teamJson);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 14
Two extension methods for the ISession interface
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
...
public static class SessionExtensions
{
public static void SetObject<T>(this ISession session,
string key, T value) {
session.SetString(key,
JsonConvert.SerializeObject(value));
}

public static T GetObject<T>(this ISession session,


string key) {
var valueJson = session.GetString(key);
if (string.IsNullOrEmpty(value)) {
return default(T);
}
else {
return JsonConvert.DeserializeObject<T>(valueJson);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 15
Code that uses the extension methods
to work with a single team
In a controller
var team =
HttpContext.Session.GetObject<Team>("team") ?? new Team();
team.Name = "Seattle Seahawks";
HttpContext.Session.SetObject("team", team);

In a view
@{
var team = Context.Session.GetObject<Team>("team");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 16
Code that uses the extension methods
to work with a list of teams
In a controller
var teams = HttpContext.Session.GetObject<List<Team>>("teams") ??
new List<Team>();
teams.Add(new Team { TeamID = "gb",
Name = "Green Bay Packers" });
HttpContext.Session.SetObject("teams", teams);

In a view
@{
var teams = Context.Session.GetObject<List<Team>>("teams");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 17
A wrapper class that encapsulates the code
for working with session state
using Microsoft.AspNetCore.Http;
...
public class MySession
{
private const string TeamsKey = "teams";

private ISession session { get; set; }


public MySession(ISession sess) {
session = sess;
}

public List<Team> GetTeams() =>


session.GetObject<List<Team>>(TeamsKey) ??
new List<Team>();

public void SetTeams(List<Team> teams) =>


session.SetObject(TeamsKey, teams);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 18
Code that uses the wrapper class to work
with a list of teams
In a controller
var session = new MySession(HttpContext.Session);
var teams = session.GetTeams();
teams.Add(new Team { TeamID = "gb",
Name = "Green Bay Packers" });
session.SetTeams(teams);

In a view
@{
var session = new MySession(Context.Session);
var teams = session.GetTeams();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 19
Two buttons on the Details page (NFL Teams 3.0)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 20
The Home page after a team has been added
to favorites (NFL Teams 3.0)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 21
The Favorites page (NFL Teams 3.0)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 22
The SessionExtensions class
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
...
public static class SessionExtensions
{
public static void SetObject<T>(this ISession session,
string key, T value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}

public static T GetObject<T>(this ISession session, string key)


{
var value = session.GetString(key);
return (value == null) ? default(T) :
JsonConvert.DeserializeObject<T>(value);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 23
The NFLSession class (part 1)
using Microsoft.AspNetCore.Http;
...
public class NFLSession
{
private const string TeamsKey = "myteams";
private const string CountKey = "teamcount";
private const string ConfKey = "conf";
private const string DivKey = "div";

private ISession session { get; set; }


public NFLSession(ISession session) {
this.session = session;
}

public void SetMyTeams(List<Team> teams) {


session.SetObject(TeamsKey, teams);
session.SetInt32(CountKey, teams.Count);
}

public List<Team> GetMyTeams() =>


session.GetObject<List<Team>>(TeamsKey) ?? new List<Team>();
public int GetMyTeamCount() => session.GetInt32(CountKey) ?? 0;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 24
The NFLSession class (part 2)
public void SetActiveConf(string activeConf) =>
session.SetString(ConfKey, activeConf);
public string GetActiveConf() => session.GetString(ConfKey);

public void SetActiveDiv(string activeDiv) =>


session.SetString(DivKey, activeDiv);
public string GetActiveDiv() => session.GetString(DivKey);

public void RemoveMyTeams() {


session.Remove(TeamsKey);
session.Remove(CountKey);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 25
The updated Index() action method
of the Home controller
public ViewResult Index(string activeConf = "all",
string activeDiv = "all")
{
var session = new NFLSession(HttpContext.Session);
session.SetActiveConf(activeConf);
session.SetActiveDiv(activeDiv);

// rest of code same as figure 8-11


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 26
The updated Details() action method
of the Home controller
public ViewResult Details(string id)
{
var session = new NFLSession(HttpContext.Session);
var model = new TeamViewModel
{
Team = context.Teams
.Include(t => t.Conference)
.Include(t => t.Division)
.FirstOrDefault(t => t.TeamID == id),
ActiveDiv = session.GetActiveDiv(),
ActiveConf = session.GetActiveConf()
};
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 27
The Add() action method of the Home controller
(NFL Teams 3.0)
[HttpPost]
public RedirectToActionResult Add(TeamViewModel model) {
model.Team = context.Teams
.Include(t => t.Conference)
.Include(t => t.Division)
.Where(t => t.TeamID == model.Team.TeamID)
.FirstOrDefault();

var session = new NFLSession(HttpContext.Session);


var teams = session.GetMyTeams();
teams.Add(model.Team);
session.SetMyTeams(teams);

TempData["message"] =
$"{model.Team.Name} added to your favorites";

return RedirectToAction("Index",
new {
ActiveConf = session.GetActiveConf(),
ActiveDiv = session.GetActiveDiv()
});
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 28
The layout (part 1)
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/lib/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
<title>@ViewBag.Title</title>
</head>
<body>
<div class="container">
<header class="text-center text-white">
<h1 class="bg-primary mt-3 p-3">NFL Teams</h1>

@* show any message in TempData *@


@if (TempData["message"] != null)
{
<h4 class="bg-success p-2">@TempData["message"]</h4>
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 29
The layout (part 2)
@* show link to Favorites page unless on page *@
@if (!ViewContext.View.Path.Contains("Favorites"))
{
var session = new NFLSession(Context.Session);
<h5 class="bg-info p-2">
<a asp-controller="Favorites"
class="text-white">My Favorite Teams</a>

@* display number of fav teams in badge *@


<span class="badge badge-light">
@(session.GetMyTeamCount())</span>
</h5>
}
</header>
<main>
@RenderBody()
</main>
</div>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 30
Some code from the Home/Index view
<div class="col-sm-9">
<ul class="list-inline">
@foreach (Team team in Model.Teams)
{
<li class="list-inline-item">
<a asp-action="Details" asp-route-id="@team.TeamID">
<img src="~/images/@team.LogoImage"
alt="@team.Name"
title="@team.Name | @team.Conference.Name
@team.Division.Name" />
</a>
</li>
}
</ul>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 31
Some code from the Home/Details view
<li class="list-group-item">
<form asp-action="Add" method="post">
<a asp-action="Index" class="btn btn-primary"
asp-route-activeConf="@Model.ActiveConf"
asp-route-activeDiv="@Model.ActiveDiv">
Return to Home Page</a>
<button type="submit" class="btn btn-primary">
Add to Favorites
</button>
<input type="hidden" asp-for="Team.TeamID" />
</form>
</li>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 32
The Favorites controller (part 1)
public class FavoritesController : Controller
{
[HttpGet]
public ViewResult Index()
{
var session = new NFLSession(HttpContext.Session);
var model = new TeamListViewModel
{
ActiveConf = session.GetActiveConf(),
ActiveDiv = session.GetActiveDiv(),
Teams = session.GetMyTeams()
};
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 33
The Favorites controller (part 2)
[HttpPost]
public RedirectToActionResult Delete()
{
var session = new NFLSession(HttpContext.Session);
session.RemoveMyTeams();

TempData["message"] = "Favorite teams cleared";

return RedirectToAction("Index", "Home",


new {
ActiveConf = session.GetActiveConf(),
ActiveDiv = session.GetActiveDiv()
});
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 34
The Favorites/Index view (part 1)
@model TeamListViewModel
@{
ViewData["Title"] = "Favorites";
}
<div class="text-center">
<h2>My Favorites</h2>
<form asp-action="Delete" method="post">
<a asp-action="Index" asp-controller="Home"
class="btn btn-primary"
asp-route-activeConf="@Model.ActiveConf"
asp-route-activeDiv="@Model.ActiveDiv">
Back to Home Page</a>
<button type="submit" class="btn btn-primary">
Clear Favorites
</button>
</form>
<br />
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 35
The Favorites/Index view (part 2)
<div class="row">
<div class="col-8 offset-2">
<ul class="list-group">
@foreach (Team team in Model.Teams)
{
<li class="list-group-item">
<div class="row">
<div class="col-sm-4">
<img src=~/images/@team.LogoImage"
alt="" />
</div>
<div class="col-sm-4">
@team.Name
</div>
<div class="col-sm-4">
@team.Conference.Name
@team.Division.Name
</div>
</div>
</li>
}
</ul>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 36
Two properties of the Controller class
Request
Response

Code that sets a session cookie


Response.Cookies.Append("username", "Grace");

Code that deletes a cookie


Response.Cookies.Delete("username");

Code that gets a cookie


string value = Request.Cookies["username"];

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 37
Properties of the CookieOptions class
Domain
Expires
Path
MaxAge
SameSite
Secure

A using directive for working


with the CookieOptions class
using Microsoft.AspNetCore.Http;

Code that sets a persistent cookie


var options = new CookieOptions {
Expires = DateTime.Now.AddDays(30)
};
Response.Cookies.Append("username", "Grace", options);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 38
The NFLCookies class (part 1)
using Microsoft.AspNetCore.Http;
...
public class NFLCookies
{
private const string MyTeams = "myteams";
private const string Delimiter = "-";

private IRequestCookieCollection requestCookies { get; set; }


private IResponseCookies responseCookies { get; set; }

public NFLCookies(IRequestCookieCollection cookies) {


requestCookies = cookies;
}
public NFLCookies(IResponseCookies cookies) {
responseCookies = cookies;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 39
The NFLCookies class (part 2)
public void SetMyTeamIds(List<Team> myteams){
List<string> ids = myteams.Select(t => t.TeamID).ToList();
string idsString = String.Join(Delimiter, ids);
CookieOptions options = new CookieOptions {
Expires = DateTime.Now.AddDays(30)
};
RemoveMyTeamIds(); // delete old cookie first
responseCookies.Append(MyTeams, idsString, options);
}

public string[] GetMyTeamsIds() {


string cookie = requestCookies[MyTeams];
if (string.IsNullOrEmpty(cookie))
return new string[] { }; // empty string array
else
return cookie.Split(Delimiter);
}

public void RemoveMyTeamIds(){


responseCookies.Delete(MyTeams);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 40
The updated NFLSession class
public class NFLSession
{
...
public int? GetMyTeamCount() =>
session.GetInt32(CountKey);
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 41
The Index() action method of the Home controller
public ViewResult Index(string activeConf = "all",
string activeDiv = "all")
{
var session = new NFLSession(HttpContext.Session);
session.SetActiveConf(activeConf);
session.SetActiveDiv(activeDiv);

// if no count in session, get cookie and restore fave teams


int? count = session.GetMyTeamCount();
if (count == null) {
var cookies = new NFLCookies(Request.Cookies);
string[] ids = cookies.GetMyTeamIds();

List<Team> myteams = new List<Team>();


if (ids.Length > 0) {
myteams = context.Teams.Include(t => t.Conference)
.Include(t => t.Division)
.Where(t => ids.Contains(t.TeamID)).ToList();
}
session.SetMyTeams(myteams);
}
// rest of code same as figure 8-11
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 42
The Add() action method of the Home controller
(NFL Teams 4.0)
[HttpPost]
public RedirectToActionResult Add(TeamViewModel model) {
model.Team = context.Teams
.Include(t => t.Conference).Include(t => t.Division)
.Where(t => t.TeamID == model.Team.TeamID)
.FirstOrDefault();

var session = new NFLSession(HttpContext.Session);


var teams = session.GetMyTeams();
teams.Add(model.Team);
session.SetMyTeams(teams);

var cookies = new NFLCookies(Response.Cookies);


cookies.SetMyTeamIds(teams);

TempData["message"] =
$"{model.Team.Name} added to your favorites";

return RedirectToAction("Index",
new { ActiveConf = session.GetActiveConf(),
ActiveDiv = session.GetActiveDiv() });
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 43
The Delete() action method
of the Favorites controller
[HttpPost]
public RedirectToActionResult Delete()
{
var session = new NFLSession(HttpContext.Session);
var cookies = new NFLCookies(Response.Cookies);

session.RemoveMyTeams();
cookies.RemoveMyTeamIds();

TempData["message"] = "Favorite teams cleared";

return RedirectToAction("Index", "Home",


new {
ActiveConf = session.GetActiveConf(),
ActiveDiv = session.GetActiveDiv()
}
);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C9, Slide 44
Chapter 10

How to work with


model binding

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 1
Objectives (part 1)
Applied
1. Use model binding with primitive and complex types.

Knowledge
1. Describe how to use controller properties to retrieve primitive
types from GET and POST requests.
2. List the order of places where MVC looks for data when it’s
binding a parameter.
3. Describe how to use model binding to retrieve primitive types from
GET and POST requests.
4. Describe how to use model binding to retrieve complex types from
POST requests.
5. Describe how to use the name and value attributes of a submit
button to POST data.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 2
Objectives (part 2)
6. Describe how to post an array to an action method.
7. Describe the use of attributes to control the source of bound values.
8. Describe the use of attributes to control which properties are set
during model binding.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 3
Two properties of the Controller class
Request
RouteData

Two properties of the Request property


Query
Form

One property of the RouteData property


Values

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 4
A URL with a query string parameter
https://localhost:5001/Home/Index?page=2

An action method that retrieves the value


of the query string parameter
public IActionResult Index() {
ViewBag.Page = Request.Query["page"];
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 5
Form data in the body of a POST request
firstname=Grace

An action method that retrieves the form data


public IActionResult Index() {
ViewBag.FirstName = Request.Form["firstname"];
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 6
A URL with a value for the id parameter
of the default route
https://localhost:5001/Home/Index/all

An action method that retrieves the value


of the route parameter named id
public IActionResult Index() {
ViewBag.Id = RouteData.Values["id"];
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 7
An action method that uses model binding
to get a string value
public IActionResult Index(string id)
{
ViewBag.Id = id;
return View();
}

Three types of data this method can retrieve


A form parameter in the body of a POST request
id=2

A route parameter
https://localhost:5001/Home/Index/2

A query string parameter


https://localhost:5001/Home/Index?id=2

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 8
The order in which MVC looks for data
to bind to a parameter
1. The body of the POST request.
2. The route values in the URL.
3. The query string parameters in the URL.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 9
The benefits of model binding
 You don’t have to write repetitive code to retrieve values.
 MVC automatically casts the value to match the data type of the
action method parameter.
 Model binding is not case sensitive.
 You can change how you pass data to an action without having to
change its code.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 10
An action method and view that bind
to primitive types
The action method
[HttpPost]
public IActionResult Add(string description,
DateTime dueDate){
ToDo task = new ToDo {
Description = description,
DueDate = duedate
};
// rest of code
}

The view
<form asp-action="Add" method="post">
<label for="description">Description:</label>
<input type="text" name="description">
<label for="duedate">Due Date:</label>
<input type="text" name="duedate">
<button type="submit">Add</button>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 11
The class for a complex type
public class ToDo
{
public int Id { get; set; }
public string Description { get; set; }
public DateTime? DueDate { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 12
An action method and view that bind
to a complex type
The action method
[HttpPost]
public IActionResult Add(ToDo task){
// rest of code
}

The view
@model ToDo
...
<form asp-action="Add" method="post">
<label asp-for="Description">Description:</label>
<input asp-for="Description">
<label asp-for="DueDate">Due Date:</label>
<input type="text" asp-for="DueDate">
<button type="submit">Add</button>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 13
The custom route in the NFL Teams app
endpoints.MapControllerRoute(
name: "custom",
pattern:
"{controller=Home}/{action=Index}/conf/{activeConf}/div/{activeDiv}");

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 14
A Home/Index() action method that binds
to primitive types
public IActionResult Index(string activeConf = "all",
string activeDiv = "all")
{
var model = new TeamListViewModel
{
ActiveConf = activeConf,
ActiveDiv = activeDiv,
Conferences = context.Conferences.ToList(),
Divisions = context.Divisions.ToList()
};

IQueryable<Team> query = context.Teams;


// conditional where clauses based
// on active conference and division
model.Teams = query.ToList();

return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 15
The updated base class
for the TeamListViewModel class
public class TeamViewModel
{
public Team Team { get; set; }
public string ActiveConf { get; set; } = "all";
public string ActiveDiv { get; set; } = "all";
}

A Home/Index() action method that binds


to a complex type
public IActionResult Index(TeamListViewModel model)
{
model.Conferences = context.Conferences.ToList();
model.Divisions = context.Divisions.ToList();

IQueryable<Team> query = context.Teams;


// conditional where clauses based
// on active conference and division
model.Teams = query.ToList();

return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 16
An action method
[HttpPost]
public IActionResult Add(Team team)
{
// action method code
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 17
An <input> element for a button
that posts a team ID to the action method
@model Team;
...
<form asp-action="Add" method="post">
<input type="submit" asp-for="TeamID"
class="btn btn-primary" />
</form>

How the button looks in the browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 18
A <button> element for a button
that posts a team ID to the action method
@model Team;
...
<form asp-action="Add" method="post">
<button type="submit" name="TeamID"
value="@Model.TeamID"
class="btn btn-primary">@Model.Name</button>
</form>

How the button looks in the browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 19
The <input> element…
 Allows you to use the asp-for tag helper to generate the name and
value attributes.
 Automatically displays the value attribute as the text for the
button.

The <button> element…


 Gives you control over the text (or image) that’s displayed on the
button.
 Requires you to manually code the name and value attributes.
 Requires you to make sure the value in the name attribute
matches the name of the action method parameter or property.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 20
An action method that binds to a model
[HttpPost]
public IActionResult Details(TeamViewModel model)
{
TempData["ActiveConf"] = model.ActiveConf;
TempData["ActiveDiv"] = model.ActiveDiv;
return RedirectToAction("Details",
new { ID = model.Team.TeamID });
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 21
Part of a view that uses a hidden field
to post the team ID
<ul class="list-inline">
@foreach (Team team in Model.Teams)
{
<li class="list-inline-item">
<form asp-action="Details" method="post">
<button type="submit">
<img src="~/images/@team.LogoImage"
alt="@team.Name"
title="@team.Name |
@team.Conference.Name
@team.Division.Name" />
</button>
<input type="hidden" asp-for="@team.TeamID" />
<input type="hidden" asp-for="ActiveConf" />
<input type="hidden" asp-for="ActiveDiv" />
</form>
</li>
}
</ul>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 22
Part of a view that uses a submit button
to post the team ID
<form asp-action="Details" method="post">
<input type="hidden" asp-for="ActiveConf" />
<input type="hidden" asp-for="ActiveDiv" />
<ul class="list-inline">
@foreach (Team team in Model.Teams)
{
<li class="list-inline-item">
<button type="submit"
name="Team.TeamID"
value="@team.TeamID">
<img src="~/images/@team.LogoImage"
alt="@team.Name"
title="@team.Name |
@team.Conference.Name
@team.Division.Name" />
</button>
</li>
}
</ul>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 23
An action method that accepts a string array
[HttpPost]
public IActionResult Filter(string[] filter)
{
// code that does the filtering
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 24
A view that posts a string array to the method
<h3>Filter By:</h3>
<form asp-action="Filter" method="post">
<!-- make sure each select element has the same name
and that it matches the action method parameter name -->
<label>Price</label>
<select name="filter">
<option value="all">All</option>
<option value="lt10">Under $10</option>
<option value="10to50">$10 to $50</option>
<option value="gt50">Over $50</option>
</select>
<label>Color</label>
<select name="filter">
<option>All</option>
<option>Red</option>
<option>Blue</option>
<option>Yellow</option>
<option>Green</option>
<option>Purple</option>
</select>
<button type="submit">Filter</button>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 25
The string array the action method receives

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 26
Some of the attributes that specify the source
of the value to be bound
[FromForm]
[FromRoute]
[FromQuery]
[FromHeader]
[FromServices]
[FromBody]

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 27
An action method that specifies the source
of its parameters
public IActionResult Index([FromRoute] string id,
[FromQuery] int pagenum)
{
ViewBag.Id = id;
ViewBag.Page = pagenum;
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 28
An action method that passes an argument
to an attribute
public IActionResult Index(
[FromHeader(Name = "User-Agent")] string agent)
{
ViewBag.UserAgent = agent;
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 29
A class that applies an attribute to a property
public class Browser
{
[FromHeader(Name = "User-Agent")]
public string UserAgent { get; set; }
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 30
Attributes that determine which values are bound
[Bind(names)]
[BindNever]

The namespaces of the attributes


Attribute Namespace
[Bind] Microsoft.AspNetCore.MVC
[BindNever] Microsoft.AspNetCore.MVC.ModelBinding

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 31
The Employee class
public class Employee {
public string Name { get; set; }
public string JobTitle { get; set; }
public bool IsManager { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 32
Three ways to make sure the IsManager property
is not bound (part 1)
With the Bind attribute in the parameter list
of an action method
[HttpPost]
public IActionResult Index(
[Bind("Name", "JobTitle")] Employee employee) {
if (ModelState.IsValid) {
if (employee.JobTitle == "Boss")
employee.IsManager = true; // can be set in code
}
return View(employee);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 33
Three ways to make sure the IsManager property
is not bound (part 2)
With the Bind attribute on the class
[Bind("Name", "JobTitle")]
public class Employee {
public string Name { get; set; }
public string JobTitle { get; set; }
public bool IsManager { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 34
Three ways to make sure the IsManager property
is not bound (part 3)
With the BindNever attribute on the IsManager property
public class Employee {
public string Name { get; set; }
public string JobTitle { get; set; }

[BindNever]
public bool IsManager { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 35
The Home page of the ToDo List app
with no filtering

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 36
The Home page after it has been filtered
to show open work tasks

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 37
The Add page with a validation message

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 38
The Category class
public class Category
{
public string CategoryId { get; set; }
public string Name { get; set; }
}

The Status class


public class Status
{
public string StatusId { get; set; }
public string Name { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 39
The ToDo class
public class ToDo
{
public int Id { get; set; }

[Required(ErrorMessage = "Please enter a description.")]


public string Description { get; set; }

[Required(ErrorMessage = "Please enter a due date.")]


public DateTime? DueDate { get; set; }

[Required(ErrorMessage = "Please select a category.")]


public string CategoryId { get; set; }
public Category Category { get; set; }

[Required(ErrorMessage = "Please select a status.")]


public string StatusId { get; set; }
public Status Status { get; set; }

public bool Overdue =>


Status?.ToLower() == "open" && DueDate < DateTime.Today;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 40
The ToDoContext class
public class ToDoContext : DbContext
{
public ToDoContext(DbContextOptions<ToDoContext> options)
: base(options) { }

public DbSet<ToDo> ToDos { get; set; }


public DbSet<Category> Categories { get; set; }
public DbSet<Status> Statuses { get; set; }

protected override void OnModelCreating(


ModelBuilder modelBuilder) {
modelBuilder.Entity<Category>().HasData(
new Category { CategoryId = "work", Name = "Work" },
new Category { CategoryId = "home", Name = "Home" },
new Category { CategoryId = "ex", Name = "Exercise" },
new Category { CategoryId = "shop", Name = "Shopping" },
new Category { CategoryId = "call", Name = "Contact" }
);
modelBuilder.Entity<Status>().HasData(
new Status { StatusId = "open", Name = "Open" },
new Status { StatusId = "closed", Name = "Completed" }
);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 41
The Filters class (part 1)
public class Filters
{
public Filters(string filterstring)
{
FilterString = filterstring ?? "all-all-all";
string[] filters = FilterString.Split('-');
CategoryId = filters[0];
Due = filters[1];
StatusId = filters[2];
}
public string FilterString { get; }
public string CategoryId { get; }
public string Due { get; }
public string StatusId { get; }

public bool HasCategory => CategoryId.ToLower() != "all";


public bool HasDue => Due.ToLower() != "all";
public bool HasStatus => StatusId.ToLower() != "all";

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 42
The Filters class (part 2)
public static Dictionary<string, string> DueFilterValues =>
new Dictionary<string, string> {
{ "future", "Future" },
{ "past", "Past" },
{ "today", "Today" }
};
public bool IsPast => Due.ToLower() == "past";
public bool IsFuture => Due.ToLower() == "future";
public bool IsToday => Due.ToLower() == "today";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 43
The Home controller (part 1)
public class HomeController : Controller
{
private ToDoContext context;
public HomeController(ToDoContext ctx) => context = ctx;

public IActionResult Index(string id)


{
var filters = new Filters(id);
ViewBag.Filters = filters;
ViewBag.Categories = context.Categories.ToList();
ViewBag.Statuses = context.Statuses.ToList();
ViewBag.DueFilters = Filters.DueFilterValues();

IQueryable<ToDo> query = context.ToDos


.Include(t => t.Category).Include(t => t.Status);
if (filters.HasCategory) {
query = query.Where(t =>
t.CategoryId == filters.CategoryId);
}
if (filters.HasStatus) {
query = query.Where(t =>
t.StatusId == filters.StatusId);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 44
The Home controller (part 2)
if (filters.HasDue) {
var today = DateTime.Today;
if (filters.IsPast)
query = query.Where(t => t.DueDate < today);
else if (filters.IsFuture)
query = query.Where(t => t.DueDate > today);
else if (filters.IsToday)
query = query.Where(t => t.DueDate == today);
}
var tasks = query.OrderBy(t => t.DueDate).ToList();
return View(tasks);
}

[HttpGet]
public IActionResult Add()
{
ViewBag.Categories = context.Categories.ToList();
ViewBag.Statuses = context.Statuses.ToList();
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 45
The Home controller (part 3)
[HttpPost]
public IActionResult Add(ToDo task)
{
if (ModelState.IsValid){
context.ToDos.Add(task);
context.SaveChanges();
return RedirectToAction("Index");
}
else {
ViewBag.Categories = context.Categories.ToList();
ViewBag.Statuses = context.Statuses.ToList();
return View(task);
}
}
[HttpPost]
public IActionResult Filter(string[] filter)
{
string id = string.Join('-', filter);
return RedirectToAction("Index", new { ID = id });
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 46
The Home controller (part 4)
[HttpPost]
public IActionResult Edit([FromRoute] string id, ToDo selected)
{
if (selected.StatusId == null) {
context.ToDos.Remove(selected);
}
else {
string newStatusId = selected.StatusId;
selected = context.ToDos.Find(selected.Id);
selected.StatusId = newStatusId;
context.ToDos.Update(selected);
}
context.SaveChanges();

return RedirectToAction("Index", new { ID = id });


}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 47
The layout
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/lib/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
<title>My Tasks</title>
</head>
<body>
<div class="container">
<header class="bg-primary text-white text-center">
<h1 class="m-3 p-3">My Tasks</h1>
</header>
<main>
@RenderBody()
</main>
</div>
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 48
The Home/Index view (part 1)
@model IEnumerable<ToDo>
@{
string Overdue(ToDo task) => task.Overdue ? "bg-warning" : "";
}
<div class="row">
<div class="col-sm-2">
<form asp-action="Filter" method="post">
<div class="form-group">
<label>Category:</label>
<select name="filter" class="form-control"
asp-items="@(new SelectList(ViewBag.Categories,
"CategoryId", "Name",
ViewBag.Filters.CategoryId))">
<option value="all">All</option>
</select></div>
<div class="form-group">
<label>Due:</label>
<select name="filter" class="form-control"
asp-items="@(new SelectList(ViewBag.DueFilters,
"Key", "Value", ViewBag.Filters.Due))">
<option value="all">All</option>
</select></div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 49
The Home/Index view (part 2)
<div class="form-group">
<label>Status:</label>
<select name="filter" class="form-control"
asp-items="@(new SelectList(ViewBag.Statuses,
"StatusId", "Name",
ViewBag.Filters.StatusId))">
<option value="all">All</option>
</select>
</div>
<button type="submit" class="btn btn-primary">
Filter</button>
<a asp-action="Index" asp-route-id=""
class="btn btn-primary">Clear</a>
</form>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 50
The Home/Index view (part 3)
<div class="col-sm-10">

<a asp-action="Add"><b>Add new task</b></a>

<table class="table table-bordered table-striped mt-2">


<thead>
<tr>
<th>Description</th>
<th>Category</th>
<th>Due Date</th>
<th>Status</th>
<th class="w-25"></th>
</tr>
</thead>
<tbody>
@foreach (ToDo task in Model)
{
string overdue = Overdue(task);
<tr>
<td>@task.Description</td>
<td>@task.Category.Name</td>
<td class="@overdue">
@task.DueDate?.ToShortDateString()</td>
<td class="@overdue">@task.Status.Name</td>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 51
The Home/Index view (part 4)
<td>
<form asp-action="Edit" method="post"
asp-route-id="@ViewBag.Filters.FilterString"
class="mr-2">

<input type="hidden"
name="@nameof(ToDo.Id)" value="@task.Id" />

<button type="submit"
name="@nameof(ToDo.StatusId)" value="closed"
class="btn btn-primary btn-sm">Completed
</button>

<button type="submit"
class="btn btn-primary btn-sm">Delete
</button>

</form>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 52
The Home/Add view (part 1)
@model ToDo

<h2>New Task</h2>

<div asp-validation-summary="All" class="text-danger">


</div>

<form asp-action="Add" method="post">


<div class="form-group">
<label asp-for="Description">Description:</label>
<input asp-for="Description" class="form-control">
</div>

<div class="form-group">
<label asp-for="CategoryId">Category:</label>
<select asp-for="CategoryId" class="form-control"
asp-items="@(new SelectList(ViewBag.Categories,
"CategoryId", "Name"))">
<option value=""></option>
</select>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 53
The Home/Add view (part 2)
<div class="form-group">
<label asp-for="DueDate">Due Date:</label>
<input type="text" asp-for="DueDate"
class="form-control">
</div>

<div class="form-group">
<label asp-for="StatusId">Status:</label>
<select asp-for="StatusId" class="form-control"
asp-items="@(new SelectList(ViewBag.Statuses,
"StatusId", "Name"))">
<option value=""></option>
</select>
</div>

<button type="submit" class="btn btn-primary">


Add</button>
<a asp-action="Index" class="btn btn-primary">
Cancel</a>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C10, Slide 54
Chapter 11

How to
validate data

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 1
Objectives (part 1)
Applied
1. Given the data validation requirements for a web form, use any of
the validation techniques presented in this chapter to implement
that validation.

Knowledge
1. Describe the default data validation provided by model binding.
2. Describe the use of attributes for data validation.
3. List six data validation attributes.
4. Describe the use of tag helpers for data validation.
5. Describe the use of the IsValid property of a controller’s
ModelState property.
6. Describe the use of CSS to format validation messages.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 2
Objectives (part 2)
7. Describe the use of a controller’s ModelState property for
checking the validation state of a control and setting a custom
error message.
8. Describe the use of model-level and property-level validation
messages.
9. Describe the use of unobtrusive client-side data validation.
10. Distinguish between client-side validation and server-side
validation.
11. Describe the process of customizing server-side data validation.
12. Describe the process of implementing remote validation.
13. Describe the process of customizing client-side data validation.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 3
The Movie class
public class Movie
{
public string Name { get; set; }
public int Rating { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 4
A strongly-typed view that posts a Movie object
to an action method
@model Movie
...
<div asp-validation-summary="All" class="text-danger">
</div>

<form asp-action="Add" method="post" class="form-inline">


<div class="form-group">
<label>Name:</label>&nbsp;
<input asp-for="Name" class="form-control" />
</div>&nbsp;
<div class="form-group">
<label>Rating (1 - 5):</label>&nbsp;
<input type="text" asp-for="Rating"
class="form-control" />
</div>&nbsp;
<button type="submit" class="btn btn-primary">Submit
</button>
</form>
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 5
An action method that receives a Movie object
from the view
[HttpPost]
public IActionResult Add(Movie movie)
{
if (ModelState.IsValid) {
/* code to add movie goes here */
return RedirectToAction("List", "Movie");
}
else {
return View(movie);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 6
How it looks in the browser when the rating
can’t be cast to an int

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 7
Common data attributes for validation
Required
Range(min, max)
Range(type, min, max)
StringLength(length)
RegularExpression(ex)
Compare(other)
Display(Name = "n")

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 8
The Movie entity class with data attributes
using System.ComponentModel.DataAnnotations;
...
public class Movie
{
[Required]
[StringLength(30)]
public string Name { get; set; }

[Range(1, 5)]
public int Rating { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 9
The view after the user submits invalid data

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 10
The default validation messages
Attribute Message
Required The field [Name] is required.
Range The field [Name] must be between
[minimum] and [maximum].
StringLength The field [Name] must be a string
with a maximum length of [length].
RegularExpression The field [Name] must match the
regular expression '[pattern]'.
Compare '[Name]' and '[OtherName]' do not
match.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 11
How to replace the default message
with a custom message
[Required(ErrorMessage = "Please enter a name")]
[StringLength(30,
ErrorMessage = "Name must be 30 characters or less.")]
public string Name { get; set; }

[Range(1, 5,
ErrorMessage = "Please enter a rating between 1 and 5.")]
public int Rating { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 12
The Customer class
public class Customer {
[Required(ErrorMessage = "Please enter a name.")]
[RegularExpression("^[a-zA-Z0-9]+$",
ErrorMessage = "Name may not contain special characters.")]
public string Name { get; set; }

[Required(ErrorMessage = "Please enter a date of birth.")]


[Range(typeof(DateTime), "1/1/1900", "12/31/9999",
ErrorMessage = "Date of birth must be after 1/1/1900.")]
public DateTime? DOB { get; set; }

[Required(ErrorMessage = "Please enter a password.")]


[StringLength(25)]
[Compare("ConfirmPassword")]
public string Password { get; set; }

[Required(ErrorMessage = "Please confirm your password.")]


[Display(Name = "Confirm Password")]
public string ConfirmPassword { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 13
A validation tag in a strongly-typed view
that posts a Customer object
<div asp-validation-summary="All" class="text-danger">
</div>
...
<input asp-for="Name" class="form-control" />
<input type="text" asp-for="DOB" class="form-control" />
<input type="password" asp-for="Password"
class="form-control" />
<input type="password" asp-for="ConfirmPassword"
class="form-control" />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 14
An action method that receives a Customer object
from the view
public IActionResult Index(Customer customer) {
if (ModelState.IsValid) {
// code that adds customer to database
return RedirectToAction("Welcome");
} else {
return View(customer);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 15
The Registration page after the user submits
invalid data

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 16
The HTML emitted for an <input> tag bound
to the Name property
<input type="text" class="form-control"
data-val="true"
data-val-regex="Name may not contain special characters."
data-val-regex-pattern="^[a-zA-Z0-9 ]&#x2B;$"
data-val-required="Please enter a name."
id="Name" name="Name" value="" />

The HTML emitted for that <input> tag


when data validation fails
<input type="text" class="form-control input-validation-error"
data-val="true"
data-val-regex="Name may not contain special characters."
data-val-regex-pattern="^[a-zA-Z0-9 ]&#x2B;$"
data-val-required="Please enter a name."
id="Name" name="Name" value="" />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 17
The HTML emitted for a summary of valid data
<div class="text-danger validation-summary-valid"
data-valmsg-summary="true">
<ul><li style="display:none"></li></ul>
</div>

The HTML emitted for a summary of invalid data


<div class="text-danger validation-summary-errors"
data-valmsg-summary="true">
<ul>
<li>Please enter a name.</li>
<li>Please enter a date of birth.</li>
</ul>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 18
Some CSS styles in the site.css file
.input-validation-error {
border: 2px solid #dc3545; /* same red as text-danger */
background-color: #faebd7; /* antique white */
}

.validation-summary-valid { display: none; }

.validation-summary-errors ul { list-style: none; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 19
The view with formatted validation

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 20
Properties of the ModelStateDictionary class
Count
ErrorCount
IsValid
Keys
Values

Methods of the ModelStateDictionary class


AddModelError(key, msg)
GetValidationState(key)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 21
Code that adds a validation message
to the ModelState property
using Microsoft.AspNetCore.Mvc.ModelBinding;
...
[HttpPost]
public IActionResult Index(Customer customer)
{
string key = nameof(Customer.DOB);

if (ModelState.GetValidationState(key) ==
ModelValidationState.Valid) {
if (customer.DOB > DateTime.Today) {
ModelState.AddModelError(
key, "Date of birth must not be a future date.");
}
}
if (ModelState.IsValid) {
// code that adds customer to database
return RedirectToAction("Welcome");
}
else {
return View(customer);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 22
Two tag helpers used with data validation
asp-validation-summary
asp-validation-for

The values of the ValidateSummary enum


All
ModelOnly
None

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 23
An action method that adds a model-level
validation message
[HttpPost]
public IActionResult Index(Customer customer) {
if (ModelState.IsValid) {
// code that adds customer to database
return RedirectToAction("Welcome");
} else {
ModelState.AddModelError("",
"There are errors in the form.");
return View(customer);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 24
Part of a view that displays both model-level
and property-level messages
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>

<form asp-action="Index" method="post">


<div class="form-group row">
<div class="col-sm-2"><label>Name:</label></div>
<div class="col-sm-4">
<input asp-for="Name" class="form-control" />
</div>
<div class="col-sm-6">
<span asp-validation-for="Name"
class="text-danger"></span>
</div>
</div>
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 25
The form in the browser after validation fails

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 26
The jQuery libraries that download by default
with the MVC template

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 27
The jQuery libraries in a Layout view
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/lib/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />
<link href="~/css/site.css" rel="stylesheet" />
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/jquery-validation/dist/
jquery.validate.min.js">
</script>
<script src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js">
</script>
<title>@ViewBag.Title</title>
</head>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 28
Some caveats to client-side validation
 It only works correctly with property-level validation, not model-
level validation.
 Any server-side validation doesn’t run until all client-side
validation passes. This can lead to a 2-step process that may
annoy some users.
 Not all data annotations work properly on the client. In the
example below, for instance, the Range annotation for the DOB
field isn’t working as it should.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 29
The form in the browser with client-side validation

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 30
Classes used to create a custom attribute
ValidationAttribute
ValidationContext
ValidationResult

A virtual method of the ValidationAttribute class


IsValid(object, context)

A constructor of the ValidationResult class


ValidationResult(string)

A field of the ValidationResult class


Success

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 31
A custom attribute that checks if a date
is in the past
public class PastDateAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value,
ValidationContext ctx)
{
if (value is DateTime) {
DateTime dateToCheck = (DateTime)value;
if (dateToCheck < DateTime.Today) {
return ValidationResult.Success;
}
}

string msg = base.ErrorMessage ??


$"{ctx.DisplayName} must be a valid past date.";
return new ValidationResult(msg);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 32
Code that uses the PastDate attribute
with the default validation message
[PastDate]
public DateTime? DOB { get; set; }

How it looks in the browser

Code that uses the PastDate attribute


with a custom validation message
[PastDate(ErrorMessage =
"Please enter a date of birth in the past.")]
public DateTime? DOB { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 33
A custom attribute that accepts values (part 1)
public class YearsFromNowAttribute : ValidationAttribute
{
private int numYears;
public YearsFromNowAttribute(int years) {
numYears = years;
}
public bool IsPast { get; set; } = false;

protected override ValidationResult IsValid(object value,


ValidationContext ctx)
{
if (value is DateTime) {
// cast value to DateTime
DateTime dateToCheck = (DateTime)value;

// calculate date range


DateTime now = DateTime.Today;
DateTime from;

if (IsPast) {
from = new DateTime(now.Year, 1, 1);
from = from.AddYears(-numYears);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 34
A custom attribute that accepts values (part 2)
else {
from = new DateTime(now.Year, 12, 31);
from = from.AddYears(numYears);
}

// check date
if (IsPast) {
if (dateToCheck >= from && dateToCheck < now) {
return ValidationResult.Success;
}
}
else {
if (dateToCheck > now && dateToCheck <= from) {
return ValidationResult.Success;
}
}
}
string msg = base.ErrorMessage ??
ctx.DisplayName + " must be a " +
(IsPast ? "past" : "future") + " date within " +
numYears + " years of now.";
return new ValidationResult(msg);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 35
A DOB property that requires a past date
no more than 100 years ago
[YearsFromNow(100, IsPast = true)]
public DateTime? DOB { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 36
A custom attribute that checks more than one
property in a class
public class RequiredContactInfoAttribute :
ValidationAttribute
{
protected override ValidationResult IsValid(object v,
ValidationContext c){
var cust = (Customer)c.ObjectInstance;
if (string.IsNullOrEmpty(cust.PhoneNumber) &&
string.IsNullOrEmpty(cust.EmailAddress))
{
string msg = base.ErrorMessage ??
"Enter phone number or email.";
return new ValidationResult(msg);
}
else {
return ValidationResult.Success;
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 37
The method of the IValidatableObject interface
Validate(context)

A constructor of the ValidationResult class


ValidationResult(m, list)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 38
A custom validation class that checks
more than one field
public class Customer : IValidatableObject {
...
[Required(ErrorMessage = "Please enter a date of birth.")]
public DateTime? DOB { get; set; }

public string PhoneNumber { get; set; }


public string EmailAddress { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext ctx) {


if (DOB > DateTime.Now)
{
yield return new ValidationResult(
"Date of birth can't be in the future.",
new[] { nameof(DOB) });
}
if (string.IsNullOrEmpty(PhoneNumber) &&
string.IsNullOrEmpty(EmailAddress))
{
yield return new ValidationResult(
"Please enter a phone number or email address.",
new[] { nameof(PhoneNumber), nameof(EmailAddress) });
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 39
A method of the IClientModelValidator interface
AddValidation(context)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 40
The updated PastDate attribute
with client-side validation (part 1)
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
...
public class PastDateAttribute : ValidationAttribute, IClientModelValidator
{
private int numYears;
public PastDateAttribute(int years = -1) => numYears = years;

protected override ValidationResult IsValid(object val,


ValidationContext ctx)
{
if (val is DateTime) {
DateTime dateToCheck = (DateTime)val;
if (numYears == -1) { // no limit on past date
if (dateToCheck < DateTime.Today)
return ValidationResult.Success;
} else {
DateTime minDate = DateTime.Today.AddYears(-numYears);
if (dateToCheck >= minDate && dateToCheck < DateTime.Today)
return ValidationResult.Success;
}
}
return new ValidationResult(GetMsg(ctx.DisplayName));
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 41
The updated PastDate attribute (part 2)
public void AddValidation(ClientModelValidationContext c)
{
if (!c.Attributes.ContainsKey("data-val"))
c.Attributes.Add("data-val", "true");
c.Attributes.Add("data-val-pastdate-numyears", numYears.ToString());
c.Attributes.Add("data-val-pastdate",
GetMsg(c.ModelMetadata.DisplayName ?? c.ModelMetadata.Name));
}

private string GetMsg(string name) =>


base.ErrorMessage ?? name + " must be a valid past date" +
(numYears == -1 ? "." : " (max " + numYears + " years ago).");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 42
A model property with the PastDate attribute
[PastDate(100)]
public DateTime? DOB { get; set; }

The HTML that the PastDate attribute emits


<input type="text" class="form-control" data-val="true"
data-val-pastdate=
"DOB must be a valid past date (max 100 years ago)."
data-val-pastdate-numyears="100" id="DOB" name="DOB" value=""
/>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 43
The pastdate.js file (part 1)
jQuery.validator.addMethod("pastdate",
function (value, element, param) {
// get date entered by user, confirm it's a date
if (value === '') return false;
var dateToCheck = new Date(value);
if (dateToCheck === "Invalid Date") return false;

// get the number of years


var numYears = Number(param);

// get the current date


var now = new Date();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 44
The pastdate.js file (part 2)
// check date
if (numYears == -1) {
if (dateToCheck < now) return true;
} else {
// calculate limit
var minDate = new Date();
var minYear = now.getFullYear() - numYears;
minDate.setFullYear(minYear);

if (dateToCheck >= minDate && dateToCheck < now)


return true;
}
return false;
});

jQuery.validator.unobtrusive.adapters.addSingleVal(
"pastdate", "numyears");

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 45
The header section of the layout
<head>
<meta name="viewport" content="width=device-width" />
<link href="~/lib/bootstrap/dist/css/bootstrap.min.css"
rel="stylesheet" />

<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/jquery-validation/jquery.validate.min.js">
</script>
<script src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js"></script>
<script src="~/js/pastdate.js"></script>
<title>@ViewBag.Title</title>
</head>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 46
Two constructors of the RemoteAttribute class
RemoteAttribute(act, ctl)
RemoteAttribute(act, ctl, area)

A model property with a Remote attribute


[Remote("CheckEmail", "Validation")]
public string Email { get; set; }

The CheckEmail() action method


public JsonResult CheckEmail(string email)
{
bool hasEmail = Utility.CheckEmail(email);
if (hasEmail)
return Json(
$"Email address {email} is already registered.");
else
return Json(true);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 47
One property of the RemoteAttribute class
AdditionalFields

Remote validation that gets data


from additional fields
The model
[Remote("CheckEmail", "Validation",
AdditionalFields = "Username, Region")]
public string Email { get; set; }
public string Username { get; set; }

The view
<input asp-for="EmailAddress" class="form-control" />
<input asp-for="Username" class="form-control" />
<input type="hidden" name="Region" value="West" />

The CheckEmail() action method


public JsonResult CheckEmail (string email, string username,
string region){
// validation code
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 48
The Registration app with invalid data

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 49
The CSS for the validation class
.input-validation-error {
border: 2px solid #dc3545; /* text-danger */
background-color: #faebd7; /* antique white */
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 50
The Customer class (part 1)
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc;
...
public class Customer
{
public int ID { get; set; } // automatically generated

[Required(ErrorMessage = "Please enter a username.")]


[RegularExpression("^[a-zA-Z0-9 ]+$", ErrorMessage =
"Username may not contain special characters.")]
public string Username { get; set; }

[Required(ErrorMessage = "Please enter an email address.")]


[Remote("CheckEmail", "Validation")]
public string EmailAddress { get; set; }

[Required(ErrorMessage = "Please enter a date of birth.")]


[MinimumAge(13, ErrorMessage =
"You must be at least 13 years old.")]
public DateTime? DOB { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 51
The Customer class (part 2)
[Required(ErrorMessage = "Please enter a password.")]
[Compare("ConfirmPassword")]
[StringLength(25, ErrorMessage =
"Please limit your password to 25 characters.")]
public string Password { get; set; }

[Required(ErrorMessage = "Please confirm your password.")]


[Display(Name = "Confirm Password")]
[NotMapped]
public string ConfirmPassword { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 52
The RegistrationContext class
public class RegistrationContext : DbContext
{
public RegistrationContext(
DbContextOptions<RegistrationContext> options)
: base(options) { }

public DbSet<Customer> Customers { get; set; }


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 53
The MinimumAgeAttribute class (part 1)
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
...
public class MinimumAgeAttribute : ValidationAttribute,
IClientModelValidator
{
private int minYears;
public MinimumAgeAttribute(int years) {
minYears = years;
}

// overrides IsValid() method of ValidationAttribute base class


protected override ValidationResult IsValid(object value,
ValidationContext ctx) {
if (value is DateTime) {
DateTime dateToCheck = (DateTime)value;
dateToCheck = dateToCheck.AddYears(minYears);
if (dateToCheck <= DateTime.Today) {
return ValidationResult.Success;
}
}
return new ValidationResult(GetMsg(ctx.DisplayName));
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 54
The MinimumAgeAttribute class (part 2)
// implements AddValidation() method of IClientModelValidator
// interface
public void AddValidation(ClientModelValidationContext ctx) {
if (!ctx.Attributes.ContainsKey("data-val"))
ctx.Attributes.Add("data-val", "true");
ctx.Attributes.Add("data-val-minimumage-years",
minYears.ToString());
ctx.Attributes.Add("data-val-minimumage",
GetMsg(ctx.ModelMetadata.DisplayName ??
ctx.ModelMetadata.Name));
}

private string GetMsg(string name) =>


base.ErrorMessage ??
$"{name} must be at least {minYears} years ago.";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 55
The minumim-age JavaScript file
jQuery.validator.addMethod("minimumage",
function(value, element, param) {
if (value === '') return false;

var dateToCheck = new Date(value);


if (dateToCheck === "Invalid Date") return false;

var minYears = Number(param);

dateToCheck.setFullYear(dateToCheck.getFullYear() +
minYears);

var today = new Date();


return (dateToCheck <= today);
});

jQuery.validator.unobtrusive.adapters.addSingleVal(
"minimumage", "years");

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 56
The Validation controller
public class ValidationController : Controller
{
private RegistrationContext context;
public RegisterController(RegistrationContext ctx) =>
context = ctx;

public JsonResult CheckEmail(string emailAddress)


{
string msg = Check.EmailExists(context,
emailAddress);
if (string.IsNullOrEmpty(msg)) {
TempData["okEmail"] = true;
return Json(true);
}
else return Json(msg);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 57
The Register controller (part 1)
public class RegisterController : Controller
{
private RegistrationContext context;
public RegisterController(RegistrationContext ctx) =>
context = ctx;

public IActionResult Index() => View();

[HttpPost]
public IActionResult Index(Customer customer)
{
if (TempData["okEmail"] == null) {
string msg = Check.EmailExists(
context, customer.EmailAddress);
if (!String.IsNullOrEmpty(msg)) {
ModelState.AddModelError(
nameof(Customer.EmailAddress), msg);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 58
The Register controller (part 2)
if (ModelState.IsValid) {
context.Customers.Add(customer);
context.SaveChanges();
return RedirectToAction("Welcome");
}
else return View(customer);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 59
The static Check class
public static class Check
{
public static string EmailExists(RegistrationContext ctx,
string email) {
string msg = "";
if (!string.IsNullOrEmpty(email)) {
var customer = ctx.Customers.FirstOrDefault(
c => c.EmailAddress.ToLower() == email.ToLower());
if (customer != null)
msg = $"Email address {email} already in use.";
}
return msg;
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 60
The layout
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
<link rel="stylesheet" type="text/css"
href="~/lib/bootstrap/dist/css/bootstrap.min.css">
<link href="~/css/site.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<header class="bg-primary text-white text-center">
<h1 class="m-3 p-3">Registration</h1>
</header>
<main>
@RenderBody()
</main>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
@RenderSection("scripts", false)
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 61
The Register/Index view (part 1)
@model Customer

@{
ViewData["Title"] = "Registration";
}

@section scripts {
<script src="~/lib/jquery-
validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js"></script>
<script src="~/js/minimum-age.js"></script>
}

<form asp-action="Index" method="post">


<div class="form-group row">
<div class="col-sm-2"><label>Username:</label></div>
<div class="col-sm-4">
<input asp-for="Username" class="form-control" /></div>
<div class="col">
<span asp-validation-for="Username"
class="text-danger"></span></div></div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 62
The Register/Index view (part 2)
<div class="form-group row">
<div class="col-sm-2"><label>Email Address:</label></div>
<div class="col-sm-4">
<input asp-for="EmailAddress" class="form-control" />
</div>
<div class="col">
<span asp-validation-for="EmailAddress"
class="text-danger"></span></div></div>
<div class="form-group row">
<div class="col-sm-2"><label>DOB:</label></div>
<div class="col-sm-4">
<input type="text" asp-for="DOB"
class="form-control" /></div>
<div class="col">
<span asp-validation-for="DOB"
class="text-danger"></span></div></div>
<!-- Password and ConfirmPassword fields -->
<div class="row">
<div class="offset-2 col-sm-4">
<button type="submit" class="btn btn-primary">
Register</button>
</div></div>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C11, Slide 63
Chapter 12

How to use
EF Core

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 1
Objectives (part 1)
Applied
1. Develop the DB context and entity classes that define the data for
an app and create a database and tables that map to those classes.
2. Given an existing database, generate the DB context and entity
classes that map to that database and its tables.

Knowledge
1. Distinguish between Code First development and Database First
development.
2. Describe the purpose of a database (DB) context class and entity
classes.
3. Describe the three techniques you can use to configure a database
with Code First development.
4. Describe the use of EF Core commands to work with a database.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 2
Objectives (part 2)
5. Give examples of entities in a one-to-many relationship, a one-
to-one relationship, and a many-to-many relationship.
6. Describe the use of foreign key properties and navigation key
properties to configure a one-to-many relationship with EF Core.
7. Describe the use of a linking entity in a many-to-many
relationship.
8. Describe the use of the Fluent API to control delete behavior.
9. Describe the use of partial classes to modify entity classes that
are generated from database tables.
10. Describe the use of LINQ to retrieve data from one or more
related database tables.
11. Describe the use of EF Core to add, modify, and delete rows in a
database table.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 3
Objectives (part 3)
12. Explain how you can provide for concurrency when using EF
Core.
13. Describe how a data access class can encapsulate the code for
working with a database.
14. Describe the repository pattern.
15. Describe the unit of work pattern.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 4
A Book entity class
public class Book
{
public int BookId { get; set; }
public string Title { get; set; }
public double Price { get; set; }
}

An Author entity class


public class Author
{
public int AuthorId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 5
A BookstoreContext class
using Microsoft.EntityFrameworkCore;
...
public class BookstoreContext : DbContext
{
public BookstoreContext(
DbContextOptions<BookstoreContext> options) : base(options) { }

public DbSet<Book> Books { get; set; }


public DbSet<Author> Authors { get; set; }

protected override void OnConfiguring(


DbContextOptionsBuilder optionsBuilder)
{
// code that configures the DbContext goes here
base.OnConfiguring(optionsBuilder);
}

protected override void OnModelCreating(


ModelBuilder modelBuilder)
{
// code that configures the DbSet entities goes here
base.OnModelCreating(modelBuilder);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 6
Some conventions for configuration in EF Core
 A property named Id or ClassNameId is the primary key. If the
property is of the int type, the key is an identity column and its
value is generated automatically by the database.
 A string property has a database type of nvarchar(max) and is
nullable.
 An int or double property is not nullable.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 7
How to set an identity primary key by convention
public class Book {
public int BookId { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 8
Some of the data attributes for configuration
Key
NotMapped
DatabaseGenerated

How to adjust the default configuration


using System.ComponentModel.DataAnnotations
using System.ComponentModel.DataAnnotations.Schema
...
public class Book {
[Key]
public string ISBN { get; set; } // primary key

[Required]
[StringLength(200)]
public string Title { get; set; } // not nullable
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 9
Some of the Fluent API methods for configuration
Entity<T>()
Property(lambda)
HasKey(lambda)
HasData(entityList)
ToTable(string)
IsRequired()
HasMaxLength(size)

How to chain Fluent API method calls


protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.Property(b => b.Title).IsRequired()
.HasMaxLength(200);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 10
Code that configures the Book entity
in the OnModelCreating() method
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>().HasKey(b => b.ISBN);

modelBuilder.Entity<Book>().Property(b => b.Title)


.IsRequired().HasMaxLength(200);

modelBuilder.Entity<Book>().HasData(
new Book { ISBN = "1548547298",
Title = "The Hobbit" },
new Book { ISBN = "0312283709",
Title = "Running With Scissors" }
);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 11
A separate configuration class for the Book entity
internal class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> entity)
{
entity.HasKey(b => b.ISBN);

entity.Property(b => b.Title)


.IsRequired().HasMaxLength(200);

entity.HasData(
new Book { ISBN = "1548547298",
Title = "The Hobbit" },
new Book { ISBN = "0312283709",
Title = "Running With Scissors" }
);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 12
Code that applies the configuration class
in the OnModelCreating() method
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new BookConfig());
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 13
How to open the NuGet PMC window
 Select ToolsNuGet Package ManagerPackage Manager
Console.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 14
Some of the PowerShell EF commands
Add-Migration
Remove-Migration
Update-Database
Drop-Database
Script-Migration
Scaffold-DbContext

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 15
Parameters for the Add-Migration command
-Name
-OutputDir

Parameter for the Update-Database command


-Name

Parameters for the Script-Migration command


-From
-To
-Output
-Idempotent

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 16
How to create and then update a database (part 1)
1. Create a migration file named Initial based on the context and
entity classes by entering this command:
PM> Add-Migration Initial
2. Create a database based on the migration file by entering this
command:
PM> Update-Database
3. Add a property named Discount of the double type to the Book
class.
4. Create a migration file named AddDiscount by entering this
command:
PM> Add-Migration AddDiscount
5. Review the migration file and note that the Discount property
doesn’t accept nulls.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 17
How to create and then update a database (part 2)
6. Change the Discount property in the Book class to the data type
of double?.
7. Generate another migration file by entering this command:
PM> Add-Migration MakeDiscountNullable
8. Apply the migration files to the database by entering this
command:
PM> Update-Database

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 18
The Migrations folder after these steps

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 19
How to revert one or more migrations
1. To revert changes to the database by running the Down() method
in every migration file that comes after the AddDiscount file,
enter this command:
PM> Update-Database AddDiscount
2. To remove the unapplied MakeDiscountNullable migration file
that was reverted in step 1 from the Migrations folder, enter this
command:
PM> Remove-Migration

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 20
How to revert all the migrations
1. To revert all changes that have been applied to the database by
running the Down() method in all the migration files, enter this
command:
PM> Update-Database 0
2. To remove all migration files from the Migrations folder, enter
the Remove-Migration command repeatedly. Or, manually
delete all the migration files from the Migrations folder,
including the snapshot file.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 21
Two entities that have a one-to-many relationship

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 22
Three types of relationships between entities
 One to many
 One to one
 Many to many

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 23
Two attributes for configuring relationships
ForeignKey
InverseProperty

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 24
The Has/With configuration pattern
in the Fluent API
 Has represents the side of the relationship where the
configuration starts.
 With represents the side of the relationship where the
configuration ends.

Fluent API methods for configuring relationships


in EF Core
HasOne(lambda)
WithOne(lambda)
HasMany(lambda)
WithMany(lambda)
HasForeignKey<T>(l)
OnDelete(behavior)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 25
How to configure a one-to-many relationship
by convention
public class Book {
public int BookId { get; set; }

public Genre Genre { get; set; }


}

public class Genre {


public int GenreId { get; set; }
public string Name { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 26
How to fully define the one-to-many relationship
by convention (recommended)
public class Book {
public int BookId { get; set; }

public int GenreId { get; set; } // foreign key property


public Genre Genre { get; set; } // navigation property
}

public class Genre {


public int GenreId { get; set; }
public string Name { get; set; }

public ICollection<Book> Books { get; set; }


// navigation property
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 27
How to configure a one-to-many relationship
with attributes
public class Book {
public int BookId { get; set; }
public int CategoryId { get; set; }

[ForeignKey("CategoryId")] // FK property in Book class


[InverseProperty("Books")] // nav property in Genre class
public Genre Category { get; set; }
}

public class Genre {


public int GenreId { get; set; }
public string Name { get; set; }

[InverseProperty("Category")] // nav property in Book class


public ICollection<Book> Books { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 28
How to configure a one-to-many relationship
with the Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasOne(b => b.Genre) // nav property in Book class
.WithMany(g => g.Books); // nav property in Genre class
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 29
How to configure a one-to-one relationship
by convention
public class Author {
public int AuthorId { get; set; } // primary key property
public string Name { get; set; }

public AuthorBio Bio { get; set; } // navigation property


}

public class AuthorBio {


public int AuthorBioId { get; set; } // primary key property
public int AuthorId { get; set; } // foreign key property
public DateTime? DOB { get; set; }

public Author Author { get; set; } // navigation property


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 30
How to configure a one-to-one relationship
with attributes
public class Author {
// same as above
}

public class AuthorBio {


[Key]
public int AuthorId { get; set; } // PK and FK property
public DateTime? DOB { get; set; }

[ForeignKey("AuthorId")] // FK property
public Author Author { get; set; } // navigation property
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 31
How to configure a one-to-one relationship
with the Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Author>()
.HasOne(a => a.Bio) // nav property in Author class
.WithOne(ab => ab.Author) // nav property in AuthorBio class
.HasForeignKey<AuthorBio>(
ab => ab.AuthorId); // FK property
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 32
How to configure a one-to-one relationship
within a single table
protected override void OnModelCreating(
ModelBuilder modelBuilder) {
modelBuilder.Entity<Author>()
.HasOne(a => a.Bio)
.WithOne(ab => ab.Author)
.HasForeignKey<AuthorBio>(ab => ab.AuthorId);

modelBuilder.Entity<Author>().ToTable("Authors");
modelBuilder.Entity<AuthorBio>().ToTable("Authors");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 33
The entities to be linked
in a many-to-many relationship
public class Book {
public int BookId { get; set; }
public string Title { get; set; }

// navigation property to linking entity


public ICollection<BookAuthor> BookAuthors { get; set; }
}

public class Author {


public int AuthorId { get; set; }
public string Name { get; set; }

// navigation property to linking entity


public ICollection<BookAuthor> BookAuthors { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 34
The linking entity
public class BookAuthor {
// composite primary key
public int BookId { get; set; } // foreign key for Book
public int AuthorId { get; set; } // foreign key for Author

// navigation properties
public Book Book { get; set; }
public Author Author { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 35
How to configure a many-to-many relationship
with the Fluent API
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
// composite primary key for BookAuthor
modelBuilder.Entity<BookAuthor>()
.HasKey(ba => new { ba.BookId, ba.AuthorId });

// one-to-many relationship between Book and BookAuthor


modelBuilder.Entity<BookAuthor>()
.HasOne(ba => ba.Book)
.WithMany(b => b.BookAuthors)
.HasForeignKey(ba => ba.BookId);

// one-to-many relationship between Author and BookAuthor


modelBuilder.Entity<BookAuthor>()
.HasOne(ba => ba.Author)
.WithMany(a => a.BookAuthors)
.HasForeignKey(ba => ba.AuthorId);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 36
The values of the DeleteBehavior enum
Cascade
SetNull
Restrict

How to configure a relationship to do nothing


on delete
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.HasOne(b => b.Genre)
.WithMany(g => g.Books)
.OnDelete(DeleteBehavior.Restrict);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 37
The Author class
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

namespace Bookstore.Models
{
public class Author
{
public int AuthorId { get; set; }

[Required(ErrorMessage = "Please enter a first name.")]


[StringLength(200)]
public string FirstName { get; set; }

[Required(ErrorMessage = "Please enter a last name.")]


[StringLength(200)]
[Remote("CheckAuthor", "Validation", "",
AdditionalFields = "FirstName, Operation")]
public string LastName { get; set; }

// read-only property
public string FullName => $"{FirstName} {LastName}";

// navigation property
public ICollection<BookAuthor> BookAuthors { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 38
The Book class
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace Bookstore.Models
{
public partial class Book
{
public int BookId { get; set; }

[Required(ErrorMessage = "Please enter a title.")]


[StringLength(200)]
public string Title { get; set; }

[Range(0.0, 1000000.0, ErrorMessage = "Price must be more than 0.")]


public double Price { get; set; }

[Required(ErrorMessage = "Please select a genre.")]


public string GenreId { get; set; } // foreign key property
public Genre Genre { get; set; } // navigation property

// navigation property
public ICollection<BookAuthor> BookAuthors { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 39
The BookAuthor class
namespace Bookstore.Models
{
public class BookAuthor
{
// composite primary key and foreign keys
public int BookId { get; set; }
public int AuthorId { get; set; }

// navigation properties
public Author Author { get; set; }
public Book Book { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 40
The Genre class
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;

namespace Bookstore.Models
{
public class Genre
{
[StringLength(10)]
[Required(ErrorMessage = "Please enter a genre id.")]
[Remote("CheckGenre", "Validation", "")]
public string GenreId { get; set; }

[StringLength(25)]
[Required(ErrorMessage = "Please enter a genre name.")]
public string Name { get; set; }

public ICollection<Book> Books { get; set; }


}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 41
The BookstoreContext class (part 1)
using Microsoft.EntityFrameworkCore;

namespace Bookstore.Models
{
public class BookstoreContext : DbContext
{
public BookstoreContext(
DbContextOptions<BookstoreContext> options) : base(options)
{ }

public DbSet<Author> Authors { get; set; }


public DbSet<Book> Books { get; set; }
public DbSet<BookAuthor> BookAuthors { get; set; }
public DbSet<Genre> Genres { get; set; }

protected override void OnModelCreating(


ModelBuilder modelBuilder)
{
// BookAuthor: set composite primary key
modelBuilder.Entity<BookAuthor>()
.HasKey(ba => new { ba.BookId, ba.AuthorId });

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 42
The BookstoreContext class (part 2)
// BookAuthor: set foreign keys
modelBuilder.Entity<BookAuthor>().HasOne(ba => ba.Book)
.WithMany(b => b.BookAuthors)
.HasForeignKey(ba => ba.BookId);
modelBuilder.Entity<BookAuthor>().HasOne(ba => ba.Author)
.WithMany(a => a.BookAuthors)
.HasForeignKey(ba => ba.AuthorId);

// Book: remove cascading delete with Genre


modelBuilder.Entity<Book>().HasOne(b => b.Genre)
.WithMany(g => g.Books)
.OnDelete(DeleteBehavior.Restrict);

// seed initial data


modelBuilder.ApplyConfiguration(new SeedGenres());
modelBuilder.ApplyConfiguration(new SeedBooks());
modelBuilder.ApplyConfiguration(new SeedAuthors());
modelBuilder.ApplyConfiguration(new SeedBookAuthors());
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 43
The SeedAuthors class
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bookstore.Models
{
internal class SeedAuthors : IEntityTypeConfiguration<Author>
{
public void Configure(EntityTypeBuilder<Author> entity)
{
entity.HasData(
new Author { AuthorId = 1, FirstName = "Michelle",
LastName = "Alexander" },
new Author { AuthorId = 2, FirstName = "Stephen E.",
LastName = "Ambrose" },
...
new Author { AuthorId = 26, FirstName = "Seth",
LastName = "Grahame-Smith" }
);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 44
The SeedBooks class
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bookstore.Models
{
internal class SeedBooks : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> entity)
{
entity.HasData(
new Book { BookId = 1, Title = "1776",
GenreId = "history", Price = 18.00 },
new Book { BookId = 2, Title = "1984",
GenreId = "scifi", Price = 5.50 },
...
new Book { BookId = 29,
Title = "Harry Potter and the Sorcerer's Stone",
GenreId = "novel", Price = 9.75 }
);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 45
The SeedBookAuthors class
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bookstore.Models
{
internal class SeedBookAuthors :
IEntityTypeConfiguration<BookAuthor>
{
public void Configure(EntityTypeBuilder<BookAuthor> entity)
{
entity.HasData(
new BookAuthor { BookId = 1, AuthorId = 18 },
new BookAuthor { BookId = 2, AuthorId = 20 },
...
new BookAuthor { BookId = 28, AuthorId = 4 },
new BookAuthor { BookId = 28, AuthorId = 26 },
new BookAuthor { BookId = 29, AuthorId = 25 }
);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 46
The SeedGenres class
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bookstore.Models
{
internal class SeedGenres : IEntityTypeConfiguration<Genre>
{
public void Configure(EntityTypeBuilder<Genre> entity)
{
entity.HasData(
new Genre { GenreId = "novel", Name = "Novel" },
new Genre { GenreId = "memoir", Name = "Memoir" },
new Genre { GenreId = "mystery", Name = "Mystery" },
new Genre { GenreId = "scifi",
Name = "Science Fiction" },
new Genre { GenreId = "history", Name = "History" }
);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 47
Parameters for the Scaffold-DbContext command
-Connection
-Provider
-OutputDir
-DataAnnotations
-Force

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 48
A connection string in the appsettings.json file
"ConnectionStrings": {
"BookstoreContext": "Server=(localdb)\\mssqllocaldb;
Database=Bookstore;Trusted_Connection=True;
MultipleActiveResultSets=true"
}

An EF command that generates entity classes


from a Sql Server database
PM> Scaffold-DbContext -Connection name=BookstoreContext
-Provider Microsoft.EntityFrameworkCore.SqlServer
-OutputDir Models\DataLayer -DataAnnotations –Force

The same command with the flags


for the required parameters omitted
PM> Scaffold-DbContext name=BookstoreContext
Microsoft.EntityFrameworkCore.SqlServer
-OutputDir Models\DataLayer -DataAnnotations -Force

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 49
A -Connection parameter with a string literal
-Connection "Server=(localdb)\mssqllocaldb;
Database=Bookstore;Trusted_Connection=True;
MultipleActiveResultSets=true"

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 50
The generated files in the Models\DataLayer folder

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 51
The OnConfiguring() method
in the generated context class
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("name=BookstoreContext");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 52
The Startup.cs file that injects the DB context
into the app
using Microsoft.EntityFrameworkCore;
using Bookstore.Models.DataLayer;

public class Startup


{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }


...

public void ConfigureServices(IServiceCollection services) {


...
services.AddDbContext<BookStoreContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString(
"BookstoreContext")));
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 53
The cleaned up OnConfiguring() method
protected override void OnConfiguring(
DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 54
A partial class generated by EF Database First
namespace Bookstore.Models.DataLayer
{
public partial class Books
{
[Key]
public int BookId { get; set; }

[Required]
[StringLength(200)]
public string Title { get; set; }

public double Price { get; set; }


...
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 55
A partial class that adds a metadata class
and a read-only property
using Microsoft.AspNetCore.Mvc;

namespace Bookstore.Models.DataLayer
{
[ModelMetadataType(typeof(BooksMetadata))]
public partial class Books
{
public bool HasTitle =>
!string.IsNullOrEmpty(Title);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 56
A metadata class that adds validation attributes
using System.ComponentModel.DataAnnotations;

namespace Bookstore.Models.DataLayer
{
public class BooksMetadata
{
[RegularExpression("^[a-zA-Z0-9 _.,!':]+$",
ErrorMessage =
"Title may not contain special characters.")]
public string Title { get; set; }

[Range(0.0, 1000000.0,
ErrorMessage = "Price must be greater than zero.")]
public double Price { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 57
The context class used in the following examples
private BookstoreContext context { get; set; }

Code that creates and executes a query


In two statements
IQueryable<Book> query = context.Books;
IEnumerable<Book> books = query.ToList();

In one statement
var books = context.Books.ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 58
Code that sorts the results
Sort by Title in ascending order (A to Z)
var books = context.Books.OrderBy(b => b.Title).ToList();

Sort by Title in descending order (Z to A)


var books = context.Books.OrderByDescending(
b => b.Title).ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 59
Code that filters the results (part 1)
Get a single book by ID
var book = context.Books.Where(
b => b.BookId == 1).FirstOrDefault();
var book = context.Books.Find(1); // shortcut for above

Get a list of books by genre


var books = context.Books.Where(
b => b.Genre.Name == "Mystery").ToList();

Get a list of books in a price range


var books = context.Books.Where(
b => b.Price > 10 && b.Price < 20).ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 60
Code that filters the results (part 2)
Conditionally filter by multiple criteria
// build the query
IQueryable<Book> query = context.Books;
if (selectedMaxPrice != null)
query = query.Where(b => b.Price < selectedMaxPrice);
if (selectedGenre != null)
query = query.Where(
b => b.Genre.Name == selectedGenre);

// execute the query


var books = query.ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 61
Code that gets a subset of results
int pageNumber = 2, booksPerPage = 4;
var books = context.Books.Skip((pageNumber - 1) *
booksPerPage).Take(booksPerPage).ToList();

Code for a read-only query


that disables change tracking
var books = context.Books.AsNoTracking().ToList();

Code that gets a random book


var randBook = context.Books.OrderBy(
r => Guid.NewGuid()).FirstOrDefault();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 62
How to create a projection
with an anonymous type
var authors = context.Authors
.Select(a => new {
a.AuthorId, // can infer property name
Name = a.FirstName + ' ' + a.LastName
// must specify property name
})
.ToList();

Error when you pass the projection


to a view that expects a list of Authors
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type
'System.Collections.Generic.List`1[<>f_AnonymousType0`2
[System.Int32,System.String]]', but this ViewDataDictionary
instance requires a model item of type
'System.Collections.Generic.IEnumberable`1
[BookList.Models.Author]'.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 63
How to create a projection with a concrete type
The concrete type
public class DropdownDTO
{
public string Value { get; set; }
public string Text { get; set; }
}

The projection
var authors = context.Authors
.Select(a => new DropdownDTO {
Value = a.AuthorId.ToString(),
Text = a.FirstName + ' ' + a.LastName
})
.ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 64
Code that includes related entities
Two techniques for getting a Book and its related Genre
var books = context.Books.Include(b => b.Genre).ToList();
var books = context.Books.Include("Genre").ToList();

Two techniques for getting a Book and related Authors


var books = context.Books.Include(b => b.BookAuthors)
.ThenInclude(ba => ba.Author)
.ToList();
var books = context.Books.Include("BookAuthors.Author")
.ToList();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 65
Three methods of the DbSet class
Add(entity)
Update(entity)
Remove(entity)

One method of the DbContext class


SaveChanges()

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 66
Code that adds a new entity
[HttpPost]
public IActionResult Add(Book book)
{
context.Books.Add(book);
context.SaveChanges();
return RedirectToAction("List", "Book");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 67
Code that updates an existing entity
in a disconnected scenario
[HttpPost]
public IActionResult Edit(Book book) // Book object is disconnected
{
context.Books.Update(book); // call to Update() required
context.SaveChanges();
return RedirectToAction("List", "Book");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 68
Code that updates an existing entity
in a connected scenario
[HttpPost]
public IActionResult Edit(int id, double price)
{
Book book = context.Books.Find(id); // Book object is connected
book.Price = price; // call to Update() not required
context.SaveChanges();
return RedirectToAction("List", "Book");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 69
Code that deletes an entity
[HttpPost]
public IActionResult Delete(Book book)
{
context.Books.Remove(book);
context.SaveChanges();
// related data deleted if cascade delete on
return RedirectToAction("List", "Book");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 70
How to configure a rowversion property
with attributes
public class Book {
public int BookId { get; set; }
public string Title { get; set; }
public double Price { get; set; }

[Timestamp]
public byte[] RowVersion { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 71
How to configure a rowversion property
with the Fluent API
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.Property(b => b.RowVersion)
.IsRowVersion();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 72
How to simulate a concurrency conflict
var book = context.Books.Find(1); // get book from database
book.Price = 14.99; // change price in memory

context.Database.ExecuteSqlRaw( // change price in database


"UPDATE dbo.Books SET Price = Price + 1 WHERE BookId = 1");

context.SaveChanges();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 73
An action method that handles
a concurrency conflict (part 1)
[HttpPost]
public IActionResult Edit(Book book)
{
if (ModelState.IsValid) {
context.Books.Update(book);

// simulate the row being changed after retrieval


// and before save to test a concurrency conflict

try {
context.SaveChanges();
return RedirectToAction("Index");
}
catch (DbUpdateConcurrencyException ex) {
var entry = ex.Entries.Single();
var dbValues = entry.GetDatabaseValues();
if (dbValues == null) {
ModelState.AddModelError("", "Unable to save - "
+ "this book was deleted by another user.");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 74
An action method that handles
a concurrency conflict (part 2)
else {
ModelState.AddModelError("", "Unable to save - "
+ "this book was modified by another user. "
+ "The current database values are displayed "
+ "below. Please edit as needed and click Save, "
+ "or click Cancel.");

var dbBook = (Book)dbValues.ToObject();

if (dbBook.Title != book.Title)
ModelState.AddModelError("Title",
$"Current db value: {dbBook.Title}");

// check rest of properties for equality


}
return View(book);
}
}
else {
return View(book);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 75
Some of the code in the Edit view
@* both primary key and row version value needed for edit *@
<input type="hidden" asp-for="BookId" />
<input type="hidden" asp-for="RowVersion" />
<button type="submit" class="btn">Submit</button>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 76
A class that adds an extension method
to the IQueryable<T> interface
public static class QueryExtensions
{
public static IQueryable<T> PageBy<T>(
this IQueryable<T> query,
int pageNumber, int pageSize)
{
return query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 77
A data access class with a method
that accepts LINQ expressions
using System.Linq.Expressions;
public class Data
{
private BookstoreContext context { get; set; }
public Data(BookstoreContext ctx) => context = ctx;

public IEnumerable<Book> GetPageOfBooks(int pageNumber,


int pageSize)
{
return context.Books.PageBy(pageNumber, pageSize).ToList();
}

public IEnumerable<Book> GetSortedFilteredBooks(


Expression<Func<Book, bool>> where,
Expression<Func<Book, Object>> orderby)
{
return context.Books
.Where(where)
.OrderBy(orderby)
.ToList();
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 78
Code that creates a data access object
var data = new Data(context);

Code that passes a lambda expression


to a data access method
var books = data.GetSortedFilteredBooks(
b => b.Price < 10,
b => b.Title);

Code that uses named arguments


for easier understanding
var books = data.GetSortedFilteredBooks(
where: b => b.Price < 10,
orderby: b => b.Title);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 79
A generic query options class
using System.Linq.Expressions;

public class QueryOptions<T>


{
// public properties for sorting, filtering, and paging
public Expression<Func<T, Object>> OrderBy { get; set; }
public Expression<Func<T, bool>> Where { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }

// public write-only property for includes private string array


private string[] includes;
public string Includes {
set => includes = value.Replace(" ", "").Split(',');
}
// public method returns includes array
public string[] GetIncludes() => includes ?? new string[0];

// read-only properties
public bool HasWhere => Where != null;
public bool HasOrderBy => OrderBy != null;
public bool HasPaging => PageNumber > 0 && PageSize > 0;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 80
A data access class with a method
that uses the options class
public class Data
{
private BookstoreContext context { get; set; }
public Data(BookstoreContext ctx) => context = ctx;

public IEnumerable<Book> GetBooks(QueryOptions<Book> options)


{
IQueryable<Book> query = context.Books;
foreach (string include in options.GetIncludes()) {
query = query.Include(include);
}
if (options.HasWhere)
query = query.Where(options.Where);
if (options.HasOrderBy)
query = query.OrderBy(options.OrderBy);
if (options.HasPaging)
query = query.PageBy(options.PageNumber,
options.PageSize);
return query.ToList();
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 81
Code that uses the method
var books = data.GetBooks(new QueryOptions<Book> {
IncludeString = "BookAuthors.Author, Genre",
OrderBy = b => b.Title
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 82
The generic IRepository interface
public interface IRepository<T> where T : class
{
IEnumerable<T> List(QueryOptions<T> options);
T Get(int id);
void Insert(T entity);
void Update(T entity);
void Delete(T entity);
void Save();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 83
A generic Repository class
that implements IRepository (part 1)
public class Repository<T> : IRepository<T> where T : class {
protected BookstoreContext context { get; set; }
private DbSet<T> dbset { get; set; }

public Repository(BookstoreContext ctx) {


context = ctx;
dbset = context.Set<T>();
}

public virtual IEnumerable<T> List(QueryOptions<T> options) {


IQueryable<T> query = dbset;
foreach (string include in options.GetIncludes()) {
query = query.Include(include);
}
if (options.HasWhere)
query = query.Where(options.Where);
if (options.HasOrderBy)
query = query.OrderBy(options.OrderBy);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 84
A generic Repository class (part 2)
if (options.HasPaging)
query = query.PageBy(options.PageNumber,
options.PageSize);
return query.ToList();
}

public virtual T Get(int id) => dbset.Find(id);


public virtual void Insert(T entity) => dbset.Add(entity);
public virtual void Update(T entity) => dbset.Update(entity);
public virtual void Delete(T entity) => dbset.Remove(entity);
public virtual void Save() => context.SaveChanges();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 85
A controller that uses the generic
Repository class
public class AuthorController : Controller {
private Repository<Author> data { get; set; }

public AuthorController(BookstoreContext ctx) =>


data = new Repository<Author>(ctx);

public ViewResult Index() {


var authors = data.List(new QueryOptions<Author> {
OrderBy = a => a.FirstName
});
return View(authors);
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 86
A unit of work class with four repositories (part 1)
public class BookstoreUnitOfWork : IBookstoreUnitOfWork
{
private BookstoreContext context { get; set; }
public BookstoreUnitOfWork(BookstoreContext ctx) => context = ctx;

private Repository<Book> bookData;


public Repository<Book> Books {
get {
if (bookData == null)
bookData = new Repository<Book>(context);
return bookData;
}
}

// properties for Authors, BookAuthors, and Genres repositories go here

public void DeleteCurrentBookAuthors(Book book) {


var currentAuthors = BookAuthors.List(new QueryOptions<BookAuthor> {
Where = ba => ba.BookId == book.BookId
});
foreach (BookAuthor ba in currentAuthors) {
BookAuthors.Delete(ba); // deletes from BookAuthors repository
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 87
A unit of work class with four repositories (part 2)
public void AddNewBookAuthors(Book book, int[] authorids) {
foreach (int id in authorids) {
BookAuthor ba =
new BookAuthor { BookId = book.BookId, AuthorId = id };
BookAuthors.Insert(ba); // adds to BookAuthors repository
}
}

public void Save() => context.SaveChanges();


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 88
A controller that uses the unit of work class
to update a book
public class BookController : Controller
{
private BookstoreUnitOfWork data { get; set; }
public BookController(BookstoreContext ctx) =>
data = new BookstoreUnitOfWork(ctx);

[HttpPost]
public IActionResult Edit(BookViewModel vm) {
if (ModelState.IsValid) {
data.DeleteCurrentBookAuthors(vm.Book);
data.AddNewBookAuthors(vm.Book,
vm.SelectedAuthors);
data.Books.Update(vm.Book);
data.Save();
...
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C12, Slide 89
Chapter 13

The bookstore
website

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 1
Objectives
Applied
1. Use any of the techniques presented in the Bookstore website.

Knowledge
1. Describe why it’s generally considered a good practice to have a
“fat” model and “skinny” controllers.
2. Describe how the Author Catalog page provides for paging and
sorting.
3. Describe how the Book Catalog page provides for paging, sorting,
and filtering.
4. Describe how the Cart page uses session state and cookies to store
its data.
5. Describe how the Manage Books tab of the Admin page provides
a feature for searching for books that’s also used by the Manage
Genres tab.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 2
The Home page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 3
The Book Catalog page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 4
The Author Catalog page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 5
The Manage Books tab of the Admin page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 6
The Controllers folder

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 7
The Models folder and its subfolders

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 8
The Views folder

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 9
The Admin area folder and its subfolders

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 10
Extension methods for the ISession interface
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
...
public static class SessionExtensions
{
public static void SetObject<T>(this ISession session,
string key, T value) =>
session.SetString(key,
JsonConvert.SerializeObject(value));

public static T GetObject<T>(this ISession session,


string key)
{
var value = session.GetString(key);
return value == null ? default(T) :
JsonConvert.DeserializeObject<T>(value);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 11
Extension methods for the String class
public static class StringExtensions
{
public static string Slug(this string s) {
var sb = new StringBuilder();
foreach (char c in s) {
if (!char.IsPunctuation(c) || c == '-') {
sb.Append(c);
}
}
return sb.ToString().Replace(' ', '-').ToLower();
}

public static bool EqualsNoCase(this string s,


string tocompare) => s?.ToLower() == tocompare?.ToLower();

public static int ToInt(this string s) {


int.TryParse(s, out int id);
return id;
}

public static string Capitalize(this string s) =>


s?.Substring(0, 1)?.ToUpper() +
s?.Substring(1).ToLower();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 12
The generic QueryOptions class (part 1)
public class QueryOptions<T>
{
public Expression<Func<T, Object>> OrderBy { get; set; }
public string OrderByDirection { get; set; } = "asc"; // default
public int PageNumber { get; set; }
public int PageSize { get; set; }

private string[] includes;


public string Includes {
set => includes = value.Replace(" ", "").Split(',');
}
public string[] GetIncludes() => includes ?? new string[0];

public WhereClauses<T> WhereClauses { get; set; }


public Expression<Func<T, bool>> Where {
set {
if (WhereClauses == null)
{
WhereClauses = new WhereClauses<T>();
}
WhereClauses.Add(value);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 13
The generic QueryOptions class (part 2)
public bool HasWhere => WhereClauses != null;
public bool HasOrderBy => OrderBy != null;
public bool HasPaging => PageNumber > 0 && PageSize > 0;
}

public class WhereClauses<T> : List<Expression<Func<T, bool>>> {}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 14
The generic Repository class (part 1)
public class Repository<T> : IRepository<T> where T : class
{
protected BookstoreContext context { get; set; }
private DbSet<T> dbset { get; set; }
public Repository(BookstoreContext ctx) {
context = ctx;
dbset = context.Set<T>();
}
private int? count;
public int Count => count ?? dbset.Count();

public virtual IEnumerable<T> List(QueryOptions<T> options) {


IQueryable<T> query = BuildQuery(options);
return query.ToList();
}
public virtual T Get(int id) => dbset.Find(id);
public virtual T Get(string id) => dbset.Find(id);
public virtual T Get(QueryOptions<T> options) {
IQueryable<T> query = BuildQuery(options);
return query.FirstOrDefault();
}
public virtual void Insert(T entity) => dbset.Add(entity);
public virtual void Update(T entity) => dbset.Update(entity);
public virtual void Delete(T entity) => dbset.Remove(entity);
public virtual void Save() => context.SaveChanges();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 15
The generic Repository class (part 2)
private IQueryable<T> BuildQuery(QueryOptions<T> options)
{
IQueryable<T> query = dbset;
foreach (string include in options.GetIncludes()) {
query = query.Include(include);
}
if (options.HasWhere) {
foreach (var clause in options.WhereClauses) {
query = query.Where(clause);
}
count = query.Count(); // get filtered count
}
if (options.HasOrderBy){
if (options.OrderByDirection == "asc")
query = query.OrderBy(options.OrderBy);
else
query = query.OrderByDescending(options.OrderBy);
}
if (options.HasPaging)
query = query.PageBy(options.PageNumber,
options.PageSize);
return query;
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 16
Page 3 of the Author Catalog sorted by last name
in descending order

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 17
The URL for the Author Catalog page
https://localhost:5001/author/list/page/3/size/4/sort/
lastname/desc

The custom route in the Startup.cs file


endpoints.MapControllerRoute(
name: "",
pattern:
"{controller}/{action}/page/{pagenumber}/size/{pagesize}/
sort/{sortfield}/{sortdirection}");

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 18
The GridDTO class with some default paging
and sorting values
public class GridDTO {
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 4;
public string SortField { get; set; }
public string SortDirection { get; set; } = "asc";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 19
The RouteDictionary class (part 1)
public class RouteDictionary : Dictionary<string, string>
{
public int PageNumber {
get => Get(nameof(GridDTO.PageNumber)).ToInt();
set => this[nameof(GridDTO.PageNumber)] = value.ToString();
}

public int PageSize {


get => Get(nameof(GridDTO.PageSize)).ToInt();
set => this[nameof(GridDTO.PageSize)] = value.ToString();
}

public string SortField {


get => Get(nameof(GridDTO.SortField));
set => this[nameof(GridDTO.SortField)] = value;
}

public string SortDirection {


get => Get(nameof(GridDTO.SortDirection));
set => this[nameof(GridDTO.SortDirection)] = value;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 20
The RouteDictionary class (part 2)
private string Get(string key) =>
Keys.Contains(key) ? this[key] : null;

public void SetSortAndDirection(string fieldName,


RouteDictionary current)
{
this[nameof(GridDTO.SortField)] = fieldName;

if (current.SortField.EqualsNoCase(fieldName) &&
current.SortDirection == "asc")
this[nameof(GridDTO.SortDirection)] = "desc";
else
this[nameof(GridDTO.SortDirection)] = "asc";
}

public RouteDictionary Clone()


{
var clone = new RouteDictionary();
foreach (var key in Keys) {
clone.Add(key, this[key]);
}
return clone;
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 21
The GridBuilder class (part 1)
public class GridBuilder
{
private const string RouteKey = "currentroute";

protected RouteDictionary routes { get; set; }


private ISession session { get; set; }

// this constructor used when just need to get route data from the session
public GridBuilder(ISession sess) {
session = sess;
routes = session.GetObject<RouteDictionary>(RouteKey) ??
new RouteDictionary();
}

// this constructor used when need to store paging-sorting route segments


public GridBuilder(ISession sess, GridDTO values,
string defaultSortField) {
session = sess;

routes = new RouteDictionary(); // clear previous route segment values


routes.PageNumber = values.PageNumber;
routes.PageSize = values.PageSize;
routes.SortField = values.SortField ?? defaultSortField;
routes.SortDirection = values.SortDirection;

SaveRouteSegments();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 22
The GridBuilder class (part 2)
public void SaveRouteSegments() =>
session.SetObject<RouteDictionary>(RouteKey, routes);

public int GetTotalPages(int count) {


int size = routes.PageSize;
return (count + size - 1) / size;
}

public RouteDictionary CurrentRoute => routes;


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 23
The Author/List view model
public class AuthorListViewModel
{
public IEnumerable<Author> Authors { get; set; }
public RouteDictionary CurrentRoute { get; set; }
public int TotalPages { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 24
The Author controller (part 1)
public class AuthorController : Controller
{
private Repository<Author> data { get; set; }
public AuthorController(BookstoreContext ctx) =>
data = new Repository<Author>(ctx);

public IActionResult Index() => RedirectToAction("List");

public ViewResult List(GridDTO vals)


{
// get GridBuilder object, load route segment values,
// store in session
string defaultSort = nameof(Author.FirstName);
var builder = new GridBuilder(HttpContext.Session,
vals, defaultSort);

// create options for querying authors


var options = new QueryOptions<Author> {
Includes = "BookAuthors.Book",
PageNumber = builder.CurrentRoute.PageNumber,
PageSize = builder.CurrentRoute.PageSize,
OrderByDirection = builder.CurrentRoute.SortDirection
};

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 25
The Author controller (part 2)
// OrderBy depends on value of SortField route
if (builder.CurrentRoute.SortField.EqualsNoCase(defaultSort))
options.OrderBy = a => a.FirstName;
else
options.OrderBy = a => a.LastName;

var vm = new AuthorListViewModel {


Authors = data.List(options),
CurrentRoute = builder.CurrentRoute,
TotalPages = builder.GetTotalPages(data.Count)
};
return View(vm);
}

public IActionResult Details(int id) {...}


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 26
The Author/List view (part 1)
@model AuthorListViewModel
@{
ViewData["Title"] = " | Author Catalog";

RouteDictionary current = Model.CurrentRoute;


RouteDictionary routes = Model.CurrentRoute.Clone();
}
<h1>Author Catalog</h1>
<table class="table table-bordered table-striped table-sm">
<thead class="thead-dark">
<tr>
<th>
@{ routes.SetSortAndDirection(nameof(Author.FirstName),
current); }
<a asp-action="List" asp-all-route-data="@routes"
class="text-white">First Name</a></th>
<th>
@{ routes.SetSortAndDirection(nameof(Author.LastName),
current); }
<a asp-action="List" asp-all-route-data="@routes"
class="text-white">Last Name</a></th>
<th>Books(s)</th>
</tr>
</thead>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 27
The Author/List view (part 2)
<tbody>
@foreach (Author author in Model.Authors) {
<tr>
<td>
<a asp-action="Details" asp-route-id="@author.AuthorId"
asp-route-slug="@author.FullName.Slug()">
@author.FirstName</a>
</td>
<td>
<a asp-action="Details" asp-route-id="@author.AuthorId"
asp-route-slug="@author.FullName.Slug()">
@author.LastName</a>
</td>
<td>
@foreach (var ba in author.BookAuthors) {
<p>
<a asp-action="Details" asp-controller="Book"
asp-route-id="@ba.BookId"
asp-route-slug="@ba.Book.Title.Slug()">
@ba.Book.Title</a></p>
}
</td>
</tr>
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 28
The Author/List view (part 3)
</tbody>
</table>

@{
// reset to current route values
routes = Model.CurrentRoute.Clone();

for (int i = 1; i <= Model.TotalPages; i++) {


routes.PageNumber = i;
<a asp-action="List" asp-all-route-data="@routes"
class="btn btn-primary
@Nav.Active(i, current.PageNumber)">@i</a>
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 29
Page 2 of the Book Catalog filtered by the novel
genre and sorted by price in ascending order

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 30
The route segments for the Book Catalog page
/page/2/size/4/sort/price/asc/filter/author-all/
genre-novel/price-all

The custom route in the Startup.cs file


endpoints.MapControllerRoute(
name: "",
pattern: "{controller=Home}/{action=Index}/
page/{pagenumber}/size/{pagesize}/
sort/{sortfield}/{sortdirection}/
filter/{author}/{genre}/{price}");

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 31
The BooksGridDTO class
with default filtering values
using Newtonsoft.Json;
...
public class BooksGridDTO : GridDTO {
[JsonIgnore]
public const string DefaultFilter = "all";

public string Author { get; set; } = DefaultFilter;


public string Genre { get; set; } = DefaultFilter;
public string Price { get; set; } = DefaultFilter;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 32
The static FilterPrefix class
public static class FilterPrefix
{
public const string Genre = "genre-";
public const string Price = "price-";
public const string Author = "author-";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 33
The updated RouteDictionary class (part 1)
public class RouteDictionary : Dictionary<string, string>
{
// paging and sorting properties and methods here

public string GenreFilter {


get => Get(nameof(BooksGridDTO.Genre))?.Replace(
FilterPrefix.Genre, "");
set => this[nameof(BooksGridDTO.Genre)] = value;
}

public string PriceFilter {


get => Get(nameof(BooksGridDTO.Price))?.Replace(
FilterPrefix.Price, "");
set => this[nameof(BooksGridDTO.Price)] = value;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 34
The updated RouteDictionary class (part 2)
public string AuthorFilter {
get {
string s = Get(nameof(BooksGridDTO.Author))?.Replace(
FilterPrefix.Author, "");
int index = s?.IndexOf('-') ?? -1;
return (index == -1) ? s : s.Substring(0, index);
}
set => this[nameof(BooksGridDTO.Author)] = value;
}

public void ClearFilters() =>


GenreFilter = PriceFilter = AuthorFilter =
BooksGridDTO.DefaultFilter;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 35
The BooksGridBuilder class (part 1)
public class BooksGridBuilder : GridBuilder
{
// this constructor gets route data from session state
public BooksGridBuilder(ISession sess) : base(sess) { }

// this constructor stores filtering route segments, as


// well as paging and sorting segments stored by the
// base constructor
public BooksGridBuilder(ISession sess, BooksGridDTO values,
string defaultSortField) : base(sess, values, defaultSortField)
{
bool isInitial =
values.Genre.IndexOf(FilterPrefix.Genre) == -1;
routes.AuthorFilter = (isInitial) ?
FilterPrefix.Author + values.Author : values.Author;
routes.GenreFilter = (isInitial) ?
FilterPrefix.Genre + values.Genre : values.Genre;
routes.PriceFilter = (isInitial) ?
FilterPrefix.Price + values.Price : values.Price;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 36
The BooksGridBuilder class (part 2)
public void LoadFilterSegments(string[] filter, Author author) {
if (author == null) {
routes.AuthorFilter = FilterPrefix.Author + filter[0];
} else {
routes.AuthorFilter = FilterPrefix.Author + filter[0]
+ "-" + author.FullName.Slug();
}
routes.GenreFilter = FilterPrefix.Genre + filter[1];
routes.PriceFilter = FilterPrefix.Price + filter[2];
}

public void ClearFilterSegments() => routes.ClearFilters();

// filter flags
string default = BooksGridDTO.DefaultFilter;
public bool IsFilterByAuthor => routes.AuthorFilter != default;
public bool IsFilterByGenre => routes.GenreFilter != default;
public bool IsFilterByPrice => routes.PriceFilter != default;
// sort flags
public bool IsSortByGenre =>
routes.SortField.EqualsNoCase(nameof(Genre));
public bool IsSortByPrice =>
routes.SortField.EqualsNoCase(nameof(Book.Price));
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 37
The BookQueryOptions class (part 1)
public class BookQueryOptions : QueryOptions<Book>
{
public void SortFilter(BooksGridBuilder builder)
{
if (builder.IsFilterByGenre) {
Where = b =>
b.GenreId == builder.CurrentRoute.GenreFilter;
}
if (builder.IsFilterByPrice) {
if (builder.CurrentRoute.PriceFilter == "under7")
Where = b => b.Price < 7;
else if (builder.CurrentRoute.PriceFilter == "7to14")
Where = b => b.Price >= 7 && b.Price <= 14;
else
Where = b => b.Price > 14;
}
if (builder.IsFilterByAuthor) {
int id = builder.CurrentRoute.AuthorFilter.ToInt();
if (id > 0)
Where = b => b.BookAuthors.Any(
a => a.Author.AuthorId == id);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 38
The BookQueryOptions class (part 2)
if (builder.IsSortByGenre) {
OrderBy = b => b.Genre.Name;
}
else if (builder.IsSortByPrice) {
OrderBy = b => b.Price;
}
else {
OrderBy = b => b.Title;
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 39
The BookstoreUnitOfWork class
public class BookstoreUnitOfWork : IBookstoreUnitOfWork
{
private BookstoreContext context { get; set; }
public BookstoreUnitOfWork(BookstoreContext ctx) =>
context = ctx;

// properties for Books, Authors, BookAuthors,


// and Genres repositories

// helper methods for deleting current authors from existing


// book or loading book authors into a new book

public void Save() => context.SaveChanges();


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 40
The Book/List view model
public class BookListViewModel
{
public IEnumerable<Book> Books { get; set; }
public RouteDictionary CurrentRoute { get; set; }
public int TotalPages { get; set; }

// for filter drop-down data – one hard-coded


public IEnumerable<Author> Authors { get; set; }
public IEnumerable<Genre> Genres { get; set; }
public Dictionary<string, string> Prices =>
new Dictionary<string, string> {
{ "under7", "Under $7" },
{ "7to14", "$7 to $14" },
{ "over14", "Over $14" } };
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 41
The List() action method of the Book controller
public ViewResult List(BooksGridDTO values) {
var builder = new BooksGridBuilder(HttpContext.Session, values,
defaultSortField: nameof(Book.Title));

var options = new BookQueryOptions {


Include = "BookAuthors.Author, Genre",
OrderByDirection = builder.CurrentRoute.SortDirection,
PageNumber = builder.CurrentRoute.PageNumber,
PageSize = builder.CurrentRoute.PageSize
};
options.SortFilter(builder);

var vm = new BookListViewModel {


Books = data.Books.List(options),
Authors = data.Authors.List(new QueryOptions<Author> {
OrderBy = a => a.FirstName }),
Genres = data.Genres.List(new QueryOptions<Genre> {
OrderBy = g => g.Name }),
CurrentRoute = builder.CurrentRoute,
TotalPages = builder.GetTotalPages(data.Books.Count)
};
return View(vm);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 42
The Filter() action method of the Book controller
[HttpPost]
public RedirectToActionResult Filter(string[] filter,
bool clear = false)
{
if (clear) {
builder.ClearFilterSegments();
}
else { // get author so you can add slug if needed
var author = data.Authors.Get(filter[0].ToInt());
builder.LoadFilterSegments(filter, author);
}
builder.SaveRouteSegments();

var builder = new BooksGridBuilder(HttpContext.Session);


return RedirectToAction("List", builder.CurrentRoute);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 43
The Book/List view (part 1)
@model BookListViewModel

@{
ViewData["Title"] = " | Book Catalog";

RouteDictionary current = Model.CurrentRoute;


RouteDictionary routes = Model.CurrentRoute.Clone();
}
<h1>Book Catalog</h1>

<form asp-action="Filter" method="post" class="form-inline">


<label>Author: </label>
<select name="filter" class="form-control m-2"
asp-items="@(new SelectList(
Model.Authors, "AuthorId", "FullName",
current.AuthorFilter))">
<option value="@BooksGridDTO.DefaultFilter">All</option>
</select>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 44
The Book/List view (part 2)
<label>Genre: </label>
<select name="filter" class="form-control m-2"
asp-items="@(new SelectList(
Model.Genres, "GenreId", "Name", current.GenreFilter))">
<option value="@BooksGridDTO.DefaultFilter">All</option>
</select>
<label>Price: </label>
<select name="filter" class="form-control m-2"
asp-items="@(new SelectList(
Model.Prices, "Key", "Value", current.PriceFilter))">
<option value="@BooksGridDTO.DefaultFilter">All</option>
</select>
<button type="submit" class="btn btn-primary mr-2">
Filter</button>
<button type="submit" class="btn btn-primary"
name="clear" value="true">Clear</button>
</form>
<form asp-action="Add" asp-controller="Cart" method="post">
<table class="table table-bordered table-striped table-sm">
<!-- table with column headers for sorting -->
</table>
</form>
@{ // Razor code block with paging links }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 45
Extension methods for cookies (part 1)
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
...
public static class CookieExtensions
{
public static string GetString(
this IRequestCookieCollection cookies, string key) =>
cookies[key];

public static int? GetInt32(


this IRequestCookieCollection cookies, string key) =>
int.TryParse(cookies[key], out int i) ? i : (int?) null;

public static T GetObject<T>(


this IRequestCookieCollection cookies, string key)
{
var value = cookies[key];
return value == null ? default(T) :
JsonConvert.DeserializeObject<T>(value);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 46
Extension methods for cookies (part 2)
public static void SetString(this IResponseCookies cookies,
string key, string value, int days = 30) {
cookies.Delete(key); // delete old value first
if (days == 0) { // session cookie
cookies.Append(key, value);
}
else { // persistent cookie
CookieOptions options = new CookieOptions {
Expires = DateTime.Now.AddDays(days)
};
cookies.Append(key, value, options);
}
}

public static void SetInt32(this IResponseCookies cookies,


string key, int value, int days = 30) =>
cookies.SetString(key, value.ToString(), days);

public static void SetObject<T>(this IResponseCookies cookies,


string key, T value, int days = 30) =>
cookies.SetString(key, JsonConvert.SerializeObject(value),
days);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 47
The Cart page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 48
The CartItem class
using Newtonsoft.Json;
...
public class CartItem
{
public BookDTO Book { get; set; }
public int Quantity { get; set; }

[JsonIgnore]
public double Subtotal => Book.Price * Quantity;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 49
The BookDTO class
public class BookDTO
{
public int BookId { get; set; }
public string Title { get; set; }
public double Price { get; set; }
public Dictionary<int, string> Authors { get; set; }

public void Load(Book book)


{
BookId = book.BookId;
Title = book.Title;
Price = book.Price;
Authors = new Dictionary<int, string>();
foreach (BookAuthor ba in book.BookAuthors) {
Authors.Add(ba.Author.AuthorId,
ba.Author.FullName);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 50
The CartItemDTO class
public class CartItemDTO
{
public int BookId { get; set; }
public int Quantity { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 51
An extension method for a list of CartItem objects
public static class CartItemListExtensions
{
public static List<CartItemDTO> ToDTO(
this List<CartItem> list) =>
list.Select(ci => new CartItemDTO {
BookId = ci.Book.BookId,
Quantity = ci.Quantity
}).ToList();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 52
The CartViewModel class
public class CartViewModel
{
public IEnumerable<CartItem> List { get; set; }
public RouteDictionary BookGridRoute { get; set; }
public double Subtotal { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 53
The Cart class (part 1)
public class Cart
{
private const string CartKey = "mycart";
private const string CountKey = "mycount";

private List<CartItem> items { get; set; }


private List<CartItemDTO> storedItems { get; set; }

private ISession session { get; set; }


private IRequestCookieCollection requestCookies { get; set; }
private IResponseCookies responseCookies { get; set; }

public Cart(HttpContext ctx) {


session = ctx.Session;
requestCookies = ctx.Request.Cookies;
responseCookies = ctx.Response.Cookies;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 54
The Cart class (part 2)
public void Load(Repository<Book> data) {
items = session.GetObject<List<CartItem>>(CartKey);
if (items == null) {
items = new List<CartItem>();
storedItems =
requestCookies.GetObject<List<CartItemDTO>>(CartKey);
}
if (storedItems?.Count > items?.Count) {
foreach (CartItemDTO storedItem in storedItems) {
var book = data.Get(new QueryOptions<Book> {
Include = "BookAuthors.Author, Genre",
Where = b => b.BookId == storedItem.BookId
});
if (book != null) {
var dto = new BookDTO();
dto.Load(book);

CartItem item = new CartItem {


Book = dto,
Quantity = storedItem.Quantity
};
items.Add(item);
}
}
Save();
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 55
The Cart class (part 3)
public double Subtotal => items.Sum(c => c.Subtotal);

public int? Count => session.GetInt32(CountKey) ??


requestCookies.GetInt32(CountKey);

public IEnumerable<CartItem> List => items;

public CartItem GetById(int id) =>


items.FirstOrDefault(ci => ci.Book.BookId == id);

public void Add(CartItem item) {


var itemInCart = GetById(item.Book.BookId);
if (itemInCart == null) { // if new, add
items.Add(item);
}
else { // otherwise, increase quantity by 1
itemInCart.Quantity += 1;
}
}

public void Edit(CartItem item) {


var itemInCart = GetById(item.Book.BookId);
if (itemInCart != null) {
itemInCart.Quantity = item.Quantity;
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 56
The Cart class (part 4)
public void Remove(CartItem item) => items.Remove(item);

public void Clear() => items.Clear();

public void Save() {


if (items.Count == 0) {
session.Remove(CartKey);
session.Remove(CountKey);
responseCookies.Delete(CartKey);
responseCookies.Delete(CountKey);
}
else {
session.SetObject<List<CartItem>>(CartKey, items);
session.SetInt32(CountKey, items.Count);
responseCookies.SetObject<List<CartItemDTO>>(
CartKey, items.ToDTO());
responseCookies.SetInt32(CountKey, items.Count);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 57
Some methods of the Cart controller (part 1)
private Cart GetCart() {
var cart = new Cart(HttpContext);
cart.Load(data);
return cart;
}

public ViewResult Index() {


Cart cart = GetCart();
var builder = new BooksGridBuilder(HttpContext.Session);
var vm = new CartViewModel {
List = cart.List,
Subtotal = cart.Subtotal,
BookGridRoute = builder.CurrentRoute
};
return View(vm);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 58
Some methods of the Cart controller (part 2)
[HttpPost]
public RedirectToActionResult Add(int id) {
var book = data.Get(new QueryOptions<Book> {
Include = "BookAuthors.Author, Genre",
Where = b => b.BookId == id
});
if (book == null) {
TempData["message"] = "Unable to add book to cart.";
}
else {
var dto = new BookDTO();
dto.Load(book);
CartItem item = new CartItem {
Book = dto, Quantity = 1 // default quantity
};
Cart cart = GetCart();
cart.Add(item);
cart.Save();
TempData["message"] = $"{book.Title} added to cart";
}
var builder = new BooksGridBuilder(HttpContext.Session);
return RedirectToAction(
"List", "Book", builder.CurrentRoute);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 59
Some methods of the Cart controller (part 3)
[HttpPost]
public RedirectToActionResult Remove(int id) {
Cart cart = GetCart();
CartItem item = cart.GetById(id);
cart.Remove(item);
cart.Save();
TempData["message"] =
$"{item.Book.Title} removed from cart.";
return RedirectToAction("Index");
}

// Edit(), Clear(), and Checkout() methods not shown here

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 60
The Cart/Index view (part 1)
@model CartViewModel

<h1>Your Cart</h1>
<form asp-action="Clear" method="post">
<ul class="list-group mb-4">
<li class="list-group-item">
<div class="row">
<div class="col">Subtotal: @Model.Subtotal.ToString("c")
</div>
<div class="col">
<div class="float-right">
<a asp-action="Checkout" class="btn btn-primary">
Checkout</a>
<button type="submit" class="btn btn-primary">
Clear Cart</button>
<a asp-action="List" asp-controller="Book"
asp-all-route-data="@Model.BookGridRoute">
Back to Shopping</a>
</div>
</div>
</div>
</li>
</ul>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 61
The Cart/Index view (part 2)
<form asp-action="Remove" method="post">
<table class="table">
<thead class="thead-dark">
<tr><th>Title</th>
<th>Author(s)</th>
<th>Price</th>
<th>Quantity</th>
<th>Subtotal</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (CartItem item in Model.List) {
<tr>
<td>
<a asp-action="Details" asp-controller="Book"
asp-route-id="@item.Book.BookId"
asp-route-slug="@item.Book.Title.Slug()">
@item.Book.Title
</a>
</td>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 62
The Cart/Index view (part 3)
<td>
@foreach (var keyValuePair in item.Book.Authors) {
<p><a asp-action="Details"
asp-controller="Author"
asp-route-id="@keyValuePair.Key"
asp-route-slug=
"@keyValuePair.Value.Slug()">
@keyValuePair.Value</a></p>
}
</td>
<!-- cells that display price, quantity, subtotal -->
<td>
<div class="float-right">
<a asp-action="Edit" asp-controller="Cart"
asp-route-id="@item.Book.BookId"
asp-route-slug="@item.Book.Title.Slug()"
class="btn btn-primary">Edit</a>
<button type="submit" name="id"
value="@item.Book.BookId"
class="btn btn-primary">Remove</button>
</div></td></tr>
}
</tbody></table>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 63
The Manage Books tab of the Admin page
for searching books

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 64
The Manage Books tab with search results

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 65
The SearchData class (part 1)
using Microsoft.AspNetCore.Mvc.ViewFeatures;
...
public class SearchData
{
private const string SearchKey = "search";
private const string TypeKey = "searchtype";

private ITempDataDictionary tempData { get; set; }


public SearchData(ITempDataDictionary temp) =>
tempData = temp;

// use Peek() rather than a straight read so value will persist


public string SearchTerm {
get => tempData.Peek(SearchKey)?.ToString();
set => tempData[SearchKey] = value;
}
public string Type {
get => tempData.Peek(TypeKey)?.ToString();
set => tempData[TypeKey] = value;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 66
The SearchData class (part 2)
public bool HasSearchTerm => !string.IsNullOrEmpty(SearchTerm);
public bool IsBook => Type.EqualsNoCase("book");
public bool IsAuthor => Type.EqualsNoCase("author");
public bool IsGenre => Type.EqualsNoCase("genre");

public void Clear() {


tempData.Remove(SearchKey);
tempData.Remove(TypeKey);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 67
The SearchViewModel class
using System.ComponentModel.DataAnnotations;
...
public class SearchViewModel
{
public IEnumerable<Book> Books { get; set; }
[Required(ErrorMessage = "Please enter a search term.")]
public string SearchTerm { get; set; }
public string Type { get; set; }
public string Header { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 68
The Search() action methods
of the Book controller (part 1)
[HttpPost]
public RedirectToActionResult Search(SearchViewModel vm) {
if (ModelState.IsValid) {
var search = new SearchData(TempData) {
SearchTerm = vm.SearchTerm,
Type = vm.Type
};
return RedirectToAction("Search");
}
else {
return RedirectToAction("Index");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 69
The Search() action methods (part 2)
[HttpGet]
public ViewResult Search() {
var search = new SearchData(TempData);

if (search.HasSearchTerm) {
var vm = new SearchViewModel {
SearchTerm = search.SearchTerm
};
var options = new QueryOptions<Book> {
Include = "Genre, BookAuthors.Author"
};
if (search.IsBook) {
options.Where = b => b.Title.Contains(vm.SearchTerm);
vm.Header =
$"Search results for book title '{vm.SearchTerm}'";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 70
The Search() action methods (part 3)
if (search.IsAuthor) {
int index = vm.SearchTerm.LastIndexOf(' ');
if (index == -1) {
options.Where = b => b.BookAuthors.Any(
ba => ba.Author.FirstName.Contains(
vm.SearchTerm) ||
ba.Author.LastName.Contains(vm.SearchTerm));
}
else {
string first = vm.SearchTerm.Substring(0, index);
string last = vm.SearchTerm.Substring(index + 1);
options.Where = b => b.BookAuthors.Any(
ba => ba.Author.FirstName.Contains(first) &&
ba.Author.LastName.Contains(last));
}
vm.Header =
$"Search results for author '{vm.SearchTerm}'";
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 71
The Search() action methods (part 4)
if (search.IsGenre) {
options.Where = b =>
b.GenreId.Contains(vm.SearchTerm);
vm.Header =
$"Search results for genre ID '{vm.SearchTerm}'";
}
vm.Books = data.Books.List(options);
return View("SearchResults", vm);
}
else {
return View("Index");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 72
The Delete() action method of the Genre controller
[HttpGet]
public IActionResult Delete(string id) {
var genre = data.Get(new QueryOptions<Genre> {
Include = "Books",
Where = g => g.GenreId == id
});

if (genre.Books.Count > 0) {
TempData["message"] = $"Can't delete genre {genre.Name} "
+ "because it's associated with these books.";
return GoToBookSearchResults(id);
}
else {
return View("Genre", genre);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 73
The helper method of the Genre controller
private RedirectToActionResult GoToBookSearchResults(string id)
{
// display search results of all books in this genre
var search = new SearchData(TempData) {
SearchTerm = id,
Type = "genre"
};
return RedirectToAction("Search", "Book");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C13, Slide 74
Chapter 14

How to use
dependency injection
and unit testing

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 1
Objectives (part 1)
Applied
1. Use dependency injection (DI) to make the code for an ASP.NET
Core web app more flexible and easier to change.
2. Use unit testing to automate the testing of a web app.

Knowledge
1. Describe how to configure a web app for dependency injection.
2. List and describe the three dependency life cycles.
3. Distinguish between controllers that are tightly coupled with EF
Core and controllers that are loosely coupled with EF Core.
4. Explain how dependency chaining works with repository objects
and unit of work objects.
5. Describe the use of DI with an HttpContextAccessor object.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 2
Objectives (part 2)
6. Describe the use of DI with an action method that uses the
FromServices attribute.
7. Describe the use of DI to inject an object into a view.
8. Explain how unit testing works.
9. Describe three advantages of unit testing.
10. Explain how to add an xUnit project to a solution.
11. Describe the Arrange/Act/Assert (AAA) pattern that’s often used
to organize the code for a unit test.
12. Explain how to run unit tests.
13. Describe the use of a fake object to test methods that have
dependencies.
14. Describe the use of Moq to create fake objects.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 3
Three methods of the IServiceCollection object
that map dependencies
AddTransient<interface, class>()
AddScoped<interface, class>()
AddSingleton<interface, class>()

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 4
How to configure DI for a class
that doesn’t use generics
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<IBookstoreUnitOfWork,
BookstoreUnitOfWork>();
...
}

How to configure DI for a generic class


services.AddTransient(typeof(IRepository<>),
typeof(Repository<>));

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 5
How to configure DI
for the HTTP context accessor
Manually
services.AddSingleton<IHttpContextAccessor,
HttpContextAccessor>();

With a method of the IServiceCollection object


services.AddHttpContextAccessor();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 6
An Author controller
That injects a DbContext object
public class AuthorController : Controller
{
private Repository<Author> data { get; set; }

public AuthorController(BookstoreContext ctx) =>


data = new Repository<Author>(ctx);
...
}

That injects a repository object


public class AuthorController : Controller
{
private IRepository<Author> data { get; set; }

public AuthorController(IRepository<Author> rep) =>


data = rep;
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 7
A Book controller
That injects a DbContext object
public class BookController : Controller
{
private BookstoreUnitOfWork data { get; set; }

public BookController(BookstoreContext ctx) =>


data = new BookstoreUnitOfWork(ctx);
...
}

That injects a unit of work object


public class BookController : Controller
{
private IBookstoreUnitOfWork data { get; set; }

public BookController(IBookstoreUnitOfWork unit) =>


data = unit;
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 8
A Cart class that injects
an HttpContextAccessor object
...
using Microsoft.AspNetCore.Http;
...
public class Cart {
...
private ISession session { get; set; }
private IRequestCookieCollection requestCookies { get; set; }
private IResponseCookies responseCookies { get; set; }

public Cart(IHttpContextAccessor ctx) {


session = ctx.HttpContext.Session;
requestCookies = ctx.HttpContext.Request.Cookies;
responseCookies = ctx.HttpContext.Response.Cookies;
}
...
public void Load(IRepository<Book> data) {
// code that uses the repository and HttpContext
// to load cart items
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 9
A Cart controller that injects
an HttpContextAccessor object (part 1)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Bookstore.Models;
...
public class CartController : Controller {
private IRepository<Book> data { get; set; }
private IHttpContextAccessor accessor { get; set; }
private Cart cart { get; set; }

public CartController(IRepository<Book> rep,


IHttpContextAccessor http)
{
data = rep;
accessor = http;
cart = new Cart(accessor);
cart.Load(data);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 10
A Cart controller that injects
an HttpContextAccessor object (part 2)
public ViewResult Index()
{
var builder =
new BooksGridBuilder(accessor.HttpContext.Session);
// rest of action method code
}
...
[HttpPost]
public RedirectToActionResult Edit(CartItem item) {
cart.Edit(item); // no need to create its own Cart object
cart.Save();
// rest of action method code
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 11
A controller that doesn’t use DI
using Microsoft.AspNetCore.Mvc;
using Bookstore.Models;
...
public class ValidationController : Controller
{
private Repository<Author> authorData { get; set; }
private Repository<Genre> genreData { get; set; }

public ValidationController(BookstoreContext ctx) {


authorData = new Repository<Author>(ctx);
genreData = new Repository<Genre>(ctx);
}
public JsonResult CheckGenre(string genreId) {
validate.CheckGenre(genreId, genreData);
...
}
public JsonResult CheckAuthor(string firstName, string lastname,
string operation) {
validate.CheckAuthor(firstName, lastName, operation,
authorData);
...
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 12
The same controller with DI in its action methods
using Microsoft.AspNetCore.Mvc;
using Bookstore.Models;
...
public class ValidationController : Controller
{
// private properties and constructor no longer needed

public JsonResult CheckGenre(


string genreId, [FromServices]IRepository<Genre> data) {
validate.CheckGenre(genreId, data);
...
}

public JsonResult CheckAuthor(string firstName, string lastName,


string operation, [FromServices] IRepository<Author> data) {
validate.CheckAuthor(firstName, lastName, operation, data);
...
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 13
Code in a layout that doesn’t inject a Cart object
@{
var cart = new Cart(Context);
...
}

A navigation link that uses the Cart object


<a class="nav-link" asp-action="Index"
asp-controller="Cart" asp-area="">
<span class="fas fa-shopping-cart"></span>&nbsp;Cart
<span class="badge badge-light">@cart.Count</span>
</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 14
How to set up the Cart object for DI
The ICart interface
public interface ICart {
// declarations for properties and methods of the Cart class
}

The Cart class updated to implement the interface


public class Cart : ICart {
// properties and methods that now implement the interface
}

The dependency mapping in the Startup.cs file


public void ConfigureServices(IServiceCollection services) {
...
// other dependency mappings
services.AddTransient<ICart, Cart>();
...
}

Code in a layout that injects a Cart object


@inject ICart cart

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 15
The unit testing process

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 16
Advantages of unit testing
 Unit testing reduces the amount of time you have to spend
manually testing an app by running it and entering data.
 Unit testing makes it easy to test an app after each significant
code change. This helps you find problems earlier in the
development cycle than you typically would when using manual
testing.
 Unit testing makes debugging easier because you typically test an
app after each significant code change. As a result, when a test
fails, you only need to debug the most recent code changes.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 17
The Add New Project dialog

How to add a unit test project to a solution


1. Right-click the solution and select AddNew Project.
2. In the Add New Project dialog, select the xUnit Test Project
(.NET Core) template and click Next. To help find the template,
you can use the dialog to search for “xunit”.
3. Enter a name and location for the project and click Create.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 18
The web app project and the unit test project

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 19
Some static methods of the Assert class
Equal(expected, result)
NotEqual(expected, result)
False(Boolean)
True(Boolean)
IsType<T>(result)
IsNull(result)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 20
Three attributes of the Xunit namespace
Fact
Theory
InlineData(p1, p2, ...)

The using directive for the Xunit namespace


using Xunit;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 21
A test class with two test methods (part 1)
public class NavTests
{
[Fact]
public void ActiveMethod_ReturnsAString()
{
string s1 = "Home"; // arrange
string s2 = "Books";

var result = Nav.Active(s1, s2); // act

Assert.IsType<string>(result); // assert
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 22
A test class with two test methods (part 2)
[Theory]
[InlineData("Home", "Home")]
[InlineData("Books", "Books")]
public void ActiveMethod_ReturnsValueActiveIfMatch(
string s1, string s2)
{
string expected = "active"; // arrange

var result = Nav.Active(s1, s2); // act

Assert.Equal(expected, result); // assert


}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 23
The Test Explorer in Visual Studio

Two ways to open the Test Explorer


 From the menu system, select TestTest Explorer.
 In the Solution Explorer, right-click on the test class and select
Run Tests.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 24
Some options to run tests in Test Explorer

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 25
A controller that depends on a repository class
public class HomeController : Controller
{
private IRepository<Book> data { get; set; }
public HomeController(IRepository<Book> rep) => data = rep;

public ViewResult Index() {


var random = data.Get(new QueryOptions<Book> {
OrderBy = b => Guid.NewGuid()
});
return View(random);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 26
A fake repository class that implements
the Get() method
using Bookstore.Models;
...
public class FakeBookRepository : IRepository<Book>
{
public int Count =>
throw new NotImplementedException();
public void Delete(Book entity) =>
throw new NotImplementedException();
public Book Get(QueryOptions<Book> options) =>
new Book();
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 27
A unit test that passes an instance
of the fake repository to the controller
[Fact]
public void IndexActionMethod_ReturnsAViewResult() {
// arrange
var rep = new FakeBookRepository();
var controller = new HomeController(rep);

// act
var result = controller.Index();

// assert
Assert.IsType<ViewResult>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 28
An action method that accesses TempData
[HttpPost]
public IActionResult Edit(Author author)
{
if (ModelState.IsValid) {
data.Update(author);
data.Save();
TempData["message"] = $"{author.FullName} updated.";
return RedirectToAction("Index");
}
else {
return View("Author", author);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 29
The FakeTempData class with an indexer
that does nothing
using Microsoft.AspNetCore.Mvc.ViewFeatures;
...
public class FakeTempData : ITempDataDictionary
{
public object this[string key] { get => null; set { } }
public ICollection<string> Keys =>
throw new NotImplementedException();
...
}

Two methods of the FakeAuthorRepository class


that do nothing
public void Update(Author entity) { }
public void Save() { }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 30
A test method that tests the action method
that uses TempData
[Fact]
public void Edit_POST_ReturnsRedirectToActionResultIfModelStateIsValid() {
// arrange
var rep = new FakeAuthorRepository();
var controller = new AuthorController(rep) {
TempData = new FakeTempData()
};

// act
var result = controller.Edit(new Author());

// assert
Assert.IsType<RedirectToActionResult>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 31
How to add Moq to your test project
1. In the Solution Explorer, right-click the test project and select
Manage NuGet Packages from the resulting menu.
2. In the NuGet Package Manager, click Browse, search for
“moq”, select the Moq package, and click Install.
3. In the resulting dialogs, click OK and I Accept.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 32
Two methods of the Mock<T> class
Setup(lambda)
Returns(value)

One property of the Mock<T> class


Object

Two static methods of the It class


IsAny<T>()
Is<T>(lambda)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 33
The using directive for the Moq namespace
using Moq;

Code that creates a Mock<IRepository<Author>>


object
var rep = new Mock<IRepository<Author>>();

Code that sets up Get() to accept any int


and return an Author object
rep.Setup(m => m.Get(It.IsAny<int>())).Returns(new Author());

The same statement adjusted to accept


any int greater than zero
rep.Setup(m => m.Get(It.Is<int>(i => i > 0))).Returns(
new Author());

Code that passes the mock object to a controller


var controller = new AuthorController(rep.Object);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 34
A test method that mocks a repository object
using Bookstore.Controllers;
...
[Fact]
public void IndexActionMethod_ReturnsViewResult()
{
// arrange
var rep = new Mock<IRepository<Book>>();
var controller = new HomeController(rep.Object);

// act
var result = controller.Index();

// assert
Assert.IsType<ViewResult>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 35
A test method that mocks a TempData object
using Bookstore.Areas.Admin.Controllers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
...
[Fact]
public void Edit_POST_ReturnsRedirectToActionResultIfModelStateIsValid()
{
// arrange
var rep = new Mock<IRepository<Author>>();
var temp = new Mock<ITempDataDictionary>();
var controller = new AuthorController(rep.Object)
{
TempData = temp.Object
};

// act
var result = controller.Edit(new Author());

// assert
Assert.IsType<RedirectToActionResult>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 36
The constructor of the Cart class
public Cart(IHttpContextAccessor ctx)
{
// assign private variables
session = ctx.HttpContext.Session;
requestCookies = ctx.HttpContext.Request.Cookies;
responseCookies = ctx.HttpContext.Response.Cookies;
items = new List<CartItem>();
}

The Subtotal property of the Cart class


public double Subtotal => items.Sum(i => i.Subtotal);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 37
A test method that tests the Subtotal property
of the Cart with Moq (part 1)
...
using Microsoft.AspNetCore.Http;
...
[Fact]
public void SubtotalProperty_ReturnsADouble()
{
// arrange
var accessor = new Mock<IHttpContextAccessor>();
var context = new DefaultHttpContext();

accessor.Setup(m => m.HttpContext).Returns(context);


accessor.Setup(m => m.HttpContext.Request)
.Returns(context.Request);
accessor.Setup(m => m.HttpContext.Response)
.Returns(context.Response);
accessor.Setup(m => m.HttpContext.Request.Cookies)
.Returns(context.Request.Cookies);
accessor.Setup(m => m.HttpContext.Response.Cookies)
.Returns(context.Response.Cookies);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 38
A test method that tests the Subtotal property
of the Cart with Moq (part 2)
var session = new Mock<ISession>();
accessor.Setup(m => m.HttpContext.Session)
.Returns(session.Object);

Cart cart = new Cart(accessor.Object);


cart.Add(new CartItem { Book = new BookDTO() });

// act
var result = cart.Subtotal;

// assert
Assert.IsType<double>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 39
The Test Explorer for the Bookstore.Tests project

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 40
The BookControllerTests class (part 1)
using Microsoft.AspNetCore.Mvc;
using Xunit;
using Moq;
using Bookstore.Controllers;
using Bookstore.Models;

namespace Bookstore.Tests
{
public class BookControllerTests
{
[Fact]
public void Index_ReturnsARedirectToActionResult() {
// arrange
var unit = new Mock<IBookstoreUnitOfWork>();
var controller = new BookController(unit.Object);

// act
var result = controller.Index();

// assert
Assert.IsType<RedirectToActionResult>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 41
The BookControllerTests class (part 2)
[Fact]
public void Index_RedirectsToListActionMethod() {
// arrange
var unit = new Mock<IBookstoreUnitOfWork>();
var controller = new BookController(unit.Object);

// act
var result = controller.Index();

// assert
Assert.Equal("List", result.ActionName);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 42
The BookControllerTests class (part 3)
[Fact]
public void Details_ModelIsABookObject() {
// arrange
var bookRep = new Mock<IRepository<Book>>();
bookRep.Setup(m => m.Get(
It.IsAny<QueryOptions<Book>>()))
.Returns(new Book { BookAuthors =
new List<BookAuthor>() });
var unit = new Mock<IBookstoreUnitOfWork>();
unit.Setup(m => m.Books).Returns(bookRep.Object);

var controller = new BookController(unit.Object);

// act
var model =
controller.Details(1).ViewData.Model as Book;

// assert
Assert.IsType<Book>(model);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 43
The AdminBookControllerTests class (part 1)
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Xunit;
using Moq;
using Bookstore.Areas.Admin.Controllers;
using Bookstore.Models;

namespace Bookstore.Tests
{
public class AdminBookControllerTests
{
public IBookstoreUnitOfWork GetUnitOfWork()
{
// set up Book repository
var bookRep = new Mock<IRepository<Book>>();
bookRep.Setup(m => m.Get(It.IsAny<QueryOptions<Book>>()))
.Returns(new Book { BookAuthors = new List<BookAuthor>() });
bookRep.Setup(m => m.List(It.IsAny<QueryOptions<Book>>()))
.Returns(new List<Book>());
bookRep.Setup(m => m.Count).Returns(0);

// set up Author repository


var authorRep = new Mock<IRepository<Author>>();
authorRep.Setup(m => m.List(It.IsAny<QueryOptions<Author>>()))
.Returns(new List<Author>());

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 44
The AdminBookControllerTests class (part 2)
// set up Genre repository
var genreRep = new Mock<IRepository<Genre>>();
genreRep.Setup(m => m.List(It.IsAny<QueryOptions<Genre>>()))
.Returns(new List<Genre>());

// set up unit of work


var unit = new Mock<IBookstoreUnitOfWork>();
unit.Setup(m => m.Books).Returns(bookRep.Object);
unit.Setup(m => m.Authors).Returns(authorRep.Object);
unit.Setup(m => m.Genres).Returns(genreRep.Object);

return unit.Object;
}

[Fact]
public void Edit_GET_ModelIsBookObject()
{
// arrange
var unit = GetUnitOfWork();
var controller = new BookController(unit);

// act
var model = controller.Edit(1).ViewData.Model as BookViewModel;

// assert
Assert.IsType<BookViewModel>(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 45
The AdminBookControllerTests class (part 3)
[Fact]
public void Edit_POST_ReturnsViewResultIfModelIsNotValid()
{
// arrange
var unit = GetUnitOfWork();
var controller = new BookController(unit);
controller.ModelState.AddModelError("", "Test error message.");
BookViewModel vm = new BookViewModel();

// act
var result = controller.Edit(vm);

// assert
Assert.IsType<ViewResult>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 46
The AdminBookControllerTests class (part 4)
[Fact]
public void Edit_POST_ReturnsRedirectToActionResultIfModelIsValid()
{
// arrange
var unit = GetUnitOfWork();
var controller = new BookController(unit);
var temp = new Mock<ITempDataDictionary>();
controller.TempData = temp.Object;
BookViewModel vm = new BookViewModel { Book = new Book() };

// act
var result = controller.Edit(vm);

// assert
Assert.IsType<RedirectToActionResult>(result);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 47
The CartTests class (part 1)
using System;
using System.Linq;
using Xunit;
using Moq;
using Microsoft.AspNetCore.Http;
using Bookstore.Models;

namespace Bookstore.Tests
{
public class CartTests
{
private Cart GetCart()
{
// create HTTP context accessor
var accessor = new Mock<IHttpContextAccessor>();

// setup request and response cookies


var context = new DefaultHttpContext();
accessor.Setup(m => m.HttpContext)
.Returns(context);
accessor.Setup(m => m.HttpContext.Request)
.Returns(context.Request);
accessor.Setup(m => m.HttpContext.Response)
.Returns(context.Response);

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 48
The CartTests class (part 2)
accessor.Setup(m => m.HttpContext.Request.Cookies)
.Returns(context.Request.Cookies);
accessor.Setup(m => m.HttpContext.Response.Cookies)
.Returns(context.Response.Cookies);

// setup session
var session = new Mock<ISession>();
accessor.Setup(m => m.HttpContext.Session)
.Returns(session.Object);

return new Cart(accessor.Object);


}

[Fact]
public void Subtotal_ReturnsADouble()
{
// arrange
Cart cart = GetCart();
cart.Add(new CartItem { Book = new BookDTO() });

// act
var result = cart.Subtotal;

// assert
Assert.IsType<double>(result);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 49
The CartTests class (part 3)
[Theory]
[InlineData(9.99, 6.89, 12.99)]
[InlineData(8.97, 45.00, 9.99, 15.00)]
public void Subtotal_ReturnsCorrectCalculation(
params double[] prices)
{
// arrange
Cart cart = GetCart();
for (int i = 0; i < prices.Length; i++)
{
var item = new CartItem
{
Book = new BookDTO { BookId = i, Price = prices[i] },
Quantity = 1
};
cart.Add(item);
}
double expected = prices.Sum();

// act
var result = cart.Subtotal;

// assert
Assert.Equal(Math.Round(expected, 2), Math.Round(result, 2));
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C14, Slide 50
Chapter 15

How to work
with tag helpers,
partial views,
and view components

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 1
Objectives (part 1)
Applied
1. Reduce code duplication in the views of a web app by creating
custom tag helpers, partial views, and view components.

Knowledge
1. Distinguish between tag helper attributes and elements.
2. Explain how to register tag helpers.
3. Explain how to create custom tag helpers for standard and non-
standard HTML elements.
4. Describe the use of the HtmlTarget attribute to control the scope of
a tag helper.
5. Describe the use of the TagHelperOutput class and the TagBuilder
class to add an HTML element before or after the tag helper
element.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 2
Objectives (part 2)
6. Describe the use of properties within a tag helper class.
7. Describe the use of dependency injection with a tag helper.
8. Describe the use of the SuppressOutput() method of the
TagHelperOutput class to create tag helpers that send an element
to the browser only under certain conditions.
9. Describe the use of the GetPathByAction() method of the
LinkGenerator class to generate a route-based URL.
10. Describe what a partial view typically contains.
11. List and describe the paths that MVC searches for a partial view.
12. Describe how to use the partial tag helper to include a partial view
and pass a model to it.
13. Distinguish between a partial view and a view component.
14. Describe how a view component works and how data can be
passed to it.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 3
Common built-in tag helpers
asp-action
asp-controller
asp-area
asp-for
asp-validation-summary
asp-validation-for
environment
partial

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 4
A tag helper that generates attributes
for an <input> tag
<input asp-for="FirstName" class="form-control" />

The HTML that’s sent to the browser


<input class="form-control" type="text" data-val="true"
data-val-maxlength="The field FirstName must be a string
or array type with a maximum length of &#x27;200&#x27;."
data-val-maxlength-max="200" data-val-required="Please
enter a first name." id="FirstName" maxlength="200"
name="FirstName" value="" />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 5
A tag helper that generates a route-based URL
in a <form> tag
<form asp-action="Edit" method="post"></form>

The HTML that’s sent to the browser


<form action="/Home/Edit" method="post"></form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 6
Tag helpers that output a different CSS link
based on hosting environment
<environment include="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/css/bootstrap.min.css" />
</environment>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 7
A _ViewImports.cshtml file that registers
all built-in and custom tag helpers
...
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Bookstore

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 8
Razor code that uses HTML helpers (old way)
@{ Html.BeginForm("Edit", "Home", FormMethod.Post,
new { @class = "form-inline" }); }
@Html.LabelFor(m => m.Title)
@Html.TextBoxFor(m => m.Title,
new { @class = "form-control m-2" })
@Html.LabelFor(m => m.Price)
@Html.TextBoxFor(m => m.Price,
new { @class = "form-control m-2",
placeholder = "e.g., $14.99" })
@{ Html.EndForm(); }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 9
HTML that uses tag helpers (new way)
<form asp-action="Edit" method="post"
class="form-inline">
<label asp-for="Title">Title</label>
<input asp-for="Title" class="form-control m-2" />
<label asp-for="Price">Price</label>
<input asp-for="Price" class="form-control m-2"
placeholder="e.g., $14.99" />
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 10
The HTML that both examples send
to the browser
<form action="/Home/Edit" method="post"
class="form-inline">
<label for="Title">Title</label>
<input type="text" id="Title" name="Title" value=""
class="form-control m-2" />
<label for="Price">Price</label>
<input type="text" id="Price" name="Price" value=""
class="form-control m-2"
placeholder="e.g., $14.99" />
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 11
The using directive for the TagHelpers namespace
using Microsoft.AspNetCore.Razor.TagHelpers;

One virtual method of the TagHelper class


Process(ctx, out)

One property of the TagHelperOutput class


Attributes

One method of the TagHelperAttributeList class


SetAttribute(name, val)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 12
A tag helper that applies to any standard
HTML <button> element
public class ButtonTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
output.Attributes.SetAttribute(
"class", "btn btn-primary");
}
}

Two button elements in a view


<button type="submit">Submit</button>
<button type="reset">Reset Form</button>

The HTML that MVC sends to the browser


<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-primary">Reset Form</button>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 13
More properties of the TagHelperOutput class
TagName
TagMode

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 14
A tag helper that applies to any non-standard
<submit-button> element
public class SubmitButtonTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
// make it a button element with start and end tags
output.TagName = "button";
output.TagMode = TagMode.StartTagAndEndTag;

// make it a submit button


output.Attributes.SetAttribute("type", "submit");

// append bootstrap button classes


string newClasses = "btn btn-primary";
string oldClasses =
output.Attributes["class"]?.Value?.ToString();
string classes = (string.IsNullOrEmpty(oldClasses)) ?
newClasses : $"{oldClasses} {newClasses}";
output.Attributes.SetAttribute("class", classes);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 15
A submit-button element in a view
<submit-button class="mr-2">Submit</submit-button>

The HTML that MVC sends to the browser


<button type="submit" class="mr-2 btn btn-primary">
Submit</button>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 16
Three extension methods for tag helpers (part 1)
using Microsoft.AspNetCore.Razor.TagHelpers;
...
public static class TagHelperExtensions
{
public static void AppendCssClass(
this TagHelperAttributeList list, string newCssClasses)
{
string oldCssClasses = list["class"]?.Value?.ToString();
string cssClasses = (string.IsNullOrEmpty(oldCssClasses)) ?
newCssClasses : $"{oldCssClasses} {newCssClasses}";
list.SetAttribute("class", cssClasses);
}

public static void BuildTag(this TagHelperOutput output,


string tagName, string classNames)
{
output.TagName = tagName;
output.TagMode = TagMode.StartTagAndEndTag;
output.Attributes.AppendCssClass(classNames);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 17
Three extension methods for tag helpers (part 2)
public static void BuildLink(this TagHelperOutput output,
string url, string className)
{
output.BuildTag("a", className);
output.Attributes.SetAttribute("href", url);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 18
Two tag helpers that use the extension methods
public class ButtonTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
output.Attributes.AppendCssClass("btn btn-primary");
}
}

public class SubmitButtonTagHelper : TagHelper


{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
output.BuildTag("button", "btn btn-primary");
output.Attributes.SetAttribute("type", "submit");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 19
Two properties of the HtmlTargetElement attribute
Attributes
ParentTag

Use the HTMLTargetElement attribute to…


 Allow a tag helper class to have a different name than the HTML
element it targets.
 Narrow the scope of a tag helper so it only targets an element
under certain conditions.
 Widen the scope of a tag helper so it targets multiple elements.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 20
A tag helper for any <label> element
public class LabelTagHelper : TagHelper {...}

Another tag helper for any <label> element


[HtmlTargetElement("label")]
public class MyLabelTagHelper : TagHelper {...}

A tag helper for bound <label> elements in a form


[HtmlTargetElement("label", Attributes = "asp-for",
ParentTag = "form")]
public class FormLabelTagHelper : TagHelper {...}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 21
A tag helper for <input> or <select> elements
in a form
[HtmlTargetElement("input", ParentTag = "form")]
[HtmlTargetElement("select", ParentTag = "form")]
public class FormTagHelper : TagHelper {...}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 22
A tag helper for any element of the submit type
or any <a> element with a my-button attribute
[HtmlTargetElement(Attributes = "[type=submit]")]
[HtmlTargetElement("a", Attributes = "my-button")]
public class MyButtonTagHelper : TagHelper {...}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 23
Three properties of the TagHelperOutput class
PreElement
Content
PostElement

Two properties of the TagBuilder class


Attributes
InnerHtml

The using directive for the Rendering namespace


using Microsoft.AspNetCore.Mvc.Rendering;
// for TagBuilder

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 24
A tag helper that adds a <span> element
after the targeted element
[HtmlTargetElement("input", Attributes = "my-required")]
public class RequiredInputTagHelper : TagHelper
{
public override void Process(TagHelperContext context,
TagHelperOutput output)
{
// add CSS class to input element
output.Attributes.AppendCssClass("form-control");

// create a <span> element


TagBuilder span = new TagBuilder("span");
span.Attributes.Add("class","text-danger mr-2");
span.InnerHtml.Append("*");

// add span element after input element


output.PostElement.AppendHtml(span);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 25
An <input> element that uses the tag helper
<input asp-for="Title" my-required />

The HTML that’s sent to the browser


<input my-required type="text" id="Title" name="Title"
value="" class="form-control" />
<span class="text-danger mr-2">*</span>

The elements displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 26
A tag helper that generates numeric options
for a <select> element
[HtmlTargetElement("select",
Attributes = "my-min-number, my-max-number")]
public class NumberDropDownTagHelper : TagHelper
{
[HtmlAttributeName("my-min-number")]
public int Min { get; set; }

[HtmlAttributeName("my-max-number")]
public int Max { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
for (int i = Min; i <= Max; i++)
{
TagBuilder option = new TagBuilder("option");
option.InnerHtml.Append(i.ToString());
output.Content.AppendHtml(option);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 27
A view that uses the NumberDropDown tag helper
@model CartItem
...
<select asp-for="Quantity" class="form-control"
my-min-number="1" my-max-number="10"></select>

The HTML that’s sent to the browser


<select class="form-control" id="Quantity"
name="Quantity">
<option>1</option>
<option>2</option>
...
<option>10</option>
</select>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 28
The <select> element displayed in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 29
One property of the TagHelperContext class
AllAttributes

Three properties of the ModelExpression class


Name
Model
Metadata

The using statement for the ViewFeatures


namespace
using Microsoft.AspNetCore.Mvc.ViewFeatures;
// for ModelExpression

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 30
The updated NumberDropDownTagHelper class
[HtmlTargetElement("select", Attributes = "my-min-number, my-max-number")]
public class NumberDropDownTagHelper : TagHelper
{
// Min and Max properties same as before

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
// get selected value from view's model
ModelExpression aspfor =
(ModelExpression) context.AllAttributes["asp-for"].Value;
int modelValue = (int)aspfor?.Model;

for (int i = Min; i <= Max; i++)


{
TagBuilder option = new TagBuilder("option");
option.InnerHtml.Append(i.ToString());

// mark option as selected if matches model's value


if (modelValue == i)
option.Attributes["selected"] = "selected";

output.Content.AppendHtml(option);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 31
A tag helper that gets a ViewContext value
via dependency injection (part 1)
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering;
...
[HtmlTargetElement("a", Attributes = "[class=nav-link]",
ParentTag = "li")]
public class ActiveNavbarTagHelper : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewCtx { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
string area =
ViewCtx.RouteData.Values["area"]?.ToString() ?? "";
string ctlr =
ViewCtx.RouteData.Values["controller"].ToString();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 32
A tag helper that gets a ViewContext value
via dependency injection (part 2)
string aspArea = context.AllAttributes["asp-area"]?.Value?
.ToString() ?? "";
string aspCtlr =
context.AllAttributes["asp-controller"].Value
.ToString();

if (area == aspArea && ctlr == aspCtlr)


output.Attributes.AppendCssClass("active");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 33
A tag helper that gets a Cart object
via dependency injection
[HtmlTargetElement("span", Attributes = "my-cart-badge")]
public class CartBadgeTagHelper : TagHelper
{
private ICart cart;
public CartBadgeTagHelper(ICart c) => cart = c;

public bool MyCartBadge { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
output.Content.SetContent(cart.Count?.ToString());
}
}

The tag helper in a layout


<span class="fas fa-shopping-cart"></span>&nbsp;Cart
<span class="badge badge-light" my-cart-badge></span>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 34
A layout that only displays an element
if there’s a value in TempData
<main>
@if (TempData.Keys.Contains("message"))
{
<h4 class="bg-info text-center text-white p-2">
@TempData["message"]
</h4>
}
@RenderBody()
</main>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 35
A method of the TagHelperOutput class
SuppressOutput()

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 36
A tag helper that only outputs HTML
if there’s a value in TempData
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering;
...
[HtmlTargetElement("my-temp-message")]
public class TempMessageTagHelper : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewCtx { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
var td = ViewCtx.TempData;
if (td.Keys.Contains("message"))
{
output.BuildTag("h4", "bg-info text-center text-white p-2");
output.Content.SetContent(td["message"].ToString());
}
else
{
output.SuppressOutput();
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 37
A layout that uses the conditional tag helper
<main>
<my-temp-message />
@RenderBody()
</main>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 38
A tag helper that generates a paging link (part 1)
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Bookstore.Models;
...
[HtmlTargetElement("my-paging-link")]
public class PagingLinkTagHelper : TagHelper
{
private LinkGenerator linkBuilder;
public PagingLinkTagHelper(LinkGenerator lg) => linkBuilder = lg;

[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewCtx { get; set; }

public int Number { get; set; }


public RouteDictionary Current { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output) {
// update routes for this paging link
var routes = Current.Clone();
routes.PageNumber = Number;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 39
A tag helper that generates a paging link (part 2)
// get controller and action method, create paging link URL
string ctlr =
ViewCtx.RouteData.Values["controller"].ToString();
string action =
ViewCtx.RouteData.Values["action"].ToString();
string url =
linkBuilder.GetPathByAction(action, ctlr, routes);

// build up CSS string


string linkClasses = "btn btn-outline-primary";
if (Number == Current.PageNumber)
linkClasses += " active";

// create link
output.BuildLink(url, linkClasses);
output.Content.SetContent(Number.ToString());
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 40
A view that uses the PagingLink tag helper
@{
for (int i = 1; i <= Model.TotalPages; i++)
{
<my-paging-link number="@i"
current="@Model.CurrentRoute" />
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 41
The Add MVC View dialog

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 42
The paths that MVC searches for a partial view
/Views/ControllerName/PartialViewName
/Views/Shared/PartialViewName

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 43
A partial view that loads the jQuery validation
libraries
<script
src="~/lib/jquery-validation/dist/jquery.validate.min.js">
</script>
<script
src="~/lib/jquery-validation-unobtrusive/
jquery.validate.unobtrusive.min.js">
</script>

A tag helper that includes the partial view


in a view
<partial name="_ValidationScriptsPartial" />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 44
A partial view that contains the HTML
for a Bootstrap navbar menu button
<button class="navbar-toggler" type="button"
data-toggle="collapse"
data-target="#menu" aria-controls="menu"
aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

A layout that uses the partial view


in a Bootstrap navbar
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<partial name="_NavbarMenuButtonPartial" />
<div class="collapse navbar-collapse" id="menu">
<ul class="navbar-nav mr-auto">...</ul>
</div>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 45
Four attributes of the partial tag helper
name
model
for
viewData

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 46
The _BookLinkPartial partial view
@model Book

<a asp-action="Details" asp-controller="Book"


asp-route-id="@Model.BookId"
asp-route-slug="@Model.Title.Slug()">
@Model.Title
</a>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 47
The partial view in a view with the same
model object (Home/Index)
@model Book
...
<h5>
<partial name="_BookLinkPartial" />
</h5>
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 48
The partial view in a view with a different
model object (Book/List)
@model BookListViewModel
...
@foreach (Book book in Model.Items) {
<tr>
<td>
<partial name="_BookLinkPartial" model="@book" />
</td>
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 49
A method in a ViewComponent class
Invoke([params])

The folder for the view component classes


/Components

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 50
A view component that passes the Cart count
to a partial view
using Microsoft.AspNetCore.Mvc;
...
public class CartBadge : ViewComponent
{
private ICart cart { get; set; }
public CartBadge(ICart c) => cart = c;

public IViewComponentResult Invoke() =>


View(cart.Count);
}

The Default.cshtml partial view


@model int?
<span class="badge badge-light">@Model</span>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 51
The paths that MVC searches
for a view component’s partial view
/Views/ControllerName/Components/ViewComponentName/ViewName
/Views/Shared/Components/ViewComponentName/ViewName

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 52
A layout that uses tag helper syntax
to call the view component
<span class="fas fa-shopping-cart"></span>&nbsp;Cart
<vc:cart-badge></vc:cart-badge>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 53
The DropDownViewModel class
public class DropDownViewModel
{
public Dictionary<string, string> Items { get; set; }
public string SelectedValue { get; set; }
public string DefaultValue { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 54
A view component with an Invoke() method
that has a parameter
public class AuthorDropDown : ViewComponent
{
private IRepository<Author> data { get; set; }
public AuthorDropDown(IRepository<Author> rep) => data = rep;

public IViewComponentResult Invoke(string selectedValue)


{
var authors = data.List(new QueryOptions<Author> {
OrderBy = a => a.FirstName
});

var vm = new DropDownViewModel {


SelectedValue = selectedValue,
DefaultValue = "All",
Items = authors.ToDictionary(
a => a.AuthorId.ToString(), a => a.FullName)
};
return View(
"~/Views/Shared/Components/Common/DropDown.cshtml", vm);
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 55
The DropDown partial view
@model DropDownViewModel

<select name="filter" class="form-control m-2"


asp-items="@(new SelectList(
Model.Items, "Key", "Value", Model.SelectedValue))">
<option value="@Model.DefaultValue">All</option>
</select>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 56
A view that uses the view component
<label>Author: </label>
<vc:author-drop-down
selected-value="@Model.CurrentRoute.AuthorFilter">
</vc:author-drop-down>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 57
The generic GridViewModel class
public class GridViewModel<T>
{
public IEnumerable<T> Items { get; set; }
public RouteDictionary CurrentRoute { get; set; }
public int TotalPages { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 58
The List() action method of the Book controller
public ViewResult List(BooksGridDTO values)
{
/* code that creates GridBuilder and QueryOptions objects to
retrieve a list of paged, sorted, and filtered books */

var vm = new GridViewModel<Book> {


Items = data.List(options),
CurrentRoute = builder.CurrentRoute,
TotalPages = builder.GetTotalPages(data.Count)
};

return View(vm);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 59
The List() action method of the Author controller
public ViewResult List(GridDTO vals)
{
/* code that creates GridBuilder and QueryOptions objects to
retrieve a list of paged and sorted authors */

var vm = new GridViewModel<Author> {


Items = data.List(options),
CurrentRoute = builder.CurrentRoute,
TotalPages = builder.GetTotalPages(data.Count)
};

return View(vm);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 60
The Book Catalog page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 61
The custom tag helpers, partial views,
and view components in this page
1. The ActiveNavbar tag helper.
2. The CartBadge view component.
3. The TempMessage tag helper.
4. The AuthorDropDown view component. The other drop-down
lists are view components that work similarly.
5. The SortingLink tag helper.
6. The _BookLink partial view.
7. The _AuthorLink partial view.
8. The Button tag helper.
9. The paging-links partial view, containing the paging-link tag
helper.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 62
The updated ActiveNavbar tag helper
that accounts for the Admin area (part 1)
[HtmlTargetElement("a", Attributes = "[class=nav-link]",
ParentTag = "li")]
public class ActiveNavbarTagHelper : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewCtx { get; set; }

[HtmlAttributeName("my-mark-area-active")]
public bool IsAreaOnly { get; set; }

public override void Process(TagHelperContext context,


TagHelperOutput output)
{
string area = ViewCtx.RouteData.Values["area"]?
.ToString() ?? "";
string ctlr = ViewCtx.RouteData.Values["controller"]
.ToString();

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 63
The updated ActiveNavbar tag helper (part 2)
string aspArea = context.AllAttributes["asp-area"]?.Value?
.ToString() ?? "";
string aspCtlr = context.AllAttributes["asp-controller"]
.Value.ToString();

if (area == aspArea && ctlr == aspCtlr)


output.Attributes.AppendCssClass("active");
else if (IsAreaOnly && area == aspArea)
output.Attributes.AppendCssClass("active");
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 64
The two active navbar links
on the Manage Authors page of the Admin area

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 65
The layout of the Bookstore app (part 1)
<!DOCTYPE html>
<html lang="en">
<head>...</head>
<body>
<div class="container">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<partial name="_NavbarMenuButtonPartial" />
<div class="collapse navbar-collapse" id="menu">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" asp-action="Index"
asp-controller="Home" asp-area="">
<span class="fas fa-home"></span>
&nbsp;Home
</a>
</li>
@* nav item links for Books and Authors *@
</ul>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 66
The layout of the Bookstore app (part 2)
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a class="nav-link" asp-action="Index"
asp-controller="Cart" asp-area="">
<span class="fas fa-shopping-cart">
</span>&nbsp;Cart
<vc:cart-badge></vc:cart-badge>
</a>
</li>
@* nav item link for Registration goes here *@
<li class="nav-item">
<a class="nav-link" asp-action="Index"
asp-controller="Book" asp-area="Admin"
my-mark-area-active>
<span class="fas fa-cog"></span>
&nbsp;Admin
</a>
</li>
</ul>
</div>
</nav>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 67
The layout of the Bookstore app (part 3)
<header class="jumbotron text-center">
<a asp-action="Index" asp-controller="Home">
<img src="~/images/logo.png"
class="img-fluid center-block" />
</a>
</header>

<main>
<my-temp-message />
@RenderBody()
</main>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js">
</script>
<script src="~/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 68
The Book/List view (part 1)
@model GridViewModel<Book>
...
<form asp-action="Filter" method="post" class="form-inline">
<label>Author: </label>
<vc:author-drop-down
selected-value="@Model.CurrentRoute.AuthorFilter">
</vc:author-drop-down>

<label>Genre: </label>
<vc:genre-drop-down
selected-value="@Model.CurrentRoute.GenreFilter">
</vc:genre-drop-down>

<label>Price: </label>
<vc:price-drop-down
selected-value="@Model.CurrentRoute.PriceFilter">
</vc:price-drop-down>

<button type="submit" class="mr-2">Filter</button>


<button type="submit" name="clear" value="true">Clear</button>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 69
The Book/List view (part 2)
<form asp-action="Add" asp-controller="Cart" method="post">
<table class="table table-bordered table-striped table-sm">
<thead class="thead-dark">
<tr>
<th>
<my-sorting-link sort-field="Title"
current="@Model.CurrentRoute">Title
</my-sorting-link></th>
<th>Author(s)</th>
<th>
<my-sorting-link sort-field="Genre"
current="@Model.CurrentRoute">Genre
</my-sorting-link></th>
<th>
<my-sorting-link sort-field="Price"
current="@Model.CurrentRoute">Price
</my-sorting-link></th>
<th></th>
</tr>
</thead>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 70
The Book/List view (part 3)
<tbody>
@foreach (Book book in Model.Items) {
<tr>
<td><partial name="_BookLinkPartial" model="@book" />
</td>
<td>
@foreach (var ba in book.BookAuthors) {
<p><partial name="_AuthorLinkPartial"
model="@ba.Author" /></p>
}
</td>
<td>@book.Genre?.Name</td>
<td>@book.Price.ToString("c")</td>
<td><button type="submit" name="id"
value="@book.BookId">Add To Cart
</button>
</td>
</tr>
}
</tbody>
</table>
</form>
<partial name="_PagingLinksPartial" />

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C15, Slide 71
Chapter 16

How to
authenticate and
authorize users

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 1
Objectives (part 1)
Applied
1. Restrict access to some or all pages of a web app.
2. Allow users who have registered and logged in to access the
restricted pages that they are authorized to access.

Knowledge
1. Distinguish between authentication and authorization.
2. Describe how individual user account authentication works.
3. Describe how to use authorization attributes to restrict access to
controllers and actions.
4. List and describe three properties of the IdentityUser class.
5. Describe how to add the Identity tables to the DB context class and
the database.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 2
Objectives (part 2)
6. Describe how to add the Identity service to the HTTP request and
response pipeline, including how to configure password options.
7. Describe how to inject a SignInManager<T> object into a layout
and use it to add Log In/Out buttons depending on whether the
user is currently logged in.
8. Describe how to inject the UserManager<T>, SignInManager<T>,
and RoleManager<T> objects into the Account controller.
9. Describe how to use the UserManager<T> class to create, update,
and delete users.
10. Describe how to use the SignInManager<T> class to log users in
and out.
11. Define roles and describe how they are used for authorization.
12. Describe how to use the RoleManager<T> and UserManager<T>
classes to work with roles.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 3
Windows-based authentication
 Causes the browser to display a login dialog box when the user
attempts to access a restricted page.
 Is supported by most browsers.
 Is configured through the IIS management console.
 Uses Windows user accounts and directory rights to grant access
to restricted pages.
 Is most appropriate for an intranet app.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 4
Individual user account authentication
 Allows developers to code a login page that gets the username
and password.
 Encrypts the username and password entered by the user if the
login page uses a secure connection.
 Doesn’t rely on Windows user accounts.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 5
Third-party authentication services
 Is provided by third parties such as Google, Facebook, Twitter,
and Microsoft using technologies like OpenID and OAuth.
 Allows users to use their existing logins and frees developers
from having to worry about the secure storage of user credentials.
 Can issue identities or accept identities from other web apps and
access user data on other services.
 Can use two-factor authentication.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 6
HTTP requests and responses

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 7
Some classes provided by ASP.NET Identity
IdentityDbContext
IdentityUser
IdentityRole
UserManager
RoleManager
SignInManager
IdentityResult

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 8
Some benefits of Identity
 It can be used with all ASP.NET frameworks.
 You have control over the schema of the data store that holds
user information, and you can change the storage system from
the default of SQL Server.
 It’s modular, so it’s easier to unit test.
 It supports claims-based authentication, which can be more
flexible than using simple roles.
 It supports third-party authentication providers.
 It’s based on OWIN (Open Web Interface for .NET) middleware.
 It’s distributed as a NuGet package, so Microsoft can deliver new
features and bug fixes faster than before.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 9
Attributes for authorization
AllowAnonymous
Authorize
Authorize(Roles = "r1, r2")

Using directive for the Authorization namespace


using Microsoft.AspNetCore.Authorization;

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 10
Only allow logged in users
to access an entire controller
[Authorize]
public class CartController : Controller {
...
}

Only allow logged in users in the Admin role


to access an entire controller
[Authorize(Roles = "Admin")]
public class BookController : Controller {
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 11
Different attributes for different action methods
public class ProductController : Controller
{
...
[AllowAnonymous]
[HttpGet]
public IActionResult List() {
...
}

[Authorize]
[HttpGet]
public IActionResult Add()
{
...
}

[Authorize(Roles = "Admin")]
[HttpGet]
public IActionResult Delete(int id)
{
...
}
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 12
The NuGet package for Identity with EF Core
Microsoft.AspNetCore.Identity.EntityFrameworkCore

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 13
Some properties of the IdentityUser class
UserName
Password
ConfirmPassword
Email
EmailConfirmed
PhoneNumber
PhoneNumberConfirmed

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 14
The User entity class
using Microsoft.AspNetCore.Identity;

namespace Bookstore.Models
{
public class User : IdentityUser {
// Inherits all IdentityUser properties
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 15
The Bookstore context class (part 1)
...
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

namespace Bookstore.Models
{
public class BookstoreContext : IdentityDbContext<User>
{
public BookstoreContext(
DbContextOptions<BookstoreContext> options)
: base(options) { }

public DbSet<Author> Authors { get; set; }


public DbSet<Book> Books { get; set; }
public DbSet<BookAuthor> BookAuthors { get; set; }
public DbSet<Genre> Genres { get; set; }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 16
The Bookstore context class (part 2)
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// BookAuthor: set primary key


modelBuilder.Entity<BookAuthor>()
.HasKey(ba => new { ba.BookId, ba.AuthorId });
...
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 17
How to add Identity tables to the database
1. Start the Package Manager Console (PMC).
2. Add a migration that adds the tables by entering a command
like this one:
Add-Migration AddIdentityTables
3. Update the database by entering a command like this one:
Update-Database

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 18
The Up() method of the generated migration class
(part 1)
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AspNetRoles",
columns: table => new
{
Id = table.Column<string>(nullable: false),
Name = table.Column<string>(maxLength: 256, nullable: true),
NormalizedName =
table.Column<string>(maxLength: 256, nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 19
The Up() method (part 2)
migrationBuilder.CreateTable(
name: "AspNetUsers",
columns: table => new
{
Id = table.Column<string>(nullable: false),
UserName = table.Column<string>(maxLength: 256, nullable: true),
NormalizedUserName =
table.Column<string>(maxLength: 256, nullable: true),
Email = table.Column<string>(maxLength: 256, nullable: true),
NormalizedEmail =
table.Column<string>(maxLength: 256, nullable: true),
EmailConfirmed = table.Column<bool>(nullable: false),
PasswordHash = table.Column<string>(nullable: true),
SecurityStamp = table.Column<string>(nullable: true),
ConcurrencyStamp = table.Column<string>(nullable: true),
PhoneNumber = table.Column<string>(nullable: true),
PhoneNumberConfirmed = table.Column<bool>(nullable: false),
TwoFactorEnabled = table.Column<bool>(nullable: false),
LockoutEnd = table.Column<DateTimeOffset>(nullable: true),
LockoutEnabled = table.Column<bool>(nullable: false),
AccessFailedCount = table.Column<int>(nullable: false),
Discriminator = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
});
...

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 20
The using directive for the Identity namespace
using Microsoft.AspNetCore.Identity;

How to add the Identity service


with default password options
public void ConfigureServices(
IServiceCollection services)
{
...
services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<BookstoreContext>()
.AddDefaultTokenProviders();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 21
Some properties of the PasswordOptions class
RequiredLength
RequireLowercase
RequireUppercase
RequireDigit
RequireNonAlphanumeric

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 22
How to configure password options
public void ConfigureServices(
IServiceCollection services)
{
...
services.AddIdentity<User, IdentityRole>(options => {
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireDigit = false;
}).AddEntityFrameworkStores<BookstoreContext>()
.AddDefaultTokenProviders();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 23
How to configure your app to use authentication
and authorization
public void Configure(IAppBuilder app,
IWebHostEnvironment env)
{
...
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseSession();
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 24
The Register link and Log In button in the navbar

The Log Out button in the navbar

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 25
Some of the code for the navbar (part 1)
<!-- Home, Books, Authors, and Cart links go here -->

@using Microsoft.AspNetCore.Identity
@inject SignInManager<User> signInManager
@if (signInManager.IsSignedIn(User))
{
// signed-in user - Log Out button and username
<li class="nav-item">
<form method="post" asp-action="Logout"
asp-controller="Account" asp-area="">
<input type="submit" value="Log Out"
class="btn btn-outline-light" />
<span class="text-light">@User.Identity.Name</span>
</form>
</li>
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 26
Some of the code for the navbar (part 2)
else
{
// get current action
var action = ViewContext.RouteData.Values["action"]?.ToString();

// anonymous user - Register link and Log In button


<li class="nav-item @Nav.Active("Register", action)">
<a asp-action="Register" asp-controller="Account"
asp-area="" class="nav-link">Register</a>
</li>
<li class="nav-item">
<a asp-action="Login" asp-controller="Account"
asp-area="" class="btn btn-outline-light">Log In</a>
</li>
}

<!-- Admin link goes here -->

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 27
Some starting code for the Account controller
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Identity;
using Bookstore.Models;

namespace Bookstore.Controllers
{
public class AccountController : Controller
{
private UserManager<User> userManager;
private SignInManager<User> signInManager;

public AccountController(UserManager<User> userMngr,


SignInManager<User> signInMngr)
{
userManager = userMngr;
signInManager = signInMngr;
}

// The Register(), LogIn(), and LogOut()methods go here


}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 28
The URL that MVC redirects to
for an unauthenticated request
/account/login

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 29
The Register page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 30
The Register view model
using System.ComponentModel.DataAnnotations;

namespace Bookstore.Models
{
public class RegisterViewModel
{
[Required(ErrorMessage = "Please enter a username.")]
[StringLength(255)]
public string Username { get; set; }

[Required(ErrorMessage = "Please enter a password.")]


[DataType(DataType.Password)]
[Compare("ConfirmPassword")]
public string Password { get; set; }

[Required(ErrorMessage = "Please confirm your password.")]


[DataType(DataType.Password)]
[Display(Name = "Confirm Password")]
public string ConfirmPassword { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 31
The Register() action method for GET requests
[HttpGet]
public IActionResult Register()
{
return View();
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 32
The Account/Register view (part 1)
@model RegisterViewModel
@{
ViewBag.Title = "Register";
}

<h1>Register</h1>
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<form method="post" asp-action="Register">
<div class="form-group row">
<div class="col-sm-2"><label>Username:</label></div>
<div class="col-sm-4">
<input asp-for="Username" class="form-control" />
</div>
<div class="col">
<span asp-validation-for="Username"
class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"><label>Password:</label></div>
<div class="col-sm-4">
<input type="password" asp-for="Password"
class="form-control" />
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 33
The Account/Register view (part 2)
<div class="col">
<span asp-validation-for="Password"
class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"><label>Confirm Password:</label></div>
<div class="col-sm-4">
<input type="password" asp-for="ConfirmPassword"
class="form-control" />
</div>
</div>
<div class="row">
<div class="offset-2 col-sm-4">
<button type="submit" class="btn btn-primary">
Register</button>
</div>
</div>
<div class="row">
<div class="offset-2 col-sm-4">
Already registered? <a asp-action="LogIn">Log In</a>
</div>
</div>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 34
Three methods of the UserManager class
CreateAsync(user)
UpdateAsync(user)
DeleteAsync(user)

Two methods of the SignInManager class


SignInAsync(user, isPersistent)
SignOutAsync()

Two properties of the IdentityResult class


Succeeded
Errors

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 35
The Register() action method for POST requests
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid) {
var user = new User { UserName = model.Username };
var result = await userManager.CreateAsync(user,
model.Password);
if (result.Succeeded) {
await signInManager.SignInAsync(user,
isPersistent: false);
return RedirectToAction("Index", "Home");
}
else {
foreach (var error in result.Errors) {
ModelState.AddModelError("", error.Description);
}
}
}
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 36
The LogOut() action method for POST requests
[HttpPost]
public async Task<IActionResult> LogOut() {
await signInManager.SignOutAsync();
return RedirectToAction("Index", "Home");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 37
The Login page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 38
The Login view model
using System.ComponentModel.DataAnnotations;

namespace Bookstore.Models
{
public class LoginViewModel
{
[Required(ErrorMessage = "Please enter a username.")]
[StringLength(255)]
public string Username { get; set; }

[Required(ErrorMessage = "Please enter a password.")]


[StringLength(255)]
public string Password { get; set; }

public string ReturnUrl { get; set; }

public bool RememberMe { get; set; }


}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 39
The LogIn() action method for GET requests
[HttpGet]
public IActionResult LogIn(string returnURL = "")
{
var model = new LoginViewModel { ReturnUrl = returnURL };
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 40
The Login view (part 1)
@model LoginViewModel
@{
ViewBag.Title = "Login";
}

<h1>Login</h1>

<div asp-validation-summary="ModelOnly" class="text-danger"></div>


<form method="post" asp-action="LogIn"
asp-route-returnUrl="@Model.ReturnUrl">
<div class="form-group row">
<div class="col-sm-2"><label>Username:</label></div>
<div class="col-sm-4">
<input asp-for="Username" class="form-control" />
</div>
<div class="col">
<span asp-validation-for="Username"
class="text-danger"></span>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 41
The Login view (part 2)
<div class="form-group row">
<div class="col-sm-2"><label>Password:</label></div>
<div class="col-sm-4">
<input type="password" asp-for="Password"
class="form-control" />
</div>
<div class="col">
<span asp-validation-for="Password"
class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="offset-sm-2 col-sm-4">
<input type="checkbox" title="Remember Me"
asp-for="RememberMe" class="form-check" />
<label>Remember Me</label>
</div>
</div>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 42
The Login view (part 3)
<div class="row">
<div class="offset-2 col-sm-4">
<button type="submit" class="btn btn-primary">Log In
</button>
</div>
</div>
<div class="row">
<div class="offset-2 col-sm-4">
Not registered?
<a asp-action="Register">Register as a new user</a>
</div>
</div>
</form>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 43
Another method of the SignInManager class
PasswordSignInAsync(username, password, isPersistent,
lockoutOnFailure)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 44
The LogIn() action method for POST requests
[HttpPost]
public async Task<IActionResult> LogIn(LoginViewModel model)
{
if (ModelState.IsValid) {
var result = await signInManager.PasswordSignInAsync(
model.Username, model.Password,
isPersistent: model.RememberMe,
lockoutOnFailure: false);

if (result.Succeeded) {
if (!string.IsNullOrEmpty(model.ReturnUrl) &&
Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
}
ModelState.AddModelError("", "Invalid username/password.");
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 45
A property of the RoleManager class
Roles

Some of the methods of the RoleManager class


FindByIdAsync(id)
FindByNameAsync(name)
CreateAsync(role)
UpdateAsync(role)
DeleteAsync(role)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 46
Another property of the UserManager class
Users

More methods of the UserManager class


FindByIdAsync(id)
FindByNameAsync(name)
IsInRoleAsync(user, roleName)
AddToRoleAsync(user, roleName)
RemoveFromRoleAsync(user, roleName)
GetRolesAsync(user)

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 47
Code that loops through all users and their roles
foreach (User user in userManager.Users) {
foreach (IdentityRole role in roleManager.Roles) {
if (await userManager.IsInRoleAsync(user,
role.Name)) {
// perform some processing if user is in role
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 48
Code that creates a role named Admin
var result = await roleManager.CreateAsync(
new IdentityRole("Admin"));
if (result.Succeeded) { ... }

Code that deletes a role


IdentityRole role = await roleManager.FindByIdAsync(id);
var result = await roleManager.DeleteAsync(role);
if (result.Succeeded) { ... }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 49
Code that adds a user to the Admin role
User user = await userManager.FindByIdAsync(id);
var result = await userManager.AddToRoleAsync(user,
"Admin");
if (result.Succeeded) { ... }

Code that removes a user from the Admin role


User user = await userManager.FindByIdAsync(id);
var result = await userManager.RemoveFromRoleAsync(user,
"Admin");
if (result.Succeeded) { ... }

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 50
The Manage Users page

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 51
The updated User entity
using System.ComponentModel.DataAnnotations.Schema;
...
public class User : IdentityUser
{
[NotMapped]
public IList<string> RoleNames { get; set; };
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 52
The User view model
public class UserViewModel
{
public IEnumerable<User> Users { get; set; }
public IEnumerable<IdentityRole> Roles { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 53
The User controller and its Index() action method
(part 1)
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Bookstore.Models;
...
[Authorize(Role = "Admin")]
[Area("Admin")]
public class UserController : Controller
{
private UserManager<User> userManager;
private RoleManager<IdentityRole> roleManager;

public UserController(UserManager<User> userMngr,


RoleManager<IdentityRole> roleMngr)
{
userManager = userMngr;
roleManager = roleMngr;
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 54
The User controller and its Index() action method
(part 2)
public async Task<IActionResult> Index()
{
List<User> users = new List<User>();
foreach (User user in userManager.Users)
{
user.RoleNames = await userManager.GetRolesAsync(user);
users.Add(user);
}
UserViewModel model = new UserViewModel
{
Users = users,
Roles = roleManager.Roles
};
return View(model);
}

// the other action methods


}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 55
The User/Index view (part 1)
@model UserViewModel
@{
ViewData["Title"] = " | Manage Users";
}

<h1 class="mb-2">Manage Users</h1>

<h5 class="mt-2"><a asp-action="Add">Add a User</a></h5>

<table class="table table-bordered table-striped table-sm">


<thead>
<tr><th>Username</th><th>Roles</th>
<th></th><th></th><th></th>
</tr>
</thead>
<tbody>
@if (Model.Users.Count() == 0)
{
<tr><td colspan="5">There are no user accounts.</td></tr>
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 56
The User/Index view (part 2)
else
{
@foreach (User user in Model.Users)
{
<tr>
<td>@user.UserName</td>
<td>
@foreach (string roleName in user.RoleNames)
{
<div>@roleName</div>
}
</td>
<td>
<form method="post" asp-action="Delete"
asp-route-id="@user.Id">
<button type="submit" class="btn btn-primary">
Delete User</button>
</form>
</td>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 57
The User/Index view (part 3)
<td>
<form method="post" asp-action="AddToAdmin"
asp-route-id="@user.Id">
<button type="submit" class="btn btn-primary">
Add To Admin</button>
</form>
</td>
<td>
<form method="post" asp-action="RemoveFromAdmin"
asp-route-id="@user.Id">
<button type="submit" class="btn btn-primary">
Remove From Admin</button>
</form>
</td>
</tr>
}
}
</tbody>
</table>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 58
The User/Index view (part 4)
<h1 class="mb-2">Manage Roles</h1>

@if (Model.Roles.Count() == 0)
{
<form method="post" asp-action="CreateAdminRole">
<button type="submit" class="btn btn-primary">
Create Admin Role</button>
</form>
}
else
{
<table class="table table-bordered table-striped table-sm">
<thead>
<tr><th>Role</th><th></th></tr>
</thead>
<tbody>
@foreach (var role in Model.Roles)
{
<tr>
<td>@role.Name</td>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 59
The User/Index view (part 5)
<td>
<form method="post" asp-action="DeleteRole"
asp-route-id="@role.Id">
<button type="submit"
class="btn btn-primary">Delete Role
</button>
</form>
</td>
</tr>
}
</tbody>
</table>
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 60
Other action methods of the User controller
(part 1)
[HttpPost]
public async Task<IActionResult> Delete(string id)
{
User user = await userManager.FindByIdAsync(id);
if (user != null) {
IdentityResult result =
await userManager.DeleteAsync(user);
if (!result.Succeeded) { // if failed
string errorMessage = "";
foreach (IdentityError error in result.Errors) {
errorMessage += error.Description + " | ";
}
TempData["message"] = errorMessage;
}
}
return RedirectToAction("Index");
}

// the Add() methods work like the Register() methods

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 61
Other action methods (part 2)
[HttpPost]
public async Task<IActionResult> AddToAdmin(string id)
{
IdentityRole adminRole =
await roleManager.FindByNameAsync("Admin");
if (adminRole == null) {
TempData["message"] = "Admin role does not exist. "
+ "Click 'Create Admin Role' button to create it.";
}
else {
User user = await userManager.FindByIdAsync(id);
await userManager.AddToRoleAsync(user, adminRole.Name);
}
return RedirectToAction("Index");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 62
Other action methods (part 3)
[HttpPost]
public async Task<IActionResult> RemoveFromAdmin(string id)
{
User user = await userManager.FindByIdAsync(id);
await userManager.RemoveFromRoleAsync(user, "Admin");
return RedirectToAction("Index");
}

[HttpPost]
public async Task<IActionResult> DeleteRole(string id)
{
IdentityRole role = await roleManager.FindByIdAsync(id);
await roleManager.DeleteAsync(role);
return RedirectToAction("Index");
}

[HttpPost]
public async Task<IActionResult> CreateAdminRole()
{
await roleManager.CreateAsync(new IdentityRole("Admin"));
return RedirectToAction("Index");
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 63
Using directive for the Authorization attributes
using Microsoft.AspNetCore.Authorization;

The Cart controller requires users to be logged in


[Authorize]
public class CartController : Controller {
...
}

All controllers in the Admin area require users


to be in the Admin role
[Authorize(Roles = "Admin")]
[Area("Admin")]
public class BookController : Controller {
...
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 64
The AccessDenied() action method
of the Account controller
public ViewResult AccessDenied()
{
return View();
}

The code for the Account/AccessDenied view


@{
ViewBag.Title = "Access Denied";
}
<h2>Access Denied</h2>

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 65
The Account/AccessDenied view

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 66
A method added to the DB context class (part 1)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
...
public static async Task CreateAdminUser(
IServiceProvider serviceProvider)
{
UserManager<User> userManager =
serviceProvider.GetRequiredService<UserManager<User>>();
RoleManager<IdentityRole> roleManager = serviceProvider
.GetRequiredService<RoleManager<IdentityRole>>();

string username = "admin";


string password = "Sesame";
string roleName = "Admin";

// if role doesn't exist, create it


if (await roleManager.FindByNameAsync(roleName) == null)
{
await roleManager.CreateAsync(new IdentityRole(roleName));
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 67
A method added to the DB context class (part 2)
// if username doesn't exist, create it and add it to role
if (await userManager.FindByNameAsync(username) == null)
{
User user = new User { UserName = username };
var result = await userManager.CreateAsync(user, password);
if (result.Succeeded)
{
await userManager.AddToRoleAsync(user, roleName);
}
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 68
A statement in Startup.cs that calls the method
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
// all other configuration statements

BookstoreContext.CreateAdminUser(app.ApplicationServices)
.Wait();
}

An option in the Program.cs file


that allows the method to execute
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>()
.UseDefaultServiceProvider(
options => options.ValidateScopes = false);

});

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 69
The User/Change Password view

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 70
The User/ChangePassword view model
using System.ComponentModel.DataAnnotations;

namespace Bookstore.Models
{
public class ChangePasswordViewModel
{
public string Username { get; set; }

[Required(ErrorMessage = "Please enter password.")]


public string OldPassword { get; set; }

[Required(ErrorMessage = "Please enter new password.")]


[DataType(DataType.Password)]
public string NewPassword { get; set; }
}
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 71
The ChangePassword() action method for POSTs
[HttpPost]
public async Task<IActionResult> ChangePassword(
ChangePasswordViewModel model)
{
if (ModelState.IsValid)
{
User user =
await userManager.FindByNameAsync(model.Username);
var result = await userManager.ChangePasswordAsync(user,
model.OldPassword, model.NewPassword);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
foreach (IdentityError error in result.Errors)
{
ModelState.AddModelError("", error.Description);
}
}
}
return View(model);
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 72
The updated User class
public class User : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }

[NotMapped]
public IList<string> RoleNames { get; set; };
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 73
The updated Register view model (part 1)
public class RegisterViewModel
{
[Required(ErrorMessage = "Please enter a username.")]
[StringLength(255)]
public string Username { get; set; }

[Required(ErrorMessage = "Please enter a first name.")]


[StringLength(255)]
public string FirstName { get; set; }

[Required(ErrorMessage = "Please enter a last name.")]


[StringLength(255)]
public string Lastname { get; set; }

[Required(ErrorMessage = "Please enter an email address.")]


[DataType(DataType.EmailAddress)]
public string Email { get; set; } // from IdentityUser class

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 74
The updated Register view model (part 2)
[Required(ErrorMessage = "Please enter a password.")]
[DataType(DataType.Password)]
[Compare("ConfirmPassword")]
public string Password { get; set; }

[Required(ErrorMessage = "Please confirm your password.")]


[DataType(DataType.Password)]
[Display(Name = "Confirm Password")]
public string ConfirmPassword { get; set; }
}

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C16, Slide 75
Chapter 17

How to use
Visual Studio Code

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 1
Objectives (part 1)
Applied
1. Use Visual Studio Code to create and work with ASP.NET Core
MVC web apps.

Knowledge
1. Distinguish between Visual Studio Code and Visual Studio.
2. Describe how to use VS Code to open and close a folder for an
existing Visual Studio project.
3. List and describe the three modes Visual Studio Code provides for
viewing and editing files.
4. Describe how to use VS Code to run an ASP.NET Core project.
5. Describe how to use the CLI to execute the EF Core commands.
6. Describe how to use the CLI to create a new ASP.NET Core MVC
web app and add NuGet packages to it.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 2
Objectives (part 2)
7. Describe how to use VS Code to work with the folders and files of
a project.
8. Describe how to use the CLI to install and manage client-side
libraries.
9. Describe how to use VS Code to debug a web app.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 3
How to open a project folder
1. Start VS Code.
2. Select FileOpen Folder from the menu system.
3. Use the resulting dialog to select the folder that contains the
Visual Studio project. For the Future Value app from chapter 2,
you can select this folder:
\murach\aspnet_core_mvc\book_apps\Ch02FutureValue\
FutureValue
4. Click Select Folder.

How to close a project folder


 Select FileClose Folder from the menu system.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 4
A typical error message after opening
a Visual Studio project

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 5
How to fix errors after opening a project
 If you get a dialog that indicates that “Required assets to build
and debug are missing” and asks if you would like to add them,
click Yes. This adds a .vscode subfolder to your project folder
that contains .json files with VS Code configurations.
 If you get a dialog that says “There are unresolved
dependencies”, click Restore.
 If VS Code still displays many errors, including underlined code,
close VS Code and start it again. The problems should go away
when VS Code rereads its configuration files.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 6
VS Code with files in Standard
and Preview Modes

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 7
Three file display modes
Mode Useful for…
Preview Switching between files to quickly view
or edit them.
Standard Opening a file indefinitely for viewing
and editing.
Zen Focusing on editing a file’s code without
distraction of other interface elements.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 8
VS Code with an app running

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 9
The app in a browser

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 10
The error message that’s displayed
if the database hasn’t been created

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 11
How to install the CLI tools for .NET EF Core
1. Display the Terminal by selecting TerminalNew Terminal.
2. At the command prompt, enter this command:
dotnet tool install --global dotnet-ef

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 12
A command prompt for the Ch04MovieList app

How to create the database for a project


1. Display the Terminal by selecting TerminalNew Terminal.
2. At the command prompt, enter this command:
dotnet ef database update

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 13
A command that installs the CLI tools
for .NET EF Core
dotnet tool install --global dotnet-ef

Some of the .NET EF Core commands


(prefix with dotnet ef)
migrations add
migrations remove
database update
database drop
dbcontext scaffold

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 14
Two commands that add migrations
dotnet ef migrations add Initial
dotnet ef migrations add Genre

A command that updates the database


to the last migration
dotnet ef database update

A command that updates the database


to the specified migration
dotnet ef migrations update Initial

A command that removes the last migration


dotnet ef migrations remove

A command that drops the database


dotnet ef database drop

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 15
A command that generates DB classes
from an existing database
dotnet ef dbcontext scaffold name=BookstoreContext
Microsoft.EntityFrameworkCore.SqlServer
--output-dir Models/DataLayer --data-annotations --force

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 16
How to create a new project
for an ASP.NET Core MVC web app
1. Use your operating system to create a new root folder for your
project.
2. Start VS Code.
3. Select FileOpen Folder and use the resulting dialog to select
the root folder for the project.
4. Select TerminalNew Terminal to open a Terminal window.
5. Enter the following command:
dotnet new mvc

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 17
Templates for creating ASP.NET Core projects
Template name CLI argument
Web Application (Model-View-Controller) mvc
Empty web

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 18
How to use the CLI to add NuGet packages
Two commands that add EF Core packages
dotnet add package
Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design

A command that adds the Newtonsoft JSON package


dotnet add package
Microsoft.AspNetCore.Mvc.NewtonsoftJson

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 19
VS Code with some starting folders and files

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 20
VS Code using the CLI tools for LibMan
to install client-side libraries

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 21
A command that installs the CLI tools for LibMan
dotnet tool install --global
Microsoft.Web.LibraryManager.Cli

How to create a libman.json file for a project


1. At the Terminal command prompt for the project, enter this
command:
libman init
2. Press Enter to accept the default provider of CDNJS.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 22
How to use LibMan to install client-side libraries
1. Open the libman.json file and edit it to include the client-side
libraries you want to install as described in chapter 3.
2. At the Terminal command prompt, enter this command:
libman restore

How to delete all client-side libraries installed


by LibMan
 At the Terminal command prompt, enter this command:
libman clean

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 23
The Movie controller with a breakpoint

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 24
How to set and remove breakpoints
 To set a breakpoint, click in the margin indicator bar to the left of
the line number for a statement. This highlights the statement and
adds a breakpoint indicator (a red dot) in the margin.
 To remove a breakpoint, click the breakpoint indicator.
 To remove all breakpoints, select DebugRemove All
Breakpoints.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 25
How to enable and disable breakpoints
 To disable a breakpoint, right-click it and select Disable
Breakpoint.
 To enable a breakpoint, right-click it and select Enable
Breakpoint.
 To disable all breakpoints, select DebugDisable All
Breakpoints.
 To enable all breakpoints, select DebugEnable All
Breakpoints.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 26
The Movie List app in break mode

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 27
How to run an app with debugging
 Press F5.
 Select DebugStart Debugging.

© 2020, Mike Murach & Associates, Inc.


Murach's ASP.NET Core MVC C17, Slide 28

You might also like