Property Grid Presenter Layer
Role
The presenter layer adapts the Quantum node graph for UI. INodePresenter is the view model for a single property row in the property grid. INodePresenterUpdater implementations customise the presenter tree — showing, hiding, and augmenting nodes — before the property grid binds to it.
For most new asset types, writing one INodePresenterUpdater subclass is all that is needed. The rest of this file explains how.
INodePresenter Key Members
| Member | Type | Purpose |
|---|---|---|
Name |
string |
Internal identifier — matches the C# property name |
DisplayName |
string |
Label shown in the property grid (settable) |
Type |
Type |
Property type |
Value |
object |
Current value (read) |
UpdateValue(object) |
method | Set a new value (triggers undo/redo, notifications) |
IsVisible |
bool |
Whether this row appears in the property grid (settable) |
IsReadOnly |
bool |
Whether the value can be edited (settable) |
Children |
IReadOnlyList<INodePresenter> |
Child presenters (nested properties) |
Parent |
INodePresenter? |
Parent presenter |
this[string] |
INodePresenter |
Access a child by name (throws if not found) |
TryGetChild(string) |
INodePresenter? |
Access a child by name without throwing |
AttachedProperties |
PropertyContainerClass |
Bag of UI metadata (min/max, category, etc.) |
Commands |
List<INodePresenterCommand> |
Commands shown as buttons or context menu entries |
Order |
int? |
Sort order within the parent (settable) |
AddDependency(node, bool) |
method | Refresh this node when another node changes |
Factory |
INodePresenterFactory |
Factory for creating virtual presenters |
IAssetNodePresenter Extensions
IAssetNodePresenter extends INodePresenter with asset-aware members:
| Member | Type | Purpose |
|---|---|---|
HasBase |
bool |
true if this property has a counterpart in a base asset |
IsInherited |
bool |
true if the value is inherited from the base (not overridden) |
IsOverridden |
bool |
true if the value has been explicitly overridden |
Asset |
AssetViewModel |
The asset this presenter belongs to |
ResetOverride() |
method | Restores this property to its inherited value |
IsObjectReference(value) |
method | Returns true if the given value would be an object reference |
Factory |
AssetNodePresenterFactory |
Narrows factory type for asset-aware virtual node creation |
Presenter Pipeline
When a property grid opens for an asset, AssetNodePresenterFactory runs the following sequence:
- Walks the asset's Quantum node graph depth-first.
- For each node, creates an
IAssetNodePresenter. - Calls
UpdateNode(presenter)on every registeredINodePresenterUpdater— once per node, after all of that node's children have been created. - After the full tree is built, calls
FinalizeTree(root)on every registeredINodePresenterUpdater— once, with the root presenter.
The tree is rebuilt whenever a node's value changes (so UpdateNode is called again for affected nodes, and FinalizeTree is called again for the whole tree).
Updater registration: Updaters are NOT auto-discovered. You must register your updater explicitly in the plugin class for your assembly. For engine assets in Stride.Assets.Presentation, register in StrideDefaultAssetsPlugin (sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs) inside its constructor:
// In StrideDefaultAssetsPlugin constructor:
RegisterNodePresenterUpdater(new %%AssetName%%AssetNodeUpdater());
INodePresenterUpdater — The Main Extension Point
Subclass AssetNodePresenterUpdaterBase and override UpdateNode and/or FinalizeTree. Note that the public UpdateNode(INodePresenter) and FinalizeTree(INodePresenter) methods (which the framework calls) are sealed in AssetNodePresenterUpdaterBase. Only the protected overloads that take IAssetNodePresenter are open for override — these are what you implement:
// sources/editor/Stride.Assets.Presentation/NodePresenters/Updaters/%%AssetName%%AssetNodeUpdater.cs
using Stride.Core.Assets.Editor.Quantum.NodePresenters;
using Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys;
using Stride.Assets.%%AssetName%%;
namespace Stride.Assets.Presentation.NodePresenters.Updaters;
internal sealed class %%AssetName%%AssetNodeUpdater : AssetNodePresenterUpdaterBase
{
// Called once per node after all of that node's children have been created.
// Safe to: set IsVisible, IsReadOnly, Order, DisplayName; set AttachedProperties;
// create virtual node presenters; add commands.
// Not safe to: navigate to sibling nodes or rely on parent's siblings existing.
protected override void UpdateNode(IAssetNodePresenter node)
{
// Guard: only operate on nodes that belong to %%AssetName%%Asset
if (node.Asset?.Asset is not %%AssetName%%Asset asset)
return;
// Example: hide a property based on another property's value
if (node.Name == nameof(%%AssetName%%Asset.SomeProperty))
{
node.IsVisible = asset.SomeFlag;
}
// Example: clamp a numeric property
if (node.Name == nameof(%%AssetName%%Asset.Iterations))
{
node.AttachedProperties.Set(NumericData.MinimumKey, 1);
node.AttachedProperties.Set(NumericData.MaximumKey, 64);
node.AttachedProperties.Set(NumericData.DecimalPlacesKey, 0);
}
}
// Called once after the full presenter tree has been built.
// Safe to: navigate the full tree; add cross-node dependencies.
// Not safe to: add or remove nodes.
protected override void FinalizeTree(IAssetNodePresenter root)
{
if (root.Asset?.Asset is not %%AssetName%%Asset)
return;
// Example: refresh SomeProperty whenever SomeFlag changes
root[nameof(%%AssetName%%Asset.SomeProperty)]
.AddDependency(root[nameof(%%AssetName%%Asset.SomeFlag)], false);
}
}
UpdateNode is called for every node in the tree, including nested nodes. Always guard by checking node.Asset?.Asset is YourAssetType before doing anything, and then check node.Name to target the specific property you want to modify.
AttachedProperties
AttachedProperties is a typed property bag for UI metadata. Set values with node.AttachedProperties.Set(key, value).
Common keys (all in Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys):
| Key | Type | Effect |
|---|---|---|
NumericData.MinimumKey |
object |
Minimum value for numeric inputs |
NumericData.MaximumKey |
object |
Maximum value for numeric inputs |
NumericData.DecimalPlacesKey |
int? |
Number of decimal places shown (0 for integers) |
NumericData.LargeStepKey |
double? |
Step size for large increments (scroll/drag) |
NumericData.SmallStepKey |
double? |
Step size for small increments |
DisplayData.AttributeDisplayNameKey |
string |
Overrides the display name shown in the property grid |
DisplayData.AutoExpandRuleKey |
ExpandRule |
Controls automatic expand/collapse of object nodes |
// Clamp a float between 0 and 1
node.AttachedProperties.Set(NumericData.MinimumKey, 0f);
node.AttachedProperties.Set(NumericData.MaximumKey, 1f);
node.AttachedProperties.Set(NumericData.DecimalPlacesKey, 3);
// Override display name
node.AttachedProperties.Set(DisplayData.AttributeDisplayNameKey, "Radius (units)");
Virtual Node Presenters
A virtual node presenter is a presenter row not backed by a real property on the asset. Use them for computed or derived values that should appear in the property grid.
// Signature (all parameters required):
INodePresenter virtualNode = node.Factory.CreateVirtualNodePresenter(
parent: node.Parent, // where to attach the virtual node
name: "AbsoluteWidth", // unique name within the parent
type: typeof(int), // value type
order: node.Order, // sort order (match adjacent node to appear next to it)
getter: () => node.Value, // reads the display value
setter: node.UpdateValue, // writes back when user edits
hasBase: () => node.HasBase, // for override indicator
isInerited: () => node.IsInherited, // note: misspelled in the API ("isInerited", not "isInherited")
isOverridden:() => node.IsOverridden);
Virtual nodes are recreated every time the presenter tree is rebuilt. Check whether the virtual node already exists before creating it to avoid duplicates:
var existing = node.Parent.TryGetChild("AbsoluteWidth");
var virtualNode = existing
?? node.Factory.CreateVirtualNodePresenter(node.Parent, "AbsoluteWidth", typeof(int), node.Order,
() => node.Value, node.UpdateValue,
() => node.HasBase, () => node.IsInherited, () => node.IsOverridden);
AddDependency
AddDependency makes node A refresh whenever node B changes. Use this in FinalizeTree to keep computed/conditional visibility up to date:
// node A refreshes when node B's value changes
nodeA.AddDependency(nodeB, refreshOnNestedNodeChanges: false);
// node A also refreshes when any child of node B changes
nodeA.AddDependency(nodeB, refreshOnNestedNodeChanges: true);
Navigating category nodes: if your asset uses [Display(category: "Size")], the category node is named using CategoryData.ComputeCategoryNodeName("Size"). Use this helper to build the node name:
// From TextureAssetNodeUpdater:
var sizeCategory = CategoryData.ComputeCategoryNodeName("Size");
root[sizeCategory][nameof(TextureAsset.Width)]
.AddDependency(root[sizeCategory][nameof(TextureAsset.IsSizeInPercentage)], false);
root[sizeCategory][nameof(TextureAsset.Height)]
.AddDependency(root[sizeCategory][nameof(TextureAsset.IsSizeInPercentage)], false);
Assembly Placement
| Type | Assembly | Location |
|---|---|---|
INodePresenter, INodePresenterUpdater interfaces |
Stride.Core.Presentation.Quantum |
sources/presentation/Stride.Core.Presentation.Quantum/Presenters/ |
IAssetNodePresenter, AssetNodePresenterUpdaterBase |
Stride.Core.Assets.Editor |
sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/ |
Attached property key classes (NumericData, DisplayData, CategoryData) |
Stride.Core.Assets.Editor |
sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Keys/ |
Your %%AssetName%%AssetNodeUpdater |
Stride.Assets.Presentation |
sources/editor/Stride.Assets.Presentation/NodePresenters/Updaters/ |