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
and ProjectStateDescription
entities. - Add an EF migration to create and configure the
Project
s and ProjectStateDescription
s tables in the database. - Demonstrate the conversion between
enum
values in the object model entities and the string
values in the Project
s and ProjectStateDescription
s database tables. - Scaffold, implement and test
Project
CRUD pages, CustomerProjects.cshtml, CustomerProjectCreate.cshtml, CustomerProjectDetails.cshtml and CustomerProjectDelete.cshtml Razor pages that include the ProjectState
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
{
public class Customer
{
#region Constructors
public Customer()
{
}
#endregion // Constructors
public int CustomerId { get; set; }
public string CustomerName { get; set; }
public string CustomerContact { get; set; }
public string CustomerPhone { get; set; }
public string CustomerEmail { get; set; }
#region Navigation Properties
public List<Project> Projects { get; set; }
#endregion // Navigation Properties
}
}
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
{
public class Project
{
public int ProjectId { get; set; }
public string ProjectName { get; set; }
#region Navigation Properties
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public ProjectState ProjectStateCode { get; set; }
public ProjectStateDescription ProjectStateDescription { get; set; }
#endregion // Navigation Properties
}
}
In addition to defining the initial Project
class, we will also define the ProjectState enum
in the Model folder.
ProjectState.cs:
namespace QuantumWeb.Model
{
public enum ProjectState
{
Prospect,
UnderReview,
StartScheduled,
InProgress,
Completed
}
}
This enum
specifies the states of the Project
workflow.
Prospect
. This addresses a prospective Project
. This Project
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, the Project
requirements, initial budget and schedule are developed. There is no commitment by Quantum
or the Customer
. 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 a string
.
To meet these requirements for the ProjectState enum
, we define the ProjectStateDescription
class.
ProjectStateDescription.cs:
using System.Collections.Generic;
namespace QuantumWeb.Model
{
public class ProjectStateDescription
{
public ProjectState ProjectStateCode { get; set; }
public string StateDescription { get; set; }
#region Navigation Properties
public List<Project> Projects { get; set; }
#endregion // Navigation Properties
}
}
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);
}
}
}
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
{
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);
}
}
}
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)
{
}
#region DbSets
public DbSet<Customer> Customers { get; set; }
public DbSet<Project> Projects { get; set; }
public DbSet<ProjectStateDescription> ProjectStateDescriptions { get; set; }
#endregion // DbSets
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CustomerConfiguration());
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
modelBuilder.ApplyConfiguration(new ProjectStateDescriptionConfiguration());
}
}
}
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>
<!--
</p>
<!--
<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 asp-page="./Details" asp-route-id="@item.CustomerId">Details</a> |
<!--
<a asp-page="./CustomerProjects" asp-route-id="@item.CustomerId">Projects</a> |
<!--
<a asp-page="./Delete" asp-route-id="@item.CustomerId">Delete</a>
<!--
</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;
}
public Customer Customer { get; set; }
public async Task<IActionResult> OnGet(int? id)
{
if (id == null)
{
return NotFound();
}
Customer = await _context.Customers
.Include(c => c.Projects)
.FirstOrDefaultAsync(c => c.CustomerId == id);
if (Customer == null)
{
return NotFound();
}
return Page();
}
}
}
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;
}
[BindProperty]
public Customer Customer { get; set; }
public async Task<IActionResult> OnGet(int? id)
{
if (id == null)
{
return NotFound();
}
Customer = await _context.Customers
.Include(c => c.Projects)
.FirstOrDefaultAsync(c => c.CustomerId == id);
if (Customer == null)
{
return NotFound();
}
ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
"ProjectStateCode", "StateDescription", ProjectState.Prospect);
return Page();
}
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
Project.CustomerId = Customer.CustomerId;
_context.Projects.Add(Project);
await _context.SaveChangesAsync();
return RedirectToPage("./CustomerProjects", new { id = Customer.CustomerId });
}
}
}
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" />
This hidden <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;
}
[BindProperty]
public Customer Customer { get; set; }
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnGet(int? id)
{
if (id == null)
{
return NotFound();
}
Project = await _context.Projects
.Include(p => p.Customer)
.FirstOrDefaultAsync(p => p.ProjectId == id);
if (Project == null)
{
return NotFound();
}
Customer = Project.Customer;
ViewData["ProjectStateCode"] = new SelectList(_context.ProjectStateDescriptions,
"ProjectStateCode", "StateDescription", ProjectState.Prospect);
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (!ModelState.IsValid)
{
return Page();
}
var projectToUpdate = await _context.Projects.FindAsync(id);
if (projectToUpdate == null)
{
return NotFound();
}
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();
}
}
}
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;
}
[BindProperty]
public Customer Customer { get; set; }
[BindProperty]
public Project Project { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Project = await _context.Projects
.Include(p => p.Customer)
.FirstOrDefaultAsync(p => p.ProjectId == id);
if (Project == null)
{
return NotFound();
}
Customer = Project.Customer;
return Page();
}
public async Task<IActionResult> OnPostAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Project = await _context.Projects
.Include(p => p.Customer)
.FirstOrDefaultAsync(p => p.ProjectId == id);
if (Project != null)
{
_context.Projects.Remove(Project);
await _context.SaveChangesAsync();
}
return RedirectToPage("./CustomerProjects", new { id = Project.Customer.CustomerId });
}
}
}
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
.
Technical professional with experience as Software Architect, IT Consultant, Developer, Engineer and Research Chemist. Current areas of emphasis are .NET, Entity Framework, application design and analysis.