From 489516d70b7f092a7cad89b41933d27cbaef7ec8 Mon Sep 17 00:00:00 2001 From: Erik White <26148654+Erik-White@users.noreply.github.com> Date: Sun, 4 Sep 2022 23:54:57 +0200 Subject: [PATCH] Refactor scanning service to replace event based scanning (#41) * Refactor scanning service to replace event based scanning --- .../IntervalScanningServiceTests.cs | 83 +++++++----- .../EventArgs/ExceptionEventArgs.cs | 12 -- .../EventArgs/StringCollectionEventArgs.cs | 12 -- .../Imaging/DNTScannerDevices.cs | 2 +- Mosey.Application/Imaging/DntScanningHost.cs | 2 +- Mosey.Application/IntervalScanningService.cs | 121 ++++++++++++------ Mosey.Application/IntervalTimer.cs | 46 +++---- .../Models/IIntervalScanningService.cs | 9 +- Mosey.Application/ScanningProgress.cs | 25 ++++ Mosey.Core/IIntervalTimer.cs | 4 +- .../ViewModels/MainViewModelTests.cs | 15 ++- Mosey.Gui/Mosey.Gui.csproj | 6 +- Mosey.Gui/ViewModels/MainViewModel.cs | 104 +++++++-------- 13 files changed, 247 insertions(+), 194 deletions(-) delete mode 100644 Mosey.Application/EventArgs/ExceptionEventArgs.cs delete mode 100644 Mosey.Application/EventArgs/StringCollectionEventArgs.cs create mode 100644 Mosey.Application/ScanningProgress.cs diff --git a/Mosey.Application.Tests/IntervalScanningServiceTests.cs b/Mosey.Application.Tests/IntervalScanningServiceTests.cs index 4079616..7b0c334 100644 --- a/Mosey.Application.Tests/IntervalScanningServiceTests.cs +++ b/Mosey.Application.Tests/IntervalScanningServiceTests.cs @@ -6,6 +6,7 @@ using AutoFixture.NUnit3; using FluentAssertions; using Mosey.Application.Tests.AutoData; +using Mosey.Core; using Mosey.Core.Imaging; using Mosey.Tests.AutoData; using Mosey.Tests.Extensions; @@ -49,19 +50,6 @@ public async Task RepeatRefreshDevices([Frozen] IImagingHost scanningHost, Inter .Should().BeInRange(5, 15); } - [Theory, AutoNSubstituteData] - public async Task CancelTask([Frozen] IImagingHost scanningHost, IntervalScanningService sut) - { - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - await sut.BeginRefreshDevices(TimeSpan.FromSeconds(1), cts.Token); - - await scanningHost - .DidNotReceiveWithAnyArgs() - .RefreshDevicesAsync(default, default); - } - [Theory, AutoNSubstituteData] public async Task Raise_DevicesRefreshedEvent(IntervalScanningService sut) { @@ -94,38 +82,75 @@ public class StartScanningShould [Theory, IntervalScanningServiceAutoData] public void SetScanningProperties(IntervalScanningService sut) { - sut.StartScanning(TimeSpan.FromDays(1), TimeSpan.Zero, 1); + _ = sut.StartScanning(new IntervalTimerConfig(TimeSpan.FromDays(1), TimeSpan.Zero, 1)); sut.IsScanRunning.Should().BeTrue(); - sut.ScanRepetitionsCount.Should().Be(0); + sut.StartTime.Should().BeBefore(sut.FinishTime); + sut.FinishTime.Should().BeAfter(sut.StartTime); } [Theory, IntervalScanningServiceAutoData] - public async Task Raise_ScanRepetitionCompletedEvent([Greedy] IntervalScanningService sut) + public async Task Cancel_BeforeScanning([Greedy] IntervalScanningService sut) { - using (var monitoredSubject = sut.Monitor()) + ScanningProgress? finalProgress = null; + var progress = new Progress((report) => { - sut.StartScanning(TimeSpan.Zero, TimeSpan.Zero, 1); + finalProgress = report; + }); - await Task.Delay(1000); + var scanningTask = sut.StartScanning(new IntervalTimerConfig(TimeSpan.FromDays(1), TimeSpan.Zero, 100), progress); + await Task.WhenAny(scanningTask, Task.Delay(100)); + sut.StopScanning(waitForCompletion: false); - monitoredSubject.Should().Raise(nameof(IntervalScanningService.ScanRepetitionCompleted)); - } + finalProgress.Should().BeNull(); } - } - public class StopScanningShould - { [Theory, IntervalScanningServiceAutoData] - public void Raise_ScanningCompletedEvent(IntervalScanningService sut) + public void Cancel_DuringScanning([Greedy] IntervalScanningService sut) { + ScanningProgress? finalProgress = null; + var progress = new Progress((report) => + { + finalProgress = report; + }); - using (var monitoredSubject = sut.Monitor()) + _ = sut.StartScanning(new IntervalTimerConfig(TimeSpan.Zero, TimeSpan.FromSeconds(10), 100), progress); + sut.StopScanning(); + + finalProgress?.Should().NotBeNull(); + finalProgress?.RepetitionCount.Should().Be(1); + finalProgress?.Stage.Should().Be(ScanningProgress.ScanningStage.Finish); + } + + [Theory, IntervalScanningServiceAutoData] + public async Task Report_IterationCount(int iterations, [Greedy] IntervalScanningService sut) + { + var iterationCount = 0; + var progress = new Progress((report) => { - sut.StopScanning(); + iterationCount = report.RepetitionCount; + }); - monitoredSubject.Should().Raise(nameof(IntervalScanningService.ScanningCompleted)); - } + await sut.StartScanning(new IntervalTimerConfig(TimeSpan.Zero, TimeSpan.Zero, iterations), progress); + + iterationCount.Should().Be(iterations); + } + + [Theory, IntervalScanningServiceAutoData, Repeat(5)] + public async Task Report_RepetitionsInAscendingOrder(int iterations, [Greedy] IntervalScanningService sut) + { + var repetitions = new List(); + var progress = new Progress((report) => + { + if (report.Stage == ScanningProgress.ScanningStage.Finish) + { + repetitions.Add(report.RepetitionCount); + } + }); + + await sut.StartScanning(new IntervalTimerConfig(TimeSpan.Zero, TimeSpan.Zero, iterations), progress); + + repetitions.Should().BeInAscendingOrder(); } } } diff --git a/Mosey.Application/EventArgs/ExceptionEventArgs.cs b/Mosey.Application/EventArgs/ExceptionEventArgs.cs deleted file mode 100644 index 7936205..0000000 --- a/Mosey.Application/EventArgs/ExceptionEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Mosey.Application -{ - public class ExceptionEventArgs : EventArgs - { - public Exception Exception { get; init; } - - public ExceptionEventArgs(Exception ex) - { - Exception = ex; - } - } -} diff --git a/Mosey.Application/EventArgs/StringCollectionEventArgs.cs b/Mosey.Application/EventArgs/StringCollectionEventArgs.cs deleted file mode 100644 index 9ea1cd3..0000000 --- a/Mosey.Application/EventArgs/StringCollectionEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Mosey.Application -{ - public class StringCollectionEventArgs : EventArgs - { - public IEnumerable Values { get; init; } - - public StringCollectionEventArgs(IEnumerable values) - { - Values = values; - } - } -} diff --git a/Mosey.Application/Imaging/DNTScannerDevices.cs b/Mosey.Application/Imaging/DNTScannerDevices.cs index a5b1025..a4e7426 100644 --- a/Mosey.Application/Imaging/DNTScannerDevices.cs +++ b/Mosey.Application/Imaging/DNTScannerDevices.cs @@ -136,7 +136,7 @@ private static T WIARetry(Func method, int connectRetries, TimeSpan retryD result = method.Invoke(); break; } - catch (Exception ex) when (ex is COMException || ex is NullReferenceException) + catch (Exception ex) when (ex is COMException || ex is AccessViolationException || ex is NullReferenceException) { if (--connectRetries > 0) { diff --git a/Mosey.Application/Imaging/DntScanningHost.cs b/Mosey.Application/Imaging/DntScanningHost.cs index 5af822a..96a4e2b 100644 --- a/Mosey.Application/Imaging/DntScanningHost.cs +++ b/Mosey.Application/Imaging/DntScanningHost.cs @@ -37,7 +37,7 @@ public async Task> PerformImagingAsync(bool useHighes using (var staQueue = new StaTaskScheduler(concurrencyLevel: 1, disableComObjectEagerCleanup: true)) { results = await Task.Factory.StartNew( - () => PerformImaging(useHighestResolution, cancellationToken), + () => PerformImaging(useHighestResolution, cancellationToken).ToList(), cancellationToken, TaskCreationOptions.LongRunning, staQueue) diff --git a/Mosey.Application/IntervalScanningService.cs b/Mosey.Application/IntervalScanningService.cs index 43e4127..6098db5 100644 --- a/Mosey.Application/IntervalScanningService.cs +++ b/Mosey.Application/IntervalScanningService.cs @@ -1,4 +1,5 @@ -using System.IO.Abstractions; +using System.Collections.Concurrent; +using System.IO.Abstractions; using Microsoft.Extensions.Logging; using Mosey.Applicaton.Configuration; using Mosey.Core; @@ -14,33 +15,33 @@ public sealed class IntervalScanningService : IIntervalScanningService, IDisposa private readonly IIntervalTimer _intervalTimer; private readonly IFileSystem _fileSystem; private readonly ILogger _log; + private readonly ConcurrentQueue _queuedScanRepetitions = new(); private readonly CancellationTokenSource _refreshDevicesCancellationSource = new(); - private readonly SemaphoreSlim _semaphore = new(1, 1); + private IProgress? _progress; private ScanningConfig _config; private CancellationTokenSource _scanningCancellationSource = new(); public event EventHandler? DevicesRefreshed; - public event EventHandler? ScanRepetitionCompleted; - public event EventHandler? ScanningCompleted; public bool CreateDirectoryPath { get; set; } = true; public TimeSpan DeviceRefreshInterval { get; set; } = TimeSpan.FromSeconds(1); - public IImagingDevices Scanners => _imagingHost.ImagingDevices; + public IImagingDevices Scanners + => _imagingHost.ImagingDevices; public bool IsScanRunning - => (_intervalTimer?.Enabled ?? false) || _imagingHost.ImagingDevices.IsImagingInProgress; - - public int ScanRepetitionsCount - => _intervalTimer?.RepetitionsCount ?? 0; + => (_intervalTimer?.Enabled ?? false) + || _imagingHost.ImagingDevices.IsImagingInProgress; public DateTime StartTime => _intervalTimer.StartTime; public DateTime FinishTime - => _intervalTimer.FinishTime; + => _intervalTimer?.Enabled ?? false + ? _intervalTimer.FinishTime + : DateTime.Now; public IntervalScanningService(IImagingHost imagingHost, IIntervalTimer intervalTimer, IFileSystem fileSystem, ILogger logger) : this(imagingHost, intervalTimer, new ScanningConfig(new DeviceConfig(), new ImagingDeviceConfig(), new ImageFileConfig()), fileSystem, logger) @@ -55,15 +56,13 @@ public IntervalScanningService(IImagingHost imagingHost, IIntervalTimer interval _fileSystem = fileSystem; _log = logger; - _intervalTimer.Tick += async (_, _) => await ScanAndSaveImages(); - _intervalTimer.Complete += OnScanningCompleted; + _intervalTimer.Tick += (_, repetitionsCount) => _queuedScanRepetitions.Enqueue(repetitionsCount); _ = BeginRefreshDevices(DeviceRefreshInterval, _refreshDevicesCancellationSource.Token).ConfigureAwait(false); } public void Dispose() { - _intervalTimer.Complete -= OnScanningCompleted; _refreshDevicesCancellationSource?.Cancel(); _refreshDevicesCancellationSource?.Dispose(); _scanningCancellationSource?.Cancel(); @@ -82,49 +81,85 @@ public long GetRequiredDiskSpace(int repetitions) /// /// The estimated time taken for all the currently active scanners to complete a single image capture. /// - /// public TimeSpan GetRequiredScanningTime() => Scanners.Devices.Count(d => d.IsEnabled) * _config.DeviceConfig.GetResolutionMetadata(_config.ImagingDeviceConfig.Resolution).ImagingTime; /// - /// Initiate scanning with all available s and save any captured images to disk + /// Initiate scanning with all available s and save any captured images to disk. /// + /// The current /// s representing file paths for scanned images public async Task> ScanAndSaveImages() + => await ScanAndSaveImages(0); + + /// + /// The current + public async Task> ScanAndSaveImages(int repetitionsCount) { _log.LogTrace($"Scan initiated with {nameof(ScanAndSaveImages)} method."); + Exception? exception = null; var results = Enumerable.Empty(); - var args = EventArgs.Empty; try { - await _semaphore.WaitAsync(_scanningCancellationSource.Token); + _progress?.Report(new ScanningProgress(repetitionsCount, ScanningProgress.ScanningStage.Start, exception)); + results = await ScanAndSaveImages(CreateDirectoryPath, _scanningCancellationSource.Token).ConfigureAwait(false); - args = new StringCollectionEventArgs(results); } catch (OperationCanceledException ex) { - _log.LogInformation(ex, "Scanning cancelled before it could be completed"); + exception = ex; } catch (Exception ex) { _log.LogError("An error occured when attempting to aquire images, or when saving them to disk", ex); - args = new ExceptionEventArgs(ex); + exception = ex; } finally { _log.LogTrace($"Scan completed with {nameof(ScanAndSaveImages)} method."); - ScanRepetitionCompleted?.Invoke(this, args); - _semaphore.Release(); + _progress?.Report(new ScanningProgress(repetitionsCount, ScanningProgress.ScanningStage.Finish, exception)); } return results; } - public void StartScanning(TimeSpan delay, TimeSpan interval, int repetitions) - => _intervalTimer.Start(delay, interval, repetitions); + /// + /// Begins scanning at set intervals. + /// + /// The interval scanning configuration + /// Progress is reported both before and after each successful scan repetition + public async Task StartScanning(IntervalTimerConfig config, IProgress? progress = null) + { + _queuedScanRepetitions.Clear(); + var tcs = new TaskCompletionSource(); + + _intervalTimer.Complete += onTimerCompleted; + + _progress = progress; + _intervalTimer.Start(config.Delay, config.Interval, config.Repetitions); + + _log.LogInformation("Scanning started with {ScanRepetitions} repetitions to complete.", config.Repetitions); + _log.LogDebug("IntervalTimer started. Delay: {Delay} Interval: {Interval} Repetitions: {Repetitions}", config.Delay, config.Interval, config.Repetitions); + + await ProcessScanRepetitions(config.Repetitions, _scanningCancellationSource.Token).ConfigureAwait(false); + await tcs.Task.WaitAsync(_scanningCancellationSource.Token).ConfigureAwait(false); + await _imagingHost.WaitForImagingToComplete(_scanningCancellationSource.Token).ConfigureAwait(false); + + _log.LogInformation("Scanning finished, {reptitionsCount} repetitions completed.", tcs.Task.Result); + + void onTimerCompleted(object? sender, int repetitionsCount) + { + tcs.SetResult(repetitionsCount); + _intervalTimer.Complete -= onTimerCompleted; + } + } + /// + /// Stop any ongoing scanning run that has been started with . + /// + /// If any currently incomplete scanning iterations should be allowed to finish public void StopScanning(bool waitForCompletion = true) { try @@ -148,13 +183,12 @@ public void UpdateConfig(ScanningConfig config) } /// - /// Starts a loop that continually refreshes the list of available scanners + /// Starts a loop that continually refreshes the list of available scanners. /// /// The duration between refreshes /// Used to stop the refresh loop internal async Task BeginRefreshDevices(TimeSpan interval, CancellationToken cancellationToken) { - var args = EventArgs.Empty; _log.LogTrace($"Device refresh initiated with {nameof(BeginRefreshDevices)}"); while (!cancellationToken.IsCancellationRequested) @@ -166,23 +200,20 @@ internal async Task BeginRefreshDevices(TimeSpan interval, CancellationToken can await Task.Delay(interval, cancellationToken).ConfigureAwait(false); await _imagingHost.RefreshDevicesAsync(enableDevices, cancellationToken).ConfigureAwait(false); + + DevicesRefreshed?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { _log.LogWarning(ex, "Error while attempting to refresh devices."); - args = new ExceptionEventArgs(ex); } finally { _log.LogTrace($"Device refresh completed in {nameof(BeginRefreshDevices)}"); - DevicesRefreshed?.Invoke(this, args); } } } - internal async Task> PerformImaging(bool useHighestResolution = false, CancellationToken cancellationToken = default) - => await _imagingHost.PerformImagingAsync(useHighestResolution, cancellationToken); - internal static string GetImageFilePath(CapturedImage image, ImageFileConfig config, bool appendIndex, DateTime saveDateTime) { var index = appendIndex ? image.Index.ToString() : null; @@ -193,17 +224,23 @@ internal static string GetImageFilePath(CapturedImage image, ImageFileConfig con return Path.Combine(directory, Path.ChangeExtension(fileName, config.ImageFormat.ToString().ToLower())); } - private async void OnScanningCompleted(object? sender, EventArgs e) + private async Task ProcessScanRepetitions(int maxRepetitions, CancellationToken cancellationToken) { - try - { - await _semaphore.WaitAsync(_scanningCancellationSource.Token); - await _imagingHost.WaitForImagingToComplete(_scanningCancellationSource.Token).ConfigureAwait(false); - ScanningCompleted?.Invoke(this, EventArgs.Empty); - } - finally - { - _semaphore.Release(); + while (!cancellationToken.IsCancellationRequested) + { + if (_queuedScanRepetitions.TryDequeue(out var scanRepetition)) + { + await ScanAndSaveImages(scanRepetition); + + if (scanRepetition >= maxRepetitions) + { + break; + } + } + else + { + await Task.Delay(10, cancellationToken); + } } } @@ -213,7 +250,7 @@ private async Task> ScanAndSaveImages(bool createDirectoryPa var results = new List(); var saveDateTime = DateTime.Now; - var images = (await PerformImaging(_config.DeviceConfig.UseHighestResolution, cancellationToken).ConfigureAwait(false)).ToList(); + var images = (await _imagingHost.PerformImagingAsync(_config.DeviceConfig.UseHighestResolution, cancellationToken).ConfigureAwait(false)).ToList(); if (!cancellationToken.IsCancellationRequested) { diff --git a/Mosey.Application/IntervalTimer.cs b/Mosey.Application/IntervalTimer.cs index ac623d1..f75eff5 100644 --- a/Mosey.Application/IntervalTimer.cs +++ b/Mosey.Application/IntervalTimer.cs @@ -16,8 +16,16 @@ public sealed class IntervalTimer : IIntervalTimer public int RepetitionsCount { get; private set; } public bool Enabled => timer is not null; public bool Paused { get; private set; } - public event EventHandler? Tick; - public event EventHandler? Complete; + + /// + /// Raised on every interval. Returns the current repetition number. + /// + public event EventHandler? Tick; + + /// + /// Raised when the timer is stopped. Returns the total number of repetitions completed. + /// + public event EventHandler? Complete; private Timer? timer; private TimeSpan intervalRemaining; @@ -42,7 +50,8 @@ public IntervalTimer(TimeSpan delay, TimeSpan interval, int repetitions) /// /// Starts a timer using the current properties /// - public void Start() => Start(Delay, Interval, Repetitions); + public void Start() + => Start(Delay, Interval, Repetitions); /// /// Starts a timer with no repetition limit @@ -50,7 +59,8 @@ public IntervalTimer(TimeSpan delay, TimeSpan interval, int repetitions) /// /// The delay before starting the first interval /// The time between each callback - public void Start(TimeSpan delay, TimeSpan interval) => Start(delay, interval, -1); + public void Start(TimeSpan delay, TimeSpan interval) + => Start(delay, interval, -1); /// /// Start a new timer. If delay is zero then the first callback will begin immediately @@ -74,30 +84,16 @@ public void Start(TimeSpan delay, TimeSpan interval, int repetitions) stopWatch.Restart(); } - /// - /// Raises an event when an interval has elapsed - /// - /// - public void OnTick(EventArgs args) - => Tick?.Invoke(this, args); - - /// - /// Raises an event when the timer has run to completion - /// - /// - public void OnComplete(EventArgs args) - => Complete?.Invoke(this, args); - /// /// Timer callback method. Continues the timer until the maximum repetition count is reached /// /// private void TimerInterval(object? state) { - if (RepetitionsCount++ <= Repetitions || Repetitions == -1) + if (++RepetitionsCount <= Repetitions || Repetitions == -1) { // Notify that a repetition was completed - OnTick(EventArgs.Empty); + Tick?.Invoke(this, RepetitionsCount); Resume(); } @@ -113,6 +109,8 @@ private void TimerInterval(object? state) /// public void Stop() { + var completedRepetitions = RepetitionsCount; + Paused = false; StartTime = DateTime.MinValue; RepetitionsCount = 0; @@ -124,7 +122,7 @@ public void Stop() timer = null; } - OnComplete(EventArgs.Empty); + Complete?.Invoke(this, completedRepetitions); } /// @@ -177,11 +175,13 @@ public void Resume() public void Dispose() { - foreach (var del in Tick?.GetInvocationList().Select(i => i as EventHandler)) + var tickInvocations = Tick?.GetInvocationList().Select(i => i as EventHandler); + foreach (var del in tickInvocations ?? Enumerable.Empty>()) { Tick -= del; } - foreach (var del in Complete?.GetInvocationList().Select(i => i as EventHandler)) + var completeInvocations = Tick?.GetInvocationList().Select(i => i as EventHandler); + foreach (var del in completeInvocations ?? Enumerable.Empty>()) { Complete -= del; } diff --git a/Mosey.Application/Models/IIntervalScanningService.cs b/Mosey.Application/Models/IIntervalScanningService.cs index b2012d3..f054f1a 100644 --- a/Mosey.Application/Models/IIntervalScanningService.cs +++ b/Mosey.Application/Models/IIntervalScanningService.cs @@ -1,16 +1,15 @@ -namespace Mosey.Application +using Mosey.Core; + +namespace Mosey.Application { public interface IIntervalScanningService : IScanningService { event EventHandler DevicesRefreshed; - event EventHandler ScanRepetitionCompleted; - event EventHandler ScanningCompleted; - int ScanRepetitionsCount { get; } DateTime StartTime { get; } DateTime FinishTime { get; } - void StartScanning(TimeSpan delay, TimeSpan interval, int repetitions); + Task StartScanning(IntervalTimerConfig config, IProgress? progress = null); void StopScanning(bool waitForCompletion = true); } diff --git a/Mosey.Application/ScanningProgress.cs b/Mosey.Application/ScanningProgress.cs new file mode 100644 index 0000000..1f4dd33 --- /dev/null +++ b/Mosey.Application/ScanningProgress.cs @@ -0,0 +1,25 @@ +using static Mosey.Application.ScanningProgress; + +namespace Mosey.Application +{ + public record ScanningProgress(int RepetitionCount, ScanningStage Stage, Exception? Exception = null) + { + public enum ProgressResult + { + Success, + Error + }; + + public enum ScanningStage + { + Start, + InProgress, + Finish + }; + + public ProgressResult Result + => Exception is null + ? ProgressResult.Success + : ProgressResult.Error; + }; +} diff --git a/Mosey.Core/IIntervalTimer.cs b/Mosey.Core/IIntervalTimer.cs index d9e344c..abbca42 100644 --- a/Mosey.Core/IIntervalTimer.cs +++ b/Mosey.Core/IIntervalTimer.cs @@ -2,8 +2,8 @@ { public interface IIntervalTimer : ITimer { - event EventHandler Tick; - event EventHandler Complete; + event EventHandler Tick; + event EventHandler Complete; DateTime StartTime { get; } DateTime FinishTime { get; } diff --git a/Mosey.Gui.Tests/ViewModels/MainViewModelTests.cs b/Mosey.Gui.Tests/ViewModels/MainViewModelTests.cs index 1cba463..511c918 100644 --- a/Mosey.Gui.Tests/ViewModels/MainViewModelTests.cs +++ b/Mosey.Gui.Tests/ViewModels/MainViewModelTests.cs @@ -8,6 +8,10 @@ using NSubstitute; using NUnit.Framework; using Mosey.Application; +using Mosey.Core; +using System; +using FluentAssertions.Execution; +using System.Threading.Tasks; namespace Mosey.Gui.ViewModels.Tests { @@ -30,14 +34,19 @@ public void InitializeScanningDevicesCollection([Frozen] IEnumerable x.IsScanRunning); monitoredSubject.Should().RaisePropertyChangeFor(x => x.ScanFinishTime); + monitoredSubject.Should().RaisePropertyChangeFor(x => x.ScanRepetitionsCount); monitoredSubject.Should().RaisePropertyChangeFor(x => x.StartStopScanCommand); } } @@ -53,8 +62,6 @@ public void RaisePropertyChanged([Frozen] IIntervalScanningService scanningServi scanningService.DevicesRefreshed += Raise.Event(); monitoredSubject.Should().RaisePropertyChangeFor(x => x.ScanningDevices); - monitoredSubject.Should().RaisePropertyChangeFor(x => x.StartScanCommand); - monitoredSubject.Should().RaisePropertyChangeFor(x => x.StartStopScanCommand); } } } diff --git a/Mosey.Gui/Mosey.Gui.csproj b/Mosey.Gui/Mosey.Gui.csproj index 5d2a105..3dea54d 100644 --- a/Mosey.Gui/Mosey.Gui.csproj +++ b/Mosey.Gui/Mosey.Gui.csproj @@ -9,12 +9,12 @@ AnyCPU https://github.com/Erik-White/Mosey https://github.com/Erik-White/Mosey.git - 2.0.2 + 2.0.3 GPL-3.0-or-later imaging, scan, scanning - 2.0.2.0 - 2.0.2.0 + 2.0.3.0 + 2.0.3.0 Multiple scanner interval imaging tool git diff --git a/Mosey.Gui/ViewModels/MainViewModel.cs b/Mosey.Gui/ViewModels/MainViewModel.cs index bf39ded..13c20aa 100644 --- a/Mosey.Gui/ViewModels/MainViewModel.cs +++ b/Mosey.Gui/ViewModels/MainViewModel.cs @@ -119,14 +119,13 @@ public int ScanRepetitions public bool IsScanRunning => _scanningService.IsScanRunning; - public int ScanRepetitionsCount - => _scanningService.ScanRepetitionsCount; + public int ScanRepetitionsCount { get; private set; } public TimeSpan ScanNextTime { get { - if (IsScanRunning && ScanRepetitionsCount != 0) + if (IsScanRunning && 0 < ScanRepetitionsCount && ScanRepetitionsCount < ScanRepetitions) { var scanNext = _scanningService.StartTime.Add(ScanRepetitionsCount * _scanInterval); return scanNext.Subtract(DateTime.Now); @@ -139,7 +138,9 @@ public TimeSpan ScanNextTime } public DateTime ScanFinishTime - => IsScanRunning ? _scanningService.FinishTime : DateTime.MinValue; + => IsScanRunning + ? _scanningService.FinishTime + : DateTime.MinValue; /// public IImagingDevices ScanningDevices { get; private set; } @@ -187,8 +188,6 @@ public MainViewModel( public void Dispose() { - _scanningService.ScanRepetitionCompleted -= ScanRepetition_Completed; - _scanningService.ScanningCompleted -= Scanning_Complete; _scanningService.DevicesRefreshed -= ScanningDevices_Refreshed; _cancelScanTokenSource?.Cancel(); @@ -296,16 +295,42 @@ public async void OnClosingAsync() /// /// Begin scanning at set intervals /// - public void StartScanning() + public async Task StartScanning() { _cancelScanTokenSource = new CancellationTokenSource(); - _scanningService.StartScanning(_scanDelay, _scanInterval, ScanRepetitions); - _log.LogDebug("IntervalTimer started. Delay: {Delay} Interval: {Interval} Repetitions: {Repetitions}", _scanDelay, _scanInterval, ScanRepetitions); + var progress = new Progress(async (report) => + { + if (report.Result == ScanningProgress.ProgressResult.Success) + { + ScanRepetitionsCount = report.RepetitionCount; + RaisePropertyChanged(nameof(ScanRepetitionsCount)); + + if (report.Stage == ScanningProgress.ScanningStage.Finish && report.RepetitionCount >= ScanRepetitions) + { + // Apply any changes to settings that were made during scanning + UpdateConfig(_appSettings.Get(AppSettings.UserSettingsKey)); + + // Update properties, scanning finished + RaisePropertyChanged(nameof(IsScanRunning)); + RaisePropertyChanged(nameof(ScanFinishTime)); + RaisePropertyChanged(nameof(StartStopScanCommand)); + } + } + else + { + // An unhandled error occurred + _log.LogError(report.Exception, "{message}", report.Exception.Message); + await _dialog.ExceptionDialog(report.Exception); + StopScanning(false); + } + }); + + await _scanningService + .StartScanning(new IntervalTimerConfig(_scanDelay, _scanInterval, ScanRepetitions), progress) + .ConfigureAwait(false); - RaisePropertyChanged(nameof(IsScanRunning)); - RaisePropertyChanged(nameof(ScanFinishTime)); - RaisePropertyChanged(nameof(StartStopScanCommand)); - _log.LogInformation("Scanning started with {ScanRepetitions} repetitions to complete.", ScanRepetitions); + // Finsihed scanning + ScanRepetitionsCount = 0; } /// @@ -339,7 +364,7 @@ public async Task StartScanningWithDialog() _log.LogWarning(ex, $"Unable to show {nameof(DialogViewModel.DiskSpaceDialog)} on path {_appSettings.CurrentValue.ImageFile.Directory} due to {ex.GetType()}"); } - StartScanning(); + await StartScanning().ConfigureAwait(false); } /// @@ -391,52 +416,9 @@ internal void UpdateConfig(AppSettings settings) _log.LogTrace($"Configuration updated with {nameof(UpdateConfig)}."); } - /// - /// Tidy up after scanning is completed - /// - private void Scanning_Complete(object sender, EventArgs e) + private void ScanningDevices_Refreshed(object sender, EventArgs e) { - _log.LogTrace($"{nameof(Scanning_Complete)} event."); - - // Apply any changes to settings that were made during scanning - UpdateConfig(_appSettings.Get(AppSettings.UserSettingsKey)); - - // Update properties - RaisePropertyChanged(nameof(ScanRepetitionsCount)); - RaisePropertyChanged(nameof(IsScanRunning)); - RaisePropertyChanged(nameof(ScanFinishTime)); - RaisePropertyChanged(nameof(StartStopScanCommand)); - - _log.LogInformation("Scanning complete."); - } - - private async void ScanRepetition_Completed(object sender, EventArgs e) - { - _log.LogTrace($"{nameof(ScanRepetition_Completed)} event."); - - if (e is ExceptionEventArgs exceptionEventArgs) - { - // An unhandled error occurred, notify the user and cancel scanning - _log.LogError(exceptionEventArgs.Exception, exceptionEventArgs.Exception.Message); - await _dialog.ExceptionDialog(exceptionEventArgs.Exception); - StopScanning(false); - } - - // Update progress - RaisePropertyChanged(nameof(ScanNextTime)); - RaisePropertyChanged(nameof(ScanRepetitionsCount)); - } - - private async void ScanningDevices_Refreshed(object sender, EventArgs e) - { - if (e is ExceptionEventArgs exceptionEventArgs) - { - await _dialog.ExceptionDialog(exceptionEventArgs.Exception, 5000, CancellationToken.None).ConfigureAwait(false); - } - RaisePropertyChanged(nameof(ScanningDevices)); - RaisePropertyChanged(nameof(StartScanCommand)); - RaisePropertyChanged(nameof(StartStopScanCommand)); } /// @@ -449,7 +431,11 @@ private async Task BeginRefreshUI(TimeSpan interval) while (!System.Windows.Application.Current?.Dispatcher?.HasShutdownStarted ?? true) { await Task.Delay(interval); + RaisePropertyChanged(nameof(IsScanRunning)); RaisePropertyChanged(nameof(ScanNextTime)); + RaisePropertyChanged(nameof(ScanFinishTime)); + RaisePropertyChanged(nameof(ScanRepetitionsCount)); + RaisePropertyChanged(nameof(StartStopScanCommand)); } } @@ -491,8 +477,6 @@ private void Initialize() // Register event callbacks _appSettings.OnChange(UpdateConfig); - _scanningService.ScanRepetitionCompleted += ScanRepetition_Completed; - _scanningService.ScanningCompleted += Scanning_Complete; _scanningService.DevicesRefreshed += ScanningDevices_Refreshed; // Start a task loop to update UI