From 5175af250e214dfcb543ae64dec26321c4371548 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Mon, 27 Apr 2026 16:12:25 +0200 Subject: [PATCH] Began porting my MultiPageViewModel from the Windows Forms library to the Avalonia library; updated copyright statement for the year 2026 --- Nuclex.Avalonia (netstandard-2.0).csproj | 4 +- Properties/AssemblyInfo.cs | 3 +- Source/AutoBinding/ConventionBinder.cs | 2 +- Source/AutoBinding/IAutoBinder.cs | 2 +- .../AsyncVirtualObservableReadOnlyList.cs | 2 +- Source/Collections/LazyFetchEventArgs.cs | 2 +- .../VirtualObservableReadOnlyList.cs | 2 +- Source/Commands/AsyncRelayCommand.cs | 153 ++++++++++++++++ Source/Commands/IAsyncCommand.cs | 43 +++++ Source/CommonDialogs/AvaloniaFileSelector.cs | 2 +- .../CommonDialogs/IDirectoryPickerService.cs | 2 +- Source/CommonDialogs/IFilePickerService.cs | 2 +- Source/IActiveWindowTracker.cs | 2 +- Source/IWindowManager.cs | 2 +- Source/IWindowScope.cs | 2 +- Source/Messages/AvaloniaMessagePresenter.cs | 2 +- Source/Messages/IMessageService.cs | 2 +- Source/Messages/MessageEventArgs.cs | 2 +- Source/Messages/MessageServiceHelper.cs | 2 +- Source/Messages/MessageText.cs | 2 +- Source/NullActiveWindowTracker.cs | 2 +- Source/Properties.cs | 2 +- Source/ViewModels/DialogResultEventArgs.cs | 2 +- Source/ViewModels/IDialogViewModel.cs | 2 +- Source/ViewModels/IMultiPageViewModel.cs | 41 +++++ Source/ViewModels/IViewLoadListener.cs | 36 ++++ Source/ViewModels/IViewUnloadListener.cs | 36 ++++ Source/ViewModels/MultiPageViewModel.cs | 164 ++++++++++++++++++ Source/WindowManager.cs | 2 +- 29 files changed, 497 insertions(+), 25 deletions(-) create mode 100644 Source/Commands/AsyncRelayCommand.cs create mode 100644 Source/Commands/IAsyncCommand.cs create mode 100644 Source/ViewModels/IMultiPageViewModel.cs create mode 100644 Source/ViewModels/IViewLoadListener.cs create mode 100644 Source/ViewModels/IViewUnloadListener.cs create mode 100644 Source/ViewModels/MultiPageViewModel.cs diff --git a/Nuclex.Avalonia (netstandard-2.0).csproj b/Nuclex.Avalonia (netstandard-2.0).csproj index 7dfec96..c05222d 100644 --- a/Nuclex.Avalonia (netstandard-2.0).csproj +++ b/Nuclex.Avalonia (netstandard-2.0).csproj @@ -23,8 +23,8 @@ - - + + all diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 4841033..939614d 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ limitations under the License. #endregion // Apache License 2.0 using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/Source/AutoBinding/ConventionBinder.cs b/Source/AutoBinding/ConventionBinder.cs index d5f69e3..c93c63c 100644 --- a/Source/AutoBinding/ConventionBinder.cs +++ b/Source/AutoBinding/ConventionBinder.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/AutoBinding/IAutoBinder.cs b/Source/AutoBinding/IAutoBinder.cs index d9bd6b4..08be7a2 100644 --- a/Source/AutoBinding/IAutoBinder.cs +++ b/Source/AutoBinding/IAutoBinder.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Collections/AsyncVirtualObservableReadOnlyList.cs b/Source/Collections/AsyncVirtualObservableReadOnlyList.cs index 6087548..90cb6a1 100644 --- a/Source/Collections/AsyncVirtualObservableReadOnlyList.cs +++ b/Source/Collections/AsyncVirtualObservableReadOnlyList.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Collections/LazyFetchEventArgs.cs b/Source/Collections/LazyFetchEventArgs.cs index db1dda9..3db2c5a 100644 --- a/Source/Collections/LazyFetchEventArgs.cs +++ b/Source/Collections/LazyFetchEventArgs.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Collections/VirtualObservableReadOnlyList.cs b/Source/Collections/VirtualObservableReadOnlyList.cs index ffadf55..801cc60 100644 --- a/Source/Collections/VirtualObservableReadOnlyList.cs +++ b/Source/Collections/VirtualObservableReadOnlyList.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Commands/AsyncRelayCommand.cs b/Source/Commands/AsyncRelayCommand.cs new file mode 100644 index 0000000..00d4261 --- /dev/null +++ b/Source/Commands/AsyncRelayCommand.cs @@ -0,0 +1,153 @@ +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Nuclex.Avalonia.Commands { + + /// Asynchronous command that delegates work to an external method + /// Type of argument accepted by the command + internal sealed class AsyncRelayCommand : IAsyncCommand { + + /// Raised when the command's executable state has changed + public event EventHandler? CanExecuteChanged; + + /// Initializes a new asynchronous relay command + /// Action that is executed when the command runs + /// + /// Optional predicate that decides whether execution is currently allowed + /// + /// + /// Whether the command may be executed while a previous execution is still running + /// + public AsyncRelayCommand( + Func executeAsync, + Predicate? canExecute = null, + bool allowConcurrentExecutions = false + ) { + this.executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync)); + this.canExecute = canExecute; + this.allowConcurrentExecutions = allowConcurrentExecutions; + } + + /// Whether the command is currently executing + public bool IsRunning { + get { return (Volatile.Read(ref this.executionCount) > 0); } + } + + /// Checks whether the command may currently execute + /// Parameter to be passed to the command callback + /// True if command execution is currently allowed + public bool CanExecute(object? parameter) { + if((!this.allowConcurrentExecutions) && IsRunning) { + return false; + } + + if(this.canExecute == null) { + return true; + } + + return this.canExecute(getParameter(parameter)); + } + + /// Executes the command callback + /// Parameter passed to the command callback + public void Execute(object? parameter) { + _ = ExecuteAsync(getParameter(parameter)); + } + + /// Executes the command callback asynchronously + /// Parameter passed to the command callback + /// A task that finishes when command execution has completed + public async Task ExecuteAsync(TArgument parameter) { + if(!CanExecute(parameter)) { + return; + } + + if(this.allowConcurrentExecutions) { + Interlocked.Increment(ref this.executionCount); + + try { + await this.executeAsync(parameter).ConfigureAwait(false); + } + finally { + Interlocked.Decrement(ref this.executionCount); + } + } else { + Interlocked.Increment(ref this.executionCount); + NotifyCanExecuteChanged(); + + try { + await this.executeAsync(parameter).ConfigureAwait(false); + } + finally { + Interlocked.Decrement(ref this.executionCount); + NotifyCanExecuteChanged(); + } + } // if concurrent executions allowed / not allowed + } + + /// + /// Notifies listeners that should be reevaluated + /// + public void NotifyCanExecuteChanged() { + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + + /// Converts and validates the untyped command parameter + /// Untyped parameter passed through + /// The parameter cast to the command's parameter type + private static TArgument getParameter(object? parameter) { + if(parameter is TArgument typedParameter) { + return typedParameter; + } + + if(parameter == null) { + if(default(TArgument) == null) { + return default!; + } + + throw new ArgumentException( + "This command expects a non-null parameter of the configured type.", + nameof(parameter) + ); + } + + throw new ArgumentException( + $"This command expects a parameter of type {typeof(TArgument).FullName}.", + nameof(parameter) + ); + } + + /// Asynchronous callback invoked when command execution is requested + private readonly Func executeAsync; + /// Optional callback deciding whether command execution is currently allowed + private readonly Predicate? canExecute; + /// Whether the command may run while a previous invocation is still active + private readonly bool allowConcurrentExecutions; + + /// Number of currently active command executions + private int executionCount; + + } + +} // namespace Nuclex.Avalonia.Commands diff --git a/Source/Commands/IAsyncCommand.cs b/Source/Commands/IAsyncCommand.cs new file mode 100644 index 0000000..315bd66 --- /dev/null +++ b/Source/Commands/IAsyncCommand.cs @@ -0,0 +1,43 @@ +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Nuclex.Avalonia.Commands { + + /// Command that executes asynchronously + /// Type of an optional argument for the command + public interface IAsyncCommand : ICommand { + + /// Whether the command is currently executing + bool IsRunning { get; } + + /// Runs the command asynchronously + /// Optional argument for the command + /// A task that completes when the command has executed + Task ExecuteAsync(TArgument argument); + + /// Triggers reevaluation of + void NotifyCanExecuteChanged(); + + } + +} // namespace Nuclex.Avalonia.Commands diff --git a/Source/CommonDialogs/AvaloniaFileSelector.cs b/Source/CommonDialogs/AvaloniaFileSelector.cs index fc9d89f..539ed56 100644 --- a/Source/CommonDialogs/AvaloniaFileSelector.cs +++ b/Source/CommonDialogs/AvaloniaFileSelector.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/CommonDialogs/IDirectoryPickerService.cs b/Source/CommonDialogs/IDirectoryPickerService.cs index 0bbe43e..92e6281 100644 --- a/Source/CommonDialogs/IDirectoryPickerService.cs +++ b/Source/CommonDialogs/IDirectoryPickerService.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/CommonDialogs/IFilePickerService.cs b/Source/CommonDialogs/IFilePickerService.cs index d55180b..ebc55c3 100644 --- a/Source/CommonDialogs/IFilePickerService.cs +++ b/Source/CommonDialogs/IFilePickerService.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/IActiveWindowTracker.cs b/Source/IActiveWindowTracker.cs index ecb899d..8c37a34 100644 --- a/Source/IActiveWindowTracker.cs +++ b/Source/IActiveWindowTracker.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/IWindowManager.cs b/Source/IWindowManager.cs index d68ed93..fdf8ca0 100644 --- a/Source/IWindowManager.cs +++ b/Source/IWindowManager.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/IWindowScope.cs b/Source/IWindowScope.cs index 59decd9..8431181 100644 --- a/Source/IWindowScope.cs +++ b/Source/IWindowScope.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Messages/AvaloniaMessagePresenter.cs b/Source/Messages/AvaloniaMessagePresenter.cs index 7b79836..20a5c24 100644 --- a/Source/Messages/AvaloniaMessagePresenter.cs +++ b/Source/Messages/AvaloniaMessagePresenter.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Messages/IMessageService.cs b/Source/Messages/IMessageService.cs index 0e6d7e6..1fe6e70 100644 --- a/Source/Messages/IMessageService.cs +++ b/Source/Messages/IMessageService.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Messages/MessageEventArgs.cs b/Source/Messages/MessageEventArgs.cs index e20fd08..72c2720 100644 --- a/Source/Messages/MessageEventArgs.cs +++ b/Source/Messages/MessageEventArgs.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Messages/MessageServiceHelper.cs b/Source/Messages/MessageServiceHelper.cs index ad7f85f..c780071 100644 --- a/Source/Messages/MessageServiceHelper.cs +++ b/Source/Messages/MessageServiceHelper.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Messages/MessageText.cs b/Source/Messages/MessageText.cs index b707434..083670a 100644 --- a/Source/Messages/MessageText.cs +++ b/Source/Messages/MessageText.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/NullActiveWindowTracker.cs b/Source/NullActiveWindowTracker.cs index 69efc80..6fd8973 100644 --- a/Source/NullActiveWindowTracker.cs +++ b/Source/NullActiveWindowTracker.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/Properties.cs b/Source/Properties.cs index eefd40c..3c17293 100644 --- a/Source/Properties.cs +++ b/Source/Properties.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/ViewModels/DialogResultEventArgs.cs b/Source/ViewModels/DialogResultEventArgs.cs index bc01d78..8d74aca 100644 --- a/Source/ViewModels/DialogResultEventArgs.cs +++ b/Source/ViewModels/DialogResultEventArgs.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/ViewModels/IDialogViewModel.cs b/Source/ViewModels/IDialogViewModel.cs index 150b3f2..3538eff 100644 --- a/Source/ViewModels/IDialogViewModel.cs +++ b/Source/ViewModels/IDialogViewModel.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Source/ViewModels/IMultiPageViewModel.cs b/Source/ViewModels/IMultiPageViewModel.cs new file mode 100644 index 0000000..ab0dabe --- /dev/null +++ b/Source/ViewModels/IMultiPageViewModel.cs @@ -0,0 +1,41 @@ +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; + +using Nuclex.Avalonia.Commands; + +namespace Nuclex.Avalonia.ViewModels { + + /// Interface for view models that can switch between different pages + /// Enum type by which pages can be indicated + public interface IMultiPageViewModel { + + /// Command to switch the active tool page + IAsyncCommand SwitchPageCommand { get; } + + /// The currently displayed page + TPageEnumeration? ActivePage { get; } + + /// View model for the page that is currently being shown + object? ActivePageViewModel { get; } + + } + +} // namespace Nuclex.Avalonia.ViewModels diff --git a/Source/ViewModels/IViewLoadListener.cs b/Source/ViewModels/IViewLoadListener.cs new file mode 100644 index 0000000..06312f5 --- /dev/null +++ b/Source/ViewModels/IViewLoadListener.cs @@ -0,0 +1,36 @@ +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading.Tasks; + +namespace Nuclex.Avalonia.ViewModels { + + /// + /// Can be implemented by view models that wish to know when their view is loaded + /// + public interface IViewLoadListener { + + /// Called when the view has finished loading + /// A task what finishes when all view load processing is done + Task OnViewLoaded(); + + } + +} // namespace Nuclex.Avalonia.ViewModels diff --git a/Source/ViewModels/IViewUnloadListener.cs b/Source/ViewModels/IViewUnloadListener.cs new file mode 100644 index 0000000..bb946df --- /dev/null +++ b/Source/ViewModels/IViewUnloadListener.cs @@ -0,0 +1,36 @@ +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Threading.Tasks; + +namespace Nuclex.Avalonia.ViewModels { + + /// + /// Can be implemented by view models that wish to know when their view is unloading + /// + public interface IViewUnloadListener { + + /// Called when the view is about to unload + /// A task what finishes when all view unload processing is done + Task OnViewUnloading(); + + } + +} // namespace Nuclex.Avalonia.ViewModels diff --git a/Source/ViewModels/MultiPageViewModel.cs b/Source/ViewModels/MultiPageViewModel.cs new file mode 100644 index 0000000..4f4957b --- /dev/null +++ b/Source/ViewModels/MultiPageViewModel.cs @@ -0,0 +1,164 @@ +#region Apache License 2.0 +/* +Nuclex Foundation libraries for .NET +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#endregion // Apache License 2.0 + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +using Nuclex.Avalonia.Commands; +using Nuclex.Support; + +namespace Nuclex.Avalonia.ViewModels { + + /// Base class for view models that have multiple child view models + /// Enum type by which pages can be indicated + public abstract class MultiPageViewModel : + Observable, IMultiPageViewModel, IDisposable { + + /// Initializes a new multi-page view model + /// + /// Window manager the view model uses to create child views + /// + /// + /// Whether child view models will be kept alive and reused + /// + public MultiPageViewModel(IWindowManager windowManager, bool cachePageViewModels = false) { + this.windowManager = windowManager; + this.createViewModelForPageDelegate = ( + new Func(CreateViewModelForPage) + ); + + if(cachePageViewModels) { + this.cachedViewModels = new ConcurrentDictionary(); + } + + SwitchPageCommand = new AsyncRelayCommand(switchPageAsync); + } + + /// Command to switch the active view + public IAsyncCommand SwitchPageCommand { get; } + + /// Immediately releases all resources owned by the instance + public virtual void Dispose() { + + // If view models are being cached, simply dispose anything in the cache that + // implements IDisposable, the active view will be part of the cache. + if(this.cachedViewModels != null) { + foreach(object cacheViewModel in this.cachedViewModels.Values) { + disposeIfSupported(cacheViewModel); + } + this.activePageViewModel = null; + + this.cachedViewModels.Clear(); + } else if(this.activePageViewModel != null) { // No cache? Dispose active view. + disposeIfSupported(this.activePageViewModel); + this.activePageViewModel = null; + } + + } + + /// Child page that is currently being displayed by the view model + public TPageEnumeration? ActivePage { + get { return this.activePage; } + } + + /// Retrieves (and, if needed, creates) the view model for the active page + /// A view model for the active page on the multi-page view model + public object? ActivePageViewModel { + get { return this.activePageViewModel; } + } + + + /// Windowmanager that can create view models and display other views + protected IWindowManager WindowManager { + get { return this.windowManager; } + } + + /// Creates a view model for the specified page + /// Page for which a view model will be created + /// The view model for the specified page + protected abstract object CreateViewModelForPage(TPageEnumeration page); + + /// Switches to another page + /// New page to switch to + /// A task that will finish when the new page has been switched to + private Task switchPageAsync(TPageEnumeration? newPage) { + if(newPage.Equals(this.activePage)) { + return Task.CompletedTask; + } + + object? viewModelToDispose; + if(this.cachedViewModels == null) { + viewModelToDispose = this.activePageViewModel; + + object? newPageViewModel = CreateViewModelForPage(newPage); + + this.activePage = newPage; + this.activePageViewModel = newPageViewModel; + } else { + viewModelToDispose = null; + + // Double-checked locking to avoid creating a view model for the same page + // multiple times if the construction takes time + object newPageViewModel; + if(!this.cachedViewModels.TryGetValue(newPage, out newPageViewModel)) { + lock(this.cachedViewModels) { + if(!this.cachedViewModels.TryGetValue(newPage, out newPageViewModel)) { + newPageViewModel = CreateViewModelForPage(newPage); + this.cachedViewModels.TryAdd(newPage, newPageViewModel); + } + } + } + + this.activePage = newPage; + this.activePageViewModel = newPageViewModel; + } + + OnPropertyChanged(nameof(ActivePage)); + OnPropertyChanged(nameof(ActivePageViewModel)); + + disposeIfSupported(viewModelToDispose); + + return Task.CompletedTask; + } + + /// Disposes the specified object if it is disposable + /// Object that will be disposed if supported + private static void disposeIfSupported(object? potentiallyDisposable) { + var disposable = potentiallyDisposable as IDisposable; + if(disposable != null) { + disposable.Dispose(); + } + } + + /// Window manager that can be used to display other views + private readonly IWindowManager windowManager; + /// Delegate for the CreateViewModelForPage() method + private readonly Func createViewModelForPageDelegate; + /// Cached page view models, if caching is enabled + private readonly ConcurrentDictionary? cachedViewModels; + + /// Page that is currently active in the multi-page view model + private TPageEnumeration activePage; + /// View model for the active page + private object? activePageViewModel; + + } + +} // namespace Nuclex.Avalonia.ViewModels diff --git a/Source/WindowManager.cs b/Source/WindowManager.cs index d1f9823..fa5b2bf 100644 --- a/Source/WindowManager.cs +++ b/Source/WindowManager.cs @@ -1,7 +1,7 @@ #region Apache License 2.0 /* Nuclex Foundation libraries for .NET -Copyright (C) 2002-2025 Markus Ewald / Nuclex Development Labs +Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.