ASP.NET Core Razor Pages Using EntityFramework Core with Enums as Strings - Part III





5.00/5 (5 votes)
Add Project and Project State Processing
Introduction
This is Part III of a multi-part article demonstrates the mapping of C# enum
values to string
values in database tables via EntityFramework Core 2.1 (EF). It addresses the mapping of enum
values in one-to-many and many-to-many relationships with application entities. It does this in the context of an ASP.NET Core Razor Page application.
EF is an Object-Relational Mapper (ORM). In an application such as this sample, there are two "worlds". One is the object world that exists as an object model in C#. The other is the relational world that exists in a relational database, like Microsoft SQL Server. These two worlds are not consistent with each other. The function of an ORM, like EntityFramework
, is the bridge between these two worlds and facilitates the transfer of data between them.
Part I. Setup the Entity Framework data context and the initial Customer
s Razor Pages
Part II. Completed CRUD functions for Customer
s
In Part III. We will create Project
and ProjectState
entities and implement a one-to-many relationship between ProjectState
and Project
s as follows:
- Add the
Project
,ProjectState
andProjectStateDescription
entities. - Add an EF migration to create and configure the
Project
s andProjectStateDescription
s tables in the database. - Demonstrate the conversion between
enum
values in the object model entities and thestring
values in theProject
s andProjectStateDescription
s database tables. - Scaffold, implement and test
Project
CRUD pages, CustomerProjects.cshtml, CustomerProjectCreate.cshtml, CustomerProjectDetails.cshtml and CustomerProjectDelete.cshtml Razor pages that include theProjectState
feature.
Part IV. Add Skill entities (Skill enum
, SkillTitle
and ProjectSkill
) and implement a many-to-many relationship between Projects
and Skills
.
Using the Code
Add Initial Project Processing.
Next, we enable Customer
Project processing. The application uses the Customer
as a "gateway" entity; everything is reached via the Customer
. There is a one-to-many relationship between a Customer
and Projects
. Therefore, we need to modify the Customer
class.
Modified Customer.cs:
using System.Collections.Generic; namespace QuantumWeb.Model { /// <summary> /// Customer Class /// </summary> public class Customer { #region Constructors /// <summary> /// Parameter-less Constructor /// </summary> /// <remarks> /// Required for scaffolding the UI /// </remarks> public Customer() { } // end public Customer() #endregion // Constructors /// <summary> /// Customer Identifier, primary key /// </summary> public int CustomerId { get; set; } /// <summary> /// Customer Name /// </summary> public string CustomerName { get; set; } /// <summary> /// Primary Customer Contact /// </summary> public string CustomerContact { get; set; } /// <summary> /// Customer Contact Phone Number /// </summary> public string CustomerPhone { get; set; } /// <summary> /// Customer Contact Email Address /// </summary> public string CustomerEmail { get; set; } #region Navigation Properties /// <summary> /// List of Projects /// </summary> public List<Project> Projects { get; set; } #endregion // Navigation Properties } // end public class Customer } // end namespace QuantumWeb.Model
We added a List of Projects (bolded above). Here, we identify certain properties as Navigation Properties. These properties reference other classes/entities so that we can navigate to them in processing. A Customer
has zero or more Project
s represented in the list of Project
s. The initial Project
class definition is below.
Initial Project.cs:
namespace QuantumWeb.Model { /// <summary> /// Project Class /// </summary> public class Project { /// <summary> /// Project Identifier, primary key /// </summary> public int ProjectId { get; set; } /// <summary> /// Project Name /// </summary> public string ProjectName { get; set; } #region Navigation Properties /// <summary> /// Customer Identifier /// </summary> public int CustomerId { get; set; } /// <summary> /// Customer /// </summary> /// <remarks> /// Every Project has a Customer /// </remarks> public Customer Customer { get; set; } /// <summary> /// Project Status Code /// </summary> public ProjectState ProjectStateCode { get; set; } /// <summary> /// ProjectStateDescription Reference /// </summary> public ProjectStateDescription ProjectStateDescription { get; set; } #endregion // Navigation Properties } // end public class Project } // end namespace QuantumApp.Model
In addition to defining the initial Project
class, we will also define the ProjectState enum
in the Model folder.
ProjectState.cs:
namespace QuantumWeb.Model { /// <summary> /// Project State Enumeration /// </summary> public enum ProjectState { Prospect, UnderReview, StartScheduled, InProgress, Completed } // end public enum ProjectState } // end namespace QuantumWeb.Model
This enum
specifies the states of the Project
workflow.
Prospect
. This addresses a prospectiveProject
. ThisProject
might have been presented via a referral or other marketing efforts. No research has been done and the specifications are not known.UnderReview
. In this state, theProject
requirements, initial budget and schedule are developed. There is no commitment byQuantum
or theCustomer
.StartScheduled
. The date that work is to start has been specified and preparation to start work is in progress.InProgress
. Actual work has started but is not complete.Completed
. Project work is complete.
As previously stated, we have two objectives for this application.
- We should define a short description for each
Project
state that will be displayed in the UI to aid in user understanding of the meaning of each state. - Each
enum
value is to be stored in the database as astring
.
To meet these requirements for the ProjectState enum
, we define the ProjectStateDescription
class.
ProjectStateDescription.cs:
using System.Collections.Generic; namespace QuantumWeb.Model { /// <summary> /// Project State Description Class /// </summary> public class ProjectStateDescription { /// <summary> /// ProjectState Code /// </summary> public ProjectState ProjectStateCode { get; set; } /// <summary> /// State Description /// </summary> public string StateDescription { get; set; } #region Navigation Properties /// <summary> /// Projects Collection /// </summary> public List<Project> Projects { get; set; } #endregion // Navigation Properties } // end public class ProjectStateDescription } // end namespace QuantumWeb.Model
The ProjectState
to Projects
one-to-many relationship is enabled through navigation properties. Each Project
has one ProjectStateDesciption
. Each ProjectStateDescripton
has a collection of Projects
.
Next, we need to define the EF configuration classes for Project
and ProjectStateDescription
and include all in the QuantumDbContext
class. All of this activity occurs in the Data folder.
Initial ProjectConfiguration.cs:
using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using QuantumWeb.Model; namespace QuantumWeb.Data { public class ProjectConfiguration : IEntityTypeConfiguration<Project> { public void Configure(EntityTypeBuilder<Project> builder) { builder.ToTable("Projects"); builder.HasKey(p => p.ProjectId); builder.Property(p => p.ProjectId) .HasColumnType("int"); builder.Property(p => p.ProjectName) .IsRequired() .HasColumnType("nvarchar(80)") .HasMaxLength(80); builder.Property(p => p.CustomerId) .HasColumnType("int") .IsRequired(); builder.HasOne(p => p.Customer) .WithMany(c => c.Projects) .HasForeignKey(p => p.CustomerId) .IsRequired(); builder.Property(p => p.ProjectStateCode) .HasColumnType("nvarchar(15)") .HasDefaultValue(ProjectState.Prospect) .HasConversion( p => p.ToString(), p => (ProjectState)Enum.Parse(typeof(ProjectState), p)); builder.HasOne(p => p.ProjectStateDescription) .WithMany(pd => pd.Projects) .HasForeignKey(p => p.ProjectStateCode); } // end public void Configure(EntityTypeBuilder<Project> builder) } // end public class ProjectConfiguration : IEntityTypeConfiguration<Project> } // end namespace QuantumWeb.Data
Take a look at the extracted lines below:
builder.HasOne(p => p.Customer) .WithMany(c => c.Projects) .HasForeignKey(p => p.CustomerId) .IsRequired();
An interpretation of these lines is, "Each Project
has one Customer
with many Projects
. Each Project
maps to a Projects
table in the database with a foreign key, CustomerId
, and is required. Thus, the Customer
-Project
relationship is one-to-many.
The one-to-many ProjectStateDescription
-Project
relationship is configured by:
builder.HasOne(p => p.ProjectStateDescription) .WithMany(pd => pd.Projects) .HasForeignKey(p => p.ProjectStateCode);
Next, we look at the way the configuration of enum
value to database string
columns is handled.
builder.Property(p => p.ProjectStateCode) .HasColumnType("nvarchar(15)") .HasDefaultValue(ProjectState.Prospect) .HasConversion( p => p.ToString(), p => (ProjectState)Enum.Parse(typeof(ProjectState), p));
These lines first configure an nvarchar(15)
column in the Projects
table named, ProjectStateCode
, with a default value taken from ProjectState.Prospect
. Next, the conversion between the ProjectState
values and the string
value is defined. When moving values from the ProjectState enum
to the Projects
table, the values are converted using the ToString()
function. When converting the other way, the string
value in the table is parsed to an enum
value. The same scheme is used throughout to convert between enum
values and string
values in database columns.
The ProjectStateDescriptionConfiguration
class is shown below.
ProjectStateDescriptionConfiguration.cs:
using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using QuantumWeb.Model; namespace QuantumWeb.Data { /// <summary> /// ProjectState Description Configuration Class /// </summary> public class ProjectStateDescriptionConfiguration : IEntityTypeConfiguration<ProjectStateDescription> { public void Configure(EntityTypeBuilder<ProjectStateDescription> builder) { builder.ToTable("ProjectStateDescriptions"); builder.HasKey(p => p.ProjectStateCode); builder.Property(p => p.ProjectStateCode) .HasColumnType("nvarchar(15)") .HasConversion( p => p.ToString(), p => (ProjectState)Enum.Parse(typeof(ProjectState), p)); builder.Property(p => p.StateDescription) .IsRequired() .HasColumnType("nvarchar(80)") .HasMaxLength(80); } // end public void Configure(EntityTypeBuilder<ProjectStateDescription> builder) } // end public class ProjectStateDescriptionConfiguration : // IEntityTypeConfiguration<ProjectStateDescription> } // end namespace QuantumWeb.Data
Now, we update the QuantumDbContext
class.
Second update to QuantumDbContext.cs:
using Microsoft.EntityFrameworkCore; using QuantumWeb.Model; namespace QuantumWeb.Data { public class QuantumDbContext : DbContext { public QuantumDbContext (DbContextOptions<QuantumDbContext> options) : base(options) { } // end public QuantumDbContext (DbContextOptions<QuantumDbContext> options) #region DbSets /// <summary> /// Customer DbSet /// </summary> public DbSet<Customer> Customers { get; set; } /// <summary> /// Project DbSet /// </summary> public DbSet<Project> Projects { get; set; } /// <summary> /// ProjectStateDescription DbSet /// </summary> public DbSet<ProjectStateDescription> ProjectStateDescriptions { get; set; } #endregion // DbSets /// <summary> /// Data Model Creation Method /// </summary> /// <param name="modelBuilder">ModelBuilder instance</param> protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfiguration(new CustomerConfiguration()); modelBuilder.ApplyConfiguration(new ProjectConfiguration()); modelBuilder.ApplyConfiguration(new ProjectStateDescriptionConfiguration()); } // end protected override void OnModelCreating(ModelBuilder modelBuilder) } // end public class QuantumDbContext : DbContext } // end namespace QuantumWeb.Data
The added lines are shown in bold. Now add an EF migration for the Project
and ProjectState
entities.
Add-Migration Added-Project-ProjectState
Generated ~\Migrations\20181021203503_Added-Project-ProjectState.cs:
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; namespace QuantumWeb.Migrations { public partial class AddedProjectProjectState : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "ProjectStateDescriptions", columns: table => new { ProjectStateCode = table.Column<string>(type: "nvarchar(15)", nullable: false), StateDescription = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false) }, constraints: table => { table.PrimaryKey("PK_ProjectStateDescriptions", x => x.ProjectStateCode); }); migrationBuilder.CreateTable( name: "Projects", columns: table => new { ProjectId = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), ProjectName = table.Column<string>(type: "nvarchar(80)", maxLength: 80, nullable: false), CustomerId = table.Column<int>(type: "int", nullable: false), ProjectStateCode = table.Column<string> (type: "nvarchar(15)", nullable: false, defaultValue: "Prospect") }, constraints: table => { table.PrimaryKey("PK_Projects", x => x.ProjectId); table.ForeignKey( name: "FK_Projects_Customers_CustomerId", column: x => x.CustomerId, principalTable: "Customers", principalColumn: "CustomerId", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_Projects_ProjectStateDescriptions_ProjectStateCode", column: x => x.ProjectStateCode, principalTable: "ProjectStateDescriptions", principalColumn: "ProjectStateCode", onDelete: ReferentialAction.Cascade); }); migrationBuilder.CreateIndex( name: "IX_Projects_CustomerId", table: "Projects", column: "CustomerId"); migrationBuilder.CreateIndex( name: "IX_Projects_ProjectStateCode", table: "Projects", column: "ProjectStateCode"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "Projects"); migrationBuilder.DropTable( name: "ProjectStateDescriptions"); } } }
After the Update
-Database
command, the database diagram from SQL Server Management Studio (SSMS) is shown below.
QuantumDbContext
Database Diagram with Customer
-Project
-ProjectState
Tables:
Modify Razor Pages for Project
and ProjectState
.
We will need to add a number of custom Customer Razor Pages to the application for the Projects. First, we need to add a link to the Customer/Index page for CustomerProjects
.
Add CustomerProjects
link to Pages\Customers\Index.cshtml:
@page @model QuantumWeb.Pages.Customers.IndexModel @{ ViewData["Title"] = "Index"; } <h2>Index</h2> <p> <a asp-page="Create">Create New</a> <!-- A link to the Pages/Customers/Create page to create a new Customer --> </p> <!-- An HTML table to display existing Customers --> <table class="table"> <thead> <tr> <th> @Html.DisplayNameFor(model => model.Customer[0].CustomerName) </th> <th> @Html.DisplayNameFor(model => model.Customer[0].CustomerContact) </th> <th> @Html.DisplayNameFor(model => model.Customer[0].CustomerPhone) </th> <th> @Html.DisplayNameFor(model => model.Customer[0].CustomerEmail) </th> <th></th> </tr> </thead> <tbody> @foreach (var item in Model.Customer) { <tr> <td> @Html.DisplayFor(modelItem => item.CustomerName) </td> <td> @Html.DisplayFor(modelItem => item.CustomerContact) </td> <td> @Html.DisplayFor(modelItem => item.CustomerPhone) </td> <td> @Html.DisplayFor(modelItem => item.CustomerEmail) </td> <td> <a asp-page="./Edit" asp-route-id="@item.CustomerId">Edit</a> | <!-- A link to the Pages/Customers/Edit page to edit an existing Customer --> <a asp-page="./Details" asp-route-id="@item.CustomerId">Details</a> | <!-- A link to the Pages/Customers/Details page to display the details for an existing Customer --> <a asp-page="./CustomerProjects" asp-route-id="@item.CustomerId">Projects</a> | <!-- A link to the Pages/Customers/CustomerProjects page to display & manage the Projects for an existing Customer --> <a asp-page="./Delete" asp-route-id="@item.CustomerId">Delete</a> <!-- A link to the Pages/Customers/Delete page to delete an existing Customer --> </td> </tr> } </tbody> </table>
We will scaffold several custom Customers
Razor Pages as follows.
Custom Scaffold for Customers Razor Pages:
Scaffold Customers/CustomerProjects Razor Page:
Clicking "Add" will produce shell files for the CustomerProjects
Index page.
Generated ~Pages\Customers\CustomerProjects.cshtml
@page @model QuantumWeb.Pages.Customers.CustomerProjectsModel @{ ViewData["Title"] = "CustomerProjects"; } <h2>CustomerProjects</h2>
Generated ~Pages\Customers\CustomerProjects.cshtml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace QuantumWeb.Pages.Customers { public class CustomerProjectsModel : PageModel { public void OnGet() { } } }
We will modify these shell files in each case to suit our needs. The modified files for the CustomerProjects
Index page are.
Modified ~Pages\Customers\CustomerProjects.cshtml
@page "{id:int?}" @model QuantumWeb.Pages.Customers.CustomerProjectsModel @{ ViewData["Title"] = "Customer Projects"; } <h2>Customer Projects</h2> <div> <h4>Customer</h4> <hr /> <dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerId) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerId) </dd> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerName) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerName) </dd> <dt> @Html.DisplayNameFor(model => model.Customer.Projects) </dt> <dd> <table class="table"> <tr> <th>Project ID</th> <th>Project Name</th> <th>Project State</th> <th></th> </tr> @foreach (var item in Model.Customer.Projects) { <tr> <td> @Html.DisplayFor(modelItem => item.ProjectId) </td> <td> @Html.DisplayFor(modelItem => item.ProjectName) </td> <td> @Html.DisplayFor(modelItem => item.ProjectStateCode) </td> <td> <a asp-page="./CustomerProjectEdit" asp-route-id="@item.ProjectId">Edit</a> | <a asp-page="./CustomerProjectDelete" asp-route-id="@item.ProjectId">Delete</a> </td> </tr> } </table> </dd> </dl> </div> <div> <a asp-page="CustomerProjectCreate" asp-route-id="@Model.Customer.CustomerId"> Create New Project</a> | <a asp-page="./Index">Back to List</a> </div>
The "{id:int?}
" indicates an integer parameter, id
, is needed or the request for the page will return an HTTP 401 (Page not found) error. In this case, this is the identifier (CustomerId
) of the target Customer
. Also, notice the link referencing the CustomerProjectCreate
page.
<a asp-page="CustomerProjectCreate" asp-route-id="@Model.Customer.CustomerId">Create New Project</a> |
This will take us to the CustomerProjectCreate
page, yet to be created, to create a new Project
for the referenced Customer
.
Modified ~Pages\Customers\CustomerProjects.cshtml.cs
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using QuantumWeb.Data; using QuantumWeb.Model; namespace QuantumWeb.Pages.Customers { public class CustomerProjectsModel : PageModel { private readonly QuantumDbContext _context; public CustomerProjectsModel(QuantumDbContext context) { _context = context; } // end public CustomerProjectsModel(QuantumDbContext context) public Customer Customer { get; set; } public async Task<IActionResult> OnGet(int? id) { if (id == null) { return NotFound(); } // endif (id == null) Customer = await _context.Customers .Include(c => c.Projects) .FirstOrDefaultAsync(c => c.CustomerId == id); if (Customer == null) { return NotFound(); } // endif (Customer == null) return Page(); } // end public async Task<IActionResult> OnGet(int? id) } // end public class CustomerProjectsModel : PageModel } // end namespace QuantumWeb.Pages.Customers
Note here that the OnGet
handler has a nullable integer parameter, id
, which should be the CustomerId
mentioned above.
QuantumWeb
Application Customers Page: https//localhost: 44306/Customers with Project links.
Customer Projects
Page: https//localhost: 44306/Customers/CustomerProjects/1 (No Projects)
The "Create New Project" link will activate the custom CustomerProjectCreate
Razor page. We now scaffold this page.
Scaffold Customers/CustomerProjectCreate Razor Page:
Initial ~Pages\Customers\CustomerProjectCreate.cshtml.cs
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using QuantumWeb.Data; using QuantumWeb.Model; namespace QuantumWeb.Pages.Customers { public class CustomerProjectCreateModel : PageModel { private readonly QuantumDbContext _context; public CustomerProjectCreateModel(QuantumDbContext context) { _context = context; } // end public CustomerProjectCreateModel(QuantumContext context) [BindProperty] public Customer Customer { get; set; } public async Task<IActionResult> OnGet(int? id) { if (id == null) { return NotFound(); } // endif (id == null) Customer = await _context.Customers .Include(c => c.Projects) .FirstOrDefaultAsync(c => c.CustomerId == id); if (Customer == null) { return NotFound(); } // endif (Customer == null) ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions, "ProjectStateCode", "StateDescription", ProjectState.Prospect); return Page(); } // end public async Task<IActionResult> OnGet(int? id) [BindProperty] public Project Project { get; set; } public async Task<IActionResult> OnPostAsync() { if (!ModelState.IsValid) { return Page(); } // endif (!ModelState.IsValid) Project.CustomerId = Customer.CustomerId; _context.Projects.Add(Project); await _context.SaveChangesAsync(); return RedirectToPage("./CustomerProjects", new { id = Customer.CustomerId }); } // end public async Task<IActionResult> OnPostAsync() } // end public class CustomerProjectCreateModel : PageModel } // end namespace QuantumWeb.Pages.Customers
Please take note of these lines in this code.
[BindProperty] public Customer Customer { get; set; }
The [BindProperty]
binds the Customer
instance to the elements of the UI so that their values are preserved between the browser and the Web server. Also, notice that this attribute is applied to Project
instance as well.
Customer = await _context.Customers .Include(c => c.Projects) .FirstOrDefaultAsync(c => c.CustomerId == id);
This statement executes a query against the database to retrieve the Customer
record whose primary key value, CustomerId
, matches the input parameter, id
, value and its associated Project
records, if any. The function of the .Include
is to include associated records in the query.
ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions, "ProjectStateCode", "StateDescription", ProjectState.Prospect);
A ViewData
is an un-typed key-value dictionary used to pass values between the CustomerProjectCreateModel
class (in the .cshtml.cs file) and the HTML in the .cshtml file. This is similar to passing data from the Controller
to the View in the MVC, In using ViewData
, the data persists only in the HTTP request. Its members are filled from a query from the ProjectStateDescriptions
database table. In this case, the _context.ProjectStateDescriptions
is an IEnumerable<ProjectStateDescription>
returned from the query. The ProjectStateCode
is the primary key in the table and represents the key in the ViewData
dictionary. The StateDescription
becomes the associated value in the ViewData.
dictionary. The ViewData
will be used to populate a <select>
element in CustomerProjectCreate.cshtml. (See below.) The ProjectState.Prospect
is the default selected value from the ProjectState enum
for the <select>
. You can read more on ViewData
at the link, https://www.tektutorialshub.com/viewbag-viewdata-asp-net-core/.
Initial ~Pages\Customers\CustomerProjectCreate.cshtml:
@page @model QuantumWeb.Pages.Customers.CustomerProjectCreateModel @{ ViewData["Title"] = "Create Customer Project"; } <h2>Create Customer Project</h2> <hr /> <dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerId) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerId) </dd> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerName) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerName) </dd> </dl> <div class="row"> <div class="col-md-4"> <form method="post"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input type="hidden" asp-for="Customer.CustomerId" /> <div class="form-group"> <label asp-for="Project.ProjectName" class="control-label"></label> <input asp-for="Project.ProjectName" class="form-control"> </div> <div class="form-group"> <label asp-for="Project.ProjectStateCode" class="control-label"></label> <select asp-for="Project.ProjectStateCode" class="form-control" asp-items="ViewBag.ProjectStateCode"> </select> </div> <div class="form-group"> <input type="submit" value="Create" class="btn btn-default" /> </div> </form> </div> </div> <div> <a asp-page="CustomerProjects" asp-route-id="@Model.Customer.CustomerId"> Back to Customer Projects </a> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
The key elements are as follows:
<input type="hidden" asp-for="Customer.CustomerId" />
<input>
captures the target CustomerId
so that it is available when the <form>
is posted to create the Project
. <select asp-for="Project.ProjectStateCode" class="form-control" asp-items="ViewBag.ProjectStateCode"> </select>
This <select>
element will be displayed as a dropdown in the UI with the values from the ViewData
populated in the CustomerProjectCreate.OnGet()
method.
Initial ~Pages\Customers\CustomerProjectCreate.cshtml:
This shows the Customers/CustomerProjectCreate page as initially displayed.
CustomerProjectCreate
Page with data:
After clicking "Create
", we will see:
Customer Projects
Page with added Project
:
The next two figures show things after other Projects
are added for both Customers
.
Customer Projects Page with 2 Projects for Mirarex Oil & Gas:
Customer Projects Page with 3 Projects for Polyolefin Processing, Inc.
We can now add another page to edit the Customer
Projects, the CustomerProjectEdit
page.
Scaffold Customers/CustomerProjectEdit Razor Page
Initial ~Pages\Customers\CustomerProjectEdit.cshtml.cs
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using QuantumWeb.Data; using QuantumWeb.Model; namespace QuantumApp.Pages.Customers { public class CustomerProjectEditModel : PageModel { private readonly QuantumDbContext _context; public CustomerProjectEditModel(QuantumDbContext context) { _context = context; } // end public CustomerProjectEditModel(QuantumDbContext context) [BindProperty] public Customer Customer { get; set; } [BindProperty] public Project Project { get; set; } public async Task<IActionResult> OnGet(int? id) { if (id == null) { return NotFound(); } // endif (id == null) Project = await _context.Projects .Include(p => p.Customer) .FirstOrDefaultAsync(p => p.ProjectId == id); if (Project == null) { return NotFound(); } // endif (Project == null) Customer = Project.Customer; ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions, "ProjectStateCode", "StateDescription", ProjectState.Prospect); return Page(); } // end public async Task<IActionResult> OnGet(int? id) public async Task<IActionResult> OnPostAsync(int? id) { if (!ModelState.IsValid) { return Page(); } // endif (!ModelState.IsValid) var projectToUpdate = await _context.Projects.FindAsync(id); if (projectToUpdate == null) { return NotFound(); } // endif (projectToUpdate == null) projectToUpdate.CustomerId = Customer.CustomerId; if (await TryUpdateModelAsync<Project>( projectToUpdate, "project", p => p.ProjectName, p => p.ProjectStateCode)) { await _context.SaveChangesAsync(); return RedirectToPage("./CustomerProjects", new { id = Customer.CustomerId }); } return Page(); } // end public async Task<IActionResult> OnPostAsync(int? id) } // end public class CustomerProjectEditModel : PageModel } // end namespace QuantumApp.Pages.Customers
This code has the same artifacts as the CustomerProjectCreate
page with regard to the .Include
and the ViewData
.
Initial ~Pages\Customers\CustomerProjectEdit.cshtml
@page "{id:int?}" @model QuantumWeb.Pages.Customers.CustomerProjectEditModel @{ ViewData["Title"] = "Edit Customer Project"; } <h2>Edit Customer Project</h2> <hr /> <dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerId) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerId) </dd> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerName) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerName) </dd> </dl> <div class="row"> <div class="col-md-4"> <form method="post"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input type="hidden" asp-for="Customer.CustomerId" /> <div class="form-group"> <label asp-for="Project.ProjectName" class="control-label"></label> <input asp-for="Project.ProjectName" class="form-control"> </div> <div class="form-group"> <label asp-for="Project.ProjectStateCode" class="control-label"></label> <select asp-for="Project.ProjectStateCode" class="form-control" asp-items="ViewBag.ProjectStateCode"> </select> </div> <div class="form-group"> <input type="submit" value="Save" class="btn btn-default" /> </div> </form> </div> </div> <div> <a asp-page="CustomerProjects" asp-route-id="@Model.Customer.CustomerId"> Back to Customer Projects </a> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
This page has the same elements as the CustomerProjectCreate
page with regard to the hidden <input>
for the CustomerId
and the <select>
.
Customer Projects
Page with 2 Projects for Mirarex Oil & Gas - For Edit:
Customer Project Edit Page for Mirarex Oil & Gas, Zolar Pipeline:
Customer Projects Page with 2 Projects for Mirarex Oil & Gas - Project Edited:
The final feature for this part Project deletion via the CustomerProjectDelete
page.
Scaffold Customers/CustomerProjectDelete Razor Page:
Initial ~Pages\Customers\CustomerProjectDelete.cshtml.cs
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using QuantumWeb.Data; using QuantumWeb.Model; namespace QuantumWeb.Pages.Customers { public class CustomerProjectDeleteModel : PageModel { private readonly QuantumDbContext _context; public CustomerProjectDeleteModel(QuantumDbContext context) { _context = context; } // end public CustomerProjectDeleteModel(QuantumContext context) [BindProperty] public Customer Customer { get; set; } [BindProperty] public Project Project { get; set; } public async Task<IActionResult> OnGetAsync(int? id) { if (id == null) { return NotFound(); } // endif (id == null) Project = await _context.Projects .Include(p => p.Customer) .FirstOrDefaultAsync(p => p.ProjectId == id); if (Project == null) { return NotFound(); } // endif (Project == null) Customer = Project.Customer; return Page(); } // end public async Task<IActionResult> OnGet(int? id) public async Task<IActionResult> OnPostAsync(int? id) { if (id == null) { return NotFound(); } // endif (id == null) Project = await _context.Projects .Include(p => p.Customer) .FirstOrDefaultAsync(p => p.ProjectId == id); if (Project != null) { _context.Projects.Remove(Project); await _context.SaveChangesAsync(); } // endif (Project != null) return RedirectToPage("./CustomerProjects", new { id = Project.Customer.CustomerId }); } // end public async Task<IActionResult> OnPostAsync(int? id) } // end public class CustomerProjectDeleteModel : PageModel } // end namespace QuantumWeb.Pages.Customer
Initial ~Pages\Customers\CustomerProjectDelete.cshtml
@page "{id:int?}" @model QuantumWeb.Pages.Customers.CustomerProjectDeleteModel @{ ViewData["Title"] = "Delete Customer Project"; } <h2>Delete Customer Project</h2> <h3>Are you sure you want to delete this?</h3> <div> <dl class="dl-horizontal"> <dt> @Html.DisplayNameFor(model => model.Customer.CustomerName) </dt> <dd> @Html.DisplayFor(model => model.Customer.CustomerName) </dd> <dt> @Html.DisplayNameFor(model => model.Project.ProjectId) </dt> <dd> @Html.DisplayFor(model => model.Project.ProjectId) </dd> <dt> @Html.DisplayNameFor(model => model.Project.ProjectName) </dt> <dd> @Html.DisplayFor(model => model.Project.ProjectName) </dd> <dt> @Html.DisplayNameFor(model => model.Project.ProjectStateCode) </dt> <dd> @Html.DisplayFor(model => model.Project.ProjectStateCode) </dd> </dl> <form method="post"> <input type="hidden" asp-for="Project.ProjectId" /> <a asp-page="CustomerProjects" asp-route-id="@Model.Customer.CustomerId"> Back to Customer Projects </a> | <input type="submit" value="Delete" class="btn btn-default" /> </form> </div>
Customer Projects Page with 3 Projects for Mirarex Oil & Gas:
Delete Customer Projects Page - Delete Ouachita Shale:
Customer Projects Page with 2 Projects for Mirarex Oil & Gas:
At this point, we can summarize the test data in the following table:
Customers, Projects, ProjectStates
CustomerId | Customer Name | ProjectId | Project Name | ProjectStateCode | StateDescription |
1 | Mirarex Oil & Gas, LLC | 1 | Zolar Pipeline | UnderReview | Project is under review and negotiation |
1 | Mirarex Oil & Gas, LLC | 2 | Nelar Ranch Gas Fracturing | Prospect | Prospective or referred project |
2 | Polyolefin Processing, Inc. | 3 | Port Gibson Plant Expansion | Prospect | Prospective or referred project |
2 | Polyolefin Processing, Inc. | 4 | Jackson Plant Control System Upgrade | Prospect | Prospective or referred project |
2 | Polyolefin Processing, Inc. | 5 | Eutaw Plant Shutdown & Maintenance | Prospect | Prospective or referred project |
Summary
We have implemented the ProjectState
to Project
one-to-many relationship and created ASP.NET Razor Pages to manage it.
Points of Interest
In Part IV of this article, we will add definitions of Skill
entities (Skill
, SkillTitle
and ProjectSkills
) and implement a many-to-many relationship between Projects
and Skills
.