Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Create the Create Page

In the last module we developed the Details page so that the user could view all of the details of a Vehicle. In this module we are going to give the user a way to add a Vehicle to the database by developing a Create page.

Table Of Contents
  1. Preparing for the Module
  2. Extend the Vehicles Repository
  3. Add the Create action methods to the Vehicles controller
    • HttpGet and HttpPost verbs
    • The ValidateAntiForgeryToken attribute
    • Overposting and the Bind attribute
    • Model Binding
    • ModelState
  4. Vehicle model modifications
  5. Unit Testing
    • Update current unit tests
    • Test the Vehicle repo
      • Install the EntityFrameworkCore.InMemory package
    • Test the Vehicle controller
  6. Create the View
    • The form element & form tag helper
      • The input element & input tag helper
      • The label element & label tag helper
      • The span element & span tag helper
        • Introduction to Validation
      • The select element & select tag helper
      • Submit & Cancel buttons
      • The hidden input element for the _RequestVerificationToken
  7. Add Create Button to home page
  8. Insert a new Vehicle
  9. What's Next

Preparing for the Module

In module 16, “Add the ImagePath Migration”, we created the images folder under the wwwroot folder with the subfolders, “Cars”, “Trucks”, and “Jeeps”. Then we downloaded all of the vehicle images in a zip file, extracted them, and placed them in the appropriate wwwroot/images subfolder.

In this module since we are going to build a Create page and test it out afterwards, we need to have an image sitting in a wwwroot/images subfolder so that the new vehicle will have an image ready for the new Vehicle’s ImagePath property to point to.

Download the new image from the path below and copy it to the FredsCars\wwwroot\images\jeeps\ folder in the FredsCars project.

Download new vehicle image (jeep5.jpg)

Extend the Vehicles Repository

Before we can add Create methods to the controller we need to extend the Vehicle repository.

First, modify IVehicleRepository.cs to extend the repo interface.

FredsCars\Models\Repositories\IVehicleRepository.cs

namespace FredsCars.Models.Repositories
{
    public interface IVehicleRepository
    {
        IQueryable<Vehicle> Vehicles { get; }
        
        Task CreateAsync(Vehicle vehicle);
    }
}

Then modify EFVehicleRepository.cs to extend the implimentation.

FredsCars\Models\Repositories\EFVehicleRepository.cs

using FredsCars.Data;

namespace FredsCars.Models.Repositories
{
    public class EFVehicleRepository : IVehicleRepository
    {
        private FredsCarsDbContext _context;

        public EFVehicleRepository(FredsCarsDbContext context)
        {
            _context = context;
        }

        public IQueryable<Vehicle> Vehicles => _context.Vehicles;

        public void CreateAsync(Vehicle vehicle)
        {
            await _context.Vehicles.AddAsync(vehicle);
            await _context.SaveChangesAsync();
        }
    }
}

In the repo interface we add a method definition called CreateAsync that takes in as a parameter a Vehicle object and returns a Task rather then void so we can implement it asynchronously in the EF implementation. We also include Async in the name of the method to reflect the fact that it is asynchronous.

In the implementation of CreateAsync we mark the method as asynchronous with the async keyword, call the AddAsync method of the DbContext’s Vehicles DbSet property, and pass it the incoming Vehicle to add to the database. Then we call the DbContext’s SaveChangesAsync method. Notice we use the async versions of the EF Core Add and SaveChanges methods. This is why we define CreateAsync as returning a Task rather then void in the interface. We did not have to do this for the Vehicles property which returns an IQueryable<Vehicle> rather then Task<List<Vehicle>>;


If we wanted to define the Vehicles property as asynchronous in the repo, we would make the property definition like the following in the interface returning a Task<List<Vehicle>> rather then IQueryable<Vehicle>.

Task<List<Vehicle>> Vehicles { get; }

And implement it in the repo EF implementation with the following statement.

public Task<List<Vehicle>> Vehicles => _context.Vehicles.ToListAsync();

But recall we made the repo Vehicles property an IQueryable because it is more flexible to work with then a List. From a controller or component we can access the repo Vehicles property which returns a DbSet. And a DbSet maps nicely to an IQueryable which inherits from IEnumerable<T> and IList<T>. Furthermore, from the controller we can continue to modify the IQueryable with methods like Where and OrderBy until we are ready to execute it with ToList or a for loop iteration.

And, we also are still able to get the benefits of asynchronous programming using an IQueryable combined with ToListAsync as we do in the Home controller’s Index method.

// -- Modify Vehicles IQueryable using LINQ Where method
// Category filter
var vehicles = _repo.Vehicles
    .Include(v => v.VehicleType)
    .Where(v =>
        category == null || v.VehicleType.Name == category);

