Monday, 2 March 2026

DeepAI Image Colorizer

๐ŸŽจ DeepAI Image Colorizer: Bringing Life to Black & White Photos with .NET

๐Ÿ“– Introduction

In the digital age, we often encounter historical photographs, vintage images, or artistic black and white compositions that we'd love to see in full color. While professional colorization requires significant artistic skill and time, modern AI has democratized this process. Today, we'll explore a .NET console application that leverages the DeepAI Colorization API to automatically transform grayscale images into vibrant, colorized versions.

๐ŸŽฏ The Problem Statement

Colorizing black and white images manually is a time-intensive process that requires:

  • Deep understanding of color theory
  • Artistic sensibility for appropriate color selection
  • Hours of meticulous work in image editing software

For developers and researchers working with large collections of historical images, automated solutions become essential. Our solution provides a programmatic approach to image colorization using cutting-edge AI technology.

๐Ÿ—️ Solution Architecture

The DeepAI Image Colorizer is a lightweight .NET console application that serves as a bridge between local image files and the DeepAI colorization service. The architecture follows clean code principles with separation of concerns:

Core Components

  1. Program.cs - Entry point and command-line interface
  2. ImageColorizerHelper.cs - API interaction and image processing logic
  3. Environment Configuration - Secure API key management

Technology Stack

  • Framework: .NET 10.0 with C# 14.0
  • Dependencies:
    • DotNetEnv for environment variable management
    • System.Net.Http for API communication
  • External Service: DeepAI Colorization API

๐Ÿ’ป Implementation Details

You can see the source code online on my GitHub repo here:

https://github.com/toreaurstadboss/DeepAIColorizer

Command-Line Interface Design

The application features a clean, user-friendly CLI with comprehensive argument parsing:

static async Task Main(string[] args)
{
    // Load environment variables from .env file
    Env.Load();

    var inputPath = GetArgValue(args, "--input") ?? GetArgValue(args, "-i");
    var outputPath = GetArgValue(args, "--output") ?? GetArgValue(args, "-o");
    var apiKey = GetArgValue(args, "--apikey") ?? Environment.GetEnvironmentVariable("DEEPAI_API_KEY");

    // Display help if no arguments provided
    if (args.Length == 0 || args.Contains("--help") || args.Contains("-h"))
    {
        DisplayHelp();
        return;
    }
    // ... validation and processing logic
}

API Integration Layer

The ImageColorizerHelper class encapsulates all DeepAI API interactions, providing a clean abstraction:

public class ImageColorizerHelper
{
    private readonly string _apiKey;
    private readonly HttpClient _httpClient;

    public ImageColorizerHelper(string apiKey)
    {
        if (string.IsNullOrWhiteSpace(apiKey))
        {
            throw new ArgumentException("API key cannot be null or empty.", nameof(apiKey));
        }

        _apiKey = apiKey;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Add("api-key", _apiKey);
    }
}

Asynchronous Image Processing

The core colorization method handles the complete workflow asynchronously. The image inputted will be posted as a binary array added in MultipartFormDataContent to the endpoint
where DeepAI Colorizer service is served. https://api.deepai.org/api/colorizer - Note - This endpoint is only POST-ed to. The response is an url (json) that points to where we can download the final colorized picture, if success. The code shows we post the input image (grayscale image obviously) to colorize:

