Part 13: Product Comparison System Matrix
To build a clean dynamic grid where products form the columns and specifications form the rows, we need a specialized data structure. In Step 1, we create a dedicated ViewModel to hold this grouped data.
Step 1: Creating the CompareProductsViewModel
This ViewModel acts as the data engine for your comparison matrix page, aggregating all the selected products and their distinct feature sets into two clean collections.
Core Properties & Logic Breakdown
The Products Collection (
public List<Product> Products { get; set; } = new();): This property stores the full database objects of the specific mobile phones the user has chosen to compare. Because it loads the completeProductentities, your Razor view will have instant access to the core details of each phone—such as images, prices, titles, and brand names—to render at the top of the comparison columns.The Specification Names Master List (
public List<string> SpecificationNames { get; set; } = new();): This is the secret to building a dynamic comparison grid. Instead of hardcoding row names, this list holds a distinct, compiled collection of all unique technical attribute names (e.g., "Battery Capacity", "RAM", "Camera Megapixels") across all the selected items. It tells your frontend view exactly how many rows it needs to draw.C# Auto-Initialization Property Pattern (
= new();): By utilizing the target-typed new expression (= new();), you ensure that these lists are nevernullwhen the ViewModel is instantiated. This defensive programming practice protects your application from crashing with aNullReferenceExceptionif a user lands on the comparison page with no items selected.
Step 2: Implementing the Compare Action Method
This method processes an array of incoming product IDs, cleanses the data, ensures it adheres to business limits, and maps out the flat specification row headers dynamically.
Core Components & LINQ Logic Breakdown
Initial Boundary Guard Checking:
if (ids == null || ids.Length < 2)This instantly protects the server resource from invalid state traffic. A comparison grid makes no sense with fewer than two items, so if this baseline rule isn't met, the request is broken off and the user is redirected safely with an error alert.Deduplication and Cap Optimization:
ids.Distinct().Take(4)This is a brilliant snippet for keeping your screen layout clean. By calling.Distinct(), you prevent a user from sending the same phone ID twice. Using.Take(4)clamps the input to a maximum of four phones, which perfectly matches a responsive layout boundary (4 grid columns fit comfortably across desktop views).Dynamic Specification Extraction via
SelectMany:products.SelectMany(p => p.Specifications).Select(s => s.Name).Distinct()This is the highlight of the method. Each product has a child list of specifications. Instead of returning nested lists,
.SelectMany()"flattens" every key-value pair across all selected products into a single long sequence of data. Then,.Select(s => s.Name).Distinct()filters that list down into unique, individual row titles (e.g., ensuring "RAM" or "Battery" only appears as one clean header entry).Contextual View Persistence Integration:
ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();Just like your earlier wishlist features, keeping your infrastructure methods updated means your site headers will remain beautifully operational and consistent while the customer reads down through their spec matrix grid.
Step 3: Creating the Dynamic HTML Comparison Grid View
This Razor file builds a highly interactive comparison interface that handles horizontal scrolling on smaller screens gracefully using modern Bootstrap utilities and standard CSS.
Core UI & Razor Matrix Logic Breakdown
The Transposed Matrix Design: The table is engineered with a transposed layout. Instead of products being rows, your products are mapped out as columns inside the
<thead>element. This lets shoppers compare multiple mobile screen widths, prices, and imagery effortlessly side-by-side.Nested Contextual Collection Traversal:
@foreach (var specName in Model.SpecificationNames) { ...@foreach (var product in Model.Products) { ... } }This is the highlight of the frontend engineering. The outer loop draws a row header for every individual specification name in the system. The inner loop then scans across each product column to check if that phone contains that specific attribute using
.FirstOrDefault(s => s.Name == specName). If it's found, it renders the metric (e.g., "5000 mAh"); if not, it outputs a clean dash (-).Accurate Fractional Star Calculations: The layout includes half-star evaluation logic utilizing custom condition gates (
else if (i - 0.5 <= product.AverageRating)). This ensures a phone with a 4.5-star rating displays four full stars and one half-star icon perfectly.CSS Sticky Horizontal Scroller Engine: The inline styles use a brilliant CSS technique:
position: sticky; left: 0; z-index: 2;. When a visitor checks out the matrix on a mobile viewport, they can swipe sideways across columns while the leftmost "Feature" column remains completely locked in place. This provides an excellent mobile user experience.
Step 4: Integrating the Compare Trigger into the Shared Product Card
This snippet injects a compact, floating action button onto the lower section of your product thumbnail card layout, routing the specific model identity into your upcoming client-side application array.
Core UI & Event Logic Breakdown
Consistent Utility Layout Layering (
position-absolute bottom-0 start-0 m-2): By utilizing Bootstrap 5 absolute positioning utility tokens, you cleanly anchor this action button into the bottom-left corner of the card's frame. This balances perfectly with your existing bottom-right elements, ensuring that the main product image and metadata text remain completely unobstructed.Contextual Visual Indicator (
bi-arrow-left-right): The choice of the Bootstrap Icons "arrow-left-right" glyph is an excellent UX decision. It uses a universally recognized symbol for horizontal sorting and feature matching, telling your shoppers what the button does without requiring bulky text labels.Explicit Model Parameter Passing (
onclick="addToCompare(@Model.Id)"): This event handler links your server-side Razor domain context directly into your client-side scripting domain. By injecting@Model.Iddirectly into the method call parameter slot on compilation, each individual card generated by your loops becomes uniquely serialized to track its respective database record entry point.Clean Adaptive Sizing (
btn-light btn-sm): Thebtn-smsizing and mutedbtn-lightsurface treatment ensure that the button stays subtle and unobtrusive. It looks like an elegant, native browser component that pops up softly when hovered, matching the design style of premium retail platforms.
Step 5: Implementing Client-Side State with LocalStorage
Adding this script to _Layout.cshtml ensures that your comparison state persists across pages as the user browses different categories, models, or details screens.
Core Functionality & Logic Breakdown
Persistent Browser Memory State (
localStorage): Instead of using server-side sessions, the code utilizeslocalStorage.getItem('compareProducts'). This keeps the active data saved inside the user's browser storage. If they close their tab and return later, their choices are perfectly preserved without placing any database memory overhead on your server.Client-Side Validation and Array Caps: Inside
addToCompare, the script enforces the same business rules written into the backend in Step 2: it checks for duplicates via.includes(productId)and restricts the list size to 4 elements (compareList.length >= 4). This catches invalid state logic before it ever makes a web request.Dynamic Array Query String Generation:
const query = compareList.map(id => `ids=${id}`).join('&');This line demonstrates excellent standard JavaScript capability. If a user selects product IDs 12, 15, and 18,
.mapand.joinconvert the array into the query formatids=12&ids=15&ids=18. This perfectly matches theint[] idsparameter format required by your ASP.NET CoreComparecontroller method from Step 2.Dynamic UI Syncing (
updateCompareBar): This function serves as your UI state engine. It looks for a navigation badge or bar element by ID and updates its numbers dynamically. If items exist, it displays the bar; if it's empty, it hides it instantly (display: 'none'). Running this onDOMContentLoadedensures the tracking badge stays perfectly synced even after a full page reload.Smart Adaptive Reload Routing (
removeCompareAndReload): When a user deletes a column inside the matrix view, this method cleans the tracking list and evaluates if a matrix layout can still be maintained. If removing an item drops the selection below the required 2-item minimum boundary rule, it automatically redirects the customer back to the main catalog index page.
Step 6: Implementing the Floating Comparison Bar Overlay
By placing this markup inside your global _Layout.cshtml file, you create an application-wide user interface overlay. It sits completely hidden from view until your Step 5 JavaScript detects items saved in the browser memory.
Core UI & Bootstrap Styling Breakdown
Fixed Viewport Pinning (
position-fixed bottom-0 start-50 translate-middle-x): This combination of utility classes anchors the panel right at the bottom center of the browser viewport. Combiningstart-50withtranslate-middle-xensures that the bar stays perfectly centered horizontally across desktops, tablets, and smartphones alike.Layer Isolation Control (
z-index:9999;): Setting an explicit high stack index ensures that this floating container will render directly on top of all other page elements—including footers, sliders, and standard content cards—preventing any rendering overlaps while browsing.DOM-Driven Display Toggle (
style="display:none;"): Initializing the markup with a hidden display state prevents it from flashing onto the screen during initial page compilation. It hands absolute control over to yourupdateCompareBar()JavaScript function, which switches it todisplay: flexcleanly when needed.Dynamic Text Hook Target (
id="compareCount"): This tiny span tag acts as the dedicated landing point for your DOM element targeting scripts. By assigning this explicit identifier, your client script can overwrite the value instantly inside the user's view whenever a item card button is triggered.
Step 7: Designing the Modern Comparison Interface Component
You will add this code to your global stylesheet (typically wwwroot/css/comparebar.css). It transforms the basic floating structural container into a beautiful, interactive card component.
Core UI Styles & Design Breakdown
Modern App Aesthetics (
border-radius: 18px& Subtle Shadows): Using a smoothborder-radius: 18pxcombined with a clean, ambient drop shadow (box-shadow: 0 10px 30px rgba(0,0,0,0.12)) strips away the dated, flat look. It makes the sidebar appear to hover gracefully over your catalog grids as an independent layer.Intuitive Circular Controls (
.compare-close): Designing a explicit circular button (width: 32px; height: 32px; border-radius: 50%) with a smooth background color shift (transition: 0.3s;) on hover creates highly predictable user interactive patterns. Switching immediately to red (#dc3545) signals a destructive "close" action instantly.Flexible Button Alignment Layouts (
display: flex; gap: 10px;): Using CSS Flexbox styles withflex: 1assigned to the primary comparison selector button dynamically stretches it out to fill up all remaining horizontal container width space. This ensures an exact, evenly balanced button bar regardless of different user viewport screen bounds.Hardware-Accelerated Entrance Transitions (
@keyframes slideUp): This adds incredible UI fluidness. Instead of jarringly snapping onto the screen when the first mobile device is added, the animation smoothly scales the component from an opacity of0up to1while translating its vertical space up by30px. This subtle hint gives your storefront a highly professional, interactive feel.
Step 8: Adding the Compare Button to the Product Details View
By adding this button to Details.cshtml, you complete the comparison user loop. It sits right alongside your wishlist or cart actions, giving the user standard, high-tier retail flexibility.
Core UI & Parameter Breakdown
Prominent Action Styling (
btn-outline-dark btn-lg): Unlike the compact button on the shared product card, the details page demands clear, high-visibility actions. Usingbtn-lg(large) makes the button match the scale of your "Add to Cart" or "Wishlist" elements. Thebtn-outline-darkclass gives it a premium, sleek contrast that highlights the option without overwhelming your primary checkout buttons.Explicit Object Traversal (
@Model.Product.Id): Because your Product Details view utilizes your compound details ViewModel (which contains the nested product specs and review structures we configured in Part 12), you access the identifier by traversing directly into the product entity via@Model.Product.Id.Unified JavaScript Event Reusability:
onclick="addToCompare(@Model.Product.Id)"This demonstrates excellent architectural layout logic to your viewers. You don't need to write a separate script for this page. The button reuses the exact same globaladdToComparealgorithm embedded in your_Layout.cshtmlfrom Step 5, checkinglocalStoragecaps and updating your floating bar completely seamlessly.
# Final Features Included
✅ LocalStorage based compare system
✅ Responsive compare table
✅ Dynamic specifications comparison
✅ Floating compare bar
✅ Same Bootstrap UI design
✅ ASP.NET Core MVC structure maintained
✅ Works with your existing Product model
✅ Works with existing Product Specifications
✅ Add to Cart directly from compare page

Comments
Post a Comment