// -- Execute IQueryable and have it return results as a List asynchronously
return View(new VehiclesListViewModel
{
    Vehicles = await vehicles
        .AsNoTracking()
        .Skip((pageNumber - 1) * PageSize)
        .Take(PageSize)
        // .Include(v => v.VehicleType)
        .ToListAsync(),
    PagingInfo = new PagingInfo

Now that we have the new repo method definition and implementation in place we need to add the Create methods to the controller.

Add the Create action methods to the Vehicles controller

HttpGet and HttpPost verbs

So far we have two action methods in our controllers.

We have the Index action method in the Home controller and the Details action method in the Vehicles controller.

// Index/Home
public async Task<ViewResult> Index(
    string? category,
    int pageNumber = 1,
    string sortColumn = "make",
    string sortOrder = "asc")
{...}
// Vehicles/Details
public async Task<ViewResult> Details(int id)
{...}

These two action methods both handle GET requests because they do not specify otherwise. If we wanted to explicitly express that these action methods handle GET requests, we would mark them with an HttpGet attribute like in the following.

[HttpGet]
public async Task<ViewResult> Index(
    string? category,
    int pageNumber = 1,
    string sortColumn = "make",
    string sortOrder = "asc")
{...}
[HttpGet]
public async Task<ViewResult> Details(int id)
{...}

To add functionality to create a new Vehicle in the Vehicles controller we need two Create action methods. One to handle a GET request which maps to HttpGet and one to handle a POST request which maps to HttpPost. These are known as Http verbs. The complete list of Http verbs is:

  1. HttpGet
  2. HttpPost
  3. HttpPut
  4. HttpPatch
  5. HttpDelete

Let’s start with the HttpGet version of the Create action method. Modify the Vehicles controller with the code below.

FredsCars\Controllers\VehiclesController.cs

using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Controllers
{
    public class VehiclesController : Controller
    {
        private IVehicleRepository _vehicleRepo;
        private IVehicleTypeRepository _vehicleTypeRepo;

        public VehiclesController(IVehicleRepository vRepo,
            IVehicleTypeRepository vtRepo)
        {
            _vehicleRepo = vRepo;
            _vehicleTypeRepo = vtRepo;
        }

        // GET: Students/Details/5
        public async Task<ViewResult> Details(int id)
        {
            Vehicle? vehicle = await _vehicleRepo.Vehicles
               .AsNoTracking()
               .Include(v => v.VehicleType)
              .FirstOrDefaultAsync(v => v.Id == id);

            if (vehicle == null)
            {
                ViewBag.NoVehicleMessage =
                "Sorry, no vehicle with that id could be found.";
            }

            return View(vehicle);
        }

        public IActionResult Create()
        {
            var vehicleTypes =
                _vehicleTypeRepo.VehicleTypes;

            ViewBag.VehicleTypeList = new SelectList(vehicleTypes,
               "Id", "Name");

             return View();
        }
    }
}

In the code above, we declare a new private variable for the VehiclesController class named _vehicleTypeRepo and assign it an instance of the VehicleTypes repository through DI.

Notice here we also changed the names of the class’s private variable from _repo to _vehicleRepo and the vehicle repo parameter name in the constructor from repo to vRepo so we can tell the two repos apart; One for vehicles and one for vehicle types.

Because of this change we have to make one other simple change in the Details action method. We have to update the name of the vehicle repo used in the LINQ query.

 Vehicle? vehicle = await _vehicleRepo.Vehicles
     .AsNoTracking()
     .Include(v => v.VehicleType)
     .FirstOrDefaultAsync(v => v.Id == id);

Within the new action method named Create we first create a variable named vehicleTypes and assign it the list of VehicleTypes (or categories) as an IQueryable from the new VehicleTypes repo we just brought in through DI.

Next, we initialize a SelectList instance and pass it the IQueryable of VehicleTypes we just captured from the repo as the first parameter. We will use this SelectList object to populate a DropDownList of categories when we build the form out in the View for a user to create a Vehicle. The second two parameters use the Id property of a VehicleType as the value of each Vehicle in the DropDownList and the Name property of a VehicleType as the text for each DropDownList item. We add the newly constructed SelectList to a dynamic property named VehicleTypeList of the ViewBag object.

Finally, the new action method handles a GET request to return a ViewResult that lays out a create form for the user to fill out in order to create a new Vehicle in the database. Again, this is the same as:

[HttpGet]
public IActionResult Create()
{
    ...
}

But we do not need to apply the HttpGet attribute because that is the default Http verb.

Next, let’s create the Post version of the Create action method which will receive a form post with the data the user filled out in order to create a new Vehicle.

FredsCars\Controllers\VehiclesController.cs

    ... existing code ...

        public IActionResult Create()
        {
            var vehicleTypes =
    _vehicleTypeRepo.VehicleTypes;

           ViewBag.VehicleTypeList = new SelectList(vehicleTypes,
    "Id", "Name");

            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Status,Year,Make,Model,Color,Price,VIN,ImagePath,VehicleTypeId")] Vehicle vehicle)
        {
            if (ModelState.IsValid)
            {
                await _vehicleRepo.CreateAsync(vehicle);
                return RedirectToAction("Index", "Home");
            }

            var vehicleTypes =
    _vehicleTypeRepo.VehicleTypes;

            ViewBag.VehicleTypeList = new SelectList(vehicleTypes,
    "Id", "Name");
            
            return View(vehicle);
        }
    }
}

Let’s inspect the Post version of Create action method above. There is quite a bit going on here. First of all, we mark the method with the HttpPost attribute. So after the user requests https://localhost:40443/vehicles/create, the HttpGet version of the action method lays out the View with the form. The user fills out the form and submits it. This will be a post request and ASP.Net Core will direct the request to the Post version of the Create method.

The ValidateAntiForgeryToken attribute

We also mark the method with the ValidateAntiForgeryToken attribute. This is to protect against cross-site request forgery (CSRF) attacks. CSRF attacks are committed by malicious code that has somehow gained authentication or trust of your web application. The ValidateAntiForgeryToken ensures that the request is coming from a form in our web application and not an attack. The Form tag helper we will use when we create the View will include a hidden field with a token to satisfy the CSRF check.

Overposting and the Bind attribute

The method takes in a parameter of type Vehicle which is marked with a Bind attribute. The Bind attribute contains a list of properties that should be populated by incoming form post data. Notice we do not include Id in the list of properties to populate. SQL Server will generate an Id for the Vehicle when EF Core inserts it into the database. The Bind attribute protects against overposting where a hacker could populate a property such as password in, for example, a UserAccount object. The Bind attribute allows us to control what incoming data should be applied to the post in a Create or Update action.

Model Binding

The incoming Vehicle parameter is automatically populated with data from the form post by a feature of ASP.Net Core called Model Binding. Model Binding maps the incoming data to the correct properties of a model class. This mapping allows developers to work with structured objects (models) in their controller actions rather than directly with raw request data. 

In the old days we would have had to have picked out each piece of data like in the following.

string status = Request.Form["Status"];
string make = Request.Form["Make"];
string model = Request.Form["Model"];
Vehicle vehicle = new Vehicle 
{
   Status = status,
   Make = make,
   Model = model
}

But, with ASP.Net Core model binding we are able to forgo this process and work directly with C# model classes like Vehicle and their properties

ModelState

Next, we check to see if the ModelState is valid.


in ASP.NET Core, ModelState is a key part of the model binding and validation system. It tracks the state of the model after binding form values, query strings, route data, or JSON data in API requests.


If so we call the repo’s CreateAsync method and pass it the Vehicle object where it will be added to the DbContext and inserted into the database via the DbContext’s SaveChangesAsync method. Next, the user will be redirected back to the Home Index page.
Otherwise the Create View is rendered again with the incoming model containing any errors. We will talk more about Validation when we build the View.

Notice, if there are model errors and the View needs to be re-rendered, we first need to populate the ViewBag object with an IQueryable of VehicleTypes exactly as we did in the HttpGet version of the Create Action method.

Vehicle model modifications

Before we go any further, we need to make a couple of tweaks to the Vehicle model. First, we need to set the Display name of the VehicleTypeId property so that when we use a label tag helper in the View’s form it will display the more friendly string “Category” rather then “VehicleTypeId”.

We also need to make the VehicleType navigation property nullable or the model binding will complain when trying to post the form data to Create a Vehicle.

Make the following modifications to the Vehicle model.

FredsCars\Models\Vehicle.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FredsCars.Models
{
    public enum Status
    {
        New,
        Used
    }
    
    public class Vehicle
    {
        public int Id { get; set; }
        public Status Status { get; set; }
        public string Year { get; set; } = string.Empty;
        public string Make { get; set; } = string.Empty;
        public string Model { get; set; } = string.Empty;
        public string Color {  get; set; } = string.Empty;
        [Column(TypeName = "decimal(9, 2)")]
        [DataType(DataType.Currency)]
        public decimal Price { get; set; }
        public string VIN { get; set; } = string.Empty;
        public string? ImagePath { get; set; }

        // Foriegn Key to VehicleType entity/table row
        [Display(Name = "Category")]
        public int VehicleTypeId { get; set; }
        // Entity Framework Navigation Property
        [Display(Name = "Category")]
        public VehicleType? VehicleType { get; set; }
    }
}

Unit Testing

Update current unit tests

Because in developing the Create feature we needed to add a VehicleTypeRepo as a second DI parameter to the VehiclesController constructor, a couple of the tests now fail. Make the following modifications to the VehiclesControllerTests.cs file.

FredsCars.Tests\Controllers\VehiclesControllerTests.cs

... existing code ...

namespace FredsCars.Tests.Controllers
{
    public class VehiclesControllerTests
    {
        [Fact]
        public async Task Can_Access_Vehicle_ById()
        {
           ... existing code ...

            // 3 - build mock IVehicleRepository
            Mock<IVehicleRepository> mockVehicleRepo =
                new Mock<IVehicleRepository>();
            mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);

            Mock<IVehicleTypeRepository> mockVehicleTypeRepo =
                new Mock<IVehicleTypeRepository>();

            VehiclesController controller = new VehiclesController(mockVehicleRepo.Object, mockVehicleTypeRepo.Object);

            // Act
            Vehicle? vehicleResult =
                (await controller.Details(3)).ViewData.Model
                    as Vehicle;
            var viewResult =
                (await controller.Details(3));


            // Assert
            Assert.Equal(3, vehicleResult?.Id);
            Assert.Null(viewResult.ViewData["NoVehicleMessage"]);
        }

        [Fact]
        public async Task Can_Send_NoVehicleFoundMessage()
        {
           ... existing code ...

            // 3 - build mock IVehicleRepository
            Mock<IVehicleRepository> mockVehicleRepo =
                new Mock<IVehicleRepository>();
            mockVehicleRepo.Setup(mvr => mvr.Vehicles).Returns(mockVehiclesIQueryable);

            Mock<IVehicleTypeRepository> mockVehicleTypeRepo =
                new Mock<IVehicleTypeRepository>();

            VehiclesController controller = new VehiclesController(mockVehicleRepo.Object, mockVehicleTypeRepo.Object);

            // Act
            Vehicle? vehicleResult =
                (await controller.Details(5)).ViewData.Model
                    as Vehicle;
            var viewResult =
                (await controller.Details(5));


            // Assert
            Assert.Null(vehicleResult);
            Assert.Equal("Sorry, no vehicle with that id could be found.",
                viewResult.ViewData["NoVehicleMessage"]);
        }
    }
}

