Skip to content

ViewModel and State Example

Omid Mafakher edited this page May 7, 2024 · 2 revisions

Overview

The ViewModel and State example an architectural pattern for managing view models and their associated states within an Avalonia application. This pattern aims to facilitate the separation of concerns, improve maintainability, and enhance the testability of applications built using Avalonia.

ViewModelBase Class

The ViewModelBase class serves as the base class for all view models within the application. It inherits from ReactiveObject to leverage reactive programming capabilities.

public class ViewModelBase : ReactiveObject
{
    public enum ViewModelStatus
    {
        Initialize,
        Starting,
        Started,
        Closed
    }
    
    public INavigator? Navigator { get; set; }

    public object? Parameter { get; set; }

    [Reactive] 
    public ViewModelStatus Status { get; private set; } = ViewModelStatus.Initialize;

    public CancellationToken? LifetimeCancellationToken { get; internal set; }

    public async Task InitializeAsync(CancellationToken cancellationToken)
    {
        if (Status != ViewModelStatus.Initialize) return;

        Status = ViewModelStatus.Starting;
        await StartAsync(cancellationToken);
        Status = ViewModelStatus.Started;
    }

    public Task HandleParameter(object parameter, CancellationToken cancellationToken)
    {
        Parameter = parameter;
        return ParameterAsync(parameter, cancellationToken);
    }

    protected virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    protected virtual Task ParameterAsync(object parameter, CancellationToken cancellationToken) => Task.CompletedTask;

    protected T GetParameter<T>() => Parameter is T cast ? cast : default;
}

Properties

  • Navigator: A property representing the navigator responsible for navigation within the application.
  • Parameter: An optional property holding parameters passed to the view model.
  • Status: Represents the current status of the view model, including states such as initialization, starting, started, and closed.
  • LifetimeCancellationToken: An internal property to manage the cancellation token associated with the lifetime of the view model.

Methods

  • InitializeAsync(CancellationToken cancellationToken): Asynchronously initializes the view model, transitioning through the initialization stages.
  • HandleParameter(object parameter, CancellationToken cancellationToken): Handles the parameters passed to the view model.
  • StartAsync(CancellationToken cancellationToken): A virtual method for performing asynchronous initialization tasks. Override this method in derived classes as needed.
  • ParameterAsync(object parameter, CancellationToken cancellationToken): A virtual method for handling parameters asynchronously. Override this method in derived classes as needed.
  • GetParameter(): Retrieves the parameter cast to the specified type, or returns the default value if the parameter does not match the type.

ModelBasePage Class

The ModelBasePage class extends the Page class and serves as the base class for all pages within the application. It is parameterized by the type of the associated view model.

public class ModelBasePage<T> : Page
    where T : ViewModelBase
{
    public T ViewModel { get; set; }

    protected override Type StyleKeyOverride => typeof(Page);

    private CancellationTokenSource? _cancellationTokenSource;
    protected CancellationToken? PageLifetimeCancellationToken => _cancellationTokenSource?.Token;

    private void KillToken()
    {
        try { _cancellationTokenSource?.Cancel(); } catch { }
        try { _cancellationTokenSource?.Dispose(); } catch { }
    }

    protected override void OnDataContextChanged(EventArgs e)
    {
        if (DataContext is T viewModel)
        {
            ViewModel = viewModel;
            viewModel.LifetimeCancellationToken = PageLifetimeCancellationToken;
            ViewModel.Navigator = Navigator;
        }
        base.OnDataContextChanged(e);
    }
    
    public override async Task InitialiseAsync(CancellationToken cancellationToken)
    {
        KillToken();
        _cancellationTokenSource = new CancellationTokenSource();
        
        if (DataContext is T viewModel)
        {
            ViewModel = viewModel;
        }
        else
        {
            DataContext = ViewModel = Locator.Current.GetService<T>() ?? throw new KeyNotFoundException("Cannot find ViewModel");
        }
        ViewModel.LifetimeCancellationToken = PageLifetimeCancellationToken;
        ViewModel.Navigator = Navigator;

        await base.InitialiseAsync(_cancellationTokenSource.Token);
        await ViewModel.InitializeAsync(_cancellationTokenSource.Token);
    }

    public override Task ArgumentAsync(object args, CancellationToken cancellationToken)
    {
        if (DataContext is ViewModelBase viewModel)
            return viewModel.HandleParameter(args, _cancellationTokenSource?.Token ?? cancellationToken);

        return base.ArgumentAsync(args, cancellationToken);
    }

    public override Task TerminateAsync(CancellationToken cancellationToken)
    {
        KillToken();
        return base.TerminateAsync(cancellationToken);
    }
}

Note

In this example, we utilize a Locator for Dependency Injection, although developers are encouraged to integrate their preferred DI frameworks according to project requirements

Properties

  • ViewModel: Represents the associated view model for the page.
  • PageLifetimeCancellationToken: A property to manage the cancellation token associated with the lifetime of the page.

Methods

  • InitialiseAsync(CancellationToken cancellationToken): Initializes the page asynchronously, setting up the view model and triggering its initialization.
  • ArgumentAsync(object args, CancellationToken cancellationToken): Passes arguments to the view model for handling.
  • TerminateAsync(CancellationToken cancellationToken): Terminates the page asynchronously, disposing of resources and canceling ongoing operations.

Usage

To utilize the ViewModel and State pattern in your Avalonia application:

  1. Inherit from ViewModelBase for your view models and implement the necessary initialization and parameter handling logic.
  2. Inherit from ModelBasePage for your pages, specifying the associated view model type T.
  3. Override the appropriate methods as needed to customize the behavior of your view models and pages.

Example

public class MyViewModel : ViewModelBase
{
    private readonly IMyService _myService;

    public MyViewModel(IMyService myService)
    {
        _myService = myService;
    }

    // Custom initialization logic if needed
    protected override Task StartAsync(CancellationToken cancellationToken)
    {
        // Custom initialization logic goes here
        return base.StartAsync(cancellationToken);
    }

    // Custom parameter handling logic
    protected override Task ParameterAsync(object parameter, CancellationToken cancellationToken)
    {
        if (parameter is MyViewModelState state)
        {
            return HandleMyViewModelStateAsync(state, cancellationToken);
        }

        if (parameter is ResultState result)
        {
            return HandleResultStateAsync(result, cancellationToken);
        }
        // Handle other parameter types if needed
        return base.ParameterAsync(parameter, cancellationToken);
    }

    // Custom logic to handle MyViewModelState 
    private async Task HandleMyViewModelStateAsync(MyViewModelState state, CancellationToken cancellationToken)
    {
        // Perform actions based on the MyViewModelState
        await Task.Delay(100, cancellationToken); // Example asynchronous operation
        // Example: Update properties or perform navigation based on the result

        await _myService.LoadPageAsync(state.Value, cancellationToken);
    }

    // Custom logic to handle ResultState
    private async Task HandleResultStateAsync(ResultState result, CancellationToken cancellationToken)
    {
        // Perform actions based on the ResultState
        await Task.Delay(100, cancellationToken); // Example asynchronous operation
        // Example: Update properties or perform navigation based on the result
    }
}