Repository navigation
Plugin Examples and Recipes
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.
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);
});
}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.
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.
await context.ApplicationDispatcher.DispatchAsync(
new PluginApplicationCommand(
"select-receiver",
new Dictionary<string, string?> { ["receiverId"] = receiver.Id.ToString() }),
cancellationToken);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.
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.