diff --git a/MinimalWorker.sln b/MinimalWorker.sln
index b2aea66..8020c84 100644
--- a/MinimalWorker.sln
+++ b/MinimalWorker.sln
@@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0932226D
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalWorker.Desktop.AvaloniaUI.Sample", "samples\MinimalWorker.Desktop.AvaloniaUI.Sample\MinimalWorker.Desktop.AvaloniaUI.Sample.csproj", "{69762A6B-98A1-46EA-9059-DB13F0610F0E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalWorker.Desktop.Wpf.Sample", "samples\MinimalWorker.Desktop.Wpf.Sample\MinimalWorker.Desktop.Wpf.Sample.csproj", "{B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -142,6 +144,18 @@ Global
{69762A6B-98A1-46EA-9059-DB13F0610F0E}.Release|x64.Build.0 = Release|Any CPU
{69762A6B-98A1-46EA-9059-DB13F0610F0E}.Release|x86.ActiveCfg = Release|Any CPU
{69762A6B-98A1-46EA-9059-DB13F0610F0E}.Release|x86.Build.0 = Release|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Debug|x64.Build.0 = Debug|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Debug|x86.Build.0 = Debug|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Release|x64.ActiveCfg = Release|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Release|x64.Build.0 = Release|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Release|x86.ActiveCfg = Release|Any CPU
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -156,5 +170,6 @@ Global
{A75FAADC-FA65-499E-A3DA-65A75AB2786E} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
{19723BBE-49D4-49A3-ABDD-122A70F67EAF} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
{69762A6B-98A1-46EA-9059-DB13F0610F0E} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
+ {B4E4367B-CC76-4A96-B260-4BCF47EAD3E0} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
EndGlobalSection
EndGlobal
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/App.xaml b/samples/MinimalWorker.Desktop.Wpf.Sample/App.xaml
new file mode 100644
index 0000000..03d2d69
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/App.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/App.xaml.cs b/samples/MinimalWorker.Desktop.Wpf.Sample/App.xaml.cs
new file mode 100644
index 0000000..4a4e4fa
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/App.xaml.cs
@@ -0,0 +1,61 @@
+using System.Windows;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using MinimalWorker;
+using MinimalWorker.Desktop.Wpf.Sample.ViewModels;
+
+namespace MinimalWorker.Desktop.Wpf.Sample;
+
+public partial class App : Application
+{
+ private IHost? _host;
+
+ protected override async void OnStartup(StartupEventArgs e)
+ {
+ base.OnStartup(e);
+
+ var builder = Host.CreateApplicationBuilder(e.Args);
+ builder.Services.AddSingleton();
+
+ _host = builder.Build();
+
+ // 1) Periodic worker — every 1 second
+ _host.RunPeriodicBackgroundWorker(TimeSpan.FromSeconds(1), (WorkerStateService state) =>
+ {
+ state.RecordExecution("Periodic", $"Tick at {DateTime.Now:HH:mm:ss}");
+ });
+
+ // 2) Slow worker — every 5 seconds (simulates heavier work)
+ _host.RunPeriodicBackgroundWorker(TimeSpan.FromSeconds(5), async (WorkerStateService state) =>
+ {
+ await Task.Delay(200); // simulate async work
+ state.RecordExecution("SlowTask", $"Completed at {DateTime.Now:HH:mm:ss}");
+ });
+
+ // 3) Cron worker — every minute ("* * * * *")
+ _host.RunCronBackgroundWorker("* * * * *", (WorkerStateService state) =>
+ {
+ state.RecordExecution("Cron", $"Fired at {DateTime.Now:HH:mm:ss}");
+ });
+
+ await _host.StartAsync();
+
+ var workerState = _host.Services.GetRequiredService();
+ var mainWindow = new MainWindow
+ {
+ DataContext = new MainWindowViewModel(workerState)
+ };
+ MainWindow = mainWindow;
+ mainWindow.Show();
+ }
+
+ protected override async void OnExit(ExitEventArgs e)
+ {
+ if (_host is not null)
+ {
+ await _host.StopAsync();
+ _host.Dispose();
+ }
+ base.OnExit(e);
+ }
+}
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/MainWindow.xaml b/samples/MinimalWorker.Desktop.Wpf.Sample/MainWindow.xaml
new file mode 100644
index 0000000..f03e529
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/MainWindow.xaml
@@ -0,0 +1,158 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/MainWindow.xaml.cs b/samples/MinimalWorker.Desktop.Wpf.Sample/MainWindow.xaml.cs
new file mode 100644
index 0000000..7dac1b8
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/MainWindow.xaml.cs
@@ -0,0 +1,11 @@
+using System.Windows;
+
+namespace MinimalWorker.Desktop.Wpf.Sample;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/MinimalWorker.Desktop.Wpf.Sample.csproj b/samples/MinimalWorker.Desktop.Wpf.Sample/MinimalWorker.Desktop.Wpf.Sample.csproj
new file mode 100644
index 0000000..681b239
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/MinimalWorker.Desktop.Wpf.Sample.csproj
@@ -0,0 +1,23 @@
+
+
+
+ WinExe
+ net9.0-windows
+ true
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/ViewModels/MainWindowViewModel.cs b/samples/MinimalWorker.Desktop.Wpf.Sample/ViewModels/MainWindowViewModel.cs
new file mode 100644
index 0000000..18f99ec
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/ViewModels/MainWindowViewModel.cs
@@ -0,0 +1,65 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows.Threading;
+
+namespace MinimalWorker.Desktop.Wpf.Sample.ViewModels;
+
+public class MainWindowViewModel : INotifyPropertyChanged
+{
+ private readonly WorkerStateService _state;
+ private readonly Dispatcher _dispatcher;
+ private string _uptime = "00:00:00";
+ private string _latestActivity = "Starting up...";
+
+ public WorkerCardViewModel Periodic { get; } = new("Periodic (1s)");
+ public WorkerCardViewModel SlowTask { get; } = new("Slow Task (5s)");
+ public WorkerCardViewModel Cron { get; } = new("Cron (every min)");
+
+ public string Uptime
+ {
+ get => _uptime;
+ set => SetField(ref _uptime, value);
+ }
+
+ public string LatestActivity
+ {
+ get => _latestActivity;
+ set => SetField(ref _latestActivity, value);
+ }
+
+ public MainWindowViewModel(WorkerStateService state)
+ {
+ _state = state;
+ _dispatcher = Dispatcher.CurrentDispatcher;
+
+ _state.StateChanged += workerName =>
+ {
+ _dispatcher.BeginInvoke(() =>
+ {
+ var info = _state.GetOrAdd(workerName);
+ var card = workerName switch
+ {
+ "Periodic" => Periodic,
+ "SlowTask" => SlowTask,
+ "Cron" => Cron,
+ _ => null
+ };
+ card?.UpdateFrom(info);
+ LatestActivity = $"[{workerName}] {info.StatusMessage}";
+ });
+ };
+
+ var uptimeTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
+ uptimeTimer.Tick += (_, _) => Uptime = _state.Uptime.ToString(@"hh\:mm\:ss");
+ uptimeTimer.Start();
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return;
+ field = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/ViewModels/WorkerCardViewModel.cs b/samples/MinimalWorker.Desktop.Wpf.Sample/ViewModels/WorkerCardViewModel.cs
new file mode 100644
index 0000000..ed86d8d
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/ViewModels/WorkerCardViewModel.cs
@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace MinimalWorker.Desktop.Wpf.Sample.ViewModels;
+
+public class WorkerCardViewModel(string label) : INotifyPropertyChanged
+{
+ private int _executionCount;
+ private string _lastRunTime = "--:--:--";
+ private string _statusMessage = "Waiting...";
+
+ public string Label { get; } = label;
+
+ public int ExecutionCount
+ {
+ get => _executionCount;
+ set => SetField(ref _executionCount, value);
+ }
+
+ public string LastRunTime
+ {
+ get => _lastRunTime;
+ set => SetField(ref _lastRunTime, value);
+ }
+
+ public string StatusMessage
+ {
+ get => _statusMessage;
+ set => SetField(ref _statusMessage, value);
+ }
+
+ public void UpdateFrom(WorkerInfo info)
+ {
+ ExecutionCount = info.ExecutionCount;
+ LastRunTime = info.LastRunTime?.ToString("HH:mm:ss") ?? "--:--:--";
+ StatusMessage = info.StatusMessage;
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ private void SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, value)) return;
+ field = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/WorkerInfo.cs b/samples/MinimalWorker.Desktop.Wpf.Sample/WorkerInfo.cs
new file mode 100644
index 0000000..e5f11c7
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/WorkerInfo.cs
@@ -0,0 +1,18 @@
+namespace MinimalWorker.Desktop.Wpf.Sample;
+
+public class WorkerInfo(string name)
+{
+ private int _executionCount;
+
+ public string Name { get; } = name;
+ public int ExecutionCount => _executionCount;
+ public DateTime? LastRunTime { get; private set; }
+ public string StatusMessage { get; private set; } = "Waiting...";
+
+ internal void Record(string message)
+ {
+ Interlocked.Increment(ref _executionCount);
+ LastRunTime = DateTime.Now;
+ StatusMessage = message;
+ }
+}
diff --git a/samples/MinimalWorker.Desktop.Wpf.Sample/WorkerStateService.cs b/samples/MinimalWorker.Desktop.Wpf.Sample/WorkerStateService.cs
new file mode 100644
index 0000000..aed8b0f
--- /dev/null
+++ b/samples/MinimalWorker.Desktop.Wpf.Sample/WorkerStateService.cs
@@ -0,0 +1,27 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+
+namespace MinimalWorker.Desktop.Wpf.Sample;
+
+public class WorkerStateService
+{
+ private readonly ConcurrentDictionary _workers = new();
+ private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
+
+ public TimeSpan Uptime => _stopwatch.Elapsed;
+
+ ///
+ /// Fired on any worker update. The string argument is the worker name.
+ ///
+ public event Action? StateChanged;
+
+ public WorkerInfo GetOrAdd(string workerName) =>
+ _workers.GetOrAdd(workerName, name => new WorkerInfo(name));
+
+ public void RecordExecution(string workerName, string message)
+ {
+ var info = GetOrAdd(workerName);
+ info.Record(message);
+ StateChanged?.Invoke(workerName);
+ }
+}