Class PropertyGrid
A Unity-Inspector-style control: lists Target's properties as rows (a label and a type-appropriate editor), letting a user edit them.
public class PropertyGrid : ItemsControl, INotifyPropertyChanging, IBindingTarget, INotifyPropertyChanged, INotifyFocusChanged, IContainerLayout, IVirtualizingScrollInfo
- Inheritance
-
PropertyGrid
- Implements
- Inherited Members
- Extension Methods
Remarks
ItemsSource holds a flat mix of PropertyGridEntry (a real row) and an internal category-header marker, rebuilt whenever Target changes. CreateContainer(DataTemplate, object) is overridden to build each row's Grid (label | editor) directly in code - see BuildEditor(PropertyGridEntry, object) for the per-Type editor dispatch - rather than through ItemTemplate/markup, mirroring TabControl's own precedent for un-templated, code-built items.
PoolingEnabled is disabled at construction for the same reason
TabControl disables it: a row built directly in code has no {Binding} for
ItemsControl's pooled-reuse path (which only reassigns the pooled container's
Content's DataContext) to refresh - without this, a
recycled row could keep showing a previous Target's editor widget/value.
This control never virtualizes: a property list is expected to be short enough that eagerly realizing every row (see MeasureContent()/ArrangeContent(), which lay rows out as a simple vertical stack) is preferable to the complexity of virtualizing it. Rows come and go in exactly one place - Icy.UI.Controls.PropertyGrid.RealizeAllRows(), driven by a Target reassignment - and never because of where the viewport happens to sit.
It does still take part in IVirtualizingScrollInfo, because ItemsControl implements that interface and a hosting ScrollViewer therefore delegates to it unconditionally - but only for the two halves of the contract that make sense here, both overridden to replace the base's estimating/virtualizing behavior:
- extent reporting - ComputeExtentHeight() returns the eagerly measured stack's real, exact height, not ItemsControl's running-average estimate over partially realized items;
-
offset-driven repositioning - OnViewportChanged(float, float, float, float) records the new
offsets/viewport and re-arranges, which ArrangeContent() honors by shifting every row by the
current scroll offset. It deliberately does not call the base implementation, whose realize walk would
de-realize every row outside the viewport and leave ArrangeContent()'s own loop over
[0, ItemCount)looking up rows that no longer exist.
Host this inside a ScrollViewer for scrolling when the target has more properties than fit on screen.
Constructors
PropertyGrid()
Initializes a new instance of the PropertyGrid class.
public PropertyGrid()
Properties
ActiveEntry
Gets the property of the row holding the keyboard focus, or null.
public PropertyGridEntry? ActiveEntry { get; }
Property Value
ActiveMessage
Gets the help text for ActiveEntry: why its typed value is rejected while it is, otherwise its Description.
public string? ActiveMessage { get; }
Property Value
Remarks
Core has no tooltips, so hosts show this themselves, for example in a status line under the grid. It updates on focus changes and keystrokes only.
Target
Gets or sets the object whose properties this grid displays and edits. Setting this re-enumerates every browsable property via EnumerateFor(object) and rebuilds every row; assigning null clears the grid.
public object? Target { get; set; }
Property Value
Remarks
This property owns ItemsSource outright - every assignment here overwrites it wholesale - so the inherited ItemsSource should not be set directly on a PropertyGrid. CreateContainer(DataTemplate, object) still falls back to a read-only display for an item it doesn't recognize rather than throwing, but that path exists purely so a stray direct assignment can't crash a layout pass - it is not a supported way to populate this control.
ValueAdapter
Gets or sets how rows read and write Target's properties. Defaults to Default, which reads and writes them directly.
public PropertyGridValueAdapter ValueAdapter { get; set; }
Property Value
Remarks
Setting it rebuilds every row.
Exceptions
- ArgumentNullException
The value is null.
Methods
ArrangeContent()
Handles custom arrange logic when overridden in any UIElement instance.
protected override void ArrangeContent()
Remarks
This method is called during the arrange pass to position and size the element's content within its final bounds. The element's position and size are already determined by the layout system and available in the ActualBounds property.
When implementing this method:
- Position child elements within the available space;
- Respect the element's padding when positioning content;
- Handle any content-specific layout requirements;
- Call Arrange() on child elements if needed.
ComputeExtentHeight()
Computes ExtentHeight - the running-average single-column estimate by default. A subclass with different virtualization geometry (e.g. a uniform grid) overrides this with its own formula instead.
protected override float ComputeExtentHeight()
Returns
- float
The total height of all items in the collection, estimated via a running average of known heights.
Remarks
Returns the eagerly realized row stack's real, exact total height (see Icy.UI.Controls.PropertyGrid.MeasureRowStack()) - ItemsControl's own running-average estimate over partially realized items would report a height this control's layout never produces, which a hosting ScrollViewer would then clamp VerticalOffset against (typically an underestimate, cutting the last rows out of reach entirely). Realizes every row first, since this can be read - via ExtentHeight, e.g. by ExtentHeight - before this control has ever been measured.
CreateContainer(DataTemplate, object)
Creates the row container for an item: a category header or a property's label and editor.
protected override ItemContainer CreateContainer(DataTemplate template, object item)
Parameters
templateDataTemplateUnused - see the class remarks.
itemobjectIdeally a PropertyGrid.CategoryHeader or a PropertyGridEntry.
Returns
- ItemContainer
A new container hosting the item's row.
Remarks
Dispatches to a category-header row for a PropertyGrid.CategoryHeader marker item, or a label+editor Grid row for a PropertyGridEntry - see BuildEditor(PropertyGridEntry, object) for the editor-widget dispatch by PropertyType. Anything else falls back to the same read-only TextBlock display BuildEditor(PropertyGridEntry, object) uses for a type it has no editor for, rather than throwing: ItemsSource is inherited and publicly settable, and a mismatched item assigned straight to it (see Target's remarks for why that isn't a supported way to populate this control) must not crash row construction from inside a layout pass.
MeasureContent()
Handles custom measure logic when overridden in any UIElement instance.
protected override Size MeasureContent()
Returns
Remarks
This method is called during the measure pass to determine the desired size of the element. The returned size should represent the natural size of the element's content, without considering any constraints, margins, or padding.
When implementing this method:
- Return the natural size needed to display the content;
- Do not include padding, margins, or borders in the returned size;
- Consider the content's actual dimensions (e.g., text length, image size);
- Return Empty if the element has no content.
OnViewportChanged(float, float, float, float)
Called by the host ScrollViewer whenever the offset or viewport size it's presenting this content through may have changed - the single point where this content decides what to realize, de-realize, and how to position it.
public override void OnViewportChanged(float newHorizontalOffset, float newVerticalOffset, float newViewportWidth, float newViewportHeight)
Parameters
Remarks
Records the offsets/viewport the host ScrollViewer reports and re-arranges (see ArrangeContent(), which offsets every row by them) - deliberately without calling OnViewportChanged(float, float, float, float), and without any realize/de-realize work of its own.
The base implementation's realize walk keeps only the rows intersecting the viewport (plus
ScrollAheadBuffer) realized and de-realizes the rest, which directly
contradicts this control's eager, keep-everything-realized design: ArrangeContent() walks
[0, ItemCount) unconditionally, so any row dropped for sitting off
screen would leave it looking up a container that no longer exists. Rows are realized and de-realized in
exactly one place, Icy.UI.Controls.PropertyGrid.RealizeAllRows(), driven by Target - never by scrolling.
Refresh()
Re-reads every row's value, expression and Reset state from Target through ValueAdapter, keeping the rows themselves.
public void Refresh()
Remarks
Call it after something other than this grid changed the target, such as an undo. Only rows whose value, expression or Reset state changed get a new editor, so a refresh after a one-property edit is cheap.
Two rows keep their editor regardless: the one being written right now, and the one holding the keyboard focus, so typing is never interrupted.
Events
ActiveMessageChanged
Occurs when ActiveEntry or ActiveMessage changed.
public event EventHandler? ActiveMessageChanged