Skip to main content

ASP.NET

Author @Saief1999

Part1, Introduction

MVC-based apps contain:

  • Models: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database. In this tutorial, a Movie model retrieves movie data from a database, provides it to the view or updates it. Updated data is written to a database.
  • Views: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.
  • Controllers: Classes that:
    • Handle browser requests.
    • Retrieve model data.
    • Call view templates that return a response.

In an MVC app, the view only displays information. The controller handles and responds to user input and interaction. For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database. For example:

  • https://localhost:5001/Home/Privacy: specifies the Home controller and the Privacy action.
  • https://localhost:5001/Movies/Edit/5: is a request to edit the movie with ID=5 using the Movies controller and the Edit action, which are detailed later in the tutorial.

Part2, Controllers

using Microsoft.AspNetCore.Mvc;
using System.Text.Encodings.Web;

namespace MvcMovie.Controllers
{
public class HelloWorldController : Controller
{
//
// GET: /HelloWorld/

public string Index()
{
return "This is my default action...";
}

//
// GET: /HelloWorld/Welcome/

public string Welcome()
{
return "This is the Welcome action method...";
}
}
}

Every public method in a controller is callable as an HTTP endpoint. In the sample above, both methods return a string. Note the comments preceding each method.

Browser window showing an app response of This is my default action

MVC invokes controller classes, and the action methods within them, depending on the incoming URL. The default URL routing logic used by MVC, uses a format like this to determine what code to invoke:

/[Controller]/[ActionName]/[Parameters]

The routing format is set in the Configure method in Startup.cs file.

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
  • When you browse to the app and don't supply any URL segments, it defaults to the "Home" controller and the "Index" method specified in the template line highlighted above. In the preceding URL segments:

  • The first URL segment determines the controller class to run. So localhost:5001/HelloWorld maps to the HelloWorldController class.

  • The second part of the URL segment determines the action method on the class. So localhost:5001/HelloWorld/Index causes the Index method of the HelloWorldController class to run. Notice that you only had to browse to localhost:5001/HelloWorld and the Index method was called by default. Index is the default method that will be called on a controller if a method name isn't explicitly specified.

  • The third part of the URL segment ( id) is for route data. Route data is explained later in the tutorial.

Example 1

  • Change the Welcome method to include two parameters as shown in the following code.
// GET: /HelloWorld/Welcome/
// Requires using System.Text.Encodings.Web;
public string Welcome(string name, int numTimes = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}

The preceding code:

  • Uses the C## optional-parameter feature to indicate that the numTimes parameter defaults to 1 if no value is passed for that parameter.
  • Uses HtmlEncoder.Default.Encode to protect the app from malicious input, such as through JavaScript.
  • Uses Interpolated Strings in $"Hello {name}, NumTimes is: {numTimes}".

Browser window showing an application response of Hello Rick, NumTimes is: 4

Example 2 (Important)

public string Welcome(string name, int ID = 1)
{
return HtmlEncoder.Default.Encode($"Hello {name}, ID: {ID}");
}

Run the app and enter the following URL: https://localhost{PORT}/HelloWorld/Welcome/3?name=Rick

  • We notice that id is not passed as a query parameter but as a route parameter instead.

  • This behavior is caused by :

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

Part3, Views

View templates are created using Razor. Razor-based view templates:

  • Have a .cshtml file extension.
  • Provide an elegant way to create HTML output with C#.
public IActionResult Index()
{
return View();
}

The preceding code:

  • Calls the controller's View method.
  • Uses a view template to generate an HTML response.

Controller methods:

  • Are referred to as action methods. For example, the Index action method in the preceding code.
  • Generally return an IActionResult or a class derived from ActionResult, not a type like string.

Add a view

@{ ViewData["Title"] = "Index"; }

<h2>Index</h2>

<p>Hello from our View Template!</p>