All tests should now be in a passing state.

Test the Vehicle repo

Up until now we have been using the MockQueryable.Moq package to unit test retrieve functions in order to make it easier to work with our asynchronous functions. But in order to test the other CRUD functions like Create, we are going to turn to another package called EntityFrameworkCore.InMemory.

EntityFrameworkCore.InMemory is an in-memory database provider that can be used for testing purposes.

In real time our web application uses the SQL Server data provider. But our unit tests will use the InMemory data provider so we can mimic creating a new vehicle and inserting into a real database.

Install the EntityFrameworkCore.InMemory package

To install the new package, open a command prompt, point it to the FredsCars.Tests project, and run the following command.

dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 9.0.5

Add the following test to the EFVehicleRepositoryTests.cs file.

FredsCars.Tests\Models\Repositories\EFVehicleRepositoryTests.cs

using FredsCars.Data;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.EntityFrameworkCore;
using MockQueryable.Moq;
using Moq;

namespace FredsCars.Tests.Models.Repositories
{
    public class EFVehicleRepositoryTests
    {
        ... existing code ...

        [Fact]
        public async Task Can_Create_Vehicle()
        {
            // Arrange
            var options =
                new DbContextOptionsBuilder<FredsCarsDbContext>()
                    .UseInMemoryDatabase($"FredCars-{Guid.NewGuid().ToString()}")
                    .Options;

            using var context = new FredsCarsDbContext(options);
            var repo = new EFVehicleRepository(context);

            Vehicle vehicle = new Vehicle
            {
                Status = 0,
                Year = "2025",
                Make = "Make",
                Model = "Gladiator Rubicon 4 X 4",
                Color = "Joose",
                Price = 64125,
                VIN = "1C6RJTBG0SL532163",
                VehicleTypeId = 3,
                ImagePath = "/images/jeeps/jeep5.jpg"
            };

            // Act
            await repo.CreateAsync(vehicle);

            // Assert
            Assert.Equal(1, context.Vehicles.Count());
            Assert.Equal("1C6RJTBG0SL532163",
                context.Vehicles.FirstOrDefault()!.VIN);
        }
    }
}

