SkiaComponent Base Class
The abstract base class for all visual components — 1386 lines of core infrastructure
Namespace: Beep.Skia — Lines: 1386 — File: SkiaComponent.cs
Table of Contents
Overview
SkiaComponent is the abstract base class for every visual element rendered on a Beep.Skia canvas. It provides the foundational infrastructure that all diagram nodes, UI controls, and custom visual components share:
- Position & Sizing: Absolute X/Y position, Width, Height, and computed bounding boxes in both local and world coordinate spaces.
- IDisposable: Proper resource cleanup for SkiaSharp objects (paints, surfaces, paths) through the
Dispose()pattern. - NodeProperties: A configurable parameter dictionary that enables dynamic property editors without requiring reflection. Every component can expose its editable fields as a
Dictionary<string, ParameterInfo>. - ConnectionPoints: Collections of input and output ports (
InPoints/OutPoints) with lazy layout recomputation for performance. - DrawContent: A
virtualmethod that subclasses override to render their specific visual content using aSKCanvas. - Invalidation: Mark the component or its region as dirty to trigger a redraw on the next render cycle.
- Hit Testing: Point-inside-bounds and per-feature hit test methods for precise mouse interaction routing.
- Absolute Coordinate System: All positioning uses an absolute, unscrolled coordinate space. Pan and zoom transforms are applied once globally by
RenderingHelper.
The class implements IDrawableComponent and serves as the trunk of a deep inheritance tree spanning 200+ node types across 18 diagram families.
Key Properties
| Property | Type | Description |
|---|---|---|
X | float | Absolute X coordinate of the component's top-left corner in world space. |
Y | float | Absolute Y coordinate of the component's top-left corner in world space. |
Width | float | Width of the component in logical units. Default sizes vary by component family. |
Height | float | Height of the component in logical units. Default sizes vary by component family. |
Bounds | SKRect | Computed bounding rectangle from (X, Y, X+Width, Y+Height). Used for hit testing and invalidation. |
NodeProperties | Dictionary<string, ParameterInfo> | Configurable parameter map powering dynamic property editors. See NodeProperties System. |
InConnectionPoints | List<IConnectionPoint> | Input connection ports. Positions recomputed lazily when dirty. |
OutConnectionPoints | List<IConnectionPoint> | Output connection ports. Positions recomputed lazily when dirty. |
IsSelected | bool | Selection state. Affects rendering (selection highlight, resize handles) and keyboard operations. |
IsVisible | bool | Visibility flag. Invisible components are skipped during rendering and hit testing. |
Id | Guid | Unique identifier for serialization, connection tracking, and lookup operations. |
Tag | object | Arbitrary user-defined data attached to the component. |
NodeProperties System
Every SkiaComponent exposes its configurable fields as a Dictionary<string, ParameterInfo>. This enables rich, dynamic property editors — such as the in-app property grid — to discover and edit component settings without runtime reflection overhead.
The ParameterInfo class captures:
- ParameterName: Internal key and property grid label.
- ParameterCurrentValue: The current value (as
object). - DefaultParameterValue: The value restored when resetting the property.
- ParameterType: Underlying CLR type (
string,int,float,bool,SKColor,enum, etc.). - Description: Tooltip/help text shown by the property grid.
- Choices: Optional fixed list of allowed string values, rendered as a dropdown.
Subclasses populate the dictionary in their constructor or InitializeComponent() override:
Defining NodeProperties
protected override void InitializeComponent()
{
base.InitializeComponent();
NodeProperties["Text"] = new ParameterInfo
{
ParameterName = "Text",
ParameterCurrentValue = "New Node",
DefaultParameterValue = "New Node",
ParameterType = typeof(string),
Description = "Label text"
};
NodeProperties["FillColor"] = new ParameterInfo
{
ParameterName = "FillColor",
ParameterCurrentValue = SKColors.CornflowerBlue,
DefaultParameterValue = SKColors.CornflowerBlue,
ParameterType = typeof(SKColor),
Description = "Fill color"
};
NodeProperties["FontSize"] = new ParameterInfo
{
ParameterName = "FontSize",
ParameterCurrentValue = 14f,
DefaultParameterValue = 14f,
ParameterType = typeof(float),
Description = "Font size in points"
};
}
At runtime, the property grid reads GetProperties() to enumerate editable values and calls SetProperties(dict) to apply changes. GetProperties() returns a Dictionary<string, object> of plain values (not ParameterInfo instances), so write back through SetProperties and invalidate the component to reflect the new values on the next draw:
Getting and Setting Properties
var properties = myComponent.GetProperties();
float currentSize = (float)properties["FontSize"];
properties["FontSize"] = 18f;
myComponent.SetProperties(properties);
myComponent.InvalidateVisual();
drawingManager.RequestRedraw();
Note
Changes to NodeProperties values do not automatically trigger Invalidate(). The caller (property editor) is responsible for invalidating the component and canvas after applying changes.
Connection Points
Connection points (ports) are the anchor locations where ConnectionLine segments attach to a component. Each point is an instance of IConnectionPoint stored in the component's InConnectionPoints (input) or OutConnectionPoints (output) collections.
Lazy Port Layout
Port positions are recomputed only when needed. The flow is:
MarkPortsDirty()(protected, onSkiaComponent) sets the dirty flag.OnBoundsChanged()calls it automatically, so moves and resizes always mark the layout stale.- Inside
DrawContent, callEnsurePortLayout(() => LayoutPorts())(protected, onMaterialControl). It invokes your layout action only whenArePortsDirtyis true, then clears the flag. LayoutPorts()recomputes port centers, positions and bounds from the currentBounds.
This keeps rapid drags cheap: geometry changes only set a flag, and the expensive layout runs once, on the next draw.
ConnectionPoint Properties
| Property | Type | Description |
|---|---|---|
Type | ConnectionPointType | In or Out. Determines which side of a component the port appears on. |
DataType | string | Descriptive data type of the values flowing through this port (for example "string", "number"). Used for data-flow visualization and connection validation. |
Position / Center | SKPoint | Absolute world-space position of the port anchor. Updated by LayoutPorts(). |
Bounds / Rect | SKRect | Hit-test rectangle around the port, recomputed with the position. |
RowId | Guid? | Optional identifier for ERD row-level connections. Enables per-field linking in entity diagrams. |
Id | Guid | Unique identifier used for serialization and connection restoration. |
IsAvailable | bool | Whether the port can accept a new connection. |
Connection | IConnectionPoint | The port this one is connected to, or null when free. |
Creating and Layouting Ports
protected override void DrawContent(SKCanvas canvas, DrawingContext context)
{
// Re-layout ports only when the dirty flag is set
EnsurePortLayout(() => LayoutPorts());
// ... draw the component content
}
protected override void LayoutPorts()
{
var b = Bounds;
float midY = b.MidY;
InConnectionPoints.Clear();
OutConnectionPoints.Clear();
InConnectionPoints.Add(new ConnectionPoint
{
Type = ConnectionPointType.In,
Center = new SKPoint(b.Left, midY),
Position = new SKPoint(b.Left, midY),
DataType = "string",
Component = this,
IsAvailable = true
});
OutConnectionPoints.Add(new ConnectionPoint
{
Type = ConnectionPointType.Out,
Center = new SKPoint(b.Right, midY),
Position = new SKPoint(b.Right, midY),
DataType = "string",
Component = this,
IsAvailable = true
});
}
Lifecycle
Every SkiaComponent follows a defined lifecycle from creation through disposal:
| Phase | Method | Description |
|---|---|---|
| Construction | SkiaComponent() | Sets default values for X, Y, Width, Height, Opacity and visibility. Initializes the connection-point collections and the NodeProperties dictionary, and assigns a new Guid. |
| Initialization | InitializeComponent() | Protected virtual, called by the constructor. Subclasses populate NodeProperties, create initial ports and set default sizes. Always call base.InitializeComponent(). |
| Render | DrawContent(SKCanvas, DrawingContext) | Abstract on SkiaComponent; every component renders here. The canvas already has pan/zoom transforms applied, and the DrawingContext carries zoom and viewport state. |
| Update | Update(DrawingContext) | Virtual, called once per frame by the render loop before drawing. Use it for time-based state (animations, hover easing). |
| Invalidation | InvalidateVisual() | Public virtual on MaterialControl. Marks the component as needing a redraw; the host schedules the next frame. |
| Hit Testing | HitTest(SKPoint) | Public virtual. Returns true if the point falls within the component. Override for precise per-shape hit testing. |
| Disposal | Dispose() / Dispose(bool) | Breaks incoming and outgoing connections, then releases SKPaint, SKPath, SKBitmap and other SkiaSharp resources. Sets State = ComponentState.Disposing to prevent use-after-free. |
Typical Component Lifecycle Usage
var node = new ProcessNode { Name = "Process Step" };
node.X = 100;
node.Y = 200;
drawingManager.AddComponent(node);
drawingManager.RequestRedraw();
drawingManager.RemoveComponent(node);
node.Dispose();
Inheritance Hierarchy
The SkiaComponent class sits at the center of a deep inheritance chain that spans from the abstract interface down to concrete diagram nodes:
| Level | Type | Purpose |
|---|---|---|
| 0 | IDrawableComponent (interface) | Contract: Draw, hit test, bounds, dispose, connection points. |
| 1 | SkiaComponent (abstract) | Position, size, NodeProperties, ConnectionPoints, base rendering. All diagram nodes derive from here. |
| 2 | MaterialControl (abstract) | Material Design 3.0 theming: elevation shadows, state layers (hover, focus, pressed), ripple animation, theme-aware color resolution. |
| 3 | Family-level base classes | Shared behavior per diagram family: flowchart routing rules, ERD entity rendering, UML notation, network device icons. Override DrawContent or DrawFlowchartContent for family-specific appearance. |
| 4 | Concrete leaf nodes | Fully rendered diagram nodes: ProcessNode, DecisionNode, ERDEntity, UMLClass, NetworkRouter, etc. |
Tip
When creating a custom component, inherit from the family-level base class rather than SkiaComponent directly. This gives you free Material Design theming, family-specific behavior, and proper palette integration through SkiaComponentRegistry.