# Events, threading and lifetime ## The bus `IPluginEvents` is a type-based publish/subscribe bus owned by LiveWall. ```csharp IDisposable Subscribe(Action handler) where TEvent : class; void Publish(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` 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`.