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:
commit
225da0d7e9
34
Nuclex.Avalonia (netstandard-2.0).csproj
Normal file
34
Nuclex.Avalonia (netstandard-2.0).csproj
Normal 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>
|
33
Nuclex.Avalonia.Tests (netstandard-2.0).csproj
Normal file
33
Nuclex.Avalonia.Tests (netstandard-2.0).csproj
Normal 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>
|
50
Properties/AssemblyInfo.cs
Normal file
50
Properties/AssemblyInfo.cs
Normal 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")]
|
62
Source/AutoBinding/ConventionBinder.cs
Normal file
62
Source/AutoBinding/ConventionBinder.cs
Normal 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
|
46
Source/AutoBinding/IAutoBinder.cs
Normal file
46
Source/AutoBinding/IAutoBinder.cs
Normal 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
|
35
Source/IActiveWindowTracker.cs
Normal file
35
Source/IActiveWindowTracker.cs
Normal 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
114
Source/IWindowManager.cs
Normal 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
64
Source/IWindowScope.cs
Normal 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, "scopes" 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
|
188
Source/Messages/AvaloniaMessagePresenter.cs
Normal file
188
Source/Messages/AvaloniaMessagePresenter.cs
Normal 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
|
||||||
|
|
58
Source/Messages/IMessageService.cs
Normal file
58
Source/Messages/IMessageService.cs
Normal 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
|
54
Source/Messages/MessageEventArgs.cs
Normal file
54
Source/Messages/MessageEventArgs.cs
Normal 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
|
117
Source/Messages/MessageServiceHelper.cs
Normal file
117
Source/Messages/MessageServiceHelper.cs
Normal 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
|
57
Source/Messages/MessageText.cs
Normal file
57
Source/Messages/MessageText.cs
Normal 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
|
37
Source/NullActiveWindowTracker.cs
Normal file
37
Source/NullActiveWindowTracker.cs
Normal 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
|
39
Source/ViewModels/IDialogViewModel.cs
Normal file
39
Source/ViewModels/IDialogViewModel.cs
Normal 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
524
Source/WindowManager.cs
Normal 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
|
Loading…
Reference in New Issue
Block a user