Navigate to https://localhost:{PORT}/HelloWorld:

  • The Index method in the HelloWorldController ran the statement return View();, which specified that the method should use a view template file to render a response to the browser.
  • A view template file name wasn't specified, so MVC defaulted to using the default view file. When the view file name isn't specified, the default view is returned. The default view has the same name as the action method, Index in this example. The view template /Views/HelloWorld/Index.cshtml is used.

Browser window

Change views and layout pages

  • In Shared/_Layout.cshtml
<!-- ... -->
<div class="container">
<main role="main" class="pb-3">@RenderBody()</main>
</div>
<!-- ... -->
  • RenderBody is a placeholder where all the view-specific pages you create show up, wrapped in the layout page. For example, if you select the Privacy link, the Views/Home/Privacy.cshtml view is rendered inside the RenderBody method.
<title>@ViewData["Title"] - Movie App</title>
<!-- ... -->
<a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>
<!-- ... -->
<div class="container">
&copy; 2020 - Movie App -
<a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>

The preceding markup made the following changes:

  • Three occurrences of MvcMovie to Movie App.
  • The anchor element <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">MvcMovie</a> to <a class="navbar-brand" asp-controller="Movies" asp-action="Index">Movie App</a>.

In the preceding markup, the asp-area="" anchor Tag Helper attribute and attribute value was omitted because this app isn't using Areas. (Areas divide the project to a set of sub projects )

  • Examine the Views/_ViewStart.cshtml file:
@{ Layout = "_Layout"; }

The Views/_ViewStart.cshtml file brings in the Views/Shared/_Layout.cshtml file to each view. The Layout property can be used to set a different layout view, or set it to null so no layout file will be used.

  • Examine the Views/_ViewStart.cshtml file:
@{ ViewData["Title"] = "Movie List"; }

<h2>My Movie List</h2>

<p>Hello from our View Template!</p>

ViewData["Title"] = "Movie List"; in the code above sets the Title property of the ViewData dictionary to "Movie List". The Title property is used in the <title> HTML element in the layout page:

he content in the Index.cshtml view template is merged with the Views/Shared/_Layout.cshtml view template. A single HTML response is sent to the browser. Layout templates make it easy to make changes that apply across all of the pages in an app.

Passing Data from the Controller to the View

Controllers are responsible for providing the data required in order for a view template to render a response.

View templates should not:

  • Do business logic
  • Interact with a database directly.

A view template should work only with the data that's provided to it by the controller. Maintaining this "separation of concerns" helps keep the code:

  • Clean.
  • Testable.
  • Maintainable.

Rather than have the controller render this response as a string, change the controller to use a view template instead. The view template generates a dynamic response, which means that appropriate data must be passed from the controller to the view to generate the response. Do this by having the controller put the dynamic data (parameters) that the view template needs in a ViewData dictionary. The view template can then access the dynamic data.

        public IActionResult Welcome(string name, int numTimes = 1)
{
ViewData["Message"] = "Hello " + name;
ViewData["NumTimes"] = numTimes;

return View();
}

The ViewData dictionary object contains data that will be passed to the view.

  • In Views/HelloWorld/Welcome.cshtml
@{
ViewData["Title"] = "Welcome";
}

<h2>Welcome</h2>

<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]; i++)
{
<li>@ViewData["Message"]</li>
}
</ul>

Part 4, Models

  • We are going to use the Model-First Method.
using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }

[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
}

The Movie class contains an Id field, which is required by the database for the primary key.

The DataType attribute on ReleaseDate specifies the type of the data (Date). With this attribute:

  • The user is not required to enter time information in the date field.
  • Only the date is displayed, not time information.
Install-Package Microsoft.EntityFrameworkCore.SqlServer

Create a database context class

A database context class is needed to coordinate EF Core functionality (Create, Read, Update, Delete) for the Movie model. The database context is derived from Microsoft.EntityFrameworkCore.DbContext and specifies the entities to include in the data model.

  • In Data/MvcMovieContext.cs
