diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Core/MethodModelBase.cs b/src/MSDIAL5/MsdialGuiApp/Model/Core/MethodModelBase.cs index d25ebf05a..f3179eeb1 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Core/MethodModelBase.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Core/MethodModelBase.cs @@ -1,6 +1,5 @@ using CompMs.App.Msdial.Model.DataObj; using CompMs.App.Msdial.Model.Service; -using CompMs.App.Msdial.ViewModel.Service; using CompMs.Common.Enum; using CompMs.CommonMVVM; using CompMs.MsdialCore.Parameter; @@ -75,7 +74,7 @@ public Task LoadAnalysisFileAsync(AnalysisFileBeanModel analysisFile, Cancellati /// Opening by hand checks the disk again; the automatic first open uses what the dataset found a moment before, /// so that a folder that is gone is not checked once for every sample in it. /// - private ErrorMessageBoxRequest? CannotOpenReason(AnalysisFileBeanModel file, bool checkAgain) { + private MessageRequest? CannotOpenReason(AnalysisFileBeanModel file, bool checkAgain) { if (BrowsingWithoutRawData == BrowsingWithoutRawData.NotSupported) { // Imaging: opening a file selects its image, and the project only opened because the raw data are there. return null; diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs index 959c9b7d8..f8d88a4c7 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs @@ -1,13 +1,12 @@ using CompMs.App.Msdial.Common; +using CompMs.App.Msdial.Model.Service; using CompMs.App.Msdial.Model.Setting; -using CompMs.App.Msdial.ViewModel.Service; using CompMs.CommonMVVM; using CompMs.Graphics.UI.Message; using CompMs.MsdialCore.DataObj; using CompMs.MsdialCore.Parameter; using CompMs.MsdialCore.Parser; using CompMs.MsdialIntegrate.Parser; -using Microsoft.Win32; using Reactive.Bindings.Notifiers; using System; using System.Collections.Concurrent; @@ -137,14 +136,14 @@ private static async Task LoadMdprojectAsync(string projectPath, I var stopwatch = System.Diagnostics.Stopwatch.StartNew(); // Reading the datasets is data work only, and a loaded library can be hundreds of MB: it runs off the UI thread, // which keeps pumping meanwhile (a minute without pumping is a ContextSwitchDeadlock under the debugger). - // The picker and the messages go to the dispatcher themselves, and the models are built after the await, - // back on the UI thread. + // The picker and the messages are published on the broker, whose view takes them to the dispatcher, + // and the models are built after the await, back on the UI thread. var projectDataStorage = await Task.Run(() => ReadDatasetsAsync( streamManager, deserializer, path => new DirectoryTreeStreamManager(path), projectDir, - LocateDatasetFileAsync, + (parameter, missingPath) => LocateDatasetFileAsync(broker, parameter, missingPath), broker)); System.Diagnostics.Debug.WriteLine($"Read the datasets of {projectPath} in {stopwatch.Elapsed.TotalSeconds:F1} s."); if (projectDataStorage == null) { @@ -198,11 +197,11 @@ internal static async Task ReadDatasetsAsync( missingFiles[parameter] = notFound.FileName; } Interlocked.Increment(ref unloadedDatasets); - broker.Publish(new ErrorMessageBoxRequest + broker.Publish(new MessageRequest { Caption = "Dataset not loaded", Content = MessageHelper.DatasetNotLoaded(parameter.ProjectFileName, error), - Icon = MessageBoxImage.Error, + Severity = MessageSeverity.Error, }); }).ConfigureAwait(false); if (projectDataStorage is not null && projectDataStorage.Storages.Count == 0 && unloadedDatasets > 0) { @@ -212,21 +211,20 @@ internal static async Task ReadDatasetsAsync( return projectDataStorage!; } - private static async Task LocateDatasetFileAsync(ProjectBaseParameter parameter, string? missingPath) { + /// + /// Asks the view, through the broker, to pick the dataset file or a copy of it, and returns the file picked, or null when none was. + /// It is asked from the thread that reads the datasets: the view shows the picker on the UI thread, and the answer is there once Publish returns. + /// + internal static Task LocateDatasetFileAsync(IMessageBroker broker, ProjectBaseParameter parameter, string? missingPath) { string? result = null; - await Application.Current.Dispatcher.InvokeAsync(() => + var request = new OpenFileRequest(path => result = path) { - var newofd = new OpenFileDialog - { - Filter = "Dataset file(.mddata)|*.mddata|All(*)|*", - Title = MessageHelper.LocateDatasetFileTitle(parameter.ProjectFileName, missingPath), - RestoreDirectory = true - }; - if (newofd.ShowDialog() == true) { - result = newofd.FileName; - } - }); - return result; + Filter = "Dataset file(.mddata)|*.mddata|All(*)|*", + Title = MessageHelper.LocateDatasetFileTitle(parameter.ProjectFileName, missingPath), + RestoreDirectory = true, + }; + broker.Publish(request); + return Task.FromResult(result); } private static async Task LoadMddatasetAsync(string mddata, IMessageBroker broker) { diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Export/ExportFailure.cs b/src/MSDIAL5/MsdialGuiApp/Model/Export/ExportFailure.cs index 0f71419a8..67220bc72 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Export/ExportFailure.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Export/ExportFailure.cs @@ -1,5 +1,5 @@ using CompMs.App.Msdial.Model.Loader; -using CompMs.App.Msdial.ViewModel.Service; +using CompMs.App.Msdial.Model.Service; using Reactive.Bindings.Notifiers; using System; using System.IO; @@ -17,7 +17,7 @@ namespace CompMs.App.Msdial.Model.Export internal static class ExportFailure { public static void Report(IMessageBroker broker, Exception error) { - broker.Publish(new ErrorMessageBoxRequest + broker.Publish(new MessageRequest { Caption = "Export failed", Content = Describe(error), diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Service/MessageRequest.cs b/src/MSDIAL5/MsdialGuiApp/Model/Service/MessageRequest.cs new file mode 100644 index 000000000..19d6576aa --- /dev/null +++ b/src/MSDIAL5/MsdialGuiApp/Model/Service/MessageRequest.cs @@ -0,0 +1,35 @@ +namespace CompMs.App.Msdial.Model.Service +{ + /// + /// How serious a is; the view shows it as the icon of the message. + /// + internal enum MessageSeverity + { + Information, + Warning, + Error, + } + + /// + /// A message that a model publishes on the message broker for the view to show, and, when it asks Yes or No, the answer. + /// It carries no UI type, so a model can say something without referring to the view or the view models. + /// + internal sealed class MessageRequest + { + public string Caption { get; set; } = "Error"; + + public string Content { get; set; } = string.Empty; + + public MessageSeverity Severity { get; set; } = MessageSeverity.Error; + + /// + /// true to ask Yes or No; otherwise the message only has an OK button. + /// + public bool AsksYesNo { get; set; } + + /// + /// true when the user answered Yes (or OK); null while nobody has answered. + /// + public bool? Result { get; set; } + } +} diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Service/RawDataGuard.cs b/src/MSDIAL5/MsdialGuiApp/Model/Service/RawDataGuard.cs index 2561ef2c7..cfb8ccb03 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Service/RawDataGuard.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Service/RawDataGuard.cs @@ -1,19 +1,17 @@ using CompMs.App.Msdial.Model.Core; using CompMs.App.Msdial.Model.DataObj; -using CompMs.App.Msdial.ViewModel.Service; using Reactive.Bindings.Notifiers; using System; using System.Collections.Generic; using System.Linq; -using System.Windows; namespace CompMs.App.Msdial.Model.Service { /// /// What MS-DIAL says when the raw data files of a project are missing: the prompt shown when a dataset opens, /// why a sample or a command is refused, and the titles of the panels that stay empty. - /// Every message here names a path or an exception, so each goes through , - /// a message box that fits them. + /// Every message here names a path or an exception, so each goes through , + /// which the view shows as a message box that fits them. /// internal static class RawDataGuard { @@ -39,10 +37,10 @@ public static bool ConfirmOpen(IMessageBroker broker, BrowsingWithoutRawData mod } var counts = $"Raw data are not available for {missing.Count} of {sampleCount} samples in this dataset"; var list = "Raw data files not found:\n" + FormatPaths(missing.Select(file => file.AnalysisFilePath).ToList()); - ErrorMessageBoxRequest request; + MessageRequest request; switch (mode) { case BrowsingWithoutRawData.Supported: - request = new ErrorMessageBoxRequest + request = new MessageRequest { Caption = RawDataNotFoundCaption, Content = $"{counts}, so only the information saved in the project will be drawn for them.\n\n" @@ -52,12 +50,12 @@ public static bool ConfirmOpen(IMessageBroker broker, BrowsingWithoutRawData mod + "the export of centroid spectra, and reprocessing.\n\n" + $"{list}\n\n" + "Continue?", - ButtonType = MessageBoxButton.YesNo, - Icon = MessageBoxImage.Warning, + AsksYesNo = true, + Severity = MessageSeverity.Warning, }; break; case BrowsingWithoutRawData.AlignmentsOnly: - request = new ErrorMessageBoxRequest + request = new MessageRequest { Caption = RawDataNotFoundCaption, Content = $"{counts}, so only the information saved in the project will be drawn.\n\n" @@ -65,25 +63,24 @@ public static bool ConfirmOpen(IMessageBroker broker, BrowsingWithoutRawData mod + "so these samples cannot be opened in it, and reprocessing is not available.\n\n" + $"{list}\n\n" + "Continue?", - ButtonType = MessageBoxButton.YesNo, - Icon = MessageBoxImage.Warning, + AsksYesNo = true, + Severity = MessageSeverity.Warning, }; break; default: - broker.Publish(new ErrorMessageBoxRequest + broker.Publish(new MessageRequest { Caption = RawDataNotFoundCaption, Content = $"{counts}.\n\n" + "Imaging projects draw their ion images from the raw data, so this project cannot be opened without them.\n\n" + $"{list}\n\n" + "Put the files back at these paths and open the project again.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Error, + Severity = MessageSeverity.Error, }); return false; } broker.Publish(request); - return request.Result == MessageBoxResult.Yes; + return request.Result == true; } /// @@ -94,14 +91,13 @@ public static bool Ensure(bool isRawDataAvailable, AnalysisFileBeanModel file, I if (isRawDataAvailable) { return true; } - broker.Publish(new ErrorMessageBoxRequest + broker.Publish(new MessageRequest { Caption = RawDataNotAvailableCaption, Content = $"{feature} needs the raw data, and the raw data file of {file.AnalysisFileName} was not found:\n" + $" {file.AnalysisFilePath}\n\n" + "Put the file back at this path, then open another sample and this one again to reload it.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Information, + Severity = MessageSeverity.Information, }); return false; } @@ -116,7 +112,7 @@ public static bool EnsureAll(IEnumerable files, IMessageB if (missing.Count == 0) { return true; } - broker.Publish(new ErrorMessageBoxRequest + broker.Publish(new MessageRequest { Caption = RawDataNotAvailableCaption, Content = $"{feature} cannot run because the raw data files of {missing.Count} of {all.Count} samples were not found. " @@ -124,69 +120,63 @@ public static bool EnsureAll(IEnumerable files, IMessageB + "Raw data files not found:\n" + $"{FormatPaths(missing.Select(file => file.AnalysisFilePath).ToList())}\n\n" + "Put the files back at these paths to run it. The saved results can still be browsed.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Information, + Severity = MessageSeverity.Information, }); return false; } - public static ErrorMessageBoxRequest SampleNeedsRawData(AnalysisFileBeanModel file) { - return new ErrorMessageBoxRequest + public static MessageRequest SampleNeedsRawData(AnalysisFileBeanModel file) { + return new MessageRequest { Caption = RawDataNotAvailableCaption, Content = $"{file.AnalysisFileName} cannot be opened in the peak spot viewer: for this type of project the viewer reads the raw data, " + "and the raw data file was not found:\n" + $" {file.AnalysisFilePath}\n\n" + "Alignment results can still be browsed.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Information, + Severity = MessageSeverity.Information, }; } - public static ErrorMessageBoxRequest PeakListMissing(AnalysisFileBeanModel file) { - return new ErrorMessageBoxRequest + public static MessageRequest PeakListMissing(AnalysisFileBeanModel file) { + return new MessageRequest { Caption = "Saved results not found", Content = $"{file.AnalysisFileName} cannot be opened in the peak spot viewer because its saved peak list was not found:\n" + $" {file.PeakListFilePath}", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Information, + Severity = MessageSeverity.Information, }; } - public static ErrorMessageBoxRequest SampleOpenFailed(AnalysisFileBeanModel file, Exception error) { - return new ErrorMessageBoxRequest + public static MessageRequest SampleOpenFailed(AnalysisFileBeanModel file, Exception error) { + return new MessageRequest { Caption = "Sample could not be opened", Content = $"{file.AnalysisFileName} could not be opened in the peak spot viewer.\n\n" + $"{error.GetType().Name}: {error.Message}\n\n" + "The project is still open. You can select another sample or an alignment result.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Error, + Severity = MessageSeverity.Error, }; } - public static ErrorMessageBoxRequest AlignmentOpenFailed(AlignmentFileBeanModel file, Exception error) { - return new ErrorMessageBoxRequest + public static MessageRequest AlignmentOpenFailed(AlignmentFileBeanModel file, Exception error) { + return new MessageRequest { Caption = "Alignment result could not be opened", Content = $"{file.FileName} could not be opened in the alignment viewer.\n\n" + $"{error.GetType().Name}: {error.Message}\n\n" + "The project is still open. You can select a sample or another alignment result.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Error, + Severity = MessageSeverity.Error, }; } - public static ErrorMessageBoxRequest MsfinderWithoutMs1(AnalysisFileBeanModel file) { - return new ErrorMessageBoxRequest + public static MessageRequest MsfinderWithoutMs1(AnalysisFileBeanModel file) { + return new MessageRequest { Caption = RawDataNotAvailableCaption, Content = $"The raw data file of {file.AnalysisFileName} was not found, so its MS1 isotope pattern cannot be read. " + "MS-FINDER receives the MS/MS spectrum only, and its formula ranking may differ from a run with the raw data.\n\n" + "Sending the same feature from the alignment result includes the isotope pattern saved with the alignment spot.", - ButtonType = MessageBoxButton.OK, - Icon = MessageBoxImage.Information, + Severity = MessageSeverity.Information, }; } diff --git a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs index 5d8a0ea30..f0a18227b 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs +++ b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs @@ -75,6 +75,8 @@ public MainWindow() { .Subscribe(OpenFileDialog); broker.ToObservable() .Subscribe(ShowErrorConfirmationMessage); + broker.ToObservable() + .Subscribe(ShowMessage); broker.ToObservable() .Subscribe(ShowRiDictionarySettingDialog); broker.ToObservable() @@ -341,14 +343,17 @@ private void GetSaveFilePath(SaveFileNameRequest request) { } private void OpenFileDialog(OpenFileRequest request) { - var ofd = new OpenFileDialog() { - Title = request.Title, - Filter = request.Filter, - }; + Dispatcher.Invoke(() => { + var ofd = new OpenFileDialog() + { + Title = request.Title, + Filter = request.Filter, + }; - if (ofd.ShowDialog(this) == true) { - request.Run(ofd.FileName); - } + if (ofd.ShowDialog(this) == true) { + request.Run(ofd.FileName); + } + }); } private void SelectFolderPath(SelectFolderRequest request) { @@ -363,10 +368,18 @@ private void SelectFolderPath(SelectFolderRequest request) { } private void ShowErrorConfirmationMessage(ErrorMessageBoxRequest request) { + Dispatcher.Invoke(() => { + var result = MessageBox.Show(request.Content, request.Caption, request.ButtonType, MessageBoxImage.Error); + request.Result = result; + }); + } + + private void ShowMessage(MessageRequest request) { Dispatcher.Invoke(() => { // The wait cursor is left to the operations that set it: the message box draws its own arrow, // and an operation that finishes while the box is open clears the cursor itself. - request.Result = MessageBox.Show(request.Content, request.Caption, request.ButtonType, request.Icon); + var result = MessageBox.Show(request.Content, request.Caption, MessageRequestBox.ButtonsOf(request), MessageRequestBox.IconOf(request)); + request.Result = MessageRequestBox.IsAccepted(result); }); } diff --git a/src/MSDIAL5/MsdialGuiApp/View/Core/MessageRequestBox.cs b/src/MSDIAL5/MsdialGuiApp/View/Core/MessageRequestBox.cs new file mode 100644 index 000000000..6df3f1a2f --- /dev/null +++ b/src/MSDIAL5/MsdialGuiApp/View/Core/MessageRequestBox.cs @@ -0,0 +1,33 @@ +using CompMs.App.Msdial.Model.Service; +using System.Windows; + +namespace CompMs.App.Msdial.View.Core +{ + /// + /// How the view shows a as a message box, and what it answers. + /// + internal static class MessageRequestBox + { + public static MessageBoxButton ButtonsOf(MessageRequest request) { + return request.AsksYesNo ? MessageBoxButton.YesNo : MessageBoxButton.OK; + } + + public static MessageBoxImage IconOf(MessageRequest request) { + switch (request.Severity) { + case MessageSeverity.Information: + return MessageBoxImage.Information; + case MessageSeverity.Warning: + return MessageBoxImage.Warning; + default: + return MessageBoxImage.Error; + } + } + + /// + /// The of an answer: true for Yes or OK. + /// + public static bool IsAccepted(MessageBoxResult result) { + return result == MessageBoxResult.Yes || result == MessageBoxResult.OK; + } + } +} diff --git a/src/MSDIAL5/MsdialGuiApp/ViewModel/Service/ErrorMessageBoxRequest.cs b/src/MSDIAL5/MsdialGuiApp/ViewModel/Service/ErrorMessageBoxRequest.cs index 4e1d3c742..509e993d5 100644 --- a/src/MSDIAL5/MsdialGuiApp/ViewModel/Service/ErrorMessageBoxRequest.cs +++ b/src/MSDIAL5/MsdialGuiApp/ViewModel/Service/ErrorMessageBoxRequest.cs @@ -10,8 +10,6 @@ public class ErrorMessageBoxRequest public MessageBoxButton ButtonType { get; set; } = MessageBoxButton.OK; - public MessageBoxImage Icon { get; set; } = MessageBoxImage.Error; - public MessageBoxResult Result { get; set; } = MessageBoxResult.Cancel; } } diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/DatasetModelTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/DatasetModelTests.cs index 62ffa6981..d34735724 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/DatasetModelTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/DatasetModelTests.cs @@ -1,6 +1,6 @@ using CompMs.App.Msdial.Model.DataObj; using CompMs.App.Msdial.Model.DataObj.Tests; -using CompMs.App.Msdial.ViewModel.Service; +using CompMs.App.Msdial.Model.Service; using CompMs.Common.Enum; using CompMs.CommonMVVM; using CompMs.MsdialCore.DataObj; @@ -13,7 +13,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Windows; namespace CompMs.App.Msdial.Model.Core.Tests { @@ -22,17 +21,17 @@ public class DatasetModelTests { private string _folder = string.Empty; private MessageBroker _broker = new MessageBroker(); - private List _messages = new List(); - private MessageBoxResult _answer = MessageBoxResult.None; + private List _messages = new List(); + private bool? _answer = null; [TestInitialize()] public void Setup() { _folder = Path.Combine(Path.GetTempPath(), $"msdial-datasetmodel-{Guid.NewGuid():N}"); Directory.CreateDirectory(_folder); _broker = new MessageBroker(); - _messages = new List(); - _answer = MessageBoxResult.None; - _broker.Subscribe(request => { + _messages = new List(); + _answer = null; + _broker.Subscribe(request => { _messages.Add(request); request.Result = _answer; }); @@ -59,11 +58,11 @@ public async Task ADatasetWithAllItsRawDataOpensWithoutAsking() { public async Task DecliningOpensNoSampleAndDisposesTheMethod() { var files = new AnalysisFileBeanModelCollection(new[] { Sample("a", raw: false), Sample("b"), }); var method = CreateMethod(BrowsingWithoutRawData.Supported, files); - _answer = MessageBoxResult.No; + _answer = false; await Assert.ThrowsExactlyAsync(() => DatasetModel.OpenMethodAsync(method, files, _broker)); - Assert.AreEqual(MessageBoxButton.YesNo, _messages.Single().ButtonType); + Assert.IsTrue(_messages.Single().AsksYesNo); StringAssert.StartsWith(_messages[0].Content, "Raw data are not available for 1 of 2 samples in this dataset"); Assert.AreEqual(0, method.Opened.Count); Assert.IsTrue(method.IsDisposed); @@ -73,7 +72,7 @@ public async Task DecliningOpensNoSampleAndDisposesTheMethod() { public async Task AcceptingOpensTheMethodWithWhatTheProjectStores() { var files = new AnalysisFileBeanModelCollection(new[] { Sample("a", raw: false), Sample("b"), }); var method = CreateMethod(BrowsingWithoutRawData.Supported, files); - _answer = MessageBoxResult.Yes; + _answer = true; Assert.AreSame(method, await DatasetModel.OpenMethodAsync(method, files, _broker)); @@ -90,7 +89,7 @@ public async Task AnExcludedSampleWithoutRawDataIsAskedAbout() { excluded.AnalysisFileIncluded = false; var files = new AnalysisFileBeanModelCollection(new[] { excluded, Sample("b"), }); var method = CreateMethod(BrowsingWithoutRawData.Supported, files); - _answer = MessageBoxResult.No; + _answer = false; await Assert.ThrowsExactlyAsync(() => DatasetModel.OpenMethodAsync(method, files, _broker)); @@ -102,11 +101,11 @@ public async Task AnExcludedSampleWithoutRawDataIsAskedAbout() { public async Task ImagingWithoutRawDataIsRefusedAndTheMethodDisposed() { var files = new AnalysisFileBeanModelCollection(new[] { Sample("a", raw: false), }); var method = CreateMethod(BrowsingWithoutRawData.NotSupported, files); - _answer = MessageBoxResult.OK; + _answer = true; await Assert.ThrowsExactlyAsync(() => DatasetModel.OpenMethodAsync(method, files, _broker)); - Assert.AreEqual(MessageBoxButton.OK, _messages.Single().ButtonType); + Assert.IsFalse(_messages.Single().AsksYesNo); Assert.AreEqual(0, method.Opened.Count); Assert.IsTrue(method.IsDisposed); } diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/MethodModelBaseTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/MethodModelBaseTests.cs index 5b6e47b58..a69e05d0c 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/MethodModelBaseTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/MethodModelBaseTests.cs @@ -1,6 +1,6 @@ using CompMs.App.Msdial.Model.DataObj; using CompMs.App.Msdial.Model.DataObj.Tests; -using CompMs.App.Msdial.ViewModel.Service; +using CompMs.App.Msdial.Model.Service; using CompMs.Common.Enum; using CompMs.CommonMVVM; using CompMs.MsdialCore.DataObj; @@ -21,15 +21,15 @@ public class MethodModelBaseTests { private string _folder = string.Empty; private MessageBroker _broker = new MessageBroker(); - private List _messages = new List(); + private List _messages = new List(); [TestInitialize()] public void Setup() { _folder = Path.Combine(Path.GetTempPath(), $"msdial-methodmodel-{Guid.NewGuid():N}"); Directory.CreateDirectory(_folder); _broker = new MessageBroker(); - _messages = new List(); - _broker.Subscribe(_messages.Add); + _messages = new List(); + _broker.Subscribe(_messages.Add); } [TestCleanup()] diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs index 429b59b94..5d41f9d87 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs @@ -1,7 +1,7 @@ using CompMs.App.Msdial.Common; using CompMs.App.Msdial.Model.DataObj; +using CompMs.App.Msdial.Model.Service; using CompMs.App.Msdial.Model.Setting; -using CompMs.App.Msdial.ViewModel.Service; using CompMs.CommonMVVM; using CompMs.MsdialCore.DataObj; using CompMs.MsdialCore.Parameter; @@ -22,13 +22,13 @@ namespace CompMs.App.Msdial.Model.Core.Tests public class ProjectModelTests { private MessageBroker _broker = new MessageBroker(); - private List _messages = new List(); + private List _messages = new List(); [TestInitialize()] public void Setup() { _broker = new MessageBroker(); - _messages = new List(); - _broker.Subscribe(request => { lock (_messages) { _messages.Add(request); } }); + _messages = new List(); + _broker.Subscribe(request => { lock (_messages) { _messages.Add(request); } }); } [TestMethod()] @@ -41,6 +41,8 @@ public async Task AProjectNoneOfWhoseDatasetsLoadsIsNotOpened() { } Assert.AreEqual("Dataset not loaded", _messages.Single().Caption); + Assert.AreEqual(MessageSeverity.Error, _messages[0].Severity); + Assert.IsFalse(_messages[0].AsksYesNo); StringAssert.Contains(_messages[0].Content, "\"Dataset1.mddata\""); StringAssert.Contains(_messages[0].Content, "InvalidDataException: Dataset1.mddata is corrupt"); } @@ -86,6 +88,39 @@ public void ThePickerAsksForACompleteCopyWhenAFileBesideTheDatasetIsMissing() { MessageHelper.LocateDatasetFileTitle("Dataset1.mddata", Path.Combine("Folder", "Dataset1_Loaded.msp2.dbs"))); } + // The picker is the view's: reading the datasets asks for it through the broker, after the message about the same dataset. + [TestMethod()] + public async Task ThePickerIsAskedForThroughTheBrokerAfterTheMessage() { + var missing = Path.Combine("Folder", "Dataset1_Loaded.msp2.dbs"); + var serializer = new FakeSerializer(title => new FileNotFoundException($"Could not find file '{missing}'.", missing)); + var stream = await SaveProjectAsync(serializer, "Dataset1.mddata"); + var pickers = new List<(OpenFileRequest Request, int MessagesBefore)>(); + // Nothing is picked, as when the picker is cancelled. + _broker.Subscribe(request => { lock (_messages) { pickers.Add((request, _messages.Count)); } }); + + using (var streamManager = ZipStreamManager.OpenGet(stream)) { + await Assert.ThrowsExactlyAsync(() => ProjectModel.ReadDatasetsAsync(streamManager, serializer, _ => null!, "Folder", + (parameter, missingPath) => ProjectModel.LocateDatasetFileAsync(_broker, parameter, missingPath), + _broker)); + } + + var (request, messagesBefore) = pickers.Single(); + Assert.AreEqual(1, messagesBefore); + Assert.AreEqual(MessageHelper.LocateDatasetFileTitle("Dataset1.mddata", missing), request.Title); + Assert.AreEqual("Dataset file(.mddata)|*.mddata|All(*)|*", request.Filter); + Assert.IsTrue(request.RestoreDirectory); + } + + [TestMethod()] + public async Task LocatingReturnsTheFilePickedInTheView() { + var picked = Path.Combine("Elsewhere", "Dataset1.mddata"); + _broker.Subscribe(request => request.Run(picked)); + + var actual = await ProjectModel.LocateDatasetFileAsync(_broker, new ProjectBaseParameter { ProjectFileName = "Dataset1.mddata", }, null); + + Assert.AreEqual(picked, actual); + } + [TestMethod()] public async Task AProjectOpensWithTheDatasetsThatLoad() { var serializer = new FakeSerializer(title => title == "Dataset1.mddata" ? new InvalidDataException($"{title} is corrupt") : null); diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Export/ExportFailureTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Export/ExportFailureTests.cs index d640cf8ea..30b4d3596 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Export/ExportFailureTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Export/ExportFailureTests.cs @@ -1,7 +1,11 @@ using CompMs.App.Msdial.Model.Loader; +using CompMs.App.Msdial.Model.Service; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reactive.Bindings.Notifiers; using System; +using System.Collections.Generic; using System.IO; +using System.Linq; namespace CompMs.App.Msdial.Model.Export.Tests { @@ -38,5 +42,20 @@ public void APlainIOExceptionIsStillAboutWriting() { StringAssert.StartsWith(description, "MS-DIAL could not write the exported files."); } + + [TestMethod()] + public void AFailureIsReportedAsAnErrorMessage() { + var broker = new MessageBroker(); + var requests = new List(); + broker.Subscribe(requests.Add); + var error = new IOException("There is not enough space on the disk."); + + ExportFailure.Report(broker, error); + + Assert.AreEqual("Export failed", requests.Single().Caption); + Assert.AreEqual(ExportFailure.Describe(error), requests[0].Content); + Assert.AreEqual(MessageSeverity.Error, requests[0].Severity); + Assert.IsFalse(requests[0].AsksYesNo); + } } } diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs index 1a9fc2981..d077cf4fa 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs @@ -1,14 +1,13 @@ using CompMs.App.Msdial.Model.Core; using CompMs.App.Msdial.Model.DataObj; using CompMs.App.Msdial.Model.DataObj.Tests; -using CompMs.App.Msdial.ViewModel.Service; +using CompMs.MsdialCore.DataObj; using Microsoft.VisualStudio.TestTools.UnitTesting; using Reactive.Bindings.Notifiers; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Windows; namespace CompMs.App.Msdial.Model.Service.Tests { @@ -44,16 +43,16 @@ public void FormatPathsListsTenAndCountsTheRest() { [TestMethod()] public void ConfirmOpenAsksNothingWhenNothingIsMissing() { var broker = new MessageBroker(); - var requests = Listen(broker, MessageBoxResult.No); + var requests = Listen(broker, false); Assert.IsTrue(RawDataGuard.ConfirmOpen(broker, BrowsingWithoutRawData.Supported, new AnalysisFileBeanModel[0], 3)); Assert.AreEqual(0, requests.Count); } [TestMethod()] - [DataRow(MessageBoxResult.Yes, true)] - [DataRow(MessageBoxResult.No, false)] - public void ConfirmOpenForLcmsAsksYesOrNo(MessageBoxResult answer, bool expected) { + [DataRow(true, true)] + [DataRow(false, false)] + public void ConfirmOpenForLcmsAsksYesOrNo(bool answer, bool expected) { var broker = new MessageBroker(); var requests = Listen(broker, answer); var missing = Missing("a", "b"); @@ -61,8 +60,9 @@ public void ConfirmOpenForLcmsAsksYesOrNo(MessageBoxResult answer, bool expected Assert.AreEqual(expected, RawDataGuard.ConfirmOpen(broker, BrowsingWithoutRawData.Supported, missing, 4)); Assert.AreEqual(1, requests.Count); - Assert.AreEqual(MessageBoxButton.YesNo, requests[0].ButtonType); - Assert.AreEqual(MessageBoxImage.Warning, requests[0].Icon); + Assert.AreEqual("Raw data not found", requests[0].Caption); + Assert.IsTrue(requests[0].AsksYesNo); + Assert.AreEqual(MessageSeverity.Warning, requests[0].Severity); StringAssert.StartsWith(requests[0].Content, "Raw data are not available for 2 of 4 samples in this dataset, so only the information saved in the project will be drawn for them."); StringAssert.Contains(requests[0].Content, $" {missing[0].AnalysisFilePath}\n {missing[1].AnalysisFilePath}"); StringAssert.EndsWith(requests[0].Content, "Continue?"); @@ -71,23 +71,24 @@ public void ConfirmOpenForLcmsAsksYesOrNo(MessageBoxResult answer, bool expected [TestMethod()] public void ConfirmOpenForAlignmentsOnlySaysTheViewerReadsTheRawData() { var broker = new MessageBroker(); - var requests = Listen(broker, MessageBoxResult.Yes); + var requests = Listen(broker, true); Assert.IsTrue(RawDataGuard.ConfirmOpen(broker, BrowsingWithoutRawData.AlignmentsOnly, Missing("a"), 2)); - Assert.AreEqual(MessageBoxButton.YesNo, requests.Single().ButtonType); + Assert.IsTrue(requests.Single().AsksYesNo); + Assert.AreEqual(MessageSeverity.Warning, requests[0].Severity); StringAssert.Contains(requests[0].Content, "For this type of project the peak spot viewer reads the raw data"); } [TestMethod()] public void ConfirmOpenForImagingRefuses() { var broker = new MessageBroker(); - var requests = Listen(broker, MessageBoxResult.Yes); + var requests = Listen(broker, true); Assert.IsFalse(RawDataGuard.ConfirmOpen(broker, BrowsingWithoutRawData.NotSupported, Missing("a"), 1)); - Assert.AreEqual(MessageBoxButton.OK, requests.Single().ButtonType); - Assert.AreEqual(MessageBoxImage.Error, requests[0].Icon); + Assert.IsFalse(requests.Single().AsksYesNo); + Assert.AreEqual(MessageSeverity.Error, requests[0].Severity); } [TestMethod()] @@ -100,7 +101,7 @@ public void ConfirmOpenWithoutAnyoneToAnswerDeclines() { [TestMethod()] public void EnsureSaysWhyOnlyWhenTheRawDataAreMissing() { var broker = new MessageBroker(); - var requests = Listen(broker, MessageBoxResult.OK); + var requests = Listen(broker, true); var file = Missing("a")[0]; Assert.IsTrue(RawDataGuard.Ensure(true, file, broker, "Save raw spectra")); @@ -108,13 +109,16 @@ public void EnsureSaysWhyOnlyWhenTheRawDataAreMissing() { Assert.IsFalse(RawDataGuard.Ensure(false, file, broker, "Save raw spectra")); Assert.AreEqual(1, requests.Count); + Assert.AreEqual("Raw data not available", requests[0].Caption); + Assert.IsFalse(requests[0].AsksYesNo); + Assert.AreEqual(MessageSeverity.Information, requests[0].Severity); StringAssert.StartsWith(requests[0].Content, "Save raw spectra needs the raw data, and the raw data file of a was not found:"); } [TestMethod()] public void EnsureAllChecksEveryFileAgain() { var broker = new MessageBroker(); - var requests = Listen(broker, MessageBoxResult.OK); + var requests = Listen(broker, true); var path = Path.Combine(_folder, "present.mzML"); var file = AnalysisFileBeanModelTestHelper.Create(); file.AnalysisFileName = "present"; @@ -128,12 +132,41 @@ public void EnsureAllChecksEveryFileAgain() { var missing = Missing("a", "b"); Assert.IsFalse(RawDataGuard.EnsureAll(missing.Prepend(file), broker, "Alignment")); Assert.AreEqual(1, requests.Count); + Assert.AreEqual("Raw data not available", requests[0].Caption); + Assert.IsFalse(requests[0].AsksYesNo); + Assert.AreEqual(MessageSeverity.Information, requests[0].Severity); StringAssert.StartsWith(requests[0].Content, "Alignment cannot run because the raw data files of 2 of 3 samples were not found."); } - private static List Listen(MessageBroker broker, MessageBoxResult answer) { - var requests = new List(); - broker.Subscribe(request => { + // The refusals and the open failures keep the caption, the OK button and the icon they had as ErrorMessageBoxRequests. + [TestMethod()] + public void RefusalsAreInformationAndFailuresAreErrorsWithAnOkButton() { + var file = Missing("a")[0]; + var alignment = new AlignmentFileBeanModel(new AlignmentFileBean { FileName = "alignment", }, new List()); + var error = new InvalidOperationException("broken"); + + var expected = new (MessageRequest Request, string Caption, MessageSeverity Severity)[] + { + (RawDataGuard.SampleNeedsRawData(file), "Raw data not available", MessageSeverity.Information), + (RawDataGuard.PeakListMissing(file), "Saved results not found", MessageSeverity.Information), + (RawDataGuard.MsfinderWithoutMs1(file), "Raw data not available", MessageSeverity.Information), + (RawDataGuard.SampleOpenFailed(file, error), "Sample could not be opened", MessageSeverity.Error), + (RawDataGuard.AlignmentOpenFailed(alignment, error), "Alignment result could not be opened", MessageSeverity.Error), + }; + + foreach (var (request, caption, severity) in expected) { + Assert.AreEqual(caption, request.Caption); + Assert.AreEqual(severity, request.Severity, caption); + Assert.IsFalse(request.AsksYesNo, caption); + } + } + + /// + /// Keeps what is published, and answers it as the view does: true for Yes or OK, false for No. + /// + private static List Listen(MessageBroker broker, bool answer) { + var requests = new List(); + broker.Subscribe(request => { requests.Add(request); request.Result = answer; }); diff --git a/tests/MSDIAL5/MsdialGuiAppTests/View/Core/MessageRequestBoxTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/View/Core/MessageRequestBoxTests.cs new file mode 100644 index 000000000..53b73504b --- /dev/null +++ b/tests/MSDIAL5/MsdialGuiAppTests/View/Core/MessageRequestBoxTests.cs @@ -0,0 +1,42 @@ +using CompMs.App.Msdial.Model.Service; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Windows; + +namespace CompMs.App.Msdial.View.Core.Tests +{ + [TestClass()] + public class MessageRequestBoxTests + { + // A message a model publishes is shown with the buttons it asks for and the icon of its severity, + // as it was when the model set them on an ErrorMessageBoxRequest. + [TestMethod()] + public void AMessageIsShownWithItsButtonsAndTheIconOfItsSeverity() { + Assert.AreEqual(MessageBoxButton.YesNo, MessageRequestBox.ButtonsOf(new MessageRequest { AsksYesNo = true, })); + Assert.AreEqual(MessageBoxButton.OK, MessageRequestBox.ButtonsOf(new MessageRequest { AsksYesNo = false, })); + + Assert.AreEqual(MessageBoxImage.Information, MessageRequestBox.IconOf(new MessageRequest { Severity = MessageSeverity.Information, })); + Assert.AreEqual(MessageBoxImage.Warning, MessageRequestBox.IconOf(new MessageRequest { Severity = MessageSeverity.Warning, })); + Assert.AreEqual(MessageBoxImage.Error, MessageRequestBox.IconOf(new MessageRequest { Severity = MessageSeverity.Error, })); + } + + // What a model leaves unset is what an ErrorMessageBoxRequest showed: the caption "Error", an OK button and the error icon. + [TestMethod()] + public void AMessageWithNothingSetIsAnErrorWithAnOkButton() { + var request = new MessageRequest(); + + Assert.AreEqual("Error", request.Caption); + Assert.AreEqual(MessageBoxButton.OK, MessageRequestBox.ButtonsOf(request)); + Assert.AreEqual(MessageBoxImage.Error, MessageRequestBox.IconOf(request)); + Assert.IsNull(request.Result); + } + + [TestMethod()] + public void OnlyYesOrOkIsAnAcceptance() { + Assert.IsTrue(MessageRequestBox.IsAccepted(MessageBoxResult.Yes)); + Assert.IsTrue(MessageRequestBox.IsAccepted(MessageBoxResult.OK)); + Assert.IsFalse(MessageRequestBox.IsAccepted(MessageBoxResult.No)); + Assert.IsFalse(MessageRequestBox.IsAccepted(MessageBoxResult.Cancel)); + Assert.IsFalse(MessageRequestBox.IsAccepted(MessageBoxResult.None)); + } + } +} diff --git a/tests/MSDIAL5/MsdialGuiAppTests/ViewModel/Core/MethodViewModelTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/ViewModel/Core/MethodViewModelTests.cs index 34ca02c07..34671ab66 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/ViewModel/Core/MethodViewModelTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/ViewModel/Core/MethodViewModelTests.cs @@ -1,9 +1,9 @@ using CompMs.App.Msdial.Model.Core; using CompMs.App.Msdial.Model.DataObj; using CompMs.App.Msdial.Model.DataObj.Tests; +using CompMs.App.Msdial.Model.Service; using CompMs.App.Msdial.ViewModel.Dims; using CompMs.App.Msdial.ViewModel.Gcms; -using CompMs.App.Msdial.ViewModel.Service; using CompMs.Common.Enum; using CompMs.CommonMVVM; using CompMs.MsdialCore.DataObj; @@ -25,15 +25,15 @@ public class MethodViewModelTests { private string _folder = string.Empty; private MessageBroker _broker = new MessageBroker(); - private List _messages = new List(); + private List _messages = new List(); [TestInitialize()] public void Setup() { _folder = Path.Combine(Path.GetTempPath(), $"msdial-methodviewmodel-{Guid.NewGuid():N}"); Directory.CreateDirectory(_folder); _broker = new MessageBroker(); - _messages = new List(); - _broker.Subscribe(_messages.Add); + _messages = new List(); + _broker.Subscribe(_messages.Add); } [TestCleanup()]