MVVM Dialogs

repository·master·Indexed 42 days ago

https://github.com/mysteryx93/hanumaninstitute.mvvmdialogs

A cross-platform library for opening dialogs (modals, message boxes, file dialogs) directly from ViewModels while adhering to MVVM principles. It supports WPF and Avalonia, providing a platform-agnostic IDialogService to abstract the UI layer and improve unit testability. Features include built-in support for file/folder pickers, custom View-to-ViewModel mapping via ViewLocator, and lifecycle event handling through interfaces like IViewLoaded and IViewClosing.

Tokens
6.1K
Snippets
19
Records
25
Agent score
3%

What's inside MVVM Dialogs

  1. Introduction to MVVM Dialogs

    master

    MVVM Dialogs is a cross-platform library designed to simplify opening dialogs from a ViewModel while adhering to MVVM principles. It enables easier unit testing of ViewModels by abstracting the UI layer.

    Supported UI Frameworks:

    • WPF: Windows Presentation Foundation via HanumanInstitute.MvvmDialogs.Wpf.
    • Avalonia: Cross-platform (including mobile/browser) via HanumanInstitute.MvvmDialogs.Avalonia.

    Built-in dialog types include:

    • Modal and Non-modal windows
    • Message boxes
    • Open/Save file dialogs
    • Open folder dialogs
  2. Handle View lifecycle events in the ViewModel

    master

    You can handle loading, closing, and closed events directly in your ViewModel by implementing specific interfaces. This avoids writing code in the View (XAML) and simplifies handling asynchronous logic during closing.

    • IViewLoaded: Implement void OnLoaded() to run code when the view is loaded.
    • IViewClosed: Implement void RaiseClosed() to run code when the view is closed.
    • IViewClosing: Implement void RaiseClosing(CancelEventArgs e) and Task OnClosingAsync(CancelEventArgs e).

    Async Closing Workflow:

    1. In RaiseClosing, set e.Cancel = true to intercept the close.
    2. The library will then call OnClosingAsync.
    3. Inside OnClosingAsync, set e.Cancel = false to allow the view to close.

    IMPORTANT: To use these features in your main ViewModel, your main window must be initialized via IDialogService rather than standard framework startup methods.

    public class MyViewModel : ViewModelBase, IViewLoaded, IViewClosing
    {
        public void OnLoaded() => // logic here;
    
        public void RaiseClosing(CancelEventArgs e)
        {
            e.Cancel = true; // Intercept close
        }
    
        public async Task OnClosingAsync(CancelEventArgs e)
        {
            // Perform async work (e.g., confirmation dialog)
            e.Cancel = false; // Allow close
        }
    }
  3. Access files cross-platform with IDialogStorageFile

    master

    To maintain platform-agnostic code (Desktop, Mobile, Web), use IDialogStorageFile and IDialogStorageFolder. These wrap Avalonia's IStorageFile and IStorageFolder interfaces.

    You can convert an Avalonia IStorageProvider item to the MvvmDialogs interface using the .ToDialog() extension method.

    var result = await Storage.TryGetWellKnownFolderAsync(WellKnownFolder.Documents);
    var folder = result?.ToDialog();
    
    var settings = new SaveFileDialogSettings
    {
        SuggestedStartLocation = folder
    };
    var file = await _dialogService.ShowSaveFileDialogAsync(this, settings);
  4. Configure View-ViewModel mapping in Avalonia

    master

    By default (v2), MvvmDialogs uses a naming convention to map ViewModels to Views: replace the folder ViewModels with Views, and change the suffix from ViewModel to Window (for desktop) or View (for mobile/navigation).

    To implement a custom convention, inherit from ViewLocatorBase. To avoid reflection and support Assembly Trimming, use StrongViewLocator to manually register pairs.

    // Custom convention using ViewLocatorBase
    public class ViewLocator : ViewLocatorBase
    {
        protected override string GetViewName(object viewModel) => 
            viewModel.GetType().FullName!.Replace("ViewModel", "");
    }
    
    // Manual registration using StrongViewLocator (recommended for Assembly Trimming)
    public class ViewLocator : StrongViewLocator
    {
        public ViewLocator()
        {
            Register<MainViewModel, MainView, MainWindow>();
            Register<CurrentTimeViewModel, CurrentTimeView, CurrentTimeWindow>();
        }
    }
  5. Use Avalonia.Fluent extension

    master

    This extension integrates FluentAvalonia to provide WinUI-style controls.

    1. Add reference to HanumanInstitute.MvvmDialogs.Avalonia.Fluent.
    2. Register using AddFluent(FluentMessageBoxType.ContentDialog).

    This adds IDialogService.ShowContentDialogAsync and IDialogService.ShowTaskDialogAsync. You can also specify if ShowMessageBoxAsync should use ContentDialog or TaskDialog via the AddFluent parameter.

        new DialogFactory().AddFluent(FluentMessageBoxType.ContentDialog)))
  6. Setup IDialogService in WPF

    master

    To use the library in a WPF application, add a reference to HanumanInstitute.MvvmDialogs.Wpf. You must register IDialogService in your Dependency Injection container.

    Note that IDialogService is defined in the core HanumanInstitute.MvvmDialogs package, while the implementation DialogService is in the .Wpf package. When registering, you should provide a viewModelFactory to the DialogService constructor to allow the service to resolve ViewModels from your DI container.

    // Example using CommunityToolkit.Mvvm
    public partial class App
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            Ioc.Default.ConfigureServices(
                new ServiceCollection()
                    .AddSingleton<IDialogService>(new DialogService(
                        new DialogManager(viewLocator: new ViewLocator()),
                        viewModelFactory: x => Ioc.Default.GetService(x)))
                    .AddTransient<MainWindowViewModel>()
                    .BuildServiceProvider());
        }
    }
  7. Setup IDialogService in Avalonia

    master

    To use MvvmDialogs in an Avalonia project, add a reference to HanumanInstitute.MvvmDialogs.Avalonia. You must register IDialogService in your Dependency Injection container. In the constructor of the DialogService, you must pass a DialogManager which requires a ViewLocator from your Avalonia project template and a DialogFactory configured with your desired dialog types.

    // Example registration using Splat/ReactiveUI
    var build = Locator.CurrentMutable;
    build.RegisterLazySingleton(() => (IDialogService)new DialogService(
        new DialogManager(
            viewLocator: new ViewLocator(),
            dialogFactory: new DialogFactory().AddMessageBox()),
        viewModelFactory: x => Locator.Current.GetService(x)));
  8. Initialize the main window via IDialogService in Avalonia

    master

    To enable lifecycle event handling in your main ViewModel, you must initialize your main window using the DialogService in App.axaml.cs.

    public override void OnFrameworkInitializationCompleted()
    {
        GC.KeepAlive(typeof(DialogService));
        DialogService.Show(null, MainWindow);
        base.OnFrameworkInitializationCompleted();
    }
    
    public static MainWindowViewModel MainWindow => Locator.Current.GetService<MainWindowViewModel>()!;
    public static IDialogService DialogService => Locator.Current.GetService<IDialogService>()!;
  9. Add new methods to IDialogService via extensions

    master

    After registering a custom DialogFactory, you should create extension methods on IDialogService to provide a clean, typed API for consumers to call your new dialogs.

    public static class Extensions
    {
        public static Task<TaskDialogButton> ShowTaskMessageBoxAsync(
            this IDialogService service, 
            INotifyPropertyChanged ownerViewModel, 
            string text, 
            string title = "")
        {
            var settings = new TaskMessageBoxSettings(text, title);
            return service.DialogManager.ShowFrameworkDialogAsync<MessageBoxSettings, TaskDialogButton>(ownerViewModel, settings);
        }
    }
  10. Configure View-to-ViewModel mapping in WPF

    master

    By default, the library assumes a naming convention where the View is named by replacing "ViewModel" with "View" in the ViewModel's type name.

    To use a custom convention, create a class that inherits from ViewLocatorBase (or implements IViewLocator) and pass it to the DialogManager constructor when initializing DialogService.

    using HanumanInstitute.MvvmDialogs.Wpf;
    
    namespace MyDemoApp;
    
    /// <summary>
    /// Maps view models to views.
    /// </summary>
    public class ViewLocator : ViewLocatorBase
    {
        /// <inheritdoc />
        protected override string GetViewName(object viewModel) => 
            viewModel.GetType().FullName!.Replace("ViewModel", "View");
    }
  11. Implement strongly-typed dialog access via DialogExtensions

    master

    To improve code readability and maintainability, it is recommended to create a DialogExtensions class. This class provides strongly-typed, high-level methods for common dialog operations, hiding the complexity of ViewModel creation and configuration from the caller.

    public static class DialogExtensions
    {
        public static async Task<PresetItem?> ShowLoadPresetViewAsync(this IDialogService dialog, INotifyPropertyChanged ownerViewModel)
        {
            dialog.CheckNotNull(nameof(dialog)); // using HanumanInstitute.Validators
    
            var viewModel = dialog.CreateViewModel<SelectPresetViewModel>();
            viewModel.Load(false);
            var result = await dialog.ShowDialogAsync(ownerViewModel, viewModel).ConfigureAwait(true);
            return result == true ? viewModel.SelectedItem : null;
        }
    }
    
    // Usage in a ViewModel:
    private async Task<string?> SavePreset()
    {
        var presetName = await dialogService.ShowSavePresetViewAsync(this);
        if (presetName != null) { /* ... */ }
        return presetName;
    }