using Microsoft.EntityFrameworkCore;
using MvcMovie.Models;

namespace MvcMovie.Data
{
public class MvcMovieContext : DbContext
{
public MvcMovieContext (DbContextOptions<MvcMovieContext> options)
: base(options)
{
}

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

The preceding code creates a DbSet property for the entity set. In Entity Framework terminology, an entity set typically corresponds to a database table. An entity corresponds to a row in the table.

Register the database context

ASP.NET Core is built with dependency injection (DI). Services (such as the EF Core DB context) must be registered with DI during application startup. Components that require these services (such as Razor Pages) are provided these services via constructor parameters. The constructor code that gets a DB context instance is shown later in the tutorial. In this section, you register the database context with the DI container.

Add the following using statements at the top of Startup.cs:

using MvcMovie.Data;
using Microsoft.EntityFrameworkCore

Add the following highlighted code in Startup.ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

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

The name of the connection string is passed in to the context by calling a method on a DbContextOptions object. For local development, the ASP.NET Core configuration system reads the connection string from the appsettings.json file.

Add a database connection string

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-1;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}

Scaffold movie pages

Use the scaffolding tool to produce Create, Read, Update, and Delete (CRUD) pages for the movie model.

view of above step

Add Scaffold dialog

Initial migration

Add-Migration InitialCreate
Update-Database
  • Add-Migration InitialCreate: Generates a Migrations/{timestamp}_InitialCreate.cs migration file. The InitialCreate argument is the migration name. Any name can be used, but by convention, a name is selected that describes the migration. Because this is the first migration, the generated class contains code to create the database schema. The database schema is based on the model specified in the MvcMovieContext class.

  • Update-Database: Updates the database to the latest migration, which the previous command created. This command runs the Up method in the Migrations/{time-stamp}_InitialCreate.cs file, which creates the database.

Dependency injection in the controller

public class MoviesController : Controller
{
private readonly MvcMovieContext _context;

public MoviesController(MvcMovieContext context)
{
_context = context;
}

The constructor uses Dependency Injection to inject the database context (MvcMovieContext) into the controller. The database context is used in each of the CRUD methods in the controller.

Strongly typed models and the @model keyword

MVC (in addition to ViewData) also provides the ability to pass strongly typed model objects to a view. This strongly typed approach enables compile time code checking. The scaffolding mechanism used this approach (that is, passing a strongly typed model) with the MoviesController class and views.

  • Examine the generated Details method in the Controllers/MoviesController.cs file:
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}

return View(movie);
}

The id parameter is generally passed as route data. For example https://localhost:5001/movies/details/1 sets:

  • The controller to the movies controller (the first URL segment).
  • The action to details (the second URL segment).
  • The id to 1 (the last URL segment).

The id parameter is defined as a nullable type (int?) in case an ID value isn't provided.

var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);

If a movie is found, an instance of the Movie model is passed to the Details view:

return View(movie);
  • Examine the contents of the Views/Movies/Details.cshtml file:
@model MvcMovie.Models.Movie @{ ViewData["Title"] = "Details"; }

<h1>Details</h1>

<div>
<h4>Movie</h4>
<hr />
<dl class="row">
<dt class="col-sm-2">@Html.DisplayNameFor(model => model.Title)</dt>
<dd class="col-sm-10">@Html.DisplayFor(model => model.Title)</dd>
<dt class="col-sm-2">@Html.DisplayNameFor(model => model.ReleaseDate)</dt>
<dd class="col-sm-10">@Html.DisplayFor(model => model.ReleaseDate)</dd>
<dt class="col-sm-2">@Html.DisplayNameFor(model => model.Genre)</dt>
<dd class="col-sm-10">@Html.DisplayFor(model => model.Genre)</dd>
<dt class="col-sm-2">@Html.DisplayNameFor(model => model.Price)</dt>
<dd class="col-sm-10">@Html.DisplayFor(model => model.Price)</dd>
</dl>
</div>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>

