Interactions and Effects
Two mechanisms make a component react to something besides its own bound values: an interaction, entirely client-side, and a client effect, which a command (or an interaction) hands the client to run.
Interactions
An interaction watches one component's event and writes a property on another — or on itself — with no
round trip. Every visual component exposes them through VisualComponentBase<TComponent>.
The id always names the source, never the target. The component the call is made on is the one whose property changes; the id argument is the component whose event is watched:
new StackPanelComponent()
.SetVisibility(UIVisibility.Hidden)
.InteractOnHoverStart(CardId, IVisualComponent.VisibilityProperty, UIVisibility.Visible)
.InteractOnHoverEnd(CardId, IVisualComponent.VisibilityProperty, UIVisibility.Hidden)
Written the other way round it compiles and does nothing — the source would be a component that is hidden
and can never raise the event. The single-argument overloads default the source to the component itself,
which is why InteractBeforeClick(LoadingProperty, true) reads naturally.
Interact — comparing a value
public TComponent Interact(UIProperty property, UIComparisonOperator @operator = UIComparisonOperator.Equal,
object? value = null, object? whenTrue = null, object? whenFalse = null)
public TComponent Interact(UIProperty source, UIProperty target, UIComparisonOperator @operator = UIComparisonOperator.Equal,
object? value = null, object? whenTrue = null, object? whenFalse = null)
public TComponent Interact(string sourceComponentId, UIProperty source, UIProperty target,
UIComparisonOperator @operator = UIComparisonOperator.Equal, object? value = null, object? whenTrue = null, object? whenFalse = null)
Watches source continuously and writes target to whenTrue or whenFalse depending on whether source @operator value holds. UIComparisonOperator has eleven members: Required, Equal, NotEqual,
Greater, GreaterOrEqual, Less, LessOrEqual, Like, In, Regex, LikeIgnoreCase.
InteractOn* — reacting to an event
public TComponent InteractBeforeClick(UIProperty target, object? value)
public TComponent InteractAfterClick(UIProperty target, object? value)
public TComponent InteractOnHoverStart(UIProperty target, object? value)
public TComponent InteractOnHoverEnd(UIProperty target, object? value)
public TComponent InteractOn(string sourceEvent, UIProperty target, object? whenTriggered)
Any other event name goes through the last, general-purpose overload — InteractOn("focus", ...).
Each has a sourceComponentId-taking overload for watching another component. InteractBeforeClick fires
after validation and just before the command dispatches; InteractAfterClick fires after the command's
result — changes and effects — has been applied. Together they are the "busy while this round trip runs"
idiom, and ButtonComponent.OnClickShowingLoading(command) is exactly this pair wired to Loading:
public T OnClickShowingLoading(string command)
=> OnClick(command)
.InteractBeforeClick(IVisualComponent.LoadingProperty, true)
.InteractAfterClick(IVisualComponent.LoadingProperty, false);
This is one of two independent ways to say a control is busy — entirely client-side, nothing bound, nothing
for a controller to clear. The other is binding Loading to a controller flag the command itself sets; that
one starts and stops on the server's own timing, for when being busy outlives the single command. Using
InteractBeforeClick without a matching InteractAfterClick (or a binding that clears it) leaves a spinner
that never stops.
An interaction may run a client effect instead of writing a property:
public TComponent InteractOn(string sourceEvent, ClientEffect effect)
public TComponent InteractOn(string sourceComponentId, string sourceEvent, ClientEffect effect)
public TComponent Interact(UIProperty source, ClientEffect effect, UIComparisonOperator @operator = UIComparisonOperator.Required, object? value = null)
public TComponent Interact(string sourceComponentId, UIProperty source, ClientEffect effect, UIComparisonOperator @operator = UIComparisonOperator.Required, object? value = null)
Only an effect whose CanRunInInteraction is true may be used this way — true only for an effect that
completes entirely on the client, with no round trip behind it (see the table below).
Client effects
A ClientEffect is something a command (or a qualifying interaction) hands the client to run after the
result — changes first, then effects, so an effect that focuses or scrolls sees what the command just
changed. ClientEffectKinds lists the built-in kinds; a package can add its own by returning a new Kind
string.
| Effect | Constructor | Does | In an interaction |
|---|---|---|---|
NavigateEffect |
NavigateEffect(UINavigationRequest request) |
Navigates the client to another route. | No |
FocusEffect |
FocusEffect(string targetComponentId, params object?[]? dynamicParameters) |
Focuses a component. | Yes |
ScrollToEffect |
ScrollToEffect(string targetComponentId, ScrollToBehavior behavior, ScrollToBlock block, params object?[]? dynamicParameters) |
Scrolls a component into view. | Yes |
ScrollEffect |
ScrollEffect(string targetComponentId, ScrollPosition position, params object?[]? dynamicParameters) |
Scrolls a container (start/end/offset/page), as opposed to bringing a component into view. | Yes |
ShowEffect |
ShowEffect(string targetComponentId, params object?[]? dynamicParameters) |
Shows a component. | No |
HideEffect |
HideEffect(string targetComponentId, params object?[]? dynamicParameters) |
Hides a component; it keeps the room it holds. | No |
CollapseEffect |
CollapseEffect(string targetComponentId, params object?[]? dynamicParameters) |
Takes a component out of the layout; the room it held closes up. | No |
OpenDialogEffect |
OpenDialogEffect(string dialogKey) |
Opens a dialog declared by the view. | No |
CloseDialogEffect |
CloseDialogEffect(string dialogKey) |
Closes a dialog. | No |
ShowNotificationEffect |
ShowNotificationEffect(string message, UIColorStyle severity = UIColorStyle.Info) |
Shows a toast. | No |
DownloadFileEffect |
DownloadFileEffect(string requestPath, string fileName) |
Fetches a file staged through Context.Downloads; the path is single-use. |
No |
SetThemeEffect |
SetThemeEffect(UIThemeMode? mode = null) |
Puts the client into a theme and persists it as the session's — the one effect with a server side. | Yes |
RenameTabEffect |
RenameTabEffect(string targetComponentId, string key, params object?[]? dynamicParameters) |
Opens the inline rename field on one tab, the way a double-click does. | Yes |
RenameNodeEffect |
RenameNodeEffect(string targetComponentId, string key, params object?[]? dynamicParameters) |
Opens the inline rename field on one tree node, the way F2 does. | Yes |
CopyToClipboardEffect |
CopyToClipboardEffect.Literal(string text) / .ValueOf(string targetComponentId, ...) |
Copies text, or a component's current value, to the clipboard. | Yes |
Returning effects from a command
UICommandResult.Ok(effects) and UICommandResult.Fail(error, effects) both take an optional
IReadOnlyList<ClientEffect>?:
[UICommand]
public UICommandResult AskBeforeDelete()
{
if (!ConfirmGroup.HasBuilds)
return UICommandResult.Ok([new ShowNotificationEffect("Nothing left to delete.", UIColorStyle.Info)]);
ConfirmGroup.Ask();
return UICommandResult.Ok([new OpenDialogEffect(ConfirmKey)]);
}
[UICommand]
public UICommandResult DeleteBuild()
{
ConfirmGroup.Delete();
return UICommandResult.Ok([new CloseDialogEffect(ConfirmKey), new ShowNotificationEffect("The build is gone.", UIColorStyle.Success)]);
}
ConfirmKey here is the Key of a UIDialog the view declared in CreateDialogs() (see
views, dialogs).
How a failed command is reported
The client never inspects success/error directly — both delivery channels only ever apply changes and
effects. A failed UICommandResult with no effects of its own gets a ShowNotificationEffect attached
automatically; returning your own effects from a failure is how a command takes over the reporting.
UIErrorHandlingOptions (configured with application.ConfigureErrorHandling(configure)) controls it:
| Option | Meaning |
|---|---|
NotFoundRoute |
Route used when a requested route was not registered. |
ErrorRoute |
Route used when an unhandled exception occurs resolving a view — not a failed command, which is always reported to the user instead of navigating anywhere. |
NotifyOnCommandFailure |
Whether a failed command with no effects of its own gets the automatic notification (default true). |
IncludeExceptionDetail |
Whether the real exception message reaches the browser — leave off in production; an exception routinely carries a connection string or a file path. |
CommandRefusedMessage |
Shown when a command is refused by an authorization check. |
CommandFailedMessage |
Shown when a command fails for any other reason. |
The message a viewer sees has three levels: what UICommandResult.Fail(error) wrote is shown as it stands;
an UnauthorizedAccessException gets CommandRefusedMessage; anything else gets CommandFailedMessage
unless IncludeExceptionDetail is on. All three go through the translator.
Navigating
NavigateEffect takes a UINavigationRequest:
public sealed class UINavigationRequest
{
public required string Route { get; init; }
public IReadOnlyDictionary<string, object?>? Parameters { get; init; }
}
return UICommandResult.Ok([new NavigateEffect(new UINavigationRequest { Route = "/dashboard" })]);
Parameters become the route's navigation parameters — the same shape a query string produces on a direct
request, readable from OnInitializeAsync (see controllers).
Where to go next
- Controllers for how a command is declared and what its return type may be.
- Views for the dialogs
OpenDialogEffect/CloseDialogEffecttarget. - Binding for the properties an interaction writes.