Initial porting attempt of my WinForms MVVM library to Avalonia; added IWindowScope as a new reature because this library should support Microsoft's dependency injector

This commit is contained in:
Markus Ewald 2025-02-12 19:16:24 +01:00 committed by Markus Ewald
commit 8619e7dc85
16 changed files with 1512 additions and 0 deletions

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<AssemblyName>Nuclex.Avalonia</AssemblyName>
<RootNamespace>Nuclex.Avalonia</RootNamespace>
<IntermediateOutputPath>obj\source</IntermediateOutputPath>
<Nullable>enable</Nullable>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Tests\**" />
<EmbeddedResource Remove="Tests\**" />
<None Remove="Tests\**" />
</ItemGroup>
<ItemGroup>
<None Remove=".git" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.1" />
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nuclex.Support\Nuclex.Support (netstandard-2.0).csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<AssemblyName>Nuclex.Support.Tests</AssemblyName>
<RootNamespace>Nuclex.Support.Tests</RootNamespace>
<IntermediateOutputPath>obj\tests</IntermediateOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Source\**" />
<EmbeddedResource Remove="Source\**" />
<None Remove="Source\**" />
</ItemGroup>
<ItemGroup>
<None Remove=".git" />
<None Remove="packages.config" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="3.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="Nuclex.Avalonia (netstandard-2.0).csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,50 @@
#region Apache License 2.0
/*
Nuclex .NET Framework
Copyright (C) 2002-2024 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.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Nuclex.Avalonia")]
[assembly: AssemblyProduct("Nuclex.Avalonia")]
[assembly: AssemblyDescription("Lean and elegant MVVM library with extras for Avalonia")]
[assembly: AssemblyCompany("Nuclex Development Labs")]
[assembly: AssemblyCopyright("Copyright © Markus Ewald / Nuclex Development Labs 2002-2025")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("08254ad9-394e-4638-8412-098c8c4a4c39")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
[assembly: AssemblyVersion("1.3.0.0")]

View File

@ -0,0 +1,62 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 Avalonia.Controls;
namespace Nuclex.Avalonia.AutoBinding {
/// <summary>
/// Binds a view to its model using a convention-over-configuration approach
/// </summary>
public class ConventionBinder : IAutoBinder {
/// <summary>Binds the specified view to an explicitly selected view model</summary>
/// <typeparam name="TViewModel">
/// Type of view model the view will be bound to
/// </typeparam>
/// <param name="view">View that will be bound to a view model</param>
/// <param name="viewModel">View model the view will be bound to</param>
public void Bind<TViewModel>(Control view, TViewModel? viewModel)
where TViewModel : class {
if(viewModel != null) {
bind(view, viewModel);
}
}
/// <summary>
/// Binds the specified view to the view model specified in its DataContext
/// </summary>
/// <param name="viewControl">View that will be bound</param>
public void Bind(Control viewControl) {
if(viewControl.DataContext != null) {
bind(viewControl, viewControl.DataContext);
}
}
/// <summary>Binds a view to a view model</summary>
/// <param name="view">View that will be bound</param>
/// <param name="viewModel">View model the view will be bound to</param>
private void bind(Control view, object viewModel) {
}
}
} // namespace Nuclex.Avalonia.AutoBinding

View File

@ -0,0 +1,46 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 Avalonia.Controls;
namespace Nuclex.Avalonia.AutoBinding {
/// <summary>Binds views to their view models</summary>
public interface IAutoBinder {
/// <summary>Binds the specified view to an explicitly selected view model</summary>
/// <typeparam name="TViewModel">
/// Type of view model the view will be bound to
/// </typeparam>
/// <param name="view">View that will be bound to a view model</param>
/// <param name="viewModel">View model the view will be bound to</param>
void Bind<TViewModel>(Control view, TViewModel? viewModel)
where TViewModel : class;
/// <summary>
/// Binds the specified view to the view model specified in its DataContext
/// </summary>
/// <param name="view">View that will be bound</param>
void Bind(Control view);
}
} // namespace Nuclex.Avalonia.AutoBinding

View File

@ -0,0 +1,35 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 Avalonia.Controls;
namespace Nuclex.Avalonia {
/// <summary>Allows the currently active top level window to be looked up</summary>
public interface IActiveWindowTracker {
/// <summary>The currently active top-level or modal window</summary>
Window? ActiveWindow { get; }
}
} // namespace Nuclex.Avalonia

114
Source/IWindowManager.cs Normal file
View File

@ -0,0 +1,114 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 Avalonia.Controls;
namespace Nuclex.Avalonia {
/// <summary>Manages open windows and connecting them to view models</summary>
public interface IWindowManager : IActiveWindowTracker {
/// <summary>Opens a view as a new root window of the application</summary>
/// <typeparam name="TViewModel">
/// Type of view model a root window will be opened for
/// </typeparam>
/// <param name="viewModel">
/// View model a window will be opened for. If null, the view model will be
/// created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <param name="disposeOnClose">
/// Whether the view model should be disposed when the view is closed
/// </param>
/// <returns>The window that has been opened by the window manager</returns>
Window OpenRoot<TViewModel>(
TViewModel? viewModel = null, bool disposeOnClose = true
) where TViewModel : class;
/// <summary>Displays a view as a modal window</summary>
/// <typeparam name="TViewModel">
/// Type of the view model for which a view will be displayed
/// </typeparam>
/// <param name="viewModel">
/// View model a modal window will be displayed for. If null, the view model will
/// be created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <param name="disposeOnClose">
/// Whether the view model should be disposed when the view is closed
/// </param>
/// <returns>The return value of the modal window</returns>
Task ShowModalAsync<TViewModel>(
TViewModel? viewModel = null, bool disposeOnClose = false
) where TViewModel : class;
/// <summary>Displays a view as a modal window</summary>
/// <typeparam name="TResult">
/// Type of result the modal dialog will return to the caller
/// </typeparam>
/// <typeparam name="TViewModel">
/// Type of the view model for which a view will be displayed
/// </typeparam>
/// <param name="viewModel">
/// View model a modal window will be displayed for. If null, the view model will
/// be created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <param name="disposeOnClose">
/// Whether the view model should be disposed when the view is closed
/// </param>
/// <returns>The return value of the modal window</returns>
Task<TResult> ShowModalAsync<TResult, TViewModel>(
TViewModel? viewModel = null, bool disposeOnClose = true
) where TViewModel : class;
/// <summary>Creates the view for the specified view model</summary>
/// <typeparam name="TViewModel">
/// Type of view model for which a view will be created
/// </typeparam>
/// <param name="viewModel">
/// View model a view will be created for. If null, the view model will be
/// created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <returns>The view for the specified view model</returns>
Control CreateView<TViewModel>(TViewModel? viewModel = null)
where TViewModel : class;
/// <summary>Creates a view model without a matching view</summary>
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
/// <returns>The new view model</returns>
/// <remarks>
/// <para>
/// This is useful if a view model needs to create child view models (i.e. paged container
/// and wants to ensure the same dependency injector (if any) if used as the window
/// manager uses for other view models it creates.
/// </para>
/// <para>
/// This way, view models can set up their child view models without having to immediately
/// bind a view to them. Later on, views can use the window manager to create a matching
/// child view and store it in a container.
/// </para>
/// </remarks>
TViewModel CreateViewModel<TViewModel>()
where TViewModel : class;
}
} // namespace Nuclex.Avalonia

64
Source/IWindowScope.cs Normal file
View File

@ -0,0 +1,64 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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;
namespace Nuclex.Avalonia {
/// <summary>Constructs views and view model in a scope</summary>
/// <remarks>
/// <para>
/// By default the <see cref="WindowManager" /> uses its own
/// <see cref="WindowManager.CreateInstance" /> method to construct views
/// and view models via <see cref="Activator.CreateInstance(Type)" />,
/// which is enough to create forms (which the Windows Forms designer already
/// requires to have parameterless constructors) and view models, so long as
/// they also have parameterless constructors.
/// </para>
/// <para>
/// To support dependency injection via constructor parameters, you can
/// inherit from the <see cref="WindowManager" /> and provide your own override
/// of <see cref="WindowManager.CreateInstance" /> that constructs the required
/// instance via your dependency injector. This is decent until you have multiple
/// view models all accessing the same resource (i.e. a database) via threads.
/// </para>
/// <para>
/// In this final case, &quot;scopes&quot; have become a common solution. Each
/// scope has access to singleton services (these exist for the lifetime of
/// the entire application), but there are also scoped services which will have
/// new instances constructed within each scope. By implementing the
/// <see cref="WindowManager.CreateWindowScope" /> method, you can make
/// the window manager set up an implicit scope per window or dialog.
/// </para>
/// </remarks>
public interface IWindowScope : IDisposable {
/// <summary>Creates an instance of the specified type in the scope</summary>
/// <param name="type">Type an instance will be created of</param>
/// <returns>The created instance</returns>
/// <remarks>
/// Use this to wire up your dependency injection container. By default,
/// the Activator class will be used to create instances which only works
/// if all of your view models are concrete classes.
/// </remarks>
object CreateInstance(Type type);
}
} // namespace Nuclex.Avalonia

View File

@ -0,0 +1,188 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
#if NET6_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using Avalonia.Controls;
using MessageBoxIcon = MsBox.Avalonia.Enums.Icon;
using MessageBoxButtons = MsBox.Avalonia.Enums.ButtonEnum;
using MessageDialogResult = MsBox.Avalonia.Enums.ButtonResult;
namespace Nuclex.Avalonia.Messages {
/// <summary>Uses Avalonia to display message boxes</summary>
public class AvaloniaMessagePresenter : IMessageService {
#region class MessageScope
/// <summary>Triggers the message displayed and acknowledged events</summary>
private class MessageScope : IDisposable {
/// <summary>
/// Initializes a new message scope, triggering the message displayed event
/// </summary>
/// <param name="self">Message service the scope belongs to</param>
/// <param name="image">Image of the message being displayed</param>
/// <param name="text">Text contained in the message being displayed</param>
public MessageScope(
AvaloniaMessagePresenter self, MessageBoxIcon image, MessageText text
) {
EventHandler<MessageEventArgs>? messageDisplayed = self.MessageDisplaying;
if(messageDisplayed != null) {
messageDisplayed(this, new MessageEventArgs(image, text));
}
this.self = self;
}
/// <summary>Triggers the message acknowledged event</summary>
public void Dispose() {
EventHandler? messageAcknowledged = self.MessageAcknowledged;
if(messageAcknowledged != null) {
messageAcknowledged(this, EventArgs.Empty);
}
}
/// <summary>Message service the scope belongs to</summary>
private AvaloniaMessagePresenter self;
}
#endregion // class MessageScope
/// <summary>Triggered when a message is displayed to the user</summary>
public event EventHandler<MessageEventArgs>? MessageDisplaying;
/// <summary>Triggered when the user has acknowledged the current message</summary>
public event EventHandler? MessageAcknowledged;
/// <summary>Initializes a new Avalonia message service</summary>
/// <param name="tracker">Used to determine the current top-level window</param>
public AvaloniaMessagePresenter(IActiveWindowTracker tracker) {
this.tracker = tracker;
}
/// <summary>Asks the user a question that can be answered via several buttons</summary>
/// <param name="image">Image that will be shown on the message box</param>
/// <param name="text">Text that will be shown to the user</param>
/// <param name="buttons">Buttons available for the user to click on</param>
/// <returns>The button the user has clicked on</returns>
public Task<MessageDialogResult> ShowQuestionAsync(
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
) {
using(var scope = new MessageScope(this, image, text)) {
MsBox.Avalonia.Base.IMsBox<MessageDialogResult> messageBox = (
MsBox.Avalonia.MessageBoxManager.GetMessageBoxStandard(
new MsBox.Avalonia.Dto.MessageBoxStandardParams() {
ContentTitle = text.Caption,
ContentHeader = text.Message,
ContentMessage = text.Details ?? string.Empty,
ButtonDefinitions = buttons,
Icon = image,
WindowStartupLocation = WindowStartupLocation.CenterOwner
}
)
);
return messageBox.ShowAsync(); // TODO: Make modal to current or main window
}
}
/// <summary>Displays a notification to the user</summary>
/// <param name="image">Image that will be shown on the message bx</param>
/// <param name="text">Text that will be shown to the user</param>
public Task ShowNotificationAsync(MessageBoxIcon image, MessageText text) {
using(var scope = new MessageScope(this, image, text)) {
MsBox.Avalonia.Base.IMsBox<MessageDialogResult> messageBox = (
MsBox.Avalonia.MessageBoxManager.GetMessageBoxStandard(
new MsBox.Avalonia.Dto.MessageBoxStandardParams() {
ContentTitle = text.Caption,
ContentHeader = text.Message,
ContentMessage = text.Details ?? string.Empty,
ButtonDefinitions = MessageBoxButtons.Ok,
Icon = image,
WindowStartupLocation = WindowStartupLocation.CenterOwner
}
)
);
Window? activeWindow = this.tracker.ActiveWindow;
if(activeWindow == null) {
return messageBox.ShowAsync();
} else {
//return messageBox.ShowAsPopupAsync(activeWindow);
return messageBox.ShowWindowDialogAsync(activeWindow);
}
}
}
/// <summary>Reports an error using the system's message box functions</summary>
/// <param name="title">Title of the message box</param>
/// <param name="message">Message text that will be displayed</param>
public static void FallbackReportError(string title, string message) {
// TODO: Escape quotes for the command-line tools
// TODO: Wait for the child process to exit so display is certain
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
MessageBoxW(IntPtr.Zero, message, title, MB_OK | MB_ICONEXCLAMATION);
} else if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
Process.Start("zenity", $"--error --title=\"{title}\" --text=\"{message}\"");
} else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
Process.Start("osascript", $"-e 'display dialog \"{message}\" with title \"{title}\" with icon stop'");
}
}
/// <summary>Windows only: display a message box with an OK button</summary>
#if NET6_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
private const uint MB_OK = 0x00000000;
/// <summary>Windows only: display a message box with an Exclamation icon</summary>
#if NET6_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
private const uint MB_ICONEXCLAMATION = 0x00000030;
/// <summary>Windows only: displays a native Windows message box</summary>
/// <param name="parentWindowHandle">Handle of the window that owns the message box</param>
/// <param name="text">Text that should be in the message box</param>
/// <param name="caption">Caption or window title of the message box</param>
/// <param name="type">Which icons and buttons that message box should have</param>
/// <returns>How the user closed the message box and which button they clicked</returns>
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
#if NET6_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
private static extern int MessageBoxW(IntPtr parentWindowHandle, string text, string caption, uint type);
// <summary>Provides the currently active top-level window</summary>
private IActiveWindowTracker tracker;
}
} // namespace Nuclex.Avalonia.Messages

View File

@ -0,0 +1,58 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 MessageBoxIcon = MsBox.Avalonia.Enums.Icon;
using MessageBoxButtons = MsBox.Avalonia.Enums.ButtonEnum;
using MessageDialogResult = MsBox.Avalonia.Enums.ButtonResult;
namespace Nuclex.Avalonia.Messages {
/// <summary>Performs simple user interaction</summary>
/// <remarks>
/// Methods provided by this service can be covered using plain old message boxes
/// and do not require special dialogs or calls to the task dialog API.
/// </remarks>
public interface IMessageService {
/// <summary>Triggered when a message is about to be displayed to the user</summary>
event EventHandler<MessageEventArgs> MessageDisplaying;
/// <summary>Triggered when the user has acknowledged the current message</summary>
event EventHandler MessageAcknowledged;
/// <summary>Asks the user a question that can be answered via several buttons</summary>
/// <param name="image">Image that will be shown on the message box</param>
/// <param name="text">Text that will be shown to the user</param>
/// <param name="buttons">Buttons available for the user to click on</param>
/// <returns>The button the user has clicked on</returns>
Task<MessageDialogResult> ShowQuestionAsync(
MessageBoxIcon image, MessageText text, MessageBoxButtons buttons
);
/// <summary>Displays a notification to the user</summary>
/// <param name="image">Image that will be shown on the message bx</param>
/// <param name="text">Text that will be shown to the user</param>
Task ShowNotificationAsync(MessageBoxIcon image, MessageText text);
}
} // namespace Nuclex.Avalonia.Messages

View File

@ -0,0 +1,54 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 MessageBoxIcon = MsBox.Avalonia.Enums.Icon;
namespace Nuclex.Avalonia.Messages {
/// <summary>Provides a displayed message and its severity to event subscribers</summary>
public class MessageEventArgs : EventArgs {
/// <summary>Initializes a new message box event argument container</summary>
/// <param name="image">Image the message box will be displaying</param>
/// <param name="text">Text that will be displayed in the message box</param>
public MessageEventArgs(MessageBoxIcon image, MessageText text) {
this.image = image;
this.text = text;
}
/// <summary>Image that indicates the severity of the message being displayed</summary>
public MessageBoxIcon Image {
get { return this.image; }
}
/// <summary>Text that is being displayed in the message box</summary>
public MessageText Text {
get { return this.text; }
}
/// <summary>Image that indicates the severity of the message being displayed</summary>
private MessageBoxIcon image;
/// <summary>Text that is being displayed in the message box</summary>
private MessageText text;
}
} // namespace Nuclex.Avalonia.Messages

View File

@ -0,0 +1,117 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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;
#if NET6_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using MessageBoxIcon = MsBox.Avalonia.Enums.Icon;
using MessageBoxButtons = MsBox.Avalonia.Enums.ButtonEnum;
using MessageDialogResult = MsBox.Avalonia.Enums.ButtonResult;
namespace Nuclex.Avalonia.Messages {
/// <summary>Contains helper methods for the message service</summary>
#if NET6_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public static class MessageServiceHelper {
/// <summary>Asks the user a question that can be answered with yes or no</summary>
/// <param name="messageService">
/// Message service that will be used to display the question
/// </param>
/// <param name="text">Text that will be shown on the message box</param>
/// <returns>The button the user has clicked on</returns>
public static Task<MessageDialogResult> AskYesNoAsync(
this IMessageService messageService, MessageText text
) {
return messageService.ShowQuestionAsync(
MessageBoxIcon.Question, text, MessageBoxButtons.YesNo
);
}
/// <summary>Asks the user a question that can be answered with ok or cancel</summary>
/// <param name="messageService">
/// Message service that will be used to display the question
/// </param>
/// <param name="text">Text that will be shown on the message box</param>
/// <returns>The button the user has clicked on</returns>
public static Task<MessageDialogResult> AskOkCancelAsync(
this IMessageService messageService, MessageText text
) {
return messageService.ShowQuestionAsync(
MessageBoxIcon.Question, text, MessageBoxButtons.OkCancel
);
}
/// <summary>
/// Asks the user a question that can be answered with yes, no or cancel
/// </summary>
/// <param name="messageService">
/// Message service that will be used to display the question
/// </param>
/// <param name="text">Text that will be shown on the message box</param>
/// <returns>The button the user has clicked on</returns>
public static Task<MessageDialogResult> AskYesNoCancelAsync(
this IMessageService messageService, MessageText text
) {
return messageService.ShowQuestionAsync(
MessageBoxIcon.Question, text, MessageBoxButtons.YesNoCancel
);
}
/// <summary>Displays an informative message</summary>
/// <param name="messageService">
/// Message service that will be used to display the warning
/// </param>
/// <param name="text">Text to be displayed on the warning message</param>
public static Task InformAsync(
this IMessageService messageService, MessageText text
) {
return messageService.ShowNotificationAsync(MessageBoxIcon.Info, text);
}
/// <summary>Displays a warning</summary>
/// <param name="messageService">
/// Message service that will be used to display the warning
/// </param>
/// <param name="text">Text to be displayed on the warning message</param>
public static Task WarnAsync(
this IMessageService messageService, MessageText text
) {
return messageService.ShowNotificationAsync(MessageBoxIcon.Warning, text);
}
/// <summary>Reports an error</summary>
/// <param name="messageService">
/// Message service that will be used to display the warning
/// </param>
/// <param name="text">Text to be displayed on the warning message</param>
public static Task ReportErrorAsync(
this IMessageService messageService, MessageText text
) {
return messageService.ShowNotificationAsync(MessageBoxIcon.Error, text);
}
}
} // namespace Nuclex.Avalonia.Messages

View File

@ -0,0 +1,57 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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;
namespace Nuclex.Avalonia.Messages {
/// <summary>Text that will be displayed in a message box</summary>
public class MessageText {
/// <summary>Initializs a new message text</summary>
public MessageText() {
Caption = "Message";
Message = "No message provided";
}
/// <summary>Initializes a new message text by copying another instance</summary>
/// <param name="other">Instance that will be copied</param>
public MessageText(MessageText other) {
Caption = other.Caption;
Message = other.Message;
Details = other.Details;
ExpandedDetails = other.ExpandedDetails;
}
/// <summary>The caption used when the is displayed in a message box</summary>
public string Caption { get; set; }
/// <summary>Main message being displayed to the user</summary>
public string Message { get; set; }
/// <summary>Message details shown below the main message</summary>
public string? Details { get; set; }
/// <summary>
/// Additional informations the user can display by expanding
/// the message dialog. Can be null, in which case the message dialog
/// will not be expandable.
/// </summary>
public string? ExpandedDetails { get; set; }
}
} // namespace Nuclex.Avalonia.Messages

View File

@ -0,0 +1,37 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 Avalonia.Controls;
namespace Nuclex.Avalonia {
/// <summary>Dummy implementation of the active window tracker service</summary>
internal class NullActiveWindowTracker : IActiveWindowTracker {
/// <summary>The default instance of the dummy window tracker</summary>
public static readonly NullActiveWindowTracker Default = new NullActiveWindowTracker();
/// <summary>The currently active top-level or modal window</summary>
public Window? ActiveWindow { get { return null; } }
}
} // namespace Nuclex.Avalonia

View File

@ -0,0 +1,39 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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 Nuclex.Support;
namespace Nuclex.Windows.Forms.ViewModels {
/// <summary>Interface for dialog view models (typically modal ones)</summary>
public interface IDialogViewModel {
/// <summary>Indicates that the view should close</summary>
event EventHandler Submitted;
/// <summary>Indicates that the dialog should be closed</summary>
/// <returns>A task that finishes when the submit notification has been sent</returns>
Task SubmitAsync();
}
} // namespace Nuclex.Windows.Forms.ViewModels

524
Source/WindowManager.cs Normal file
View File

@ -0,0 +1,524 @@
#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2025 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.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Nuclex.Support;
using Nuclex.Avalonia.AutoBinding;
namespace Nuclex.Avalonia {
/// <summary>Manages an application's windows and views</summary>
public class WindowManager : Observable, IWindowManager {
#region class WindowManagerScope
/// <summary>Global scope that uses the WindowManager's CreateInstance()</summary>
public class WindowManagerScope : IWindowScope {
/// <summary>Initializes a new global window scope</summary>
/// <param name="windowManager">
/// Window manager whose CreateInstance() method will be used
/// </param>
public WindowManagerScope(WindowManager windowManager) {
this.windowManager = windowManager;
}
/// <summary>Creates an instance of the specified type in the scope</summary>
/// <param name="type">Type an instance will be created of</param>
/// <returns>The created instance</returns>
object IWindowScope.CreateInstance(Type type) {
return this.windowManager.CreateInstance(type);
}
/// <summary>Does nothing because this is the global fallback scope</summary>
void IDisposable.Dispose() {}
/// <summary>WindowManager whose CreateInstance() method will be wrapped</summary>
private WindowManager windowManager;
}
#endregion // class WindwoManagerScope
#region class CancellableDisposer
/// <summary>Disposes an object that potentially implements IDisposable</summary>
private struct CancellableDisposer : IDisposable {
/// <summary>Initializes a new cancellable disposer</summary>
/// <param name="potentiallyDisposable">
/// Object that potentially implements IDisposable
/// </param>
public CancellableDisposer(object? potentiallyDisposable = null) {
this.potentiallyDisposable = potentiallyDisposable;
}
/// <summary>
/// Disposes the assigned object if the disposer has not been cancelled
/// </summary>
public void Dispose() {
IDisposable? disposable = this.potentiallyDisposable as IDisposable;
if(disposable != null) {
disposable.Dispose();
}
}
/// <summary>Cancels the call to Dispose(), keeping the object alive</summary>
public void Dismiss() {
this.potentiallyDisposable = null;
}
/// <summary>Assigns a new potentially disposable object</summary>
/// <param name="potentiallyDisposable">
/// Potentially disposable object that the disposer will dispose
/// </param>
public void Set(object? potentiallyDisposable) {
this.potentiallyDisposable = potentiallyDisposable;
}
/// <summary>Object that will be disposed unless the disposer is cancelled</summary>
private object? potentiallyDisposable;
}
#endregion // class CancellableDisposer
/// <summary>Initializes a new window manager</summary>
/// <param name="autoBinder">
/// View model binder that will be used to bind all created views to their models
/// </param>
public WindowManager(IAutoBinder? autoBinder = null) {
this.autoBinder = autoBinder;
this.rootWindowActivatedDelegate = rootWindowActivated;
this.rootWindowClosedDelegate = rootWindowClosed;
this.windowManagerAsScope = new WindowManagerScope(this);
this.viewTypesForViewModels = new ConcurrentDictionary<Type, Type>();
}
/// <summary>The currently active top-level or modal window</summary>
public Window? ActiveWindow {
get { return this.activeWindow; }
private set {
if(value != this.activeWindow) {
this.activeWindow = value;
OnPropertyChanged(nameof(ActiveWindow));
}
}
}
/// <summary>Opens a view as a new root window of the application</summary>
/// <typeparam name="TViewModel">
/// Type of view model a root window will be opened for
/// </typeparam>
/// <param name="viewModel">
/// View model a window will be opened for. If null, the view model will be
/// created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <param name="disposeOnClose">
/// Whether the view model should be disposed when the view is closed
/// </param>
/// <returns>The window that has been opened by the window manager</returns>
public Window OpenRoot<TViewModel>(
TViewModel? viewModel = null, bool disposeOnClose = false
) where TViewModel : class {
Window window = (Window)CreateView(viewModel);
window.Activated += this.rootWindowActivatedDelegate;
window.Closed += this.rootWindowClosedDelegate;
// If we either created the view model or the user explicitly asked us to
// dispose their view model, prepare the window so that it will dispose its
// view model when the window is done.
if((viewModel == null) || disposeOnClose) {
setupViewModelDisposal(window);
}
window.Show();
return window;
}
/// <summary>Displays a view as a modal window</summary>
/// <typeparam name="TViewModel">
/// Type of the view model for which a view will be displayed
/// </typeparam>
/// <param name="viewModel">
/// View model a modal window will be displayed for. If null, the view model will
/// be created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <param name="disposeOnClose">
/// Whether the view model should be disposed when the view is closed
/// </param>
/// <returns>The return value of the modal window</returns>
public Task ShowModalAsync<TViewModel>(
TViewModel? viewModel = null, bool disposeOnClose = false
) where TViewModel : class {
return ShowModalAsync<object?, TViewModel>(viewModel, disposeOnClose);
}
/// <summary>Displays a view as a modal window</summary>
/// <typeparam name="TResult">
/// Type of result the modal dialog will return to the caller
/// </typeparam>
/// <typeparam name="TViewModel">
/// Type of the view model for which a view will be displayed
/// </typeparam>
/// <param name="viewModel">
/// View model a modal window will be displayed for. If null, the view model will
/// be created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <param name="disposeOnClose">
/// Whether the view model should be disposed when the view is closed
/// </param>
/// <returns>The return value of the modal window</returns>
public Task<TResult> ShowModalAsync<TResult, TViewModel>(
TViewModel? viewModel = null, bool disposeOnClose = false
) where TViewModel : class {
if(this.activeWindow == null) {
throw new InvalidOperationException("Showing a modal dialog requires an active window");
}
Window window = (Window)CreateView(viewModel);
Window? parentWindow = this.activeWindow;
window.Activated += this.rootWindowActivatedDelegate;
try {
// If we either created the view model or the user explicitly asked us to
// dispose his view model, prepare the window so that it will dispose its
// view model when the window is done.
if((viewModel == null) || disposeOnClose) {
setupViewModelDisposal(window);
}
return window.ShowDialog<TResult>(this.activeWindow);
}
finally {
window.Activated -= this.rootWindowActivatedDelegate;
ActiveWindow = parentWindow;
if(disposeOnClose) {
disposeIfDisposable(window);
}
}
}
/// <summary>Creates the view for the specified view model</summary>
/// <typeparam name="TViewModel">
/// Type of view model for which a view will be created
/// </typeparam>
/// <param name="viewModel">
/// View model a view will be created for. If null, the view model will be
/// created as well (unless the dialog already specifies one as a resource)
/// </param>
/// <returns>The view for the specified view model</returns>
public virtual Control CreateView<TViewModel>(
TViewModel? viewModel = null
) where TViewModel : class {
Control viewControl;
{
Type viewType = LocateViewForViewModel(typeof(TViewModel));
IWindowScope scope = CreateWindowScope();
using(var scopeDisposer = new CancellableDisposer(scope)) {
viewControl = (Control)scope.CreateInstance(viewType);
using(var viewDisposer = new CancellableDisposer(viewControl)) {
// Create a view model if none was provided, and in either case assign
// the view model to the view (provided it implements IView).
using(var viewModelDisposer = new CancellableDisposer()) {
if(viewModel == null) { // No view model provided, create one
if(viewControl.DataContext == null) { // View has no view model
viewModel = (TViewModel)scope.CreateInstance(typeof(TViewModel));
viewModelDisposer.Set(viewModel);
viewControl.DataContext = viewModel;
} else { // There's an existing view model
viewModel = viewControl.DataContext as TViewModel;
if(viewModel == null) { // The existing view model is another type
viewModel = (TViewModel)scope.CreateInstance(typeof(TViewModel));
viewModelDisposer.Set(viewModel);
viewControl.DataContext = viewModel;
}
}
} else { // Caller has provided a view model
viewControl.DataContext = viewModel;
}
// If an auto binder was provided, automatically bind the view to the view model
if(this.autoBinder != null) {
this.autoBinder.Bind(viewControl, viewModel);
}
viewModelDisposer.Dismiss(); // Everything went well, we keep the view model
}
viewDisposer.Dismiss(); // Everything went well, we keep the view
}
scopeDisposer.Dismiss(); // Everything went well, we keep the scope
}
setupScopeDisposal(viewControl, scope);
} // beauty scope
return viewControl;
}
/// <summary>Creates a view model without a matching view</summary>
/// <typeparam name="TViewModel">Type of view model that will be created</typeparam>
/// <returns>The new view model</returns>
/// <remarks>
/// <para>
/// This is useful if a view model needs to create child view models (i.e. paged container
/// and wants to ensure the same dependency injector (if any) if used as the window
/// manager uses for other view models it creates.
/// </para>
/// <para>
/// This way, view models can set up their child view models without having to immediately
/// bind a view to them. Later on, views can use the window manager to create a matching
/// child view and store it in a container.
/// </para>
/// </remarks>
public TViewModel CreateViewModel<TViewModel>() where TViewModel : class {
return (TViewModel)CreateInstance(typeof(TViewModel));
}
/// <summary>Locates the view that will be used to a view model</summary>
/// <param name="viewModelType">
/// Type of view model for which the view will be located
/// </param>
/// <returns>The type of view that should be used for the specified view model</returns>
protected virtual Type LocateViewForViewModel(Type viewModelType) {
Type? viewType;
if(!this.viewTypesForViewModels.TryGetValue(viewModelType, out viewType)) {
string viewName = viewModelType.Name;
if(viewName.EndsWith("ViewModel")) {
viewName = viewName.Substring(0, viewName.Length - 9);
}
Type[] exportedTypes = viewModelType.Assembly.GetExportedTypes();
Type[] namespaceTypes = filterTypesByNamespace(exportedTypes, viewModelType.Namespace);
// First, search the own namespace (because if two identical view models exist in
// different namespaces, the one in the same namespace is most likely the desired one)
viewType = findBestMatch(
namespaceTypes,
viewName + "View",
viewName + "Page",
viewName + "Form",
viewName + "Window",
viewName + "Dialog",
viewName + "Control"
);
// If the view model doesn't exist in the same namespace, expand the search to
// the entire assembly the view is in.
if(viewType == null) {
viewType = findBestMatch(
exportedTypes,
viewName + "View",
viewName + "Page",
viewName + "Form",
viewName + "Window",
viewName + "Dialog",
viewName + "Control"
);
}
// Still no view found? We give up!
if(viewType == null) {
throw new InvalidOperationException(
string.Format("Could not locate view for view model '{0}'", viewModelType.Name)
);
}
this.viewTypesForViewModels.TryAdd(viewModelType, viewType);
}
return viewType;
}
/// <summary>Creates an instance of the specified type</summary>
/// <param name="type">Type an instance will be created of</param>
/// <returns>The created instance</returns>
/// <remarks>
/// Use this to wire up your dependency injection container. By default,
/// the Activator class will be used to create instances which only works
/// if all of your view models are concrete classes.
/// </remarks>
protected virtual object CreateInstance(Type type) {
return Activator.CreateInstance(type);
}
/// <summary>Creates a new scope in which window-specific instances live</summary>
/// <returns>
/// A new scope in which scoped services requested by the window's view model
/// will live
/// </returns>
/// <remarks>
/// If you do not override this method, services will be constructed through
/// the normal <see cref="CreateInstance" /> method (which actually may not
/// work without managing your own service scope in case your dependency
/// injector supports scopes and some of your services are scoped). By
/// overriding this method, you can automatically cause a new scope to be
/// created for each window or dialog. That way, an implicit service scope
/// will cover the lifetime of each window and its view model and any
/// non-singleton services will use new instances, avoiding, for example,
/// that multiple dialogs access the same database connection simultaneously.
/// </remarks>
protected virtual IWindowScope CreateWindowScope() {
return this.windowManagerAsScope;
}
/// <summary>Called when one of the application's root windows is closed</summary>
/// <param name="sender">Window that has been closed</param>
/// <param name="arguments">Not used</param>
private void rootWindowClosed(object sender, EventArgs arguments) {
Window closedWindow = (Window)sender;
closedWindow.Closed -= this.rootWindowClosedDelegate;
closedWindow.Activated -= this.rootWindowActivatedDelegate;
lock(this) {
ActiveWindow = null;
}
// The IoC container already does this and it's the user's responsibility anyways
//disposeIfDisposable(closedWindow);
}
/// <summary>Called when one of the application's root windows is activated</summary>
/// <param name="sender">Window that has been put in the foreground</param>
/// <param name="arguments">Not used</param>
private void rootWindowActivated(object? sender, EventArgs arguments) {
lock(this) {
ActiveWindow = (Window?)sender;
}
}
/// <summary>Tries to find the best match for a named type in a list of types</summary>
/// <param name="types">List of types the search will take place in</param>
/// <param name="typeNames">
/// The candidates the method will look for, starting with the best match
/// </param>
/// <returns>The best match in the list of types, if any match was found</returns>
private static Type? findBestMatch(Type[] types, params string[] typeNames) {
int bestMatchFound = typeNames.Length;
Type? type = null;
for(int index = 0; index < types.Length; ++index) {
for(int nameIndex = 0; nameIndex < bestMatchFound; ++nameIndex) {
if(types[index].Name == typeNames[nameIndex]) {
bestMatchFound = nameIndex;
type = types[index];
if(bestMatchFound == 0) { // There can be no better match
return type;
}
break;
}
}
}
return type;
}
/// <summary>Disposes the specified object if it implements IDisposable</summary>
/// <typeparam name="T">Type of object that will disposed if possible</typeparam>
/// <param name="instance">Object that the method will attempt to dispose</param>
private static void disposeIfDisposable<T>(T instance) where T : class {
var disposable = instance as IDisposable;
if(disposable != null) {
disposable.Dispose();
}
}
/// <summary>Attaches a view model disposer to a control</summary>
/// <param name="control">
/// Control whose view model will be disposed when it is itself disposed
/// </param>
private static void setupViewModelDisposal(Control control) {
IDisposable? disposableViewModel = control.DataContext as IDisposable;
if(disposableViewModel != null) {
control.Unloaded += delegate(object? sender, RoutedEventArgs arguments) {
disposableViewModel.Dispose();
control.DataContext = null;
};
}
//window.Tag = "DisposeViewModelOnClose"; // TODO: Wrap SetProp() instead?
//window.SetValue(DisposeViewModelOnCloseProperty, true);
}
/// <summary>Attaches a scope disposer to a control</summary>
/// <param name="control">
/// Control that will dispose a scope when it is itself disposed
/// </param>
/// <param name="scope">
/// Scope that will be disposed together with the control
/// </param>
private void setupScopeDisposal(Control control, IWindowScope scope) {
IDisposable disposableScope = (IDisposable)scope;
if(disposableScope != null) {
control.Unloaded += delegate(object? sender, RoutedEventArgs arguments) {
disposableScope.Dispose();
};
}
}
/// <summary>Filters a list of types to contain only those in a specific namespace</summary>
/// <param name="exportedTypes">List of exported types that will be filtered</param>
/// <param name="filteredNamespace">
/// Namespace the types in the filtered list will be in
/// </param>
/// <returns>A subset of the specified types that are in the provided namespace</returns>
private static Type[] filterTypesByNamespace(Type[] exportedTypes, string? filteredNamespace) {
var filteredTypes = new List<Type>(exportedTypes.Length / 2);
for(int index = 0; index < exportedTypes.Length; ++index) {
Type exportedType = exportedTypes[index];
if(exportedType.Namespace == filteredNamespace) {
filteredTypes.Add(exportedType);
}
}
return filteredTypes.ToArray();
}
/// <summary>The application's currently active root window</summary>
private Window? activeWindow;
/// <summary>Invoked when a root window is put in the foreground</summary>
private EventHandler rootWindowActivatedDelegate;
/// <summary>Invoked when a root window has been closed</summary>
private EventHandler rootWindowClosedDelegate;
/// <summary>Scope that uses the WindowManager's global CreateInstance() method</summary>
private WindowManagerScope windowManagerAsScope;
/// <summary>View model binder that will be used on all created views</summary>
private IAutoBinder? autoBinder;
/// <summary>Caches the view types to use for a view model</summary>
private ConcurrentDictionary<Type, Type> viewTypesForViewModels;
}
} // namespace Nuclex.Avalonia