LANCommander/LANCommander.Documentation/Plugins/Extension Points.md
2026-07-28 19:44:59 -05:00

7.4 KiB

sidebar_label sidebar_position
Extension Points 3

Extension Points

This page is a tour of everything a plugin can extend, with a short example for each. For exact signatures and every available type, see the API Reference, which is generated directly from the source.

The registration pattern

Almost every extension point follows the same shape: implement an interface, then register your implementation in IPlugin.ConfigureServices. The host resolves all registered implementations of a given interface and wires them in automatically.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ISettingsPageExtension, MySettingsExtension>();
    services.AddSingleton<IContextMenuExtension, MyContextMenuExtension>();
    services.AddSingleton<IPluginPowerShellExtension, MyPowerShellExtension>();
}

Where a UI extension point exposes an Order property, implementations are sorted ascending, lower values appear first, and rendered alongside the built-in items.

Launcher UI extensions

The launcher exposes five UI extension points, all in the LANCommander.Launcher.Plugins.Extensions namespace. Each one builds an Avalonia Control.

Interface What it adds
INavigationPageExtension A top-level navigable page reachable from the shell.
ISettingsPageExtension A section on the settings page.
IGameDetailTabExtension A tab on a game's detail view.
IContextMenuExtension Items on a game's context menu.
IFooterExtension A widget in the shell footer.

:::tip Build controls in code, not XAML Because plugins load in their own AssemblyLoadContext, Avalonia's compiled-XAML asset resolution (avares://) does not reliably resolve across the boundary. The recommended authoring path for plugin views is to build controls in code, as the examples below do. This is exactly what the reference plugin does. :::

Example: a settings section

using Avalonia.Controls;
using Avalonia.Layout;
using LANCommander.Launcher.Plugins.Extensions;

public sealed class MySettingsExtension : ISettingsPageExtension
{
    public string Title => "My Plugin";
    public int Order => 0;

    public Control BuildContent()
    {
        var panel = new StackPanel { Spacing = 8 };

        panel.Children.Add(new TextBlock
        {
            Text = "This settings section was added by my plugin.",
            TextWrapping = Avalonia.Media.TextWrapping.Wrap,
        });

        panel.Children.Add(new CheckBox
        {
            Content = "Enable my feature",
            HorizontalAlignment = HorizontalAlignment.Left,
        });

        return panel;
    }
}

Example: game context menu items

IContextMenuExtension receives the id of the game the menu was opened for and returns any number of controls (typically MenuItems) to append:

public sealed class MyContextMenuExtension : IContextMenuExtension
{
    public int Order => 0;

    public IEnumerable<Control> BuildMenuItems(Guid gameId)
    {
        var item = new MenuItem { Header = "Do something with this game" };
        item.Click += (_, _) => { /* ... */ };
        yield return item;
    }
}

Example: a navigation page

A navigation page pairs a view model (deriving from PluginViewModelBase) with a control that renders it. The view model type doubles as the registry key used by the shell's content control.

public sealed class MyPageExtension : INavigationPageExtension
{
    public string Label => "My Page";
    public int Order => 100;
    public Type ViewModelType => typeof(MyPageViewModel);

    public PluginViewModelBase CreateViewModel() => new MyPageViewModel();

    public Control BuildView() => new TextBlock { Text = "Hello from my page" };
}

Server metadata providers

On the server, a plugin can contribute additional metadata providers by registering an IMetadataProvider (from LANCommander.Server.Services). Registered providers are picked up by the server's provider enumeration and used alongside the built-in ones.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IMetadataProvider, MyMetadataProvider>();
}

PowerShell extensions

LANCommander runs installs and other tasks through an embedded PowerShell runtime. A plugin can add its own cmdlets and script modules by implementing IPluginPowerShellExtension. Registered extensions are picked up whenever a script runspace is created, in both hosts.

using System.Management.Automation;
using LANCommander.SDK.Plugins;

// The cmdlet itself.
[Cmdlet(VerbsCommon.Get, "MyGreeting")]
public sealed class GetMyGreetingCmdlet : PSCmdlet
{
    [Parameter(Position = 0)]
    public string Name { get; set; } = "World";

    protected override void ProcessRecord() => WriteObject($"Hello, {Name}!");
}

// The extension that exposes it to the runspace.
public sealed class MyPowerShellExtension : IPluginPowerShellExtension
{
    public IEnumerable<Type> GetCmdletTypes() => new[] { typeof(GetMyGreetingCmdlet) };

    public IEnumerable<string> GetModulePaths() => Array.Empty<string>();
}

Once registered, Get-MyGreeting is callable from any LANCommander script. To ship script modules (.psm1/.psd1) instead of (or in addition to) cmdlets, return their absolute paths from GetModulePaths(). The plugin directory is available to you via PluginContext.PluginDirectory.

Lifecycle events

The IPluginEventBus is an in-process, strongly typed event aggregator registered as a singleton in both hosts. Resolve it in InitializeAsync and subscribe to the events you care about. Subscribe returns an IDisposable; keep it and dispose it to unsubscribe.

public Task InitializeAsync(PluginContext context, CancellationToken cancellationToken)
{
    var events = context.Services.GetRequiredService<IPluginEventBus>();

    events.Subscribe<GameInstalledEvent>((evt, ct) =>
    {
        context.Logger.LogInformation("Installed {GameId} to {Dir}", evt.GameId, evt.InstallDirectory);
        return Task.CompletedTask;
    });

    return Task.CompletedTask;
}

Handlers are awaited and isolated: a throwing handler cannot break the publisher or other subscribers.

The events published today live in the LANCommander.SDK.Plugins.Events namespace:

Event Raised when
GameInstallingEvent A game install is about to begin.
GameInstalledEvent A game has finished installing.
GameInstallFailedEvent A game install fails.
GameUninstallingEvent A game is about to be uninstalled.
GameUninstalledEvent A game has finished uninstalling.
GameBeforeLaunchEvent A game's executable is about to launch.
GameAfterExitEvent A launched game process has exited.
InstallQueueChangedEvent The install/download queue changes.
UserLoggedInEvent A user successfully logs in.
UserLoggedOutEvent A user logs out.

See the API Reference for the exact payload of each event.