Welcome to Part 26 of our mobile shop development series! Now that we have our product list grid up and running with advanced filtering, it's time to build the pipeline that allows us to add new inventory.
In Step 1, we are implementing the Create GET method. This action method's job is to prepare everything the back-office user interface needs before displaying the blank entry form.
Step 1: Pre-populating Form Dependencies
When adding a product like a smartphone, the administrator shouldn't have to manually type out the Category or the Brand. Doing so would lead to typos, broken links, and database clutter. Instead, we must provide clean, pre-populated dropdown menus.
// GET: Admin/Products/Create
public async Task<IActionResult> Create()
{
// Populate lookup collections for lookups/drop-downs before loading the form
ViewBag.Categories = await _context.Categories.Where(c => c.IsActive).ToListAsync();
ViewBag.Brands = await _context.Brands.Where(b => b.IsActive).ToListAsync();
return View();
}
The Lifecycle of the GET Method
Core Logic & Architectural Components Explained
Asynchronous Lookup Processing (async Task<IActionResult>):
We fetch our configuration datasets asynchronously using ToListAsync(). This keeps our web server highly responsive. The thread is released back to the server thread pool while waiting for SQL Server to return the records, preventing system slowdowns during busy hours.
Data Integrity Filtering (Where(c => c.IsActive)):
We intentionally filter our database lookups using the .Where(x => x.IsActive) clause. This ensures that if a category or brand has been hidden or soft-deleted (which we set up in Part 23), it will automatically be excluded from the dropdown list. This prevents admins from accidentally assigning new products to discontinued sections.
Dynamic Data Transportation (ViewBag):
ViewBag is a dynamic property wrapper that allows you to pass extra pieces of data from your controller action method into your front-end Razor view. Here, we pack our active categories and active brands lists into ViewBag.Categories and ViewBag.Brands respectively, making them instantly available to build our HTML <select> dropdown tags in the view layout.
In Step 2, we design the Create Product Razor View. Managing inventory details like retail pricing rules, variant stock quantities, and physical multi-image file uploads requires a split-screen dashboard workspace using Bootstrap 5 layout primitives.
Step 2: Advanced Inventory Submission Layout
This view builds an asymmetrical 2-column input layout that divides textual information from dynamic product attributes like promotional badges and binary photo media streams.
@model Product
@{
ViewData["Title"] = "Add Product";
var categories = ViewBag.Categories as List<Category> ?? new List<Category>();
var brands = ViewBag.Brands as List<Brand> ?? new List<Brand>();
}
<div class="container-fluid px-4">
<h1 class="mt-4">Create Product</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Dashboard</a></li>
<li class="breadcrumb-item"><a asp-action="Index">Products</a></li>
<li class="breadcrumb-item active">Create</li>
</ol>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-plus me-1"></i> New Product
</div>
<div class="card-body">
@if (!ViewData.ModelState.IsValid)
{
<div class="alert alert-danger">
<h6>Validation Errors:</h6>
<ul>
@foreach (var state in ViewData.ModelState)
{
foreach (var error in state.Value.Errors)
{
<li><strong>@state.Key:</strong> @error.ErrorMessage</li>
}
}
</ul>
</div>
}
<form asp-action="Create" method="post" enctype="multipart/form-data" id="createProductForm">
@Html.Hidden("Category.Name", "")
@Html.Hidden("Brand.Name", "")
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card mb-3">
<div class="card-header bg-light">Basic Information</div>
<div class="card-body">
<div class="form-group mb-3">
<label asp-for="Name" class="form-label">Product Name *</label>
<input asp-for="Name" class="form-control" placeholder="Enter product name" required />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group mb-3">
<label asp-for="Model" class="form-label">Model Number</label>
<input asp-for="Model" class="form-control" placeholder="Enter model number" />
<span asp-validation-for="Model" class="text-danger"></span>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group mb-3">
<label asp-for="CategoryId" class="form-label">Category *</label>
<select name="CategoryId" id="CategoryId" class="form-select" required>
<option value="">-- Select Category --</option>
@foreach (var cat in categories)
{
<option value="@cat.Id">@cat.Name</option>
}
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label asp-for="BrandId" class="form-label">Brand *</label>
<select name="BrandId" id="BrandId" class="form-select" required>
<option value="">-- Select Brand --</option>
@foreach (var brand in brands)
{
<option value="@brand.Id">@brand.Name</option>
}
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Pricing & Stock -->
<div class="card mb-3">
<div class="card-header bg-light">Pricing & Stock</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="form-group mb-3">
<label asp-for="OriginalPrice" class="form-label">Original Price (RS ) *</label>
<input asp-for="OriginalPrice" class="form-control" type="number" step="0.01" min="0.01" required />
</div>
</div>
<div class="col-md-4">
<div class="form-group mb-3">
<label asp-for="SalePrice" class="form-label">Sale Price (RS ) *</label>
<input asp-for="SalePrice" class="form-control" type="number" step="0.01" min="0.01" required />
</div>
</div>
<div class="col-md-4">
<div class="form-group mb-3">
<label asp-for="StockQuantity" class="form-label">Stock Quantity *</label>
<input asp-for="StockQuantity" class="form-control" type="number" min="0" value="0" required />
</div>
</div>
</div>
</div>
</div>
<!-- Descriptions -->
<div class="card mb-3">
<div class="card-header bg-light">Descriptions</div>
<div class="card-body">
<div class="form-group mb-3">
<label asp-for="ShortDescription" class="form-label">Short Description</label>
<textarea asp-for="ShortDescription" class="form-control" rows="2" maxlength="1000"></textarea>
</div>
<div class="form-group mb-3">
<label asp-for="Description" class="form-label">Full Description</label>
<textarea asp-for="Description" class="form-control" rows="6" maxlength="2000"></textarea>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Product Status -->
<div class="card mb-3">
<div class="card-header bg-light">Product Status</div>
<div class="card-body">
<div class="form-check mb-2">
<input type="checkbox" name="IsActive" id="IsActive" value="true" class="form-check-input" checked />
<input type="hidden" name="IsActive" value="false" />
<label for="IsActive" class="form-check-label">Active</label>
</div>
<div class="form-check mb-2">
<input type="checkbox" name="IsFeatured" id="IsFeatured" value="true" class="form-check-input" />
<input type="hidden" name="IsFeatured" value="false" />
<label for="IsFeatured" class="form-check-label">Featured Product</label>
</div>
<div class="form-check mb-2">
<input type="checkbox" name="IsNewArrival" id="IsNewArrival" value="true" class="form-check-input" />
<input type="hidden" name="IsNewArrival" value="false" />
<label for="IsNewArrival" class="form-check-label">New Arrival</label>
</div>
<div class="form-check">
<input type="checkbox" name="IsBestseller" id="IsBestseller" value="true" class="form-check-input" />
<input type="hidden" name="IsBestseller" value="false" />
<label for="IsBestseller" class="form-check-label">Bestseller</label>
</div>
</div>
</div>
<!-- Images -->
<div class="card mb-3">
<div class="card-header bg-light">Product Images</div>
<div class="card-body">
<div class="form-group mb-3">
<label class="form-label">Main Image *</label>
<input type="file" name="images" class="form-control" accept="image/*" required />
<small class="text-muted">First image will be main image</small>
</div>
<div class="form-group">
<label class="form-label">Additional Images</label>
<input type="file" name="images" class="form-control" accept="image/*" multiple />
<small class="text-muted">Hold Ctrl/Cmd to select multiple</small>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-save"></i> Create Product
</button>
<a asp-action="Index" class="btn btn-secondary btn-lg">
<i class="fas fa-times"></i> Cancel
</a>
</div>
</form>
</div>
</div>
</div transform>
@section Scripts {
<partial name="_ValidationScriptsPartial" />
<script>
// Form validation intercepts and pricing checks
document.getElementById('createProductForm').addEventListener('submit', function(e) {
const originalPrice = parseFloat(document.querySelector('[name="OriginalPrice"]').value) || 0;
const salePrice = parseFloat(document.querySelector('[name="SalePrice"]').value) || 0;
const categoryId = document.querySelector('[name="CategoryId"]').value;
const brandId = document.querySelector('[name="BrandId"]').value;
if (!categoryId) {
e.preventDefault();
alert('Please select a Category!');
return false;
}
if (!brandId) {
e.preventDefault();
alert('Please select a Brand!');
return false;
}
if (salePrice > originalPrice) {
e.preventDefault();
alert('Sale price cannot be greater than original price!');
return false;
}
});
</script>
}
Core UI Mechanics & Form Features Explained
Safe Null-Object Casting Protection:
At the top of the file, we explicitly cast our loosely-typed dynamic data layers into strongly-typed generic List<T> variables. Using the null-coalescing operator (??) ensures that if the database lookup fails or returns empty, the layout falls back to a clean, empty initialized collection, completely avoiding a raw page-rendering null crash.
Multipart Data Form Encoding Header (enctype="multipart/form-data"):
Standard forms only package plain text values into standard payload streams. Because this creation layout accepts hardware product photos, adding enctype="multipart/form-data" is absolutely mandatory. This instructs the client browser to split the form payload into a multi-part binary stream so ASP.NET Core can receive the files as incoming controller IFormFile objects.
Detailed Model State Validation Loop:
Instead of utilizing a simple single-line summary string, this code block uses an evaluation check over ViewData.ModelState.IsValid. It runs a nested loop over every failed constraint property to build a precise, bulleted alert box highlighting exactly why the server rejected the model data.
Client-Side Pricing Guardrails:
The embedded JavaScript block at the bottom acts as an instant validation layer. Before the browser spends server bandwidth transmitting multi-megabyte image assets, this logic verifies your financial rules on the client side—preventing administrators from setting a store discount price higher than the base item cost.
In Step 3, we implement the backend engine of our product creation form: the HTTP POST Create action method. This method acts as a high-security gatekeeper that parses our inbound multi-part form payload, cleans up relational model requirements, processes image storage via a file service, and records multiple tracking rows back to SQL Server in a structured transaction lifecycle.
Step 3: Product Ingestion & Multi-Asset Storage Engine
This backend code processes a complex entity graph, dividing file binaries from relational mapping identifiers cleanly.
// POST: Admin/Products/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Name,Model,CategoryId,BrandId,OriginalPrice,SalePrice,StockQuantity,ShortDescription,Description,IsActive,IsFeatured,IsNewArrival,IsBestseller")] Product product, List<IFormFile> images)
{
// Remove EF validation errors for complex navigation properties to prevent false validation failures
ModelState.Remove("Category");
ModelState.Remove("Brand");
ModelState.Remove("ProductImages");
ModelState.Remove("Specifications");
ModelState.Remove("Reviews");
ModelState.Remove("OrderItems");
ModelState.Remove("WishlistItems");
// DEBUG: Log server-side model state errors to console/file trackers if invalid
if (!ModelState.IsValid)
{
foreach (var error in ModelState)
{
_logger.LogError($"Key: {error.Key}, Errors: {string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage))}");
}
}
if (ModelState.IsValid)
{
// Save the primary placeholder/Main image if uploaded
if (images.Count > 0)
{
product.MainImageUrl = await _fileService.SaveFileAsync(images[0], "images/products");
}
_context.Products.Add(product);
await _context.SaveChangesAsync(); // Generates product.Id for relational storage
// Loop through and assign additional gallery references
for (int i = 1; i < images.Count; i++)
{
var imagePath = await _fileService.SaveFileAsync(images[i], "images/products");
_context.ProductImages.Add(new ProductImage
{
ProductId = product.Id,
ImageUrl = imagePath,
DisplayOrder = i
});
}
await _context.SaveChangesAsync();
TempData["Success"] = "Product created successfully.";
return RedirectToAction(nameof(Index));
}
// Repopulate lookup configurations if validation checks fail to prevent form rendering breaking
ViewBag.Categories = await _context.Categories.Where(c => c.IsActive).ToListAsync();
ViewBag.Brands = await _context.Brands.Where(b => b.IsActive).ToListAsync();
return View(product);
}
Core Logic & Architectural Patterns
Over-Posting Prevention Guardrails ([Bind]):
The explicit [Bind] attribute restricts the framework's automatic model binder. It ensures that incoming HTTP payload updates can only inject values into the properties listed inside the binding definition string. This blocks malicious over-posting attacks where a user injects extra form properties to alter unexposed database values.
Clearing Navigation Property Validation Bloat (ModelState.Remove):
Because our Product model class likely contains full navigational properties like public Category Category { get; set; }, the .NET default model validator flags the incoming data as invalid because those complete objects are missing from a plain text form payload. Explicitly calling ModelState.Remove() wipes these empty reference checks away so the engine can evaluate the raw model data constraints accurately.
Multi-Tier File Ingestion Flow:
Our List<IFormFile> images parameter separates input file objects smoothly:
images[0]: Extracted as the main visual asset and stored directly on the core Product database record as the primary URL path mapping string (MainImageUrl).
for (int i = 1; i < images.Count; i++): An intentional loop index that skips the first image and iterates over remaining secondary photo sheets, building individual sub-entity ProductImage database rows tied back to the primary record via product.Id.
The Two-Step Transaction Commit Routine:
Notice the implementation calls await _context.SaveChangesAsync(); twice:
Commit 1: Saves the base product record first. This triggers SQL Server to generate the primary key Id sequence increment.
Commit 2: Captures that newly created identity ID property (product.Id) to form valid relational foreign keys for the subsequent multi-image attachment table rows before committing everything to rest.
Comments
Post a Comment