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.4The 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
π Table of Contents
π 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)
Primary Responsibility:
Accepts a data object and transforms it into a hierarchical property structure
Key Functions:
OnParametersSet()- Initializes the component when parameters are passedMapPropertiesOfDataContext()- Recursively walks the object graph and builds theHierarchicalPropertyInfotreeIsNestedProperty()- 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 treeOnValueSetCallback()- Listens for value changes and emitsPropertySetValueCallbackevents to the parent component
Parameters:
DataContext(object) - The root object to displayObjectTitle(string) - Display title for the gridIsEditingAllowed(bool) - Whether fields are editablePropertySetValueCallback(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)
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 callbacksToggleExpandButton()- 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 renderDepth(int) - Nesting depth for styling and collapse behaviorDisplayedFullPropertyPaths(List<string>) - Tracks which paths have been rendered (prevents duplication)
Features:
- Editable/Read-only modes based on
IsEditableflag - 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)
Primary Responsibility:
Act as a node in a tree structure that mirrors the original object's property graph
Properties:
PropertyName(string) - Name of the propertyPropertyValue(object) - Current value of the propertyPropertyType(Type) - CLR type of the propertySubProperties(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 stateNewValue(object) - Tracks modified value before submissionValueSetCallback(EventCallback) - Callback when this property's value changes
4. PropertyChangedInfoNotificationInfoPayload (PropertyChangedInfoNotificationInfoPayload.cs)
Primary Responsibility:
Communicate property change events with detailed context
Properties:
FieldName(string) - Name of the propertyFullPropertyPath(string) - Complete path to the propertyValue(object) - New valueValueType(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]