3.7 KiB
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;
Subscribereturns a token: dispose it inShutdown(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 receivesWallpaperChangedEvent), 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.Runif 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.