diff --git a/PrismLibrary_Avalonia.slnf b/PrismLibrary_Avalonia.slnf
new file mode 100644
index 0000000000..1b49b7e34a
--- /dev/null
+++ b/PrismLibrary_Avalonia.slnf
@@ -0,0 +1,14 @@
+{
+ "solution": {
+ "path": "Prism.Avalonia.sln",
+ "projects": [
+ "src\\Containers\\Prism.DryIoc.Shared\\Prism.DryIoc.Shared.shproj",
+ "src\\Avalonia\\Prism.Avalonia\\Prism.Avalonia.csproj",
+ "src\\Avalonia\\Prism.DryIoc.Avalonia\\Prism.DryIoc.Avalonia.csproj",
+ "tests\\Avalonia\\Prism.Avalonia.Tests\\Prism.Avalonia.Tests.csproj",
+ "tests\\Avalonia\\Prism.Container.Avalonia.Shared\\Prism.Container.Avalonia.Shared.shproj",
+ "tests\\Avalonia\\Prism.DryIoc.Avalonia.Tests\\Prism.DryIoc.Avalonia.Tests.csproj",
+ "tests\\Avalonia\\Prism.IocContainer.Avalonia.Tests.Support\\Prism.IocContainer.Avalonia.Tests.Support.csproj"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs b/src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs
new file mode 100644
index 0000000000..a28ec34cd2
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Common/MvvmHelpers.cs
@@ -0,0 +1,80 @@
+using System;
+using System.ComponentModel;
+using Avalonia.Controls;
+using Prism.Mvvm;
+
+namespace Prism.Common
+{
+ ///
+ /// Helper class for MVVM.
+ ///
+ public static class MvvmHelpers
+ {
+ ///
+ /// Sets the AutoWireViewModel property to true for the .
+ ///
+ ///
+ /// The AutoWireViewModel property will only be set to true if the view
+ /// is a , the DataContext of the view is null, and
+ /// the AutoWireViewModel property of the view is null.
+ ///
+ /// The View or ViewModel.
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ internal static void AutowireViewModel(object viewOrViewModel)
+ {
+ if (viewOrViewModel is Control view &&
+ view.DataContext is null &&
+ ViewModelLocator.GetAutoWireViewModel(view) is null)
+ {
+ ViewModelLocator.SetAutoWireViewModel(view, true);
+ }
+ }
+
+ ////#endif
+
+ ///
+ /// Perform an on a view and ViewModel.
+ ///
+ ///
+ /// The action will be performed on the view and its ViewModel if they implement .
+ ///
+ /// The parameter type.
+ /// The view to perform the on.
+ /// The to perform.
+ public static void ViewAndViewModelAction(object view, Action action) where T : class
+ {
+ if (view is T viewAsT)
+ action(viewAsT);
+
+ if (view is Control element && element.DataContext is T viewModelAsT)
+ {
+ action(viewModelAsT);
+ }
+ }
+
+ ///
+ /// Get an implementer from a view or ViewModel.
+ ///
+ ///
+ /// If the view implements it will be returned.
+ /// Otherwise if the view's implements it will be returned instead.
+ ///
+ /// The implementer type to get.
+ /// The view to get from.
+ /// view or ViewModel as .
+ public static T GetImplementerFromViewOrViewModel(object view) where T : class
+ {
+ if (view is T viewAsT)
+ {
+ return viewAsT;
+ }
+
+ if (view is Control element && element.DataContext is T vmAsT)
+ {
+ return vmAsT;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs b/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs
new file mode 100644
index 0000000000..cde4a6d88a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Common/ObservableObject.cs
@@ -0,0 +1,57 @@
+using System;
+using System.ComponentModel;
+using Avalonia;
+using Avalonia.Controls;
+
+namespace Prism.Common
+{
+ ///
+ /// Class that wraps an object, so that other classes can notify for Change events. Typically, this class is set as
+ /// a Dependency Property on AvaloniaObjects, and allows other classes to observe any changes in the Value.
+ ///
+ ///
+ /// This class is required, because in Silverlight, it's not possible to receive Change notifications for Dependency properties that you do not own.
+ ///
+ /// The type of the property that's wrapped in the Observable object
+ public class ObservableObject : Control, INotifyPropertyChanged
+ {
+ ///
+ /// Identifies the Value property of the ObservableObject
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "This is the pattern for WPF dependency properties")]
+ public static readonly StyledProperty ValueProperty =
+ AvaloniaProperty.Register(name: nameof(Value));
+
+ //StyledProperty.Register("Value", typeof(T), typeof(ObservableObject), new PropertyMetadata(ValueChangedCallback));
+
+ ///
+ /// Event that gets invoked when the Value property changes.
+ ///
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ ///
+ /// The value that's wrapped inside the ObservableObject.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
+ public T Value
+ {
+ get { return (T)this.GetValue(ValueProperty); }
+ set { this.SetValue(ValueProperty, value); }
+ }
+
+ private static void ValueChangedCallback(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
+ {
+ ObservableObject thisInstance = ((ObservableObject)d);
+ PropertyChangedEventHandler eventHandler = thisInstance.PropertyChanged;
+ if (eventHandler != null)
+ {
+ eventHandler(thisInstance, new PropertyChangedEventArgs(nameof(Value)));
+ }
+ }
+
+ static ObservableObject()
+ {
+ ValueProperty.Changed.Subscribe(args => ValueChangedCallback(args?.Sender, args));
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs b/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs
new file mode 100644
index 0000000000..bfdc794627
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/Dialog.cs
@@ -0,0 +1,76 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Styling;
+
+namespace Prism.Dialogs
+{
+ ///
+ /// This class contains attached properties.
+ ///
+ public class Dialog
+ {
+ /// Identifies the WindowStyle attached property.
+ /// This attached property is used to specify the style of a .
+ public static readonly AvaloniaProperty WindowStyleProperty =
+ AvaloniaProperty.RegisterAttached("WindowStyle", typeof(Dialog));
+
+ /// Identifies the WindowStartupLocation attached property.
+ /// This attached property is used to specify the startup location of a .
+ public static readonly AvaloniaProperty WindowStartupLocationProperty =
+ AvaloniaProperty.RegisterAttached(
+ name: "WindowStartupLocation",
+ ownerType: typeof(Dialog));
+
+ public Dialog()
+ {
+ WindowStartupLocationProperty.Changed.Subscribe(args => OnWindowStartupLocationChanged(args?.Sender, args));
+ }
+
+ ///
+ /// Gets the value for the attached property.
+ ///
+ /// The target element.
+ /// The attached to the element.
+ public static Style GetWindowStyle(AvaloniaObject obj)
+ {
+ return (Style)obj.GetValue(WindowStyleProperty);
+ }
+
+ ///
+ /// Sets the attached property.
+ ///
+ /// The target element.
+ /// The Style to attach.
+ public static void SetWindowStyle(AvaloniaObject obj, Style value)
+ {
+ obj.SetValue(WindowStyleProperty, value);
+ }
+
+ ///
+ /// Gets the value for the attached property.
+ ///
+ /// The target element.
+ /// The attached to the element.
+ public static WindowStartupLocation GetWindowStartupLocation(AvaloniaObject obj)
+ {
+ return (WindowStartupLocation)obj.GetValue(WindowStartupLocationProperty);
+ }
+
+ ///
+ /// Sets the attached property.
+ ///
+ /// The target element.
+ /// The WindowStartupLocation to attach.
+ public static void SetWindowStartupLocation(AvaloniaObject obj, WindowStartupLocation value)
+ {
+ obj.SetValue(WindowStartupLocationProperty, value);
+ }
+
+ private static void OnWindowStartupLocationChanged(AvaloniaObject sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (sender is Window window)
+ window.WindowStartupLocation = (WindowStartupLocation)e.NewValue;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs b/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs
new file mode 100644
index 0000000000..54faeaad47
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogService.cs
@@ -0,0 +1,176 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Prism.Common;
+using Prism.Ioc;
+
+namespace Prism.Dialogs
+{
+ /// Implements to show modal and non-modal dialogs.
+ /// The dialog's ViewModel must implement IDialogAware.
+ public class DialogService : IDialogService
+ {
+ private readonly IContainerExtension _containerExtension;
+
+ /// Initializes a new instance of the class.
+ /// The
+ public DialogService(IContainerExtension containerExtension)
+ {
+ _containerExtension = containerExtension;
+ }
+
+ public void ShowDialog(string name, IDialogParameters parameters, DialogCallback callback)
+ {
+ parameters ??= new DialogParameters();
+ var isModal = parameters.TryGetValue(KnownDialogParameters.ShowNonModal, out var show) ? !show : true;
+ var windowName = parameters.TryGetValue(KnownDialogParameters.WindowName, out var wName) ? wName : null;
+ var owner = parameters.TryGetValue(KnownDialogParameters.ParentWindow, out var hWnd) ? hWnd : null;
+
+ IDialogWindow dialogWindow = CreateDialogWindow(windowName);
+ ConfigureDialogWindowEvents(dialogWindow, callback);
+ ConfigureDialogWindowContent(name, dialogWindow, parameters);
+
+ ShowDialogWindow(dialogWindow, isModal, owner);
+ }
+
+ /// Shows the dialog window.
+ /// The dialog window to show.
+ /// If true; dialog is shown as a modal
+ /// Optional host window of the dialog. Use-case, Dialog calling a dialog.
+ protected virtual void ShowDialogWindow(IDialogWindow dialogWindow, bool isModal, Window owner = null)
+ {
+ if (isModal &&
+ Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime deskLifetime)
+ {
+ // Ref:
+ // - https://docs.avaloniaui.net/docs/reference/controls/window#show-a-window-as-a-dialog
+ // - https://github.com/AvaloniaUI/Avalonia/discussions/7924
+ // (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+
+ if (owner != null)
+ dialogWindow.ShowDialog(owner);
+ else
+ dialogWindow.ShowDialog(deskLifetime.MainWindow);
+ }
+ else
+ {
+ dialogWindow.Show();
+ }
+ }
+
+ ///
+ /// Create a new .
+ ///
+ /// The name of the hosting window registered with the IContainerRegistry.
+ /// The created .
+ protected virtual IDialogWindow CreateDialogWindow(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ return _containerExtension.Resolve();
+ else
+ return _containerExtension.Resolve(name);
+ }
+
+ ///
+ /// Configure content.
+ ///
+ /// The name of the dialog to show.
+ /// The hosting window.
+ /// The parameters to pass to the dialog.
+ protected virtual void ConfigureDialogWindowContent(string dialogName, IDialogWindow window, IDialogParameters parameters)
+ {
+ var content = _containerExtension.Resolve(dialogName);
+ if (!(content is Avalonia.Controls.Control dialogContent))
+ throw new NullReferenceException("A dialog's content must be a FrameworkElement");
+
+ MvvmHelpers.AutowireViewModel(dialogContent);
+
+ if (!(dialogContent.DataContext is IDialogAware viewModel))
+ throw new NullReferenceException("A dialog's ViewModel must implement the IDialogAware interface");
+
+ ConfigureDialogWindowProperties(window, dialogContent, viewModel);
+
+ MvvmHelpers.ViewAndViewModelAction(viewModel, d => d.OnDialogOpened(parameters));
+ }
+
+ ///
+ /// Configure and events.
+ ///
+ /// The hosting window.
+ /// The action to perform when the dialog is closed.
+ protected virtual void ConfigureDialogWindowEvents(IDialogWindow dialogWindow, DialogCallback callback)
+ {
+ Action requestCloseHandler = (result) =>
+ {
+ dialogWindow.Result = result;
+ dialogWindow.Close();
+ };
+
+ EventHandler loadedHandler = null;
+
+ loadedHandler = (o, e) =>
+ {
+ // WPF: dialogWindow.Loaded -= loadedHandler;
+ dialogWindow.Opened -= loadedHandler;
+ DialogUtilities.InitializeListener(dialogWindow.GetDialogViewModel(), requestCloseHandler);
+ };
+
+ dialogWindow.Opened += loadedHandler;
+
+ EventHandler closingHandler = null;
+ closingHandler = (o, e) =>
+ {
+ if (!dialogWindow.GetDialogViewModel().CanCloseDialog())
+ e.Cancel = true;
+ };
+
+ dialogWindow.Closing += closingHandler;
+
+ EventHandler closedHandler = null;
+ closedHandler = async (o, e) =>
+ {
+ dialogWindow.Closed -= closedHandler;
+ dialogWindow.Closing -= closingHandler;
+
+ dialogWindow.GetDialogViewModel().OnDialogClosed();
+
+ if (dialogWindow.Result == null)
+ dialogWindow.Result = new DialogResult();
+
+ await callback.Invoke(dialogWindow.Result);
+
+ dialogWindow.DataContext = null;
+ dialogWindow.Content = null;
+ };
+
+ dialogWindow.Closed += closedHandler;
+ }
+
+ ///
+ /// Configure properties.
+ ///
+ /// The hosting window.
+ /// The dialog to show.
+ /// The dialog's ViewModel.
+ protected virtual void ConfigureDialogWindowProperties(IDialogWindow window, Avalonia.Controls.Control dialogContent, IDialogAware viewModel)
+ {
+ // Avalonia returns 'null' for Dialog.GetWindowStyle(dialogContent);
+ // WPF: Window > ContentControl > FrameworkElement
+ // Ava: Window > WindowBase > TopLevel > Control > InputElement > Interactive > Layoutable > Visual > StyledElement.Styles (collection)
+
+ // WPF:
+ //// var windowStyle = Dialog.GetWindowStyle(dialogContent);
+ //// if (windowStyle != null)
+ //// window.Style = windowStyle;
+
+ // Make the host window and the dialog window to share the same context
+ window.Content = dialogContent;
+ window.DataContext = viewModel;
+
+ // WPF:
+ //// if (window.Owner == null)
+ //// window.Owner = Application.Current?.Windows.OfType().FirstOrDefault(x => x.IsActive);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml
new file mode 100644
index 0000000000..b97c3bc63c
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs
new file mode 100644
index 0000000000..5c322549e4
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/DialogWindow.axaml.cs
@@ -0,0 +1,29 @@
+using System;
+using System.ComponentModel;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Prism.Dialogs
+{
+ /// Prism's default dialog host.
+ public partial class DialogWindow : Window, IDialogWindow
+ {
+ /// The of the dialog.
+ public IDialogResult Result { get; set; }
+
+ /// Initializes a new instance of the class.
+ public DialogWindow()
+ {
+ InitializeComponent();
+
+#if DEBUG
+ //// this.AttachDevTools();
+#endif
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs
new file mode 100644
index 0000000000..eb9b8384f5
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogServiceCompatExtensions.cs
@@ -0,0 +1,63 @@
+using System;
+using Avalonia.Controls;
+
+namespace Prism.Dialogs
+{
+ /// Extensions for the IDialogService
+ public static class IDialogServiceCompatExtensions
+ {
+ /// Shows a non-modal dialog.
+ /// The DialogService
+ /// The name of the dialog to show.
+ public static void Show(this IDialogService dialogService, string name, IDialogParameters parameters, Action callback)
+ {
+ parameters = EnsureShowNonModalParameter(parameters);
+ dialogService.ShowDialog(name, parameters, new DialogCallback().OnClose(callback));
+ }
+
+ /// Shows a non-modal dialog.
+ /// The DialogService
+ /// The name of the dialog to show.
+ /// The parameters to pass to the dialog.
+ /// The action to perform when the dialog is closed.
+ /// The name of the hosting window registered with the IContainerRegistry.
+ public static void Show(this IDialogService dialogService, string name, IDialogParameters parameters, Action callback, string windowName)
+ {
+ parameters = EnsureShowNonModalParameter(parameters);
+
+ if (!string.IsNullOrEmpty(windowName))
+ parameters.Add(KnownDialogParameters.WindowName, windowName);
+
+ dialogService.ShowDialog(name, parameters, new DialogCallback().OnClose(callback));
+ }
+
+ /// Shows a non-modal dialog.
+ /// The DialogService
+ /// The name of the dialog to show.
+ public static void Show(this IDialogService dialogService, string name)
+ {
+ var parameters = EnsureShowNonModalParameter(null);
+ dialogService.Show(name, parameters, null);
+ }
+
+ /// Shows a non-modal dialog.
+ /// The DialogService
+ /// The name of the dialog to show.
+ /// The action to perform when the dialog is closed.
+ public static void Show(this IDialogService dialogService, string name, Action callback)
+ {
+ var parameters = EnsureShowNonModalParameter(null);
+ dialogService.Show(name, parameters, callback);
+ }
+
+ private static IDialogParameters EnsureShowNonModalParameter(IDialogParameters parameters)
+ {
+ parameters ??= new DialogParameters();
+
+ if (!parameters.ContainsKey(KnownDialogParameters.ShowNonModal))
+ parameters.Add(KnownDialogParameters.ShowNonModal, true);
+
+ return parameters;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs
new file mode 100644
index 0000000000..c367f1f3cb
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindow.cs
@@ -0,0 +1,70 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Styling;
+
+namespace Prism.Dialogs
+{
+ ///
+ /// Interface for a dialog hosting window.
+ ///
+ public interface IDialogWindow
+ {
+ /// Dialog content.
+ object Content { get; set; }
+
+ /// Close the window.
+ void Close();
+
+ /// The window's owner.
+ /// Avalonia's WindowBase.Owner's property access is { get; protected set; }.
+ WindowBase Owner { get; }
+
+ /// Show a non-modal dialog.
+ void Show();
+
+ /// Show a modal dialog.
+ ///
+ Task ShowDialog(Window owner);
+
+ ///
+ /// The data context of the window.
+ ///
+ ///
+ /// The data context must implement .
+ ///
+ object DataContext { get; set; }
+
+ /// Called when the window is loaded.
+ ///
+ /// Avalonia currently doesn't implement the Loaded event like WPF.
+ /// Window > WindowBase > TopLevel.Opened
+ /// Window > WindowBase > TopLevel > Control > InputElement > Interactive > layout > Visual > StyledElement.Initialized
+ ///
+ //// WPF: event RoutedEventHandler Loaded;
+ event EventHandler Opened;
+
+ ///
+ /// Called when the window is closed.
+ ///
+ event EventHandler Closed;
+
+ ///
+ /// Called when the window is closing.
+ ///
+ // WPF: event CancelEventHandler Closing;
+ // Ava: ...
+ event EventHandler? Closing;
+
+ ///
+ /// The result of the dialog.
+ ///
+ IDialogResult Result { get; set; }
+
+ /// The window style.
+ // WPF: Window > ContentControl > FrameworkElement
+ // Ava: Window > WindowBase > TopLevel > ContentControl > TemplatedControl > Control
+ //Style Style { get; set; }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs
new file mode 100644
index 0000000000..d60158aa03
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/IDialogWindowExtensions.cs
@@ -0,0 +1,18 @@
+namespace Prism.Dialogs
+{
+ ///
+ /// extensions.
+ ///
+ internal static class IDialogWindowExtensions
+ {
+ ///
+ /// Get the ViewModel from a .
+ ///
+ /// to get ViewModel from.
+ /// ViewModel as a .
+ internal static IDialogAware GetDialogViewModel(this IDialogWindow dialogWindow)
+ {
+ return (IDialogAware)dialogWindow.DataContext;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs b/src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs
new file mode 100644
index 0000000000..b5314049ea
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Dialogs/KnownDialogParameters.cs
@@ -0,0 +1,14 @@
+namespace Prism.Dialogs;
+
+/// Provides Dialog Parameter Keys for well known parameters used by the
+public static class KnownDialogParameters
+{
+ /// The name of the window.
+ public const string WindowName = "windowName";
+
+ /// Flag to show the Dialog Modally or Non-Modally.
+ public const string ShowNonModal = "nonModal";
+
+ /// Host Window; when different from default.
+ public const string ParentWindow = "parentWindow";
+}
diff --git a/src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs b/src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs
new file mode 100644
index 0000000000..51207195d5
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Extensions/AvaloniaObjectExtensions.cs
@@ -0,0 +1,17 @@
+using Avalonia;
+using Avalonia.Controls;
+
+namespace Prism
+{
+ /// AvaloniaObject Extensions.
+ /// Equivalent to WPF's DependencyObject
+ internal static partial class AvaloniaObjectExtensions
+ {
+ /// Determines if a has a binding set.
+ /// The to use to search for the property.
+ /// The property to search.
+ /// true if there is an active binding, otherwise false .
+ public static bool HasBinding(this Control instance, AvaloniaProperty property)
+ => instance.GetBindingObservable(property) != null;
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Extensions/CollectionExtensions.cs b/src/Avalonia/Prism.Avalonia/Extensions/CollectionExtensions.cs
new file mode 100644
index 0000000000..34a23c00bd
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Extensions/CollectionExtensions.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+
+namespace System.Collections.ObjectModel
+{
+ ///
+ /// Class that provides extension methods to Collection
+ ///
+ public static class CollectionExtensions
+ {
+ ///
+ /// Add a range of items to a collection.
+ ///
+ /// Type of objects within the collection.
+ /// The collection to add items to.
+ /// The items to add to the collection.
+ /// The collection.
+ /// An is thrown if or is .
+ public static Collection AddRange(this Collection collection, IEnumerable items)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
+ if (items == null)
+ throw new ArgumentNullException(nameof(items));
+
+ foreach (var each in items)
+ {
+ collection.Add(each);
+ }
+
+ return collection;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs b/src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs
new file mode 100644
index 0000000000..b2f81082a1
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Interactivity/CommandBehaviorBase.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Windows.Input;
+using Avalonia.Controls;
+
+namespace Prism.Interactivity
+{
+ ///
+ /// Base behavior to handle connecting a to a Command.
+ ///
+ /// The target object must derive from Control.
+ ///
+ /// CommandBehaviorBase can be used to provide new behaviors for commands.
+ ///
+ public class CommandBehaviorBase where T : Control
+ {
+ private ICommand _command;
+ private object _commandParameter;
+ private readonly WeakReference _targetObject;
+ private readonly EventHandler _commandCanExecuteChangedHandler;
+
+ ///
+ /// Constructor specifying the target object.
+ ///
+ /// The target object the behavior is attached to.
+ public CommandBehaviorBase(T targetObject)
+ {
+ _targetObject = new WeakReference(targetObject);
+
+ _commandCanExecuteChangedHandler = CommandCanExecuteChanged;
+ }
+
+ bool _autoEnabled = true;
+ ///
+ /// If true the target object's IsEnabled property will update based on the commands ability to execute.
+ /// If false the target object's IsEnabled property will not update.
+ ///
+ public bool AutoEnable
+ {
+ get { return _autoEnabled; }
+ set
+ {
+ _autoEnabled = value;
+ UpdateEnabledState();
+ }
+ }
+
+ ///
+ /// Corresponding command to be execute and monitored for .
+ ///
+ public ICommand Command
+ {
+ get { return _command; }
+ set
+ {
+ if (_command != null)
+ {
+ _command.CanExecuteChanged -= _commandCanExecuteChangedHandler;
+ }
+
+ _command = value;
+ if (_command != null)
+ {
+ _command.CanExecuteChanged += _commandCanExecuteChangedHandler;
+ UpdateEnabledState();
+ }
+ }
+ }
+
+ ///
+ /// The parameter to supply the command during execution.
+ ///
+ public object CommandParameter
+ {
+ get { return _commandParameter; }
+ set
+ {
+ if (_commandParameter != value)
+ {
+ _commandParameter = value;
+ UpdateEnabledState();
+ }
+ }
+ }
+
+ ///
+ /// Object to which this behavior is attached.
+ ///
+ protected T TargetObject
+ {
+ get
+ {
+ return _targetObject.Target as T;
+ }
+ }
+
+ ///
+ /// Updates the target object's IsEnabled property based on the commands ability to execute.
+ ///
+ protected virtual void UpdateEnabledState()
+ {
+ if (TargetObject == null)
+ {
+ Command = null;
+ CommandParameter = null;
+ }
+ else if (Command != null)
+ {
+ if (AutoEnable)
+ TargetObject.IsEnabled = Command.CanExecute(CommandParameter);
+ }
+ }
+
+ private void CommandCanExecuteChanged(object sender, EventArgs e)
+ {
+ UpdateEnabledState();
+ }
+
+ ///
+ /// Executes the command, if it's set, providing the .
+ ///
+ protected virtual void ExecuteCommand(object parameter)
+ {
+ if (Command != null)
+ Command.Execute(CommandParameter ?? parameter);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs b/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs
new file mode 100644
index 0000000000..d86a095fd4
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Interactivity/InvokeCommandAction.cs
@@ -0,0 +1,226 @@
+// TODO - 2022-07-12
+// * Updated the public StyleProperty fields for Avalonia
+// * Needs:
+// - Methods updated and verified - OnAttached, OnDetatching, , etc.
+//
+// Reference:
+// https://github.com/wieslawsoltes/AvaloniaBehaviors/blob/master/src/Avalonia.Xaml.Interactions/Core/InvokeCommandAction.cs
+//
+////using System.Reflection;
+////using System.Windows.Input;
+////using Avalonia;
+////using Avalonia.Controls;
+////using Microsoft.Xaml.Behaviors;
+////
+////namespace Prism.Interactivity
+////{
+//// ///
+//// /// Trigger action that executes a command when invoked.
+//// /// It also maintains the Enabled state of the target control based on the CanExecute method of the command.
+//// ///
+//// public class InvokeCommandAction : TriggerAction
+//// {
+//// private ExecutableCommandBehavior _commandBehavior;
+////
+//// ///
+//// /// Dependency property identifying if the associated element should automatically be enabled or disabled based on the result of the Command's CanExecute
+//// ///
+//// public static readonly StyledProperty AutoEnableProperty =
+//// AvaloniaProperty.Register(nameof(AutoEnable));
+//// ////public static readonly StyledProperty AutoEnableProperty =
+//// //// StyledProperty.Register("AutoEnable", typeof(bool), typeof(InvokeCommandAction),
+//// //// new PropertyMetadata(true, (d, e) => ((InvokeCommandAction)d).OnAllowDisableChanged((bool)e.NewValue)));
+////
+//// ///
+//// /// Gets or sets whether or not the associated element will automatically be enabled or disabled based on the result of the commands CanExecute
+//// ///
+//// public bool AutoEnable
+//// {
+//// get { return (bool)this.GetValue(AutoEnableProperty); }
+//// set { this.SetValue(AutoEnableProperty, value); }
+//// }
+////
+//// private void OnAllowDisableChanged(bool newValue)
+//// {
+//// var behavior = GetOrCreateBehavior();
+//// if (behavior != null)
+//// behavior.AutoEnable = newValue;
+//// }
+////
+//// ///
+//// /// Dependency property identifying the command to execute when invoked.
+//// ///
+//// public static readonly StyledProperty CommandProperty =
+//// AvaloniaProperty.Register(nameof(Command));
+//// ////public static readonly StyledProperty CommandProperty =
+//// //// StyledProperty.Register("Command", typeof(ICommand), typeof(InvokeCommandAction),
+//// //// new PropertyMetadata(null, (d, e) => ((InvokeCommandAction)d).OnCommandChanged((ICommand)e.NewValue)));
+////
+//// ///
+//// /// Gets or sets the command to execute when invoked.
+//// ///
+//// public ICommand Command
+//// {
+//// get { return this.GetValue(CommandProperty) as ICommand; }
+//// set { this.SetValue(CommandProperty, value); }
+//// }
+////
+//// private void OnCommandChanged(ICommand newValue)
+//// {
+//// var behavior = GetOrCreateBehavior();
+//// if (behavior != null)
+//// behavior.Command = newValue;
+//// }
+////
+//// ///
+//// /// Dependency property identifying the command parameter to supply on command execution.
+//// ///
+//// public static readonly StyledProperty CommandParameterProperty =
+//// AvaloniaProperty.Register(nameof(CommandParameter));
+//// ////public static readonly StyledProperty CommandParameterProperty =
+//// //// StyledProperty.Register("CommandParameter", typeof(object), typeof(InvokeCommandAction),
+//// //// new PropertyMetadata(null, (d, e) => ((InvokeCommandAction)d).OnCommandParameterChanged(e.NewValue)));
+////
+//// ///
+//// /// Gets or sets the command parameter to supply on command execution.
+//// ///
+//// public object CommandParameter
+//// {
+//// get { return this.GetValue(CommandParameterProperty); }
+//// set { this.SetValue(CommandParameterProperty, value); }
+//// }
+////
+//// private void OnCommandParameterChanged(object newValue)
+//// {
+//// var behavior = GetOrCreateBehavior();
+//// if (behavior != null)
+//// behavior.CommandParameter = newValue;
+//// }
+////
+//// ///
+//// /// Dependency property identifying the TriggerParameterPath to be parsed to identify the child property of the trigger parameter to be used as the command parameter.
+//// ///
+//// public static readonly StyledProperty TriggerParameterPathProperty =
+//// AvaloniaProperty.Register(nameof(TriggerParameterPath));
+//// ////public static readonly StyledProperty TriggerParameterPathProperty =
+//// //// StyledProperty.Register("TriggerParameterPath", typeof(string), typeof(InvokeCommandAction),
+//// //// new PropertyMetadata(null, (d, e) => { }));
+////
+//// ///
+//// /// Gets or sets the TriggerParameterPath value.
+//// ///
+//// public string TriggerParameterPath
+//// {
+//// get { return this.GetValue(TriggerParameterPathProperty) as string; }
+//// set { this.SetValue(TriggerParameterPathProperty, value); }
+//// }
+////
+//// ///
+//// /// Public wrapper of the Invoke method.
+//// ///
+//// public void InvokeAction(object parameter)
+//// {
+//// Invoke(parameter);
+//// }
+////
+//// ///
+//// /// Executes the command
+//// ///
+//// /// This parameter is passed to the command; the CommandParameter specified in the CommandParameterProperty is used for command invocation if not null.
+//// protected override void Invoke(object parameter)
+//// {
+//// if (!string.IsNullOrEmpty(TriggerParameterPath))
+//// {
+//// //Walk the ParameterPath for nested properties.
+//// var propertyPathParts = TriggerParameterPath.Split('.');
+//// object propertyValue = parameter;
+//// foreach (var propertyPathPart in propertyPathParts)
+//// {
+//// var propInfo = propertyValue.GetType().GetTypeInfo().GetProperty(propertyPathPart);
+//// propertyValue = propInfo.GetValue(propertyValue);
+//// }
+//// parameter = propertyValue;
+//// }
+////
+//// var behavior = GetOrCreateBehavior();
+////
+//// if (behavior != null)
+//// {
+//// behavior.ExecuteCommand(parameter);
+//// }
+//// }
+////
+//// ///
+//// /// Sets the Command and CommandParameter properties to null.
+//// ///
+//// protected override void OnDetaching()
+//// {
+//// base.OnDetaching();
+////
+//// Command = null;
+//// CommandParameter = null;
+////
+//// _commandBehavior = null;
+//// }
+////
+//// ///
+//// /// This method is called after the behavior is attached.
+//// /// It updates the command behavior's Command and CommandParameter properties if necessary.
+//// ///
+//// protected override void OnAttached()
+//// {
+//// base.OnAttached();
+////
+//// // In case this action is attached to a target object after the Command and/or CommandParameter properties are set,
+//// // the command behavior would be created without a value for these properties.
+//// // To cover this scenario, the Command and CommandParameter properties of the behavior are updated here.
+//// var behavior = GetOrCreateBehavior();
+////
+//// behavior.AutoEnable = AutoEnable;
+////
+//// if (behavior.Command != Command)
+//// behavior.Command = Command;
+////
+//// if (behavior.CommandParameter != CommandParameter)
+//// behavior.CommandParameter = CommandParameter;
+//// }
+////
+//// private ExecutableCommandBehavior GetOrCreateBehavior()
+//// {
+//// // In case this method is called prior to this action being attached,
+//// // the CommandBehavior would always keep a null target object (which isn't changeable afterwards).
+//// // Therefore, in that case the behavior shouldn't be created and this method should return null.
+//// if (_commandBehavior == null && AssociatedObject != null)
+//// {
+//// _commandBehavior = new ExecutableCommandBehavior(AssociatedObject);
+//// }
+////
+//// return _commandBehavior;
+//// }
+////
+//// ///
+//// /// A CommandBehavior that exposes a public ExecuteCommand method. It provides the functionality to invoke commands and update Enabled state of the target control.
+//// /// It is not possible to make the inherit from , since the
+//// /// must already inherit from , so we chose to follow the aggregation approach.
+//// ///
+//// private class ExecutableCommandBehavior : CommandBehaviorBase
+//// {
+//// ///
+//// /// Constructor specifying the target object.
+//// ///
+//// /// The target object the behavior is attached to.
+//// public ExecutableCommandBehavior(Control target)
+//// : base(target)
+//// {
+//// }
+////
+//// ///
+//// /// Executes the command, if it's set.
+//// ///
+//// public new void ExecuteCommand(object parameter)
+//// {
+//// base.ExecuteCommand(parameter);
+//// }
+//// }
+//// }
+////}
diff --git a/src/Avalonia/Prism.Avalonia/Ioc/ContainerProviderExtension.cs b/src/Avalonia/Prism.Avalonia/Ioc/ContainerProviderExtension.cs
new file mode 100644
index 0000000000..f8c25e3a3e
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Ioc/ContainerProviderExtension.cs
@@ -0,0 +1,70 @@
+using System;
+using Avalonia.Markup.Xaml;
+
+namespace Prism.Ioc
+{
+ ///
+ /// Provides Types and Services registered with the Container
+ ///
+ ///
+ /// Usage as markup extension:
+ ///
+ /// ]]>
+ ///
+ ///
+ /// Usage as XML element:
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// ]]>
+ ///
+ ///
+ ///
+ public class ContainerProviderExtension : MarkupExtension
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContainerProviderExtension()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type to Resolve
+ public ContainerProviderExtension(Type type)
+ {
+ Type = type;
+ }
+
+ ///
+ /// The type to Resolve
+ ///
+ public Type Type { get; set; }
+
+ ///
+ /// The Name used to register the type with the Container
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Provide resolved object from
+ ///
+ ///
+ ///
+ public override object ProvideValue(IServiceProvider serviceProvider)
+ {
+ return string.IsNullOrEmpty(Name)
+ ? ContainerLocator.Container?.Resolve(Type)
+ : ContainerLocator.Container?.Resolve(Type, Name);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs b/src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs
new file mode 100644
index 0000000000..0ab09e2a1f
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Ioc/IContainerRegistryExtensions.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Prism.Mvvm;
+
+namespace Prism.Ioc
+{
+ ///
+ /// extensions.
+ ///
+ public static class IContainerRegistryExtensions
+ {
+ ///
+ /// Registers an object to be used as a dialog in the IDialogService.
+ ///
+ /// The Type of object to register as the dialog
+ ///
+ /// The unique name to register with the dialog.
+ public static void RegisterDialog<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TView>(this IContainerRegistry containerRegistry, string name = null)
+ {
+ containerRegistry.RegisterForNavigation(name);
+ }
+
+ ///
+ /// Registers an object to be used as a dialog in the IDialogService.
+ ///
+ /// The Type of object to register as the dialog
+ /// The ViewModel to use as the DataContext for the dialog
+ ///
+ /// The unique name to register with the dialog.
+ public static void RegisterDialog<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TView, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TViewModel>(this IContainerRegistry containerRegistry, string name = null) where TViewModel : Dialogs.IDialogAware
+ {
+ containerRegistry.RegisterForNavigation(name);
+ }
+
+ ///
+ /// Registers an object that implements IDialogWindow to be used to host all dialogs in the IDialogService.
+ ///
+ /// The Type of the Window class that will be used to host dialogs in the IDialogService
+ ///
+ public static void RegisterDialogWindow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWindow>(this IContainerRegistry containerRegistry) where TWindow : Dialogs.IDialogWindow
+ {
+ containerRegistry.Register(typeof(Dialogs.IDialogWindow), typeof(TWindow));
+ }
+
+ ///
+ /// Registers an object that implements IDialogWindow to be used to host all dialogs in the IDialogService.
+ ///
+ /// The Type of the Window class that will be used to host dialogs in the IDialogService
+ ///
+ /// The name of the dialog window
+ public static void RegisterDialogWindow<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TWindow>(this IContainerRegistry containerRegistry, string name) where TWindow : Dialogs.IDialogWindow
+ {
+ containerRegistry.Register(typeof(Dialogs.IDialogWindow), typeof(TWindow), name);
+ }
+
+ ///
+ /// Registers an object for navigation
+ ///
+ ///
+ /// The type of object to register
+ /// The unique name to register with the object.
+ public static void RegisterForNavigation(this IContainerRegistry containerRegistry, Type type, string name)
+ {
+ containerRegistry.Register(typeof(object), type, name);
+ }
+
+ ///
+ /// Registers an object for navigation.
+ ///
+ /// The Type of the object to register as the view
+ ///
+ /// The unique name to register with the object.
+ public static void RegisterForNavigation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this IContainerRegistry containerRegistry, string name = null)
+ {
+ Type type = typeof(T);
+ string viewName = string.IsNullOrWhiteSpace(name) ? type.Name : name;
+ containerRegistry.RegisterForNavigation(type, viewName);
+ }
+
+ ///
+ /// Registers an object for navigation with the ViewModel type to be used as the DataContext.
+ ///
+ /// The Type of object to register as the view
+ /// The ViewModel to use as the DataContext for the view
+ ///
+ /// The unique name to register with the view
+ public static void RegisterForNavigation<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TView, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TViewModel>(this IContainerRegistry containerRegistry, string name = null)
+ {
+ containerRegistry.RegisterForNavigationWithViewModel(typeof(TView), name);
+ }
+
+ private static void RegisterForNavigationWithViewModel<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] TViewModel>(this IContainerRegistry containerRegistry, Type viewType, string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ name = viewType.Name;
+
+ ViewModelLocationProvider.Register(viewType.ToString(), typeof(TViewModel));
+ containerRegistry.RegisterForNavigation(viewType, name);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs
new file mode 100644
index 0000000000..5681e11391
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/AssemblyResolver.Desktop.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Handles AppDomain's AssemblyResolve event to be able to load assemblies dynamically in
+ /// the LoadFrom context, but be able to reference the type from assemblies loaded in the Load context.
+ ///
+ public class AssemblyResolver : IAssemblyResolver, IDisposable
+ {
+ private readonly List registeredAssemblies = new List();
+
+ private bool handlesAssemblyResolve;
+
+ ///
+ /// Registers the specified assembly and resolves the types in it when the AppDomain requests for it.
+ ///
+ /// The path to the assembly to load in the LoadFrom context.
+ /// This method does not load the assembly immediately, but lazily until someone requests a
+ /// declared in the assembly.
+ public void LoadAssemblyFrom(string assemblyFilePath)
+ {
+ if (!this.handlesAssemblyResolve)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve += this.CurrentDomain_AssemblyResolve;
+ this.handlesAssemblyResolve = true;
+ }
+
+ Uri assemblyUri = GetFileUri(assemblyFilePath);
+
+ if (assemblyUri == null)
+ {
+ throw new ArgumentException(Resources.InvalidArgumentAssemblyUri, nameof(assemblyFilePath));
+ }
+
+ if (!File.Exists(assemblyUri.LocalPath))
+ {
+ throw new FileNotFoundException(null, assemblyUri.LocalPath);
+ }
+
+ AssemblyName assemblyName = AssemblyName.GetAssemblyName(assemblyUri.LocalPath);
+ AssemblyInfo assemblyInfo = this.registeredAssemblies.FirstOrDefault(a => assemblyName == a.AssemblyName);
+
+ if (assemblyInfo != null)
+ {
+ return;
+ }
+
+ assemblyInfo = new AssemblyInfo() { AssemblyName = assemblyName, AssemblyUri = assemblyUri };
+ this.registeredAssemblies.Add(assemblyInfo);
+ }
+
+ private static Uri GetFileUri(string filePath)
+ {
+ if (String.IsNullOrEmpty(filePath))
+ {
+ return null;
+ }
+
+ Uri uri;
+ if (!Uri.TryCreate(filePath, UriKind.Absolute, out uri))
+ {
+ return null;
+ }
+
+ if (!uri.IsFile)
+ {
+ return null;
+ }
+
+ return uri;
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom")]
+ private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
+ {
+ AssemblyName assemblyName = new AssemblyName(args.Name);
+
+ AssemblyInfo assemblyInfo = this.registeredAssemblies.FirstOrDefault(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.AssemblyName));
+
+ if (assemblyInfo != null)
+ {
+ if (assemblyInfo.Assembly == null)
+ {
+ assemblyInfo.Assembly = Assembly.LoadFrom(assemblyInfo.AssemblyUri.LocalPath);
+ }
+
+ return assemblyInfo.Assembly;
+ }
+
+ return null;
+ }
+
+ private class AssemblyInfo
+ {
+ public AssemblyName AssemblyName { get; set; }
+
+ public Uri AssemblyUri { get; set; }
+
+ public Assembly Assembly { get; set; }
+ }
+
+ #region Implementation of IDisposable
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// Calls .
+ /// 2
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes the associated .
+ ///
+ /// When , it is being called from the Dispose method.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (this.handlesAssemblyResolve)
+ {
+ AppDomain.CurrentDomain.AssemblyResolve -= this.CurrentDomain_AssemblyResolve;
+ this.handlesAssemblyResolve = false;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs
new file mode 100644
index 0000000000..552f64ded5
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationModuleCatalog.Desktop.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+
+ ///
+ /// A catalog built from a configuration file.
+ ///
+ public class ConfigurationModuleCatalog : ModuleCatalog
+ {
+ ///
+ /// Builds an instance of ConfigurationModuleCatalog with a as the default store.
+ ///
+ public ConfigurationModuleCatalog()
+ {
+ Store = new ConfigurationStore();
+ }
+
+ ///
+ /// Gets or sets the store where the configuration is kept.
+ ///
+ public IConfigurationStore Store { get; set; }
+
+ ///
+ /// Loads the catalog from the configuration.
+ ///
+ protected override void InnerLoad()
+ {
+ if (Store == null)
+ {
+ throw new InvalidOperationException(Resources.ConfigurationStoreCannotBeNull);
+ }
+
+ EnsureModulesDiscovered();
+ }
+
+ private void EnsureModulesDiscovered()
+ {
+ ModulesConfigurationSection section = Store.RetrieveModuleConfigurationSection();
+
+ if (section != null)
+ {
+ foreach (ModuleConfigurationElement element in section.Modules)
+ {
+ IList dependencies = new List();
+
+ if (element.Dependencies.Count > 0)
+ {
+ foreach (ModuleDependencyConfigurationElement dependency in element.Dependencies)
+ {
+ dependencies.Add(dependency.ModuleName);
+ }
+ }
+
+ ModuleInfo moduleInfo = new ModuleInfo(element.ModuleName, element.ModuleType)
+ {
+ Ref = GetFileAbsoluteUri(element.AssemblyFile),
+ InitializationMode = element.StartupLoaded ? InitializationMode.WhenAvailable : InitializationMode.OnDemand
+ };
+ moduleInfo.DependsOn.AddRange(dependencies.ToArray());
+ AddModule(moduleInfo);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs
new file mode 100644
index 0000000000..53081bacb1
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ConfigurationStore.Desktop.cs
@@ -0,0 +1,19 @@
+using System.Configuration;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Defines a store for the module metadata.
+ ///
+ public class ConfigurationStore : IConfigurationStore
+ {
+ ///
+ /// Gets the module configuration data.
+ ///
+ /// A instance.
+ public ModulesConfigurationSection RetrieveModuleConfigurationSection()
+ {
+ return ConfigurationManager.GetSection("modules") as ModulesConfigurationSection;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs
new file mode 100644
index 0000000000..d4c3465bd5
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.net45.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Security.Policy;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Represets a catalog created from a directory on disk.
+ ///
+ ///
+ /// The directory catalog will scan the contents of a directory, locating classes that implement
+ /// and add them to the catalog based on contents in their associated .
+ /// Assemblies are loaded into a new application domain with ReflectionOnlyLoad. The application domain is destroyed
+ /// once the assemblies have been discovered.
+ ///
+ /// The diretory catalog does not continue to monitor the directory after it has created the initialze catalog.
+ ///
+ public class DirectoryModuleCatalog : ModuleCatalog
+ {
+ ///
+ /// Directory containing modules to search for.
+ ///
+ public string ModulePath { get; set; }
+
+ ///
+ /// Drives the main logic of building the child domain and searching for the assemblies.
+ ///
+ protected override void InnerLoad()
+ {
+ if (string.IsNullOrEmpty(this.ModulePath))
+ throw new InvalidOperationException(Resources.ModulePathCannotBeNullOrEmpty);
+
+ if (!Directory.Exists(this.ModulePath))
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.CurrentCulture, Resources.DirectoryNotFound, this.ModulePath));
+
+ AppDomain childDomain = this.BuildChildDomain(AppDomain.CurrentDomain);
+
+ try
+ {
+ List loadedAssemblies = new List();
+
+ var assemblies = (
+ from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
+ where !(assembly is System.Reflection.Emit.AssemblyBuilder)
+ && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
+ // TODO: Do this in a less hacky way... probably never gonna happen
+ && !assembly.GetName().Name.StartsWith("xunit")
+ && !string.IsNullOrEmpty(assembly.Location)
+ select assembly.Location
+ );
+
+ loadedAssemblies.AddRange(assemblies);
+
+ Type loaderType = typeof(InnerModuleInfoLoader);
+
+ if (loaderType.Assembly != null)
+ {
+ var loader =
+ (InnerModuleInfoLoader)
+ childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
+ loader.LoadAssemblies(loadedAssemblies);
+ this.Items.AddRange(loader.GetModuleInfos(this.ModulePath));
+ }
+ }
+ finally
+ {
+ AppDomain.Unload(childDomain);
+ }
+ }
+
+
+ ///
+ /// Creates a new child domain and copies the evidence from a parent domain.
+ ///
+ /// The parent domain.
+ /// The new child domain.
+ ///
+ /// Grabs the evidence and uses it to construct the new
+ /// because in a ClickOnce execution environment, creating an
+ /// will by default pick up the partial trust environment of
+ /// the AppLaunch.exe, which was the root executable. The AppLaunch.exe does a
+ /// create domain and applies the evidence from the ClickOnce manifests to
+ /// create the domain that the application is actually executing in. This will
+ /// need to be Full Trust for Prism applications.
+ ///
+ /// An is thrown if is null.
+ protected virtual AppDomain BuildChildDomain(AppDomain parentDomain)
+ {
+ if (parentDomain == null)
+ throw new ArgumentNullException(nameof(parentDomain));
+
+ Evidence evidence = new Evidence(parentDomain.Evidence);
+ AppDomainSetup setup = parentDomain.SetupInformation;
+ return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);
+ }
+
+ private class InnerModuleInfoLoader : MarshalByRefObject
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
+ internal ModuleInfo[] GetModuleInfos(string path)
+ {
+ DirectoryInfo directory = new DirectoryInfo(path);
+
+ ResolveEventHandler resolveEventHandler =
+ delegate (object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, directory); };
+
+ AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
+
+ Assembly moduleReflectionOnlyAssembly =
+ AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
+ asm => asm.FullName == typeof(IModule).Assembly.FullName);
+ Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);
+
+ IEnumerable modules = GetNotAlreadyLoadedModuleInfos(directory, IModuleType);
+
+ var array = modules.ToArray();
+ AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
+ return array;
+ }
+
+ private static IEnumerable GetNotAlreadyLoadedModuleInfos(DirectoryInfo directory, Type IModuleType)
+ {
+ List validAssemblies = new List();
+ Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();
+
+ var fileInfos = directory.GetFiles("*.dll")
+ .Where(file => alreadyLoadedAssemblies
+ .FirstOrDefault(
+ assembly =>
+ String.Compare(Path.GetFileName(assembly.Location), file.Name,
+ StringComparison.OrdinalIgnoreCase) == 0) == null);
+
+ foreach (FileInfo fileInfo in fileInfos)
+ {
+ try
+ {
+ Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName);
+ validAssemblies.Add(fileInfo);
+ }
+ catch (BadImageFormatException)
+ {
+ // skip non-.NET Dlls
+ }
+ }
+
+ return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName)
+ .GetExportedTypes()
+ .Where(IModuleType.IsAssignableFrom)
+ .Where(t => t != IModuleType)
+ .Where(t => !t.IsAbstract)
+ .Select(type => CreateModuleInfo(type)));
+ }
+
+ private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
+ {
+ Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
+ asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
+ if (loadedAssembly != null)
+ {
+ return loadedAssembly;
+ }
+ AssemblyName assemblyName = new AssemblyName(args.Name);
+ string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
+ if (File.Exists(dependentAssemblyFilename))
+ {
+ return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
+ }
+ return Assembly.ReflectionOnlyLoad(args.Name);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
+ internal void LoadAssemblies(IEnumerable assemblies)
+ {
+ foreach (string assemblyPath in assemblies)
+ {
+ try
+ {
+ Assembly.ReflectionOnlyLoadFrom(assemblyPath);
+ }
+ catch (FileNotFoundException)
+ {
+ // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
+ }
+ }
+ }
+
+ private static ModuleInfo CreateModuleInfo(Type type)
+ {
+ string moduleName = type.Name;
+ List dependsOn = new List();
+ bool onDemand = false;
+ var moduleAttribute =
+ CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(
+ cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);
+
+ if (moduleAttribute != null)
+ {
+ foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
+ {
+ string argumentName = argument.MemberInfo.Name;
+ switch (argumentName)
+ {
+ case "ModuleName":
+ moduleName = (string)argument.TypedValue.Value;
+ break;
+
+ case "OnDemand":
+ onDemand = (bool)argument.TypedValue.Value;
+ break;
+
+ case "StartupLoaded":
+ onDemand = !((bool)argument.TypedValue.Value);
+ break;
+ }
+ }
+ }
+
+ var moduleDependencyAttributes =
+ CustomAttributeData.GetCustomAttributes(type).Where(
+ cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);
+
+ foreach (CustomAttributeData cad in moduleDependencyAttributes)
+ {
+ dependsOn.Add((string)cad.ConstructorArguments[0].Value);
+ }
+
+ ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
+ {
+ InitializationMode =
+ onDemand
+ ? InitializationMode.OnDemand
+ : InitializationMode.WhenAvailable,
+ Ref = type.Assembly.EscapedCodeBase,
+ };
+ moduleInfo.DependsOn.AddRange(dependsOn);
+ return moduleInfo;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs
new file mode 100644
index 0000000000..fd6693f278
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/DirectoryModuleCatalog.netcore.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Represents a catalog created from a directory on disk.
+ ///
+ ///
+ /// The directory catalog will scan the contents of a directory, locating classes that implement
+ /// and add them to the catalog based on contents in their associated .
+ /// Assemblies are loaded into a new application domain with ReflectionOnlyLoad. The application domain is destroyed
+ /// once the assemblies have been discovered.
+ ///
+ /// The directory catalog does not continue to monitor the directory after it has created the initialize catalog.
+ ///
+ public class DirectoryModuleCatalog : ModuleCatalog
+ {
+ ///
+ /// Directory containing modules to search for.
+ ///
+ public string ModulePath { get; set; }
+
+ ///
+ /// Drives the main logic of building the child domain and searching for the assemblies.
+ ///
+ protected override void InnerLoad()
+ {
+ if (string.IsNullOrEmpty(this.ModulePath))
+ throw new InvalidOperationException(Resources.ModulePathCannotBeNullOrEmpty);
+
+ if (!Directory.Exists(this.ModulePath))
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.CurrentCulture, Resources.DirectoryNotFound, this.ModulePath));
+
+ AppDomain childDomain = AppDomain.CurrentDomain;
+
+ try
+ {
+ List loadedAssemblies = new List();
+
+ var assemblies = (
+ from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
+ where !(assembly is System.Reflection.Emit.AssemblyBuilder)
+ && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
+ && !String.IsNullOrEmpty(assembly.Location)
+ select assembly.Location
+ );
+
+ loadedAssemblies.AddRange(assemblies);
+
+ Type loaderType = typeof(InnerModuleInfoLoader);
+
+ if (loaderType.Assembly != null)
+ {
+ var loader =
+ (InnerModuleInfoLoader)
+ childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
+
+ this.Items.AddRange(loader.GetModuleInfos(this.ModulePath));
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new Exception("There was an error loading assemblies.", ex);
+ }
+ }
+
+ private class InnerModuleInfoLoader : MarshalByRefObject
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
+ internal ModuleInfo[] GetModuleInfos(string path)
+ {
+ DirectoryInfo directory = new DirectoryInfo(path);
+
+ ResolveEventHandler resolveEventHandler =
+ delegate (object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, directory); };
+
+ AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
+
+ Assembly moduleReflectionOnlyAssembly = AppDomain.CurrentDomain.GetAssemblies().First(asm => asm.FullName == typeof(IModule).Assembly.FullName);
+ Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);
+
+ IEnumerable modules = GetNotAlreadyLoadedModuleInfos(directory, IModuleType);
+
+ var array = modules.ToArray();
+ AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;
+ return array;
+ }
+
+ private static IEnumerable GetNotAlreadyLoadedModuleInfos(DirectoryInfo directory, Type IModuleType)
+ {
+ List validAssemblies = new List();
+ Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(p => !p.IsDynamic).ToArray();
+
+ var fileInfos = directory.GetFiles("*.dll")
+ .Where(file => alreadyLoadedAssemblies.FirstOrDefault(
+ assembly => String.Compare(Path.GetFileName(assembly.Location),
+ file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null).ToList();
+
+ foreach (FileInfo fileInfo in fileInfos)
+ {
+ try
+ {
+ validAssemblies.Add(Assembly.LoadFrom(fileInfo.FullName));
+ }
+ catch (BadImageFormatException)
+ {
+ // skip non-.NET Dlls
+ }
+ }
+
+ return validAssemblies.SelectMany(assembly => assembly
+ .GetExportedTypes()
+ .Where(IModuleType.IsAssignableFrom)
+ .Where(t => t != IModuleType)
+ .Where(t => !t.IsAbstract)
+ .Select(type => CreateModuleInfo(type)));
+ }
+
+ private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, DirectoryInfo directory)
+ {
+ Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
+ asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
+ if (loadedAssembly != null)
+ {
+ return loadedAssembly;
+ }
+
+ AssemblyName assemblyName = new AssemblyName(args.Name);
+ string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
+ if (File.Exists(dependentAssemblyFilename))
+ {
+ return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
+ }
+
+ return Assembly.ReflectionOnlyLoad(args.Name);
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
+ internal void LoadAssemblies(IEnumerable assemblies)
+ {
+ foreach (string assemblyPath in assemblies)
+ {
+ try
+ {
+ Assembly.ReflectionOnlyLoadFrom(assemblyPath);
+ }
+ catch (FileNotFoundException)
+ {
+ // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
+ }
+ }
+ }
+
+ private static ModuleInfo CreateModuleInfo(Type type)
+ {
+ string moduleName = type.Name;
+ List dependsOn = new List();
+ bool onDemand = false;
+ var moduleAttribute =
+ CustomAttributeData.GetCustomAttributes(type).FirstOrDefault(
+ cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleAttribute).FullName);
+
+ if (moduleAttribute != null)
+ {
+ foreach (CustomAttributeNamedArgument argument in moduleAttribute.NamedArguments)
+ {
+ string argumentName = argument.MemberInfo.Name;
+ switch (argumentName)
+ {
+ case "ModuleName":
+ moduleName = (string)argument.TypedValue.Value;
+ break;
+
+ case "OnDemand":
+ onDemand = (bool)argument.TypedValue.Value;
+ break;
+
+ case "StartupLoaded":
+ onDemand = !((bool)argument.TypedValue.Value);
+ break;
+ }
+ }
+ }
+
+ var moduleDependencyAttributes =
+ CustomAttributeData.GetCustomAttributes(type).Where(
+ cad => cad.Constructor.DeclaringType.FullName == typeof(ModuleDependencyAttribute).FullName);
+
+ foreach (CustomAttributeData cad in moduleDependencyAttributes)
+ {
+ dependsOn.Add((string)cad.ConstructorArguments[0].Value);
+ }
+
+ ModuleInfo moduleInfo = new ModuleInfo(moduleName, type.AssemblyQualifiedName)
+ {
+ InitializationMode = onDemand ? InitializationMode.OnDemand : InitializationMode.WhenAvailable,
+ Ref = type.Assembly.EscapedCodeBase,
+ };
+
+ moduleInfo.DependsOn.AddRange(dependsOn);
+ return moduleInfo;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs
new file mode 100644
index 0000000000..5bdf0dde36
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/FileModuleTypeLoader.Desktop.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Loads modules from an arbitrary location on the filesystem. This typeloader is only called if
+ /// classes have a Ref parameter that starts with "file://".
+ /// This class is only used on the Desktop version of the Prism Library.
+ ///
+ public class FileModuleTypeLoader : IModuleTypeLoader, IDisposable
+ {
+ private const string RefFilePrefix = "file://";
+
+ private readonly IAssemblyResolver assemblyResolver;
+ private HashSet downloadedUris = new HashSet();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This is disposed of in the Dispose method.")]
+ public FileModuleTypeLoader()
+ : this(new AssemblyResolver())
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The assembly resolver.
+ public FileModuleTypeLoader(IAssemblyResolver assemblyResolver)
+ {
+ this.assemblyResolver = assemblyResolver;
+ }
+
+ ///
+ /// Raised repeatedly to provide progress as modules are loaded in the background.
+ ///
+ public event EventHandler ModuleDownloadProgressChanged;
+
+ private void RaiseModuleDownloadProgressChanged(IModuleInfo moduleInfo, long bytesReceived, long totalBytesToReceive)
+ {
+ this.RaiseModuleDownloadProgressChanged(new ModuleDownloadProgressChangedEventArgs(moduleInfo, bytesReceived, totalBytesToReceive));
+ }
+
+ private void RaiseModuleDownloadProgressChanged(ModuleDownloadProgressChangedEventArgs e)
+ {
+ ModuleDownloadProgressChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Raised when a module is loaded or fails to load.
+ ///
+ public event EventHandler LoadModuleCompleted;
+
+ private void RaiseLoadModuleCompleted(IModuleInfo moduleInfo, Exception error)
+ {
+ this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, error));
+ }
+
+ private void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e)
+ {
+ this.LoadModuleCompleted?.Invoke(this, e);
+ }
+
+ ///
+ /// Evaluates the property to see if the current typeloader will be able to retrieve the .
+ /// Returns true if the property starts with "file://", because this indicates that the file
+ /// is a local file.
+ ///
+ /// Module that should have it's type loaded.
+ ///
+ /// if the current typeloader is able to retrieve the module, otherwise .
+ ///
+ /// An is thrown if is null.
+ public bool CanLoadModuleType(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo == null)
+ {
+ throw new ArgumentNullException(nameof(moduleInfo));
+ }
+
+ return moduleInfo.Ref != null && moduleInfo.Ref.StartsWith(RefFilePrefix, StringComparison.Ordinal);
+ }
+
+ ///
+ /// Retrieves the .
+ ///
+ /// Module that should have it's type loaded.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is rethrown as part of a completion event")]
+ public void LoadModuleType(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo == null)
+ {
+ throw new ArgumentNullException(nameof(moduleInfo));
+ }
+
+ try
+ {
+ Uri uri = new Uri(moduleInfo.Ref, UriKind.RelativeOrAbsolute);
+
+ // If this module has already been downloaded, I fire the completed event.
+ if (this.IsSuccessfullyDownloaded(uri))
+ {
+ this.RaiseLoadModuleCompleted(moduleInfo, null);
+ }
+ else
+ {
+ string path = uri.LocalPath;
+
+ long fileSize = -1L;
+ if (File.Exists(path))
+ {
+ FileInfo fileInfo = new FileInfo(path);
+ fileSize = fileInfo.Length;
+ }
+
+ // Although this isn't asynchronous, nor expected to take very long, I raise progress changed for consistency.
+ this.RaiseModuleDownloadProgressChanged(moduleInfo, 0, fileSize);
+
+ this.assemblyResolver.LoadAssemblyFrom(moduleInfo.Ref);
+
+ // Although this isn't asynchronous, nor expected to take very long, I raise progress changed for consistency.
+ this.RaiseModuleDownloadProgressChanged(moduleInfo, fileSize, fileSize);
+
+ // I remember the downloaded URI.
+ this.RecordDownloadSuccess(uri);
+
+ this.RaiseLoadModuleCompleted(moduleInfo, null);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.RaiseLoadModuleCompleted(moduleInfo, ex);
+ }
+ }
+
+ private bool IsSuccessfullyDownloaded(Uri uri)
+ {
+ lock (this.downloadedUris)
+ {
+ return this.downloadedUris.Contains(uri);
+ }
+ }
+
+ private void RecordDownloadSuccess(Uri uri)
+ {
+ lock (this.downloadedUris)
+ {
+ this.downloadedUris.Add(uri);
+ }
+ }
+
+ #region Implementation of IDisposable
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// Calls .
+ /// 2
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes the associated .
+ ///
+ /// When , it is being called from the Dispose method.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (this.assemblyResolver is IDisposable disposableResolver)
+ {
+ disposableResolver.Dispose();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs
new file mode 100644
index 0000000000..e0c3c9b9c9
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/IAssemblyResolver.Desktop.cs
@@ -0,0 +1,14 @@
+namespace Prism.Modularity
+{
+ ///
+ /// Interface for classes that are responsible for resolving and loading assembly files.
+ ///
+ public interface IAssemblyResolver
+ {
+ ///
+ /// Load an assembly when it's required by the application.
+ ///
+ ///
+ void LoadAssemblyFrom(string assemblyFilePath);
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs
new file mode 100644
index 0000000000..d6e3cc91dc
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/IConfigurationStore.Desktop.cs
@@ -0,0 +1,14 @@
+namespace Prism.Modularity
+{
+ ///
+ /// Defines a store for the module metadata.
+ ///
+ public interface IConfigurationStore
+ {
+ ///
+ /// Gets the module configuration data.
+ ///
+ /// A instance.
+ ModulesConfigurationSection RetrieveModuleConfigurationSection();
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs b/src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs
new file mode 100644
index 0000000000..8c999a08e1
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/IModuleCatalogExtensions.cs
@@ -0,0 +1,187 @@
+using System;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+ ///
+ /// extensions.
+ ///
+ public static class IModuleCatalogExtensions
+ {
+ ///
+ /// Adds the module to the .
+ ///
+ /// The catalog to add the module to.
+ /// The to use.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The type parameter.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, InitializationMode mode = InitializationMode.WhenAvailable, params string[] dependsOn)
+ where T : IModule
+ {
+ return catalog.AddModule(typeof(T).Name, mode, dependsOn);
+ }
+
+ ///
+ /// Adds the module to the .
+ ///
+ /// The catalog to add the module to.
+ /// Name of the module to be added.
+ /// The to use.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The type parameter.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, string name, InitializationMode mode = InitializationMode.WhenAvailable, params string[] dependsOn)
+ where T : IModule
+ {
+ return catalog.AddModule(name, typeof(T).AssemblyQualifiedName, mode, dependsOn);
+ }
+
+ ///
+ /// Adds a groupless to the catalog.
+ ///
+ /// The catalog to add the module to.
+ /// of the module to be added.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, Type moduleType, params string[] dependsOn)
+ {
+ return catalog.AddModule(moduleType, InitializationMode.WhenAvailable, dependsOn);
+ }
+
+ ///
+ /// Adds a groupless to the catalog.
+ ///
+ /// The catalog to add the module to.
+ /// of the module to be added.
+ /// Stage on which the module to be added will be initialized.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, Type moduleType, InitializationMode initializationMode, params string[] dependsOn)
+ {
+ if (moduleType == null)
+ throw new ArgumentNullException(nameof(moduleType));
+
+ return catalog.AddModule(moduleType.Name, moduleType.AssemblyQualifiedName, initializationMode, dependsOn);
+ }
+
+ ///
+ /// Adds a groupless to the catalog.
+ ///
+ /// The catalog to add the module to.
+ /// Name of the module to be added.
+ /// of the module to be added.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, string moduleName, string moduleType, params string[] dependsOn)
+ {
+ return catalog.AddModule(moduleName, moduleType, InitializationMode.WhenAvailable, dependsOn);
+ }
+
+ ///
+ /// Adds a groupless to the catalog.
+ ///
+ /// The catalog to add the module to.
+ /// Name of the module to be added.
+ /// of the module to be added.
+ /// Stage on which the module to be added will be initialized.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, string moduleName, string moduleType, InitializationMode initializationMode, params string[] dependsOn)
+ {
+ return catalog.AddModule(moduleName, moduleType, null, initializationMode, dependsOn);
+ }
+
+ ///
+ /// Adds a groupless to the catalog.
+ ///
+ /// The catalog to add the module to.
+ /// Name of the module to be added.
+ /// of the module to be added.
+ /// Reference to the location of the module to be added assembly.
+ /// Stage on which the module to be added will be initialized.
+ /// Collection of module names ( ) of the modules on which the module to be added logically depends on.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, string moduleName, string moduleType, string refValue, InitializationMode initializationMode, params string[] dependsOn)
+ {
+ if (moduleName == null)
+ throw new ArgumentNullException(nameof(moduleName));
+
+ if (moduleType == null)
+ throw new ArgumentNullException(nameof(moduleType));
+
+ ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleType, dependsOn)
+ {
+ InitializationMode = initializationMode,
+ Ref = refValue
+ };
+ return catalog.AddModule(moduleInfo);
+ }
+
+ ///
+ /// Adds the module to the .
+ ///
+ /// The catalog to add the module to.
+ /// The to use.
+ /// The type parameter.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, InitializationMode mode = InitializationMode.WhenAvailable)
+ where T : IModule =>
+ catalog.AddModule(typeof(T).Name, mode);
+
+ ///
+ /// Adds the module to the .
+ ///
+ /// The catalog to add the module to.
+ /// Name of the module to be added.
+ /// The type parameter.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, string name)
+ where T : IModule =>
+ catalog.AddModule(name, InitializationMode.WhenAvailable);
+
+ ///
+ /// Adds the module to the .
+ ///
+ /// The catalog to add the module to.
+ /// Name of the module to be added.
+ /// The to use.
+ /// The type parameter.
+ /// The same instance with the added module.
+ public static IModuleCatalog AddModule(this IModuleCatalog catalog, string name, InitializationMode mode)
+ where T : IModule =>
+ catalog.AddModule(new ModuleInfo(typeof(T), name, mode));
+
+ ///
+ /// Creates and adds a to the catalog.
+ ///
+ /// The catalog to add the module to.
+ /// Stage on which the module group to be added will be initialized.
+ /// Reference to the location of the module group to be added.
+ /// Collection of included in the group.
+ /// The same with the added module group.
+ public static IModuleCatalog AddGroup(this IModuleCatalog catalog, InitializationMode initializationMode, string refValue, params ModuleInfo[] moduleInfos)
+ {
+ if (!(catalog is IModuleGroupsCatalog groupSupport))
+ throw new NotSupportedException(Resources.MustBeModuleGroupCatalog);
+
+ if (moduleInfos == null)
+ throw new ArgumentNullException(nameof(moduleInfos));
+
+ ModuleInfoGroup newGroup = new ModuleInfoGroup
+ {
+ InitializationMode = initializationMode,
+ Ref = refValue
+ };
+
+ foreach (var info in moduleInfos)
+ {
+ newGroup.Add(info);
+ }
+
+ groupSupport.Items.Add(newGroup);
+
+ return catalog;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs b/src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs
new file mode 100644
index 0000000000..ac1c7b30fc
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/IModuleGroupsCatalog.cs
@@ -0,0 +1,17 @@
+using System.Collections.ObjectModel;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Defines a model that can get the collection of .
+ ///
+ public interface IModuleGroupsCatalog
+ {
+ ///
+ /// Gets the items in the . This property is mainly used to add s or
+ /// s through XAML.
+ ///
+ /// The items in the catalog.
+ Collection Items { get; }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs b/src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs
new file mode 100644
index 0000000000..a22ac48b42
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/IModuleTypeLoader.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Defines the interface for moduleTypeLoaders
+ ///
+ public interface IModuleTypeLoader
+ {
+ ///
+ /// Evaluates the property to see if the current typeloader will be able to retrieve the .
+ ///
+ /// Module that should have it's type loaded.
+ /// if the current typeloader is able to retrieve the module, otherwise .
+ bool CanLoadModuleType(IModuleInfo moduleInfo);
+
+ ///
+ /// Retrieves the .
+ ///
+ /// Module that should have it's type loaded.
+ void LoadModuleType(IModuleInfo moduleInfo);
+
+ ///
+ /// Raised repeatedly to provide progress as modules are downloaded in the background.
+ ///
+ event EventHandler ModuleDownloadProgressChanged;
+
+ ///
+ /// Raised when a module is loaded or fails to load.
+ ///
+ ///
+ /// This event is raised once per ModuleInfo instance requested in .
+ ///
+ event EventHandler LoadModuleCompleted;
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs
new file mode 100644
index 0000000000..e62a2905b9
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleAttribute.Desktop.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Indicates that the class should be considered a named module using the
+ /// provided module name.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public sealed class ModuleAttribute : Attribute
+ {
+ ///
+ /// Gets or sets the name of the module.
+ ///
+ /// The name of the module.
+ public string ModuleName { get; set; }
+
+ ///
+ /// Gets or sets the value indicating whether the module should be loaded OnDemand.
+ ///
+ /// When (default value), it indicates the module should be loaded as soon as it's dependencies are satisfied.
+ /// Otherwise you should explicitly load this module via the .
+ public bool OnDemand { get; set; }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs
new file mode 100644
index 0000000000..a358c79f8b
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleCatalog.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using Avalonia.Metadata;
+
+namespace Prism.Modularity
+{
+ ///
+ /// The holds information about the modules that can be used by the
+ /// application. Each module is described in a class, that records the
+ /// name, type and location of the module.
+ ///
+ /// It also verifies that the is internally valid. That means that
+ /// it does not have:
+ ///
+ /// - Circular dependencies
+ /// - Missing dependencies
+ /// -
+ /// Invalid dependencies, such as a Module that's loaded at startup that depends on a module
+ /// that might need to be retrieved.
+ ///
+ ///
+ /// The also serves as a baseclass for more specialized Catalogs .
+ ///
+ ////[ContentProperty("Items")] // Avalonia does use, System.Windows.Markup. See property `Items` below.
+ public class ModuleCatalog : ModuleCatalogBase, IModuleGroupsCatalog
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ModuleCatalog() : base()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class while providing an
+ /// initial list of s.
+ ///
+ /// The initial list of modules.
+ public ModuleCatalog(IEnumerable modules) : base(modules)
+ {
+ }
+
+ ///
+ /// Gets the items in the Prism.Modularity.IModuleCatalog. This property is mainly
+ /// used to add Prism.Modularity.IModuleInfoGroups or Prism.Modularity.IModuleInfos
+ /// through XAML.
+ ///
+ [Content]
+ public new Collection Items => base.Items;
+
+ ///
+ /// Creates a valid file uri to locate the module assembly file
+ ///
+ /// The relative path to the file
+ /// The valid absolute file path
+ protected virtual string GetFileAbsoluteUri(string filePath)
+ {
+ UriBuilder uriBuilder = new UriBuilder();
+ uriBuilder.Host = String.Empty;
+ uriBuilder.Scheme = Uri.UriSchemeFile;
+ uriBuilder.Path = Path.GetFullPath(filePath);
+ Uri fileUri = uriBuilder.Uri;
+
+ return fileUri.ToString();
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs
new file mode 100644
index 0000000000..189f0f1a14
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElement.Desktop.cs
@@ -0,0 +1,88 @@
+using System.Configuration;
+
+namespace Prism.Modularity
+{
+ ///
+ /// A configuration element to declare module metadata.
+ ///
+ public class ModuleConfigurationElement : ConfigurationElement
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ModuleConfigurationElement()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The assembly file where the module is located.
+ /// The type of the module.
+ /// The name of the module.
+ /// This attribute specifies whether the module is loaded at startup.
+ public ModuleConfigurationElement(string assemblyFile, string moduleType, string moduleName, bool startupLoaded)
+ {
+ base["assemblyFile"] = assemblyFile;
+ base["moduleType"] = moduleType;
+ base["moduleName"] = moduleName;
+ base["startupLoaded"] = startupLoaded;
+ }
+
+ ///
+ /// Gets or sets the assembly file.
+ ///
+ /// The assembly file.
+ [ConfigurationProperty("assemblyFile", IsRequired = true)]
+ public string AssemblyFile
+ {
+ get { return (string)base["assemblyFile"]; }
+ set { base["assemblyFile"] = value; }
+ }
+
+ ///
+ /// Gets or sets the module type.
+ ///
+ /// The module's type.
+ [ConfigurationProperty("moduleType", IsRequired = true)]
+ public string ModuleType
+ {
+ get { return (string)base["moduleType"]; }
+ set { base["moduleType"] = value; }
+ }
+
+ ///
+ /// Gets or sets the module name.
+ ///
+ /// The module's name.
+ [ConfigurationProperty("moduleName", IsRequired = true)]
+ public string ModuleName
+ {
+ get { return (string)base["moduleName"]; }
+ set { base["moduleName"] = value; }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether the module should be loaded at startup.
+ ///
+ /// A value indicating whether the module should be loaded at startup.
+ [ConfigurationProperty("startupLoaded", IsRequired = false, DefaultValue = true)]
+ public bool StartupLoaded
+ {
+ get { return (bool)base["startupLoaded"]; }
+ set { base["startupLoaded"] = value; }
+ }
+
+ ///
+ /// Gets or sets the modules this module depends on.
+ ///
+ /// The names of the modules that this depends on.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
+ [ConfigurationProperty("dependencies", IsDefaultCollection = true, IsKey = false)]
+ public ModuleDependencyCollection Dependencies
+ {
+ get { return (ModuleDependencyCollection)base["dependencies"]; }
+ set { base["dependencies"] = value; }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs
new file mode 100644
index 0000000000..f7af7ede5d
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleConfigurationElementCollection.Desktop.cs
@@ -0,0 +1,141 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+
+namespace Prism.Modularity
+{
+ ///
+ /// A collection of .
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")]
+ public class ModuleConfigurationElementCollection : ConfigurationElementCollection
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ModuleConfigurationElementCollection()
+ {
+ }
+
+ ///
+ /// Initializes a new .
+ ///
+ /// The initial set of .
+ /// An is thrown if is .
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
+ public ModuleConfigurationElementCollection(ModuleConfigurationElement[] modules)
+ {
+ if (modules == null)
+ throw new ArgumentNullException(nameof(modules));
+
+ foreach (ModuleConfigurationElement module in modules)
+ {
+ BaseAdd(module);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether an exception should be raised if a duplicate element is found.
+ /// This property will always return true.
+ ///
+ /// A value.
+ protected override bool ThrowOnDuplicate
+ {
+ get { return true; }
+ }
+
+ ///
+ ///Gets the type of the .
+ ///
+ ///
+ ///The of this collection.
+ ///
+ public override ConfigurationElementCollectionType CollectionType
+ {
+ get { return ConfigurationElementCollectionType.BasicMap; }
+ }
+
+ ///
+ ///Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class.
+ ///
+ ///
+ ///The name of the collection; otherwise, an empty string.
+ ///
+ protected override string ElementName
+ {
+ get { return "module"; }
+ }
+
+ ///
+ /// Gets the located at the specified index in the collection.
+ ///
+ /// The index of the element in the collection.
+ /// A .
+ public ModuleConfigurationElement this[int index]
+ {
+ get { return (ModuleConfigurationElement)base.BaseGet(index); }
+ }
+
+ ///
+ /// Adds a to the collection.
+ ///
+ /// A instance.
+ public void Add(ModuleConfigurationElement module)
+ {
+ BaseAdd(module);
+ }
+
+ ///
+ /// Tests if the collection contains the configuration for the specified module name.
+ ///
+ /// The name of the module to search the configuration for.
+ /// if a configuration for the module is present; otherwise .
+ public bool Contains(string moduleName)
+ {
+ return base.BaseGet(moduleName) != null;
+ }
+
+ ///
+ /// Searches the collection for all the that match the specified predicate.
+ ///
+ /// A that implements the match test.
+ /// A with the successful matches.
+ /// An is thrown if is null.
+ public IList FindAll(Predicate match)
+ {
+ if (match == null)
+ throw new ArgumentNullException(nameof(match));
+
+ IList found = new List();
+ foreach (ModuleConfigurationElement moduleElement in this)
+ {
+ if (match(moduleElement))
+ {
+ found.Add(moduleElement);
+ }
+ }
+ return found;
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// A .
+ protected override ConfigurationElement CreateNewElement()
+ {
+ return new ModuleConfigurationElement();
+ }
+
+ ///
+ /// Gets the element key for a specified configuration element when overridden in a derived class.
+ ///
+ /// The to return the key for.
+ ///
+ /// An that acts as the key for the specified .
+ ///
+ protected override object GetElementKey(ConfigurationElement element)
+ {
+ return ((ModuleConfigurationElement)element).ModuleName;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs
new file mode 100644
index 0000000000..112ace84fb
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyCollection.Desktop.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Configuration;
+
+namespace Prism.Modularity
+{
+ ///
+ /// A collection of .
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")]
+ public class ModuleDependencyCollection : ConfigurationElementCollection
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ModuleDependencyCollection()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// An array of with initial list of dependencies.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
+ public ModuleDependencyCollection(ModuleDependencyConfigurationElement[] dependencies)
+ {
+ if (dependencies == null)
+ throw new ArgumentNullException(nameof(dependencies));
+
+ foreach (ModuleDependencyConfigurationElement dependency in dependencies)
+ {
+ BaseAdd(dependency);
+ }
+ }
+
+ ///
+ ///Gets the type of the .
+ ///
+ ///
+ ///The of this collection.
+ ///
+ public override ConfigurationElementCollectionType CollectionType
+ {
+ get { return ConfigurationElementCollectionType.BasicMap; }
+ }
+
+ ///
+ ///Gets the name used to identify this collection of elements in the configuration file when overridden in a derived class.
+ ///
+ ///
+ ///The name of the collection; otherwise, an empty string.
+ ///
+ protected override string ElementName
+ {
+ get { return "dependency"; }
+ }
+
+ ///
+ /// Gets the located at the specified index in the collection.
+ ///
+ /// The index of the element in the collection.
+ /// A .
+ public ModuleDependencyConfigurationElement this[int index]
+ {
+ get { return (ModuleDependencyConfigurationElement)base.BaseGet(index); }
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// A .
+ protected override ConfigurationElement CreateNewElement()
+ {
+ return new ModuleDependencyConfigurationElement();
+ }
+
+ ///
+ ///Gets the element key for a specified configuration element when overridden in a derived class.
+ ///
+ /// The to return the key for.
+ ///
+ ///An that acts as the key for the specified .
+ ///
+ protected override object GetElementKey(ConfigurationElement element)
+ {
+ return ((ModuleDependencyConfigurationElement)element).ModuleName;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs
new file mode 100644
index 0000000000..a6e486e50b
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleDependencyConfigurationElement.Desktop.cs
@@ -0,0 +1,37 @@
+using System.Configuration;
+
+namespace Prism.Modularity
+{
+ ///
+ /// A for module dependencies.
+ ///
+ public class ModuleDependencyConfigurationElement : ConfigurationElement
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ public ModuleDependencyConfigurationElement()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// A module name.
+ public ModuleDependencyConfigurationElement(string moduleName)
+ {
+ base["moduleName"] = moduleName;
+ }
+
+ ///
+ /// Gets or sets the name of a module another module depends on.
+ ///
+ /// The name of a module another module depends on.
+ [ConfigurationProperty("moduleName", IsRequired = true, IsKey = true)]
+ public string ModuleName
+ {
+ get { return (string)base["moduleName"]; }
+ set { base["moduleName"] = value; }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs
new file mode 100644
index 0000000000..ba324a0bcf
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.Desktop.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Prism.Modularity
+{
+ [Serializable]
+ public partial class ModuleInfo
+ {
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs
new file mode 100644
index 0000000000..62fb28c628
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfo.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.ObjectModel;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Defines the metadata that describes a module.
+ ///
+ public partial class ModuleInfo : IModuleInfo
+ {
+ ///
+ /// Initializes a new empty instance of .
+ ///
+ public ModuleInfo()
+ : this(null, null, new string[0])
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The module's name.
+ /// The module 's AssemblyQualifiedName.
+ /// The modules this instance depends on.
+ /// An is thrown if is .
+ public ModuleInfo(string name, string type, params string[] dependsOn)
+ {
+ if (dependsOn == null)
+ throw new ArgumentNullException(nameof(dependsOn));
+
+ this.ModuleName = name;
+ this.ModuleType = type;
+ this.DependsOn = new Collection();
+ foreach (string dependency in dependsOn)
+ {
+ this.DependsOn.Add(dependency);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The module's name.
+ /// The module's type.
+ public ModuleInfo(string name, string type) : this(name, type, new string[0])
+ {
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The module's type.
+ public ModuleInfo(Type moduleType)
+ : this(moduleType, moduleType.Name) { }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The module's type.
+ /// The module's name.
+ public ModuleInfo(Type moduleType, string moduleName)
+ : this(moduleType, moduleName, InitializationMode.WhenAvailable) { }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The module's type.
+ /// The module's name.
+ /// The module's .
+ public ModuleInfo(Type moduleType, string moduleName, InitializationMode initializationMode)
+ : this(moduleName, moduleType.AssemblyQualifiedName)
+ {
+ InitializationMode = initializationMode;
+ }
+
+ ///
+ /// Gets or sets the name of the module.
+ ///
+ /// The name of the module.
+ public string ModuleName { get; set; }
+
+ ///
+ /// Gets or sets the module 's AssemblyQualifiedName.
+ ///
+ /// The type of the module.
+ public string ModuleType { get; set; }
+
+ ///
+ /// Gets or sets the list of modules that this module depends upon.
+ ///
+ /// The list of modules that this module depends upon.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", Justification = "The setter is here to work around a Silverlight issue with setting properties from within Xaml.")]
+ public Collection DependsOn { get; set; }
+
+ ///
+ /// Specifies on which stage the Module will be initialized.
+ ///
+ public InitializationMode InitializationMode { get; set; }
+
+ ///
+ /// Reference to the location of the module assembly.
+ /// The following are examples of valid values:
+ /// file://c:/MyProject/Modules/MyModule.dll for a loose DLL in WPF.
+ ///
+ ///
+ public string Ref { get; set; }
+
+ ///
+ /// Gets or sets the state of the with regards to the module loading and initialization process.
+ ///
+ public ModuleState State { get; set; }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs
new file mode 100644
index 0000000000..82a1031345
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroup.cs
@@ -0,0 +1,349 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Represents a group of instances that are usually deployed together. s
+ /// are also used by the to prevent common deployment problems such as having a module that's required
+ /// at startup that depends on modules that will only be downloaded on demand.
+ ///
+ /// The group also forwards and values to the s that it
+ /// contains.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
+ public class ModuleInfoGroup : IModuleInfoGroup
+ {
+ private readonly Collection _modules = new Collection();
+
+ ///
+ /// Gets or sets the for the whole group. Any classes that are
+ /// added after setting this value will also get this .
+ ///
+ ///
+ /// The initialization mode.
+ public InitializationMode InitializationMode { get; set; }
+
+ ///
+ /// Gets or sets the value for the whole group. Any classes that are
+ /// added after setting this value will also get this .
+ ///
+ /// The ref value will also be used by the to determine which to use.
+ /// For example, using an "file://" prefix with a valid URL will cause the FileModuleTypeLoader to be used
+ /// (Only available in the desktop version of CAL).
+ ///
+ ///
+ /// The ref value that will be used.
+ public string Ref { get; set; }
+
+ ///
+ /// Adds an moduleInfo to the .
+ ///
+ /// The to the .
+ public void Add(IModuleInfo item)
+ {
+ ForwardValues(item);
+ _modules.Add(item);
+ }
+
+ internal void UpdateModulesRef()
+ {
+ foreach (var module in _modules)
+ {
+ module.Ref = Ref;
+ }
+ }
+
+ ///
+ /// Forwards and properties from this
+ /// to .
+ ///
+ /// The module info to forward values to.
+ /// An is thrown if is .
+ protected void ForwardValues(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo == null)
+ throw new ArgumentNullException(nameof(moduleInfo));
+
+ if (moduleInfo.Ref == null)
+ {
+ moduleInfo.Ref = Ref;
+ }
+
+ if (moduleInfo.InitializationMode == InitializationMode.WhenAvailable && InitializationMode != InitializationMode.WhenAvailable)
+ {
+ moduleInfo.InitializationMode = InitializationMode;
+ }
+ }
+
+ ///
+ /// Removes all s from the .
+ ///
+ public void Clear() => _modules.Clear();
+
+ ///
+ /// Determines whether the contains a specific value.
+ ///
+ /// The object to locate in the .
+ ///
+ /// true if is found in the ; otherwise, false.
+ ///
+ public bool Contains(IModuleInfo item) => _modules.Contains(item);
+
+ ///
+ /// Copies the elements of the to an , starting at a particular index.
+ ///
+ /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing.
+ /// The zero-based index in at which copying begins.
+ ///
+ /// is null.
+ ///
+ ///
+ /// is less than 0.
+ ///
+ ///
+ /// is multidimensional.
+ /// -or-
+ /// is equal to or greater than the length of .
+ /// -or-
+ /// The number of elements in the source is greater than the available space from to the end of the destination .
+ ///
+ public void CopyTo(IModuleInfo[] array, int arrayIndex)
+ {
+ _modules.CopyTo(array, arrayIndex);
+ }
+
+ ///
+ /// Gets the number of elements contained in the .
+ ///
+ ///
+ ///
+ /// The number of elements contained in the .
+ ///
+ public int Count => _modules.Count;
+
+ ///
+ /// Gets a value indicating whether the is read-only.
+ ///
+ ///
+ /// false, because the is not Read-Only.
+ ///
+ public bool IsReadOnly => false;
+
+ ///
+ /// Removes the first occurrence of a specific object from the .
+ ///
+ /// The object to remove from the .
+ ///
+ /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original .
+ ///
+ public bool Remove(IModuleInfo item) => _modules.Remove(item);
+
+ ///
+ /// Returns an enumerator that iterates through the collection.
+ ///
+ ///
+ /// A that can be used to iterate through the collection.
+ ///
+ public IEnumerator GetEnumerator() => _modules.GetEnumerator();
+
+ ///
+ /// Returns an enumerator that iterates through a collection.
+ ///
+ ///
+ /// An object that can be used to iterate through the collection.
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ ///
+ /// Adds an item to the .
+ ///
+ ///
+ /// The to add to the .
+ /// Must be of type
+ ///
+ ///
+ /// The position into which the new element was inserted.
+ ///
+ int IList.Add(object value)
+ {
+ this.Add((IModuleInfo)value);
+ return 1;
+ }
+
+ ///
+ /// Determines whether the contains a specific value.
+ ///
+ ///
+ /// The to locate in the .
+ /// Must be of type
+ ///
+ ///
+ /// true if the is found in the ; otherwise, false.
+ ///
+ bool IList.Contains(object value)
+ {
+ if (value == null)
+ throw new ArgumentNullException(nameof(value));
+
+ if (!(value is IModuleInfo moduleInfo))
+ throw new ArgumentException(Resources.ValueMustBeOfTypeModuleInfo, nameof(value));
+
+ return Contains(moduleInfo);
+ }
+
+ ///
+ /// Determines the index of a specific item in the .
+ ///
+ ///
+ /// The to locate in the .
+ /// Must be of type
+ ///
+ ///
+ /// The index of if found in the list; otherwise, -1.
+ ///
+ public int IndexOf(object value) => _modules.IndexOf((IModuleInfo)value);
+
+ ///
+ /// Inserts an item to the at the specified index.
+ ///
+ /// The zero-based index at which should be inserted.
+ ///
+ /// The to insert into the .
+ /// Must be of type
+ ///
+ ///
+ /// is not a valid index in the .
+ ///
+ ///
+ /// If is null.
+ ///
+ ///
+ /// If is not of type
+ ///
+ public void Insert(int index, object value)
+ {
+ if (value == null)
+ throw new ArgumentNullException(nameof(value));
+
+ if (!(value is IModuleInfo moduleInfo))
+ throw new ArgumentException(Resources.ValueMustBeOfTypeModuleInfo, nameof(value));
+
+ _modules.Insert(index, moduleInfo);
+ }
+
+ ///
+ /// Gets a value indicating whether the has a fixed size.
+ ///
+ /// false, because the does not have a fixed length.
+ ///
+ public bool IsFixedSize => false;
+
+ ///
+ /// Removes the first occurrence of a specific object from the .
+ ///
+ ///
+ /// The to remove from the .
+ /// Must be of type
+ ///
+ void IList.Remove(object value)
+ {
+ Remove((IModuleInfo)value);
+ }
+
+ ///
+ /// Removes the item at the specified index.
+ ///
+ /// The zero-based index of the item to remove.
+ ///
+ /// is not a valid index in the .
+ ///
+ ///
+ /// The is read-only.
+ ///
+ public void RemoveAt(int index) => _modules.RemoveAt(index);
+
+ ///
+ /// Gets or sets the at the specified index.
+ ///
+ ///
+ object IList.this[int index]
+ {
+ get => this[index];
+ set => this[index] = (ModuleInfo)value;
+ }
+
+ ///
+ /// Copies the elements of the to an , starting at a particular index.
+ ///
+ /// The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing.
+ /// The zero-based index in at which copying begins.
+ ///
+ /// is null.
+ ///
+ ///
+ /// is less than zero.
+ ///
+ ///
+ /// is multidimensional.
+ /// -or-
+ /// is equal to or greater than the length of .
+ /// -or-
+ /// The number of elements in the source is greater than the available space from to the end of the destination .
+ ///
+ ///
+ /// The type of the source cannot be cast automatically to the type of the destination .
+ ///
+ void ICollection.CopyTo(Array array, int index) =>
+ ((ICollection)_modules).CopyTo(array, index);
+
+ ///
+ /// Gets a value indicating whether access to the is synchronized (thread safe).
+ ///
+ ///
+ /// true if access to the is synchronized (thread safe); otherwise, false.
+ ///
+ public bool IsSynchronized => ((ICollection)_modules).IsSynchronized;
+
+ ///
+ /// Gets an object that can be used to synchronize access to the .
+ ///
+ ///
+ ///
+ /// An object that can be used to synchronize access to the .
+ ///
+ public object SyncRoot => ((ICollection)_modules).SyncRoot;
+
+ ///
+ /// Determines the index of a specific item in the .
+ ///
+ /// The object to locate in the .
+ ///
+ /// The index of if found in the list; otherwise, -1.
+ ///
+ public int IndexOf(IModuleInfo item) => _modules.IndexOf(item);
+
+ ///
+ /// Inserts an item to the at the specified index.
+ ///
+ /// The zero-based index at which should be inserted.
+ /// The object to insert into the .
+ ///
+ /// is not a valid index in the .
+ ///
+ public void Insert(int index, IModuleInfo item) => _modules.Insert(index, item);
+
+ ///
+ /// Gets or sets the at the specified index.
+ ///
+ /// The at the specified index
+ public IModuleInfo this[int index]
+ {
+ get => _modules[index];
+ set => _modules[index] = value;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs
new file mode 100644
index 0000000000..2813965598
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInfoGroupExtensions.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.ObjectModel;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Defines extension methods for the class.
+ ///
+ public static class ModuleInfoGroupExtensions
+ {
+ ///
+ /// Adds a new module that is statically referenced to the specified module info group.
+ ///
+ /// The group where to add the module info in.
+ /// The name for the module.
+ /// The type for the module. This type should be a descendant of .
+ /// The names for the modules that this module depends on.
+ /// Returns the instance of the passed in module info group, to provide a fluid interface.
+ public static ModuleInfoGroup AddModule(
+ this ModuleInfoGroup moduleInfoGroup,
+ string moduleName,
+ Type moduleType,
+ params string[] dependsOn)
+ {
+ if (moduleType == null)
+ throw new ArgumentNullException(nameof(moduleType));
+
+ if (moduleInfoGroup == null)
+ throw new ArgumentNullException(nameof(moduleInfoGroup));
+
+ ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleType.AssemblyQualifiedName);
+ moduleInfo.DependsOn.AddRange(dependsOn);
+ moduleInfoGroup.Add(moduleInfo);
+ return moduleInfoGroup;
+ }
+
+ ///
+ /// Adds a new module that is statically referenced to the specified module info group.
+ ///
+ /// The group where to add the module info in.
+ /// The type for the module. This type should be a descendant of .
+ /// The names for the modules that this module depends on.
+ /// Returns the instance of the passed in module info group, to provide a fluid interface.
+ /// The name of the module will be the type name.
+ public static ModuleInfoGroup AddModule(
+ this ModuleInfoGroup moduleInfoGroup,
+ Type moduleType,
+ params string[] dependsOn)
+ {
+ if (moduleType == null)
+ throw new ArgumentNullException(nameof(moduleType));
+
+ return AddModule(moduleInfoGroup, moduleType.Name, moduleType, dependsOn);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs
new file mode 100644
index 0000000000..5047fd8829
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleInitializer.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Globalization;
+using Prism.Ioc;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Implements the interface. Handles loading of a module based on a type.
+ ///
+ public class ModuleInitializer : IModuleInitializer
+ {
+ private readonly IContainerExtension _containerExtension;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The container that will be used to resolve the modules by specifying its type.
+ public ModuleInitializer(IContainerExtension containerExtension)
+ {
+ this._containerExtension = containerExtension ?? throw new ArgumentNullException(nameof(containerExtension));
+ }
+
+ ///
+ /// Initializes the specified module.
+ ///
+ /// The module to initialize
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Catches Exception to handle any exception thrown during the initialization process with the HandleModuleInitializationError method.")]
+ public void Initialize(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo == null)
+ throw new ArgumentNullException(nameof(moduleInfo));
+
+ IModule moduleInstance = null;
+ try
+ {
+ moduleInstance = this.CreateModule(moduleInfo);
+ if (moduleInstance != null)
+ {
+ moduleInstance.RegisterTypes(_containerExtension);
+ moduleInstance.OnInitialized(_containerExtension);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.HandleModuleInitializationError(
+ moduleInfo,
+ moduleInstance?.GetType().Assembly.FullName,
+ ex);
+ }
+ }
+
+ ///
+ /// Handles any exception occurred in the module Initialization process,
+ /// This method can be overridden to provide a different behavior.
+ ///
+ /// The module metadata where the error happened.
+ /// The assembly name.
+ /// The exception thrown that is the cause of the current error.
+ ///
+ public virtual void HandleModuleInitializationError(IModuleInfo moduleInfo, string assemblyName, Exception exception)
+ {
+ if (moduleInfo == null)
+ throw new ArgumentNullException(nameof(moduleInfo));
+
+ if (exception == null)
+ throw new ArgumentNullException(nameof(exception));
+
+ Exception moduleException;
+
+ if (exception is ModuleInitializeException)
+ {
+ moduleException = exception;
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(assemblyName))
+ {
+ moduleException = new ModuleInitializeException(moduleInfo.ModuleName, assemblyName, exception.Message, exception);
+ }
+ else
+ {
+ moduleException = new ModuleInitializeException(moduleInfo.ModuleName, exception.Message, exception);
+ }
+ }
+
+ throw moduleException;
+ }
+
+ ///
+ /// Uses the container to resolve a new by specifying its .
+ ///
+ /// The module to create.
+ /// A new instance of the module specified by .
+ protected virtual IModule CreateModule(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo == null)
+ throw new ArgumentNullException(nameof(moduleInfo));
+
+ return this.CreateModule(moduleInfo.ModuleType);
+ }
+
+ ///
+ /// Uses the container to resolve a new by specifying its .
+ ///
+ /// The type name to resolve. This type must implement .
+ /// A new instance of .
+ protected virtual IModule CreateModule(string typeName)
+ {
+ Type moduleType = Type.GetType(typeName);
+ if (moduleType == null)
+ {
+ throw new ModuleInitializeException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.FailedToGetType, typeName));
+ }
+
+ return (IModule)_containerExtension.Resolve(moduleType);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs
new file mode 100644
index 0000000000..3d2a3f0a52
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.Desktop.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Component responsible for coordinating the modules' type loading and module initialization process.
+ ///
+ public partial class ModuleManager
+ {
+ ///
+ /// Returns the list of registered instances that will be
+ /// used to load the types of modules.
+ ///
+ /// The module type loaders.
+ public virtual IEnumerable ModuleTypeLoaders
+ {
+ get
+ {
+ if (this.typeLoaders == null)
+ {
+ this.typeLoaders = new List
+ {
+ new FileModuleTypeLoader()
+ };
+ }
+
+ return this.typeLoaders;
+ }
+
+ set
+ {
+ this.typeLoaders = value;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs
new file mode 100644
index 0000000000..f27ec26d6d
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleManager.cs
@@ -0,0 +1,316 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Prism.Properties;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Component responsible for coordinating the modules' type loading and module initialization process.
+ ///
+ public partial class ModuleManager : IModuleManager, IDisposable
+ {
+ private readonly IModuleInitializer moduleInitializer;
+ private IEnumerable typeLoaders;
+ private HashSet subscribedToModuleTypeLoaders = new HashSet();
+
+ ///
+ /// Initializes an instance of the class.
+ ///
+ /// Service used for initialization of modules.
+ /// Catalog that enumerates the modules to be loaded and initialized.
+ public ModuleManager(IModuleInitializer moduleInitializer, IModuleCatalog moduleCatalog)
+ {
+ this.moduleInitializer = moduleInitializer ?? throw new ArgumentNullException(nameof(moduleInitializer));
+ ModuleCatalog = moduleCatalog ?? throw new ArgumentNullException(nameof(moduleCatalog));
+ }
+
+ ///
+ /// The module catalog specified in the constructor.
+ ///
+ protected IModuleCatalog ModuleCatalog { get; }
+
+ ///
+ /// Gets all the classes that are in the .
+ ///
+ public IEnumerable Modules => ModuleCatalog.Modules;
+
+ ///
+ /// Raised repeatedly to provide progress as modules are loaded in the background.
+ ///
+ public event EventHandler ModuleDownloadProgressChanged;
+
+ private void RaiseModuleDownloadProgressChanged(ModuleDownloadProgressChangedEventArgs e)
+ {
+ ModuleDownloadProgressChanged?.Invoke(this, e);
+ }
+
+ ///
+ /// Raised when a module is loaded or fails to load.
+ ///
+ public event EventHandler LoadModuleCompleted;
+
+ private void RaiseLoadModuleCompleted(IModuleInfo moduleInfo, Exception error)
+ {
+ this.RaiseLoadModuleCompleted(new LoadModuleCompletedEventArgs(moduleInfo, error));
+ }
+
+ private void RaiseLoadModuleCompleted(LoadModuleCompletedEventArgs e)
+ {
+ this.LoadModuleCompleted?.Invoke(this, e);
+ }
+
+ ///
+ /// Initializes the modules marked as on the .
+ ///
+ public void Run()
+ {
+ this.ModuleCatalog.Initialize();
+
+ this.LoadModulesWhenAvailable();
+ }
+
+
+ ///
+ /// Loads and initializes the module on the with the name .
+ ///
+ /// Name of the module requested for initialization.
+ public void LoadModule(string moduleName)
+ {
+ var module = this.ModuleCatalog.Modules.Where(m => m.ModuleName == moduleName);
+ if (module == null || module.Count() != 1)
+ {
+ throw new ModuleNotFoundException(moduleName, string.Format(CultureInfo.CurrentCulture, Resources.ModuleNotFound, moduleName));
+ }
+
+ var modulesToLoad = this.ModuleCatalog.CompleteListWithDependencies(module);
+
+ this.LoadModuleTypes(modulesToLoad);
+ }
+
+ ///
+ /// Checks if the module needs to be retrieved before it's initialized.
+ ///
+ /// Module that is being checked if needs retrieval.
+ ///
+ protected virtual bool ModuleNeedsRetrieval(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo == null)
+ throw new ArgumentNullException(nameof(moduleInfo));
+
+ if (moduleInfo.State == ModuleState.NotStarted)
+ {
+ // If we can instantiate the type, that means the module's assembly is already loaded into
+ // the AppDomain and we don't need to retrieve it.
+ bool isAvailable = Type.GetType(moduleInfo.ModuleType) != null;
+ if (isAvailable)
+ {
+ moduleInfo.State = ModuleState.ReadyForInitialization;
+ }
+
+ return !isAvailable;
+ }
+
+ return false;
+ }
+
+ private void LoadModulesWhenAvailable()
+ {
+ var whenAvailableModules = this.ModuleCatalog.Modules.Where(m => m.InitializationMode == InitializationMode.WhenAvailable);
+ var modulesToLoadTypes = this.ModuleCatalog.CompleteListWithDependencies(whenAvailableModules);
+ if (modulesToLoadTypes != null)
+ {
+ this.LoadModuleTypes(modulesToLoadTypes);
+ }
+ }
+
+ private void LoadModuleTypes(IEnumerable moduleInfos)
+ {
+ if (moduleInfos == null)
+ {
+ return;
+ }
+
+ foreach (var moduleInfo in moduleInfos)
+ {
+ if (moduleInfo.State == ModuleState.NotStarted)
+ {
+ if (this.ModuleNeedsRetrieval(moduleInfo))
+ {
+ this.BeginRetrievingModule(moduleInfo);
+ }
+ else
+ {
+ moduleInfo.State = ModuleState.ReadyForInitialization;
+ }
+ }
+ }
+
+ this.LoadModulesThatAreReadyForLoad();
+ }
+
+ ///
+ /// Loads the modules that are not initialized and have their dependencies loaded.
+ ///
+ protected virtual void LoadModulesThatAreReadyForLoad()
+ {
+ bool keepLoading = true;
+ while (keepLoading)
+ {
+ keepLoading = false;
+ var availableModules = this.ModuleCatalog.Modules.Where(m => m.State == ModuleState.ReadyForInitialization);
+
+ foreach (var moduleInfo in availableModules)
+ {
+ if ((moduleInfo.State != ModuleState.Initialized) && (this.AreDependenciesLoaded(moduleInfo)))
+ {
+ moduleInfo.State = ModuleState.Initializing;
+ this.InitializeModule(moduleInfo);
+ keepLoading = true;
+ break;
+ }
+ }
+ }
+ }
+
+ private void BeginRetrievingModule(IModuleInfo moduleInfo)
+ {
+ var moduleInfoToLoadType = moduleInfo;
+ IModuleTypeLoader moduleTypeLoader = this.GetTypeLoaderForModule(moduleInfoToLoadType);
+ moduleInfoToLoadType.State = ModuleState.LoadingTypes;
+
+ // Delegate += works differently between SL and WPF.
+ // We only want to subscribe to each instance once.
+ if (!this.subscribedToModuleTypeLoaders.Contains(moduleTypeLoader))
+ {
+ moduleTypeLoader.ModuleDownloadProgressChanged += this.IModuleTypeLoader_ModuleDownloadProgressChanged;
+ moduleTypeLoader.LoadModuleCompleted += this.IModuleTypeLoader_LoadModuleCompleted;
+ this.subscribedToModuleTypeLoaders.Add(moduleTypeLoader);
+ }
+
+ moduleTypeLoader.LoadModuleType(moduleInfo);
+ }
+
+ private void IModuleTypeLoader_ModuleDownloadProgressChanged(object sender, ModuleDownloadProgressChangedEventArgs e)
+ {
+ this.RaiseModuleDownloadProgressChanged(e);
+ }
+
+ private void IModuleTypeLoader_LoadModuleCompleted(object sender, LoadModuleCompletedEventArgs e)
+ {
+ if (e.Error == null)
+ {
+ if ((e.ModuleInfo.State != ModuleState.Initializing) && (e.ModuleInfo.State != ModuleState.Initialized))
+ {
+ e.ModuleInfo.State = ModuleState.ReadyForInitialization;
+ }
+
+ // This callback may call back on the UI thread, but we are not guaranteeing it.
+ // If you were to add a custom retriever that retrieved in the background, you
+ // would need to consider dispatching to the UI thread.
+ this.LoadModulesThatAreReadyForLoad();
+ }
+ else
+ {
+ this.RaiseLoadModuleCompleted(e);
+
+ // If the error is not handled then I log it and raise an exception.
+ if (!e.IsErrorHandled)
+ {
+ this.HandleModuleTypeLoadingError(e.ModuleInfo, e.Error);
+ }
+ }
+ }
+
+ ///
+ /// Handles any exception occurred in the module typeloading process,
+ /// and throws a .
+ /// This method can be overridden to provide a different behavior.
+ ///
+ /// The module metadata where the error happened.
+ /// The exception thrown that is the cause of the current error.
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1")]
+ protected virtual void HandleModuleTypeLoadingError(IModuleInfo moduleInfo, Exception exception)
+ {
+ if (moduleInfo == null)
+ throw new ArgumentNullException(nameof(moduleInfo));
+
+
+ if (!(exception is ModuleTypeLoadingException moduleTypeLoadingException))
+ {
+ moduleTypeLoadingException = new ModuleTypeLoadingException(moduleInfo.ModuleName, exception.Message, exception);
+ }
+
+ throw moduleTypeLoadingException;
+ }
+
+ private bool AreDependenciesLoaded(IModuleInfo moduleInfo)
+ {
+ var requiredModules = this.ModuleCatalog.GetDependentModules(moduleInfo);
+ if (requiredModules == null)
+ {
+ return true;
+ }
+
+ int notReadyRequiredModuleCount =
+ requiredModules.Count(requiredModule => requiredModule.State != ModuleState.Initialized);
+
+ return notReadyRequiredModuleCount == 0;
+ }
+
+ private IModuleTypeLoader GetTypeLoaderForModule(IModuleInfo moduleInfo)
+ {
+ foreach (IModuleTypeLoader typeLoader in this.ModuleTypeLoaders)
+ {
+ if (typeLoader.CanLoadModuleType(moduleInfo))
+ {
+ return typeLoader;
+ }
+ }
+
+ throw new ModuleTypeLoaderNotFoundException(moduleInfo.ModuleName, string.Format(CultureInfo.CurrentCulture, Resources.NoRetrieverCanRetrieveModule, moduleInfo.ModuleName), null);
+ }
+
+ private void InitializeModule(IModuleInfo moduleInfo)
+ {
+ if (moduleInfo.State == ModuleState.Initializing)
+ {
+ this.moduleInitializer.Initialize(moduleInfo);
+ moduleInfo.State = ModuleState.Initialized;
+ this.RaiseLoadModuleCompleted(moduleInfo, null);
+ }
+ }
+
+ #region Implementation of IDisposable
+
+ ///
+ /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+ ///
+ /// Calls .
+ /// 2
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes the associated s.
+ ///
+ /// When , it is being called from the Dispose method.
+ protected virtual void Dispose(bool disposing)
+ {
+ foreach (IModuleTypeLoader typeLoader in this.ModuleTypeLoaders)
+ {
+ if (typeLoader is IDisposable disposableTypeLoader)
+ {
+ disposableTypeLoader.Dispose();
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs
new file mode 100644
index 0000000000..fb7e0a919d
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.Desktop.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Prism.Modularity
+{
+ [Serializable]
+ public partial class ModuleTypeLoaderNotFoundException
+ {
+ ///
+ /// Initializes a new instance with serialized data.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ protected ModuleTypeLoaderNotFoundException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs
new file mode 100644
index 0000000000..fdf7ddd04f
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModuleTypeLoaderNotFoundException.cs
@@ -0,0 +1,53 @@
+using System;
+
+namespace Prism.Modularity
+{
+ ///
+ /// Exception that's thrown when there is no registered in
+ /// that can handle this particular type of module.
+ ///
+ public partial class ModuleTypeLoaderNotFoundException : ModularityException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ModuleTypeLoaderNotFoundException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ ///
+ /// The message that describes the error.
+ ///
+ public ModuleTypeLoaderNotFoundException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ ///
+ /// The message that describes the error.
+ ///
+ /// The inner exception
+ public ModuleTypeLoaderNotFoundException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes the exception with a particular module, error message and inner exception that happened.
+ ///
+ /// The name of the module.
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception,
+ /// or a reference if no inner exception is specified.
+ public ModuleTypeLoaderNotFoundException(string moduleName, string message, Exception innerException)
+ : base(moduleName, message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs b/src/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs
new file mode 100644
index 0000000000..9fc6a10a72
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/ModulesConfigurationSection.Desktop.cs
@@ -0,0 +1,22 @@
+using System.Configuration;
+
+namespace Prism.Modularity
+{
+ ///
+ /// A for module configuration.
+ ///
+ public class ModulesConfigurationSection : ConfigurationSection
+ {
+ ///
+ /// Gets or sets the collection of modules configuration.
+ ///
+ /// A of .
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
+ [ConfigurationProperty("", IsDefaultCollection = true, IsKey = false)]
+ public ModuleConfigurationElementCollection Modules
+ {
+ get { return (ModuleConfigurationElementCollection)base[""]; }
+ set { base[""] = value; }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs b/src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs
new file mode 100644
index 0000000000..f66ca007de
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Modularity/XamlModuleCatalog.cs
@@ -0,0 +1,121 @@
+// TODO: This feature is currently disabled temporally
+// NOTE: This is only used by Prism.WPF and not Prism.UNO, Prism.Forms, or Prism.MAUI
+/*
+using System;
+using System.IO;
+using Avalonia.Markup.Xaml;
+
+namespace Prism.Modularity
+{
+ ///
+ /// A catalog built from a XAML file.
+ ///
+ public class XamlModuleCatalog : ModuleCatalog
+ {
+ private readonly Uri _resourceUri;
+
+ private const string _refFilePrefix = "file://";
+ private int _refFilePrefixLength = _refFilePrefix.Length;
+
+ ///
+ /// Creates an instance of a XamlResourceCatalog.
+ ///
+ /// The name of the XAML file
+ public XamlModuleCatalog(string fileName)
+ : this(new Uri(fileName, UriKind.Relative))
+ {
+ }
+
+ ///
+ /// Creates an instance of a XamlResourceCatalog.
+ ///
+ /// The pack url of the XAML file resource
+ public XamlModuleCatalog(Uri resourceUri)
+ {
+ _resourceUri = resourceUri;
+ }
+
+ ///
+ /// Loads the catalog from the XAML file.
+ ///
+ protected override void InnerLoad()
+ {
+ var catalog = CreateFromXaml(_resourceUri);
+
+ foreach (IModuleCatalogItem item in catalog.Items)
+ {
+ if (item is ModuleInfo mi)
+ {
+ if (!string.IsNullOrWhiteSpace(mi.Ref))
+ mi.Ref = GetFileAbsoluteUri(mi.Ref);
+ }
+ else if (item is ModuleInfoGroup mg)
+ {
+ if (!string.IsNullOrWhiteSpace(mg.Ref))
+ {
+ mg.Ref = GetFileAbsoluteUri(mg.Ref);
+ mg.UpdateModulesRef();
+ }
+ else
+ {
+ foreach (var module in mg)
+ {
+ module.Ref = GetFileAbsoluteUri(module.Ref);
+ }
+ }
+ }
+
+ Items.Add(item);
+ }
+ }
+
+ ///
+ protected override string GetFileAbsoluteUri(string path)
+ {
+ //this is to maintain backwards compatibility with the old file:/// and file:// syntax for Xaml module catalog Ref property
+ if (path.StartsWith(_refFilePrefix + "/", StringComparison.Ordinal))
+ {
+ path = path.Substring(_refFilePrefixLength + 1);
+ }
+ else if (path.StartsWith(_refFilePrefix, StringComparison.Ordinal))
+ {
+ path = path.Substring(_refFilePrefixLength);
+ }
+
+ return base.GetFileAbsoluteUri(path);
+ }
+
+ ///
+ /// Creates a from XAML.
+ ///
+ /// that contains the XAML declaration of the catalog.
+ /// An instance of built from the XAML.
+ private static ModuleCatalog CreateFromXaml(Stream xamlStream)
+ {
+ if (xamlStream == null)
+ {
+ throw new ArgumentNullException(nameof(xamlStream));
+ }
+
+ return AvaloniaRuntimeXamlLoader.Load(xamlStream, null) as ModuleCatalog;
+ }
+
+ ///
+ /// Creates a from a XAML included as an Application Resource.
+ ///
+ /// Relative that identifies the XAML included as an Application Resource.
+ /// An instance of build from the XAML.
+ private static ModuleCatalog CreateFromXaml(Uri builderResourceUri)
+ {
+ var streamInfo = System.Windows.Application.GetResourceStream(builderResourceUri);
+
+ if ((streamInfo != null) && (streamInfo.Stream != null))
+ {
+ return CreateFromXaml(streamInfo.Stream);
+ }
+
+ return null;
+ }
+ }
+}
+*/
diff --git a/src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs b/src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs
new file mode 100644
index 0000000000..b6c68f5d87
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Mvvm/ViewModelLocator.cs
@@ -0,0 +1,69 @@
+using System.ComponentModel;
+using System.Threading;
+using System;
+using Avalonia;
+using Avalonia.Controls;
+
+namespace Prism.Mvvm
+{
+ ///
+ /// This class defines the attached property and related change handler that calls the ViewModelLocator in Prism.Mvvm.
+ ///
+ public static class ViewModelLocator
+ {
+ static ViewModelLocator()
+ {
+ // Bind AutoWireViewModelProperty.Changed to its callback
+ AutoWireViewModelProperty.Changed.Subscribe(args => AutoWireViewModelChanged(args?.Sender, args));
+ }
+
+ ///
+ /// The AutoWireViewModel attached property.
+ ///
+ public static AvaloniaProperty AutoWireViewModelProperty =
+ AvaloniaProperty.RegisterAttached(
+ name: "AutoWireViewModel",
+ ownerType: typeof(ViewModelLocator),
+ defaultValue: null);
+
+ ///
+ /// Gets the value for the attached property.
+ ///
+ /// The target element.
+ /// The attached to the element.
+ public static bool? GetAutoWireViewModel(AvaloniaObject obj)
+ {
+ return (bool?)obj.GetValue(AutoWireViewModelProperty);
+ }
+
+ ///
+ /// Sets the attached property.
+ ///
+ /// The target element.
+ /// The value to attach.
+ public static void SetAutoWireViewModel(AvaloniaObject obj, bool value)
+ {
+ obj.SetValue(AutoWireViewModelProperty, value);
+ }
+
+ private static void AutoWireViewModelChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
+ {
+ var value = (bool?)e.NewValue;
+ if (value.HasValue && value.Value)
+ {
+ ViewModelLocationProvider.AutoWireViewModelChanged(d, Bind);
+ }
+ }
+
+ ///
+ /// Sets the DataContext of a View
+ ///
+ /// The View to set the DataContext on
+ /// The object to use as the DataContext for the View
+ static void Bind(object view, object viewModel)
+ {
+ if (view is Avalonia.Controls.Control element)
+ element.DataContext = viewModel;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs
new file mode 100644
index 0000000000..4f43a6ade1
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/AllActiveRegion.cs
@@ -0,0 +1,27 @@
+using System;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Region that keeps all the views in it as active. Deactivation of views is not allowed.
+ ///
+ public class AllActiveRegion : Region
+ {
+ ///
+ /// Gets a readonly view of the collection of all the active views in the region. These are all the added views.
+ ///
+ /// An of all the active views.
+ public override IViewsCollection ActiveViews => Views;
+
+ ///
+ /// Deactivate is not valid in this Region. This method will always throw .
+ ///
+ /// The view to deactivate.
+ /// Every time this method is called.
+ public override void Deactivate(object view)
+ {
+ throw new InvalidOperationException(Resources.DeactiveNotPossibleException);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs
new file mode 100644
index 0000000000..52657c0626
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/AutoPopulateRegionBehavior.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using Prism.Ioc;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Populates the target region with the views registered to it in the .
+ ///
+ public class AutoPopulateRegionBehavior : RegionBehavior
+ {
+ ///
+ /// The key of this behavior.
+ ///
+ public const string BehaviorKey = "AutoPopulate";
+
+ private readonly IRegionViewRegistry regionViewRegistry;
+
+ ///
+ /// Creates a new instance of the AutoPopulateRegionBehavior
+ /// associated with the received.
+ ///
+ /// that the behavior will monitor for views to populate the region.
+ public AutoPopulateRegionBehavior(IRegionViewRegistry regionViewRegistry)
+ {
+ this.regionViewRegistry = regionViewRegistry;
+ }
+
+ ///
+ /// Attaches the AutoPopulateRegionBehavior to the Region.
+ ///
+ protected override void OnAttach()
+ {
+ if (string.IsNullOrEmpty(Region.Name))
+ {
+ Region.PropertyChanged += Region_PropertyChanged;
+ }
+ else
+ {
+ StartPopulatingContent();
+ }
+ }
+
+ private void StartPopulatingContent()
+ {
+ foreach (object view in CreateViewsToAutoPopulate())
+ {
+ AddViewIntoRegion(view);
+ }
+
+ regionViewRegistry.ContentRegistered += OnViewRegistered;
+ }
+
+ ///
+ /// Returns a collection of views that will be added to the
+ /// View collection.
+ ///
+ ///
+ protected virtual IEnumerable CreateViewsToAutoPopulate()
+ {
+ return regionViewRegistry.GetContents(Region.Name);
+ }
+
+ ///
+ /// Adds a view into the views collection of this region.
+ ///
+ ///
+ protected virtual void AddViewIntoRegion(object viewToAdd)
+ {
+ Region.Add(viewToAdd);
+ }
+
+ private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name))
+ {
+ Region.PropertyChanged -= Region_PropertyChanged;
+ StartPopulatingContent();
+ }
+ }
+
+ ///
+ /// Handler of the event that fires when a new viewtype is registered to the registry.
+ ///
+ /// Although this is a public method to support Weak Delegates in Silverlight, it should not be called by the user.
+ ///
+ ///
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers", Justification = "This has to be public in order to work with weak references in partial trust or Silverlight environments.")]
+ public virtual void OnViewRegistered(object sender, ViewRegisteredEventArgs e)
+ {
+ if (e == null)
+ throw new ArgumentNullException(nameof(e));
+
+ if (e.RegionName == Region.Name)
+ {
+ AddViewIntoRegion(e.GetView(ContainerLocator.Container));
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs
new file mode 100644
index 0000000000..27da651e5e
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/BindRegionContextToAvaloniaObjectBehavior.cs
@@ -0,0 +1,102 @@
+using Avalonia;
+using Prism.Common;
+using System.Collections;
+using System.Collections.Specialized;
+using System.ComponentModel;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Defines a behavior that forwards the
+ /// to the views in the region.
+ ///
+ public class BindRegionContextToAvaloniaObjectBehavior : IRegionBehavior
+ {
+ /// The key of this behavior.
+ /// TODO (DS 2024-04-11): This SHOULD be ''ContextToAvaloniaObject'.
+ public const string BehaviorKey = "ContextToDependencyObject";
+
+ /// Behavior's attached region.
+ public IRegion Region { get; set; }
+
+ /// Attaches the behavior to the specified region.
+ public void Attach()
+ {
+ Region.Views.CollectionChanged += Views_CollectionChanged;
+ Region.PropertyChanged += Region_PropertyChanged;
+ SetContextToViews(Region.Views, Region.Context);
+ AttachNotifyChangeEvent(Region.Views);
+ }
+
+ private static void SetContextToViews(IEnumerable views, object context)
+ {
+ foreach (var view in views)
+ {
+ AvaloniaObject avaloniaObjectView = view as AvaloniaObject;
+ if (avaloniaObjectView != null)
+ {
+ ObservableObject contextWrapper = RegionContext.GetObservableContext(avaloniaObjectView);
+ contextWrapper.Value = context;
+ }
+ }
+ }
+
+ private void AttachNotifyChangeEvent(IEnumerable views)
+ {
+ foreach (var view in views)
+ {
+ var avaloniaObject = view as AvaloniaObject;
+ if (avaloniaObject != null)
+ {
+ ObservableObject viewRegionContext = RegionContext.GetObservableContext(avaloniaObject);
+ viewRegionContext.PropertyChanged += ViewRegionContext_OnPropertyChangedEvent;
+ }
+ }
+ }
+
+ private void DetachNotifyChangeEvent(IEnumerable views)
+ {
+ foreach (var view in views)
+ {
+ var avaloniaObject = view as AvaloniaObject;
+ if (avaloniaObject != null)
+ {
+ ObservableObject viewRegionContext = RegionContext.GetObservableContext(avaloniaObject);
+ viewRegionContext.PropertyChanged -= ViewRegionContext_OnPropertyChangedEvent;
+ }
+ }
+ }
+
+ private void ViewRegionContext_OnPropertyChangedEvent(object sender, PropertyChangedEventArgs args)
+ {
+ if (args.PropertyName == "Value")
+ {
+ var context = (ObservableObject)sender;
+ Region.Context = context.Value;
+ }
+ }
+
+ private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Add)
+ {
+ SetContextToViews(e.NewItems, Region.Context);
+ AttachNotifyChangeEvent(e.NewItems);
+ }
+ else if (e.Action == NotifyCollectionChangedAction.Remove && Region.Context != null)
+ {
+ DetachNotifyChangeEvent(e.OldItems);
+ SetContextToViews(e.OldItems, null);
+
+ }
+ }
+
+ private void Region_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == "Context")
+ {
+ SetContextToViews(Region.Views, Region.Context);
+ }
+ }
+}
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs
new file mode 100644
index 0000000000..94ffbd858b
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/ClearChildViewsRegionBehavior.cs
@@ -0,0 +1,89 @@
+using Avalonia;
+using Avalonia.Controls;
+using Prism.Navigation.Regions;
+using System;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Behavior that removes the RegionManager attached property of all the views in a region once the RegionManager property of a region becomes null.
+ /// This is useful when removing views with nested regions, to ensure these nested regions get removed from the RegionManager as well.
+ ///
+ /// This behavior does not apply by default.
+ /// In order to activate it, the ClearChildViews attached property must be set to True in the view containing the affected child regions.
+ ///
+ ///
+ public class ClearChildViewsRegionBehavior : RegionBehavior
+ {
+ ///
+ /// The behavior key.
+ ///
+ public const string BehaviorKey = "ClearChildViews";
+
+ ///
+ /// This attached property can be defined on a view to indicate that regions defined in it must be removed from the region manager when the parent view gets removed from a region.
+ ///
+ public static readonly AvaloniaProperty ClearChildViewsProperty =
+ AvaloniaProperty.RegisterAttached("ClearChildViews", typeof(ClearChildViewsRegionBehavior));
+
+ ///
+ /// Gets the ClearChildViews attached property from a .
+ ///
+ /// The object from which to get the value.
+ /// The value of the ClearChildViews attached property in the target specified.
+ public static bool GetClearChildViews(AvaloniaObject target)
+ {
+ if (target == null)
+ throw new ArgumentNullException(nameof(target));
+
+ return (bool)target.GetValue(ClearChildViewsRegionBehavior.ClearChildViewsProperty);
+ }
+
+ ///
+ /// Sets the ClearChildViews attached property in a .
+ ///
+ /// The object in which to set the value.
+ /// The value of to set in the target object's ClearChildViews attached property.
+ public static void SetClearChildViews(AvaloniaObject target, bool value)
+ {
+ if (target == null)
+ throw new ArgumentNullException(nameof(target));
+
+ target.SetValue(ClearChildViewsRegionBehavior.ClearChildViewsProperty, value);
+ }
+
+ ///
+ /// Subscribes to the 's PropertyChanged method to monitor its property.
+ ///
+ protected override void OnAttach()
+ {
+ Region.PropertyChanged += Region_PropertyChanged;
+ }
+
+ private static void ClearChildViews(IRegion region)
+ {
+ foreach (var view in region.Views)
+ {
+ AvaloniaObject avaloniaObject = view as AvaloniaObject;
+ if (avaloniaObject != null)
+ {
+ if (GetClearChildViews(avaloniaObject))
+ {
+ avaloniaObject.ClearValue(RegionManager.RegionManagerProperty);
+ }
+ }
+ }
+ }
+
+ private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == "RegionManager")
+ {
+ if (Region.RegionManager == null)
+ {
+ ClearChildViews(Region);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs
new file mode 100644
index 0000000000..b20e641d9a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DelayedRegionCreationBehavior.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Behavior that creates a new , when the control that will host the (see )
+ /// is added to the VisualTree. This behavior will use the class to find the right type of adapter to create
+ /// the region. After the region is created, this behavior will detach.
+ ///
+ ///
+ /// Attached property value inheritance is not available in Silverlight, so the current approach walks up the visual tree when requesting a region from a region manager.
+ /// The is now responsible for walking up the Tree.
+ ///
+ public class DelayedRegionCreationBehavior
+ {
+ private readonly RegionAdapterMappings regionAdapterMappings;
+ private WeakReference elementWeakReference;
+ private bool regionCreated;
+
+ private static ICollection _instanceTracker = new Collection();
+ private object _trackerLock = new object();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// The region adapter mappings, that are used to find the correct adapter for
+ /// a given control type. The control type is determined by the value.
+ ///
+ public DelayedRegionCreationBehavior(RegionAdapterMappings regionAdapterMappings)
+ {
+ this.regionAdapterMappings = regionAdapterMappings;
+ RegionManagerAccessor = new DefaultRegionManagerAccessor();
+ }
+
+ ///
+ /// Sets a class that interfaces between the 's static properties/events and this behavior,
+ /// so this behavior can be tested in isolation.
+ ///
+ /// The region manager accessor.
+ public IRegionManagerAccessor RegionManagerAccessor { get; set; }
+
+ ///
+ /// The element that will host the Region.
+ ///
+ /// The target element.
+ public AvaloniaObject TargetElement
+ {
+ get { return elementWeakReference != null ? elementWeakReference.Target as AvaloniaObject : null; }
+ set { elementWeakReference = new WeakReference(value); }
+ }
+
+ ///
+ /// Start monitoring the and the to detect when the becomes
+ /// part of the Visual Tree. When that happens, the Region will be created and the behavior will .
+ ///
+ public void Attach()
+ {
+ RegionManagerAccessor.UpdatingRegions += OnUpdatingRegions;
+ WireUpTargetElement();
+ }
+
+ ///
+ /// Stop monitoring the and the , so that this behavior can be garbage collected.
+ ///
+ public void Detach()
+ {
+ RegionManagerAccessor.UpdatingRegions -= OnUpdatingRegions;
+ UnWireTargetElement();
+ }
+
+ ///
+ /// Called when the is updating it's collection.
+ ///
+ ///
+ /// This method has to be public, because it has to be callable using weak references in silverlight and other partial trust environments.
+ ///
+ /// The .
+ /// The instance containing the event data.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers", Justification = "This has to be public in order to work with weak references in partial trust or Silverlight environments.")]
+ public void OnUpdatingRegions(object sender, EventArgs e)
+ {
+ TryCreateRegion();
+ }
+
+ private void TryCreateRegion()
+ {
+ AvaloniaObject targetElement = TargetElement;
+ if (targetElement == null)
+ {
+ Detach();
+ return;
+ }
+
+ if (Dispatcher.UIThread.CheckAccess())
+ {
+ Detach();
+
+ if (!regionCreated)
+ {
+ string regionName = RegionManagerAccessor.GetRegionName(targetElement);
+ CreateRegion(targetElement, regionName);
+ regionCreated = true;
+ }
+ }
+ }
+
+ ///
+ /// Method that will create the region, by calling the right .
+ ///
+ /// The target element that will host the .
+ /// Name of the region.
+ /// The created
+ protected virtual IRegion CreateRegion(AvaloniaObject targetElement, string regionName)
+ {
+ if (targetElement == null)
+ throw new ArgumentNullException(nameof(targetElement));
+
+ try
+ {
+ // Build the region
+ IRegionAdapter regionAdapter = regionAdapterMappings.GetMapping(targetElement.GetType());
+ IRegion region = regionAdapter.Initialize(targetElement, regionName);
+
+ return region;
+ }
+ catch (Exception ex)
+ {
+ throw new RegionCreationException(string.Format(CultureInfo.CurrentCulture, Resources.RegionCreationException, regionName, ex), ex);
+ }
+ }
+
+ private void ElementLoaded(object sender, VisualTreeAttachmentEventArgs e)
+ {
+ UnWireTargetElement();
+ TryCreateRegion();
+ }
+
+ private void WireUpTargetElement()
+ {
+ Control element = TargetElement as Control;
+ if (element != null)
+ {
+ element.AttachedToVisualTree += ElementLoaded;
+ return;
+ }
+
+ // TODO: NEEDS UPGRADED TO AVALONIA!
+ ////System.Windows.FrameworkContentElement fcElement = this.TargetElement as System.Windows.FrameworkContentElement;
+ ////Avalonia.Controls.Control fcElement = this.TargetElement as Control;
+ ////if (fcElement != null)
+ ////{
+ //// fcElement.Loaded += this.ElementLoaded;
+ //// return;
+ ////}
+
+ //if the element is a dependency object, and not a Control, nothing is holding onto the reference after the DelayedRegionCreationBehavior
+ //is instantiated inside RegionManager.CreateRegion(DependencyObject element). If the GC runs before RegionManager.UpdateRegions is called, the region will
+ //never get registered because it is gone from the updatingRegionsListeners list inside RegionManager. So we need to hold on to it. This should be rare.
+ AvaloniaObject depObj = TargetElement as AvaloniaObject;
+ if (depObj != null)
+ {
+ Track();
+ return;
+ }
+ }
+
+ private void UnWireTargetElement()
+ {
+ Control element = TargetElement as Control;
+ if (element != null)
+ {
+ element.AttachedToVisualTree -= ElementLoaded;
+ return;
+ }
+
+ // TODO: NEEDS UPGRADED TO AVALONIA!
+ //FrameworkContentElement fcElement = this.TargetElement as FrameworkContentElement;
+ //Avalonia.Controls.Control fcElement = this.TargetElement as Control;
+ //if (fcElement != null)
+ //{
+ // fcElement.Loaded -= this.ElementLoaded;
+ // return;
+ //}
+
+ AvaloniaObject depObj = TargetElement as AvaloniaObject;
+ if (depObj != null)
+ {
+ Untrack();
+ return;
+ }
+ }
+
+ ///
+ /// Add the instance of this class to to keep it alive
+ ///
+ private void Track()
+ {
+ lock (_trackerLock)
+ {
+ if (!_instanceTracker.Contains(this))
+ {
+ _instanceTracker.Add(this);
+ }
+ }
+ }
+
+ ///
+ /// Remove the instance of this class from
+ /// so it can eventually be garbage collected
+ ///
+ private void Untrack()
+ {
+ lock (_trackerLock)
+ {
+ _instanceTracker.Remove(this);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs
new file mode 100644
index 0000000000..47252c3610
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/DestructibleRegionBehavior.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Specialized;
+using Prism.Common;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Calls on Views and ViewModels
+ /// removed from the collection.
+ ///
+ ///
+ /// The View and/or ViewModels must implement for this behavior to work.
+ ///
+ public class DestructibleRegionBehavior : RegionBehavior
+ {
+ ///
+ /// The key of this behavior.
+ ///
+ public const string BehaviorKey = "IDestructibleRegionBehavior";
+
+ ///
+ /// Attaches the to the collection.
+ ///
+ protected override void OnAttach()
+ {
+ Region.Views.CollectionChanged += Views_CollectionChanged;
+ }
+
+ private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Remove)
+ {
+ foreach (var item in e.OldItems)
+ {
+ Action invocation = destructible => destructible.Destroy();
+ MvvmHelpers.ViewAndViewModelAction(item, invocation);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs
new file mode 100644
index 0000000000..3bfb34a16c
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/IHostAwareRegionBehavior.cs
@@ -0,0 +1,18 @@
+using Avalonia;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Defines a that not allows extensible behaviors on regions which also interact
+ /// with the target element that the is attached to.
+ ///
+ public interface IHostAwareRegionBehavior : IRegionBehavior
+ {
+ ///
+ /// Gets or sets the that the is attached to.
+ ///
+ /// A that the is attached to.
+ /// This is usually a that is part of the tree.
+ AvaloniaObject HostControl { get; set; }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs
new file mode 100644
index 0000000000..7eb8bc2d13
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionActiveAwareBehavior.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Collections.Specialized;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Prism.Common;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Behavior that monitors a object and
+ /// changes the value for the property when
+ /// an object that implements gets added or removed
+ /// from the collection.
+ ///
+ ///
+ /// This class can also sync the active state for any scoped regions directly on the view based on the .
+ /// If you use the method with the createRegionManagerScope option, the scoped manager will be attached to the view.
+ ///
+ public class RegionActiveAwareBehavior : IRegionBehavior
+ {
+ ///
+ /// Name that identifies the behavior in a collection of .
+ ///
+ public const string BehaviorKey = "ActiveAware";
+
+ ///
+ /// The region that this behavior is extending
+ ///
+ public IRegion Region { get; set; }
+
+ ///
+ /// Attaches the behavior to the specified region
+ ///
+ public void Attach()
+ {
+ INotifyCollectionChanged collection = GetCollection();
+ if (collection != null)
+ {
+ collection.CollectionChanged += OnCollectionChanged;
+ }
+ }
+
+ ///
+ /// Detaches the behavior from the .
+ ///
+ public void Detach()
+ {
+ INotifyCollectionChanged collection = GetCollection();
+ if (collection != null)
+ {
+ collection.CollectionChanged -= OnCollectionChanged;
+ }
+ }
+
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Add)
+ {
+ foreach (object item in e.NewItems)
+ {
+ Action invocation = activeAware => activeAware.IsActive = true;
+
+ MvvmHelpers.ViewAndViewModelAction(item, invocation);
+ InvokeOnSynchronizedActiveAwareChildren(item, invocation);
+ }
+ }
+ else if (e.Action == NotifyCollectionChangedAction.Remove)
+ {
+ foreach (object item in e.OldItems)
+ {
+ Action invocation = activeAware => activeAware.IsActive = false;
+
+ MvvmHelpers.ViewAndViewModelAction(item, invocation);
+ InvokeOnSynchronizedActiveAwareChildren(item, invocation);
+ }
+ }
+
+ // May need to handle other action values (reset, replace). Currently the ViewsCollection class does not raise CollectionChanged with these values.
+ }
+
+ private void InvokeOnSynchronizedActiveAwareChildren(object item, Action invocation)
+ {
+ var avaloniaObjectView = item as AvaloniaObject;
+
+ if (avaloniaObjectView != null)
+ {
+ // We are assuming that any scoped region managers are attached directly to the
+ // view.
+ var regionManager = RegionManager.GetRegionManager(avaloniaObjectView);
+
+ // If the view's RegionManager attached property is different from the region's RegionManager,
+ // then the view's region manager is a scoped region manager.
+ if (regionManager == null || regionManager == Region.RegionManager) return;
+
+ var activeViews = regionManager.Regions.SelectMany(e => e.ActiveViews);
+
+ var syncActiveViews = activeViews.Where(ShouldSyncActiveState);
+
+ foreach (var syncActiveView in syncActiveViews)
+ {
+ MvvmHelpers.ViewAndViewModelAction(syncActiveView, invocation);
+ }
+ }
+ }
+
+ private bool ShouldSyncActiveState(object view)
+ {
+ if (Attribute.IsDefined(view.GetType(), typeof(SyncActiveStateAttribute)))
+ {
+ return true;
+ }
+
+ var viewAsFrameworkElement = view as Control;
+
+ if (viewAsFrameworkElement != null)
+ {
+ var viewModel = viewAsFrameworkElement.DataContext;
+
+ return viewModel != null && Attribute.IsDefined(viewModel.GetType(), typeof(SyncActiveStateAttribute));
+ }
+
+ return false;
+ }
+
+ private INotifyCollectionChanged GetCollection()
+ {
+ return Region.ActiveViews;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs
new file mode 100644
index 0000000000..da9a3c322a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionManagerRegistrationBehavior.cs
@@ -0,0 +1,158 @@
+using System;
+using System.ComponentModel;
+using Prism.Properties;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.VisualTree;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Subscribes to a static event from the in order to register the target
+ /// in a when one is available on the host control by walking up the tree and finding
+ /// a control whose property is not .
+ ///
+ public class RegionManagerRegistrationBehavior : RegionBehavior, IHostAwareRegionBehavior
+ {
+ ///
+ /// The key of this behavior.
+ ///
+ public static readonly string BehaviorKey = "RegionManagerRegistration";
+
+ private WeakReference attachedRegionManagerWeakReference;
+ private AvaloniaObject hostControl;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public RegionManagerRegistrationBehavior()
+ {
+ RegionManagerAccessor = new DefaultRegionManagerAccessor();
+ }
+
+ ///
+ /// Provides an abstraction on top of the RegionManager static members.
+ ///
+ public IRegionManagerAccessor RegionManagerAccessor { get; set; }
+
+ ///
+ /// Gets or sets the that the is attached to.
+ ///
+ /// A that the is attached to.
+ /// This is usually a that is part of the tree.
+ /// When this member is set after the method has being called.
+ public AvaloniaObject HostControl
+ {
+ get
+ {
+ return hostControl;
+ }
+ set
+ {
+ if (IsAttached)
+ {
+ throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach);
+ }
+
+ hostControl = value;
+ }
+ }
+
+ ///
+ /// When the has a name assigned, the behavior will start monitoring the ancestor controls in the element tree
+ /// to look for an where to register the region in.
+ ///
+ protected override void OnAttach()
+ {
+ if (string.IsNullOrEmpty(Region.Name))
+ {
+ Region.PropertyChanged += Region_PropertyChanged;
+ }
+ else
+ {
+ StartMonitoringRegionManager();
+ }
+ }
+
+ private void Region_PropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == "Name" && !string.IsNullOrEmpty(Region.Name))
+ {
+ Region.PropertyChanged -= Region_PropertyChanged;
+ StartMonitoringRegionManager();
+ }
+ }
+
+ private void StartMonitoringRegionManager()
+ {
+ RegionManagerAccessor.UpdatingRegions += OnUpdatingRegions;
+ TryRegisterRegion();
+ }
+
+ private void TryRegisterRegion()
+ {
+ AvaloniaObject targetElement = HostControl;
+ if (targetElement.CheckAccess())
+ {
+ IRegionManager regionManager = FindRegionManager(targetElement);
+
+ IRegionManager attachedRegionManager = GetAttachedRegionManager();
+
+ if (regionManager != attachedRegionManager)
+ {
+ if (attachedRegionManager != null)
+ {
+ attachedRegionManagerWeakReference = null;
+ attachedRegionManager.Regions.Remove(Region.Name);
+ }
+
+ if (regionManager != null)
+ {
+ attachedRegionManagerWeakReference = new WeakReference(regionManager);
+ regionManager.Regions.Add(Region);
+ }
+ }
+ }
+ }
+
+ ///
+ /// This event handler gets called when a RegionManager is requering the instances of a region to be registered if they are not already.
+ /// Although this is a public method to support Weak Delegates in Silverlight, it should not be called by the user.
+ ///
+ /// The sender.
+ /// The arguments.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2109:ReviewVisibleEventHandlers", Justification = "This has to be public in order to work with weak references in partial trust or Silverlight environments.")]
+ public void OnUpdatingRegions(object sender, EventArgs e)
+ {
+ TryRegisterRegion();
+ }
+
+ private IRegionManager FindRegionManager(AvaloniaObject avaloniaObject)
+ {
+ var regionmanager = RegionManagerAccessor.GetRegionManager(avaloniaObject);
+ if (regionmanager != null)
+ {
+ return regionmanager;
+ }
+
+ //TODO: this is should be ok in Avalonia. I have to test it
+ AvaloniaObject parent = ((avaloniaObject as Visual)?.GetVisualParent() ?? null) as AvaloniaObject;
+ if (parent != null)
+ {
+ return FindRegionManager(parent);
+ }
+
+ return null;
+ }
+
+ private IRegionManager GetAttachedRegionManager()
+ {
+ if (attachedRegionManagerWeakReference != null)
+ {
+ return attachedRegionManagerWeakReference.Target as IRegionManager;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs
new file mode 100644
index 0000000000..a09a6b4d02
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/RegionMemberLifetimeBehavior.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using Avalonia.Controls;
+using Prism.Common;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// The RegionMemberLifetimeBehavior determines if items should be removed from the
+ /// when they are deactivated.
+ ///
+ ///
+ /// The monitors the
+ /// collection to discover items that transition into a deactivated state.
+ ///
+ /// The behavior checks the removed items for either the
+ /// or the (in that order) to determine if it should be kept
+ /// alive on removal.
+ ///
+ /// If the item in the collection is a , it will
+ /// also check it's DataContext for or the .
+ ///
+ /// The order of checks are:
+ ///
+ /// - Region Item's IRegionMemberLifetime.KeepAlive value.
+ /// - Region Item's DataContext's IRegionMemberLifetime.KeepAlive value.
+ /// - Region Item's RegionMemberLifetimeAttribute.KeepAlive value.
+ /// - Region Item's DataContext's RegionMemberLifetimeAttribute.KeepAlive value.
+ ///
+ ///
+ public class RegionMemberLifetimeBehavior : RegionBehavior
+ {
+ ///
+ /// The key for this behavior.
+ ///
+ public const string BehaviorKey = "RegionMemberLifetimeBehavior";
+
+ ///
+ /// Override this method to perform the logic after the behavior has been attached.
+ ///
+ protected override void OnAttach()
+ {
+ Region.ActiveViews.CollectionChanged += OnActiveViewsChanged;
+ }
+
+ private void OnActiveViewsChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ // We only pay attention to items removed from the ActiveViews list.
+ // Thus, we expect that any ICollectionView implementation would
+ // always raise a remove and we don't handle any resets
+ // unless we wanted to start tracking views that used to be active.
+ if (e.Action != NotifyCollectionChangedAction.Remove) return;
+
+ var inactiveViews = e.OldItems;
+ foreach (var inactiveView in inactiveViews)
+ {
+ if (!ShouldKeepAlive(inactiveView))
+ {
+ if (Region.Views.Contains(inactiveView))
+ Region.Remove(inactiveView);
+ }
+ }
+ }
+
+ private static bool ShouldKeepAlive(object inactiveView)
+ {
+ IRegionMemberLifetime lifetime = MvvmHelpers.GetImplementerFromViewOrViewModel(inactiveView);
+ if (lifetime != null)
+ {
+ return lifetime.KeepAlive;
+ }
+
+ RegionMemberLifetimeAttribute lifetimeAttribute = GetItemOrContextLifetimeAttribute(inactiveView);
+ if (lifetimeAttribute != null)
+ {
+ return lifetimeAttribute.KeepAlive;
+ }
+
+ return true;
+ }
+
+ private static RegionMemberLifetimeAttribute GetItemOrContextLifetimeAttribute(object inactiveView)
+ {
+ var lifetimeAttribute = GetCustomAttributes(inactiveView.GetType()).FirstOrDefault();
+ if (lifetimeAttribute != null)
+ {
+ return lifetimeAttribute;
+ }
+
+ var control = inactiveView as Control;
+ if (control != null && control.DataContext != null)
+ {
+ var dataContext = control.DataContext;
+ var contextLifetimeAttribute =
+ GetCustomAttributes(dataContext.GetType()).FirstOrDefault();
+ return contextLifetimeAttribute;
+ }
+
+ return null;
+ }
+
+ private static IEnumerable GetCustomAttributes(Type type)
+ {
+ return type.GetCustomAttributes(typeof(T), true).OfType();
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs
new file mode 100644
index 0000000000..11cc9fe180
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SelectorItemsSourceSyncBehavior.cs
@@ -0,0 +1,191 @@
+// TODO: 2022-07-08 - Feature disabled until a workaround can be created
+// Consider using Avalonia.Styling.IStylable or Avalonia.Styling.Selector
+// in place of WPF's `Selector` object or AvaloniaObject.
+// This theory is untested and causes issues on code such as, `hostControl.Items`
+// - private Selector hostControl;
+// - public IStyleable HostControl
+// Ref:
+// - https://github.com/AvaloniaUI/Avalonia/issues/3593
+// - https://stackoverflow.com/questions/44241761/how-do-style-selectors-work-in-avalonia
+// - https://stackoverflow.com/questions/72238118/avalonia-style-selector-doent-works-on-derived-classes
+/*
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using Prism.Properties;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Controls.Primitives;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Defines the attached behavior that keeps the items of the host control in synchronization with the .
+ ///
+ /// This behavior also makes sure that, if you activate a view in a region, the SelectedItem is set. If you set the SelectedItem or SelectedItems (ListBox)
+ /// then this behavior will also call Activate on the selected items.
+ ///
+ /// When calling Activate on a view, you can only select a single active view at a time. By setting the SelectedItems property of a listbox, you can set
+ /// multiple views to active.
+ ///
+ ///
+ public class SelectorItemsSourceSyncBehavior : RegionBehavior, IHostAwareRegionBehavior
+ {
+ ///
+ /// Name that identifies the SelectorItemsSourceSyncBehavior behavior in a collection of RegionsBehaviors.
+ ///
+ public static readonly string BehaviorKey = "SelectorItemsSourceSyncBehavior";
+ private bool updatingActiveViewsInHostControlSelectionChanged;
+ private Selector hostControl;
+
+ ///
+ /// Gets or sets the that the is attached to.
+ ///
+ ///
+ /// A that the is attached to.
+ ///
+ /// For this behavior, the host control must always be a or an inherited class.
+ public AvaloniaObject HostControl
+ {
+ get
+ {
+ return this.hostControl;
+ }
+
+ set
+ {
+ this.hostControl = value as Selector;
+ }
+ }
+
+ ///
+ /// Starts to monitor the to keep it in synch with the items of the .
+ ///
+ protected override void OnAttach()
+ {
+ bool itemsSourceIsSet = this.hostControl.ItemsSource != null;
+ itemsSourceIsSet = itemsSourceIsSet || (hostControl.HasBinding(this.hostControl, ItemsControl.ItemsSourceProperty) != null);
+ ////itemsSourceIsSet = itemsSourceIsSet || (BindingOperations.GetBinding(this.hostControl, ItemsControl.ItemsSourceProperty) != null);
+
+ if (itemsSourceIsSet)
+ {
+ throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException);
+ }
+
+ this.SynchronizeItems();
+
+ this.hostControl.SelectionChanged += this.HostControlSelectionChanged;
+ this.Region.ActiveViews.CollectionChanged += this.ActiveViews_CollectionChanged;
+ this.Region.Views.CollectionChanged += this.Views_CollectionChanged;
+ }
+
+ private void Views_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Add)
+ {
+ int startIndex = e.NewStartingIndex;
+ foreach (object newItem in e.NewItems)
+ {
+ this.hostControl.Items.Insert(startIndex++, newItem);
+ }
+ }
+ else if (e.Action == NotifyCollectionChangedAction.Remove)
+ {
+ foreach (object oldItem in e.OldItems)
+ {
+ this.hostControl.Items.Remove(oldItem);
+ }
+ }
+ }
+
+ private void SynchronizeItems()
+ {
+ List existingItems = new List();
+
+ // Control must be empty before "Binding" to a region
+ foreach (object childItem in this.hostControl.Items)
+ {
+ existingItems.Add(childItem);
+ }
+
+ foreach (object view in this.Region.Views)
+ {
+ this.hostControl.Items.Add(view);
+ }
+
+ foreach (object existingItem in existingItems)
+ {
+ this.Region.Add(existingItem);
+ }
+ }
+
+
+ private void ActiveViews_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (this.updatingActiveViewsInHostControlSelectionChanged)
+ {
+ // If we are updating the ActiveViews collection in the HostControlSelectionChanged, that
+ // means the user has set the SelectedItem or SelectedItems himself and we don't need to do that here now
+ return;
+ }
+
+ if (e.Action == NotifyCollectionChangedAction.Add)
+ {
+ if (this.hostControl.SelectedItem != null
+ && this.hostControl.SelectedItem != e.NewItems[0]
+ && this.Region.ActiveViews.Contains(this.hostControl.SelectedItem))
+ {
+ this.Region.Deactivate(this.hostControl.SelectedItem);
+ }
+
+ this.hostControl.SelectedItem = e.NewItems[0];
+ }
+ else if (e.Action == NotifyCollectionChangedAction.Remove &&
+ e.OldItems.Contains(this.hostControl.SelectedItem))
+ {
+ this.hostControl.SelectedItem = null;
+ }
+ }
+
+ private void HostControlSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ try
+ {
+ // Record the fact that we are now updating active views in the HostControlSelectionChanged method.
+ // This is needed to prevent the ActiveViews_CollectionChanged() method from firing.
+ this.updatingActiveViewsInHostControlSelectionChanged = true;
+
+ object source;
+ source = e.Source; // source = e.OriginalSource;
+
+ if (source == sender)
+ {
+ foreach (object item in e.RemovedItems)
+ {
+ // check if the view is in both Views and ActiveViews collections (there may be out of sync)
+ if (this.Region.Views.Contains(item) && this.Region.ActiveViews.Contains(item))
+ {
+ this.Region.Deactivate(item);
+ }
+ }
+
+ foreach (object item in e.AddedItems)
+ {
+ if (this.Region.Views.Contains(item) && !this.Region.ActiveViews.Contains(item))
+ {
+ this.Region.Activate(item);
+ }
+ }
+ }
+ }
+ finally
+ {
+ this.updatingActiveViewsInHostControlSelectionChanged = false;
+ }
+ }
+ }
+}
+*/
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs
new file mode 100644
index 0000000000..93385e8d8d
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Behaviors/SyncRegionContextWithHostBehavior.cs
@@ -0,0 +1,110 @@
+using System;
+using Avalonia;
+using Prism.Common;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions.Behaviors
+{
+ ///
+ /// Behavior that synchronizes the property of a with
+ /// the control that hosts the Region. It does this by setting the
+ /// Dependency Property on the host control.
+ ///
+ /// This behavior allows the usage of two way data binding of the RegionContext from XAML.
+ ///
+ public class SyncRegionContextWithHostBehavior : RegionBehavior, IHostAwareRegionBehavior
+ {
+ private const string RegionContextPropertyName = "Context";
+ private AvaloniaObject hostControl;
+
+ ///
+ /// Name that identifies the SyncRegionContextWithHostBehavior behavior in a collection of RegionsBehaviors.
+ ///
+ public static readonly string BehaviorKey = "SyncRegionContextWithHost";
+
+ private ObservableObject HostControlRegionContext
+ {
+ get
+ {
+ return RegionContext.GetObservableContext(hostControl);
+ }
+ }
+
+ ///
+ /// Gets or sets the that the is attached to.
+ ///
+ ///
+ /// A that the is attached to.
+ /// This is usually a that is part of the tree.
+ ///
+ public AvaloniaObject HostControl
+ {
+ get
+ {
+ return hostControl;
+ }
+ set
+ {
+ if (IsAttached)
+ {
+ throw new InvalidOperationException(Resources.HostControlCannotBeSetAfterAttach);
+ }
+
+ hostControl = value;
+ }
+ }
+
+ ///
+ /// Override this method to perform the logic after the behavior has been attached.
+ ///
+ protected override void OnAttach()
+ {
+ if (HostControl != null)
+ {
+ // Sync values initially.
+ SynchronizeRegionContext();
+
+ // Now register for events to keep them in sync
+ HostControlRegionContext.PropertyChanged += RegionContextObservableObject_PropertyChanged;
+ Region.PropertyChanged += Region_PropertyChanged;
+ }
+ }
+
+ private void Region_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == RegionContextPropertyName)
+ {
+ if (RegionManager.GetRegionContext(HostControl) != Region.Context)
+ {
+ // Setting this Dependency Property will automatically also change the HostControlRegionContext.Value
+ // (see RegionManager.OnRegionContextChanged())
+ RegionManager.SetRegionContext(hostControl, Region.Context);
+ }
+ }
+ }
+
+ private void RegionContextObservableObject_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == "Value")
+ {
+ SynchronizeRegionContext();
+ }
+ }
+
+ private void SynchronizeRegionContext()
+ {
+ // Forward this value to the Region
+ if (Region.Context != HostControlRegionContext.Value)
+ {
+ Region.Context = HostControlRegionContext.Value;
+ }
+
+ // Also make sure the region's StyledProperty was changed (this can occur if the value
+ // was changed only on the HostControlRegionContext)
+ if (RegionManager.GetRegionContext(HostControl) != HostControlRegionContext.Value)
+ {
+ RegionManager.SetRegionContext(HostControl, HostControlRegionContext.Value);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs
new file mode 100644
index 0000000000..f2a10cf66b
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ContentControlRegionAdapter.cs
@@ -0,0 +1,64 @@
+using Avalonia.Controls;
+using Prism.Properties;
+using System;
+using System.Collections.Specialized;
+using System.Linq;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Adapter that creates a new and monitors its
+ /// active view to set it on the adapted .
+ ///
+ public class ContentControlRegionAdapter : RegionAdapterBase
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The factory used to create the region behaviors to attach to the created regions.
+ public ContentControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
+ : base(regionBehaviorFactory)
+ {
+ }
+
+ ///
+ /// Adapts a to an .
+ ///
+ /// The new region being used.
+ /// The object to adapt.
+ protected override void Adapt(IRegion region, ContentControl regionTarget)
+ {
+ if (regionTarget == null)
+ throw new ArgumentNullException(nameof(regionTarget));
+
+ bool contentIsSet = regionTarget.Content != null;
+ contentIsSet = contentIsSet || regionTarget[ContentControl.ContentProperty] != null;
+
+ if (contentIsSet)
+ throw new InvalidOperationException(Resources.ContentControlHasContentException);
+
+ region.ActiveViews.CollectionChanged += delegate
+ {
+ regionTarget.Content = region.ActiveViews.FirstOrDefault();
+ };
+
+ region.Views.CollectionChanged +=
+ (sender, e) =>
+ {
+ if (e.Action == NotifyCollectionChangedAction.Add && region.ActiveViews.Count() == 0)
+ {
+ region.Activate(e.NewItems[0]);
+ }
+ };
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// A new instance of .
+ protected override IRegion CreateRegion()
+ {
+ return new SingleActiveRegion();
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs
new file mode 100644
index 0000000000..2f4d1c49cd
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/DefaultRegionManagerAccessor.cs
@@ -0,0 +1,46 @@
+using System;
+using Avalonia;
+
+namespace Prism.Navigation.Regions
+{
+ internal class DefaultRegionManagerAccessor : IRegionManagerAccessor
+ {
+ ///
+ /// Notification used by attached behaviors to update the region managers appropriatelly if needed to.
+ ///
+ /// This event uses weak references to the event handler to prevent this static event of keeping the
+ /// target element longer than expected.
+ public event EventHandler UpdatingRegions
+ {
+ add { RegionManager.UpdatingRegions += value; }
+ remove { RegionManager.UpdatingRegions -= value; }
+ }
+
+ ///
+ /// Gets the value for the RegionName attached property.
+ ///
+ /// The object to adapt. This is typically a container (i.e a control).
+ /// The name of the region that should be created when
+ /// the RegionManager is also set in this element.
+ public string GetRegionName(AvaloniaObject element)
+ {
+ if (element == null)
+ throw new ArgumentNullException(nameof(element));
+
+ return element.GetValue(RegionManager.RegionNameProperty) as string;
+ }
+
+ ///
+ /// Gets the value of the RegionName attached property.
+ ///
+ /// The target element.
+ /// The attached to the element.
+ public IRegionManager GetRegionManager(AvaloniaObject element)
+ {
+ if (element == null)
+ throw new ArgumentNullException(nameof(element));
+
+ return element.GetValue(RegionManager.RegionManagerProperty) as IRegionManager;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs
new file mode 100644
index 0000000000..94e2eb301e
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/INavigationAware.cs
@@ -0,0 +1,8 @@
+namespace Prism.Navigation.Regions
+{
+ /// Provides a way for objects involved in navigation to be notified of navigation activities.
+ /// Provides compatibility for Legacy Prism.Avalonia apps.
+ public interface INavigationAware : IRegionAware
+ {
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs
new file mode 100644
index 0000000000..850e697b16
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/IRegionManagerAccessor.cs
@@ -0,0 +1,33 @@
+using System;
+using Avalonia;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Provides an abstraction on top of the RegionManager static members.
+ ///
+ public interface IRegionManagerAccessor
+ {
+ ///
+ /// Notification used by attached behaviors to update the region managers appropriately if needed to.
+ ///
+ /// This event uses weak references to the event handler to prevent this static event of keeping the
+ /// target element longer than expected.
+ event EventHandler UpdatingRegions;
+
+ ///
+ /// Gets the value for the RegionName attached property.
+ ///
+ /// The object to adapt. This is typically a container (i.e a control).
+ /// The name of the region that should be created when
+ /// the RegionManager is also set in this element.
+ string GetRegionName(AvaloniaObject element);
+
+ ///
+ /// Gets the value of the RegionName attached property.
+ ///
+ /// The target element.
+ /// The attached to the element.
+ IRegionManager GetRegionManager(AvaloniaObject element);
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs
new file mode 100644
index 0000000000..85e32d6b5a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemMetadata.cs
@@ -0,0 +1,65 @@
+using System;
+using Avalonia;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Defines a class that wraps an item and adds metadata for it.
+ ///
+ public class ItemMetadata : AvaloniaObject
+ {
+ /// The name of the wrapped item.
+ public static readonly StyledProperty NameProperty = AvaloniaProperty.Register(nameof(Name));
+
+ /// Value indicating whether the wrapped item is considered active.
+ public static readonly StyledProperty IsActiveProperty = AvaloniaProperty.Register(nameof(IsActive));
+
+ /// Initializes a new instance of .
+ /// The item to wrap.
+ public ItemMetadata(object item)
+ {
+ // check for null
+ Item = item;
+ }
+
+ static ItemMetadata()
+ {
+ IsActiveProperty.Changed.Subscribe(args => StyledPropertyChanged(args?.Sender, args));
+ }
+
+ /// Gets the wrapped item.
+ /// The wrapped item.
+ public object Item { get; private set; }
+
+ /// Gets or sets a name for the wrapped item.
+ /// The name of the wrapped item.
+ public string Name
+ {
+ get { return GetValue(NameProperty); }
+ set { SetValue(NameProperty, value); }
+ }
+
+ /// Gets or sets a value indicating whether the wrapped item is considered active.
+ /// if the item should be considered active; otherwise .
+ public bool IsActive
+ {
+ get { return GetValue(IsActiveProperty); }
+ set { SetValue(IsActiveProperty, value); }
+ }
+
+ /// Occurs when metadata on the item changes.
+ public event EventHandler MetadataChanged;
+
+ /// Explicitly invokes to notify listeners.
+ public void InvokeMetadataChanged()
+ {
+ MetadataChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private static void StyledPropertyChanged(AvaloniaObject avaloniaObject, AvaloniaPropertyChangedEventArgs args)
+ {
+ var itemMetadata = avaloniaObject as ItemMetadata;
+ itemMetadata?.InvokeMetadataChanged();
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemsControlRegionAdapter.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemsControlRegionAdapter.cs
new file mode 100644
index 0000000000..f3859d7464
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ItemsControlRegionAdapter.cs
@@ -0,0 +1,71 @@
+using System;
+using Avalonia.Controls;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Adapter that creates a new and binds all
+ /// the views to the adapted .
+ ///
+ public class ItemsControlRegionAdapter : RegionAdapterBase
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The factory used to create the region behaviors to attach to the created regions.
+ public ItemsControlRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
+ : base(regionBehaviorFactory)
+ {
+ }
+
+ ///
+ /// Adapts an to an .
+ ///
+ /// The new region being used.
+ /// The object to adapt.
+ protected override void Adapt(IRegion region, ItemsControl regionTarget)
+ {
+ if (region == null)
+ throw new ArgumentNullException(nameof(region));
+
+ if (regionTarget == null)
+ throw new ArgumentNullException(nameof(regionTarget));
+
+ // NOTE: In Avalonia, Items will never be null
+ bool itemsSourceIsSet = regionTarget.ItemCount > 0;
+ itemsSourceIsSet = itemsSourceIsSet || regionTarget.HasBinding(ItemsControl.ItemsSourceProperty);
+
+ if (itemsSourceIsSet)
+ {
+ throw new InvalidOperationException(Resources.ItemsControlHasItemsSourceException);
+ }
+
+ // If control has child items, move them to the region and then bind control to region. Can't set ItemsSource if child items exist.
+ if (regionTarget.ItemCount > 0)
+ {
+ foreach (object childItem in regionTarget.Items)
+ {
+ region.Add(childItem);
+ }
+
+ // Control must be empty before setting ItemsSource
+ regionTarget.Items.Clear();
+ }
+
+ // Avalonia v11-Preview5 needs IRegion implement IList. Enforcing it to return AvaloniaList fixes this.
+ // Avalonia v11-Preview8 ItemsControl.Items is readonly (#10827).
+ ////regionTarget.Items = region.Views as Avalonia.Collections.AvaloniaList;
+ regionTarget.ItemsSource = region.Views as Avalonia.Collections.AvaloniaList;
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// A new instance of .
+ protected override IRegion CreateRegion()
+ {
+ return new AllActiveRegion();
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs
new file mode 100644
index 0000000000..f445c3fa74
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/Region.cs
@@ -0,0 +1,448 @@
+using System;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Avalonia;
+using Prism.Ioc;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ /// Implementation of that allows multiple active views.
+ public class Region : IRegion
+ {
+ private ObservableCollection _itemMetadataCollection;
+ private string _name;
+ private ViewsCollection _views;
+ private ViewsCollection _activeViews;
+ private object _context;
+ private IRegionManager _regionManager;
+ private IRegionNavigationService _regionNavigationService;
+
+ private Comparison _sort;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public Region()
+ {
+ Behaviors = new RegionBehaviorCollection(this);
+
+ _sort = DefaultSortComparison;
+ }
+
+ /// Occurs when a property value changes.
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ /// Gets the collection of s that can extend the behavior of regions.
+ public IRegionBehaviorCollection Behaviors { get; }
+
+ /// Gets or sets a context for the region. This value can be used by the user to share context with the views.
+ /// The context value to be shared.
+ public object Context
+ {
+ get => _context;
+
+ set
+ {
+ if (_context != value)
+ {
+ _context = value;
+ OnPropertyChanged(nameof(Context));
+ }
+ }
+ }
+
+ ///
+ /// Gets the name of the region that uniequely identifies the region within a .
+ ///
+ /// The name of the region.
+ public string Name
+ {
+ get => _name;
+
+ set
+ {
+ if (_name != null && _name != value)
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.CannotChangeRegionNameException, _name));
+ }
+
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException(Resources.RegionNameCannotBeEmptyException);
+ }
+
+ _name = value;
+ OnPropertyChanged(nameof(Name));
+ }
+ }
+
+ ///
+ /// Gets a readonly view of the collection of views in the region.
+ ///
+ /// An of all the added views.
+ public virtual IViewsCollection Views
+ {
+ get
+ {
+ if (_views == null)
+ {
+ _views = new ViewsCollection(ItemMetadataCollection, x => true)
+ {
+ SortComparison = _sort
+ };
+ }
+
+ return _views;
+ }
+ }
+
+ ///
+ /// Gets a readonly view of the collection of all the active views in the region.
+ ///
+ /// An of all the active views.
+ public virtual IViewsCollection ActiveViews
+ {
+ get
+ {
+ if (_views == null)
+ {
+ _views = new ViewsCollection(ItemMetadataCollection, x => true)
+ {
+ SortComparison = _sort
+ };
+ }
+
+ if (_activeViews == null)
+ {
+ _activeViews = new ViewsCollection(ItemMetadataCollection, x => x.IsActive)
+ {
+ SortComparison = _sort
+ };
+ }
+
+ return _activeViews;
+ }
+ }
+
+ ///
+ /// Gets or sets the comparison used to sort the views.
+ ///
+ /// The comparison to use.
+ public Comparison SortComparison
+ {
+ get => _sort;
+ set
+ {
+ _sort = value;
+
+ if (_activeViews != null)
+ {
+ _activeViews.SortComparison = _sort;
+ }
+
+ if (_views != null)
+ {
+ _views.SortComparison = _sort;
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets the that will be passed to the views when adding them to the region, unless the view is added by specifying createRegionManagerScope as .
+ ///
+ /// The where this is registered.
+ /// This is usually used by implementations of and should not be
+ /// used by the developer explicitly.
+ public IRegionManager RegionManager
+ {
+ get => _regionManager;
+
+ set
+ {
+ if (_regionManager != value)
+ {
+ _regionManager = value;
+ OnPropertyChanged(nameof(RegionManager));
+ }
+ }
+ }
+
+ ///
+ /// Gets the navigation service.
+ ///
+ /// The navigation service.
+ public IRegionNavigationService NavigationService
+ {
+ get
+ {
+ if (_regionNavigationService == null)
+ {
+ _regionNavigationService = ContainerLocator.Container.Resolve();
+ _regionNavigationService.Region = this;
+ }
+
+ return _regionNavigationService;
+ }
+
+ set => _regionNavigationService = value;
+ }
+
+ ///
+ /// Gets the collection with all the views along with their metadata.
+ ///
+ /// An of with all the added views.
+ protected virtual ObservableCollection ItemMetadataCollection
+ {
+ get
+ {
+ _itemMetadataCollection ??= new ObservableCollection();
+ return _itemMetadataCollection;
+ }
+ }
+
+ /// Adds a new view to the region.
+ /// Adds a new view to the region.
+ /// The view to add.
+ /// The that is set on the view if it is a . It will be the current region manager when using this overload.
+ public IRegionManager Add(string viewName)
+ {
+ var view = ContainerLocator.Container.Resolve(viewName);
+ return Add(view, viewName, false);
+ }
+
+ /// Adds a new view to the region.
+ ///
+ /// Adds a new view to the region.
+ ///
+ /// The view to add.
+ /// The that is set on the view if it is a . It will be the current region manager when using this overload.
+ public IRegionManager Add(object view)
+ {
+ return Add(view, null, false);
+ }
+
+ /// Adds a new view to the region.
+ /// The view to add.
+ /// The name of the view. This can be used to retrieve it later by calling .
+ /// The that is set on the view if it is a . It will be the current region manager when using this overload.
+ public IRegionManager Add(object view, string viewName)
+ {
+ if (string.IsNullOrEmpty(viewName))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeNullOrEmpty, "viewName"));
+ }
+
+ return Add(view, viewName, false);
+ }
+
+ /// Adds a new view to the region.
+ /// The view to add.
+ /// The name of the view. This can be used to retrieve it later by calling .
+ /// When , the added view will receive a new instance of , otherwise it will use the current region manager for this region.
+ /// The that is set on the view if it is a .
+ public virtual IRegionManager Add(object view, string viewName, bool createRegionManagerScope)
+ {
+ IRegionManager manager = createRegionManagerScope ? RegionManager.CreateRegionManager() : RegionManager;
+ InnerAdd(view, viewName, manager);
+ return manager;
+ }
+
+ /// Removes the specified view from the region.
+ /// The view to remove.
+ public virtual void Remove(object view)
+ {
+ ItemMetadata itemMetadata = GetItemMetadataOrThrow(view);
+
+ ItemMetadataCollection.Remove(itemMetadata);
+
+ if (view is AvaloniaObject avaloniaObject && Regions.RegionManager.GetRegionManager(avaloniaObject) == RegionManager)
+ {
+ avaloniaObject.ClearValue(Regions.RegionManager.RegionManagerProperty);
+ }
+ }
+
+ /// Removes all views from the region.
+ public void RemoveAll()
+ {
+ foreach (var view in Views)
+ {
+ Remove(view);
+ }
+ }
+
+ /// Marks the specified view as active.
+ /// The view to activate.
+ public virtual void Activate(object view)
+ {
+ ItemMetadata itemMetadata = GetItemMetadataOrThrow(view);
+
+ if (!itemMetadata.IsActive)
+ {
+ itemMetadata.IsActive = true;
+ }
+ }
+
+ /// Marks the specified view as inactive.
+ /// The view to deactivate.
+ public virtual void Deactivate(object view)
+ {
+ ItemMetadata itemMetadata = GetItemMetadataOrThrow(view);
+
+ if (itemMetadata.IsActive)
+ {
+ itemMetadata.IsActive = false;
+ }
+ }
+
+ /// Returns the view instance that was added to the region using a specific name.
+ /// The name used when adding the view to the region.
+ /// Returns the named view or if the view with does not exist in the current region.
+ public virtual object GetView(string viewName)
+ {
+ if (string.IsNullOrEmpty(viewName))
+ {
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeNullOrEmpty, "viewName"));
+ }
+
+ ItemMetadata metadata = ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName);
+
+ if (metadata != null)
+ {
+ return metadata.Item;
+ }
+
+ return null;
+ }
+
+ /// Initiates navigation to the specified target.
+ /// The target.
+ /// A callback to execute when the navigation request is completed.
+ public void RequestNavigate(Uri target, Action navigationCallback)
+ {
+ RequestNavigate(target, navigationCallback, null);
+ }
+
+ /// Initiates navigation to the specified target.
+ /// The target.
+ /// A callback to execute when the navigation request is completed.
+ /// The navigation parameters specific to the navigation request.
+ public void RequestNavigate(Uri target, Action navigationCallback, INavigationParameters navigationParameters)
+ {
+ NavigationService.RequestNavigate(target, navigationCallback, navigationParameters);
+ }
+
+ private void InnerAdd(object view, string viewName, IRegionManager scopedRegionManager)
+ {
+ if (ItemMetadataCollection.FirstOrDefault(x => x.Item == view) != null)
+ {
+ throw new InvalidOperationException(Resources.RegionViewExistsException);
+ }
+
+ var itemMetadata = new ItemMetadata(view);
+ if (!string.IsNullOrEmpty(viewName))
+ {
+ if (ItemMetadataCollection.FirstOrDefault(x => x.Name == viewName) != null)
+ {
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.RegionViewNameExistsException, viewName));
+ }
+
+ itemMetadata.Name = viewName;
+ }
+
+ if (view is AvaloniaObject avaloniaObject)
+ {
+ Regions.RegionManager.SetRegionManager(avaloniaObject, scopedRegionManager);
+ }
+
+ ItemMetadataCollection.Add(itemMetadata);
+ }
+
+ private ItemMetadata GetItemMetadataOrThrow(object view)
+ {
+ if (view == null)
+ throw new ArgumentNullException(nameof(view));
+
+ ItemMetadata itemMetadata = ItemMetadataCollection.FirstOrDefault(x => x.Item == view);
+
+ if (itemMetadata == null)
+ throw new ArgumentException(Resources.ViewNotInRegionException, nameof(view));
+
+ return itemMetadata;
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
+ }
+
+ ///
+ /// The default sort algorithm.
+ ///
+ /// The first view to compare.
+ /// The second view to compare.
+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "y")]
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "x")]
+ public static int DefaultSortComparison(object x, object y)
+ {
+ if (x == null)
+ {
+ if (y == null)
+ {
+ return 0;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+ else
+ {
+ if (y == null)
+ {
+ return 1;
+ }
+ else
+ {
+ Type xType = x.GetType();
+ Type yType = y.GetType();
+
+ ViewSortHintAttribute xAttribute = xType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute;
+ ViewSortHintAttribute yAttribute = yType.GetCustomAttributes(typeof(ViewSortHintAttribute), true).FirstOrDefault() as ViewSortHintAttribute;
+
+ return ViewSortHintAttributeComparison(xAttribute, yAttribute);
+ }
+ }
+ }
+
+ private static int ViewSortHintAttributeComparison(ViewSortHintAttribute x, ViewSortHintAttribute y)
+ {
+ if (x == null)
+ {
+ if (y == null)
+ {
+ return 0;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+ else
+ {
+ if (y == null)
+ {
+ return 1;
+ }
+ else
+ {
+ return string.Compare(x.Hint, y.Hint, StringComparison.Ordinal);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs
new file mode 100644
index 0000000000..d84d3356d9
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterBase.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Globalization;
+using Avalonia;
+using Prism.Navigation.Regions.Behaviors;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Base class to facilitate the creation of implementations.
+ ///
+ /// Type of object to adapt.
+ public abstract class RegionAdapterBase : IRegionAdapter where T : class
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The factory used to create the region behaviors to attach to the created regions.
+ protected RegionAdapterBase(IRegionBehaviorFactory regionBehaviorFactory)
+ {
+ RegionBehaviorFactory = regionBehaviorFactory;
+ }
+
+ ///
+ /// Gets or sets the factory used to create the region behaviors to attach to the created regions.
+ ///
+ protected IRegionBehaviorFactory RegionBehaviorFactory { get; set; }
+
+ ///
+ /// Adapts an object and binds it to a new .
+ ///
+ /// The object to adapt.
+ /// The name of the region to be created.
+ /// The new instance of that the is bound to.
+ public IRegion Initialize(T regionTarget, string regionName)
+ {
+ if (regionName == null)
+ throw new ArgumentNullException(nameof(regionName));
+
+ IRegion region = CreateRegion();
+ region.Name = regionName;
+
+ SetObservableRegionOnHostingControl(region, regionTarget);
+
+ Adapt(region, regionTarget);
+ AttachBehaviors(region, regionTarget);
+ AttachDefaultBehaviors(region, regionTarget);
+ return region;
+ }
+
+ ///
+ /// Adapts an object and binds it to a new .
+ ///
+ /// The object to adapt.
+ /// The name of the region to be created.
+ /// The new instance of that the is bound to.
+ /// This methods performs validation to check that
+ /// is of type .
+ /// When is .
+ /// When is not of type .
+ IRegion IRegionAdapter.Initialize(object regionTarget, string regionName)
+ {
+ return Initialize(GetCastedObject(regionTarget), regionName);
+ }
+
+ ///
+ /// This method adds the default behaviors by using the object.
+ ///
+ /// The region being used.
+ /// The object to adapt.
+ protected virtual void AttachDefaultBehaviors(IRegion region, T regionTarget)
+ {
+ if (region == null)
+ throw new ArgumentNullException(nameof(region));
+
+ if (regionTarget == null)
+ throw new ArgumentNullException(nameof(regionTarget));
+
+ IRegionBehaviorFactory behaviorFactory = RegionBehaviorFactory;
+ if (behaviorFactory != null)
+ {
+ AvaloniaObject avaloniaObjectRegionTarget = regionTarget as AvaloniaObject;
+
+ foreach (string behaviorKey in behaviorFactory)
+ {
+ if (!region.Behaviors.ContainsKey(behaviorKey))
+ {
+ IRegionBehavior behavior = behaviorFactory.CreateFromKey(behaviorKey);
+
+ if (avaloniaObjectRegionTarget != null)
+ {
+ IHostAwareRegionBehavior hostAwareRegionBehavior = behavior as IHostAwareRegionBehavior;
+ if (hostAwareRegionBehavior != null)
+ {
+ hostAwareRegionBehavior.HostControl = avaloniaObjectRegionTarget;
+ }
+ }
+
+ region.Behaviors.Add(behaviorKey, behavior);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Template method to attach new behaviors.
+ ///
+ /// The region being used.
+ /// The object to adapt.
+ protected virtual void AttachBehaviors(IRegion region, T regionTarget)
+ {
+ }
+
+ ///
+ /// Template method to adapt the object to an .
+ ///
+ /// The new region being used.
+ /// The object to adapt.
+ protected abstract void Adapt(IRegion region, T regionTarget);
+
+ ///
+ /// Template method to create a new instance of
+ /// that will be used to adapt the object.
+ ///
+ /// A new instance of .
+ protected abstract IRegion CreateRegion();
+
+ private static T GetCastedObject(object regionTarget)
+ {
+ if (regionTarget == null)
+ throw new ArgumentNullException(nameof(regionTarget));
+
+ T castedObject = regionTarget as T;
+
+ if (castedObject == null)
+ throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Resources.AdapterInvalidTypeException, typeof(T).Name));
+
+ return castedObject;
+ }
+
+ private static void SetObservableRegionOnHostingControl(IRegion region, T regionTarget)
+ {
+ AvaloniaObject targetElement = regionTarget as AvaloniaObject;
+
+ if (targetElement != null)
+ {
+ // Set the region as a dependency property on the control hosting the region
+ // Because we are using an observable region, the hosting control can detect that the
+ // region has actually been created. This is an ideal moment to hook up custom behaviors
+ RegionManager.GetObservableRegion(targetElement).Value = region;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs
new file mode 100644
index 0000000000..fc818110f7
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionAdapterMappings.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Prism.Ioc;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// This class maps with .
+ ///
+ public class RegionAdapterMappings
+ {
+ private readonly Dictionary mappings = new Dictionary();
+
+ ///
+ /// Registers the mapping between a type and an adapter.
+ ///
+ /// The type of the control.
+ /// The adapter to use with the type.
+ /// When any of or are .
+ /// If a mapping for already exists.
+ public void RegisterMapping(Type controlType, IRegionAdapter adapter)
+ {
+ if (controlType == null)
+ throw new ArgumentNullException(nameof(controlType));
+
+ if (adapter == null)
+ throw new ArgumentNullException(nameof(adapter));
+
+ if (mappings.ContainsKey(controlType))
+ throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
+ Resources.MappingExistsException, controlType.Name));
+
+ mappings.Add(controlType, adapter);
+ }
+
+ ///
+ /// Registers the mapping between a type and an adapter.
+ ///
+ /// The type of the control
+ public void RegisterMapping(IRegionAdapter adapter)
+ {
+ RegisterMapping(typeof(TControl), adapter);
+ }
+
+ ///
+ /// Registers the mapping between a type and an adapter.
+ ///
+ /// The type of the control
+ /// The type of the IRegionAdapter to use with the TControl
+ public void RegisterMapping() where TAdapter : IRegionAdapter
+ {
+ RegisterMapping(typeof(TControl), ContainerLocator.Container.Resolve());
+ }
+
+ ///
+ /// Returns the adapter associated with the type provided.
+ ///
+ /// The type to obtain the mapped.
+ /// The mapped to the .
+ /// This class will look for a registered type for and if there is not any,
+ /// it will look for a registered type for any of its ancestors in the class hierarchy.
+ /// If there is no registered type for or any of its ancestors,
+ /// an exception will be thrown.
+ /// When there is no registered type for or any of its ancestors.
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "controlType")]
+ public IRegionAdapter GetMapping(Type controlType)
+ {
+ Type currentType = controlType;
+
+ while (currentType != null)
+ {
+ if (mappings.ContainsKey(currentType))
+ {
+ return mappings[currentType];
+ }
+
+ currentType = currentType.BaseType;
+ }
+
+ throw new KeyNotFoundException(string.Format(CultureInfo.CurrentCulture, Resources.NoRegionAdapterException, controlType));
+ }
+
+ ///
+ /// Returns the adapter associated with the type provided.
+ ///
+ /// The control type used to obtain the mapped.
+ /// The mapped to the .
+ /// This class will look for a registered type for and if there is not any,
+ /// it will look for a registered type for any of its ancestors in the class hierarchy.
+ /// If there is no registered type for or any of its ancestors,
+ /// an exception will be thrown.
+ /// When there is no registered type for or any of its ancestors.
+ public IRegionAdapter GetMapping()
+ {
+ return GetMapping(typeof(T));
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs
new file mode 100644
index 0000000000..29a129a36c
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionContext.cs
@@ -0,0 +1,50 @@
+using System;
+using Avalonia;
+using Prism.Common;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Class that holds methods to Set and Get the RegionContext from a .
+ ///
+ /// RegionContext allows sharing of contextual information between the view that's hosting a
+ /// and any views that are inside the Region.
+ ///
+ public static class RegionContext
+ {
+ private static readonly AvaloniaProperty ObservableRegionContextProperty =
+ AvaloniaProperty.RegisterAttached>("ObservableRegionContext", typeof(RegionContext));
+
+ static RegionContext()
+ {
+ ObservableRegionContextProperty.Changed.Subscribe(args => GetObservableContext(args?.Sender as Visual));
+ }
+
+ ///
+ /// Returns an wrapper around the RegionContext value. The RegionContext
+ /// will be set on any views (dependency objects) that are inside the collection by
+ /// the Behavior.
+ /// The RegionContext will also be set to the control that hosts the Region, by the Behavior.
+ ///
+ /// If the wrapper does not already exist, an empty one will be created. This way, an observer can
+ /// notify when the value is set for the first time.
+ ///
+ /// Any view that hold the RegionContext value.
+ /// Wrapper around the value.
+ public static ObservableObject GetObservableContext(AvaloniaObject view)
+ {
+ if (view == null)
+ throw new ArgumentNullException(nameof(view));
+
+ ObservableObject context = view.GetValue(ObservableRegionContextProperty) as ObservableObject;
+
+ if (context == null)
+ {
+ context = new ObservableObject();
+ view.SetValue(ObservableRegionContextProperty, context);
+ }
+
+ return context;
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs
new file mode 100644
index 0000000000..b12b2ed62a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionManager.cs
@@ -0,0 +1,550 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using Prism.Common;
+using Prism.Events;
+using Prism.Ioc;
+using Prism.Properties;
+using Prism.Ioc.Internals;
+using Avalonia;
+using Avalonia.Controls;
+using Prism.Navigation.Regions.Behaviors;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// This class is responsible for maintaining a collection of regions and attaching regions to controls.
+ ///
+ ///
+ /// This class supplies the attached properties that can be used for simple region creation from XAML.
+ ///
+ public class RegionManager : IRegionManager
+ {
+ #region Static members (for XAML support)
+
+ private static readonly WeakDelegatesManager updatingRegionsListeners = new WeakDelegatesManager();
+
+ ///
+ /// Identifies the RegionName attached property.
+ ///
+ ///
+ /// When a control has both the and
+ /// attached properties set to
+ /// a value different than and there is a
+ /// mapping registered for the control, it
+ /// will create and adapt a new region for that control, and register it
+ /// in the with the specified region name.
+ ///
+ public static readonly AvaloniaProperty RegionNameProperty = AvaloniaProperty.RegisterAttached(
+ "RegionName",
+ typeof(RegionManager));
+
+ ///
+ /// Sets the attached property.
+ ///
+ /// The object to adapt. This is typically a container (i.e a control).
+ /// The name of the region to register.
+ public static void SetRegionName(AvaloniaObject regionTarget, string regionName)
+ {
+ if (regionTarget == null)
+ throw new ArgumentNullException(nameof(regionTarget));
+
+ regionTarget.SetValue(RegionNameProperty, regionName);
+ }
+
+ ///
+ /// Gets the value for the attached property.
+ ///
+ /// The object to adapt. This is typically a container (i.e a control).
+ /// The name of the region that should be created when
+ /// is also set in this element.
+ public static string GetRegionName(AvaloniaObject regionTarget)
+ {
+ if (regionTarget == null)
+ throw new ArgumentNullException(nameof(regionTarget));
+
+ return regionTarget.GetValue(RegionNameProperty) as string;
+ }
+
+ private static readonly AvaloniaProperty ObservableRegionProperty =
+ AvaloniaProperty.RegisterAttached>("ObservableRegion", typeof(RegionManager));
+
+ ///
+ /// Returns an wrapper that can hold an . Using this wrapper
+ /// you can detect when an has been created by the .
+ ///
+ /// If the wrapper does not yet exist, a new wrapper will be created. When the region
+ /// gets created and assigned to the wrapper, you can use the event
+ /// to get notified of that change.
+ ///
+ /// The view that will host the region.
+ /// Wrapper that can hold an value and can notify when the value changes.
+ public static ObservableObject GetObservableRegion(AvaloniaObject view)
+ {
+ if (view == null) throw new ArgumentNullException(nameof(view));
+
+ ObservableObject regionWrapper = view.GetValue(ObservableRegionProperty) as ObservableObject;
+
+ if (regionWrapper == null)
+ {
+ regionWrapper = new ObservableObject();
+ view.SetValue(ObservableRegionProperty, regionWrapper);
+ }
+
+ return regionWrapper;
+ }
+
+ private static void OnSetRegionNameCallback(AvaloniaObject element, AvaloniaPropertyChangedEventArgs args)
+ {
+ if (!IsInDesignMode(element))
+ {
+ CreateRegion(element);
+ }
+ }
+
+ private static void CreateRegion(AvaloniaObject element)
+ {
+ var container = ContainerLocator.Container;
+ DelayedRegionCreationBehavior regionCreationBehavior = container.Resolve();
+ regionCreationBehavior.TargetElement = element;
+ regionCreationBehavior.Attach();
+ }
+
+ ///
+ /// Identifies the RegionManager attached property.
+ ///
+ ///
+ /// When a control has both the and
+ /// attached properties set to
+ /// a value different than and there is a
+ /// mapping registered for the control, it
+ /// will create and adapt a new region for that control, and register it
+ /// in the with the specified region name.
+ ///
+ public static readonly AvaloniaProperty RegionManagerProperty =
+ AvaloniaProperty.RegisterAttached("RegionManager", typeof(RegionManager));
+
+ ///
+ /// Gets the value of the attached property.
+ ///
+ /// The target element.
+ /// The attached to the element.
+ public static IRegionManager GetRegionManager(AvaloniaObject target)
+ {
+ if (target == null)
+ throw new ArgumentNullException(nameof(target));
+
+ return (IRegionManager)target.GetValue(RegionManagerProperty);
+ }
+
+ ///
+ /// Sets the attached property.
+ ///
+ /// The target element.
+ /// The value.
+ public static void SetRegionManager(AvaloniaObject target, IRegionManager value)
+ {
+ if (target == null)
+ throw new ArgumentNullException(nameof(target));
+
+ target.SetValue(RegionManagerProperty, value);
+ }
+
+ ///
+ /// Identifies the RegionContext attached property.
+ ///
+ public static readonly AvaloniaProperty RegionContextProperty =
+ AvaloniaProperty.RegisterAttached("RegionContext", typeof(RegionManager));
+
+ private static void OnRegionContextChanged(AvaloniaObject depObj, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (RegionContext.GetObservableContext(depObj as AvaloniaObject).Value != e.NewValue)
+ {
+ RegionContext.GetObservableContext(depObj as AvaloniaObject).Value = e.NewValue;
+ }
+ }
+
+ ///
+ /// Gets the value of the attached property.
+ ///
+ /// The target element.
+ /// The region context to pass to the contained views.
+ public static object GetRegionContext(AvaloniaObject target)
+ {
+ if (target == null)
+ throw new ArgumentNullException(nameof(target));
+
+ return target.GetValue(RegionContextProperty);
+ }
+
+ ///
+ /// Sets the attached property.
+ ///
+ /// The target element.
+ /// The value.
+ public static void SetRegionContext(AvaloniaObject target, object value)
+ {
+ if (target == null)
+ throw new ArgumentNullException(nameof(target));
+
+ target.SetValue(RegionContextProperty, value);
+ }
+
+ ///
+ /// Notification used by attached behaviors to update the region managers appropriately if needed to.
+ ///
+ /// This event uses weak references to the event handler to prevent this static event of keeping the
+ /// target element longer than expected.
+ public static event EventHandler UpdatingRegions
+ {
+ add { updatingRegionsListeners.AddListener(value); }
+ remove { updatingRegionsListeners.RemoveListener(value); }
+ }
+
+ ///
+ /// Notifies attached behaviors to update the region managers appropriately if needed to.
+ ///
+ ///
+ /// This method is normally called internally, and there is usually no need to call this from user code.
+ ///
+ public static void UpdateRegions()
+ {
+
+ try
+ {
+ updatingRegionsListeners.Raise(null, EventArgs.Empty);
+ }
+ catch (TargetInvocationException ex)
+ {
+ Exception rootException = ex.GetRootException();
+
+ throw new UpdateRegionsException(string.Format(CultureInfo.CurrentCulture,
+ Resources.UpdateRegionException, rootException), ex.InnerException);
+ }
+ }
+
+ private static bool IsInDesignMode(AvaloniaObject element)
+ {
+ return Design.IsDesignMode;
+ }
+
+ #endregion
+
+ private readonly RegionCollection regionCollection;
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ public RegionManager()
+ {
+ regionCollection = new RegionCollection(this);
+ }
+
+ static RegionManager()
+ {
+ // TODO: Could this go into the default constructor?
+ RegionNameProperty.Changed.Subscribe(args => OnSetRegionNameCallback(args?.Sender, args));
+ RegionContextProperty.Changed.Subscribe(args => OnRegionContextChanged(args?.Sender, args));
+ }
+
+ ///
+ /// Gets a collection of that identify each region by name. You can use this collection to add or remove regions to the current region manager.
+ ///
+ /// A with all the registered regions.
+ public IRegionCollection Regions
+ {
+ get { return regionCollection; }
+ }
+
+ ///
+ /// Creates a new region manager.
+ ///
+ /// A new region manager that can be used as a different scope from the current region manager.
+ public IRegionManager CreateRegionManager()
+ {
+ return new RegionManager();
+ }
+
+ ///
+ /// Add a view to the Views collection of a Region. Note that the region must already exist in this .
+ ///
+ /// The name of the region to add a view to
+ /// The view to add to the views collection
+ /// The RegionManager, to easily add several views.
+ public IRegionManager AddToRegion(string regionName, object view)
+ {
+ if (!Regions.ContainsRegionWithName(regionName))
+ throw new ArgumentException(string.Format(Thread.CurrentThread.CurrentCulture, Resources.RegionNotFound, regionName), nameof(regionName));
+
+ return Regions[regionName].Add(view);
+ }
+
+ ///
+ /// Add a view to the Views collection of a Region. Note that the region must already exist in this .
+ ///
+ /// The name of the region to add a view to
+ /// The view to add to the views collection
+ /// The RegionManager, to easily add several views.
+ public IRegionManager AddToRegion(string regionName, string targetName)
+ {
+ if (!Regions.ContainsRegionWithName(regionName))
+ throw new ArgumentException(string.Format(Thread.CurrentThread.CurrentCulture, Resources.RegionNotFound, regionName), nameof(regionName));
+
+ var view = CreateNewRegionItem(targetName);
+
+ return Regions[regionName].Add(view);
+ }
+
+ ///
+ /// Associate a view with a region, by registering a type. When the region get's displayed
+ /// this type will be resolved using the ServiceLocator into a concrete instance. The instance
+ /// will be added to the Views collection of the region
+ ///
+ /// The name of the region to associate the view with.
+ /// The type of the view to register with the
+ /// The , for adding several views easily
+ public IRegionManager RegisterViewWithRegion(string regionName, Type viewType)
+ {
+ var regionViewRegistry = ContainerLocator.Container.Resolve();
+
+ regionViewRegistry.RegisterViewWithRegion(regionName, viewType);
+
+ return this;
+ }
+
+ ///
+ /// Associate a view with a region, by registering a type. When the region get's displayed
+ /// this type will be resolved using the ServiceLocator into a concrete instance. The instance
+ /// will be added to the Views collection of the region
+ ///
+ /// The name of the region to associate the view with.
+ /// The type of the view to register with the
+ /// The , for adding several views easily
+ public IRegionManager RegisterViewWithRegion(string regionName, string targetName)
+ {
+ var viewType = ContainerLocator.Current.GetRegistrationType(targetName);
+
+ return RegisterViewWithRegion(regionName, viewType);
+ }
+
+ ///
+ /// Associate a view with a region, using a delegate to resolve a concrete instance of the view.
+ /// When the region get's displayed, this delegate will be called and the result will be added to the
+ /// views collection of the region.
+ ///
+ /// The name of the region to associate the view with.
+ /// The delegate used to resolve a concrete instance of the view.
+ /// The , for adding several views easily
+ public IRegionManager RegisterViewWithRegion(string regionName, Func getContentDelegate)
+ {
+ var regionViewRegistry = ContainerLocator.Container.Resolve();
+
+ regionViewRegistry.RegisterViewWithRegion(regionName, getContentDelegate);
+
+ return this;
+ }
+
+ ///
+ /// Navigates the specified region manager.
+ ///
+ /// The name of the region to call Navigate on.
+ /// The URI of the content to display.
+ /// The navigation callback.
+ public void RequestNavigate(string regionName, Uri source, Action navigationCallback)
+ {
+ if (navigationCallback == null)
+ throw new ArgumentNullException(nameof(navigationCallback));
+
+ if (Regions.ContainsRegionWithName(regionName))
+ {
+ Regions[regionName].RequestNavigate(source, navigationCallback);
+ }
+ else
+ {
+ navigationCallback(new NavigationResult(new NavigationContext(null, source), false));
+ }
+ }
+
+ ///
+ /// This method allows an IRegionManager to locate a specified region and navigate in it to the specified target Uri, passing a navigation callback and an instance of , which holds a collection of object parameters.
+ ///
+ /// The name of the region where the navigation will occur.
+ /// A Uri that represents the target where the region will navigate.
+ /// The navigation callback that will be executed after the navigation is completed.
+ /// An instance of , which holds a collection of object parameters.
+ public void RequestNavigate(string regionName, Uri target, Action navigationCallback, INavigationParameters navigationParameters)
+ {
+ if (navigationCallback == null)
+ throw new ArgumentNullException(nameof(navigationCallback));
+
+ if (Regions.ContainsRegionWithName(regionName))
+ {
+ Regions[regionName].RequestNavigate(target, navigationCallback, navigationParameters);
+ }
+ else
+ {
+ navigationCallback(new NavigationResult(new NavigationContext(null, target, navigationParameters), false));
+ }
+ }
+
+ ///
+ /// Provides a new item for the region based on the supplied candidate target contract name.
+ ///
+ /// The target contract to build.
+ /// An instance of an item to put into the .
+ protected virtual object CreateNewRegionItem(string candidateTargetContract)
+ {
+ try
+ {
+ var view = ContainerLocator.Container.Resolve(candidateTargetContract);
+
+ MvvmHelpers.AutowireViewModel(view);
+
+ return view;
+ }
+ catch (ContainerResolutionException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.CurrentCulture, Resources.CannotCreateNavigationTarget, candidateTargetContract),
+ e);
+ }
+ }
+
+ private class RegionCollection : IRegionCollection
+ {
+ private readonly IRegionManager regionManager;
+ private readonly List regions;
+
+ public RegionCollection(IRegionManager regionManager)
+ {
+ this.regionManager = regionManager;
+ regions = new List();
+ }
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ public IEnumerator GetEnumerator()
+ {
+ UpdateRegions();
+
+ return regions.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public IRegion this[string regionName]
+ {
+ get
+ {
+ UpdateRegions();
+
+ IRegion region = GetRegionByName(regionName);
+ if (region == null)
+ {
+ throw new KeyNotFoundException(string.Format(CultureInfo.CurrentUICulture, Resources.RegionNotInRegionManagerException, regionName));
+ }
+
+ return region;
+ }
+ }
+
+ public void Add(IRegion region)
+ {
+ if (region == null)
+ throw new ArgumentNullException(nameof(region));
+
+ UpdateRegions();
+
+ if (region.Name == null)
+ {
+ throw new InvalidOperationException(Resources.RegionNameCannotBeEmptyException);
+ }
+
+ if (GetRegionByName(region.Name) != null)
+ {
+ throw new ArgumentException(string.Format(CultureInfo.InvariantCulture,
+ Resources.RegionNameExistsException, region.Name));
+ }
+
+ regions.Add(region);
+ region.RegionManager = regionManager;
+
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, region, 0));
+ }
+
+ public bool Remove(string regionName)
+ {
+ UpdateRegions();
+
+ bool removed = false;
+
+ IRegion region = GetRegionByName(regionName);
+ if (region != null)
+ {
+ removed = true;
+ regions.Remove(region);
+ region.RegionManager = null;
+
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, region, 0));
+ }
+
+ return removed;
+ }
+
+ public bool ContainsRegionWithName(string regionName)
+ {
+ UpdateRegions();
+
+ return GetRegionByName(regionName) != null;
+ }
+
+ ///
+ /// Adds a region to the with the name received as argument.
+ ///
+ /// The name to be given to the region.
+ /// The region to be added to the .
+ /// Thrown if is .
+ /// Thrown if and 's name do not match and the is not .
+ public void Add(string regionName, IRegion region)
+ {
+ if (region == null)
+ throw new ArgumentNullException(nameof(region));
+
+ if (region.Name != null && region.Name != regionName)
+ throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.RegionManagerWithDifferentNameException, region.Name, regionName), nameof(regionName));
+
+ if (region.Name == null)
+ region.Name = regionName;
+
+ Add(region);
+ }
+
+ private IRegion GetRegionByName(string regionName)
+ {
+ return regions.FirstOrDefault(r => r.Name == regionName);
+ }
+
+ private void OnCollectionChanged(NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
+ {
+ var handler = CollectionChanged;
+
+ if (handler != null)
+ {
+ handler(this, notifyCollectionChangedEventArgs);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs
new file mode 100644
index 0000000000..2d7f6dcb40
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationContentLoader.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Controls;
+using Prism.Common;
+using Prism.Ioc;
+using Prism.Ioc.Internals;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Implementation of that relies on a
+ /// to create new views when necessary.
+ ///
+ public class RegionNavigationContentLoader : IRegionNavigationContentLoader
+ {
+ private readonly IContainerExtension _container;
+
+ ///
+ /// Initializes a new instance of the class with a service locator.
+ ///
+ /// The .
+ public RegionNavigationContentLoader(IContainerExtension container)
+ {
+ _container = container;
+ }
+
+ ///
+ /// Gets the view to which the navigation request represented by applies.
+ ///
+ /// The region.
+ /// The context representing the navigation request.
+ ///
+ /// The view to be the target of the navigation request.
+ ///
+ ///
+ /// If none of the views in the region can be the target of the navigation request, a new view
+ /// is created and added to the region.
+ ///
+ /// when a new view cannot be created for the navigation request.
+ public object LoadContent(IRegion region, NavigationContext navigationContext)
+ {
+ if (region == null)
+ throw new ArgumentNullException(nameof(region));
+
+ if (navigationContext == null)
+ throw new ArgumentNullException(nameof(navigationContext));
+
+ string candidateTargetContract = GetContractFromNavigationContext(navigationContext);
+
+ var candidates = GetCandidatesFromRegion(region, candidateTargetContract);
+
+ var acceptingCandidates =
+ candidates.Where(
+ v =>
+ {
+ if (v is IRegionAware navigationAware && !navigationAware.IsNavigationTarget(navigationContext))
+ {
+ return false;
+ }
+
+ if (!(v is Control control))
+ {
+ return true;
+ }
+
+ navigationAware = control.DataContext as IRegionAware;
+ return navigationAware == null || navigationAware.IsNavigationTarget(navigationContext);
+ });
+
+ var view = acceptingCandidates.FirstOrDefault();
+
+ if (view != null)
+ {
+ return view;
+ }
+
+ view = CreateNewRegionItem(candidateTargetContract);
+
+ AddViewToRegion(region, view);
+
+ return view;
+ }
+
+ ///
+ /// Adds the view to the region.
+ ///
+ /// The region to add the view to
+ /// The view to add to the region
+ protected virtual void AddViewToRegion(IRegion region, object view)
+ {
+ region.Add(view);
+ }
+
+ ///
+ /// Provides a new item for the region based on the supplied candidate target contract name.
+ ///
+ /// The target contract to build.
+ /// An instance of an item to put into the .
+ protected virtual object CreateNewRegionItem(string candidateTargetContract)
+ {
+ try
+ {
+ var newRegionItem = _container.Resolve(candidateTargetContract);
+ MvvmHelpers.AutowireViewModel(newRegionItem);
+ return newRegionItem;
+ }
+ catch (ContainerResolutionException)
+ {
+ throw;
+ }
+ catch (Exception e)
+ {
+ throw new InvalidOperationException(
+ string.Format(CultureInfo.CurrentCulture, Resources.CannotCreateNavigationTarget, candidateTargetContract),
+ e);
+ }
+ }
+
+ ///
+ /// Returns the candidate TargetContract based on the .
+ ///
+ /// The navigation contract.
+ /// The candidate contract to seek within the and to use, if not found, when resolving from the container.
+ protected virtual string GetContractFromNavigationContext(NavigationContext navigationContext)
+ {
+ if (navigationContext == null) throw new ArgumentNullException(nameof(navigationContext));
+
+ var candidateTargetContract = UriParsingHelper.GetAbsolutePath(navigationContext.Uri);
+ candidateTargetContract = candidateTargetContract.TrimStart('/');
+ return candidateTargetContract;
+ }
+
+ ///
+ /// Returns the set of candidates that may satisfy this navigation request.
+ ///
+ /// The region containing items that may satisfy the navigation request.
+ /// The candidate navigation target as determined by
+ /// An enumerable of candidate objects from the
+ protected virtual IEnumerable GetCandidatesFromRegion(IRegion region, string candidateNavigationContract)
+ {
+ if (region is null)
+ {
+ throw new ArgumentNullException(nameof(region));
+ }
+
+ if (string.IsNullOrEmpty(candidateNavigationContract))
+ {
+ throw new ArgumentNullException(nameof(candidateNavigationContract));
+ }
+
+ var contractCandidates = GetCandidatesFromRegionViews(region, candidateNavigationContract);
+
+ if (!contractCandidates.Any())
+ {
+ var matchingType = _container.GetRegistrationType(candidateNavigationContract);
+ if (matchingType is null)
+ {
+ return Array.Empty();
+ }
+
+ return GetCandidatesFromRegionViews(region, matchingType.FullName);
+ }
+
+ return contractCandidates;
+ }
+
+ private IEnumerable GetCandidatesFromRegionViews(IRegion region, string candidateNavigationContract)
+ {
+ return region.Views.Where(v => v is not null && ViewIsMatch(v.GetType(), candidateNavigationContract));
+ }
+
+ private static bool ViewIsMatch(Type viewType, string navigationSegment)
+ {
+ var names = new[] { viewType.Name, viewType.FullName };
+ return names.Any(x => x.Equals(navigationSegment, StringComparison.Ordinal));
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs
new file mode 100644
index 0000000000..7ca44c99c8
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionNavigationService.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Avalonia.Controls;
+using Prism.Common;
+using Prism.Ioc;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ /// Provides navigation for regions.
+ public class RegionNavigationService : IRegionNavigationService
+ {
+ private readonly IContainerProvider _container;
+ private readonly IRegionNavigationContentLoader _regionNavigationContentLoader;
+ private NavigationContext _currentNavigationContext;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The navigation target handler.
+ /// The journal.
+ public RegionNavigationService(IContainerExtension container, IRegionNavigationContentLoader regionNavigationContentLoader, IRegionNavigationJournal journal)
+ {
+ _container = container ?? throw new ArgumentNullException(nameof(container));
+ _regionNavigationContentLoader = regionNavigationContentLoader ?? throw new ArgumentNullException(nameof(regionNavigationContentLoader));
+ Journal = journal ?? throw new ArgumentNullException(nameof(journal));
+ Journal.NavigationTarget = this;
+ }
+
+ /// Gets or sets the region.
+ /// The region.
+ public IRegion Region { get; set; }
+
+ /// Gets the journal.
+ /// The journal.
+ public IRegionNavigationJournal Journal { get; private set; }
+
+ /// Raised when the region is about to be navigated to content.
+ public event EventHandler Navigating;
+
+ private void RaiseNavigating(NavigationContext navigationContext)
+ {
+ Navigating?.Invoke(this, new RegionNavigationEventArgs(navigationContext));
+ }
+
+ /// Raised when the region is navigated to content.
+ public event EventHandler Navigated;
+
+ private void RaiseNavigated(NavigationContext navigationContext)
+ {
+ Navigated?.Invoke(this, new RegionNavigationEventArgs(navigationContext));
+ }
+
+ /// Raised when a navigation request fails.
+ public event EventHandler NavigationFailed;
+
+ private void RaiseNavigationFailed(NavigationContext navigationContext, Exception error)
+ {
+ NavigationFailed?.Invoke(this, new RegionNavigationFailedEventArgs(navigationContext, error));
+ }
+
+ /// Initiates navigation to the specified target.
+ /// The target.
+ /// A callback to execute when the navigation request is completed.
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is marshalled to callback")]
+ public void RequestNavigate(Uri target, Action navigationCallback)
+ {
+ RequestNavigate(target, navigationCallback, null);
+ }
+
+ /// Initiates navigation to the specified target.
+ /// The target.
+ /// A callback to execute when the navigation request is completed.
+ /// The navigation parameters specific to the navigation request.
+ public void RequestNavigate(Uri target, Action navigationCallback, INavigationParameters navigationParameters)
+ {
+ if (navigationCallback == null)
+ throw new ArgumentNullException(nameof(navigationCallback));
+
+ try
+ {
+ DoNavigate(target, navigationCallback, navigationParameters);
+ }
+ catch (Exception e)
+ {
+ NotifyNavigationFailed(new NavigationContext(this, target), navigationCallback, e);
+ }
+ }
+
+ private void DoNavigate(Uri source, Action navigationCallback, INavigationParameters navigationParameters)
+ {
+ if (source == null)
+ throw new ArgumentNullException(nameof(source));
+
+ if (Region == null)
+ throw new InvalidOperationException(Resources.NavigationServiceHasNoRegion);
+
+ _currentNavigationContext = new NavigationContext(this, source, navigationParameters);
+
+ // starts querying the active views
+ RequestCanNavigateFromOnCurrentlyActiveView(
+ _currentNavigationContext,
+ navigationCallback,
+ Region.ActiveViews.ToArray(),
+ 0);
+ }
+
+ private void RequestCanNavigateFromOnCurrentlyActiveView(
+ NavigationContext navigationContext,
+ Action navigationCallback,
+ object[] activeViews,
+ int currentViewIndex)
+ {
+ if (currentViewIndex < activeViews.Length)
+ {
+ if (activeViews[currentViewIndex] is IConfirmNavigationRequest vetoingView)
+ {
+ // the current active view implements IConfirmNavigationRequest, request confirmation
+ // providing a callback to resume the navigation request
+ vetoingView.ConfirmNavigationRequest(
+ navigationContext,
+ canNavigate =>
+ {
+ if (_currentNavigationContext == navigationContext && canNavigate)
+ {
+ RequestCanNavigateFromOnCurrentlyActiveViewModel(
+ navigationContext,
+ navigationCallback,
+ activeViews,
+ currentViewIndex);
+ }
+ else
+ {
+ NotifyNavigationFailed(navigationContext, navigationCallback, null);
+ }
+ });
+ }
+ else
+ {
+ RequestCanNavigateFromOnCurrentlyActiveViewModel(
+ navigationContext,
+ navigationCallback,
+ activeViews,
+ currentViewIndex);
+ }
+ }
+ else
+ {
+ ExecuteNavigation(navigationContext, activeViews, navigationCallback);
+ }
+ }
+
+ private void RequestCanNavigateFromOnCurrentlyActiveViewModel(
+ NavigationContext navigationContext,
+ Action navigationCallback,
+ object[] activeViews,
+ int currentViewIndex)
+ {
+ if (activeViews[currentViewIndex] is Control control)
+ {
+ if (control.DataContext is IConfirmNavigationRequest vetoingViewModel)
+ {
+ // the data model for the current active view implements IConfirmNavigationRequest, request confirmation
+ // providing a callback to resume the navigation request
+ vetoingViewModel.ConfirmNavigationRequest(
+ navigationContext,
+ canNavigate =>
+ {
+ if (_currentNavigationContext == navigationContext && canNavigate)
+ {
+ RequestCanNavigateFromOnCurrentlyActiveView(
+ navigationContext,
+ navigationCallback,
+ activeViews,
+ currentViewIndex + 1);
+ }
+ else
+ {
+ NotifyNavigationFailed(navigationContext, navigationCallback, null);
+ }
+ });
+
+ return;
+ }
+ }
+
+ RequestCanNavigateFromOnCurrentlyActiveView(
+ navigationContext,
+ navigationCallback,
+ activeViews,
+ currentViewIndex + 1);
+ }
+
+ [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is marshalled to callback")]
+ private void ExecuteNavigation(NavigationContext navigationContext, object[] activeViews, Action navigationCallback)
+ {
+ try
+ {
+ NotifyActiveViewsNavigatingFrom(navigationContext, activeViews);
+
+ object view = _regionNavigationContentLoader.LoadContent(Region, navigationContext);
+
+ // Raise the navigating event just before activating the view.
+ RaiseNavigating(navigationContext);
+
+ Region.Activate(view);
+
+ // Update the navigation journal before notifying others of navigation
+ IRegionNavigationJournalEntry journalEntry = _container.Resolve();
+ journalEntry.Uri = navigationContext.Uri;
+ journalEntry.Parameters = navigationContext.Parameters;
+
+ bool persistInHistory = PersistInHistory(view);
+
+ Journal.RecordNavigation(journalEntry, persistInHistory);
+
+ // The view can be informed of navigation
+ Action action = (n) => n.OnNavigatedTo(navigationContext);
+ MvvmHelpers.ViewAndViewModelAction(view, action);
+
+ navigationCallback(new NavigationResult(navigationContext, true));
+
+ // Raise the navigated event when navigation is completed.
+ RaiseNavigated(navigationContext);
+ }
+ catch (Exception e)
+ {
+ NotifyNavigationFailed(navigationContext, navigationCallback, e);
+ }
+ }
+
+ private static bool PersistInHistory(object view)
+ {
+ bool persist = true;
+ MvvmHelpers.ViewAndViewModelAction(view, ija => { persist &= ija.PersistInHistory(); });
+ return persist;
+ }
+
+ private void NotifyNavigationFailed(NavigationContext navigationContext, Action navigationCallback, Exception e)
+ {
+ var navigationResult =
+ e != null ? new NavigationResult(navigationContext, e) : new NavigationResult(navigationContext, false);
+
+ navigationCallback(navigationResult);
+ RaiseNavigationFailed(navigationContext, e);
+ }
+
+ private static void NotifyActiveViewsNavigatingFrom(NavigationContext navigationContext, object[] activeViews)
+ {
+ InvokeOnNavigationAwareElements(activeViews, (n) => n.OnNavigatedFrom(navigationContext));
+ }
+
+ private static void InvokeOnNavigationAwareElements(IEnumerable items, Action invocation)
+ {
+ foreach (var item in items)
+ {
+ MvvmHelpers.ViewAndViewModelAction(item, invocation);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs
new file mode 100644
index 0000000000..142cf17d00
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/RegionViewRegistry.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reflection;
+using Prism.Common;
+using Prism.Events;
+using Prism.Ioc;
+using Prism.Properties;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Defines a registry for the content of the regions used on View Discovery composition.
+ ///
+ public class RegionViewRegistry : IRegionViewRegistry
+ {
+ private readonly IContainerProvider _container;
+ private readonly ListDictionary> _registeredContent = new ListDictionary>();
+ private readonly WeakDelegatesManager _contentRegisteredListeners = new WeakDelegatesManager();
+
+ /// Creates a new instance of the class.
+ /// used to create the instance of the views from its .
+ public RegionViewRegistry(IContainerExtension container)
+ {
+ _container = container;
+ }
+
+ /// Occurs whenever a new view is registered.
+ public event EventHandler ContentRegistered
+ {
+ add => _contentRegisteredListeners.AddListener(value);
+ remove => _contentRegisteredListeners.RemoveListener(value);
+ }
+
+ /// Returns the contents registered for a region.
+ /// Name of the region which content is being requested.
+ /// The to use to get the View.
+ /// Collection of contents registered for the region.
+ public IEnumerable GetContents(string regionName, IContainerProvider container)
+ {
+ var items = new List();
+ foreach (Func getContentDelegate in _registeredContent[regionName])
+ {
+ items.Add(getContentDelegate(container));
+ }
+
+ return items;
+ }
+
+ /// Registers a content type with a region name.
+ /// Region name to which the will be registered.
+ /// Content type to be registered for the .
+ public void RegisterViewWithRegion(string regionName, Type viewType)
+ {
+ RegisterViewWithRegion(regionName, _ => CreateInstance(viewType));
+ }
+
+ /// Registers a delegate that can be used to retrieve the content associated with a region name.
+ /// Region name to which the will be registered.
+ /// Delegate used to retrieve the content associated with the .
+ public void RegisterViewWithRegion(string regionName, Func getContentDelegate)
+ {
+ _registeredContent.Add(regionName, getContentDelegate);
+ OnContentRegistered(new ViewRegisteredEventArgs(regionName, getContentDelegate));
+ }
+
+ ///
+ /// Associate a view with a region, by registering a type. When the region get's displayed
+ /// this type will be resolved using the ServiceLocator into a concrete instance. The instance
+ /// will be added to the Views collection of the region
+ ///
+ /// The name of the region to associate the view with.
+ /// The type of the view to register with the
+ /// The , for adding several views easily
+ public void RegisterViewWithRegion(string regionName, string targetName) =>
+ RegisterViewWithRegion(regionName, c => c.Resolve(targetName));
+
+ /// Creates an instance of a registered view .
+ /// Type of the registered view.
+ /// Instance of the registered view.
+ protected virtual object CreateInstance(Type type)
+ {
+ var view = _container.Resolve(type);
+ MvvmHelpers.AutowireViewModel(view);
+ return view;
+ }
+
+ private void OnContentRegistered(ViewRegisteredEventArgs e)
+ {
+ // TODO (2022-11-28): Consider returning an object with Success(bool) and Exception(ex)
+ try
+ {
+ _contentRegisteredListeners.Raise(this, e);
+ }
+ catch (TargetInvocationException ex)
+ {
+ Exception rootException;
+ if (ex.InnerException != null)
+ {
+ rootException = ex.InnerException.GetRootException();
+ }
+ else
+ {
+ rootException = ex.GetRootException();
+ }
+
+ // TODO (2022-11-28): Consider safely informing user of XAML error when encountered
+ throw new ViewRegistrationException(string.Format(CultureInfo.CurrentCulture,
+ Resources.OnViewRegisteredException, e.RegionName, rootException), ex.InnerException);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs
new file mode 100644
index 0000000000..a55ec6ad38
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SelectorRegionAdapter.cs
@@ -0,0 +1,70 @@
+// TODO: Feature currently disabled
+/*
+using Prism.Navigation.Regions.Behaviors;
+using System;
+using Avalonia.Controls.Primitives;
+using Avalonia.Styling;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Adapter that creates a new and binds all
+ /// the views to the adapted .
+ /// It also keeps the and the selected items
+ /// of the in sync.
+ ///
+ public class SelectorRegionAdapter : RegionAdapterBase
+ {
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The factory used to create the region behaviors to attach to the created regions.
+ public SelectorRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
+ : base(regionBehaviorFactory)
+ {
+ }
+
+ ///
+ /// Adapts an to an .
+ ///
+ /// The new region being used.
+ /// The object to adapt.
+ protected override void Adapt(IRegion region, Selector regionTarget)
+ {
+ }
+
+ ///
+ /// Attach new behaviors.
+ ///
+ /// The region being used.
+ /// The object to adapt.
+ ///
+ /// This class attaches the base behaviors and also listens for changes in the
+ /// activity of the region or the control selection and keeps the in sync.
+ ///
+ protected override void AttachBehaviors(IRegion region, Selector regionTarget)
+ {
+ if (region == null)
+ throw new ArgumentNullException(nameof(region));
+
+ // Add the behavior that syncs the items source items with the rest of the items
+ region.Behaviors.Add(SelectorItemsSourceSyncBehavior.BehaviorKey, new SelectorItemsSourceSyncBehavior()
+ {
+ /////HostControl = regionTarget
+ HostControl = regionTarget.SelectedItem as Avalonia.AvaloniaObject // TODO: Verify '.SelectedItem ...'
+ });
+
+ base.AttachBehaviors(region, regionTarget);
+ }
+
+ ///
+ /// Creates a new instance of .
+ ///
+ /// A new instance of .
+ protected override IRegion CreateRegion()
+ {
+ return new Region();
+ }
+ }
+}
+*/
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs
new file mode 100644
index 0000000000..0b44f93652
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/SingleActiveRegion.cs
@@ -0,0 +1,28 @@
+using System.Linq;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Region that allows a maximum of one active view at a time.
+ ///
+ public class SingleActiveRegion : Region
+ {
+ ///
+ /// Marks the specified view as active.
+ ///
+ /// The view to activate.
+ /// If there is an active view before calling this method,
+ /// that view will be deactivated automatically.
+ public override void Activate(object view)
+ {
+ object currentActiveView = ActiveViews.FirstOrDefault();
+
+ if (currentActiveView != null && currentActiveView != view && Views.Contains(currentActiveView))
+ {
+ base.Deactivate(currentActiveView);
+ }
+
+ base.Activate(view);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs
new file mode 100644
index 0000000000..1b2965b62e
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Navigation/Regions/ViewsCollection.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+
+namespace Prism.Navigation.Regions
+{
+ ///
+ /// Implementation of that takes an of
+ /// and filters it to display an collection of
+ /// elements (the items which the wraps).
+ ///
+ public class ViewsCollection : IViewsCollection
+ {
+ private readonly ObservableCollection subjectCollection;
+
+ private readonly Dictionary monitoredItems =
+ new Dictionary();
+
+ private readonly Predicate filter;
+ private Comparison sort;
+ private List filteredItems = new List();
+
+ /// Initializes a new instance of the class.
+ /// The list to wrap and filter.
+ /// A predicate to filter the collection.
+ public ViewsCollection(ObservableCollection list, Predicate filter)
+ {
+ subjectCollection = list;
+ this.filter = filter;
+ MonitorAllMetadataItems();
+ subjectCollection.CollectionChanged += SourceCollectionChanged;
+ UpdateFilteredItemsList();
+ }
+
+ /// Occurs when the collection changes.
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ /// Gets or sets the comparison used to sort the views.
+ /// The comparison to use.
+ public Comparison SortComparison
+ {
+ get { return sort; }
+ set
+ {
+ if (sort != value)
+ {
+ sort = value;
+ UpdateFilteredItemsList();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+ }
+ }
+
+ /// Determines whether the collection contains a specific value.
+ /// The object to locate in the collection.
+ /// if is found in the collection; otherwise, .
+ public bool Contains(object value)
+ {
+ return filteredItems.Contains(value);
+ }
+
+ ///Returns an enumerator that iterates through the collection.summary>
+ ///
+ ///A that can be used to iterate through the collection.
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return filteredItems.GetEnumerator();
+ }
+
+ ///Returns an enumerator that iterates through a collection.summary>
+ ///
+ ///An object that can be used to iterate through the collection.
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ /// Used to invoked the event.
+ ///
+ private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
+ {
+ CollectionChanged?.Invoke(this, e);
+ }
+
+ private void NotifyReset()
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ /// Removes all monitoring of underlying MetadataItems and re-adds them.
+ private void ResetAllMonitors()
+ {
+ RemoveAllMetadataMonitors();
+ MonitorAllMetadataItems();
+ }
+
+ /// Adds all underlying MetadataItems to the list from the subjectCollection
+ private void MonitorAllMetadataItems()
+ {
+ foreach (var item in subjectCollection)
+ {
+ AddMetadataMonitor(item, filter(item));
+ }
+ }
+
+ /// Removes all monitored items from our monitoring list.
+ private void RemoveAllMetadataMonitors()
+ {
+ foreach (var item in monitoredItems)
+ {
+ item.Key.MetadataChanged -= OnItemMetadataChanged;
+ }
+
+ monitoredItems.Clear();
+ }
+
+ /// Adds handler to monitor the MetadataItem and adds it to our monitoring list.
+ ///
+ ///
+ private void AddMetadataMonitor(ItemMetadata itemMetadata, bool isInList)
+ {
+ itemMetadata.MetadataChanged += OnItemMetadataChanged;
+ monitoredItems.Add(
+ itemMetadata,
+ new MonitorInfo
+ {
+ IsInList = isInList
+ });
+ }
+
+ /// Unhooks from the MetadataItem change event and removes from our monitoring list.
+ ///
+ private void RemoveMetadataMonitor(ItemMetadata itemMetadata)
+ {
+ itemMetadata.MetadataChanged -= OnItemMetadataChanged;
+ monitoredItems.Remove(itemMetadata);
+ }
+
+ /// Invoked when any of the underlying ItemMetadata items we're monitoring changes.
+ ///
+ ///
+ private void OnItemMetadataChanged(object sender, EventArgs e)
+ {
+ ItemMetadata itemMetadata = (ItemMetadata)sender;
+
+ // Our monitored item may have been removed during another event before
+ // our OnItemMetadataChanged got called back, so it's not unexpected
+ // that we may not have it in our list.
+ MonitorInfo monitorInfo;
+ bool foundInfo = monitoredItems.TryGetValue(itemMetadata, out monitorInfo);
+ if (!foundInfo) return;
+
+ if (filter(itemMetadata))
+ {
+ if (!monitorInfo.IsInList)
+ {
+ // This passes our filter and wasn't marked
+ // as in our list so we can consider this
+ // an Add.
+ monitorInfo.IsInList = true;
+ UpdateFilteredItemsList();
+ NotifyAdd(itemMetadata.Item);
+ }
+ }
+ else
+ {
+ // This doesn't fit our filter, we remove from our
+ // tracking list, but should not remove any monitoring in
+ // case it fits our filter in the future.
+ monitorInfo.IsInList = false;
+ RemoveFromFilteredList(itemMetadata.Item);
+ }
+ }
+
+ ///
+ /// The event handler due to changes in the underlying collection.
+ ///
+ ///
+ ///
+ private void SourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ UpdateFilteredItemsList();
+ foreach (ItemMetadata itemMetadata in e.NewItems)
+ {
+ bool isInFilter = filter(itemMetadata);
+ AddMetadataMonitor(itemMetadata, isInFilter);
+ if (isInFilter)
+ {
+ NotifyAdd(itemMetadata.Item);
+ }
+ }
+
+ // If we're sorting we can't predict how
+ // the collection has changed on an add so we
+ // resort to a reset notification.
+ if (sort != null)
+ {
+ NotifyReset();
+ }
+
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ foreach (ItemMetadata itemMetadata in e.OldItems)
+ {
+ RemoveMetadataMonitor(itemMetadata);
+ if (filter(itemMetadata))
+ {
+ RemoveFromFilteredList(itemMetadata.Item);
+ }
+ }
+
+ break;
+
+ default:
+ ResetAllMonitors();
+ UpdateFilteredItemsList();
+ NotifyReset();
+
+ break;
+ }
+ }
+
+ private void NotifyAdd(object item)
+ {
+ int newIndex = filteredItems.IndexOf(item);
+ NotifyAdd(new[] { item }, newIndex);
+ }
+
+ private void RemoveFromFilteredList(object item)
+ {
+ int index = filteredItems.IndexOf(item);
+ UpdateFilteredItemsList();
+ NotifyRemove(new[] { item }, index);
+ }
+
+ private void UpdateFilteredItemsList()
+ {
+ filteredItems = subjectCollection.Where(i => filter(i)).Select(i => i.Item)
+ .OrderBy(o => o, new RegionItemComparer(SortComparison)).ToList();
+ }
+
+ private class MonitorInfo
+ {
+ public bool IsInList { get; set; }
+ }
+
+ private class RegionItemComparer : Comparer
+ {
+ private readonly Comparison comparer;
+
+ public RegionItemComparer(Comparison comparer)
+ {
+ this.comparer = comparer;
+ }
+
+ public override int Compare(object x, object y)
+ {
+ if (comparer == null)
+ {
+ return 0;
+ }
+
+ return comparer(x, y);
+ }
+ }
+
+ private void NotifyAdd(IList items, int newStartingIndex)
+ {
+ if (items.Count > 0)
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Add,
+ items,
+ newStartingIndex));
+ }
+ }
+
+ private void NotifyRemove(IList items, int originalIndex)
+ {
+ if (items.Count > 0)
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(
+ NotifyCollectionChangedAction.Remove,
+ items,
+ originalIndex));
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj b/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj
new file mode 100644
index 0000000000..ccd31efc1a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Prism.Avalonia.csproj
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+ Properties
+ Prism
+ net6.0;net7.0;net8.0
+ Prism.Avalonia is a fully open source version of the Prism guidance originally produced by Microsoft Patterns & Practices. Prism.Avalonia provides an implementation of a collection of design patterns that are helpful in writing well structured, maintainable, and testable XAML applications, including MVVM, dependency injection, commanding, event aggregation, and more. Prism's core functionality is a shared library targeting the .NET Framework and .NET Standard. Features that need to be platform specific are implemented in the respective libraries for the target platform (Avalonia, WPF, Uno Platform, and Xamarin Forms).
+
+Prism.Avalonia helps you more easily design and build rich, flexible, and easy to maintain cross-platform Avalonia desktop applications. This library provides user interface composition as well as modularity support.
+ prism;mvvm;xaml;avalonia;navigation;dialog;prismavalonia;
+ Copyright (c) 2024 Xeno Innovations, Inc.
+ Damian Suess, Suess Labs, various contributors
+ Prism.Avalonia
+ README.md
+ Prism.Avalonia.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs b/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs
new file mode 100644
index 0000000000..76856a7996
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/PrismApplicationBase.cs
@@ -0,0 +1,187 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Prism.Common;
+using Prism.Ioc;
+using Prism.Modularity;
+using Prism.Navigation.Regions;
+
+namespace Prism
+{
+ ///
+ /// Base application class that provides a basic initialization sequence
+ ///
+ ///
+ /// This class must be overridden to provide application specific configuration.
+ ///
+ public abstract class PrismApplicationBase : Application
+ {
+ private IContainerExtension _containerExtension;
+ private IModuleCatalog _moduleCatalog;
+
+ // FROM Prism.Avalonia7.1.2
+ public AvaloniaObject MainWindow { get; private set; }
+
+ ///
+ /// The dependency injection container used to resolve objects
+ ///
+ public IContainerProvider Container => _containerExtension;
+
+ ///
+ /// Configures the used by Prism.
+ ///
+ protected virtual void ConfigureViewModelLocator()
+ {
+ PrismInitializationExtensions.ConfigureViewModelLocator();
+ }
+
+ ///
+ /// Runs the initialization sequence to configure the Prism application.
+ ///
+ ///
+ /// Though, Prism.WPF v8.1 uses, `protected virtual void Initialize()`
+ /// Avalonia's AppBuilderBase.cs calls, `.Setup() { ... Instance.Initialize(); ... }`
+ /// Therefore, we need this as a `public override void` in PrismApplicationBase.cs
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ ConfigureViewModelLocator();
+
+ ContainerLocator.SetContainerExtension(CreateContainerExtension());
+ _containerExtension = ContainerLocator.Current;
+ _moduleCatalog = CreateModuleCatalog();
+ RegisterRequiredTypes(_containerExtension);
+ RegisterTypes(_containerExtension);
+
+ ConfigureModuleCatalog(_moduleCatalog);
+
+ var regionAdapterMappings = _containerExtension.Resolve();
+ ConfigureRegionAdapterMappings(regionAdapterMappings);
+
+ var defaultRegionBehaviors = _containerExtension.Resolve();
+ ConfigureDefaultRegionBehaviors(defaultRegionBehaviors);
+
+ RegisterFrameworkExceptionTypes();
+
+ var shell = CreateShell();
+ if (shell != null)
+ {
+ MvvmHelpers.AutowireViewModel(shell);
+ RegionManager.SetRegionManager(shell, _containerExtension.Resolve());
+ RegionManager.UpdateRegions();
+ InitializeShell(shell);
+ }
+
+ InitializeModules();
+
+ OnInitialized();
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+ desktopLifetime.MainWindow = MainWindow as Window;
+ else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
+ singleViewLifetime.MainView = MainWindow as Control;
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ ///
+ /// Creates the container used by Prism.
+ ///
+ /// The container
+ protected abstract IContainerExtension CreateContainerExtension();
+
+ ///
+ /// Creates the used by Prism.
+ ///
+ ///
+ /// The base implementation returns a new ModuleCatalog.
+ ///
+ protected virtual IModuleCatalog CreateModuleCatalog()
+ {
+ return new ModuleCatalog();
+ }
+
+ ///
+ /// Registers all types that are required by Prism to function with the container.
+ ///
+ ///
+ protected virtual void RegisterRequiredTypes(IContainerRegistry containerRegistry)
+ {
+ containerRegistry.RegisterRequiredTypes(_moduleCatalog);
+ }
+
+ ///
+ /// Used to register types with the container that will be used by your application.
+ ///
+ protected abstract void RegisterTypes(IContainerRegistry containerRegistry);
+
+ ///
+ /// Configures the .
+ /// This will be the list of default behaviors that will be added to a region.
+ ///
+ protected virtual void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
+ {
+ regionBehaviors?.RegisterDefaultRegionBehaviors();
+ }
+
+ ///
+ /// Configures the default region adapter mappings to use in the application, in order
+ /// to adapt UI controls defined in XAML to use a region and register it automatically.
+ /// May be overwritten in a derived class to add specific mappings required by the application.
+ ///
+ /// The instance containing all the mappings.
+ protected virtual void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
+ {
+ regionAdapterMappings?.RegisterDefaultRegionAdapterMappings();
+ }
+
+ ///
+ /// Registers the s of the Exceptions that are not considered
+ /// root exceptions by the .
+ ///
+ protected virtual void RegisterFrameworkExceptionTypes()
+ {
+ }
+
+ ///
+ /// Creates the shell or main window of the application.
+ ///
+ /// The shell of the application.
+ protected abstract AvaloniaObject CreateShell();
+
+ ///
+ /// Initializes the shell.
+ ///
+ protected virtual void InitializeShell(AvaloniaObject shell)
+ {
+ MainWindow = shell;
+ }
+
+ ///
+ /// Contains actions that should occur last.
+ ///
+ protected virtual void OnInitialized()
+ {
+ (MainWindow as Window)?.Show();
+ }
+
+ ///
+ /// Configures the used by Prism.
+ ///
+ protected virtual void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { }
+
+ ///
+ /// Initializes the modules.
+ ///
+ protected virtual void InitializeModules()
+ {
+ PrismInitializationExtensions.RunModuleManager(Container);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs b/src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs
new file mode 100644
index 0000000000..2503d2645a
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/PrismBootstrapperBase.cs
@@ -0,0 +1,183 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Prism.Common;
+using Prism.Ioc;
+using Prism.Modularity;
+using Prism.Navigation.Regions;
+
+namespace Prism
+{
+ ///
+ /// Base class that provides a basic bootstrapping sequence and hooks
+ /// that specific implementations can override
+ ///
+ ///
+ /// This class must be overridden to provide application specific configuration.
+ ///
+ public abstract class PrismBootstrapperBase
+ {
+ private IContainerExtension _containerExtension;
+ private IModuleCatalog _moduleCatalog;
+
+ ///
+ /// The dependency injection container used to resolve objects
+ ///
+ public IContainerProvider Container => _containerExtension;
+
+ ///
+ /// Gets the shell user interface
+ ///
+ /// The shell user interface.
+ protected AvaloniaObject Shell { get; set; }
+
+ ///
+ /// Runs the bootstrapper process.
+ ///
+ public void Run()
+ {
+ ConfigureViewModelLocator();
+ Initialize();
+ OnInitialized();
+ }
+
+ ///
+ /// Configures the used by Prism.
+ ///
+ protected virtual void ConfigureViewModelLocator()
+ {
+ PrismInitializationExtensions.ConfigureViewModelLocator();
+ }
+
+ ///
+ /// Runs the initialization sequence to configure the Prism application.
+ ///
+ protected virtual void Initialize()
+ {
+ ContainerLocator.SetContainerExtension(CreateContainerExtension());
+ _containerExtension = ContainerLocator.Current;
+ _moduleCatalog = CreateModuleCatalog();
+ RegisterRequiredTypes(_containerExtension);
+ RegisterTypes(_containerExtension);
+
+ ConfigureModuleCatalog(_moduleCatalog);
+
+ var regionAdapterMappings = _containerExtension.Resolve();
+ ConfigureRegionAdapterMappings(regionAdapterMappings);
+
+ var defaultRegionBehaviors = _containerExtension.Resolve();
+ ConfigureDefaultRegionBehaviors(defaultRegionBehaviors);
+
+ RegisterFrameworkExceptionTypes();
+
+ var shell = CreateShell();
+ if (shell != null)
+ {
+ MvvmHelpers.AutowireViewModel(shell);
+ RegionManager.SetRegionManager(shell, _containerExtension.Resolve());
+ RegionManager.UpdateRegions();
+ InitializeShell(shell);
+ }
+
+ InitializeModules();
+ }
+
+ ///
+ /// Creates the container used by Prism.
+ ///
+ /// The container
+ protected abstract IContainerExtension CreateContainerExtension();
+
+ ///
+ /// Creates the used by Prism.
+ ///
+ ///
+ /// The base implementation returns a new ModuleCatalog.
+ ///
+ protected virtual IModuleCatalog CreateModuleCatalog()
+ {
+ return new ModuleCatalog();
+ }
+
+ ///
+ /// Registers all types that are required by Prism to function with the container.
+ ///
+ ///
+ protected virtual void RegisterRequiredTypes(IContainerRegistry containerRegistry)
+ {
+ if (_moduleCatalog == null)
+ throw new InvalidOperationException("IModuleCatalog");
+
+ containerRegistry.RegisterRequiredTypes(_moduleCatalog);
+ }
+
+ ///
+ /// Used to register types with the container that will be used by your application.
+ ///
+ protected abstract void RegisterTypes(IContainerRegistry containerRegistry);
+
+ ///
+ /// Configures the .
+ /// This will be the list of default behaviors that will be added to a region.
+ ///
+ protected virtual void ConfigureDefaultRegionBehaviors(IRegionBehaviorFactory regionBehaviors)
+ {
+ regionBehaviors?.RegisterDefaultRegionBehaviors();
+ }
+
+ ///
+ /// Configures the default region adapter mappings to use in the application, in order
+ /// to adapt UI controls defined in XAML to use a region and register it automatically.
+ /// May be overwritten in a derived class to add specific mappings required by the application.
+ ///
+ /// The instance containing all the mappings.
+ protected virtual void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings)
+ {
+ regionAdapterMappings?.RegisterDefaultRegionAdapterMappings();
+ }
+
+ ///
+ /// Registers the s of the Exceptions that are not considered
+ /// root exceptions by the .
+ ///
+ protected virtual void RegisterFrameworkExceptionTypes()
+ {
+ }
+
+ ///
+ /// Creates the shell or main window of the application.
+ ///
+ /// The shell of the application.
+ protected abstract AvaloniaObject CreateShell();
+
+ ///
+ /// Initializes the shell.
+ ///
+ protected virtual void InitializeShell(AvaloniaObject shell)
+ {
+ Shell = shell;
+ }
+
+ ///
+ /// Contains actions that should occur last.
+ ///
+ protected virtual void OnInitialized()
+ {
+ if (Shell is Window window)
+ window.Show();
+ }
+
+ ///
+ /// Configures the used by Prism.
+ ///
+ protected virtual void ConfigureModuleCatalog(IModuleCatalog moduleCatalog) { }
+
+ ///
+ /// Initializes the modules.
+ ///
+ protected virtual void InitializeModules()
+ {
+ PrismInitializationExtensions.RunModuleManager(Container);
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/PrismInitializationExtensions.cs b/src/Avalonia/Prism.Avalonia/PrismInitializationExtensions.cs
new file mode 100644
index 0000000000..dc17adb593
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/PrismInitializationExtensions.cs
@@ -0,0 +1,66 @@
+using Avalonia.Controls;
+using Prism.Events;
+using Prism.Ioc;
+using Prism.Modularity;
+using Prism.Mvvm;
+using Prism.Dialogs;
+using Prism.Navigation.Regions;
+using Prism.Navigation.Regions.Behaviors;
+
+namespace Prism
+{
+ internal static class PrismInitializationExtensions
+ {
+ internal static void ConfigureViewModelLocator()
+ {
+ ViewModelLocationProvider.SetDefaultViewModelFactory((view, type) =>
+ {
+ return ContainerLocator.Container.Resolve(type);
+ });
+ }
+
+ internal static void RegisterRequiredTypes(this IContainerRegistry containerRegistry, IModuleCatalog moduleCatalog)
+ {
+ containerRegistry.RegisterInstance(moduleCatalog);
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.RegisterSingleton();
+ containerRegistry.Register();
+ containerRegistry.Register();
+ containerRegistry.Register();
+ containerRegistry.Register(); //default dialog host
+ }
+
+ internal static void RegisterDefaultRegionBehaviors(this IRegionBehaviorFactory regionBehaviors)
+ {
+ //// Avalonia to WPF Equivilant: BindRegionContextToAvaloniaObjectBehavior == BindRegionContextToDependencyObjectBehavior
+ regionBehaviors.AddIfMissing(BindRegionContextToAvaloniaObjectBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(RegionActiveAwareBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(SyncRegionContextWithHostBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(RegionManagerRegistrationBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(RegionMemberLifetimeBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(ClearChildViewsRegionBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(AutoPopulateRegionBehavior.BehaviorKey);
+ regionBehaviors.AddIfMissing(DestructibleRegionBehavior.BehaviorKey);
+ }
+
+ internal static void RegisterDefaultRegionAdapterMappings(this RegionAdapterMappings regionAdapterMappings)
+ {
+ //// regionAdapterMappings.RegisterMapping();
+ regionAdapterMappings.RegisterMapping();
+ regionAdapterMappings.RegisterMapping();
+ }
+
+ internal static void RunModuleManager(IContainerProvider containerProvider)
+ {
+ IModuleManager manager = containerProvider.Resolve();
+ manager.Run();
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs b/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..d24a6afdc4
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Properties/AssemblyInfo.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Metadata;
+
+[assembly: ComVisible(false)]
+[assembly: CLSCompliant(true)]
+
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Navigation.Regions")]
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Navigation.Regions.Behaviors")]
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Mvvm")]
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Interactivity")]
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Dialogs")]
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.Ioc")]
diff --git a/src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs b/src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..0d55e231c1
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Properties/Resources.Designer.cs
@@ -0,0 +1,523 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Prism.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Prism.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The object must be of type '{0}' in order to use the current region adapter..
+ ///
+ internal static string AdapterInvalidTypeException {
+ get {
+ return ResourceManager.GetString("AdapterInvalidTypeException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cannot change the region name once is set. The current region name is '{0}'..
+ ///
+ internal static string CannotChangeRegionNameException {
+ get {
+ return ResourceManager.GetString("CannotChangeRegionNameException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Cannot create navigation target '{0}'..
+ ///
+ internal static string CannotCreateNavigationTarget {
+ get {
+ return ResourceManager.GetString("CannotCreateNavigationTarget", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Type '{0}' does not implement from IRegionBehavior..
+ ///
+ internal static string CanOnlyAddTypesThatInheritIFromRegionBehavior {
+ get {
+ return ResourceManager.GetString("CanOnlyAddTypesThatInheritIFromRegionBehavior", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The ConfigurationStore cannot contain a null value. .
+ ///
+ internal static string ConfigurationStoreCannotBeNull {
+ get {
+ return ResourceManager.GetString("ConfigurationStoreCannotBeNull", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ContentControl's Content property is not empty.
+ /// This control is being associated with a region, but the control is already bound to something else.
+ /// If you did not explicitly set the control's Content property,
+ /// this exception may be caused by a change in the value of the inherited RegionManager attached property..
+ ///
+ internal static string ContentControlHasContentException {
+ get {
+ return ResourceManager.GetString("ContentControlHasContentException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Deactivation is not possible in this type of region..
+ ///
+ internal static string DeactiveNotPossibleException {
+ get {
+ return ResourceManager.GetString("DeactiveNotPossibleException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {1}: {2}. Priority: {3}. Timestamp:{0:u}..
+ ///
+ internal static string DefaultTextLoggerPattern {
+ get {
+ return ResourceManager.GetString("DefaultTextLoggerPattern", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Neither the executeMethod nor the canExecuteMethod delegates can be null..
+ ///
+ internal static string DelegateCommandDelegatesCannotBeNull {
+ get {
+ return ResourceManager.GetString("DelegateCommandDelegatesCannotBeNull", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to T for DelegateCommand<T> is not an object nor Nullable..
+ ///
+ internal static string DelegateCommandInvalidGenericPayloadType {
+ get {
+ return ResourceManager.GetString("DelegateCommandInvalidGenericPayloadType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Directory {0} was not found..
+ ///
+ internal static string DirectoryNotFound {
+ get {
+ return ResourceManager.GetString("DirectoryNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A duplicated module group with name {0} has been found by the loader..
+ ///
+ internal static string DuplicatedModuleGroup {
+ get {
+ return ResourceManager.GetString("DuplicatedModuleGroup", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unable to retrieve the module type {0} from the loaded assemblies. You may need to specify a more fully-qualified type name..
+ ///
+ internal static string FailedToGetType {
+ get {
+ return ResourceManager.GetString("FailedToGetType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to HostControl cannot have null value when behavior attaches. .
+ ///
+ internal static string HostControlCannotBeNull {
+ get {
+ return ResourceManager.GetString("HostControlCannotBeNull", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The HostControl property cannot be set after Attach method has been called..
+ ///
+ internal static string HostControlCannotBeSetAfterAttach {
+ get {
+ return ResourceManager.GetString("HostControlCannotBeSetAfterAttach", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to HostControl type must be a TabControl..
+ ///
+ internal static string HostControlMustBeATabControl {
+ get {
+ return ResourceManager.GetString("HostControlMustBeATabControl", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The IModuleEnumerator interface is no longer used and has been replaced by ModuleCatalog..
+ ///
+ internal static string IEnumeratorObsolete {
+ get {
+ return ResourceManager.GetString("IEnumeratorObsolete", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The argument must be a valid absolute Uri to an assembly file..
+ ///
+ internal static string InvalidArgumentAssemblyUri {
+ get {
+ return ResourceManager.GetString("InvalidArgumentAssemblyUri", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The Target of the IDelegateReference should be of type {0}..
+ ///
+ internal static string InvalidDelegateRerefenceTypeException {
+ get {
+ return ResourceManager.GetString("InvalidDelegateRerefenceTypeException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to ItemsControl's ItemsSource property is not empty.
+ /// This control is being associated with a region, but the control is already bound to something else.
+ /// If you did not explicitly set the control's ItemSource property,
+ /// this exception may be caused by a change in the value of the inherited RegionManager attached property..
+ ///
+ internal static string ItemsControlHasItemsSourceException {
+ get {
+ return ResourceManager.GetString("ItemsControlHasItemsSourceException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Mapping with the given type is already registered: {0}..
+ ///
+ internal static string MappingExistsException {
+ get {
+ return ResourceManager.GetString("MappingExistsException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Module {0} was not found in the catalog..
+ ///
+ internal static string ModuleNotFound {
+ get {
+ return ResourceManager.GetString("ModuleNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The ModulePath cannot contain a null value or be empty.
+ ///
+ internal static string ModulePathCannotBeNullOrEmpty {
+ get {
+ return ResourceManager.GetString("ModulePathCannotBeNullOrEmpty", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to load type '{0}' from assembly '{1}'..
+ ///
+ internal static string ModuleTypeNotFound {
+ get {
+ return ResourceManager.GetString("ModuleTypeNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The ModuleCatalog must implement IModuleGroupCatalog to add groups.
+ ///
+ internal static string MustBeModuleGroupCatalog {
+ get {
+ return ResourceManager.GetString("MustBeModuleGroupCatalog", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Navigation is already in progress on region with name '{0}'..
+ ///
+ internal static string NavigationInProgress {
+ get {
+ return ResourceManager.GetString("NavigationInProgress", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Navigation cannot proceed until a region is set for the RegionNavigationService..
+ ///
+ internal static string NavigationServiceHasNoRegion {
+ get {
+ return ResourceManager.GetString("NavigationServiceHasNoRegion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The IRegionAdapter for the type {0} is not registered in the region adapter mappings. You can register an IRegionAdapter for this control by overriding the ConfigureRegionAdapterMappings method in the bootstrapper..
+ ///
+ internal static string NoRegionAdapterException {
+ get {
+ return ResourceManager.GetString("NoRegionAdapterException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to There is currently no moduleTypeLoader in the ModuleManager that can retrieve the specified module..
+ ///
+ internal static string NoRetrieverCanRetrieveModule {
+ get {
+ return ResourceManager.GetString("NoRetrieverCanRetrieveModule", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to An exception has occurred while trying to add a view to region '{0}'.
+ /// - The most likely causing exception was was: '{1}'.
+ /// But also check the InnerExceptions for more detail or call .GetRootException(). .
+ ///
+ internal static string OnViewRegisteredException {
+ get {
+ return ResourceManager.GetString("OnViewRegisteredException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The member access expression does not access a property..
+ ///
+ internal static string PropertySupport_ExpressionNotProperty_Exception {
+ get {
+ return ResourceManager.GetString("PropertySupport_ExpressionNotProperty_Exception", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The expression is not a member access expression..
+ ///
+ internal static string PropertySupport_NotMemberAccessExpression_Exception {
+ get {
+ return ResourceManager.GetString("PropertySupport_NotMemberAccessExpression_Exception", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The referenced property is a static property..
+ ///
+ internal static string PropertySupport_StaticExpression_Exception {
+ get {
+ return ResourceManager.GetString("PropertySupport_StaticExpression_Exception", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The Attach method cannot be called when Region property is null..
+ ///
+ internal static string RegionBehaviorAttachCannotBeCallWithNullRegion {
+ get {
+ return ResourceManager.GetString("RegionBehaviorAttachCannotBeCallWithNullRegion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The Region property cannot be set after Attach method has been called..
+ ///
+ internal static string RegionBehaviorRegionCannotBeSetAfterAttach {
+ get {
+ return ResourceManager.GetString("RegionBehaviorRegionCannotBeSetAfterAttach", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to An exception occurred while creating a region with name '{0}'. The exception was: {1}. .
+ ///
+ internal static string RegionCreationException {
+ get {
+ return ResourceManager.GetString("RegionCreationException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The region being added already has a name of '{0}' and cannot be added to the region manager with a different name ('{1}')..
+ ///
+ internal static string RegionManagerWithDifferentNameException {
+ get {
+ return ResourceManager.GetString("RegionManagerWithDifferentNameException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The region name cannot be null or empty..
+ ///
+ internal static string RegionNameCannotBeEmptyException {
+ get {
+ return ResourceManager.GetString("RegionNameCannotBeEmptyException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Region with the given name is already registered: {0}.
+ ///
+ internal static string RegionNameExistsException {
+ get {
+ return ResourceManager.GetString("RegionNameExistsException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to This RegionManager does not contain a Region with the name '{0}'..
+ ///
+ internal static string RegionNotFound {
+ get {
+ return ResourceManager.GetString("RegionNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The region manager does not contain the {0} region..
+ ///
+ internal static string RegionNotInRegionManagerException {
+ get {
+ return ResourceManager.GetString("RegionNotInRegionManagerException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to View already exists in region..
+ ///
+ internal static string RegionViewExistsException {
+ get {
+ return ResourceManager.GetString("RegionViewExistsException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to View with name '{0}' already exists in the region..
+ ///
+ internal static string RegionViewNameExistsException {
+ get {
+ return ResourceManager.GetString("RegionViewNameExistsException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The provided String argument {0} must not be null or empty..
+ ///
+ internal static string StringCannotBeNullOrEmpty {
+ get {
+ return ResourceManager.GetString("StringCannotBeNullOrEmpty", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The provided String argument {0} must not be null or empty..
+ ///
+ internal static string StringCannotBeNullOrEmpty1 {
+ get {
+ return ResourceManager.GetString("StringCannotBeNullOrEmpty1", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to No BehaviorType with key '{0}' was registered..
+ ///
+ internal static string TypeWithKeyNotRegistered {
+ get {
+ return ResourceManager.GetString("TypeWithKeyNotRegistered", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to An exception occurred while trying to create region objects.
+ /// - The most likely causing exception was: '{0}'.
+ /// But also check the InnerExceptions for more detail or call .GetRootException(). .
+ ///
+ internal static string UpdateRegionException {
+ get {
+ return ResourceManager.GetString("UpdateRegionException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The value must be of type ModuleInfo..
+ ///
+ internal static string ValueMustBeOfTypeModuleInfo {
+ get {
+ return ResourceManager.GetString("ValueMustBeOfTypeModuleInfo", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to {0} not found..
+ ///
+ internal static string ValueNotFound {
+ get {
+ return ResourceManager.GetString("ValueNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The region does not contain the specified view..
+ ///
+ internal static string ViewNotInRegionException {
+ get {
+ return ResourceManager.GetString("ViewNotInRegionException", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Properties/Resources.resx b/src/Avalonia/Prism.Avalonia/Properties/Resources.resx
new file mode 100644
index 0000000000..0cd2cda19f
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Properties/Resources.resx
@@ -0,0 +1,280 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ The object must be of type '{0}' in order to use the current region adapter.
+
+
+ Cannot change the region name once is set. The current region name is '{0}'.
+
+
+ Cannot create navigation target '{0}'.
+
+
+ Type '{0}' does not implement from IRegionBehavior.
+
+
+ The ConfigurationStore cannot contain a null value.
+
+
+ ContentControl's Content property is not empty.
+ This control is being associated with a region, but the control is already bound to something else.
+ If you did not explicitly set the control's Content property,
+ this exception may be caused by a change in the value of the inherited RegionManager attached property.
+
+
+ Deactivation is not possible in this type of region.
+
+
+ {1}: {2}. Priority: {3}. Timestamp:{0:u}.
+
+
+ Neither the executeMethod nor the canExecuteMethod delegates can be null.
+
+
+ T for DelegateCommand<T> is not an object nor Nullable.
+
+
+ Directory {0} was not found.
+
+
+ A duplicated module group with name {0} has been found by the loader.
+
+
+ Unable to retrieve the module type {0} from the loaded assemblies. You may need to specify a more fully-qualified type name.
+
+
+ HostControl cannot have null value when behavior attaches.
+
+
+ The HostControl property cannot be set after Attach method has been called.
+
+
+ HostControl type must be a TabControl.
+
+
+ The IModuleEnumerator interface is no longer used and has been replaced by ModuleCatalog.
+
+
+ The argument must be a valid absolute Uri to an assembly file.
+
+
+ The Target of the IDelegateReference should be of type {0}.
+
+
+ ItemsControl's ItemsSource property is not empty.
+ This control is being associated with a region, but the control is already bound to something else.
+ If you did not explicitly set the control's ItemSource property,
+ this exception may be caused by a change in the value of the inherited RegionManager attached property.
+
+
+ Mapping with the given type is already registered: {0}.
+
+
+ Module {0} was not found in the catalog.
+
+
+ The ModulePath cannot contain a null value or be empty
+
+
+ Failed to load type '{0}' from assembly '{1}'.
+
+
+ Navigation is already in progress on region with name '{0}'.
+
+
+ Navigation cannot proceed until a region is set for the RegionNavigationService.
+
+
+ The IRegionAdapter for the type {0} is not registered in the region adapter mappings. You can register an IRegionAdapter for this control by overriding the ConfigureRegionAdapterMappings method in the bootstrapper.
+
+
+ There is currently no moduleTypeLoader in the ModuleManager that can retrieve the specified module.
+
+
+ An exception has occurred while trying to add a view to region '{0}'.
+ - The most likely causing exception was was: '{1}'.
+ But also check the InnerExceptions for more detail or call .GetRootException().
+
+
+ The member access expression does not access a property.
+
+
+ The expression is not a member access expression.
+
+
+ The referenced property is a static property.
+
+
+ The Attach method cannot be called when Region property is null.
+
+
+ The Region property cannot be set after Attach method has been called.
+
+
+ An exception occurred while creating a region with name '{0}'. The exception was: {1}.
+
+
+ The region being added already has a name of '{0}' and cannot be added to the region manager with a different name ('{1}').
+
+
+ The region name cannot be null or empty.
+
+
+ Region with the given name is already registered: {0}
+
+
+ This RegionManager does not contain a Region with the name '{0}'.
+
+
+ The region manager does not contain the {0} region.
+
+
+ View already exists in region.
+
+
+ View with name '{0}' already exists in the region.
+
+
+ The provided String argument {0} must not be null or empty.
+
+
+ The provided String argument {0} must not be null or empty.
+
+
+ No BehaviorType with key '{0}' was registered.
+
+
+ An exception occurred while trying to create region objects.
+ - The most likely causing exception was: '{0}'.
+ But also check the InnerExceptions for more detail or call .GetRootException().
+
+
+ The value must be of type ModuleInfo.
+
+
+ {0} not found.
+
+
+ The region does not contain the specified view.
+
+
+ The ModuleCatalog must implement IModuleGroupCatalog to add groups
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs b/src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs
new file mode 100644
index 0000000000..dd563d61d7
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Properties/Settings.Designer.cs
@@ -0,0 +1,26 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.0
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Prism.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.Avalonia/Properties/Settings.settings b/src/Avalonia/Prism.Avalonia/Properties/Settings.settings
new file mode 100644
index 0000000000..033d7a5e9e
--- /dev/null
+++ b/src/Avalonia/Prism.Avalonia/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/GlobalSuppressions.cs b/src/Avalonia/Prism.DryIoc.Avalonia/GlobalSuppressions.cs
new file mode 100644
index 0000000000..a69d206a20
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/GlobalSuppressions.cs
@@ -0,0 +1,11 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+//
+// To add a suppression to this file, right-click the message in the
+// Error List, point to "Suppress Message(s)", and click
+// "In Project Suppression File".
+// You do not need to add suppressions to this file manually.
+
+[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames")]
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj b/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj
new file mode 100644
index 0000000000..9b31122e27
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/Prism.DryIoc.Avalonia.csproj
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+ Prism.DryIoc
+ net6.0;net7.0;net8.0
+ This extension is used to build Prism.Avalonia applications based on DryIoc. Users must install the Prism.Avalonia NuGet package as well.
+ Damian Suess, Suess Labs, various contributors
+ Copyright (c) 2024 Xeno Innovations, Inc.
+ Prism.DryIoc.Avalonia
+ README.md
+ prism;mvvm;xaml;avalonia;dryioc;dependencyinjection;navigation;dialog;prismavalonia;
+ Prism.Avalonia.png
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs b/src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs
new file mode 100644
index 0000000000..953ae7793b
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/PrismApplication.cs
@@ -0,0 +1,38 @@
+using System;
+using DryIoc;
+using Prism.Container.DryIoc;
+using Prism.Ioc;
+using ExceptionExtensions = System.ExceptionExtensions;
+
+namespace Prism.DryIoc
+{
+ ///
+ /// Base application class that uses as it's container.
+ ///
+ public abstract class PrismApplication : PrismApplicationBase
+ {
+ ///
+ /// Create to alter behavior of
+ ///
+ /// An instance of
+ protected virtual Rules CreateContainerRules() => DryIocContainerExtension.DefaultRules;
+
+ ///
+ /// Create a new used by Prism.
+ ///
+ /// A new .
+ protected override IContainerExtension CreateContainerExtension()
+ {
+ return new DryIocContainerExtension(CreateContainerRules());
+ }
+
+ ///
+ /// Registers the s of the Exceptions that are not considered
+ /// root exceptions by the .
+ ///
+ protected override void RegisterFrameworkExceptionTypes()
+ {
+ ExceptionExtensions.RegisterFrameworkExceptionType(typeof(ContainerException));
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs b/src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs
new file mode 100644
index 0000000000..e9d0b021e0
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/PrismBootstrapper.cs
@@ -0,0 +1,31 @@
+using System;
+using DryIoc;
+using Prism.Container.DryIoc;
+using Prism.Ioc;
+
+namespace Prism.DryIoc
+{
+ /// Base bootstrapper class that uses as it's container.
+ public abstract class PrismBootstrapper : PrismBootstrapperBase
+ {
+ /// Create to alter behavior of
+ /// An instance of
+ protected virtual Rules CreateContainerRules() => DryIocContainerExtension.DefaultRules;
+
+ /// Create a new used by Prism.
+ /// A new .
+ protected override IContainerExtension CreateContainerExtension()
+ {
+ return new DryIocContainerExtension(CreateContainerRules());
+ }
+
+ ///
+ /// Registers the s of the Exceptions that are not considered
+ /// root exceptions by the .
+ ///
+ protected override void RegisterFrameworkExceptionTypes()
+ {
+ ExceptionExtensions.RegisterFrameworkExceptionType(typeof(ContainerException));
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7d36c4f34f
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Avalonia.Metadata;
+
+// 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)]
+
+[assembly: XmlnsDefinition("http://prismlibrary.com/", "Prism.DryIoc")]
+
+[assembly: InternalsVisibleTo("Prism.DryIoc.Avalonia.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001008f34b619d7a39e44cebe5ccbd5607eaa0784c9c124431ba336a14e4fecd874f151b57163961505e76943910c7cabea9c7229edc3553dfc33ac7b269087e5cef9404bdb491907ffd9f9b26d737fa2c359620a2cbf2802f54118471d7c0ead3b95c916783dd4b99b9b1a0cd2785e1b5d614d3d9140a60c8c64c217e1c2b0ec8296")]
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..7fdd419bff
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.Designer.cs
@@ -0,0 +1,270 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace Prism.DryIoc.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ public class Resources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Prism.DryIoc.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ public static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to a.
+ ///
+ public static string a {
+ get {
+ return ResourceManager.GetString("a", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Bootstrapper sequence completed..
+ ///
+ public static string BootstrapperSequenceCompleted {
+ get {
+ return ResourceManager.GetString("BootstrapperSequenceCompleted", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Configuring default region behaviors..
+ ///
+ public static string ConfiguringDefaultRegionBehaviors {
+ get {
+ return ResourceManager.GetString("ConfiguringDefaultRegionBehaviors", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Configuring the DryIoc container..
+ ///
+ public static string ConfiguringDryIocContainer {
+ get {
+ return ResourceManager.GetString("ConfiguringDryIocContainer", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Configuring module catalog..
+ ///
+ public static string ConfiguringModuleCatalog {
+ get {
+ return ResourceManager.GetString("ConfiguringModuleCatalog", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Configuring region adapters..
+ ///
+ public static string ConfiguringRegionAdapters {
+ get {
+ return ResourceManager.GetString("ConfiguringRegionAdapters", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Configuring ServiceLocator singleton..
+ ///
+ public static string ConfiguringServiceLocatorSingleton {
+ get {
+ return ResourceManager.GetString("ConfiguringServiceLocatorSingleton", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Configuring the ViewModelLocator to use DryIoc..
+ ///
+ public static string ConfiguringViewModelLocator {
+ get {
+ return ResourceManager.GetString("ConfiguringViewModelLocator", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Creating DryIoc container..
+ ///
+ public static string CreatingDryIocContainer {
+ get {
+ return ResourceManager.GetString("CreatingDryIocContainer", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Creating module catalog..
+ ///
+ public static string CreatingModuleCatalog {
+ get {
+ return ResourceManager.GetString("CreatingModuleCatalog", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Creating the shell..
+ ///
+ public static string CreatingShell {
+ get {
+ return ResourceManager.GetString("CreatingShell", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Initializing modules..
+ ///
+ public static string InitializingModules {
+ get {
+ return ResourceManager.GetString("InitializingModules", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Initializing the shell..
+ ///
+ public static string InitializingShell {
+ get {
+ return ResourceManager.GetString("InitializingShell", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Logger was created successfully..
+ ///
+ public static string LoggerCreatedSuccessfully {
+ get {
+ return ResourceManager.GetString("LoggerCreatedSuccessfully", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The method 'GetModuleEnumerator' of the bootstrapper must be overwritten in order to use the default module initialization logic..
+ ///
+ public static string NotOverwrittenGetModuleEnumeratorException {
+ get {
+ return ResourceManager.GetString("NotOverwrittenGetModuleEnumeratorException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The ContainerBuilder is required and cannot be null..
+ ///
+ public static string NullDryIocContainerBuilderException {
+ get {
+ return ResourceManager.GetString("NullDryIocContainerBuilderException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The IContainer is required and cannot be null..
+ ///
+ public static string NullDryIocContainerException {
+ get {
+ return ResourceManager.GetString("NullDryIocContainerException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The ILoggerFacade is required and cannot be null..
+ ///
+ public static string NullLoggerFacadeException {
+ get {
+ return ResourceManager.GetString("NullLoggerFacadeException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The IModuleCatalog is required and cannot be null in order to initialize the modules..
+ ///
+ public static string NullModuleCatalogException {
+ get {
+ return ResourceManager.GetString("NullModuleCatalogException", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Registering Framework Exception Types..
+ ///
+ public static string RegisteringFrameworkExceptionTypes {
+ get {
+ return ResourceManager.GetString("RegisteringFrameworkExceptionTypes", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Setting the RegionManager..
+ ///
+ public static string SettingTheRegionManager {
+ get {
+ return ResourceManager.GetString("SettingTheRegionManager", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Type '{0}' was already registered by the application. Skipping....
+ ///
+ public static string TypeMappingAlreadyRegistered {
+ get {
+ return ResourceManager.GetString("TypeMappingAlreadyRegistered", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Updating Regions..
+ ///
+ public static string UpdatingRegions {
+ get {
+ return ResourceManager.GetString("UpdatingRegions", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx
new file mode 100644
index 0000000000..7ec61e1f8a
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/Properties/Resources.resx
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ a
+
+
+ Bootstrapper sequence completed.
+
+
+ Configuring default region behaviors.
+
+
+ Configuring the DryIoc container.
+
+
+ Configuring module catalog.
+
+
+ Configuring region adapters.
+
+
+ Configuring ServiceLocator singleton.
+
+
+ Configuring the ViewModelLocator to use DryIoc.
+
+
+ Creating DryIoc container.
+
+
+ Creating module catalog.
+
+
+ Creating the shell.
+
+
+ Initializing modules.
+
+
+ Initializing the shell.
+
+
+ Logger was created successfully.
+
+
+ The method 'GetModuleEnumerator' of the bootstrapper must be overwritten in order to use the default module initialization logic.
+
+
+ The ContainerBuilder is required and cannot be null.
+
+
+ The IContainer is required and cannot be null.
+
+
+ The ILoggerFacade is required and cannot be null.
+
+
+ The IModuleCatalog is required and cannot be null in order to initialize the modules.
+
+
+ Registering Framework Exception Types.
+
+
+ Setting the RegionManager.
+
+
+ Type '{0}' was already registered by the application. Skipping...
+
+
+ Updating Regions.
+
+
\ No newline at end of file
diff --git a/src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets b/src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets
new file mode 100644
index 0000000000..f85cca105a
--- /dev/null
+++ b/src/Avalonia/Prism.DryIoc.Avalonia/build/Package.targets
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/src/Avalonia/ReadMe.md b/src/Avalonia/ReadMe.md
new file mode 100644
index 0000000000..8c070b6c43
--- /dev/null
+++ b/src/Avalonia/ReadMe.md
@@ -0,0 +1,16 @@
+
+Thank you for installing Prism.Avalonia 8.1!
+
+## Help Support Prism
+
+While Prism.Avalonia is, and will continue to be totally free to download, Open Source is not free. If you or your company have the resources, please consider becoming a GitHub Sponsor. GitHub Sponsorships help to make Open Source Development more sustainable.
+
+## Support & FAQ
+
+1) Support: Prism is distributed under the MIT License this means "AS IS", WITHOUT WARRANTY OF ANY KIND. If you require code level support, architectural guidance, or custom builds of Prism, please reach out at https://github.com/AvaloniaCommunity/Prism.Avalonia to inquire about Enterprise support options.
+
+2) Community Support: GitHub issues are not the place to ask questions, these are reserved for feature requests and legitimate bug reports. You are free to ask questions on Stack Overflow however the Prism team does not monitor questions posted there. We do encourage you to post questions using GitHub Discussions on the main Prism repo. If you see a question that you know the answer to please pay it forward and help other developers that may just be starting out. https://github.com/AvaloniaCommunity/Prism.Avalonia/discussions
+
+3) Reporting Bugs: If you believe you have encountered a bug, please be sure to search the open and closed issues as you may find the issue has already been fixed as and is awaiting release. If you find that you have a new issue, please create a new project that focuses on ONLY the necessary steps to reproduce the issue you are facing. Issues that are opened which do not have a sample app reproducing the issue will be closed. In addition to this being required by the Prism team, this is just good etiquette for any Open Source project. Additionally we ask that you do not try to be an Archaeologist, trying to comment on PR's, commits and issues from long ago, if there is a legitimate issue, open a new issue referencing what you need to reference. Archaeologists will be ignored.
+
+4) Samples: Currently the Prism.Avalonia team maintains a wide variety of samples for Avalonia. If you would like to help give back, and help build out a sample(s) repo please contact the Prism.Avalonia team via the GitHub Discussions forum.
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs b/tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs
new file mode 100644
index 0000000000..4144973396
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/CollectionChangedTracker.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Prism.Avalonia.Tests
+{
+ public class CollectionChangedTracker
+ {
+ private readonly List eventList = new List();
+
+ public CollectionChangedTracker(INotifyCollectionChanged collection)
+ {
+ collection.CollectionChanged += OnCollectionChanged;
+ }
+
+ public IEnumerable ActionsFired { get { return this.eventList.Select(e => e.Action); } }
+ public IEnumerable NotifyEvents { get { return this.eventList; } }
+
+ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ this.eventList.Add(e);
+ }
+ }
+}
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs
new file mode 100644
index 0000000000..aecadd0bc5
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/CollectionExtensionsFixture.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using Xunit;
+
+namespace Prism.Avalonia.Tests
+{
+
+ public class CollectionExtensionsFixture
+ {
+ [Fact]
+ public void CanAddRangeToCollection()
+ {
+ Collection col = new Collection();
+ List itemsToAdd = new List { "1", "2" };
+
+ col.AddRange(itemsToAdd);
+
+ Assert.Equal(2, col.Count);
+ Assert.Equal("1", col[0]);
+ Assert.Equal("2", col[1]);
+ }
+ }
+}
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs b/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs
new file mode 100644
index 0000000000..2c2138ec0d
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/CompilerHelper.Desktop.cs
@@ -0,0 +1,191 @@
+using System;
+using System.CodeDom.Compiler;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using Microsoft.CSharp;
+using Prism.Ioc;
+using Prism.Modularity;
+using Xunit;
+
+namespace Prism.Avalonia.Tests
+{
+ public class CompilerHelper
+ {
+ private static string moduleTemplate =
+ @"using System;
+ using Prism.Ioc;
+ using Prism.Modularity;
+ namespace TestModules
+ {
+ #module#
+ public class #className#Class : IModule
+ {
+ public void OnInitialized(IContainerProvider containerProvider)
+ {
+ Console.WriteLine(""#className#.Start"");
+ }
+ public void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+
+ }
+ }
+ }";
+
+ public static Assembly CompileFileAndLoadAssembly(string input, string output, params string[] references)
+ {
+ return CompileFile(input, output, references).CompiledAssembly;
+ }
+
+ public static CompilerResults CompileFile(string input, string output, params string[] references)
+ {
+ CreateOutput(output);
+
+ List referencedAssemblies = new List(references.Length + 3);
+
+ referencedAssemblies.AddRange(references);
+ referencedAssemblies.Add("System.dll");
+ referencedAssemblies.Add(typeof(IContainerRegistry).Assembly.CodeBase.Replace(@"file:///", ""));
+ referencedAssemblies.Add(typeof(IModule).Assembly.CodeBase.Replace(@"file:///", ""));
+ referencedAssemblies.Add(typeof(ModuleAttribute).Assembly.CodeBase.Replace(@"file:///", ""));
+
+ CSharpCodeProvider codeProvider = new CSharpCodeProvider();
+ CompilerParameters cp = new CompilerParameters(referencedAssemblies.ToArray(), output);
+
+ using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(input);
+
+ if (stream == null)
+ {
+ throw new ArgumentException("input");
+ }
+
+ StreamReader reader = new StreamReader(stream);
+ string source = reader.ReadToEnd();
+ CompilerResults results = codeProvider.CompileAssemblyFromSource(cp, source);
+ ThrowIfCompilerError(results);
+ return results;
+ }
+
+ public static void CreateOutput(string output)
+ {
+ string dir = Path.GetDirectoryName(output);
+ if (!Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+ else
+ {
+ //Delete the file if exists
+ if (File.Exists(output))
+ {
+ try
+ {
+ File.Delete(output);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ //The file might be locked by Visual Studio, so rename it
+ if (File.Exists(output + ".locked"))
+ File.Delete(output + ".locked");
+ File.Move(output, output + ".locked");
+ }
+ }
+ }
+ }
+
+ public static CompilerResults CompileCode(string code, string output)
+ {
+ CreateOutput(output);
+ List referencedAssemblies = new List();
+ referencedAssemblies.Add("System.dll");
+ referencedAssemblies.Add(typeof(IContainerExtension).Assembly.CodeBase.Replace(@"file:///", ""));
+ referencedAssemblies.Add(typeof(IModule).Assembly.CodeBase.Replace(@"file:///", ""));
+ referencedAssemblies.Add(typeof(ModuleAttribute).Assembly.CodeBase.Replace(@"file:///", ""));
+
+ CompilerResults results = new CSharpCodeProvider().CompileAssemblyFromSource(
+ new CompilerParameters(referencedAssemblies.ToArray(), output), code);
+
+ ThrowIfCompilerError(results);
+
+ return results;
+ }
+
+ public static string GenerateDynamicModule(string assemblyName, string moduleName, string outpath, params string[] dependencies)
+ {
+ CreateOutput(outpath);
+
+ // Create temporary module.
+ string moduleCode = moduleTemplate.Replace("#className#", assemblyName);
+ if (!string.IsNullOrEmpty(moduleName))
+ {
+ moduleCode = moduleCode.Replace("#module#", String.Format("[Module(ModuleName = \"{0}\") #dependencies#]", moduleName));
+ }
+ else
+ {
+ moduleCode = moduleCode.Replace("#module#", "");
+ }
+
+ string depString = string.Empty;
+
+ foreach (string module in dependencies)
+ {
+ depString += String.Format(", ModuleDependency(\"{0}\")", module);
+ }
+
+ moduleCode = moduleCode.Replace("#dependencies#", depString);
+
+ CompileCode(moduleCode, outpath);
+
+ return outpath;
+ }
+
+ public static string GenerateDynamicModule(string assemblyName, string moduleName, params string[] dependencies)
+ {
+ string assemblyFile = assemblyName + ".dll";
+ string outpath = Path.Combine(assemblyName, assemblyFile);
+
+ return GenerateDynamicModule(assemblyName, moduleName, outpath, dependencies);
+ }
+
+ public static void ThrowIfCompilerError(CompilerResults results)
+ {
+ if (results.Errors.HasErrors)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine("Compilation failed.");
+ foreach (CompilerError error in results.Errors)
+ {
+ sb.AppendLine(error.ToString());
+ }
+
+ Assert.False(results.Errors.HasErrors, sb.ToString());
+ }
+ }
+
+ public static void CleanUpDirectory(string path)
+ {
+ if (!Directory.Exists(path))
+ {
+ Directory.CreateDirectory(path);
+ }
+ else
+ {
+ foreach (string file in Directory.GetFiles(path))
+ {
+ try
+ {
+ File.Delete(file);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ //The file might be locked by Visual Studio, so rename it
+ if (File.Exists(file + ".locked"))
+ File.Delete(file + ".locked");
+ File.Move(file, file + ".locked");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs b/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs
new file mode 100644
index 0000000000..0d3e8b1112
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/ExceptionAssert.cs
@@ -0,0 +1,28 @@
+using Xunit;
+
+namespace Prism.Avalonia.Tests
+{
+ public static class ExceptionAssert
+ {
+ public static void Throws(Action action)
+ where TException : Exception
+ {
+ Throws(typeof(TException), action);
+ }
+
+ public static void Throws(Type expectedExceptionType, Action action)
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception ex)
+ {
+ Assert.IsType(expectedExceptionType, ex);
+ return;
+ }
+
+ //Assert.Fail("No exception thrown. Expected exception type of {0}.", expectedExceptionType.Name);
+ }
+ }
+}
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs
new file mode 100644
index 0000000000..633b3fdfda
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/CommandBehaviorBaseFixture.cs
@@ -0,0 +1,169 @@
+using System.Windows.Input;
+using Avalonia.Controls;
+using Prism.Interactivity;
+using Xunit;
+
+namespace Prism.Avalonia.Tests.Interactivity
+{
+
+ public class CommandBehaviorBaseFixture
+ {
+ [Fact]
+ public void ExecuteUsesCommandParameterWhenSet()
+ {
+ var targetUIElement = new Control();
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ target.CommandParameter = "123";
+ TestCommand testCommand = new TestCommand();
+ target.Command = testCommand;
+
+ target.ExecuteCommand("testparam");
+
+ Assert.Equal("123", testCommand.ExecuteCalledWithParameter);
+ }
+
+ [Fact]
+ public void ExecuteUsesParameterWhenCommandParameterNotSet()
+ {
+ var targetUIElement = new Control();
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ TestCommand testCommand = new TestCommand();
+ target.Command = testCommand;
+
+ target.ExecuteCommand("testparam");
+
+ Assert.Equal("testparam", testCommand.ExecuteCalledWithParameter);
+ }
+
+ [Fact]
+ public void CommandBehaviorBaseAllowsDisableByDefault()
+ {
+ var targetUIElement = new Control();
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+
+ Assert.True(target.AutoEnable);
+ }
+
+ [StaFact]
+ public void CommandBehaviorBaseEnablesUIElement()
+ {
+ var targetUIElement = new Control();
+ targetUIElement.IsEnabled = false;
+
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ TestCommand testCommand = new TestCommand();
+ target.Command = testCommand;
+ target.ExecuteCommand(null);
+
+ Assert.True(targetUIElement.IsEnabled);
+ }
+
+ [StaFact]
+ public void CommandBehaviorBaseDisablesUIElement()
+ {
+ var targetUIElement = new Control();
+ targetUIElement.IsEnabled = true;
+
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ TestCommand testCommand = new TestCommand();
+ testCommand.CanExecuteResult = false;
+ target.Command = testCommand;
+ target.ExecuteCommand(null);
+
+ Assert.False(targetUIElement.IsEnabled);
+ }
+
+ [StaFact]
+ public void WhenAutoEnableIsFalse_ThenDisabledUIElementRemainsDisabled()
+ {
+ var targetUIElement = new Control();
+ targetUIElement.IsEnabled = false;
+
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ target.AutoEnable = false;
+ TestCommand testCommand = new TestCommand();
+ target.Command = testCommand;
+ target.ExecuteCommand(null);
+
+ Assert.False(targetUIElement.IsEnabled);
+ }
+
+ [StaFact]
+ public void WhenAutoEnableIsUpdated_ThenDisabledUIElementIsEnabled()
+ {
+ var targetUIElement = new Control();
+ targetUIElement.IsEnabled = false;
+
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ target.AutoEnable = false;
+ TestCommand testCommand = new TestCommand();
+ target.Command = testCommand;
+ target.ExecuteCommand(null);
+
+ Assert.False(targetUIElement.IsEnabled);
+
+ target.AutoEnable = true;
+
+ Assert.True(targetUIElement.IsEnabled);
+ }
+
+ [StaFact]
+ public void WhenAutoEnableIsUpdated_ThenEnabledUIElementIsDisabled()
+ {
+ var targetUIElement = new Control();
+ targetUIElement.IsEnabled = true;
+
+ var target = new TestableCommandBehaviorBase(targetUIElement);
+ target.AutoEnable = false;
+ TestCommand testCommand = new TestCommand();
+ testCommand.CanExecuteResult = false;
+ target.Command = testCommand;
+ target.ExecuteCommand(null);
+
+ Assert.True(targetUIElement.IsEnabled);
+
+ target.AutoEnable = true;
+
+ Assert.False(targetUIElement.IsEnabled);
+ }
+ }
+
+ class TestableCommandBehaviorBase : CommandBehaviorBase
+ {
+ public TestableCommandBehaviorBase(Control targetObject)
+ : base(targetObject)
+ { }
+
+ public new void ExecuteCommand(object parameter)
+ {
+ base.ExecuteCommand(parameter);
+ }
+ }
+
+ class TestCommand : ICommand
+ {
+ bool _canExecte = true;
+ public bool CanExecuteResult
+ {
+ get { return _canExecte; }
+ set { _canExecte = value; }
+ }
+
+ public object CanExecuteCalledWithParameter { get; set; }
+
+ public bool CanExecute(object parameter)
+ {
+ CanExecuteCalledWithParameter = parameter;
+ return CanExecuteResult;
+ }
+
+ public event EventHandler CanExecuteChanged;
+
+ public object ExecuteCalledWithParameter { get; set; }
+ public void Execute(object parameter)
+ {
+ ExecuteCalledWithParameter = parameter;
+ }
+ }
+
+}
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs
new file mode 100644
index 0000000000..1eef597648
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/Interactivity/InvokeCommandActionFixture.cs
@@ -0,0 +1,383 @@
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Prism.Avalonia.Tests.Mocks;
+using Xunit;
+
+namespace Prism.Avalonia.Tests.Interactivity
+{
+ // Override Prism.Interactivity.InvokeCommandAction until it can be implemented.
+ //// Reference:
+ //// https://github.com/wieslawsoltes/AvaloniaBehaviors/blob/master/src/Avalonia.Xaml.Interactions/Core/InvokeCommandAction.cs
+ public class InvokeCommandAction : AvaloniaObject
+ {
+ ///
+ /// Dependency property identifying if the associated element should automatically be enabled or disabled based on the result of the Command's CanExecute
+ ///
+ public static readonly StyledProperty AutoEnableProperty =
+ AvaloniaProperty.Register(nameof(AutoEnable));
+
+ /// Identifies the avalonia property.
+ public static readonly StyledProperty CommandProperty =
+ AvaloniaProperty.Register(nameof(Command));
+
+ /// Identifies the avalonia property.
+ public static readonly StyledProperty CommandParameterProperty =
+ AvaloniaProperty.Register(nameof(CommandParameter));
+
+ ///
+ /// Dependency property identifying the TriggerParameterPath to be parsed to identify the child property of the trigger parameter to be used as the command parameter.
+ ///
+ public static readonly StyledProperty TriggerParameterPathProperty =
+ AvaloniaProperty.Register(nameof(TriggerParameterPath));
+
+ ///
+ /// Gets or sets whether or not the associated element will automatically be enabled or disabled based on the result of the commands CanExecute
+ ///
+ public bool AutoEnable
+ {
+ get { return (bool)this.GetValue(AutoEnableProperty); }
+ set { this.SetValue(AutoEnableProperty, value); }
+ }
+
+ public ICommand? Command { get; set; }
+
+ public object? CommandParameter
+ {
+ get => GetValue(CommandParameterProperty);
+ set => SetValue(CommandParameterProperty, value);
+ }
+
+ public string TriggerParameterPath
+ {
+ get => GetValue(TriggerParameterPathProperty) as string;
+ set => SetValue(TriggerParameterPathProperty, value);
+ }
+
+ public void Attach(Control? ctrl)
+ { }
+
+ public void Detach()
+ { }
+
+ public void InvokeAction(object? action)
+ { }
+ }
+
+ public class InvokeCommandActionFixture
+ {
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCommandPropertyIsSet_ThenHooksUpCommandBehavior()
+ {
+ var someControl = new TextBox();
+ var commandAction = new InvokeCommandAction();
+ var command = new MockCommand();
+ commandAction.Attach(someControl);
+ commandAction.Command = command;
+
+ Assert.False(command.ExecuteCalled);
+
+ commandAction.InvokeAction(null);
+
+ Assert.True(command.ExecuteCalled);
+ Assert.Same(command, commandAction.GetValue(InvokeCommandAction.CommandProperty));
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAttachedAfterCommandPropertyIsSetAndInvoked_ThenInvokesCommand()
+ {
+ var someControl = new TextBox();
+ var commandAction = new InvokeCommandAction();
+ var command = new MockCommand();
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.False(command.ExecuteCalled);
+
+ commandAction.InvokeAction(null);
+
+ Assert.True(command.ExecuteCalled);
+ Assert.Same(command, commandAction.GetValue(InvokeCommandAction.CommandProperty));
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenChangingProperty_ThenUpdatesCommand()
+ {
+ var someControl = new TextBox();
+ var oldCommand = new MockCommand();
+ var newCommand = new MockCommand();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Attach(someControl);
+ commandAction.Command = oldCommand;
+ commandAction.Command = newCommand;
+ commandAction.InvokeAction(null);
+
+ Assert.True(newCommand.ExecuteCalled);
+ Assert.False(oldCommand.ExecuteCalled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenInvokedWithCommandParameter_ThenPassesCommandParaeterToExecute()
+ {
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Attach(someControl);
+ commandAction.Command = command;
+ commandAction.CommandParameter = parameter;
+
+ Assert.Null(command.ExecuteParameter);
+
+ commandAction.InvokeAction(null);
+
+ Assert.True(command.ExecuteCalled);
+ Assert.NotNull(command.ExecuteParameter);
+ Assert.Same(parameter, command.ExecuteParameter);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCommandParameterChanged_ThenUpdatesIsEnabledState()
+ {
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Attach(someControl);
+ commandAction.Command = command;
+
+ Assert.Null(command.CanExecuteParameter);
+ Assert.True(someControl.IsEnabled);
+
+ command.CanExecuteReturnValue = false;
+ commandAction.CommandParameter = parameter;
+
+ Assert.NotNull(command.CanExecuteParameter);
+ Assert.Same(parameter, command.CanExecuteParameter);
+ Assert.False(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCanExecuteChanged_ThenUpdatesIsEnabledState()
+ {
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Attach(someControl);
+ commandAction.Command = command;
+ commandAction.CommandParameter = parameter;
+
+ Assert.True(someControl.IsEnabled);
+
+ command.CanExecuteReturnValue = false;
+ command.RaiseCanExecuteChanged();
+
+ Assert.NotNull(command.CanExecuteParameter);
+ Assert.Same(parameter, command.CanExecuteParameter);
+ Assert.False(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenDetatched_ThenSetsCommandAndCommandParameterToNull()
+ {
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Attach(someControl);
+ commandAction.Command = command;
+ commandAction.CommandParameter = parameter;
+
+ Assert.NotNull(commandAction.Command);
+ Assert.NotNull(commandAction.CommandParameter);
+
+ commandAction.Detach();
+
+ Assert.Null(commandAction.Command);
+ Assert.Null(commandAction.CommandParameter);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCommandIsSetAndThenBehaviorIsAttached_ThenCommandsCanExecuteIsCalledOnce()
+ {
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.Equal(1, command.CanExecuteTimesCalled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCommandAndCommandParameterAreSetPriorToBehaviorBeingAttached_ThenCommandIsExecutedCorrectlyOnInvoke()
+ {
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Command = command;
+ commandAction.CommandParameter = parameter;
+ commandAction.Attach(someControl);
+
+ commandAction.InvokeAction(null);
+
+ Assert.True(command.ExecuteCalled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCommandParameterNotSet_ThenEventArgsPassed()
+ {
+ var eventArgs = new TestEventArgs(null);
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ commandAction.InvokeAction(eventArgs);
+
+ Assert.IsType(command.ExecuteParameter);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenCommandParameterNotSetAndEventArgsParameterPathSet_ThenPathedValuePassed()
+ {
+ var eventArgs = new TestEventArgs("testname");
+ var someControl = new TextBox();
+ var command = new MockCommand();
+ var parameter = new object();
+ var commandAction = new InvokeCommandAction();
+ commandAction.Command = command;
+ commandAction.TriggerParameterPath = "Thing1.Thing2.Name";
+ commandAction.Attach(someControl);
+
+ commandAction.InvokeAction(eventArgs);
+
+ Assert.Equal("testname", command.ExecuteParameter);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAttachedAndCanExecuteReturnsTrue_ThenDisabledUIElementIsEnabled()
+ {
+ var someControl = new TextBox();
+ someControl.IsEnabled = false;
+
+ var command = new MockCommand();
+ command.CanExecuteReturnValue = true;
+ var commandAction = new InvokeCommandAction();
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.True(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAttachedAndCanExecuteReturnsFalse_ThenEnabledUIElementIsDisabled()
+ {
+ var someControl = new TextBox();
+ someControl.IsEnabled = true;
+
+ var command = new MockCommand();
+ command.CanExecuteReturnValue = false;
+ var commandAction = new InvokeCommandAction();
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.False(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAutoEnableIsFalse_ThenDisabledUIElementRemainsDisabled()
+ {
+ var someControl = new TextBox();
+ someControl.IsEnabled = false;
+
+ var command = new MockCommand();
+ command.CanExecuteReturnValue = true;
+ var commandAction = new InvokeCommandAction();
+ commandAction.AutoEnable = false;
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.False(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAutoEnableIsFalse_ThenEnabledUIElementRemainsEnabled()
+ {
+ var someControl = new TextBox();
+ someControl.IsEnabled = true;
+
+ var command = new MockCommand();
+ command.CanExecuteReturnValue = false;
+ var commandAction = new InvokeCommandAction();
+ commandAction.AutoEnable = false;
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.True(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAutoEnableIsUpdated_ThenDisabledUIElementIsEnabled()
+ {
+ var someControl = new TextBox();
+ someControl.IsEnabled = false;
+
+ var command = new MockCommand();
+ var commandAction = new InvokeCommandAction();
+ commandAction.AutoEnable = false;
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.False(someControl.IsEnabled);
+
+ commandAction.AutoEnable = true;
+
+ Assert.True(someControl.IsEnabled);
+ }
+
+ [StaFact(Skip = "InvokeCommandAction is not implmented.")]
+ public void WhenAutoEnableIsUpdated_ThenEnabledUIElementIsDisabled()
+ {
+ var someControl = new TextBox();
+ someControl.IsEnabled = true;
+
+ var command = new MockCommand();
+ command.CanExecuteReturnValue = false;
+ var commandAction = new InvokeCommandAction();
+ commandAction.AutoEnable = false;
+ commandAction.Command = command;
+ commandAction.Attach(someControl);
+
+ Assert.True(someControl.IsEnabled);
+
+ commandAction.AutoEnable = true;
+
+ Assert.False(someControl.IsEnabled);
+ }
+ }
+
+ internal class TestEventArgs : EventArgs
+ {
+ public TestEventArgs(string name)
+ {
+ this.Thing1 = new Thing1 { Thing2 = new Thing2 { Name = name } };
+ }
+
+ public Thing1 Thing1 { get; set; }
+ }
+
+ internal class Thing1
+ {
+ public Thing2 Thing2 { get; set; }
+ }
+
+ internal class Thing2
+ {
+ public string Name { get; set; }
+ }
+}
diff --git a/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs b/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs
new file mode 100644
index 0000000000..af44c38436
--- /dev/null
+++ b/tests/Avalonia/Prism.Avalonia.Tests/ListDictionaryFixture.cs
@@ -0,0 +1,260 @@
+using Prism.Common;
+using Xunit;
+
+namespace Prism.Avalonia.Tests
+{
+ public class ListDictionaryFixture
+ {
+ static ListDictionary list;
+
+ public ListDictionaryFixture()
+ {
+ list = new ListDictionary();
+ }
+
+ [Fact]
+ public void AddThrowsIfKeyNull()
+ {
+ var ex = Assert.Throws(() =>
+ {
+ list.Add(null, new object());
+ });
+
+ }
+
+ [Fact]
+ public void AddThrowsIfValueNull()
+ {
+ var ex = Assert.Throws(() =>
+ {
+ list.Add("", null);
+ });
+
+ }
+
+ [Fact]
+ public void CanAddValue()
+ {
+ object value1 = new object();
+ object value2 = new object();
+
+ list.Add("foo", value1);
+ list.Add("foo", value2);
+
+ Assert.Equal(2, list["foo"].Count);
+ Assert.Same(value1, list["foo"][0]);
+ Assert.Same(value2, list["foo"][1]);
+ }
+
+ [Fact]
+ public void CanIndexValuesByKey()
+ {
+ list.Add("foo", new object());
+ list.Add("foo", new object());
+
+ Assert.Equal(2, list["foo"].Count);
+ }
+
+ [Fact]
+ public void ThrowsIfRemoveKeyNull()
+ {
+ var ex = Assert.Throws