In the Arrange section of the unit test shown above, we first instantiate a DbContextOptionsBuilder of type FredsCarsDbContext (DbContextOptionsBuilder<FredsCarsDbContext>), tell it to use an in-memory database with its UseInMemoryDatabase method, and assign its Options property to a variable named options. The UseInMemoryDatabase method takes a string parameter specifying what to name the in-memory database. We use string interpolation and the Guid object’s NewGuid method to concatenate the result with the literal string, “FredsCars-“. This will result in a unique name for each in-memory database for each unit test in the unit test project. If we do not do this step and use the same name for each in-memory database across all unit tests, there will be clashes between tests and they will use each other’s entities. So if we are expecting a count of 1 Vehicle back, we may get 2. And our asserts will fail. This is because in-memory databases behave slightly different then databases like SQL Server. Some people do not recommend using in-memory databases for unit tests, but I felt it fit the needs of our Create scenario testing well here.

Next, we instantiate an instance of FredsCarsDbContext and pass the DbContextOptions object to its constructor. We assign the result to a variable named context. Notice the C# using keyword at the beginning of the context declaration and initialization statement.

using var context = new FredsCarsDbContext(options);

The using keyword implements the DbContext.Dispose method and will release all resources allocated to the to the FredsCars DbContext. A using statement will work for any object in this way that implements IDisposable.

Finally, we instantiate an instance of EFVehicleRepository, pass the context to its constructor, and assign the result to a variable named repo.

We also instantiate one Vehicle object to use as test data.


In the Act section we call the CreateAsync method of the EFVehicleRepository and pass it the Vehicle instance.


In the Assert section we verify that the Vehicles DbSet property of the context now contains one vehicle; the vehicle we just inserted. And that the new Vehicle’s VIN number matches that of the Vehicle instance we inserted.

