ArionView class Core

A view is a data-shape + behavior variant (grid, pivot, cards, later gantt/timeline). It owns its projection stack (what ArionView.Source the engine scrolls) and its drawing model (ArionView.Template), and it interprets the semantic gestures the host forwards. Fully backend-neutral and unit-testable without a window — it talks to the host only through IViewHost.

public abstract class ArionView
Namespace
ArionUI.Presentation
Package
ArionUI.Presentation · dotnet add package ArionUI.Presentation --prerelease

Constructors

ArionView

protected ArionView();

Properties

Host

protected IViewHost Host { get; }

The host driving this view. Valid after ArionView.Attach.

HostOrNull

protected internal IViewHost? HostOrNull { get; }

The host, or null when detached — for callbacks that may arrive on a PRODUCER thread, where ArionView.Host's throw-if-detached would turn an ordinary race into an exception on somebody else's thread.

IsAttached

public bool IsAttached { get; }

PaneHost

public IGridPaneHost? PaneHost { get; set; }

Set by the rendering host: shows the grid-owned standard panes for requests the app did not override (see IGridPaneHost).

RecommendedFrozenColumns

public virtual int? RecommendedFrozenColumns { get; }

How many leading columns this view WANTS frozen — the host applies it on attach.

A chart or a timeline only works with a fixed axis: the Y axis stays left, the plot scrolls. The view knows this, so the application does not have to.

Null means »no opinion«: the host then leaves its setting unchanged. That is the normal case for an ordinary table, where the user decides what stays put.

Source

public abstract IItemsSource Source { get; }

The item source the engine virtualizes (top of this view's projection stack — e.g. a grouping over a sort/filter over the data).

Template

public abstract ITemplate Template { get; }

The drawing model the renderer uses for this view.

Methods

Attach

public void Attach(IViewHost host);

Called by the host when the view becomes its MainView.

A view belongs to exactly ONE host (see ArionView.EnsureCanAttachTo). Attaching again to the same host does nothing — both controls attach in the constructor and once more on the first visual-tree event.

Transactional: if ArionView.OnAttached throws, host AND PaneHost are rolled back.

Detach

public void Detach();

Called by the host when this view stops being its MainView, and when the control unloads. The counterpart ArionView.Attach never had.

Without it a view swap left the OLD view attached: it kept its host reference, and it kept reacting to its data source — so mutating the old source repainted a grid nobody is looking at, and, worse, a long-lived source held the old view, which held the control. Swapping views in a shell that keeps one collection around therefore accumulated whole grids, complete with their GPU surfaces.

Detaching does NOT unsubscribe from the data source: a view may be attached again later, and its filter/sort/footer state is expected to be current when it comes back. It severs the view → host half, which is the half that holds the control. Use IDisposable where a view is truly finished.

EnsureCanAttachTo

public void EnsureCanAttachTo(IViewHost host);

May this view attach to host? Throws if not — and changes NOTHING.

A host calls this before it reconfigures anything on itself. Otherwise, by the time it throws, it has already rewired template, presenter and input and then claims to show a view that drives another grid.

Mutate

protected void Mutate(Action change);

Run a structural mutation so the render thread cannot read through the middle of it.

The GPU frame holds Engine.RenderGate for its whole pass, because it reads the source and the metrics LIVE. A lock on the reader's side alone is not synchronisation, though — and the view-side writers did not take it. Rebuilding a pivot mutates its dictionaries, lists and sets IN PLACE, so a frame in flight could walk a dictionary that was being rehashed: a hang or a corrupt read, not a wrong pixel.

Unattached the mutation just runs — there is no render thread to protect it from, and a view is expected to be configurable before it is ever shown.

OnAttached

protected virtual void OnAttached();

OnCellDoubleClick

public virtual void OnCellDoubleClick(int itemIndex, int elementId);

Double-click on a read-only, non-action cell (forwarded from InteractionController.CellDoubleClicked) — the hook for view-specific gestures like the pivot's drill-down.

OnColumnFiltersChanged

public virtual void OnColumnFiltersChanged(IReadOnlyDictionary<int, string> filters);

The full current per-column filter-row text state (element id → text), pushed by the host after its debounce. The view applies the whole set at once — matching "apply all active filters", robust against several columns changing within one debounce window.

OnDetached

protected virtual void OnDetached();

OnFrameTick

protected internal virtual bool OnFrameTick(TimeSpan now);

Apply work that must happen ONCE PER FRAME (accumulated live changes), and report whether more is already waiting — that return value is what keeps the host's pump running under a feed and stops it the moment the feed goes quiet.

Default: nothing to do, never pump. A view that does not have live data pays nothing, and neither host ever ticks for it.

Parameters

now

The frame's timestamp, injected — no view reads a clock itself, so anything paced (settle intervals, decays) stays deterministically testable.

OnHeaderClick

public virtual void OnHeaderClick(double contentX, double y, ArionModifiers mods, HeaderButtonKind? button = default);

A click on the column header's leaf caption row.

Parameters

button

The caption button the click hit (funnel, summary), or null for the caption itself. The grid decides it with the same button layout it draws and hovers with, including the pin marker's slot, so a view never re-derives it.