< Summary

Information
Class: MyNet.UI.ViewModels.ViewModelBase
Assembly: MyNet.UI
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/ViewModels/ViewModelBase.cs
Tag: 323_28699572109
Line coverage
45%
Covered lines: 25
Uncovered lines: 30
Coverable lines: 55
Total lines: 268
Line coverage: 45.4%
Branch coverage
30%
Covered branches: 3
Total branches: 10
Branch coverage: 30%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
set_State(...)100%11100%
LoadAsync(...)100%11100%
OnLoadAsync(...)100%11100%
ExecuteStateAsync()0%3246.66%
ExecuteAsync(...)100%11100%
ExecuteSafeAsync(...)100%11100%
ExecuteAsync(...)100%210%
ExecuteSafeAsync(...)100%210%
ExecuteWithProgressAsync(...)100%210%
ExecuteSafeWithProgressAsync(...)100%210%
ExecuteCoreAsync()100%22100%
ExecuteCoreAsync()0%620%
ExecuteProgressCoreAsync()0%620%
OnExecutionError(...)100%210%
GetHashCode()100%11100%
Equals(...)50%22100%
DisposeManagedResources()100%11100%

File(s)

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

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ViewModelBase.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Diagnostics.CodeAnalysis;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using MyNet.Observable;
 12using MyNet.Primitives;
 13using MyNet.UI.Loading;
 14using MyNet.UI.Loading.Models;
 15
 16namespace MyNet.UI.ViewModels;
 17
 18/// <summary>
 19/// Base class for all view models in the application.
 20/// Provides common functionality like busy indication for async operations, error handling, and state management.
 21/// </summary>
 22/// <remarks>
 23/// <see cref="BusyService"/> is scoped to this view model (bind <see cref="BusyService"/> in the view).
 24/// For application-wide busy, inject <see cref="IBusyService"/> on the operation or service that needs it.
 25/// <para>
 26/// Async execution helpers differ by error propagation:
 27/// <list type="bullet">
 28/// <item><see cref="ExecuteStateAsync{TBusy}"/> — updates <see cref="State"/> and always rethrows after <see cref="OnEx
 29/// <item><see cref="ExecuteAsync(Func{CancellationToken, Task}, CancellationToken)"/> — local busy only; rethrows when 
 30/// <item><see cref="ExecuteSafeAsync(Func{CancellationToken, Task}, CancellationToken)"/> — local busy only; never reth
 31/// </list>
 32/// Override <see cref="OnExecutionError"/> and return <see langword="false"/> to propagate an exception to callers or <
 33/// </para>
 34/// </remarks>
 35[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Fields is not disposable and d
 36public abstract class ViewModelBase : ObservableObject, IIdentifiable<Guid>
 37{
 45038    private readonly SemaphoreSlim _stateLock = new(1, 1);
 39
 40    /// <summary>
 41    /// Gets the unique identifier for this view model instance.
 42    /// Useful for tracking, logging, and debugging.
 43    /// </summary>
 44    public Guid Id { get; } = Guid.NewGuid();
 45
 46    /// <summary>
 47    /// Gets the current loading state of the workspace.
 48    /// </summary>
 9049    public LoadState State { get; private set => SetProperty(ref field, value); } = LoadState.NotLoaded;
 50
 51    /// <summary>
 52    /// Gets the local busy service for operations scoped to this view model.
 53    /// </summary>
 54    public IBusyService BusyService { get; } = new BusyService();
 55
 56    #region Loading
 57
 58    /// <summary>
 59    /// Loads the view model asynchronously if it is not already loaded. If the view model is in the <see cref="LoadStat
 60    /// </summary>
 61    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 62    /// <returns>A task representing the asynchronous operation.</returns>
 4563    protected Task LoadAsync(CancellationToken cancellationToken = default) => ExecuteStateAsync<IndeterminateBusy>(asyn
 64
 65    /// <summary>
 66    /// When overridden in a derived class, performs the actual loading logic for the view model. This method is called 
 67    /// </summary>
 68    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 69    /// <returns>A task representing the asynchronous operation.</returns>
 4870    protected virtual Task OnLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 71
 72    #endregion
 73
 74    #region Async Execution
 75
 76    /// <summary>
 77    /// Executes an asynchronous action with a busy indicator of the specified type, and manages the loading state of th
 78    /// </summary>
 79    /// <param name="action">The asynchronous action to execute.</param>
 80    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 81    protected async Task ExecuteStateAsync<TBusy>(Func<TBusy, CancellationToken, Task> action, CancellationToken cancell
 82        where TBusy : IBusy, new()
 83    {
 4584        await _stateLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 85
 86        try
 87        {
 4588            State = LoadState.Loading;
 89
 4590            await BusyService.RunAsync(action, cancellationToken).ConfigureAwait(false);
 91
 4592            State = LoadState.Loaded;
 4593        }
 094        catch (OperationCanceledException)
 95        {
 096            State = LoadState.NotLoaded;
 097            throw;
 98        }
 099        catch (Exception ex) when (ex is not OperationCanceledException)
 100        {
 0101            State = LoadState.Error;
 102
 0103            if (OnExecutionError(ex))
 0104                return;
 105
 0106            throw;
 107        }
 108        finally
 109        {
 45110            _stateLock.Release();
 111        }
 45112    }
 113
 114    /// <summary>
 115    /// Executes an asynchronous action with the local busy service.
 116    /// When <see cref="OnExecutionError"/> returns <see langword="false"/>, the exception is rethrown to the caller.
 117    /// </summary>
 118    /// <param name="action">The asynchronous action to execute.</param>
 119    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 120    protected Task ExecuteAsync(Func<CancellationToken, Task> action, CancellationToken cancellationToken = default)
 6121        => ExecuteCoreAsync(action, rethrowOnUnhandledError: true, cancellationToken);
 122
 123    /// <summary>
 124    /// Executes an asynchronous action with the local busy service and never rethrows after <see cref="OnExecutionError
 125    /// </summary>
 126    /// <param name="action">The asynchronous action to execute.</param>
 127    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 128    protected Task ExecuteSafeAsync(Func<CancellationToken, Task> action, CancellationToken cancellationToken = default)
 33129        => ExecuteCoreAsync(action, rethrowOnUnhandledError: false, cancellationToken);
 130
 131    /// <summary>
 132    /// Executes an asynchronous function that returns a result with automatic error handling.
 133    /// When <see cref="OnExecutionError"/> returns <see langword="false"/>, the exception is rethrown.
 134    /// </summary>
 135    /// <typeparam name="TResult">The type of the result.</typeparam>
 136    /// <param name="func">The asynchronous function to execute.</param>
 137    /// <param name="defaultValue">The default value to return when the exception is handled by <see cref="OnExecutionEr
 138    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 139    /// <returns>A task representing the asynchronous operation with the result, or <paramref name="defaultValue"/> when
 140    protected Task<TResult> ExecuteAsync<TResult>(
 141        Func<CancellationToken, Task<TResult>> func,
 142        TResult defaultValue = default!,
 143        CancellationToken cancellationToken = default)
 0144        => ExecuteCoreAsync(func, defaultValue, rethrowOnUnhandledError: true, cancellationToken);
 145
 146    /// <summary>
 147    /// Executes an asynchronous function with automatic error handling and never rethrows after <see cref="OnExecutionE
 148    /// </summary>
 149    /// <typeparam name="TResult">The type of the result.</typeparam>
 150    /// <param name="func">The asynchronous function to execute.</param>
 151    /// <param name="defaultValue">The default value to return when an error occurs.</param>
 152    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 153    /// <returns>A task representing the asynchronous operation with the result, or <paramref name="defaultValue"/> when
 154    protected Task<TResult> ExecuteSafeAsync<TResult>(
 155        Func<CancellationToken, Task<TResult>> func,
 156        TResult defaultValue = default!,
 157        CancellationToken cancellationToken = default)
 0158        => ExecuteCoreAsync(func, defaultValue, rethrowOnUnhandledError: false, cancellationToken);
 159
 160    /// <summary>
 161    /// Executes an asynchronous function with progress tracking and cancellation support on the local busy service.
 162    /// When <see cref="OnExecutionError"/> returns <see langword="false"/>, the exception is rethrown.
 163    /// </summary>
 164    /// <param name="action">The asynchronous function to execute. Receives a <see cref="ProgressionBusy"/> instance for
 165    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 166    /// <returns>A task representing the asynchronous operation.</returns>
 167    protected Task ExecuteWithProgressAsync(
 168        Func<ProgressionBusy, CancellationToken, Task> action,
 169        CancellationToken cancellationToken = default)
 0170        => ExecuteProgressCoreAsync(action, rethrowOnUnhandledError: true, cancellationToken);
 171
 172    /// <summary>
 173    /// Executes an asynchronous function with progress tracking and never rethrows after <see cref="OnExecutionError"/>
 174    /// </summary>
 175    /// <param name="action">The asynchronous function to execute. Receives a <see cref="ProgressionBusy"/> instance for
 176    /// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
 177    /// <returns>A task representing the asynchronous operation.</returns>
 178    protected Task ExecuteSafeWithProgressAsync(
 179        Func<ProgressionBusy, CancellationToken, Task> action,
 180        CancellationToken cancellationToken = default)
 0181        => ExecuteProgressCoreAsync(action, rethrowOnUnhandledError: false, cancellationToken);
 182
 183    private async Task ExecuteCoreAsync(
 184        Func<CancellationToken, Task> action,
 185        bool rethrowOnUnhandledError,
 186        CancellationToken cancellationToken)
 187    {
 188        try
 189        {
 39190            await BusyService.RunIndeterminateAsync(action, cancellationToken).ConfigureAwait(false);
 30191        }
 9192        catch (Exception ex) when (ex is not OperationCanceledException)
 193        {
 9194            if (!OnExecutionError(ex) && rethrowOnUnhandledError)
 3195                throw;
 6196        }
 36197    }
 198
 199    private async Task<TResult> ExecuteCoreAsync<TResult>(
 200        Func<CancellationToken, Task<TResult>> func,
 201        TResult defaultValue,
 202        bool rethrowOnUnhandledError,
 203        CancellationToken cancellationToken)
 204    {
 205        try
 206        {
 0207            TResult? result = default;
 0208            await BusyService.RunAsync<IndeterminateBusy>(
 0209                async (_, ct) => result = await func(ct).ConfigureAwait(false),
 0210                cancellationToken).ConfigureAwait(false);
 211
 0212            return result!;
 213        }
 0214        catch (Exception ex) when (ex is not OperationCanceledException)
 215        {
 0216            if (!OnExecutionError(ex) && rethrowOnUnhandledError)
 0217                throw;
 218
 0219            return defaultValue;
 220        }
 0221    }
 222
 223    private async Task ExecuteProgressCoreAsync(
 224        Func<ProgressionBusy, CancellationToken, Task> action,
 225        bool rethrowOnUnhandledError,
 226        CancellationToken cancellationToken)
 227    {
 228        try
 229        {
 0230            await BusyService.RunProgressionAsync(action, cancellationToken).ConfigureAwait(false);
 0231        }
 0232        catch (Exception ex) when (ex is not OperationCanceledException)
 233        {
 0234            if (!OnExecutionError(ex) && rethrowOnUnhandledError)
 0235                throw;
 0236        }
 0237    }
 238
 239    #endregion
 240
 241    #region Error Handling
 242
 243    /// <summary>
 244    /// Called when an exception occurs during an async operation executed through the <see cref="ExecuteAsync"/> helper
 245    /// </summary>
 246    /// <param name="exception">The exception that occurred.</param>
 247    /// <returns>
 248    /// <see langword="true"/> when the exception is handled and must not be rethrown;
 249    /// <see langword="false"/> to propagate the exception to the caller (for methods that support rethrowing).
 250    /// </returns>
 0251    protected virtual bool OnExecutionError(Exception exception) => true;
 252
 253    #endregion
 254
 255    /// <inheritdoc />
 57256    public override int GetHashCode() => Id.GetHashCode();
 257
 258    /// <inheritdoc />
 27259    public override bool Equals(object? obj) => obj is ViewModelBase viewModel && Id == viewModel.Id;
 260
 261    /// <inheritdoc />
 262    protected override void DisposeManagedResources()
 263    {
 120264        _stateLock.Dispose();
 120265        base.DisposeManagedResources();
 120266    }
 267}
 268