The @model statement at the top of the view file specifies the type of object that the view expects. When the movie controller was created, the following @model statement was included:

@model MvcMovie.Models.Movie

This @model directive allows access to the movie that the controller passed to the view. The Model object is strongly typed. For example, in the Details.cshtml view, the code passes each movie field to the DisplayNameFor and DisplayFor HTML Helpers with the strongly typed Model object. The Create and Edit methods and views also pass a Movie model object.

Listing

Examine the Index.cshtml view and the Index method in the Movies controller. Notice how the code creates a List object when it calls the View method. The code passes this Movies list from the Index action method to the view:

// GET: Movies
public async Task<IActionResult> Index()
{
return View(await _context.Movie.ToListAsync());
}

When the movies controller was created, scaffolding included the following @model statement at the top of the Index.cshtml file:

@model IEnumerable<MvcMovie.Models.Movie>

The @model directive allows you to access the list of movies that the controller passed to the view by using a Model object that's strongly typed. For example, in the Index.cshtml view, the code loops through the movies with a foreach statement over the strongly typed Model object:

@model IEnumerable<MvcMovie.Models.Movie>
@{ ViewData["Title"] = "Index"; }

<h1>Index</h1>

<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.Title)</th>
<th>@Html.DisplayNameFor(model => model.ReleaseDate)</th>
<th>@Html.DisplayNameFor(model => model.Genre)</th>
<th>@Html.DisplayNameFor(model => model.Price)</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>@Html.DisplayFor(modelItem => item.Title)</td>
<td>@Html.DisplayFor(modelItem => item.ReleaseDate)</td>
<td>@Html.DisplayFor(modelItem => item.Genre)</td>
<td>@Html.DisplayFor(modelItem => item.Price)</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table></MvcMovie.Models.Movie
>

Because the Model object is strongly typed (as an IEnumerable<Movie> object), each item in the loop is typed as Movie. Among other benefits, this means that you get compile time checking of the code.

Part 5, work with a database

The MvcMovieContext object handles the task of connecting to the database and mapping Movie objects to database records. The database context is registered with the Dependency Injection container in the ConfigureServices method in the Startup.cs file:

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();

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

The ASP.NET Core Configuration system reads the ConnectionString. For local development, it gets the connection string from the appsettings.json file:

"ConnectionStrings": {
"MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovieContext-2;Trusted_Connection=True;MultipleActiveResultSets=true"
}

By default, EF will make a property named ID the primary key.

Seed the database

Create a new class named SeedData in the Models folder. Replace the generated code with the following:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MvcMovie.Data;
using System;
using System.Linq;

namespace MvcMovie.Models
{
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new MvcMovieContext(
serviceProvider.GetRequiredService<
DbContextOptions<MvcMovieContext>>()))
{
// Look for any movies.
if (context.Movie.Any())
{
return; // DB has been seeded
}

context.Movie.AddRange(
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-2-12"),
Genre = "Romantic Comedy",
Price = 7.99M
},

new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
},

new Movie
{
Title = "Ghostbusters 2",
ReleaseDate = DateTime.Parse("1986-2-23"),
Genre = "Comedy",
Price = 9.99M
},

new Movie
{
Title = "Rio Bravo",
ReleaseDate = DateTime.Parse("1959-4-15"),
Genre = "Western",
Price = 3.99M
}
);
context.SaveChanges();
}
}
}
}

If there are any movies in the DB, the seed initializer returns and no movies are added.

if (context.Movie.Any())
{
return; // DB has been seeded.
}

Add the seed initializer

In Program.cs :

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using MvcMovie.Data;
using MvcMovie.Models;
using System;

namespace MvcMovie
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();

using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;

try
{
SeedData.Initialize(services);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}

