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); + } +}