Skip to content

Developer preview

Mark static methods with [UiPreview] and Developer Tools renders your view in states that are hard to reach live.

using MacroDeck.Ui.Dsl;
using MacroDeck.Ui.Previews;
internal static class WeatherViewPreviews
{
[UiPreview("Sunny", Profile = UiPreviewProfiles.Widget)]
public static UiElement Sunny() => WeatherView.Build(new MockWeatherService { City = "Vienna" });
[UiPreview("Offline", Profile = UiPreviewProfiles.Widget)]
public static UiElement Offline() => WeatherView.Build(new MockWeatherService { Offline = true });
[UiPreview("Long city name", Profile = UiPreviewProfiles.Widget)]
public static UiElement LongName()
=> WeatherView.Build(new MockWeatherService { City = "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch" });
}

Developer Tools lists the three scenarios under WeatherView, renders each through the same renderer the app uses, and lets you resize the surface freely while it re-lays-out. Good candidates: disconnected, empty, mid-load, failing, and a translation three times longer than the English.

[UiPreview("Offline")]
public static UiElement Offline() => WeatherView.Build(new MockWeatherService { Offline = true });

The method must be static, take no parameters, and return a UiElement, a UiView or a UiPreview. Nothing is injected: a scenario builds its own mocks, and nothing reaches the real service.

A returned UiView belongs to the preview, which disposes it when the preview ends. Build a new one on every call rather than returning a cached instance, or the next open gets a view that ignores every event.

[UiPreview("Long text", View = "WeatherDetailsView", Profile = UiPreviewProfiles.Config)]
Member Default Meaning
Scenario (constructor) - The scenario’s name. Required, not blank.
View The declaring type’s name without a trailing Previews (WeatherViewPreviews becomes WeatherView) The view the scenario groups under.
Profile UiPreviewProfiles.Config (config) The component namespace the tree is authored in: config or widget. It only sets up the canvas before the first tree arrives; the tree decides how it renders.
[UiPreview("Live feed", Profile = UiPreviewProfiles.Widget)]
public static UiPreview LiveFeed()
{
var feed = new MockWeatherFeed();
return UiPreview.Of(WeatherView.Build(feed), feed);
}

Opening, switching, refreshing and closing a preview each end the previous session. Hand anything that must be stopped - a timer, a subscription, a fake connection - to UiPreview.Of as an IDisposable or IAsyncDisposable, and Macro Deck disposes it when the preview ends. A scenario that only builds a tree from plain data returns the element and never mentions UiPreview.

Fact Consequence
Discovery reads metadata only. A scenario runs only when someone opens it; declaring one costs a running host nothing.
A preview renders on its own developer-preview surface. No provider you wrote for config, widget, dialog or folder is ever asked to serve one.
A malformed [UiPreview] method (not static, takes parameters, wrong return type) is skipped and reported in Developer Tools. Discovery never fails, so one bad preview cannot take down your real surfaces.
A scenario that throws when opened fails only that preview. Other previews and sessions carry on.
Listing and opening previews require an admin session. Previews are not reachable by deck clients.

Open a scenario once and leave it open while you change the code. Developer Tools keeps it on screen and brings it back after a reload.

You change What updates How
A plugin scenario or view, under dotnet watch or your IDE’s Hot Reload The open preview, in place The SDK rebuilds the scenario and sends its new tree. No restart, and the preview keeps its session.
Any plugin code, under dotnet watch or your IDE’s Hot Reload Every other open view of the plugin: deck widgets, widget, action and integration configuration, folder views, screensavers Macro Deck opens each view again and your provider builds it with the new code. A configuration editor keeps the unsaved changes. See Real views.
A plugin change Hot Reload cannot apply, or a plain rebuild and restart The open preview, once the plugin is back The last tree stays on screen, dimmed, with a notice that the plugin is not connected. The preview reopens by itself when the plugin reconnects.
A scenario that no longer exists (renamed or removed) A notice instead of the preview Pick the scenario again from the list, which refreshes by itself.
A built-in Macro Deck view The open preview, after the host restarts The preview reopens once Macro Deck is back.
Nothing, but the same scenario is opened in a second window The second window The first window says the preview was closed. Refresh takes it back. Different scenarios can stay open side by side.

The selected scenario and the canvas size are part of the Developer Tools address, so reloading the window brings you back to the same scenario at the same size.

A component view on the canvas is laid out the way a deck tile is: each preset cell is one deck cell, so a UiResponsive switches layouts at the same sizes it does on the deck. A folder view or a dialog in the app is laid out in CSS pixels instead, 120 of them to a cell, so check its thresholds there too.

macrodeck-plugin run --project <path> --watch sets this loop up against the running Macro Deck; see Watching for changes. dotnet watch run with the debugging launch profile does the same from your IDE.

A reload rebuilds the scenario from its code, so what the scenario creates starts over: its mock data, and anything you typed or selected inside the preview. Put the state you want to look at into the scenario itself.

A plugin whose first-ever scenario is added by Hot Reload does not declare a UI yet, so that scenario appears after the next restart.

macrodeck-plugin preview render --project <path> --size 200x200 --output previews draws every widget scenario to a PNG without a running Macro Deck, which is how store images are regenerated after a redesign. See preview. Configuration views are not drawn.

A preview is not the only view that follows Hot Reload. Every view your plugin serves in the running app is opened again after each update, whichever code changed, so you can work on a widget or a configuration view with real data instead of a scenario:

  • In the desktop app, and for widgets on a deck, the view keeps showing its last tree until the new one arrives. A folder view or screensaver in the web client shows its loading state in between.
  • A widget, action or integration configuration editor keeps the unsaved changes: Macro Deck opens the new session with the draft it holds, or, for an integration’s setup, sends the new session a change event for each field the user edited.
  • State that lives in the old session is gone, such as the open tab. State your provider keeps outside the session survives.
  • An open dialog is left alone, because ending it would cancel the action waiting for its answer. The next dialog uses the new code.
  • A view whose new code throws while it is created shows as unavailable after a few attempts. Open it again once the code is fixed.
  • A view that was still being created while the update landed may keep the old code. Open it again.
  • If dotnet watch reports Failed to load type warnings for an update, restart the plugin (Ctrl+R in dotnet watch, or restart it from your IDE). Hot Reload still reports success, but the process can no longer build views reliably, so views stop updating or stay empty.

describe lists your scenarios in previews. MacroDeck.Plugin.Hosting scans the assemblies of your registered integrations plus the entry assembly, lazily. A developer-preview surface names one scenario by id in its attributes; the SDK builds that scenario ahead of every production path and never consults IUiProvider. See Serving a view.

When .NET Hot Reload updates the plugin, MacroDeck.Plugin.Hosting builds every open preview’s scenario again, sends the new tree as an unrequested ui/snapshot, and sends state.update for ui so the host re-reads describe and the list picks up new or removed scenarios. A scenario that throws on rebuild faults only its own preview. Every other open session except a dialog gets a ui/reload (see Host callbacks), and the host ends it so that clients open it again. Nothing changes for a plugin that is not being hot reloaded.