Skip to content

Plugin Examples and Recipes

NyxionSoftware edited this page Aug 5, 2026 · 1 revision

Plugin Examples and Recipes

Add a themed card beneath Now Playing

Use the named workspace grid, inspect its rows, and add a card without relying on a hard-coded child index. A production plugin should preserve and restore any row-definition changes it makes.

Grid? workspace = await context.UiManager.FindElementAsync<Grid>(Info.PluginIdentifier, "ReceiverWorkspaceGrid", cancellationToken);

if (workspace is null)
    return;

Border card = new() { Padding = new Thickness(18), Margin = new Thickness(0, 12, 0, 0) };
card.SetResourceReference(FrameworkElement.StyleProperty, "CardBorderStyle");
card.Child = new TextBlock
{
    Text = "Content supplied by Example Plugin"
};

workspace.Children.Add(card);
context.UiManager.RegisterCleanup(Info.PluginIdentifier, () => workspace.Children.Remove(card));

The exact placement policy is plugin-specific. Prefer a clearly named host when one exists. If changing rows or columns, record the original state and restore it in cleanup.

Add a toolbar action

StackPanel? toolbar = await context.UiManager.FindElementAsync<StackPanel>(Info.PluginIdentifier, "ApplicationToolbarActions", cancellationToken);

if (toolbar is not null)
{
    Button button = new() { Content = "Plugin Action", Margin = new Thickness(0, 0, 10, 0) };
    button.SetResourceReference(FrameworkElement.StyleProperty, "SecondaryButtonStyle");
    button.Click += PluginActionClicked;
    toolbar.Children.Insert(0, button);

    context.UiManager.RegisterCleanup(Info.PluginIdentifier, () =>
    {
        button.Click -= PluginActionClicked;
        toolbar.Children.Remove(button);
    });
}

React to receiver selection

private async void SelectedReceiverChanged(object? sender, ReceiverSelectionChangedEventArgs e)
{
    try
    {
        await UpdatePluginCardAsync(e.SelectedReceiver, _lifetimeCancellation.Token);
    }
    catch (OperationCanceledException)
    {
    }
    catch (Exception ex)
    {
        _context!.Logger.LogError(ex, "Plugin card update failed.");
    }
}

Avoid async void except event handlers. Catch exceptions inside them.

Right-click a plugin grid row

ContextMenu menu = new();
menu.SetResourceReference(FrameworkElement.StyleProperty, "ModernContextMenuStyle");

MenuItem setChannel = new() { Header = "Set selected receiver to this channel" };
setChannel.SetResourceReference(FrameworkElement.StyleProperty, "ModernMenuItemStyle");
setChannel.Click += async (_, _) => await SetSelectedReceiverChannelAsync(row.Channel);
menu.Items.Add(setChannel);

Validate that a receiver is selected and online where the operation requires it. Show an error notification for user-action failures and log diagnostic detail.

Dispatch selection and navigation

await context.ApplicationDispatcher.DispatchAsync(
    new PluginApplicationCommand(
        "select-receiver",
        new Dictionary<string, string?> { ["receiverId"] = receiver.Id.ToString() }),
    cancellationToken);

Store per-receiver plugin settings

Use the application receiver Guid, not the numeric hardware ID:

Dictionary<Guid, FeedEndpoint> endpoints = await context.DataStore.ReadAsync<Dictionary<Guid, FeedEndpoint>>("receiver-endpoints", cancellationToken) ?? new();
endpoints[receiver.Id] = new FeedEndpoint("239.1.2.3", 5000);
await context.DataStore.WriteAsync("receiver-endpoints", endpoints, cancellationToken);

Remove orphaned entries when receivers are deleted or tolerate them during lookup.

Background refresh loop

private CancellationTokenSource? _lifetimeCancellation;
private Task? _refreshTask;

public Task EnableAsync(CancellationToken cancellationToken = default)
{
    _lifetimeCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    _refreshTask = RefreshLoopAsync(_lifetimeCancellation.Token);
    return Task.CompletedTask;
}

public async Task DisableAsync(CancellationToken cancellationToken = default)
{
    _lifetimeCancellation?.Cancel();

    if (_refreshTask is not null)
    {
        try { await _refreshTask; }
        catch (OperationCanceledException) { }
    }

    _lifetimeCancellation?.Dispose();
    _lifetimeCancellation = null;
    _refreshTask = null;
}

Never leave fire-and-forget loops running across disablement.

Clone this wiki locally