Files
2026-11-04 14:29:22 -06:00

3.7 KiB

Events, threading and lifetime

The bus

IPluginEvents is a type-based publish/subscribe bus owned by LiveWall.

IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
void Publish<TEvent>(TEvent payload) where TEvent : class;
  • Subscribe returns a token: dispose it in Shutdown (or you leak your plugin into the load context and it will never unload).
  • Handlers are matched on the exact runtime type (Subscribe<WallpaperChangedEvent> only receives WallpaperChangedEvent), not on interfaces or base classes.
  • Handlers run on the WPF UI thread, regardless of which thread published. Never block in one and never touch a plugin whose state you cannot see.
  • Host publications are delivered synchronously as part of the host's own operation; a slow handler therefore slows the wallpaper change. Do the work in Task.Run if it is not instant.
  • Exceptions thrown by a handler are logged and the handler is unsubscribed from that publication onward — a broken handler cannot repeatedly break the app, and cannot break the other handlers.

Catalogue

Event When Useful fields
AppStartedEvent After settings load, wallpaper attach and plugin load HostVersion, PluginApiVersion
AppStoppingEvent On exit, before plugins are unloaded Reason
WallpaperChangedEvent A new source finished loading (or failed to) Kind, Source, RendererName, FromPlugin
PlaybackChangedEvent Play/pause/suspend, fullscreen pause, battery pause IsPlaying, IsPaused, Reason
InteractionChangedEvent Interactive mode toggled Interactive
SettingsChangedEvent Any settings write; only for non-plugin-related keys Property, Snapshot
DisplayChangedEvent Monitor topology change or WorkerW re-attach Devices, Reattached
SessionChangedEvent Lock screen, battery, RDP Locked, OnBattery, RemoteSession
HostErrorEvent Something in the host failed and the user was told Message, Source
PluginMessageEvent Plugin-to-plugin convention FromPluginId, Topic, Payload

Events are not queued: you miss whatever happened before you subscribed. Pull the current state from context.Host.Current / context.Settings during Initialize if you need a starting point.

Threading cheat sheet

Place Thread
Initialize, Shutdown UI thread (Shutdown may be called while the app tears down)
Overlay CreateView / Update / OnVisibilityChanged UI thread
Settings page CreateView UI thread
Command Run, hotkey action UI thread
Filter Apply UI thread, inside the wallpaper change
Renderer Create / Load / Play / Pause / Unload / ApplyPlayback UI thread
Event handlers UI thread

Anything you start yourself is your business: use Task.Run, dispatch UI updates with Application.Current.Dispatcher, and pass context.ShutdownToken to cancellable work.

Lifetime in order

Rescan / enable
  ├─ manifest parsed, assembly hashed, permissions checked
  ├─ assembly loaded into a collectible AssemblyLoadContext
  ├─ PluginInfo filled in, IPluginContext created (storage opened, sub-bus created)
  └─ Initialize(context)                     ← you register everything here
Disable / rescan / exit
  ├─ ShutdownToken cancelled
  ├─ Shutdown()
  ├─ your overlays, pages, commands, filters, renderers and hotkeys are dropped
  ├─ renderers disposed, overlays unhooked
  └─ AssemblyLoadContext.Unload()

There is no "re-initialise": a plugin that needs to restart shows as Disabled then Active after a Rescan, with a brand-new context object. Any state you want to survive goes in IPluginStorage.