-
Notifications
You must be signed in to change notification settings - Fork 11
ViewModel and State Example
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.
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;
}
- 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.
- 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.
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
- ViewModel: Represents the associated view model for the page.
- PageLifetimeCancellationToken: A property to manage the cancellation token associated with the lifetime of the page.
- 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.
To utilize the ViewModel and State pattern in your Avalonia application:
- Inherit from ViewModelBase for your view models and implement the necessary initialization and parameter handling logic.
- Inherit from ModelBasePage for your pages, specifying the associated view model type T.
- Override the appropriate methods as needed to customize the behavior of your view models and pages.
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
}
}