From 8f8be42e356d68cbf44ccec498fc8a5515aebb0b Mon Sep 17 00:00:00 2001 From: "DESKTOP-382ETUR\\Hiroshi Tsugawa" Date: Fri, 16 Oct 2026 00:37:34 +0900 Subject: [PATCH 1/4] Let models say what they need to through a message request free of WPF The raw-data guard, MethodModelBase, ProjectModel and ExportFailure are models, and they published ErrorMessageBoxRequest, which lives in ViewModel.Service and carries MessageBoxButton, MessageBoxImage and MessageBoxResult. RawDataGuard also imported System.Windows for them. So a model referred to a view model namespace and to WPF types, the wrong way round for View -> ViewModel -> Model. MessageRequest, in Model.Service beside ShortMessageRequest and OpenFileRequest, says the same things without them: Caption, Content, a MessageSeverity (Information, Warning, Error), AsksYesNo, and a bool? Result that is true for Yes or OK. MainWindow subscribes to it and shows it as ShowErrorConfirmationMessage did: through the dispatcher, Yes/No or OK, the icon of the severity, and no owner. Every message keeps its text, caption, buttons and icon: - ConfirmOpen still asks Yes or No with the warning icon, and opens only on Yes; with nobody to answer it still declines (Result stays null). - The imaging refusal, Ensure, EnsureAll and the sample, peak list, alignment and MS-FINDER messages keep OK and their icons. - "Dataset not loaded" and "Export failed" keep OK and the error icon. ErrorMessageBoxRequest goes back to what it was before Icon was added for these messages: the authors' setting models that publish it never set Icon, and its handler shows the error icon again, as it did. The tests listen for MessageRequest and answer with true or false, and pin caption, severity and buttons where they pinned them before. A new test pins what ExportFailure.Report publishes. Co-Authored-By: Claude Opus 5.5 --- .../Model/Core/MethodModelBase.cs | 3 +- .../MsdialGuiApp/Model/Core/ProjectModel.cs | 6 +- .../Model/Export/ExportFailure.cs | 4 +- .../Model/Service/MessageRequest.cs | 35 +++++++++ .../Model/Service/RawDataGuard.cs | 72 ++++++++----------- .../MsdialGuiApp/View/Core/MainWindow.xaml.cs | 21 +++++- .../Service/ErrorMessageBoxRequest.cs | 2 - .../Model/Core/DatasetModelTests.cs | 25 ++++--- .../Model/Core/MethodModelBaseTests.cs | 8 +-- .../Model/Core/ProjectModelTests.cs | 8 +-- .../Model/Export/ExportFailureTests.cs | 19 +++++ .../Model/Service/RawDataGuardTests.cs | 42 ++++++----- .../ViewModel/Core/MethodViewModelTests.cs | 8 +-- 13 files changed, 159 insertions(+), 94 deletions(-) create mode 100644 src/MSDIAL5/MsdialGuiApp/Model/Service/MessageRequest.cs 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..ada7974d8 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs @@ -1,6 +1,6 @@ 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; @@ -198,11 +198,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) { 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..6e8fb76b5 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() @@ -363,10 +365,27 @@ 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, + request.AsksYesNo ? MessageBoxButton.YesNo : MessageBoxButton.OK, + request.Severity switch + { + MessageSeverity.Information => MessageBoxImage.Information, + MessageSeverity.Warning => MessageBoxImage.Warning, + _ => MessageBoxImage.Error, + }); + request.Result = 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..214eedb68 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()] 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..3241d1564 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs @@ -1,14 +1,12 @@ 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 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 +42,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 +59,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 +70,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 +100,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 +108,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"; @@ -131,9 +134,12 @@ public void EnsureAllChecksEveryFileAgain() { 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 => { + /// + /// 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/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()] From 6777a7218ef0264ff4f672f22a2590473e59fffb Mon Sep 17 00:00:00 2001 From: "DESKTOP-382ETUR\\Hiroshi Tsugawa" Date: Fri, 16 Oct 2026 00:39:43 +0900 Subject: [PATCH 2/4] Ask the view for the dataset file picker through the broker ProjectModel.LocateDatasetFileAsync opened a Microsoft.Win32 OpenFileDialog itself, through Application.Current.Dispatcher: a model building a dialog. It now publishes an OpenFileRequest on the broker that reading the datasets already has, as MainWindowModel does for the project picker, with the same filter, title (MessageHelper.LocateDatasetFileTitle) and RestoreDirectory, and returns the file picked, or null when none was. The request is published from the thread that reads the datasets (the Task.Run in LoadMdprojectAsync), so MainWindow's OpenFileDialog handler now shows the dialog through Dispatcher.Invoke, as its message handlers do. On the UI thread, where every other OpenFileRequest comes from, Invoke runs the dialog inline, so nothing changes for them. The reading thread waits for the answer instead of awaiting it, which is how the dataset messages before the picker are already shown. Two tests: through ReadDatasetsAsync, the picker is asked for after the message about the same dataset, with its title and filter, and cancelling still refuses the project; and the file picked in the view is returned. Co-Authored-By: Claude Opus 5.5 --- .../MsdialGuiApp/Model/Core/ProjectModel.cs | 32 +++++++++--------- .../MsdialGuiApp/View/Core/MainWindow.xaml.cs | 17 ++++++---- .../Model/Core/ProjectModelTests.cs | 33 +++++++++++++++++++ 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs b/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs index ada7974d8..f8d88a4c7 100644 --- a/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs +++ b/src/MSDIAL5/MsdialGuiApp/Model/Core/ProjectModel.cs @@ -7,7 +7,6 @@ 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) { @@ -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/View/Core/MainWindow.xaml.cs b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs index 6e8fb76b5..fe10e773f 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs +++ b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs @@ -343,14 +343,17 @@ private void GetSaveFilePath(SaveFileNameRequest request) { } private void OpenFileDialog(OpenFileRequest request) { - var ofd = new OpenFileDialog() { - Title = request.Title, - Filter = request.Filter, - }; + // A model can ask from off the UI thread (the dataset picker while a project is read); on it, this runs inline. + 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) { diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs index 214eedb68..f64d6f324 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs @@ -86,6 +86,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); From 5d4414c9d3836787ca77ad798f0a8ebaa8c638e9 Mon Sep 17 00:00:00 2001 From: "DESKTOP-382ETUR\\Hiroshi Tsugawa" Date: Fri, 16 Oct 2026 00:53:42 +0900 Subject: [PATCH 3/4] Write the file picker handler as the 5.6 line already has it MainWindow.OpenFileDialog now shows the dialog through Dispatcher.Invoke so that the dataset picker can be asked for from the thread that reads the datasets. improve/dynamic-rawdata-loading wraps the same handler in Dispatcher.Invoke too, but with the brace of the initializer on its own line and without the comment. Written differently, the two sides changed the same lines and the next merge of master into that line conflicts in MainWindow.xaml.cs, where it does not conflict today. Written as it is there, the change is the same on both sides and merges cleanly. Why the request can come from off the UI thread is said where it is asked, in ProjectModel.LocateDatasetFileAsync. Co-Authored-By: Claude Opus 5.5 --- src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs index fe10e773f..d864d8d7b 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs +++ b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs @@ -343,9 +343,9 @@ private void GetSaveFilePath(SaveFileNameRequest request) { } private void OpenFileDialog(OpenFileRequest request) { - // A model can ask from off the UI thread (the dataset picker while a project is read); on it, this runs inline. Dispatcher.Invoke(() => { - var ofd = new OpenFileDialog() { + var ofd = new OpenFileDialog() + { Title = request.Title, Filter = request.Filter, }; From 49454e2e7d275b3e3b1e966d3c1c426465a100ea Mon Sep 17 00:00:00 2001 From: "DESKTOP-382ETUR\\Hiroshi Tsugawa" Date: Fri, 16 Oct 2026 00:53:56 +0900 Subject: [PATCH 4/4] Pin the buttons and icons a message request is shown with What keeps every message looking as it did is the view's mapping from a MessageRequest to a message box: AsksYesNo to Yes/No or OK, the severity to the information, warning or error icon, and Yes or OK to a Result of true. It sat inline in MainWindow.ShowMessage, where no test reaches it (the test project cannot bind to MainWindow, a RibbonWindow), so swapping two icons went unnoticed. It moves, unchanged, to MessageRequestBox beside MainWindow, and MessageRequestBoxTests pins it, including that a request with nothing set is shown as an ErrorMessageBoxRequest was: "Error", OK, error icon. The model tests also pin, for the messages that only had their caption or text pinned, the buttons and the severity they had as ErrorMessageBoxRequests: the sample, peak list, MS-FINDER, sample-open and alignment-open messages of RawDataGuard, EnsureAll, and "Dataset not loaded". Co-Authored-By: Claude Opus 5.5 --- .../MsdialGuiApp/View/Core/MainWindow.xaml.cs | 13 +----- .../View/Core/MessageRequestBox.cs | 33 +++++++++++++++ .../Model/Core/ProjectModelTests.cs | 2 + .../Model/Service/RawDataGuardTests.cs | 27 ++++++++++++ .../View/Core/MessageRequestBoxTests.cs | 42 +++++++++++++++++++ 5 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 src/MSDIAL5/MsdialGuiApp/View/Core/MessageRequestBox.cs create mode 100644 tests/MSDIAL5/MsdialGuiAppTests/View/Core/MessageRequestBoxTests.cs diff --git a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs index d864d8d7b..f0a18227b 100644 --- a/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs +++ b/src/MSDIAL5/MsdialGuiApp/View/Core/MainWindow.xaml.cs @@ -378,17 +378,8 @@ 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. - var result = MessageBox.Show( - request.Content, - request.Caption, - request.AsksYesNo ? MessageBoxButton.YesNo : MessageBoxButton.OK, - request.Severity switch - { - MessageSeverity.Information => MessageBoxImage.Information, - MessageSeverity.Warning => MessageBoxImage.Warning, - _ => MessageBoxImage.Error, - }); - request.Result = result == MessageBoxResult.Yes || result == MessageBoxResult.OK; + 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/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs index f64d6f324..5d41f9d87 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Core/ProjectModelTests.cs @@ -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"); } diff --git a/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs b/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs index 3241d1564..d077cf4fa 100644 --- a/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs +++ b/tests/MSDIAL5/MsdialGuiAppTests/Model/Service/RawDataGuardTests.cs @@ -1,6 +1,7 @@ using CompMs.App.Msdial.Model.Core; using CompMs.App.Msdial.Model.DataObj; using CompMs.App.Msdial.Model.DataObj.Tests; +using CompMs.MsdialCore.DataObj; using Microsoft.VisualStudio.TestTools.UnitTesting; using Reactive.Bindings.Notifiers; using System; @@ -131,9 +132,35 @@ 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."); } + // 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. /// 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)); + } + } +}