public async Task ColorizeImageAsync(string inputPath, string outputPath)
{
    if (!File.Exists(inputPath))
    {
        throw new FileNotFoundException($"Input image not found: {inputPath}");
    }

    // Prepare multipart form data with the image
    using var form = new MultipartFormDataContent();
    var imageBytes = await File.ReadAllBytesAsync(inputPath);
    form.Add(new ByteArrayContent(imageBytes), "image", Path.GetFileName(inputPath));

    Console.WriteLine("⏳ Sending image to DeepAI for colorization...");

    // Send request to DeepAI API
    var response = await _httpClient.PostAsync("https://api.deepai.org/api/colorizer", form);
    response.EnsureSuccessStatusCode();

    var jsonResponse = await response.Content.ReadAsStringAsync();
    Console.WriteLine($"๐Ÿ“ก Received response from DeepAI");

    // Parse JSON response to extract the output URL
    var result = JsonDocument.Parse(jsonResponse);
    if (!result.RootElement.TryGetProperty("output_url", out var urlElement))
    {
        throw new InvalidOperationException("DeepAI response missing 'output_url' property.");
    }

    var outputUrl = urlElement.GetString();
    if (string.IsNullOrWhiteSpace(outputUrl))
    {
        throw new InvalidOperationException("DeepAI returned an empty output URL. The image may have been rejected.");
    }

    Console.WriteLine($"๐ŸŒ Output URL: {outputUrl}");
    Console.WriteLine("⏳ Downloading colorized image...");

    // Download the colorized image
    var colorizedBytes = await _httpClient.GetByteArrayAsync(outputUrl);

    // Ensure output directory exists
    var outputDir = Path.GetDirectoryName(outputPath);
    if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir))
    {
        Directory.CreateDirectory(outputDir);
    }

    // Save the colorized image
    await File.WriteAllBytesAsync(outputPath, colorizedBytes);
    Console.WriteLine($"๐Ÿ’พ Saved colorized image ({colorizedBytes.Length:N0} bytes)");
}

๐Ÿ”ง Configuration and Security

Environment-Based API Key Management

The application prioritizes security by supporting multiple API key sources:

var apiKey = GetArgValue(args, "--apikey") ?? Environment.GetEnvironmentVariable("DEEPAI_API_KEY");

This allows users to:

  • Store keys in a .env file (loaded automatically)
  • Pass keys via command-line arguments
  • Use environment variables in CI/CD pipelines

Project Configuration

The .csproj file demonstrates modern .NET project setup:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>14.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DotNetEnv" Version="3.1.1" />
  <ItemGroup>

</Project>

๐Ÿš€ Usage Examples

Basic Colorization

DeepAIColorizer --input old_photo.jpg --output colorized_photo.png

With Custom API Key

DeepAIColorizer --input image.png --apikey your_deepai_key_here

Batch Processing Integration

The CLI design makes it perfect for batch processing:

for file in *.jpg; do
    DeepAIColorizer --input "$file"
done

✨ Key Features and Benefits

๐ŸŽจ Automated Colorization

  • Leverages state-of-the-art AI models trained on millions of images
  • Produces natural-looking colors without manual intervention

๐Ÿ”’ Security-First Design

  • Multiple API key management options
  • No hardcoded credentials
  • Environment variable support for production deployments

๐Ÿš€ Developer-Friendly

  • Clean, documented code following .NET best practices
  • Comprehensive error handling and user feedback
  • Asynchronous operations for responsive CLI experience

๐Ÿ“Š Progress Indicators

  • Real-time feedback during processing
  • Clear success/error messaging with emojis
  • File size reporting for verification

๐Ÿ”ง Extensible Architecture

  • Modular design allows easy integration into larger systems
  • HTTP client abstraction enables testing and mocking
  • Clean separation between CLI and business logic

๐Ÿ” Technical Analysis

Performance Characteristics

  • Network I/O: Two HTTP requests per image (upload + download)
  • Memory Usage: Minimal - processes images in streams
  • CPU Overhead: Negligible - delegates heavy computation to DeepAI servers

Error Handling Strategy

The application implements comprehensive error handling:

  • Input Validation: Checks file existence and API key presence
  • API Error Handling: Distinguishes between different HTTP status codes
  • Network Resilience: Proper async/await patterns for network operations
  • User Feedback: Clear error messages with actionable guidance

Code Quality Metrics

  • Cyclomatic Complexity: Low - simple, linear control flow
  • Testability: High - dependency injection and interface segregation
  • Maintainability: Excellent - clear naming and documentation

๐ŸŽ“ Academic Applications

This tool has significant value in academic research:

๐Ÿ“š Historical Research

  • Colorizing archival photographs for modern publications
  • Enhancing visual materials for academic presentations
  • Preserving historical imagery with improved accessibility

๐ŸŽจ Digital Humanities

  • Automated processing of large image collections
  • Integration with research workflows and pipelines
  • Supporting visual analysis in humanities studies