Part 6, controller methods and views (Important)

  • In Models/Movie.cs
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace MvcMovie.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }

[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }

[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
}
}
  • The Display attribute specifies what to display for the name of a field (in this case "Release Date" instead of "ReleaseDate"). The DataType attribute specifies the type of the data (Date), so the time information stored in the field isn't displayed.

  • The [Column(TypeName = "decimal(18, 2)")] data annotation is required so Entity Framework Core can correctly map Price to currency in the database. For more information, see Data Types.

  • In Views/Movies/Index.cshtml

        <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>

Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files. In the code above, the AnchorTagHelper dynamically generates the HTML href attribute value from the controller action method and route id.

  • The following code shows the HTTP POST Edit method, which processes the posted movie values:
// POST: Movies/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("ID,Title,ReleaseDate,Genre,Price")] Movie movie)
{
if (id != movie.ID)
{
return NotFound();
}

if (ModelState.IsValid)
{
try
{
_context.Update(movie);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MovieExists(movie.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction("Index");
}
return View(movie);
}

The [Bind] attribute is one way to protect against over-posting. You should only include properties in the [Bind] attribute that you want to change. For more information, see Protect your controller from over-posting. ViewModels provide an alternative approach to prevent over-posting.

The HttpPost attribute specifies that this Edit method can be invoked only for POST requests. You could apply the [HttpGet] attribute to the first edit method, but that's not necessary because [HttpGet] is the default.

The model binding system takes the posted form values and creates a Movie object that's passed as the movie parameter. The ModelState.IsValid property verifies that the data submitted in the form can be used to modify (edit or update) a Movie object. If the data is valid, it's saved. The updated (edited) movie data is saved to the database by calling the SaveChangesAsync method of database context. After saving the data, the code redirects the user to the Index action method of the MoviesController class, which displays the movie collection, including the changes just made.

Before the form is posted to the server, client-side validation checks any validation rules on the fields. If there are any validation errors, an error message is displayed and the form isn't posted. If JavaScript is disabled, you won't have client-side validation but the server will detect the posted values that are not valid, and the form values will be redisplayed with error messages. Later in the tutorial we examine Model Validation in more detail. The Validation Tag Helper in the *Views/Movies/Edit.cshtml* view template takes care of displaying appropriate error messages.

the HttpGet Edit method takes the movie ID parameter, looks up the movie using the Entity Framework FindAsync method, and returns the selected movie to the Edit view. If a movie cannot be found, NotFound (HTTP 404) is returned.

// GET: Movies/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie.FindAsync(id);
if (movie == null)
{
return NotFound();
}
return View(movie);
}

When the scaffolding system created the Edit view, it examined the Movie class and created code to render <label> and <input> elements for each property of the class. The following example shows the Edit view that was generated by the Visual Studio scaffolding system:

CSHTMLCopy

@model MvcMovie.Models.Movie @{ ViewData["Title"] = "Edit"; }

<h1>Edit</h1>

<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Edit">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-primary" />
</div>
</form>
</div>
</div>

<div>
<a asp-action="Index">Back to List</a>
</div>

@section Scripts { @{await
Html.RenderPartialAsync("_ValidationScriptsPartial");} }

Notice how the view template has a @model MvcMovie.Models.Movie statement at the top of the file. @model MvcMovie.Models.Movie specifies that the view expects the model for the view template to be of type Movie.

The scaffolded code uses several Tag Helper methods to streamline the HTML markup. The Label Tag Helper displays the name of the field ("Title", "ReleaseDate", "Genre", or "Price"). The Input Tag Helper renders an HTML <input> element. The Validation Tag Helper displays any validation messages associated with that property.

Add Search by name

Update the Index method found inside Controllers/MoviesController.cs with the following code:

public async Task<IActionResult> Index(string searchString)
{
var movies = from m in _context.Movie
select m;

if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

return View(await movies.ToListAsync());
}
  1. We create a LINQ query
  2. If the searchString parameter contains a string, the movies query is modified to filter on the value of the search string
    • The s => s.Title.Contains() code above is a Lambda Expression. Lambdas are used in method-based LINQ queries as arguments to standard query operator methods such as the Where method or Contains (used in the code above). LINQ queries are not executed when they're defined or when they're modified by calling a method such as Where, Contains, or OrderBy. Rather, query execution is deferred. That means that the evaluation of an expression is delayed until its realized value is actually iterated over or the ToListAsync method is called.
    • Note: The Contains method is run on the database, not in the c## code shown above. The case sensitivity on the query depends on the database and the collation. On SQL Server, Contains maps to SQL LIKE, which is case insensitive. In SQLite, with the default collation, it's case sensitive.

In Views/Movies/Index.cshtml

<form asp-controller="Movies" asp-action="Index" method="get">
<p>
Title: <input type="text" name="SearchString" />
<input type="submit" value="Filter" />
</p>
</form>
  • If we don't the form method , it defaults to POST.

Add Search by genre

Add the following MovieGenreViewModel class to the Models folder:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
public class MovieGenreViewModel
{
public List<Movie> Movies { get; set; }
public SelectList Genres { get; set; }
public string MovieGenre { get; set; }
public string SearchString { get; set; }
}
}