Test the Vehicle controller

Add the following test to the VehiclesControllerTests.cs file.

FredsCars.Tests\Controllers\VehiclesControllerTests.cs

using FredsCars.Controllers;
using FredsCars.Data;
using FredsCars.Models;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MockQueryable;
using Moq;

namespace FredsCars.Tests.Controllers
{
    public class VehiclesControllerTests
    {
        ... existing code ...

        [Fact]
        public async Task Can_Create_Vehicle()
        {
            // Arrange
            var options =
                new DbContextOptionsBuilder<FredsCarsDbContext>()
                    .UseInMemoryDatabase($"FredCars-{Guid.NewGuid().ToString()}")
                    .Options;

            using var context = new FredsCarsDbContext(options);

            var vRepo = new EFVehicleRepository(context);
            var vtRepo = new EFVehicleTypeRepository(context);

            var target = new VehiclesController(vRepo, vtRepo);

            Vehicle vehicle = new Vehicle
            {
                Status = 0,
                Year = "2025",
                Make = "Make",
                Model = "Gladiator Rubicon 4 X 4",
                Color = "Joose",
                Price = 64125,
                VIN = "1C6RJTBG0SL532163",
                VehicleTypeId = 3,
                ImagePath = "/images/jeeps/jeep5.jpg"
            };
      
            // Act
            RedirectToActionResult? result =
                await target.Create(vehicle)
                    as RedirectToActionResult;

            // Assert
            Assert.Equal(1, context.Vehicles.Count());
            Assert.Equal("1C6RJTBG0SL532163",
                context.Vehicles.FirstOrDefault()!.VIN);
            Assert.Equal("/Home/Index",
                $"/{result?.ControllerName}/{result?.ActionName}");

        }
    }
}

This test is much like the one we just wrote for EFVehicleRepository in the previous section except that in the Arrange section we instantiate a second repo, a VehilceTypes repo, to satisfy the constructor of the VehiclesController upon instantiation.

In the Act section we call the Create method of the Vehicles controller (which will use the Post version of Create), pass it a test Vehicle, and assign the results to a variable named result of type nullable RedirectToActionResult (RedirectToActionResult?).

ASP.Net Core will know to use the POST version of the Create action method here since we pass it a Vehicle object as a parameter. This matches the POST Create method’s parameter list while the GET version of the Create method has an empty parameter list. It takes no parameters. And so there is no match.

In the Assert section we verify that the context’s Vehicles DbSet property has a count of 1, the expected VIN number matches that of the test data Vehicle, and that the RedirectToAction result of the method redirects to the Home controller’s Index method.

Create the View

Create a Razor View file in the Views/Vehicles folder in the FredsCars project and name it Create.cshtml. Modify the file with the code below.

FredsCars\Views\Vehicles\Create.cshtml

@model Vehicle

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

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Create a Vehicle
    </h3>
</div>
   
<form asp-action="Create">
    <div class="container">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4">
                <div class="mb-3">
                    <label asp-for="Status" class="form-label fw-bold"></label>
                    <Select asp-for="Status" class="form-select">
                        <option id=""><-- Select One --></></option>
                        <option id="0">New</option>
                        <option id="1">Used</option>
                    </Select>
                    <span asp-validation-for="Status" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Year" class="form-label fw-bold"></label>
                    <input asp-for="Year" class="form-control" />
                    <span asp-validation-for="Year" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Make" class="form-label fw-bold"></label>
                    <input asp-for="Make" class="form-control" />
                    <span asp-validation-for="Make" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Color" class="form-label fw-bold"></label>
                    <input asp-for="Color" class="form-control" />
                    <span asp-validation-for="Color" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="ImagePath" class="form-label fw-bold"></label>
                    <br />
                    <span class="text-info-emphasis">
                        Format as:<br />
                        /images/[Cars|Trucks|Jeeps]/filename.ext
                    </span>
                    <input asp-for="ImagePath" class="form-control" />
                    <span asp-validation-for="ImagePath" class="text-danger"></span>
                </div>
            </div>
            <div class="col-4">
                <div class="mb-3">
                    <label asp-for="VehicleTypeId" class="form-label fw-bold"></label>
                    <Select asp-for="VehicleTypeId"
                            asp-items="@ViewBag.VehicleTypeList"
                            class="form-select">
                        <option id=""><-- Select One --></></option>
                    </Select>
                    <span asp-validation-for="VehicleTypeId" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Price" class="form-label fw-bold"></label>
                    <input asp-for="Price" class="form-control" />
                    <span asp-validation-for="Price" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Model" class="form-label fw-bold"></label>
                    <input asp-for="Model" class="form-control" />
                    <span asp-validation-for="Model" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="VIN" class="form-label fw-bold"></label>
                    <input asp-for="VIN" class="form-control" />
                    <span asp-validation-for="VIN" class="text-danger"></span>
                </div>
            </div>
            <div class="col-2"></div>
        </div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4 text-center">
                <div>
                    <input type="submit"
                           value="Create"
                           class="btn btn-primary" />
                    <a asp-controller="Home"
                       asp-action="Index"
                       class="btn btn-secondary">Cancel</a>
                </div>
            </div>
            <div class="col-2"></div>
        </div>
    </div>
