From 9a2d3936ac426d8f155f5faddd34e97cb6f88941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Mon, 2 Sep 2024 14:19:26 +0100 Subject: [PATCH] Implemented overlay dialogs. --- samples/Movere.Sample/App.axaml.cs | 28 ++---- .../ViewModels/MainWindowViewModel.cs | 91 ++++++++++++------- samples/Movere.Sample/Views/MainWindow.axaml | 21 ++++- .../Avalonia/Services/DialogHostBase.cs | 58 ++++++++++++ .../Avalonia/Services/OverlayDialogHost.cs | 74 +++++++++++++++ .../Avalonia/Services/WindowDialogHost.cs | 33 +++++++ src/Movere/Models/OpenFileDialogOptions.cs | 7 +- src/Movere/Models/PrintDialogOptions.cs | 5 + src/Movere/Models/SaveFileDialogOptions.cs | 7 +- src/Movere/MovereStorageProviderOptions.cs | 5 + src/Movere/MovereSystemDialogImpl.cs | 74 +++++---------- src/Movere/Services/ContentDialogService.cs | 50 ++++------ src/Movere/Services/DialogView.cs | 16 ---- src/Movere/Services/IDialogHost.cs | 13 +++ src/Movere/Services/IDialogView.cs | 2 +- src/Movere/Services/IMovereDialogHost.cs | 9 ++ src/Movere/Services/MessageDialogService.cs | 16 +--- src/Movere/Services/OpenFileDialogService.cs | 43 +++------ src/Movere/Services/PrintDialogService.cs | 32 ++++--- src/Movere/Services/SaveFileDialogService.cs | 47 +++------- src/Movere/Services/ServicesModule.cs | 4 - src/Movere/Services/ViewResolver.cs | 21 ++++- src/Movere/Storage/MovereStorageProvider.cs | 81 ++++++----------- .../Storage/MovereStorageProviderFactory.cs | 15 ++- .../ViewModels/DesignDialogWindowViewModel.cs | 19 ++++ .../ViewModels/IDialogWindowViewModel.cs | 7 +- .../InternalDialogWindowViewModel.cs | 2 +- .../ViewModels/OpenFileDialogViewModel.cs | 24 +++-- src/Movere/ViewModels/PrintDialogViewModel.cs | 25 +++-- .../ViewModels/SaveFileDialogViewModel.cs | 25 +++-- src/Movere/Views/DialogOverlay.axaml | 65 +++++++++++++ src/Movere/Views/DialogOverlay.axaml.cs | 35 +++++++ src/Movere/Views/OpenFileDialog.axaml | 30 +++--- src/Movere/Views/OpenFileDialog.axaml.cs | 10 +- src/Movere/Views/PrintDialog.axaml | 28 +++--- src/Movere/Views/PrintDialog.axaml.cs | 10 +- src/Movere/Views/SaveFileDialog.axaml | 30 +++--- src/Movere/Views/SaveFileDialog.axaml.cs | 10 +- src/Movere/Views/ViewsModule.cs | 6 ++ 39 files changed, 676 insertions(+), 402 deletions(-) create mode 100644 src/Movere/Avalonia/Services/DialogHostBase.cs create mode 100644 src/Movere/Avalonia/Services/OverlayDialogHost.cs create mode 100644 src/Movere/Avalonia/Services/WindowDialogHost.cs delete mode 100644 src/Movere/Services/DialogView.cs create mode 100644 src/Movere/Services/IDialogHost.cs create mode 100644 src/Movere/Services/IMovereDialogHost.cs create mode 100644 src/Movere/ViewModels/DesignDialogWindowViewModel.cs create mode 100644 src/Movere/Views/DialogOverlay.axaml create mode 100644 src/Movere/Views/DialogOverlay.axaml.cs diff --git a/samples/Movere.Sample/App.axaml.cs b/samples/Movere.Sample/App.axaml.cs index fd1d85e..0eac54d 100644 --- a/samples/Movere.Sample/App.axaml.cs +++ b/samples/Movere.Sample/App.axaml.cs @@ -11,9 +11,9 @@ using Avalonia.Platform.Storage; using Avalonia.ReactiveUI; +using Movere.Avalonia.Services; using Movere.Sample.ViewModels; using Movere.Sample.Views; -using Movere.Services; using Movere.Storage; using Movere.Win32; @@ -31,29 +31,21 @@ public override void OnFrameworkInitializationCompleted() { var mainWindow = new MainWindow(); - var messageDialogService = new MessageDialogService(mainWindow); + var customContentViewResolver = new CustomContentViewResolver(); - var contentDialogService = new ContentDialogService( - mainWindow, - new CustomContentViewResolver()); - - var openFileDialogService = new OpenFileDialogService(mainWindow); - var saveFileDialogService = new SaveFileDialogService(mainWindow); - - var printDialogService = new PrintDialogService(mainWindow); + var windowHost = new WindowDialogHost(this, mainWindow, customContentViewResolver); + var overlayHost = new OverlayDialogHost(this, mainWindow, customContentViewResolver); mainWindow.DataContext = new MainWindowViewModel( + windowHost, + overlayHost, () => AvaloniaOpenFile(mainWindow), () => AvaloniaSaveFile(mainWindow), #pragma warning disable CS0612 // Type or member is obsolete () => AvaloniaOldOpenFile(mainWindow), - () => AvaloniaOldSaveFile(mainWindow), + () => AvaloniaOldSaveFile(mainWindow) #pragma warning restore CS0612 // Type or member is obsolete - messageDialogService, - contentDialogService, - openFileDialogService, - saveFileDialogService, - printDialogService); + ); desktop.MainWindow = mainWindow; } @@ -78,7 +70,7 @@ private static AppBuilder BuildAvaloniaApp() => .UseMovereWin32() .UseReactiveUI(); - private static Task AvaloniaOpenFile(Window parent) + private static Task AvaloniaOpenFile(TopLevel parent) { var options = new FilePickerOpenOptions() { @@ -93,7 +85,7 @@ private static Task AvaloniaOpenFile(Window parent) return parent.StorageProvider.OpenFilePickerAsync(options); } - private static Task AvaloniaSaveFile(Window parent) + private static Task AvaloniaSaveFile(TopLevel parent) { var options = new FilePickerSaveOptions() { diff --git a/samples/Movere.Sample/ViewModels/MainWindowViewModel.cs b/samples/Movere.Sample/ViewModels/MainWindowViewModel.cs index 85b7f8f..01f9c72 100644 --- a/samples/Movere.Sample/ViewModels/MainWindowViewModel.cs +++ b/samples/Movere.Sample/ViewModels/MainWindowViewModel.cs @@ -25,49 +25,57 @@ internal enum FormResult internal class MainWindowViewModel : ReactiveObject { - private readonly MessageDialogService _messageDialogService; - private readonly ContentDialogService _contentDialogService; + private sealed class DialogServices(IDialogHost host) + { + public IMessageDialogService Message { get; } = + new MessageDialogService(host); + + public IContentDialogService Content { get; } = + new ContentDialogService(host); - private readonly OpenFileDialogService _openFileDialogService; - private readonly SaveFileDialogService _saveFileDialogService; + public IOpenFileDialogService OpenFile { get; } = + new OpenFileDialogService(host); - private readonly PrintDialogService _printDialogService; + public ISaveFileDialogService SaveFile { get; } = + new SaveFileDialogService(host); + + public IPrintDialogService Print { get; } = + new PrintDialogService(host); + } + + private readonly DialogServices _windowDialogServices; + private readonly DialogServices _overlayDialogServices; private string _messageDialogResult = "Not opened yet"; private string _contentDialogResult = "Not opened yet"; + private bool _useOverlayDialogs = false; + public MainWindowViewModel( + IDialogHost windowHost, + IDialogHost overlayHost, Func avaloniaOpenFile, Func avaloniaSaveFile, Func avaloniaOldOpenFile, - Func avaloniaOldSaveFile, - MessageDialogService messageDialogService, - ContentDialogService contentDialogService, - OpenFileDialogService openFileDialogService, - SaveFileDialogService saveFileDialogService, - PrintDialogService printDialogService) + Func avaloniaOldSaveFile + ) { - _messageDialogService = messageDialogService; - _contentDialogService = contentDialogService; - - _openFileDialogService = openFileDialogService; - _saveFileDialogService = saveFileDialogService; + _windowDialogServices = new DialogServices(windowHost); + _overlayDialogServices = new DialogServices(overlayHost); - _printDialogService = printDialogService; + ShowMessageCommand = ReactiveCommand.CreateFromTask(ShowMessageAsync); + ShowCustomContentCommand = ReactiveCommand.CreateFromTask(ShowCustomContentAsync); - ShowMessageCommand = ReactiveCommand.Create(ShowMessageAsync); - ShowCustomContentCommand = ReactiveCommand.Create(ShowCustomContentAsync); + OpenFileCommand = ReactiveCommand.CreateFromTask(OpenFileAsync); + SaveFileCommand = ReactiveCommand.CreateFromTask(SaveFileAsync); - OpenFileCommand = ReactiveCommand.Create(OpenFileAsync); - SaveFileCommand = ReactiveCommand.Create(SaveFileAsync); + PrintCommand = ReactiveCommand.CreateFromTask(PrintAsync); - PrintCommand = ReactiveCommand.Create(PrintAsync); + AvaloniaOpenFileCommand = ReactiveCommand.CreateFromTask(avaloniaOpenFile); + AvaloniaSaveFileCommand = ReactiveCommand.CreateFromTask(avaloniaSaveFile); - AvaloniaOpenFileCommand = ReactiveCommand.Create(avaloniaOpenFile); - AvaloniaSaveFileCommand = ReactiveCommand.Create(avaloniaSaveFile); - - AvaloniaOldOpenFileCommand = ReactiveCommand.Create(avaloniaOldOpenFile); - AvaloniaOldSaveFileCommand = ReactiveCommand.Create(avaloniaOldSaveFile); + AvaloniaOldOpenFileCommand = ReactiveCommand.CreateFromTask(avaloniaOldOpenFile); + AvaloniaOldSaveFileCommand = ReactiveCommand.CreateFromTask(avaloniaOldSaveFile); } public string MessageDialogResult @@ -100,9 +108,20 @@ public string ContentDialogResult public ICommand AvaloniaOldSaveFileCommand { get; } + public bool UseOverlayDialogs + { + get => _useOverlayDialogs; + set => this.RaiseAndSetIfChanged(ref _useOverlayDialogs, value); + } + + private DialogServices Dialogs => + UseOverlayDialogs + ? _overlayDialogServices + : _windowDialogServices; + private async Task ShowMessageAsync() => MessageDialogResult = ( - await _messageDialogService.ShowMessageDialogAsync( + await Dialogs.Message.ShowMessageDialogAsync( new MessageDialogOptions( "Some really really really really really really really really really really really " + "really really really really really really really really really really really really " + @@ -157,7 +176,7 @@ select from value in field.WhenAnyValue(x => x.Value) ) ); - var result = await _contentDialogService.ShowDialogAsync( + var result = await Dialogs.Content.ShowDialogAsync( ContentDialogOptions.Create("Custom content", vm, DialogActionSet.Create(actions, actions[2], actions[0])) ); @@ -165,9 +184,15 @@ select from value in field.WhenAnyValue(x => x.Value) } private Task OpenFileAsync() => - _openFileDialogService.ShowDialogAsync(new OpenFileDialogOptions() { AllowMultipleSelection = true }); + Dialogs.OpenFile.ShowDialogAsync( + new OpenFileDialogOptions() + { + AllowMultipleSelection = true + } + ); - private Task SaveFileAsync() => _saveFileDialogService.ShowDialogAsync(); + private Task SaveFileAsync() => + Dialogs.SaveFile.ShowDialogAsync(); private async Task PrintAsync() { @@ -179,7 +204,9 @@ private async Task PrintAsync() using var document = new PrintDocument(); document.PrintPage += PrintDocument; - await _printDialogService.ShowDialogAsync(new PrintDialogOptions(document)); + + await Dialogs.Print + .ShowDialogAsync(new PrintDialogOptions(document)); } private static void PrintDocument(object sender, PrintPageEventArgs e) diff --git a/samples/Movere.Sample/Views/MainWindow.axaml b/samples/Movere.Sample/Views/MainWindow.axaml index 15d57e2..9bfd59d 100644 --- a/samples/Movere.Sample/Views/MainWindow.axaml +++ b/samples/Movere.Sample/Views/MainWindow.axaml @@ -1,5 +1,6 @@  - + @@ -54,6 +57,22 @@ + + + + + + diff --git a/src/Movere/Avalonia/Services/DialogHostBase.cs b/src/Movere/Avalonia/Services/DialogHostBase.cs new file mode 100644 index 0000000..963d9a6 --- /dev/null +++ b/src/Movere/Avalonia/Services/DialogHostBase.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; + +using Autofac; + +using Avalonia; +using Avalonia.Controls.Templates; + +using Movere.Services; +using Movere.ViewModels; + +namespace Movere.Avalonia.Services +{ + public abstract class DialogHostBase : IMovereDialogHost + { + private readonly IContainer _container; + + private protected DialogHostBase(Application application, IDataTemplate? dataTemplate = null) + { + var containerBuilder = new ContainerBuilder() + { + Properties = + { + ["Application"] = application + } + }; + + containerBuilder + .RegisterAssemblyModules(typeof(WindowDialogHost).Assembly); + + containerBuilder + .RegisterInstance(this); + + _container = containerBuilder.Build(); + + var resolver = _container.Resolve(); + + DataTemplate = dataTemplate is null + ? resolver + : new FuncDataTemplate( + x => dataTemplate.Match(x) || resolver.Match(x), + (x, _) => dataTemplate.Match(x) ? dataTemplate.Build(x) : resolver.Build(x) + ); + } + + protected IDataTemplate DataTemplate { get; } + + IContainer IMovereDialogHost.Container => + _container; + + public abstract IObservable ShowDialog( + Func, IDialogWindowViewModel> viewModelFactory + ); + + public ValueTask DisposeAsync() => + _container.DisposeAsync(); + } +} diff --git a/src/Movere/Avalonia/Services/OverlayDialogHost.cs b/src/Movere/Avalonia/Services/OverlayDialogHost.cs new file mode 100644 index 0000000..a266d24 --- /dev/null +++ b/src/Movere/Avalonia/Services/OverlayDialogHost.cs @@ -0,0 +1,74 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; + +using Movere.Services; +using Movere.ViewModels; +using Movere.Views; + +namespace Movere.Avalonia.Services +{ + internal sealed class OverlayDialogHost(Application application, Visual target, IDataTemplate? dataTemplate = null) + : DialogHostBase(application, dataTemplate) + { + private sealed class DialogView : IDialogView + { + private readonly OverlayLayer _layer; + private readonly DialogOverlay _overlay; + + private readonly ISubject _resultSubject = new Subject(); + + public DialogView(OverlayLayer layer, DialogOverlay overlay) + { + _layer = layer; + _overlay = overlay; + + Result = _resultSubject.AsObservable(); + } + + public IObservable Result { get; } + + public void Close(TResult result) + { + if (_overlay.OnClosing()) + { + _layer.Children.Remove(_overlay); + + _resultSubject.OnNext(result); + _resultSubject.OnCompleted(); + } + } + } + + public override IObservable ShowDialog( + Func, IDialogWindowViewModel> viewModelFactory + ) + { + var layer = OverlayLayer.GetOverlayLayer(target); + + if (layer is null) + { + return Observable.Create( + observer => + { + observer.OnError(new Exception()); + return Disposable.Empty; + } + ); + } + + var overlay = new DialogOverlay() { DataTemplates = { DataTemplate } }; + var view = new DialogView(layer, overlay); + + overlay.DataContext = viewModelFactory(view); + layer.Children.Add(overlay); + + return view.Result; + } + } +} diff --git a/src/Movere/Avalonia/Services/WindowDialogHost.cs b/src/Movere/Avalonia/Services/WindowDialogHost.cs new file mode 100644 index 0000000..3498a8b --- /dev/null +++ b/src/Movere/Avalonia/Services/WindowDialogHost.cs @@ -0,0 +1,33 @@ +using System; +using System.Reactive.Threading.Tasks; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Templates; + +using Movere.Services; +using Movere.ViewModels; +using Movere.Views; + +namespace Movere.Avalonia.Services +{ + internal sealed class WindowDialogHost(Application application, Window owner, IDataTemplate? dataTemplate = null) + : DialogHostBase(application, dataTemplate) + { + private sealed class DialogView(Window window) : IDialogView + { + public void Close(TResult result) => + window.Close(result); + } + + public override IObservable ShowDialog( + Func, IDialogWindowViewModel> viewModelFactory + ) + { + var window = new DialogWindow() { DataTemplates = { DataTemplate } }; + window.DataContext = viewModelFactory(new DialogView(window)); + + return window.ShowDialog(owner).ToObservable(); + } + } +} diff --git a/src/Movere/Models/OpenFileDialogOptions.cs b/src/Movere/Models/OpenFileDialogOptions.cs index 8c130fd..633c81f 100644 --- a/src/Movere/Models/OpenFileDialogOptions.cs +++ b/src/Movere/Models/OpenFileDialogOptions.cs @@ -1,12 +1,17 @@ using System.Collections.Generic; using System.IO; +using Movere.Resources; + namespace Movere.Models { public sealed record OpenFileDialogOptions { public static OpenFileDialogOptions Default { get; } = new OpenFileDialogOptions(); - + + public LocalizedString Title { get; init; } = + new LocalizedString(Strings.ResourceManager, nameof(Strings.OpenFile)); + public bool AllowMultipleSelection { get; init; } public IEnumerable Filters { get; init; } = []; diff --git a/src/Movere/Models/PrintDialogOptions.cs b/src/Movere/Models/PrintDialogOptions.cs index b8a4f8d..832e2b8 100644 --- a/src/Movere/Models/PrintDialogOptions.cs +++ b/src/Movere/Models/PrintDialogOptions.cs @@ -1,6 +1,8 @@ using System; using System.Drawing.Printing; +using Movere.Resources; + namespace Movere.Models { public sealed record PrintDialogOptions @@ -11,5 +13,8 @@ public PrintDialogOptions(PrintDocument document) } public PrintDocument Document { get; init; } + + public LocalizedString Title { get; init; } = + new LocalizedString(Strings.ResourceManager, nameof(Strings.Print)); } } diff --git a/src/Movere/Models/SaveFileDialogOptions.cs b/src/Movere/Models/SaveFileDialogOptions.cs index cb30f7c..7297b5d 100644 --- a/src/Movere/Models/SaveFileDialogOptions.cs +++ b/src/Movere/Models/SaveFileDialogOptions.cs @@ -1,11 +1,16 @@ using System.IO; +using Movere.Resources; + namespace Movere.Models { public sealed record SaveFileDialogOptions { public static SaveFileDialogOptions Default { get; } = new SaveFileDialogOptions(); - + + public LocalizedString Title { get; init; } = + new LocalizedString(Strings.ResourceManager, nameof(Strings.SaveFile)); + // not supported yet //public IEnumerable Filters { get; init; } = []; diff --git a/src/Movere/MovereStorageProviderOptions.cs b/src/Movere/MovereStorageProviderOptions.cs index 03d9742..73fb564 100644 --- a/src/Movere/MovereStorageProviderOptions.cs +++ b/src/Movere/MovereStorageProviderOptions.cs @@ -9,5 +9,10 @@ public sealed record MovereStorageProviderOptions /// should be queried first and, if available, used instead of Movere's implementation /// public bool IsFallback { get; init; } = true; + + /// + /// If the dialogs should be windows, when possible. Otherwise, they will be overlays. + /// + public bool PreferWindowDialogs { get; init; } = true; } } diff --git a/src/Movere/MovereSystemDialogImpl.cs b/src/Movere/MovereSystemDialogImpl.cs index e93dd2a..b3b12b0 100644 --- a/src/Movere/MovereSystemDialogImpl.cs +++ b/src/Movere/MovereSystemDialogImpl.cs @@ -4,19 +4,15 @@ using System.Linq; using System.Threading.Tasks; -using Autofac; - -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; using AvaloniaFilter = Avalonia.Controls.FileDialogFilter; +using Movere.Avalonia.Services; using Movere.Models; using Movere.Services; -using Movere.ViewModels; +using Movere.Storage; using MovereFilter = Movere.Models.FileDialogFilter; -using MovereOpenFileDialog = Movere.Views.OpenFileDialog; -using MovereSaveFileDialog = Movere.Views.SaveFileDialog; namespace Movere { @@ -25,24 +21,13 @@ internal sealed class MovereSystemDialogImpl : ISystemDialogImpl { public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { - if (dialog is OpenFileDialog openFileDialog) - { - var view = new MovereOpenFileDialog(); - - var containerBuilder = new ContainerBuilder() - { - Properties = { ["Application"] = Application.Current } - }; + var application = MovereStorageProviderFactory.GetApplication(); - containerBuilder - .RegisterAssemblyModules(typeof(MovereSystemDialogImpl).Assembly); + await using var host = new WindowDialogHost(application, parent); - containerBuilder - .RegisterInstance(view); - - using var container = containerBuilder.Build(); - - view.DataTemplates.Add(container.Resolve()); + if (dialog is OpenFileDialog openFileDialog) + { + var service = new OpenFileDialogService(host); var options = new OpenFileDialogOptions() { @@ -56,33 +41,20 @@ internal sealed class MovereSystemDialogImpl : ISystemDialogImpl InitialFileName = dialog.InitialFileName }; - var viewModelFactory = container - .Resolve>(); - - view.DataContext = viewModelFactory(options); + if (dialog.Title is { } title) + { + // no conditional assignment of init properties + // (https://github.com/dotnet/csharplang/discussions/5588) + options = options with { Title = title }; + } - var result = await view.ShowDialog(parent); - return result is null ? Array.Empty() : result.SelectedPaths.ToArray(); + var result = await service.ShowDialogAsync(options); + return result.SelectedPaths.ToArray(); } if (dialog is SaveFileDialog saveFileDialog) { - var view = new MovereSaveFileDialog(); - - var containerBuilder = new ContainerBuilder() - { - Properties = { ["Application"] = Application.Current } - }; - - containerBuilder - .RegisterAssemblyModules(typeof(MovereSystemDialogImpl).Assembly); - - containerBuilder - .RegisterInstance(view); - - using var container = containerBuilder.Build(); - - view.DataTemplates.Add(container.Resolve()); + var service = new SaveFileDialogService(host); var options = new SaveFileDialogOptions() { @@ -95,13 +67,15 @@ internal sealed class MovereSystemDialogImpl : ISystemDialogImpl InitialFileName = dialog.InitialFileName }; - var viewModelFactory = container - .Resolve>(); - - view.DataContext = viewModelFactory(options); + if (dialog.Title is { } title) + { + // no conditional assignment of init properties + // (https://github.com/dotnet/csharplang/discussions/5588) + options = options with { Title = title }; + } - var result = await view.ShowDialog(parent); - return (result is null || result.SelectedPath is null) ? Array.Empty() : new string[] { result.SelectedPath }; + var result = await service.ShowDialogAsync(options); + return result.SelectedPath is null ? [] : [result.SelectedPath]; } throw new NotImplementedException(); diff --git a/src/Movere/Services/ContentDialogService.cs b/src/Movere/Services/ContentDialogService.cs index bf21379..a14cbab 100644 --- a/src/Movere/Services/ContentDialogService.cs +++ b/src/Movere/Services/ContentDialogService.cs @@ -1,41 +1,27 @@ -using System.Threading.Tasks; - -using Avalonia.Controls; -using Avalonia.Controls.Templates; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; using Movere.ViewModels; -using Movere.Views; namespace Movere.Services { - public sealed class ContentDialogService : IContentDialogService + public sealed class ContentDialogService(IDialogHost host) + : IContentDialogService where TContent : notnull { - private readonly Window _owner; - private readonly IDataTemplate _viewResolver; - - public ContentDialogService(Window owner, IDataTemplate viewResolver) - { - _owner = owner; - - _viewResolver = new FuncDataTemplate( - x => x is IContentDialogViewModel || viewResolver.Match(x), - (x, _) => x is IContentDialogViewModel - ? new ContentDialogView() - : viewResolver.Build(x) - ); - } - - public Task ShowDialogAsync(ContentDialogOptions options) - { - var window = new DialogWindow() { DataTemplates = { _viewResolver } }; - var dialogView = new DialogView(window); - - var viewModel = ContentDialogViewModel.Create(options.Content, options.Actions); - - window.DataContext = InternalDialogWindowViewModel.Create(dialogView, options.Title, viewModel); - - return window.ShowDialog(_owner); - } + public Task ShowDialogAsync(ContentDialogOptions options) => + host + .ShowDialog( + (IDialogView view) => + InternalDialogWindowViewModel.Create( + view, + options.Title, + ContentDialogViewModel.Create( + options.Content, + options.Actions + ) + ) + ) + .ToTask(); } } diff --git a/src/Movere/Services/DialogView.cs b/src/Movere/Services/DialogView.cs deleted file mode 100644 index e1bb734..0000000 --- a/src/Movere/Services/DialogView.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Avalonia.Controls; - -namespace Movere.Services -{ - public sealed class DialogView : IDialogView - { - private readonly Window _view; - - public DialogView(Window view) - { - _view = view; - } - - public void Close(TResult result) => _view.Close(result); - } -} diff --git a/src/Movere/Services/IDialogHost.cs b/src/Movere/Services/IDialogHost.cs new file mode 100644 index 0000000..b331224 --- /dev/null +++ b/src/Movere/Services/IDialogHost.cs @@ -0,0 +1,13 @@ +using System; + +using Movere.ViewModels; + +namespace Movere.Services +{ + public interface IDialogHost : IAsyncDisposable + { + IObservable ShowDialog( + Func, IDialogWindowViewModel> viewModelFactory + ); + } +} diff --git a/src/Movere/Services/IDialogView.cs b/src/Movere/Services/IDialogView.cs index f28fad4..b851b24 100644 --- a/src/Movere/Services/IDialogView.cs +++ b/src/Movere/Services/IDialogView.cs @@ -1,6 +1,6 @@ namespace Movere.Services { - public interface IDialogView + public interface IDialogView { void Close(TResult result); } diff --git a/src/Movere/Services/IMovereDialogHost.cs b/src/Movere/Services/IMovereDialogHost.cs new file mode 100644 index 0000000..e390c08 --- /dev/null +++ b/src/Movere/Services/IMovereDialogHost.cs @@ -0,0 +1,9 @@ +using Autofac; + +namespace Movere.Services +{ + internal interface IMovereDialogHost : IDialogHost + { + IContainer Container { get; } + } +} diff --git a/src/Movere/Services/MessageDialogService.cs b/src/Movere/Services/MessageDialogService.cs index 34e4241..e1ff2ae 100644 --- a/src/Movere/Services/MessageDialogService.cs +++ b/src/Movere/Services/MessageDialogService.cs @@ -1,24 +1,14 @@ using System.Threading.Tasks; -using Avalonia.Controls; -using Avalonia.Controls.Templates; - using Movere.Models; using Movere.ViewModels; -using Movere.Views; namespace Movere.Services { - public sealed class MessageDialogService : IMessageDialogService + public sealed class MessageDialogService(IDialogHost host) : IMessageDialogService { - private static readonly IDataTemplate s_viewResolver = new FuncDataTemplate((vm, ns) => new MessageDialogView()); - - private readonly ContentDialogService _contentDialogService; - - public MessageDialogService(Window owner) - { - _contentDialogService = new ContentDialogService(owner, s_viewResolver); - } + private readonly ContentDialogService _contentDialogService = + new ContentDialogService(host); public Task ShowMessageDialogAsync(MessageDialogOptions options) => _contentDialogService.ShowDialogAsync( diff --git a/src/Movere/Services/OpenFileDialogService.cs b/src/Movere/Services/OpenFileDialogService.cs index 17e7925..3521ffc 100644 --- a/src/Movere/Services/OpenFileDialogService.cs +++ b/src/Movere/Services/OpenFileDialogService.cs @@ -1,53 +1,34 @@ using System; +using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using Autofac; -using Avalonia; -using Window = Avalonia.Controls.Window; - using Movere.Models; using Movere.ViewModels; -using Movere.Views; namespace Movere.Services { - public sealed class OpenFileDialogService : IOpenFileDialogService + public sealed class OpenFileDialogService(IDialogHost host) : IOpenFileDialogService { - private readonly Window _owner; - - public OpenFileDialogService(Window owner) - { - _owner = owner; - } - public async Task ShowDialogAsync(OpenFileDialogOptions? options = null) { options ??= OpenFileDialogOptions.Default; - var dialog = new OpenFileDialog(); - - var containerBuilder = new ContainerBuilder() - { - Properties = { ["Application"] = Application.Current } - }; - - containerBuilder - .RegisterAssemblyModules(typeof(OpenFileDialogService).Assembly); - - containerBuilder - .RegisterInstance(dialog); - - using var container = containerBuilder.Build(); - - dialog.DataTemplates.Add(container.Resolve()); + var container = (host as IMovereDialogHost)?.Container + ?? throw new InvalidOperationException( + $"{nameof(OpenFileDialogService)} only supports dialog hosts implemented by Movere!" + ); var viewModelFactory = container .Resolve>(); - dialog.DataContext = viewModelFactory(options); - - return await dialog.ShowDialog(_owner); + return await host + .ShowDialog( + view => + InternalDialogWindowViewModel.Create(view, options.Title, viewModelFactory(options)) + ) + .ToTask(); } } } diff --git a/src/Movere/Services/PrintDialogService.cs b/src/Movere/Services/PrintDialogService.cs index 1855751..39524b9 100644 --- a/src/Movere/Services/PrintDialogService.cs +++ b/src/Movere/Services/PrintDialogService.cs @@ -1,30 +1,32 @@ -using System.Threading.Tasks; +using System; +using System.Reactive.Threading.Tasks; +using System.Threading.Tasks; -using Avalonia.Controls; +using Autofac; using Movere.Models; using Movere.ViewModels; -using Movere.Views; namespace Movere.Services { - public sealed class PrintDialogService : IPrintDialogService + public sealed class PrintDialogService(IDialogHost host) : IPrintDialogService { - private readonly Window _owner; - - public PrintDialogService(Window owner) - { - _owner = owner; - } - public Task ShowDialogAsync(PrintDialogOptions options) { - var dialog = new PrintDialog(); - var viewModel = new PrintDialogViewModel(options, r => dialog.Close(r)); + var container = (host as IMovereDialogHost)?.Container + ?? throw new InvalidOperationException( + $"{nameof(PrintDialogService)} only supports dialog hosts implemented by Movere!" + ); - dialog.DataContext = viewModel; + var viewModelFactory = container + .Resolve>(); - return dialog.ShowDialog(_owner); + return host + .ShowDialog( + view => + InternalDialogWindowViewModel.Create(view, options.Title, viewModelFactory(options)) + ) + .ToTask(); } } } diff --git a/src/Movere/Services/SaveFileDialogService.cs b/src/Movere/Services/SaveFileDialogService.cs index 3db5c7e..5bc6012 100644 --- a/src/Movere/Services/SaveFileDialogService.cs +++ b/src/Movere/Services/SaveFileDialogService.cs @@ -1,53 +1,34 @@ using System; +using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using Autofac; -using Avalonia; -using Avalonia.Controls; - using Movere.Models; using Movere.ViewModels; -using SaveFileDialog = Movere.Views.SaveFileDialog; namespace Movere.Services { - public sealed class SaveFileDialogService : ISaveFileDialogService + public sealed class SaveFileDialogService(IDialogHost host) : ISaveFileDialogService { - private readonly Window _owner; - - public SaveFileDialogService(Window owner) - { - _owner = owner; - } - - public async Task ShowDialogAsync(SaveFileDialogOptions? options = null) + public Task ShowDialogAsync(SaveFileDialogOptions? options = null) { options ??= SaveFileDialogOptions.Default; - var dialog = new SaveFileDialog(); - - var containerBuilder = new ContainerBuilder() - { - Properties = { ["Application"] = Application.Current } - }; - - containerBuilder - .RegisterAssemblyModules(typeof(SaveFileDialogService).Assembly); - - containerBuilder - .RegisterInstance(dialog); - - using var container = containerBuilder.Build(); - - dialog.DataTemplates.Add(container.Resolve()); - + var container = (host as IMovereDialogHost)?.Container + ?? throw new InvalidOperationException( + $"{nameof(SaveFileDialogService)} only supports dialog hosts implemented by Movere!" + ); + var viewModelFactory = container .Resolve>(); - dialog.DataContext = viewModelFactory(options); - - return await dialog.ShowDialog(_owner); + return host + .ShowDialog( + view => + InternalDialogWindowViewModel.Create(view, options.Title, viewModelFactory(options)) + ) + .ToTask(); } } } diff --git a/src/Movere/Services/ServicesModule.cs b/src/Movere/Services/ServicesModule.cs index dad847c..9f99c32 100644 --- a/src/Movere/Services/ServicesModule.cs +++ b/src/Movere/Services/ServicesModule.cs @@ -17,10 +17,6 @@ protected override void Load(ContainerBuilder builder) .AsSelf() .AsImplementedInterfaces(); - builder - .RegisterGeneric(typeof(DialogView<>)) - .As(typeof(IDialogView<>)); - if (builder .Properties .TryGetValue("Application", out var applicationObj) diff --git a/src/Movere/Services/ViewResolver.cs b/src/Movere/Services/ViewResolver.cs index 8335e56..51edac1 100644 --- a/src/Movere/Services/ViewResolver.cs +++ b/src/Movere/Services/ViewResolver.cs @@ -17,11 +17,22 @@ public ViewResolver(IIndex> index) } public bool Match(object? data) => - data is not null && _index.TryGetValue(data.GetType(), out _); + GetFactory(data) is not null; - public Control Build(object? param) => - param is not null && _index.TryGetValue(param.GetType(), out var factory) - ? factory() - : throw new NotSupportedException(); + public Control? Build(object? param) => + GetFactory(param)?.Invoke() + ?? throw new NotSupportedException(); + + private Func? GetFactory(object? vm) => + vm is not null + && ( + _index.TryGetValue(vm.GetType(), out var factory) + || ( + vm.GetType().IsConstructedGenericType + && _index.TryGetValue(vm.GetType().GetGenericTypeDefinition(), out factory) + ) + ) + ? factory + : null; } } diff --git a/src/Movere/Storage/MovereStorageProvider.cs b/src/Movere/Storage/MovereStorageProvider.cs index baf90d3..1260584 100644 --- a/src/Movere/Storage/MovereStorageProvider.cs +++ b/src/Movere/Storage/MovereStorageProvider.cs @@ -5,29 +5,20 @@ using System.Linq; using System.Threading.Tasks; -using Autofac; - -using Avalonia.Controls; using Avalonia.Platform.Storage; using Movere.Models; using Movere.Services; -using Movere.ViewModels; using MovereFilter = Movere.Models.FileDialogFilter; -using MovereOpenFileDialog = Movere.Views.OpenFileDialog; -using MovereSaveFileDialog = Movere.Views.SaveFileDialog; namespace Movere.Storage { - internal sealed class MovereStorageProvider : BclStorageProvider + internal sealed class MovereStorageProvider( + Func hostFactory, + MovereStorageProviderOptions options + ) + : BclStorageProvider { - private readonly Window _window; - - public MovereStorageProvider(Window window) - { - _window = window; - } - public override bool CanOpen => true; public override bool CanSave => true; @@ -36,19 +27,9 @@ public MovereStorageProvider(Window window) public override async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - var view = new MovereOpenFileDialog(); - - var containerBuilder = new ContainerBuilder(); - - containerBuilder - .RegisterAssemblyModules(typeof(MovereStorageProvider).Assembly); - - containerBuilder - .RegisterInstance(view); - - await using var container = containerBuilder.Build(); + await using var host = hostFactory(); - view.DataTemplates.Add(container.Resolve()); + var service = new OpenFileDialogService(host); var convertedOptions = new OpenFileDialogOptions() { @@ -59,34 +40,25 @@ public override async Task> OpenFilePickerAsync(File InitialFileName = options.SuggestedFileName }; - var viewModelFactory = container - .Resolve>(); - - view.DataContext = viewModelFactory(convertedOptions); + if (options.Title is { } title) + { + // no conditional assignment of init properties + // (https://github.com/dotnet/csharplang/discussions/5588) + convertedOptions = convertedOptions with { Title = title }; + } - var result = await view.ShowDialog(_window); + var result = await service.ShowDialogAsync(convertedOptions); - return (result?.SelectedPaths + return result.SelectedPaths .Select(static x => new BclStorageFile(new FileInfo(x))) - .ToImmutableArray()) - ?? ImmutableArray.Empty; + .ToImmutableArray(); } public override async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - var view = new MovereSaveFileDialog(); - - var containerBuilder = new ContainerBuilder(); - - containerBuilder - .RegisterAssemblyModules(typeof(MovereStorageProvider).Assembly); + await using var host = hostFactory(); - containerBuilder - .RegisterInstance(view); - - await using var container = containerBuilder.Build(); - - view.DataTemplates.Add(container.Resolve()); + var service = new SaveFileDialogService(host); // TODO: implement FileTypeChoices and DefaultExtension // TODO: implement ShowOverwritePrompt (it's always shown) @@ -96,14 +68,19 @@ public override async Task> OpenFilePickerAsync(File InitialDirectory = TryConvertStorageFolder(options.SuggestedStartLocation, checkIfExists: true), InitialFileName = options.SuggestedFileName }; - - var viewModelFactory = container - .Resolve>(); - view.DataContext = viewModelFactory(convertedOptions); + if (options.Title is { } title) + { + // no conditional assignment of init properties + // (https://github.com/dotnet/csharplang/discussions/5588) + convertedOptions = convertedOptions with { Title = title }; + } + + var result = await service.ShowDialogAsync(convertedOptions); - var result = await view.ShowDialog(_window); - return result?.SelectedPath is null ? null : new BclStorageFile(new FileInfo(result.SelectedPath)); + return result.SelectedPath is null + ? null + : new BclStorageFile(new FileInfo(result.SelectedPath)); } public override Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) => diff --git a/src/Movere/Storage/MovereStorageProviderFactory.cs b/src/Movere/Storage/MovereStorageProviderFactory.cs index 8f76035..2c0b1ee 100644 --- a/src/Movere/Storage/MovereStorageProviderFactory.cs +++ b/src/Movere/Storage/MovereStorageProviderFactory.cs @@ -1,10 +1,13 @@ using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Platform; using Avalonia.Platform.Storage; +using Movere.Avalonia.Services; + namespace Movere.Storage { internal sealed class MovereStorageProviderFactory( @@ -24,8 +27,16 @@ public IStorageProvider CreateProvider(TopLevel topLevel) => ) ? provider : new MovereStorageProvider( - (topLevel as Window) - ?? throw new NotSupportedException("Movere is only supported on Window top levels!") + _options.PreferWindowDialogs && topLevel is Window window + ? () => new WindowDialogHost(GetApplication(), window) + : () => new OverlayDialogHost(GetApplication(), topLevel), + _options + ); + + internal static Application GetApplication() => + Application.Current + ?? throw new InvalidOperationException( + $"{nameof(Application)}.{nameof(Application.Current)} is null!" ); } } diff --git a/src/Movere/ViewModels/DesignDialogWindowViewModel.cs b/src/Movere/ViewModels/DesignDialogWindowViewModel.cs new file mode 100644 index 0000000..8ddb950 --- /dev/null +++ b/src/Movere/ViewModels/DesignDialogWindowViewModel.cs @@ -0,0 +1,19 @@ +using Movere.Models; + +namespace Movere.ViewModels +{ + internal sealed class DesignDialogWindowViewModel : IDialogWindowViewModel + { + public LocalizedString Title => + new LocalizedString("Design Dialog"); + + public object Content { get; set; } = + "Dialog Content"; + + public bool IsBusy => + false; + + public bool OnClosing() => + false; + } +} diff --git a/src/Movere/ViewModels/IDialogWindowViewModel.cs b/src/Movere/ViewModels/IDialogWindowViewModel.cs index 68710c6..9285371 100644 --- a/src/Movere/ViewModels/IDialogWindowViewModel.cs +++ b/src/Movere/ViewModels/IDialogWindowViewModel.cs @@ -2,7 +2,7 @@ namespace Movere.ViewModels { - internal interface IDialogWindowViewModel + public interface IDialogWindowViewModel { LocalizedString Title { get; } @@ -12,4 +12,9 @@ internal interface IDialogWindowViewModel bool OnClosing(); } + + public interface IDialogWindowViewModel : IDialogWindowViewModel + { + new TContent Content { get; } + } } diff --git a/src/Movere/ViewModels/InternalDialogWindowViewModel.cs b/src/Movere/ViewModels/InternalDialogWindowViewModel.cs index 9ff9391..3c04ee0 100644 --- a/src/Movere/ViewModels/InternalDialogWindowViewModel.cs +++ b/src/Movere/ViewModels/InternalDialogWindowViewModel.cs @@ -19,7 +19,7 @@ TContent content new InternalDialogWindowViewModel(view, title, content); } - internal sealed class InternalDialogWindowViewModel : ReactiveObject, IDialogWindowViewModel + internal sealed class InternalDialogWindowViewModel : ReactiveObject, IDialogWindowViewModel where TContent : IDialogContentViewModel { private readonly IDialogView _view; diff --git a/src/Movere/ViewModels/OpenFileDialogViewModel.cs b/src/Movere/ViewModels/OpenFileDialogViewModel.cs index d9091a2..3d8272f 100644 --- a/src/Movere/ViewModels/OpenFileDialogViewModel.cs +++ b/src/Movere/ViewModels/OpenFileDialogViewModel.cs @@ -3,20 +3,22 @@ using System.Collections.Immutable; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Windows.Input; using ReactiveUI; using Movere.Models; using Movere.Models.Filters; -using Movere.Services; using File = Movere.Models.File; namespace Movere.ViewModels { - internal sealed class OpenFileDialogViewModel : ReactiveObject + internal sealed class OpenFileDialogViewModel + : ReactiveObject, IDialogContentViewModel { - private readonly IDialogView _view; + private readonly ISubject> _resultSubject = + new Subject>(); private string _fileName; @@ -24,12 +26,9 @@ internal sealed class OpenFileDialogViewModel : ReactiveObject public OpenFileDialogViewModel( OpenFileDialogOptions options, - IDialogView view, Func>, FileExplorerViewModel> fileExplorerFactory ) { - _view = view; - Filters = options.Filters.Select(FileDialogFilterViewModel.New).ToImmutableArray(); SelectedFilter = Filters.FirstOrDefault(); @@ -52,6 +51,8 @@ static IFilter FileFilterMatches(FileDialogFilterViewModel? x) OpenCommand = ReactiveCommand.Create(Open); CancelCommand = ReactiveCommand.Create(Cancel); + Result = _resultSubject.AsObservable(); + FileExplorer.FileOpened.Subscribe(_ => Open()); FileExplorer.FileExplorerFolder.WhenAnyValue(vm => vm.SelectedItem).Subscribe(SelectedItemChanged); } @@ -76,6 +77,11 @@ public FileDialogFilterViewModel? SelectedFilter public ICommand CancelCommand { get; } + public IObservable> Result { get; } + + public void Close() => + Cancel(); + private void Open() { if (FileExplorer.FileExplorerFolder.SelectedItem is Folder folder) @@ -89,7 +95,11 @@ private void Open() private void Cancel() => Close(new OpenFileDialogResult(Enumerable.Empty())); - private void Close(OpenFileDialogResult result) => _view.Close(result); + private void Close(OpenFileDialogResult result) + { + _resultSubject.OnNext(Observable.Return(result)); + _resultSubject.OnCompleted(); + } private void SelectedItemChanged(FileSystemEntry? entry) { diff --git a/src/Movere/ViewModels/PrintDialogViewModel.cs b/src/Movere/ViewModels/PrintDialogViewModel.cs index c9a3799..e897ed5 100644 --- a/src/Movere/ViewModels/PrintDialogViewModel.cs +++ b/src/Movere/ViewModels/PrintDialogViewModel.cs @@ -3,6 +3,7 @@ using System.Drawing.Printing; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Windows.Input; using static System.Drawing.Printing.PrinterSettings; @@ -12,26 +13,24 @@ namespace Movere.ViewModels { - internal sealed class PrintDialogViewModel : ReactiveObject + internal sealed class PrintDialogViewModel + : ReactiveObject, IDialogContentViewModel { private readonly PrintDocument _document; private readonly PreviewPrintController _controller = new PreviewPrintController(); - private readonly Action _closeAction; + private readonly ISubject> _resultSubject = + new Subject>(); private IReadOnlyList _printPreviewPages; private IReadOnlyList _availablePrinters = InstalledPrinters.ToReadOnlyList(); #pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. - public PrintDialogViewModel( - PrintDialogOptions options, - Action closeAction - ) + public PrintDialogViewModel(PrintDialogOptions options) #pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. { _document = options.Document; - _closeAction = closeAction; _document.PrintController = _controller; @@ -49,6 +48,8 @@ Action closeAction CancelCommand = ReactiveCommand.Create(Cancel); + Result = _resultSubject.AsObservable(); + RefreshAvailablePrinters(); } @@ -74,6 +75,12 @@ public IReadOnlyList PrintPreviewPages private void RefreshAvailablePrinters() => AvailablePrinters = InstalledPrinters.ToReadOnlyList(); + + public IObservable> Result { get; } + + public void Close() => + Cancel(); + private void Print() { _document.PrinterSettings = PrinterSettings.PrinterSettings; @@ -85,7 +92,9 @@ private void Print() private void Close(bool result) { _document.PrintController = new StandardPrintController(); - _closeAction(result); + + _resultSubject.OnNext(Observable.Return(result)); + _resultSubject.OnCompleted(); } private void UpdatePrintPreview(PrinterSettings printerSettings) diff --git a/src/Movere/ViewModels/SaveFileDialogViewModel.cs b/src/Movere/ViewModels/SaveFileDialogViewModel.cs index 0e96f64..1718911 100644 --- a/src/Movere/ViewModels/SaveFileDialogViewModel.cs +++ b/src/Movere/ViewModels/SaveFileDialogViewModel.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Threading.Tasks; using System.Windows.Input; @@ -12,10 +14,9 @@ namespace Movere.ViewModels { - internal sealed class SaveFileDialogViewModel : ReactiveObject + internal sealed class SaveFileDialogViewModel + : ReactiveObject, IDialogContentViewModel { - private readonly IDialogView _view; - private static readonly MessageDialogOptions s_fileAlreadyExistsMessageDialogOptions = new MessageDialogOptions( new LocalizedString(Strings.ResourceManager, nameof(Strings.FileAlreadyExistsMessage)), @@ -28,16 +29,17 @@ internal sealed class SaveFileDialogViewModel : ReactiveObject private readonly IMessageDialogService _messageDialogService; + private readonly ISubject> _resultSubject = + new Subject>(); + private string _fileName; public SaveFileDialogViewModel( SaveFileDialogOptions options, - IDialogView view, Func fileExplorerFactory, IMessageDialogService messageDialogService ) { - _view = view; _messageDialogService = messageDialogService; _fileName = options.InitialFileName ?? String.Empty; @@ -52,6 +54,8 @@ IMessageDialogService messageDialogService SaveCommand = ReactiveCommand.Create(SaveAsync); CancelCommand = ReactiveCommand.Create(Cancel); + Result = _resultSubject.AsObservable(); + FileExplorer.FileOpened.Subscribe(async file => await SaveAsync()); FileExplorer.FileExplorerFolder.WhenAnyValue(vm => vm.SelectedItem).Subscribe(SelectedItemChanged); @@ -69,6 +73,11 @@ public string FileName public ICommand CancelCommand { get; } + public IObservable> Result { get; } + + public void Close() => + Cancel(); + private async Task SaveAsync() { if (FileExplorer.FileExplorerFolder.SelectedItem is Folder folder) @@ -107,7 +116,11 @@ private async Task SaveAsync() private void Cancel() => Close(new SaveFileDialogResult(null)); - private void Close(SaveFileDialogResult result) => _view.Close(result); + private void Close(SaveFileDialogResult result) + { + _resultSubject.OnNext(Observable.Return(result)); + _resultSubject.OnCompleted(); + } private void SelectedItemChanged(FileSystemEntry? entry) { diff --git a/src/Movere/Views/DialogOverlay.axaml b/src/Movere/Views/DialogOverlay.axaml new file mode 100644 index 0000000..99b1db8 --- /dev/null +++ b/src/Movere/Views/DialogOverlay.axaml @@ -0,0 +1,65 @@ + + + + + + + +