The movie-genre view model will contain:

  • A list of movies.
  • A SelectList containing the list of genres. This allows the user to select a genre from the list.
  • MovieGenre, which contains the selected genre.
  • SearchString, which contains the text users enter in the search text box.

Replace the Index method in MoviesController.cs with the following code:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
orderby m.Genre
select m.Genre;

var movies = from m in _context.Movie
select m;

if (!string.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}

if (!string.IsNullOrEmpty(movieGenre))
{
movies = movies.Where(x => x.Genre == movieGenre);
}

var movieGenreVM = new MovieGenreViewModel
{
Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
Movies = await movies.ToListAsync()
};

return View(movieGenreVM);
}

The SelectList of genres is created by projecting the distinct genres (we don't want our select list to have duplicate genres).

When the user searches for the item, the search value is retained in the search box.

<select asp-for="MovieGenre" asp-items="Model.Genres">
<option value="">All</option>
</select>
Title: <input type="text" asp-for="SearchString" />
<input type="submit" value="Filter" />

asp-for="MovieGenre" is similar to name="MovieGenre" (same exact thing)

We need To also change the lambda expressions ( to access model.Movies[0].Title instead of model.Title)

<table class="table">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.Movies[0].Title)</th>
<th>@Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)</th>
<th>@Html.DisplayNameFor(model => model.Movies[0].Genre)</th>
<th>@Html.DisplayNameFor(model => model.Movies[0].Price)</th>
<th></th>
</tr>
</thead>
</table>

Part 8, add a new field

When EF Code First is used to automatically create a database, Code First:

  • Adds a table to the database to track the schema of the database.
  • Verifies the database is in sync with the model classes it was generated from. If they aren't in sync, EF throws an exception. This makes it easier to find inconsistent database/code issues.