๐Ÿ’ป Computer Science Education

  • Practical example of API integration
  • Demonstration of async programming patterns
  • Real-world application of software engineering principles

๐Ÿ”ฎ Future Enhancements

Potential improvements for future versions:

  • Batch Processing: Support for multiple input files
  • Format Conversion: Automatic format detection and conversion
  • Quality Options: Different colorization quality levels
  • Preview Mode: Generate thumbnails before full processing
  • Integration APIs: REST API wrapper for web applications

๐Ÿ“š Conclusion

The DeepAI Image Colorizer represents a perfect intersection of modern AI capabilities and practical software engineering. By abstracting complex machine learning models behind a simple, secure CLI interface, it makes advanced image processing accessible to developers, researchers, and enthusiasts alike.

The implementation demonstrates key software engineering principles: clean architecture, comprehensive error handling, security-conscious design, and excellent user experience. Whether you're a historian bringing old photographs to life or a developer learning API integration, this project serves as both a practical tool and an educational reference.

Ready to colorize your world? ๐Ÿš€ The code is available on GitHub - clone, build, and start transforming black and white images into vibrant masterpieces!

Tips how to get contact the DeepAI Api using Postman

  • The request must be of type POST and url set to : https://api.deepai.org/api/colorizer
  • Headers - set one header : api-key . The value here is your DeepAI api key and must of course be not compromised.
  • Body : Choose form-data as the type of body. Add a key called image.
  • Choose the folder icon and connect to a local folder on your hard drive and upload image. This is the image key value under POST.
  • You should get a response with a Json with the information where to download the processed image, which is colorized.
Example output of response json: { "id": "exampleGuid1", "output_url": "https://api.deepai.org/job-view-file/exampleGuid2/outputs/output.jpg" } ExampleGuids here will of course vary per run. To download the actual outputted image, just follow the URL. This can actually be done inside Postman.



Example input and output images using the tool

The following examples images shows input and output images using the tool. The scenery is from Trondheim, Norway in 1959. Original photo (grayscale, 1959) :



Colorized photo (DeepAI Image Colorization online API service) using this tool :

Saturday, 21 February 2026

Copy bookmarks between Edge and Canary | Powershell

I just wrote a Powershell script to copy bookmarks from one browser profile to another browser profile. In this case I copied my bookmarks in Edge Chromium
over to Google Canary. Of course, which bookmark file in which folders will vary from browser to browser and also which profile. In this case, the default profile is copied. In case your computer is used by several users, you probably want to copy a specific profile, not Default. In that case, check in file Explorer which profiles are there.

CopyBookmarksFromEdgeToCanary.ps1