</form>

Now restart the application and navigate to https://localhost:40443/Vehicles/Create.

Your browser should look similar to the following.

Let’s break down the Razor View code above.

To begin with, we declare the model of the View we will be working with to be a Vehicle object and set ViewData[“Title”] to “Create” so the tab of the browser will contain the text, “Fred’s Cars – Create”.

@model Vehicle

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

Next, we create an inner title header of sorts to show we are on the Create page containing the text, “Create a Vehicle”.

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2">
        Create a Vehicle
    </h3>
</div>

The code snippet above contains a div element with the Bootstrap classes container-fluid causing the element to take up the whole width of the screen and mt-3 (margin-top) spacing the element down three pixels from the black title bar in the layout.

The div element has an inner h3 heading element with the following Bootstrap classes:

  • text-center: Center the text within the h3 element.
  • bg-primary-subtle: use the primary background subtle scheme color (a light blue) for the h3 element.
  • py-2: pad the y axis with two pixels on each side causing the white space gutters look and making the width of the custom inner title bar slightly less then the black title bar in the layout.

The inner text of the h3 element is Create a Vehicle.

The form element & form tag helper

The meat of this Razor View is the form we created which contains the form fields a user will fill out in order to create a new vehicle; Status, Category, Year, and so on. It also has the Create submit button which will submit and post all of the form field data in the form to the HttpPost version of our Create action method in the Vehicles controller.


Note: I did not include the form element in the Html chapter, Chapter one, “Static HTML – Designing the landing page.” It’s kind of hard to show what a form is supposed to do without some kind of backend processing going on like a php script, or in our case the Create action method. So, this will be all new material relating to HTML as well as to ASP.Net Core. We will use forms here in ASP.Net Core MVC as well as the rest of the book.


To create an HTML form we use the form element. And ASP.Net has a corresponding built-in tag helper for a form tag.

<form asp-action="Create">
   ...
</form

In the above snippet, asp-action is an attribute of the form tag helper and tells ASP.Net Core we want to post to the Create action method in the same controller we just came from, Vehicles. This renders an HTML form as the following.

<form action="/Vehicles/Create" method="post">
   ...
</form>

There are two html form element attributes we are interested in here.

  • action – where the form data will be submitted to.
  • method – values [get | post].
    • get would submit data as a querystring.
    • post submits data as form post data. This is the method we are using here.

Within the form tag we set up a div as a Bootstrap container. Recall that while container-fluid takes up 100% of the screen, container has a max width which changes at specific Bootstrap breakpoints. I only want each form field to take up four columns in width of the 12 column Bootstrap grid system otherwise they will look too long on the screen. To center the form I use a trick with two gutter columns taking up two columns of the grid, and two col-4 columns for the actual form fields. 2 + 4 + 4 + 2 = 12 columns to center the form fields and keep them short enough in width to be pleasing to the eye.


   
<form asp-action="Create">
    <div class="container">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4">
                ... form fields ...
            </div>
            <div class="col-4">
                ... form fields ...
            </div>
            <div class="col-2"></div>
        </div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4 text-center">
               ... submit & cancel buttons
            </div>
            <div class="col-2"></div>
        </div>
    </div>
</form>

Each col-4 div has form field groups laid out vertically. The left col-4 div has five form groups including Status, Year, Make, Color, and ImagePath. The right col-4 div has four form groups including Category, Price, Model, and VIN.

The Select elements which render DropDownList elements are a little more complicated then the other form fields. So, let’s look at examples of form fields relating to string and decimal properties first.

The form group relating to the Vehicle object’s Make property looks like the following.

<div class="mb-3">
    <label asp-for="Make" class="form-label fw-bold"></label>
    <input asp-for="Make" class="form-control" />
    <span asp-validation-for="Make" class="text-danger"></span>
</div>

Each form group is contained within a div element with the mb-3 (margin-bottom) Bootstrap class pushing the next form group down by three pixels.

The input element & input tag helper

Let’s look at an input element of the form group first. We’ll use the Make form field input and Make form group as an example.

<input asp-for="Make" class="form-control" />

The input element gets a Bootstrap class named form-control. Bootstrap classes can be used to give all of your web pages and form controls a consistent look and feel. The following compares the form control with and without the Bootstrap form-control class.

The asp-for tag helper attribute binds the form control to a property of the Vehicle object. In this case the Make property.
asp-for="Make"

By including asp-for="Make", the input tag helper renders the following HTML.

<input class="form-control"
       type="text"
       data-val="true"
       data-val-required="The Make field is required."
       id="Make" name="Make"
       value="">

In the above html rendered by the input tag helper, the html attribute and value type="text" marks this input form control as a text box.

There are many types of input form controls and many values we can apply to the type attribute.

  • <input type="checkbox"> : renders the input control as a checkbox
  • <input type="date"> : modern browsers render the input control as a date picker.
  • <input type="color"> : modern browsers render the input control as a color picker.

