< Summary

Information
Class: MyNet.UI.ViewModels.Crud.EditionViewModel
Assembly: MyNet.UI
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/ViewModels/Crud/EditionViewModel.cs
Tag: 323_28699572109
Line coverage
10%
Covered lines: 8
Uncovered lines: 66
Coverable lines: 74
Total lines: 328
Line coverage: 10.8%
Branch coverage
8%
Covered branches: 3
Total branches: 34
Branch coverage: 8.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_IsDirty()100%11100%
.ctor(...)50%66100%
CreateTitle(...)0%620%
OnOpenedAsync()100%210%
SavingRequestAsync(...)100%210%
CanCloseAsync()0%110100%
CanCloseWithYesResultAsync()0%620%
OnClosingWithNoResult(...)100%210%
OnClosingWithCancelResult(...)100%210%
CanCancelAsync()0%620%
CancelAsync()0%620%
SaveInternalAsync()100%210%
Save(...)100%210%
SaveAsync()0%4260%
HandleValidationErrors()100%210%
SaveCoreAsync(...)100%210%
OnSaveSucceeded()100%210%
OnSaveFailed(...)100%210%
OnSaveRequested(...)100%210%
CanSave()100%210%
SaveAndClose(...)0%620%
SaveAndCloseAsync()0%620%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/ViewModels/Crud/EditionViewModel.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="EditionViewModel.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.ComponentModel;
 10using System.Globalization;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using System.Windows.Input;
 14using FluentValidation;
 15using MyNet.Globalization.Culture;
 16using MyNet.Observable;
 17using MyNet.Observable.Behaviors;
 18using MyNet.Observable.Validation.Validators;
 19using MyNet.UI.Commands;
 20using MyNet.UI.Dialogs;
 21using MyNet.UI.Dialogs.MessageBox;
 22using MyNet.UI.Loading;
 23using MyNet.UI.Notifications;
 24using MyNet.UI.Resources;
 25using MyNet.UI.ViewModels.Dialog;
 26using MyNet.UI.ViewModels.Workspace;
 27
 28namespace MyNet.UI.ViewModels.Crud;
 29
 30/// <summary>
 31/// Provides a reusable base implementation for create/edit dialog view models.
 32/// It encapsulates save, save-and-close, cancel, validation and close-confirmation workflows.
 33/// </summary>
 34public abstract class EditionViewModel : DialogViewModel<bool>, IEditionStateViewModel
 35{
 36    private readonly IDialogService _dialogService;
 37    private readonly INotificationPublisher _notificationPublisher;
 38    private bool _closingByCommand;
 39
 40    /// <summary>
 41    /// Gets the command that validates and saves the edited data.
 42    /// </summary>
 43    public ICommand SaveCommand { get; }
 44
 45    /// <summary>
 46    /// Gets the command that validates, saves and closes the dialog.
 47    /// </summary>
 48    public ICommand SaveAndCloseCommand { get; }
 49
 50    /// <summary>
 51    /// Gets the command that cancels edition and closes the dialog.
 52    /// </summary>
 53    public ICommand CancelCommand { get; }
 54
 55    /// <inheritdoc />
 1556    public bool IsDirty => this.IsModified();
 57
 58    /// <summary>
 59    /// Initializes a new instance of the <see cref="EditionViewModel"/> class.
 60    /// </summary>
 61    /// <param name="dialogService">Dialog service used for confirmation prompts.</param>
 62    /// <param name="notificationPublisher">Notification publisher used to display validation errors.</param>
 63    /// <param name="commandFactory">Optional command factory used to create commands.</param>
 64    /// <param name="validator">Optional validator used to validate this view model.</param>
 65    /// <param name="cultureService">Optional culture service used to manage culture changes.</param>
 66    protected EditionViewModel(
 67        IDialogService dialogService,
 68        INotificationPublisher notificationPublisher,
 69        ICommandFactory? commandFactory = null,
 70        IValidator? validator = null,
 71        ICultureService? cultureService = null)
 1572        : base(commandFactory, cultureService)
 73    {
 1574        _dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
 1575        _notificationPublisher = notificationPublisher ?? throw new ArgumentNullException(nameof(notificationPublisher))
 76
 77        CancelCommand = Commands.Create(() => CancelAsync());
 78        SaveCommand = Commands.Create(() => SaveAsync(), CanSave);
 79        SaveAndCloseCommand = Commands.Create(() => SaveAndCloseAsync(), CanSave);
 80
 1581        Mode = ScreenMode.Creation;
 82
 1583        this.UseTracking()
 1584            .UseValidation(validator ?? EmptyValidator.Instance);
 1585    }
 86
 87    /// <summary>
 88    /// Creates the localized title according to the current edition mode.
 89    /// </summary>
 90    /// <param name="culture">The culture used to create the title.</param>
 91    /// <returns>The localized title for creation or edition mode.</returns>
 092    protected override string? CreateTitle(CultureInfo culture) => Mode == ScreenMode.Edition ? UiResources.Edition : Ui
 93
 94    /// <summary>
 95    /// Resets internal close-state when the dialog opens.
 96    /// </summary>
 97    /// <returns>A task representing the asynchronous operation.</returns>
 98    public override Task OnOpenedAsync()
 99    {
 0100        _closingByCommand = false;
 0101        return base.OnOpenedAsync();
 102    }
 103
 104    /// <summary>
 105    /// Asks whether pending changes should be saved before closing.
 106    /// </summary>
 107    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 108    /// <returns>The user's decision.</returns>
 109    protected virtual Task<MessageBoxResult> SavingRequestAsync(CancellationToken cancellationToken = default)
 0110        => _dialogService.ShowQuestionWithCancelAsync(MessageResources.ItemSavingQuestion, UiResources.Edition, cancella
 111
 112    /// <summary>
 113    /// Determines whether the dialog can be closed.
 114    /// </summary>
 115    /// <returns><see langword="true"/> when closing is allowed; otherwise <see langword="false"/>.</returns>
 116    public override async Task<bool> CanCloseAsync()
 117    {
 0118        if (_closingByCommand || !this.IsModified())
 0119            return true;
 120
 0121        var result = await SavingRequestAsync(CancellationToken.None).ConfigureAwait(false);
 122
 123        switch (result)
 124        {
 125            case MessageBoxResult.Yes:
 0126                return await CanCloseWithYesResultAsync().ConfigureAwait(false);
 127            case MessageBoxResult.No:
 0128                SetResult(false);
 0129                return true;
 130            case MessageBoxResult.Cancel or MessageBoxResult.None:
 0131                return false;
 132            default:
 0133                return true;
 134        }
 0135    }
 136
 137    /// <summary>
 138    /// Handles close confirmation when the user chooses to save before closing.
 139    /// </summary>
 140    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 141    /// <returns><see langword="true"/> when close can continue; otherwise <see langword="false"/>.</returns>
 142    protected virtual async Task<bool> CanCloseWithYesResultAsync(CancellationToken cancellationToken = default)
 143    {
 0144        var isSaved = await SaveAsync(cancellationToken).ConfigureAwait(false);
 145
 0146        if (isSaved)
 0147            SetResult(true);
 148
 0149        return isSaved;
 0150    }
 151
 152    /// <summary>
 153    /// Handles closing behavior when the user chooses not to save.
 154    /// </summary>
 155    /// <param name="e">The cancel event arguments.</param>
 0156    protected virtual void OnClosingWithNoResult(CancelEventArgs e) => SetResult(false);
 157
 158    /// <summary>
 159    /// Handles closing behavior when the user cancels the close request.
 160    /// </summary>
 161    /// <param name="e">The cancel event arguments.</param>
 0162    protected virtual void OnClosingWithCancelResult(CancelEventArgs e) => e.Cancel = true;
 163
 164    #region Cancel
 165
 166    /// <summary>
 167    /// Determines whether cancellation is allowed.
 168    /// </summary>
 169    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 170    /// <returns><see langword="true"/> when cancellation is allowed; otherwise <see langword="false"/>.</returns>
 171    protected virtual async Task<bool> CanCancelAsync(CancellationToken cancellationToken = default)
 0172        => !this.IsModified() || await _dialogService.ShowQuestionAsync(MessageResources.ItemModificationCancellingQuest
 173
 174    /// <summary>
 175    /// Cancels edition and closes the dialog when allowed.
 176    /// </summary>
 177    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 178    /// <returns>A task representing the asynchronous operation.</returns>
 179    protected virtual async Task CancelAsync(CancellationToken cancellationToken = default)
 180    {
 0181        if (await CanCancelAsync(cancellationToken).ConfigureAwait(false))
 182        {
 0183            _closingByCommand = true;
 0184            Close(false);
 185        }
 0186    }
 187
 188    #endregion Cancel
 189
 190    #region Save
 191
 192    private async Task<bool> SaveInternalAsync(CancellationToken cancellationToken)
 193    {
 194        try
 195        {
 196            await BusyService.RunIndeterminateAsync(async (_, ct) => await SaveCoreAsync(ct).ConfigureAwait(false), canc
 0197            this.ResetIsModified();
 0198            return true;
 199        }
 0200        catch (Exception ex)
 201        {
 0202            OnExecutionError(ex);
 0203            return false;
 204        }
 0205    }
 206
 207    /// <summary>
 208    /// Saves changes synchronously.
 209    /// </summary>
 210    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 211    /// <returns><see langword="true"/> when save succeeds; otherwise <see langword="false"/>.</returns>
 212    protected bool Save(CancellationToken cancellationToken = default)
 0213        => SaveAsync(cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult();
 214
 215    /// <summary>
 216    /// Validates and saves changes asynchronously.
 217    /// </summary>
 218    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 219    /// <returns><see langword="true"/> when save succeeds; otherwise <see langword="false"/>.</returns>
 220    protected async Task<bool> SaveAsync(CancellationToken cancellationToken = default)
 221    {
 0222        var args = new CancelEventArgs();
 0223        OnSaveRequested(args);
 224
 0225        if (args.Cancel)
 0226            return false;
 227
 0228        if (!this.Validate())
 0229            return HandleValidationErrors();
 230
 0231        var isSaved = await SaveInternalAsync(cancellationToken).ConfigureAwait(false);
 232
 0233        if (!isSaved)
 0234            return false;
 235
 0236        OnSaveSucceeded();
 237
 0238        return true;
 0239    }
 240
 241    private bool HandleValidationErrors()
 242    {
 0243        var errors = Behaviors.Get<IValidationBehavior>().Errors;
 0244        _notificationPublisher.PublishErrors(errors);
 0245        OnSaveFailed(errors);
 246
 0247        return false;
 248    }
 249
 250    /// <summary>
 251    /// Saves the current data synchronously.
 252    /// </summary>
 253    protected abstract void SaveCore();
 254
 255    /// <summary>
 256    /// Saves the current data asynchronously.
 257    /// </summary>
 258    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 259    /// <returns>A task representing the asynchronous operation.</returns>
 260    protected virtual Task SaveCoreAsync(CancellationToken cancellationToken)
 261    {
 0262        cancellationToken.ThrowIfCancellationRequested();
 0263        SaveCore();
 0264        return Task.CompletedTask;
 265    }
 266
 267    /// <summary>
 268    /// Called after a successful save operation.
 269    /// </summary>
 0270    protected virtual void OnSaveSucceeded() { }
 271
 272    /// <summary>
 273    /// Called after a failed save operation caused by validation errors.
 274    /// </summary>
 275    /// <param name="errors">The validation errors that caused the save failure.</param>
 0276    protected virtual void OnSaveFailed(IReadOnlyCollection<string> errors) { }
 277
 278    /// <summary>
 279    /// Called before save starts and can cancel the operation.
 280    /// </summary>
 281    /// <param name="args">The cancel event arguments.</param>
 282    protected virtual void OnSaveRequested(CancelEventArgs args)
 283    {
 0284    }
 285
 286    /// <summary>
 287    /// Determines whether save commands can execute.
 288    /// </summary>
 289    /// <returns><see langword="true"/> when save commands can execute; otherwise <see langword="false"/>.</returns>
 0290    protected virtual bool CanSave() => true;
 291
 292    #endregion Save
 293
 294    #region SaveAndClose
 295
 296    /// <summary>
 297    /// Saves data and closes the dialog.
 298    /// </summary>
 299    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 300    /// <returns><see langword="true"/> when the operation succeeds; otherwise <see langword="false"/>.</returns>
 301    protected virtual bool SaveAndClose(CancellationToken cancellationToken = default)
 302    {
 0303        if (!Save(cancellationToken))
 0304            return false;
 305
 0306        _closingByCommand = true;
 0307        Close(true);
 0308        return true;
 309    }
 310
 311    /// <summary>
 312    /// Saves data and closes the dialog asynchronously.
 313    /// </summary>
 314    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 315    /// <returns><see langword="true"/> when the operation succeeds; otherwise <see langword="false"/>.</returns>
 316    protected virtual async Task<bool> SaveAndCloseAsync(CancellationToken cancellationToken = default)
 317    {
 0318        if (!await SaveAsync(cancellationToken).ConfigureAwait(false))
 0319            return false;
 320
 0321        _closingByCommand = true;
 0322        Close(true);
 0323        return true;
 0324    }
 325
 326    #endregion SaveAndClose
 327}
 328