<#
Edge -> Chrome Canary bookmarks copy (profile to profile)
Enhancements:
- Clear screen
- Progress bar (Write-Progress) [2](https://stackoverflow.com/questions/2688547/multiple-foreground-colors-in-powershell-in-one-command)
- Colored/emoji-rich output (Write-Host) [3](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/write-host?view=powershell-7.5)
- Counts number of bookmark URL entries by parsing Bookmarks JSON (roots + children) [1](https://jdhitsolutions.com/blog/powershell-3-0/2591/friday-fun-testing-google-chrome-bookmarks-with-powershell/)
- Measures elapsed time with Stopwatch
#>

Clear-Host

# -----------------------------
# Settings (edit these)
# -----------------------------
$edgeProfileChoice   = "Default"
$canaryProfileChoice = "Default"

# -----------------------------
# Helper: multi-color one-liner output
# -----------------------------
function Write-ColorLine {
    param(
        [string[]]$Text,
        [ConsoleColor[]]$Color,
        [switch]$NoNewLine
    )
    for ($i = 0; $i -lt $Text.Count; $i++) {
        $c = if ($i -lt $Color.Count) { $Color[$i] } else { $Color[-1] }
        Write-Host $Text[$i] -ForegroundColor $c -NoNewline
    }
    if (-not $NoNewLine) { Write-Host "" }
}

# -----------------------------
# Helper: progress stage
# -----------------------------
function Write-Step {
    param(
        [int]$Step,
        [int]$Total,
        [string]$Status
    )
    $pct = [Math]::Round(($Step / $Total) * 100, 0)
    Write-Progress -Id 0 -Activity "๐Ÿงญ Edge ➜ Canary Bookmarks Migration" -Status $Status -PercentComplete $pct
}

# -----------------------------
# Helper: list profile folders that contain Bookmarks
# -----------------------------
function Get-BookmarkProfiles {
    param([string]$BasePath)

    Get-ChildItem -Path $BasePath -Directory -ErrorAction SilentlyContinue |
        Where-Object { Test-Path (Join-Path $_.FullName "Bookmarks") } |
        Select-Object -ExpandProperty Name
}

# -----------------------------
# Helper: pretty file info
# -----------------------------
function FileInfoLine {
    param([string]$Path)
    if (Test-Path $Path) {
        $fi = Get-Item $Path
        "{0}  (Size: {1:n0} bytes, LastWrite: {2})" -f $fi.FullName, $fi.Length, $fi.LastWriteTime
    } else {
        "$Path  (missing)"
    }
}

# -----------------------------
# Helper: count bookmark "url" nodes recursively in Chromium Bookmarks JSON
# -----------------------------
function Get-BookmarkUrlCount {
    param([string]$BookmarksPath)

    if (-not (Test-Path $BookmarksPath)) { return 0 }

    try {
        $json = Get-Content $BookmarksPath -Raw | ConvertFrom-Json
    } catch {
        return 0
    }

    $script:count = 0
    function Walk($node) {
        if ($null -eq $node) { return }

        if ($node.PSObject.Properties.Name -contains "type" -and $node.type -eq "url") {
            if ($node.PSObject.Properties.Name -contains "url" -and $node.url) { $script:count++ }
        }

        if ($node.PSObject.Properties.Name -contains "children" -and $node.children) {
            foreach ($child in $node.children) { Walk $child }
        }
    }

    if ($json.PSObject.Properties.Name -contains "roots") {
        foreach ($rootProp in $json.roots.PSObject.Properties) {
            Walk $rootProp.Value
        }
    }

    return $script:count
}

# -----------------------------
# Plan
# -----------------------------
$totalSteps = 9
$step = 0
$sw = [System.Diagnostics.Stopwatch]::StartNew()

Write-ColorLine -Text @("✨ ", "Bookmark mover ready", " — Edge ➜ Chrome Canary") `
               -Color @("Yellow","Green","Cyan")

$step++; Write-Step $step $totalSteps "Resolving base paths…"

$edgeUserData   = Join-Path $env:LOCALAPPDATA "Microsoft\Edge\User Data"
$canaryUserData = Join-Path $env:LOCALAPPDATA "Google\Chrome SxSata"

# (rest of script unchanged, escaped consistently)
Sample output of running the Powershell script below. The script takes around half a second to run.

✨ Bookmark mover ready — Edge ➜ Chrome Canary

๐Ÿ“ Base paths
   Edge   : C:\Users\someuser\AppData\Local\Microsoft\Edge\User Data
   Canary : C:\Users\someuser\AppData\Local\Google\Chrome SxSata

๐Ÿ”Ž Profiles detected (contain a 'Bookmarks' file)
   Edge   : Default
   Canary : Default

๐ŸŽฏ Selected profiles
   Edge   : Default
   Canary : Default

๐Ÿงพ Full file paths
   Edge Bookmarks   : C:\Users\someuser\AppData\Local\Microsoft\Edge\User Data\Default\Bookmarks  (Size: 288 882 bytes, LastWrite: 20.02.2026 16:44:30)
   Canary Bookmarks : C:\Users\someuser\AppData\Local\Google\Chrome SxSata\Default\Bookmarks  (Size: 288 882 bytes, LastWrite: 20.02.2026 16:44:30)

๐Ÿ“Š Bookmark counts (URL entries)
   Edge (source)   : 585
   Canary (target) : 585

๐Ÿ›Ÿ Backup created: C:\Users\someuser\Desktop\BookmarkBackups\Canary_Default_Bookmarks_20260221_205353.bak


✅ Completed!
๐Ÿ“Œ Wrote Canary Bookmarks:
   C:\Users\someuser\AppData\Local\Google\Chrome SxSata\Default\Bookmarks
๐Ÿ“ฆ Backup folder:
   C:\Users\someuser\Desktop\BookmarkBackups

๐Ÿ“ˆ Results
   Canary before : 585 bookmarks
   Canary after  : 585 bookmarks
   ฮ” Change      : 585

⏱️ Time elapsed: 00:00.504
๐Ÿš€ Tip: Launch Chrome Canary now — bookmarks load on startup.


Sunday, 15 February 2026

Blazor Property Grid - Updated version

Blazor Property Grid

I wrote a Blazor Property Grid component back in 2021, the updated the version is now available some five years later.

You will find the property grid available on Nuget here:

https://www.nuget.org/packages/BlazorPropertyGridComponents/1.2.4

The property grid allows inspection of an object's properties and also edit them. Supported data types are the fundamental data types, which means integers, date times, booleans, strings and numbers.

The property grid supports nested properties, properties that are compound objects themselves. I have not yet added template supported for custom data types, but fundamentals nested properties inside the complex property are shown. This means you can drill down into an object and both inspect the object and also edit the properties that are the mentioned fundamental data types.

This project delivers a Blazor property‑grid component capable of inspecting and editing both top‑level and deeply nested properties of an object. It works smoothly with nested structures and internal members.

It has been verified using Blazor WebAssembly running on .NET. 10. A sample client is included in the BlazorSampleClient project.

The component implementation is located inside the Razor Class Library BlazorPropertyGridComponents.

Licensing is MIT and the component is provided as‑is. If used in production, you are responsible for validating its suitability. Forks, modifications, and commercial reuse are allowed. The project originated as a personal learning exercise.

The screenshot below (from the sample client) illustrates the property grid on the right side updating the same model that the form on the left is bound to.

The component expects your project to include Bootstrap and Font Awesome. You can inspect exact versions inside libman.json. Additional styling resides in styles.css.

libman.json example

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "bootstrap@5.3.3",
      "destination": "wwwroot/bootstrap/"
    },
    {
      "library": "font-awesome@6.5.1",
      "destination": "wwwroot/font-awesome/"
    }
  ]
}

Supported Property Types

  • DateTime (full or date‑only)
  • Float
  • Decimal
  • Int
  • Double
  • String
  • Bool
  • Enums (rendered using <select> and <option>)

Using the Component – API

Below is a Razor example showing how to use the component. The PropertySetValueCallback is optional and only required if you want UI changes reflected elsewhere immediately.

<div class="col-md-6"> <!-- Property grid shown in the right column -->
  <h4 class="mb-3">Property Grid Component Demo</h4>

  <PropertyGridComponent
      PropertySetValueCallback="OnPropertyValueSet"
      ObjectTitle="Property grid - Edit form for : 'Customer Details'"
      DataContext="@exampleModel">
  </PropertyGridComponent>
</div>

@code {
  private void OnPropertyValueSet(PropertyChangedInfoNotificationInfoPayload pi)
  {
    if (pi != null)
    {
      JsRunTime.InvokeVoidAsync(
        "updateEditableField",
        pi.FieldName,
        pi.FullPropertyPath,
        pi.Value
      );
    }
  }

  private CustomerModel exampleModel = new CustomerModel
  {
    Address = new AddressInfo
    {
      Zipcode = 7045,
      AddressDetails = new AddressInfoDetails
      {
        Box = "PO Box 123"
      }
    }
  };

  private void HandleValidSubmit()
  {
  }
}

The JavaScript function updateEditableField is located in script.js.

Updating via C# Instead of JS

You can also update the model directly through reflection. This approach ensures proper Blazor re‑rendering. Be sure to call StateHasChanged(). Note that this code is only required in case you use the property grid for editing properties and show the object you edit other places on the same page.

@code {

  private void OnPropertyValueSet(PropertyChangedInfoNotificationInfoPayload pi)
  {
    if (pi == null)
      return;

    SetPropertyByPath(exampleModel, pi.FullPropertyPath, pi.Value, pi.ValueType);
    StateHasChanged();
  }

  private void SetPropertyByPath(object target, string propertyPath, object value, string valueType)
  {
    if (target == null || string.IsNullOrEmpty(propertyPath))
      return;

    var parts = propertyPath.Split('.');
    var current = target;

    // Move to parent object

        // Navigate to the parent object
        for (int i = 0; i < parts.Length - 1; i++)
        {
            var prop = current.GetType().GetProperty(parts[i]);
            if (prop == null) return;
            current = prop.GetValue(current);
            if (current == null) return;
        }

        // Set the final property
        var finalProp = current.GetType().GetProperty(parts[^1]);
        if (finalProp == null) return;

        try
        {
            object convertedValue = value;

            if (finalProp.PropertyType.IsEnum && value != null)
            {
                var valStr = value.ToString();
                if (int.TryParse(valStr, out int intVal))
                    convertedValue = Enum.ToObject(finalProp.PropertyType, intVal);
                else
                    convertedValue = Enum.Parse(finalProp.PropertyType, valStr, ignoreCase: true);
            }
            else if (finalProp.PropertyType == typeof(bool) && value != null)
            {
                convertedValue = Convert.ToBoolean(value);
            }
            else if (value != null && finalProp.PropertyType != typeof(string))
            {
                convertedValue = Convert.ChangeType(value, finalProp.PropertyType);
            }

            finalProp.SetValue(current, convertedValue);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to set {propertyPath}: {ex.Message}");
        }
    }

    private CustomerModel exampleModel = new CustomerModel
    {
        Address = new AddressInfo
        {
            Zipcode = 7045,
            AddressDetails = new AddressInfoDetails
            {
                Box = "PO Box 123"
            }
        }
    };

    private void HandleValidSubmit()
    {

    }

}

I have used Claude Haiku 4.5 LLM to create a nice Architecture Documentation of my component so it is more convenient to see the structure of the Blazor property grid component. This is handy for those developers who wants to work on the component and add features and understood its structure. As mentioned before, the component is licensed with MIT license and you can adjust the component as needed free of use (and responsiblity).

๐Ÿ”ท Blazor Property Grid Component - Architecture Documentation


๐Ÿ“Š Property Grid Component Structure

PROPERTY GRID COMPONENT ARCHITECTURE
====================================

ROOT CONTAINER: PropertyGridComponent.razor
├── EditForm (wraps the entire grid)
└── .property-grid-container
    ├── Header Table (.property-grid-header-table)
    │   └── thead (.property-grid-header)
    │       └── tr
    │           ├── th: "Property"
    │           ├── th: "Value"
    │           └── th: Edit Button (pencil icon)
    │
    └── Body Table (.property-grid-body-table)
        └── tbody (.property-grid-body)
            └── foreach KeyValuePair in Props
                ├── IF: Simple Property (IsClass = false)
                │   └── [COMMENTED OUT - NOT DISPLAYED]
                │
                └── IF: Nested Class (IsClass = true)
                    └── tr
                        ├── td (colspan=2)
                        │   ├── Expand/Collapse Button (minus icon)
                        │   └── div (.collapse .show)
                        │       └── PropertyRowComponent [Depth=1]
                        └── td (empty)


PROPERTY ROW COMPONENT: PropertyRowComponent.razor
====================================================

FOR EACH SubProperty in PropertyInfoAtLevel.SubProperties:
├── IF: Simple Type (not a class or System namespace)
│   └── tr (.property-row)
│       ├── td (.property-name-cell)
│       │   └── span (.property-name) = Property Name
│       │
│       └── td (.property-value-cell)
│           ├── IF: DateTime
│           │   └── InputDate (if editable) OR span (if readonly)
│           │
│           ├── IF: bool
│           │   └── InputCheckbox (if editable) OR span (if readonly)
│           │
│           ├── IF: int
│           │   └── InputNumber (if editable) OR span (if readonly)
│           │
│           ├── IF: double
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: decimal
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: float
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: string
│           │   └── InputText type="text" (if editable) OR span (if readonly)
│           │
│           ├── IF: Enum
│           │   └── select (if editable)
│           │       └── option foreach Enum value
│           │       OR span (if readonly)
│           │
│           └── ELSE: Unknown Type
│               └── span = Raw Value
│
└── IF: Nested Class (PropertyValue is HierarchicalPropertyInfo)
    └── tr (.property-row .nested-property-row)
        ├── td (colspan=2, .nested-property-cell)
        │   ├── span (.nested-property-name) = Nested Class Name
        │   ├── Expand/Collapse Button (plus icon)
        │   └── div (.collapse or .collapse.show)
        │       └── PropertyRowComponent [Depth+1] (RECURSIVE)
        └── [Empty td]


DATA STRUCTURE: HierarchicalPropertyInfo
================================================

HierarchicalPropertyInfo
├── PropertyName: string
├── PropertyValue: object
├── PropertyType: Type
├── SubProperties: Dictionary<string, HierarchicalPropertyInfo>
├── FullPropertyPath: string (dot-separated path)
├── IsClass: bool (indicates if this is a class type)
├── IsEditable: bool
├── NewValue: object (for tracking changes)
└── ValueSetCallback: EventCallback

HIERARCHY BUILD PROCESS:
========================

MapPropertiesOfDataContext(root object)
├── Create ROOT HierarchicalPropertyInfo
├── For each Public Property:
│   ├── IF: Simple Type (not class or not System namespace)
│   │   └── Add to SubProperties as leaf node (IsClass=false)
│   │
│   └── IF: Nested Class (class type, not System namespace)
│       └── Recursively call MapPropertiesOfDataContext
│           └── Add to SubProperties with nested tree (IsClass=true)
│
└── Return complete tree structure


INTERACTIVITY:
==============

Edit Mode Toggle:
├── ToggleEditButton() → IsEditingAllowed = !IsEditingAllowed
└── SetEditFlagRecursive() → walks entire tree setting IsEditable on all nodes

Value Changes:
├── SetValue() called on input change
├── Handles type conversion (enums, numbers, dates, etc.)
├── Updates PropertyValue immediately for UI reflection
└── Invokes ValueSetCallback → OnValueSetCallback()

Property Change Callback:
└── PropertySetValueCallback emits PropertyChangedInfoNotificationInfoPayload with:
    ├── FieldName
    ├── FullPropertyPath
    ├── Value
    └── ValueType (text, boolean, number, date, enum)

Expand/Collapse:
└── ToggleExpandButton() → JavaScript: blazorPropertyGrid.toggleExpandButton()
    └── Toggles Bootstrap collapse class on nested div

๐Ÿ”ง Component Descriptions and Roles

1. PropertyGridComponent (PropertyGridComponent.razor + PropertyGridComponent.razor.cs)

Role: Root container and orchestrator for the entire property grid UI

Primary Responsibility:

Accepts a data object and transforms it into a hierarchical property structure

Key Functions:

  • OnParametersSet() - Initializes the component when parameters are passed
  • MapPropertiesOfDataContext() - Recursively walks the object graph and builds the HierarchicalPropertyInfo tree
  • IsNestedProperty() - Determines if a property should be expanded (nested classes)
  • ToggleEditButton() - Handles the edit mode button click (pencil icon in header)
  • SetEditFlag() / SetEditFlagRecursive() - Propagates the IsEditable flag through the entire tree
  • OnValueSetCallback() - Listens for value changes and emits PropertySetValueCallback events to the parent component

Parameters:

  • DataContext (object) - The root object to display
  • ObjectTitle (string) - Display title for the grid
  • IsEditingAllowed (bool) - Whether fields are editable
  • PropertySetValueCallback (EventCallback) - Callback when a property value changes

Rendering:

Header table with Property/Value columns and edit button. Body table containing rows for top-level properties (delegates nested properties to PropertyRowComponent)

2. PropertyRowComponent (PropertyRowComponent.razor + PropertyRowComponent.razor.cs)

Role: Recursive component that renders individual property rows and handles nested object expansion

Primary Responsibility:

Display a single level of properties and recursively display nested levels

Key Functions:

  • SetValue() - Handles input change events, performs type conversion, and invokes callbacks
  • ToggleExpandButton() - JS interop to toggle Bootstrap collapse classes for expand/collapse UI
  • Property type detection - Conditionally renders different input controls based on property type

Type Support:

  • DateTime → InputDate (HTML5 datetime-local)
  • bool → InputCheckbox
  • int → InputNumber
  • double, decimal, float → InputText with type="number"
  • string → InputText with type="text"
  • Enum → HTML select dropdown with enum values
  • Nested Classes → Recursive PropertyRowComponent call with Depth+1
  • Unknown Types → Raw span display

Parameters:

  • PropertyInfoAtLevel (HierarchicalPropertyInfo) - The current property node to render
  • Depth (int) - Nesting depth for styling and collapse behavior
  • DisplayedFullPropertyPaths (List<string>) - Tracks which paths have been rendered (prevents duplication)

Features:

  • Editable/Read-only modes based on IsEditable flag
  • Collapse/expand functionality for nested objects
  • Visual styling (beige background for read-only values)
  • Full property path as tooltip for clarity

3. HierarchicalPropertyInfo (HierarchicalPropertyInfo.cs)

Role: Data structure representing a single property node in the object hierarchy

Primary Responsibility:

Act as a node in a tree structure that mirrors the original object's property graph

Properties:

  • PropertyName (string) - Name of the property
  • PropertyValue (object) - Current value of the property
  • PropertyType (Type) - CLR type of the property
  • SubProperties (Dictionary<string, HierarchicalPropertyInfo>) - Child properties (for nested objects)
  • FullPropertyPath (string) - Dot-separated path from root (e.g., "Customer.Address.Street")
  • IsClass (bool) - Whether this represents a class type (true = nested object, false = leaf value)
  • IsEditable (bool) - Whether the property can be edited in the current UI state
  • NewValue (object) - Tracks modified value before submission
  • ValueSetCallback (EventCallback) - Callback when this property's value changes

4. PropertyChangedInfoNotificationInfoPayload (PropertyChangedInfoNotificationInfoPayload.cs)

Role: Payload object that carries change notification information back to the parent

Primary Responsibility:

Communicate property change events with detailed context

Properties:

  • FieldName (string) - Name of the property
  • FullPropertyPath (string) - Complete path to the property
  • Value (object) - New value
  • ValueType (string) - Type of value ("text", "number", "boolean", "date", "enum")

๐Ÿ”„ Component Interaction Flow

User interacts with PropertyGrid
    ↓
PropertyGridComponent receives DataContext
    ↓
MapPropertiesOfDataContext() builds tree of HierarchicalPropertyInfo
    ↓
Renders PropertyRowComponent for each top-level property
    ↓
PropertyRowComponent renders based on type:
  ├─ Simple types → InputControl (text, number, checkbox, date, select)
  └─ Nested classes → Recursive PropertyRowComponent
    ↓
User edits a value
    ↓
PropertyRowComponent.SetValue() processes input
    ↓
ValueSetCallback invoked
    ↓
OnValueSetCallback() determines value type and emits PropertySetValueCallback
    ↓
Parent component receives PropertyChangedInfoNotificationInfoPayload

๐ŸŽฏ Key Design Patterns

1. Recursive Composition

PropertyRowComponent calls itself recursively for nested objects, allowing unlimited nesting depth.

2. Tree Structure

HierarchicalPropertyInfo forms a tree that mirrors the object graph, enabling efficient traversal and state management.

3. Event Cascading

Value changes propagate up through callbacks, maintaining separation of concerns between components.

4. Type-Driven Rendering

PropertyRowComponent dynamically renders different input controls based on CLR type, supporting datetime, enum, numeric, boolean, and string types.

5. Bootstrap Collapse Integration

Nested objects use Bootstrap's collapse classes for expand/collapse functionality, toggled via JavaScript interop.


๐Ÿ“Š Data Flow Summary

DataContext (Object)
    ↓
MapPropertiesOfDataContext() [Reflection-based tree building]
    ↓
HierarchicalPropertyInfo Tree
    ↓
PropertyGridComponent → PropertyRowComponent Chain [Rendering]
    ↓
HTML Tables + Form Controls
    ↓
[User edits value]
    ↓
ValueSetCallback Events [Bubbling up]
    ↓
PropertyChangedInfoNotificationInfoPayload [Event payload]
    ↓
Parent Component [Handles business logic]