In this book we will usually use text input form controls.

You can read more about input controls at w3schools.com.


The tag helper attribute and value asp-for="Make" render the id and name attributes of the input control both having the value “Make” to correspond to the Make property of the Vehicle object.

id="Make" name="Make"

The name attribute’s value is what gets passed as the input control’s value when the form is submitted. The id attribute is commonly used in CSS and jQuery selectors.


The value attribute is set to an empty string because we are creating a new vehicle. No value for Make exists yet.

value=""

When we create the Update form in the next module, the value object will be bound to the Vehicle we are updating and will contain the current Make property value.


The data-val and data-val-required attributes have to do with validation. We will talk more about validation in a later section.

The label element & label tag helper

The label tag helper for a label element also uses the tag helper asp-for attribute to bind to the Vehicle object’s Make property.

<label asp-for="Make" class="form-label fw-bold"></label>

The asp-for="Make" attribute and value render the following HTML.

<label class="form-label fw-bold" for="Make">Make</label>

label elements have a for attribute with a value matching the id of the form control element it corresponds to. asp-for="Make" sets the for attribute to “Make”. And since this matches the id of our input control, when you click on the label it will set the focus to the Make input control.

The span element & span tag helper

A span element is much like a div element in that it can be used as a container for content except that a div element is a block level element. It pushes itself down to another line from the previous element and puts space between itself and the previous element. It also pushes down the following element to the next line and puts white space between itself and the next element.

A span element on the other hand is an inline element and flows inline with the rest of the content.

Introduction to Validation

A span tag helper can be used with validation for a form control.

<span asp-validation-for="Make" class="text-danger"></span>

The asp-validation-for tag helper attribute tells ASP.Net Core this span element is going to be used to display any errors to the user for the Make input form control since its value is “Make”. ASP.Net Core looks at the Vehicle model and sees that Make is a string and not a nullable string (string?).

So it renders this HTML.

<span class="text-danger field-validation-valid" data-valmsg-for="Make" data-valmsg-replace="true"></span>

This span element has two classes. The Bootstrap class text-danger will render the validation error message in red. field-validation-valid is a class placed in the span element by the validation system signifying the element is valid. Even though it is a required field the form has just been rendered so it is still valid.

There are also two data dash (data-) attributes placed on the span element. The first is data-valmsg-for with the value “Make” so this span will show validation messages for the Make form field. The second is data-valmsg-replace which let’s the framework know it can inject the validation error message as text content into the span element.


To see the validation system in action, go ahead and click the Create button at the bottom of the form.

You will then see the span element for each form group show their validation error messages.

When the Create form is submitted to the HttpPost Create action in the Vehicles controller, the action method checks if the ModelState for the posted Vehicle is valid. If not the form is re-rendered with the error messages.

Let’s look at the validation span tag helper’s HTML for the Make input form control once again.

<span class="text-danger field-validation-error" data-valmsg-for="Make" data-valmsg-replace="true">The Make field is required.</span>

The field-validation-valid class in the span element has been replaced by field-validation-error. These classes come in handy if you ever want to use them to create custom CSS looks for valid and invalid fields.

The Make span validator gets its message to display from the dat-val-required attribute in the Make input element.

<input class="form-control input-validation-error" type="text"
 data-val="true"
 data-val-required="The Make field is required."
 id="Make" name="Make" value="">

NOTE: The validation being described here is Default Server-Side Validation. We look at creating custom validation using Data Annotations in the model and Client-Side Validation using unobtrusive JavaScript in later modules on Validation.

The select element & select tag helper

The Create form has two Select lists created by select tag helpers; Status and Category. Let’s look at the Status Select list first.

<Select asp-for="Status" class="form-select">
    <option id=""><-- Select One --></></option>
    <option id="0">New</option>
    <option id="1">Used</option>
</Select>

In the above snippet we set asp-for to the Status property of the Vehicle we are creating. Recall from the Vehicle model that Status is an enum containing the constants “New”, and “Used”. Since the Status property of the Vehicle class is of type Status and not Status? (nullable Status) it is a required field.

Here we manually add three items using option elements to the select list. The option tags with ids of 0 and 1 represent New and Used respectively. In C#, by default, the associated constant values of enum members are of type int; they start with zero and increase by one following the definition text order.

The option with an id of "" represents the Status being null which will trigger the error message.

The select tag helper to create the Category select list looks like the following.

<Select asp-for="VehicleTypeId"
        asp-items="@ViewBag.VehicleTypeList"
        class="form-select">
    <option id=""><-- Select One --></></option>
</Select>

Again, here, theVehicleTypeId property of the Vehicle class model is int and not nullable int (int?) so this is a required field. We add the null option tag manually like we did in the Status select element.

<option id=""><-- Select One --></></option>

But, here we add the rest of the option items using the built-in select tag helper’s asp-items attribute and set its value to the collection of VehicleTypes (Categories) assigned to the ViewBag object’s VehicleTypeList property in the Create action of the Vehicles controller.