We add a new Field Rating :

  • We Modify our Model

  • We modify The view

  • We modify the controller [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")]

  • We create our migration & update the database with code first migrations:

    Add-Migration Rating
    Update-Database

Part 9, add validation

The validation support provided by MVC and Entity Framework Core Code First is a good example of the DRY(Don't Repeat Yourself) principle in action. You can declaratively specify validation rules in one place (in the model class) and the rules are enforced everywhere in the app.

Add validation rules to the movie model

public class Movie
{
public int Id { get; set; }

[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; }

[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
[StringLength(5)]
[Required]
public string Rating { get; set; }
}

Validation Error UI

<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>

@*Markup removed for brevity.*@

The preceding markup is used by the action methods to display the initial form and to redisplay it in the event of an error.

The Input Tag Helper uses the DataAnnotations attributes and produces HTML attributes needed for jQuery Validation on the client side. The Validation Tag Helper displays validation errors. See Validation for more information.

What's really nice about this approach is that neither the controller nor the Create view template knows anything about the actual validation rules being enforced or about the specific error messages displayed. The validation rules and the error strings are specified only in the Movie class. These same validation rules are automatically applied to the Edit view and any other views templates you might create that edit your model.

Using Datatype Attributes

The DataType attributes only provide hints for the view engine to format the data (and supplies elements/attributes such as <a> for URL's and <a href="mailto:EmailAddress.com"> for email. You can use the RegularExpression attribute to validate the format of the data. The DataType attribute is used to specify a data type that's more specific than the database intrinsic type, they're not validation attributes. In this case we only want to keep track of the date, not the time. The DataType Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress and more. The DataType attribute can also enable the application to automatically provide type-specific features. For example, a mailto: link can be created for DataType.EmailAddress, and a date selector can be provided for DataType.Date in browsers that support HTML5. The DataType attributes emit HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers can understand. The DataType attributes do not provide any validation.

DataType.Date doesn't specify the format of the date that's displayed. By default, the data field is displayed according to the default formats based on the server's CultureInfo.

The DisplayFormat attribute is used to explicitly specify the date format:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime ReleaseDate { get; set; }

The ApplyFormatInEditMode setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields — for example, for currency values, you probably don't want the currency symbol in the text box for editing.)

You can use the DisplayFormat attribute by itself, but it's generally a good idea to use the DataType attribute. The DataType attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don't get with DisplayFormat:

  • The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, etc.)
  • By default, the browser will render data using the correct format based on your locale.
  • The DataType attribute can enable MVC to choose the right field template to render the data (the DisplayFormat if used by itself uses the string template).

The following code shows combining attributes on one line:

public class Movie
{
public int Id { get; set; }

[StringLength(60, MinimumLength = 3)]
public string Title { get; set; }

[Display(Name = "Release Date"), DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$"), Required, StringLength(30)]
public string Genre { get; set; }

[Range(1, 100), DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }

[RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$"), StringLength(5)]
public string Rating { get; set; }
}

Part 10, examine the Details and Delete methods

Delete Method

Examine the Delete and DeleteConfirmed methods.

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}

var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.Id == id);
if (movie == null)
{
return NotFound();
}

return View(movie);
}

// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var movie = await _context.Movie.FindAsync(id);
_context.Movie.Remove(movie);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

Note that the HTTP GET Delete method doesn't delete the specified movie, it returns a view of the movie where you can submit (HttpPost) the deletion. Performing a delete operation in response to a GET request (or for that matter, performing an edit operation, create operation, or any other operation that changes data) opens up a security hole.

The [HttpPost] method that deletes the data is named DeleteConfirmed to give the HTTP POST method a unique signature or name. The two method signatures are shown below:

// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{

The common language runtime (CLR) requires overloaded methods to have a unique parameter signature (same method name but different list of parameters). However, here you need two Delete methods -- one for GET and one for POST -- that both have the same parameter signature. (They both need to accept a single integer as a parameter.)

There are two approaches to this problem, one is to give the methods different names. That's what the scaffolding mechanism did in the preceding example. However, this introduces a small problem: ASP.NET maps segments of a URL to action methods by name, and if you rename a method, routing normally wouldn't be able to find that method. The solution is what you see in the example, which is to add the ActionName("Delete") attribute to the DeleteConfirmed method. That attribute performs mapping for the routing system so that a URL that includes /Delete/ for a POST request will find the DeleteConfirmed method.

Another common work around for methods that have identical names and signatures is to artificially change the signature of the POST method to include an extra (unused) parameter. That's what we did in a previous post when we added the notUsed parameter. You could do the same thing here for the [HttpPost] Delete method:

// POST: Movies/Delete/6
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)