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]