asp-items="@ViewBag.VehicleTypeList"

The select tag helper renders the following html without errors.

<select class="form-select"
        data-val="true"
        data-val-required="The Category field is required."
        id="VehicleTypeId" name="VehicleTypeId">
	<option id="">&lt;-- Select One --&gt;</option>
	<option value="1">Cars</option>
	<option value="2">Trucks</option>
	<option value="3">Jeeps</option>
</select>

And the following html with a null error.

<select class="form-select input-validation-error"
        data-val="true"
        data-val-required="The Category field is required."
        id="VehicleTypeId" name="VehicleTypeId">
	<option id="">&lt;-- Select One --&gt;</option>
	<option value="1">Cars</option>
	<option value="2">Trucks</option>
	<option value="3">Jeeps</option>
</select>

Submit & Cancel buttons

After the Bootstrap row containing all of the Input and Select form controls for the form, we have another Bootstrap row with the same 2 X 4 X 4 X 2 col structure set up as the first row to center the content.

<div class="row">
    <div class="col-2"></div>
    <div class="col-4 text-center">
        <div>
            <input type="submit"
                   value="Create"
                   class="btn btn-primary" />
            <a asp-controller="Home"
               asp-action="Index"
               class="btn btn-secondary">Cancel</a>
        </div>
    </div>
    <div class="col-2"></div>

The input tag has a type value of submit. This makes it a submit button for the form. It will submit to the path given in the form element’s action attribute.
<form action="/Vehicles/Create" method="post">
This path is of course the Create action in the Vehicles controller.

The Cancel button is an anchor text link created with an anchor tag helper dressed up with Bootstrap classes to look like a button and takes the user back to the Home/Index page when clicked.

The hidden input element for the _RequestVerificationToken

The built-in form tag helper inserts a hidden field at the end of the form with html that looks like the following.

<input name="__RequestVerificationToken"
	   type="hidden"
	   value="CfDJ8D1cCCnuzXtJquUrlq4SpEyS21LteSJyOU7tx2CHcLTmCHg4G_Ngwq18fMmgW2oynySk3oBeUy0-lDYFfnqK82LYi2Xle3pjET_eFaPup95GCS6itn9v0e0nmlpVw37MFEpao3qovQfkhYFPkOdLn6E">

Hidden fields in html forms are input controls that are hidden from the user and do not render, but still have a name and a value that gets posted with the form. The hidden field in the snippet above has a name attribute with the value __RequestVerificationToken.
It also has a value attribute with a long unique value that represents a token. This value will satisfy the cross-site request forgery (CSRF) check that the ValidateAntiForgeryToken attribute we talked about earlier for the POST Create action method in the VehiclesController will perform.

Add Create Button to home page

We have been getting to the Create web page in the browser by manually typing in the following URL.
https://localhost:40443/vehicles/create

But a user of our web application won’t know about this URL. We need to provide a way for them to navigate from the home/index page to the Create page. To do this we will add a “Create New” link to the main page. Modify the Home/Index.cshtml file with the code change shown below.

FredsCars\Views\Home\Index.cshtml

<!-- Categories -->
<div class="col-4 col-md-3 col-lg-2"
     style="border-right: 2px solid black">
     <div class="d-grid gap-2 button-grid">
        <p class="container-fluid text-start">
            <a asp-controller="Vehicles" asp-action="Create">Create New</a>
        </p>
         <vc:categories-component />
    </div>
</div>

In the code above we used the built-in anchor tag helper to add a “Create New” anchor link which when clicked will take the user to the Create page. The Bootstrap text-start class in the containing paragraph element will left align the anchor link. text-end would right align it.

Insert a new Vehicle

Now let’s use the form to insert a new Vehicle into the database. Navigate to the landing page at https://localhost:40443/ and click the new “Create New” link which will take you to the Create page at https://localhost:40443/Vehicles/Create.

Fill out the form with the following values:

  • Status: New
  • Category: Jeeps
  • Year: 2025
  • Price: 64125
  • Make: Jeep
  • Model: Gladiator Rubicon 4 X 4
  • Color: Joose
  • VIN: 1C6RJTBG0SL532163
  • ImagePath: /images/jeeps/jeep5.jpg

ImagePath is the one that needs to be filled out exactly as the path shown otherwise the Home/Index page will not be able to show the thumbnail image for the vehicle and the Details page will not be able to find the image to show at the top of the page.

Click the Create button.

The new vehicle should successfully be inserted into the database and you will be redirected back to the Home/Index page. Click the Jeeps category button and you should see the newly entered Jeep.

What’s Next

In this module we added the Create feature in order to enable users to add a new vehicle to the database. Along the way we expanded the Vehicle Repository, created the HTTP Get and Post versions of the Create action method, Tested the new features of our Vehicle repo and Vehicles controller, created the Create view, and learned about HTML forms.

In the next module, we will add the Update feature to let users update information about vehicles.

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page
  • Create the Update Page
  • Create the Delete Page
  • Validation
  • Logging & Configuration
  • Storing Secrets
  • Error Handling
  • Security & Administration

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme