From 33804c0f5b93f1e4e7a23def8378d4a699ab8205 Mon Sep 17 00:00:00 2001 From: Ken Tucker Date: Sun, 8 May 2022 10:14:21 -0400 Subject: [PATCH 001/135] Add avalonia ui to current code base --- .../Resources/Resource.Designer.cs | 2 +- samples/setup/Setup.Avalonia/App.axaml | 8 + samples/setup/Setup.Avalonia/App.axaml.cs | 20 ++ samples/setup/Setup.Avalonia/Bootstrapper.cs | 15 + samples/setup/Setup.Avalonia/Program.cs | 22 ++ .../Setup.Avalonia/Setup.Avalonia.csproj | 23 ++ .../ViewModels/ShellViewModel.cs | 17 + .../Setup.Avalonia/Views/ShellView.axaml | 9 + .../Setup.Avalonia/Views/ShellView.axaml.cs | 24 ++ samples/setup/Setup.Avalonia/nuget.config | 11 + samples/setup/Setup.Avalonia/readme.md | 20 ++ .../Resources/Resource.designer.cs | 2 +- samples/setup/Setup.sln | 58 ++++ .../Caliburn.Micro.Avalonia.csproj | 29 ++ .../netcore-avalonia/AttachedCollection.cs | 93 ++++++ .../netcore-avalonia/BootstrapperBase.cs | 187 +++++++++++ .../netcore-avalonia/IWindowManager.cs | 49 +++ .../Platforms/netcore-avalonia/Parameter.cs | 85 +++++ .../netcore-avalonia/TriggerAction.cs | 117 +++++++ .../netcore-avalonia/WindowConductor.cs | 121 +++++++ .../netcore-avalonia/WindowManager.cs | 303 ++++++++++++++++++ .../Caliburn.Micro.Core.csproj | 2 +- .../Caliburn.Micro.Platform.Core.csproj | 2 +- src/Caliburn.Micro.Platform/Action.cs | 29 +- .../ActionExecutionContext.cs | 3 + src/Caliburn.Micro.Platform/ActionMessage.cs | 254 +++++++++++---- src/Caliburn.Micro.Platform/Bind.cs | 62 +++- src/Caliburn.Micro.Platform/BindingScope.cs | 40 ++- src/Caliburn.Micro.Platform/ChildResolver.cs | 3 + .../ConventionManager.cs | 169 ++++++++-- .../DependencyPropertyHelper.cs | 2 + .../ElementConvention.cs | 5 + src/Caliburn.Micro.Platform/Message.cs | 43 ++- src/Caliburn.Micro.Platform/Parser.cs | 20 +- .../Platforms/Xamarin.Forms/ActionMessage.cs | 4 +- .../Xamarin.Forms/ConventionManager.cs | 4 +- src/Caliburn.Micro.Platform/View.cs | 198 +++++++++--- src/Caliburn.Micro.Platform/ViewLocator.cs | 16 +- .../ViewModelBinder.cs | 10 + .../ViewModelLocator.cs | 6 + .../XamlPlatformProvider.cs | 25 +- src/Caliburn.Micro.sln | 20 +- src/global.json | 2 +- 43 files changed, 1963 insertions(+), 171 deletions(-) create mode 100644 samples/setup/Setup.Avalonia/App.axaml create mode 100644 samples/setup/Setup.Avalonia/App.axaml.cs create mode 100644 samples/setup/Setup.Avalonia/Bootstrapper.cs create mode 100644 samples/setup/Setup.Avalonia/Program.cs create mode 100644 samples/setup/Setup.Avalonia/Setup.Avalonia.csproj create mode 100644 samples/setup/Setup.Avalonia/ViewModels/ShellViewModel.cs create mode 100644 samples/setup/Setup.Avalonia/Views/ShellView.axaml create mode 100644 samples/setup/Setup.Avalonia/Views/ShellView.axaml.cs create mode 100644 samples/setup/Setup.Avalonia/nuget.config create mode 100644 samples/setup/Setup.Avalonia/readme.md create mode 100644 src/Caliburn.Micro.Avalonia/Caliburn.Micro.Avalonia.csproj create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/AttachedCollection.cs create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/BootstrapperBase.cs create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/IWindowManager.cs create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/Parameter.cs create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/TriggerAction.cs create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowConductor.cs create mode 100644 src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowManager.cs diff --git a/samples/setup/Setup.Android/Resources/Resource.Designer.cs b/samples/setup/Setup.Android/Resources/Resource.Designer.cs index 99f53cdfa..55c64bcf9 100644 --- a/samples/setup/Setup.Android/Resources/Resource.Designer.cs +++ b/samples/setup/Setup.Android/Resources/Resource.Designer.cs @@ -14,7 +14,7 @@ namespace Setup.Android { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.1.0.11")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.2.4.160")] public partial class Resource { diff --git a/samples/setup/Setup.Avalonia/App.axaml b/samples/setup/Setup.Avalonia/App.axaml new file mode 100644 index 000000000..1964e8093 --- /dev/null +++ b/samples/setup/Setup.Avalonia/App.axaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/samples/setup/Setup.Avalonia/App.axaml.cs b/samples/setup/Setup.Avalonia/App.axaml.cs new file mode 100644 index 000000000..ec44b8965 --- /dev/null +++ b/samples/setup/Setup.Avalonia/App.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace Setup.Avalonia +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + base.OnFrameworkInitializationCompleted(); + new Bootstrapper(); + } + } +} diff --git a/samples/setup/Setup.Avalonia/Bootstrapper.cs b/samples/setup/Setup.Avalonia/Bootstrapper.cs new file mode 100644 index 000000000..83ac928b4 --- /dev/null +++ b/samples/setup/Setup.Avalonia/Bootstrapper.cs @@ -0,0 +1,15 @@ +using Caliburn.Micro; +using Setup.Avalonia.ViewModels; + +namespace Setup.Avalonia +{ + public class Bootstrapper : BootstrapperBase + { + public Bootstrapper() + { + Initialize(); + + DisplayRootViewFor(); + } + } +} diff --git a/samples/setup/Setup.Avalonia/Program.cs b/samples/setup/Setup.Avalonia/Program.cs new file mode 100644 index 000000000..95cadb603 --- /dev/null +++ b/samples/setup/Setup.Avalonia/Program.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using System; + +namespace Setup.Avalonia +{ + class Program + { + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/setup/Setup.Avalonia/Setup.Avalonia.csproj b/samples/setup/Setup.Avalonia/Setup.Avalonia.csproj new file mode 100644 index 000000000..4233440a2 --- /dev/null +++ b/samples/setup/Setup.Avalonia/Setup.Avalonia.csproj @@ -0,0 +1,23 @@ + + + WinExe + net6.0 + enable + + + + + + + + + + + + ..\..\..\bin\Caliburn.Micro.Avalonia\Release\net6.0\Caliburn.Micro.Avalonia.dll + + + ..\..\..\bin\Caliburn.Micro.Avalonia\Release\net6.0\Caliburn.Micro.Core.dll + + + diff --git a/samples/setup/Setup.Avalonia/ViewModels/ShellViewModel.cs b/samples/setup/Setup.Avalonia/ViewModels/ShellViewModel.cs new file mode 100644 index 000000000..4c0aaa077 --- /dev/null +++ b/samples/setup/Setup.Avalonia/ViewModels/ShellViewModel.cs @@ -0,0 +1,17 @@ +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls.Chrome; +using Caliburn.Micro; + +namespace Setup.Avalonia.ViewModels +{ + public class ShellViewModel : Screen + { + protected override async Task OnInitializeAsync(CancellationToken cancellationToken) + { + await base.OnInitializeAsync(cancellationToken); + + DisplayName = "Welcome to Caliburn.Micro.Avalonia!"; + } + } +} diff --git a/samples/setup/Setup.Avalonia/Views/ShellView.axaml b/samples/setup/Setup.Avalonia/Views/ShellView.axaml new file mode 100644 index 000000000..9621802a3 --- /dev/null +++ b/samples/setup/Setup.Avalonia/Views/ShellView.axaml @@ -0,0 +1,9 @@ + + + diff --git a/samples/setup/Setup.Avalonia/Views/ShellView.axaml.cs b/samples/setup/Setup.Avalonia/Views/ShellView.axaml.cs new file mode 100644 index 000000000..2facbaf8c --- /dev/null +++ b/samples/setup/Setup.Avalonia/Views/ShellView.axaml.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Caliburn.Micro; + +namespace Setup.Avalonia.Views +{ + public class ShellView : Window + { + public ShellView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/setup/Setup.Avalonia/nuget.config b/samples/setup/Setup.Avalonia/nuget.config new file mode 100644 index 000000000..6c273ab3d --- /dev/null +++ b/samples/setup/Setup.Avalonia/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/samples/setup/Setup.Avalonia/readme.md b/samples/setup/Setup.Avalonia/readme.md new file mode 100644 index 000000000..88a3f69a4 --- /dev/null +++ b/samples/setup/Setup.Avalonia/readme.md @@ -0,0 +1,20 @@ +# Setting up a new Avalonia project + +1. File > New Project > `New Avalonia Application`. +2. Add nuget package `Caliburn.Micro.Avalonia` (or add references to `Caliburn.Micro.Core.dll` and `Caliburn.Micro.Avalonia.dll` from the `Caliburn.Micro.Avalonia` project) +3. Add nuget packages `Avalonia.Markup.Xaml.Loader`, `Avalonia.Xaml.Interactivity` and `Avalonia.Xaml.Behaviors`. +4. Delete `MainWindow.axaml`. +5. Create the `Views` and `ViewModels` folders. +6. Add new class `ShellViewModel` to the `ViewModels` folder. +7. Add new window `ShellView` to the `Views` folder. +8. Add new class `Bootstrapper` to the root folder. +9. Replace body of `OnFrameworkInitializationCompleted` method of `Application` class in `App.axaml.cs` to `new Bootstrapper();` + +## Troubleshooting: + +### Main window doesn't appear + +Make sure you have added the following nuget packages: +* `Avalonia.Markup.Xaml.Loader` +* `Avalonia.Xaml.Interactivity` +* `Avalonia.Xaml.Behaviors` diff --git a/samples/setup/Setup.Forms5/Setup.Forms5/Setup.Forms5.Android/Resources/Resource.designer.cs b/samples/setup/Setup.Forms5/Setup.Forms5/Setup.Forms5.Android/Resources/Resource.designer.cs index 4b786272f..15005cb1e 100644 --- a/samples/setup/Setup.Forms5/Setup.Forms5/Setup.Forms5.Android/Resources/Resource.designer.cs +++ b/samples/setup/Setup.Forms5/Setup.Forms5/Setup.Forms5.Android/Resources/Resource.designer.cs @@ -14,7 +14,7 @@ namespace Setup.Forms5.Droid { - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.1.0.11")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Android.Build.Tasks", "12.2.4.160")] public partial class Resource { diff --git a/samples/setup/Setup.sln b/samples/setup/Setup.sln index 8c50a8155..a0af1b3b0 100644 --- a/samples/setup/Setup.sln +++ b/samples/setup/Setup.sln @@ -33,6 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "VB.Net", "VB.Net", "{D5FC43 EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "Setup.UWP.VB", "Setup.UWP.VB\Setup.UWP.VB.vbproj", "{434D2C94-81BB-4EB4-9863-6042D4087C54}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Setup.Avalonia", "Setup.Avalonia\Setup.Avalonia.csproj", "{A6893278-0BD0-4134-831B-7A0384581054}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -823,6 +825,62 @@ Global {434D2C94-81BB-4EB4-9863-6042D4087C54}.Release|x86.ActiveCfg = Release|x86 {434D2C94-81BB-4EB4-9863-6042D4087C54}.Release|x86.Build.0 = Release|x86 {434D2C94-81BB-4EB4-9863-6042D4087C54}.Release|x86.Deploy.0 = Release|x86 + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|ARM.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|ARM64.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|ARM64.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|iPhone.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|x64.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|x64.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|x86.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.AppStore|x86.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|ARM.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|ARM.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|ARM64.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|iPhone.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|x64.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Debug|x86.Build.0 = Debug|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|Any CPU.Build.0 = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|ARM.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|ARM.Build.0 = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|ARM64.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|ARM64.Build.0 = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|iPhone.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|iPhone.Build.0 = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|x64.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|x64.Build.0 = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|x86.ActiveCfg = Release|Any CPU + {A6893278-0BD0-4134-831B-7A0384581054}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Caliburn.Micro.Avalonia/Caliburn.Micro.Avalonia.csproj b/src/Caliburn.Micro.Avalonia/Caliburn.Micro.Avalonia.csproj new file mode 100644 index 000000000..9b3f25fc8 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Caliburn.Micro.Avalonia.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0;netcoreapp3.1;net6.0 + Caliburn.Micro + Caliburn.Micro + Caliburn.Micro + .\..\Caliburn.Micro.snk + true + + + + AVALONIA + + + + + + + + + + + + + + + + diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/AttachedCollection.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/AttachedCollection.cs new file mode 100644 index 000000000..7658c74b0 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/AttachedCollection.cs @@ -0,0 +1,93 @@ +using System.Collections.Specialized; +using System.Linq; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Xaml.Interactivity; + +namespace Caliburn.Micro +{ + /// + /// A collection that can exist as part of a behavior. + /// + /// The type of item in the attached collection. + public class AttachedCollection : AvaloniaList, IBehavior + where T : AvaloniaObject, IBehavior + { + IAvaloniaObject associatedObject; + + /// + /// Creates an instance of + /// + public AttachedCollection() + { + ((INotifyCollectionChanged)this).CollectionChanged += OnCollectionChanged; + } + + /// + /// Attached the collection. + /// + /// The dependency object to attach the collection to. + public void Attach(IAvaloniaObject AvaloniaObject) + { + associatedObject = AvaloniaObject; + + this.Apply(x => x.Attach(associatedObject)); + } + + /// + /// Detaches the collection. + /// + public void Detach() + { + this.Apply(x => x.Detach()); + + associatedObject = null; + } + + IAvaloniaObject IBehavior.AssociatedObject + { + get { return associatedObject; } + } + + /// + /// Called when an item is added from the collection. + /// + /// The item that was added. + protected virtual void OnItemAdded(T item) + { + if (associatedObject != null) + item.Attach(associatedObject); + } + + /// + /// Called when an item is removed from the collection. + /// + /// The item that was removed. + protected virtual void OnItemRemoved(T item) + { + if (item.AssociatedObject != null) + item.Detach(); + } + + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + e.NewItems.OfType().Where(x => !Contains(x)).Apply(OnItemAdded); + break; + case NotifyCollectionChangedAction.Remove: + e.OldItems.OfType().Apply(OnItemRemoved); + break; + case NotifyCollectionChangedAction.Replace: + e.OldItems.OfType().Apply(OnItemRemoved); + e.NewItems.OfType().Where(x => !Contains(x)).Apply(OnItemAdded); + break; + case NotifyCollectionChangedAction.Reset: + this.Apply(OnItemRemoved); + this.Apply(OnItemAdded); + break; + } + } + } +} diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/BootstrapperBase.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/BootstrapperBase.cs new file mode 100644 index 000000000..ab2901127 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/BootstrapperBase.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Caliburn.Micro; + +namespace Caliburn.Micro +{ + public class BootstrapperBase + { + bool isInitialized; + + /// + /// The application. + /// + protected Application Application { get; set; } + + /// + /// Initialize the framework. + /// + public void Initialize() + { + if (isInitialized) + { + return; + } + + isInitialized = true; + + PlatformProvider.Current = new XamlPlatformProvider(); + + + var baseExtractTypes = AssemblySourceCache.ExtractTypes; + + AssemblySourceCache.ExtractTypes = assembly => + { + var baseTypes = baseExtractTypes(assembly); + var elementTypes = assembly.GetExportedTypes() + .Where(t => typeof(Control).IsAssignableFrom(t)); + + return baseTypes.Union(elementTypes); + }; + + AssemblySource.Instance.Refresh(); + + if (Execute.InDesignMode) + { + try + { + StartDesignTime(); + } + catch + { + //if something fails at design-time, there's really nothing we can do... + isInitialized = false; + throw; + } + } + else + { + StartRuntime(); + } + } + + /// + /// Called by the bootstrapper's constructor at design time to start the framework. + /// + protected virtual void StartDesignTime() + { + AssemblySource.Instance.Clear(); + AssemblySource.AddRange(SelectAssemblies()); + + Configure(); + IoC.GetInstance = GetInstance; + IoC.GetAllInstances = GetAllInstances; + IoC.BuildUp = BuildUp; + } + + /// + /// Called by the bootstrapper's constructor at runtime to start the framework. + /// + protected virtual void StartRuntime() + { + AssemblySourceCache.Install(); + AssemblySource.AddRange(SelectAssemblies()); + + Application = Application.Current; + PrepareApplication(); + + Configure(); + IoC.GetInstance = GetInstance; + IoC.GetAllInstances = GetAllInstances; + IoC.BuildUp = BuildUp; + } + + /// + /// Provides an opportunity to hook into the application object. + /// + protected virtual void PrepareApplication() + { + //TODO: (Avalonia) Implement application events when Avalonia supports them + + //Application.Startup += OnStartup; + + //Aplication.DispatcherUnhandledException += OnUnhandledException; + + //Application.Exit += OnExit; + } + + /// + /// Override to configure the framework and setup your IoC container. + /// + protected virtual void Configure() + { + } + + /// + /// Override to tell the framework where to find assemblies to inspect for views, etc. + /// + /// A list of assemblies to inspect. + protected virtual IEnumerable SelectAssemblies() + { + return new[] { GetType().Assembly }; + } + + /// + /// Override this to provide an IoC specific implementation. + /// + /// The service to locate. + /// The key to locate. + /// The located service. + protected virtual object GetInstance(Type service, string key) + { + if (service == typeof(IWindowManager)) + service = typeof(WindowManager); + + return Activator.CreateInstance(service); + } + + /// + /// Override this to provide an IoC specific implementation + /// + /// The service to locate. + /// The located services. + protected virtual IEnumerable GetAllInstances(Type service) + { + return new[] { Activator.CreateInstance(service) }; + } + + /// + /// Override this to provide an IoC specific implementation. + /// + /// The instance to perform injection on. + protected virtual void BuildUp(object instance) + { + } + + /// + /// Locates the view model, locates the associate view, binds them and shows it as the root view. + /// + /// The view model type. + /// The optional window settings. + protected async Task DisplayRootViewForAsync(Type viewModelType, IDictionary settings = null) + { + var windowManager = IoC.Get(); + var window = await windowManager.CreateWindowAsync(IoC.GetInstance(viewModelType, null), false, null, settings); + if (Application.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow == null) + desktop.MainWindow = window; + window.Show(); + } + + + /// + /// Locates the view model, locates the associate view, binds them and shows it as the root view. + /// + /// The view model type. + /// The optional window settings. + protected Task DisplayRootViewFor(IDictionary settings = null) + { + return DisplayRootViewForAsync(typeof(TViewModel), settings); + } + } +} diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/IWindowManager.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/IWindowManager.cs new file mode 100644 index 000000000..183289bb9 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/IWindowManager.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace Caliburn.Micro +{ + /// + /// A service that manages windows. + /// + public interface IWindowManager + { + /* + /// + /// Shows a modal dialog for the specified model. + /// + /// The root model. + /// The context. + /// The optional dialog settings. + /// The dialog result. + Task ShowDialogAsync(Window owner, object rootModel, object context = null, IDictionary settings = null); + */ + + /// + /// Shows a non-modal window for the specified model. + /// + /// The root model. + /// The context. + /// The optional window settings. + Task ShowWindowAsync(object rootModel, object context = null, IDictionary settings = null); + + /// + /// Shows a popup at the current mouse position. + /// + /// The root model. + /// The view context. + /// The optional popup settings. + Task ShowPopupAsync(object rootModel, object context = null, IDictionary settings = null); + + /// + /// Creates a window. + /// + /// The view model. + /// Whethor or not the window is being shown as a dialog. + /// The view context. + /// The optional popup settings. + /// The window. + Task CreateWindowAsync(object rootModel, bool isDialog, object context = null, IDictionary settings = null); + } +} diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/Parameter.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/Parameter.cs new file mode 100644 index 000000000..10c372667 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/Parameter.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; +using Avalonia; +using Avalonia.Xaml.Interactivity; + +namespace Caliburn.Micro +{ + /// + /// Represents a parameter of an . + /// + public class Parameter : AvaloniaObject, IBehavior + { + /// + /// A dependency property representing the parameter's value. + /// + public static readonly AvaloniaProperty ValueProperty = + AvaloniaProperty.Register("Value"); + + IAvaloniaObject associatedObject; + WeakReference owner; + + static Parameter() + { + ValueProperty.Changed.Subscribe(OnValueChanged); + } + + /// + /// Gets or sets the value of the parameter. + /// + /// The value. + [Category("Common Properties")] + public object Value + { + get { return GetValue(ValueProperty); } + set { SetValue(ValueProperty, value); } + } + + IAvaloniaObject IBehavior.AssociatedObject + { + get + { + return associatedObject; + } + } + + /// + /// Gets or sets the owner. + /// + protected ActionMessage Owner + { + get { return owner == null ? null : owner.Target as ActionMessage; } + set { owner = new WeakReference(value); } + } + + void IBehavior.Attach(IAvaloniaObject AvaloniaObject) + { + associatedObject = AvaloniaObject; + } + + void IBehavior.Detach() + { + associatedObject = null; + } + + /// + /// Makes the parameter aware of the that it's attached to. + /// + /// The action message. + internal void MakeAwareOf(ActionMessage owner) + { + Owner = owner; + } + + static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) + { + var parameter = (Parameter) e.Sender; + var owner = parameter.Owner; + + if (owner != null) + { + owner.UpdateAvailability(); + } + } + } +} diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/TriggerAction.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/TriggerAction.cs new file mode 100644 index 000000000..6e8e9d0d8 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/TriggerAction.cs @@ -0,0 +1,117 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Xaml.Interactivity; + +namespace Caliburn.Micro +{ + public abstract class TriggerAction : TriggerAction + { + } + + /// + /// Represents an attachable object that encapsulates a unit of functionality. + /// + public abstract class TriggerAction : AvaloniaObject, IAction + { + private Control _associatedObject; + /* + /// + /// The associated object property. + /// + public static readonly AvaloniaProperty AssociatedObjectProperty = + AvaloniaProperty.Register, Control>("AssociatedObject"); + + static TriggerAction() + { + AssociatedObjectProperty.Changed.Subscribe(OnAssociatedObjectChanged); + } + + /// + /// Gets or sets the object to which this is attached. + /// + public Control AssociatedObject + { + get + { + return (Control)GetValue(AssociatedObjectProperty); + } + set + { + SetValue(AssociatedObjectProperty, value); + } + } + */ + + public Control AssociatedObject + { + get => _associatedObject; + set + { + if (_associatedObject == value) + return; + + if (_associatedObject != null) + OnDetaching(); + _associatedObject = value; + if (_associatedObject != null) + OnAttached(); + } + } + + /// + /// Invokes the action. + /// + /// The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference. + protected abstract void Invoke(object parmeter); + + /// + /// Executes the action. + /// + /// The that is passed to the action by the behavior. Generally this is or a target object. + /// The value of this parameter is determined by the caller. + /// + /// Returns the result of the action. + /// + public virtual object Execute(object sender, object parameter) + { + + if (AssociatedObject == null && sender is Control) + { + AssociatedObject = (Control)sender; + } + + Invoke(parameter); + return null; + } + + /// + /// Called after the action is attached to an AssociatedObject. + /// + protected virtual void OnAttached() + { + } + + /// + /// Called when the action is being detached from its AssociatedObject, but before it has actually occurred. + /// + protected virtual void OnDetaching() + { + } + /* + private static void OnAssociatedObjectChanged(AvaloniaPropertyChangedEventArgs e) + { + var triggerAction = e.Sender as TriggerAction; + + if (triggerAction == null) + return; + + if (e.OldValue != null) + triggerAction.OnDetaching(); + + if (e.NewValue != null) + triggerAction.OnAttached(); + } + */ + } +} diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowConductor.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowConductor.cs new file mode 100644 index 000000000..d80824636 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowConductor.cs @@ -0,0 +1,121 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls; + +namespace Caliburn.Micro +{ + public class WindowConductor + { + private bool deactivatingFromView; + private bool deactivateFromViewModel; + private bool actuallyClosing; + private readonly Window view; + private readonly object model; + + public WindowConductor(object model, Window view) + { + this.model = model; + this.view = view; + } + + public async Task InitialiseAsync() + { + if (model is IActivate activator) + { + await activator.ActivateAsync(); + } + + if (model is IDeactivate deactivatable) + { + view.Closed += Closed; + deactivatable.Deactivated += Deactivated; + } + + if (model is IGuardClose guard) + { + view.Closing += Closing; + } + } + + private async void Closed(object sender, EventArgs e) + { + view.Closed -= Closed; + view.Closing -= Closing; + + if (deactivateFromViewModel) + { + return; + } + + var deactivatable = (IDeactivate)model; + + deactivatingFromView = true; + await deactivatable.DeactivateAsync(true); + deactivatingFromView = false; + } + + private Task Deactivated(object sender, DeactivationEventArgs e) + { + if (!e.WasClosed) + { + return Task.FromResult(false); + } + + ((IDeactivate)model).Deactivated -= Deactivated; + + if (deactivatingFromView) + { + return Task.FromResult(true); + } + + deactivateFromViewModel = true; + actuallyClosing = true; + view.Close(); + actuallyClosing = false; + deactivateFromViewModel = false; + + return Task.FromResult(true); + } + + private async void Closing(object sender, CancelEventArgs e) + { + if (e.Cancel) + { + return; + } + + var guard = (IGuardClose)model; + + if (actuallyClosing) + { + actuallyClosing = false; + return; + } + + //var cachedDialogResult = view.DialogResult; + + e.Cancel = true; + + await Task.Yield(); + + var canClose = await guard.CanCloseAsync(CancellationToken.None); + + if (!canClose) + return; + + actuallyClosing = true; + view.Close(); + + //if (cachedDialogResult == null) + //{ + //view.Close(); + //} + //else if (view.DialogResult != cachedDialogResult) + //{ + //view.DialogResult = cachedDialogResult; + //} + } + } +} diff --git a/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowManager.cs b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowManager.cs new file mode 100644 index 000000000..7b9666ab8 --- /dev/null +++ b/src/Caliburn.Micro.Avalonia/Platforms/netcore-avalonia/WindowManager.cs @@ -0,0 +1,303 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Caliburn.Micro; +using JetBrains.Annotations; + +namespace Caliburn.Micro +{ + /// + /// A service that manages windows. + /// + public class WindowManager : IWindowManager + { + /// + /// Shows a modal dialog for the specified model. + /// + /// The root model. + /// The context. + /// The dialog popup settings. + /// The dialog result. + public virtual async Task ShowDialogAsync(object rootModel, object context = null, IDictionary settings = null) + { + var window = await CreateWindowAsync(rootModel, true, context, settings); + + await window.ShowDialog(InferOwnerOf(window)); + } + + /// + /// Shows a window for the specified model. + /// + /// The root model. + /// The context. + /// The optional window settings. + public virtual async Task ShowWindowAsync(object rootModel, object context = null, IDictionary settings = null) + { + /* + NavigationWindow navWindow = null; + + var application = Application.Current; + if (application != null && application.MainWindow != null) + { + navWindow = application.MainWindow as NavigationWindow; + } + + if (navWindow != null) + { + var window = await CreatePageAsync(rootModel, context, settings); + navWindow.Navigate(window); + } + else + { + */ + var window = await CreateWindowAsync(rootModel, false, context, settings); + + window.Show(); + /*}*/ + } + + /// + /// Shows a popup at the current mouse position. + /// + /// The root model. + /// The view context. + /// The optional popup settings. + public virtual async Task ShowPopupAsync(object rootModel, object context = null, IDictionary settings = null) + { + var popup = CreatePopup(rootModel, settings); + var view = ViewLocator.LocateForModel(rootModel, popup, context); + + popup.Child = view; + popup.SetValue(View.IsGeneratedProperty, true); + + ViewModelBinder.Bind(rootModel, popup, null); + Action.SetTargetWithoutContext(view, rootModel); + + if (rootModel is IActivate activator) + { + await activator.ActivateAsync(); + } + + if (rootModel is IDeactivate deactivator) + { + popup.Closed += async (s, e) => await deactivator.DeactivateAsync(true); + } + + popup.IsOpen = true; + //popup.CaptureMouse(); //todo: mouse capture + } + + /// + /// Creates a popup for hosting a popup window. + /// + /// The model. + /// The optional popup settings. + /// The popup. + protected virtual Popup CreatePopup(object rootModel, IDictionary settings) + { + var popup = new Popup(); + + if (ApplySettings(popup, settings)) + { + if (!settings.ContainsKey("PlacementTarget") && !settings.ContainsKey("Placement")) + { + popup.PlacementMode = PlacementMode.Pointer; + } + + //if (!settings.ContainsKey("AllowsTransparency")) + //{ + // popup.AllowsTransparency = true; + //} + } + else + { + //popup.AllowsTransparency = true; + popup.PlacementMode = PlacementMode.Pointer; + } + + return popup; + } + + /// + /// Creates a window. + /// + /// The view model. + /// Whethor or not the window is being shown as a dialog. + /// The view context. + /// The optional popup settings. + /// The window. + public virtual async Task CreateWindowAsync(object rootModel, bool isDialog, object context, IDictionary settings) + { + var view = EnsureWindow(rootModel, ViewLocator.LocateForModel(rootModel, null, context), isDialog); + ViewModelBinder.Bind(rootModel, view, context); + + var haveDisplayName = rootModel as IHaveDisplayName; + if (string.IsNullOrEmpty(view.Title) && haveDisplayName != null && !ConventionManager.HasBinding(view, Window.TitleProperty)) + { + var binding = new Binding("DisplayName") { Mode = BindingMode.TwoWay }; + view.Bind(Window.TitleProperty, binding); + } + + ApplySettings(view, settings); + + var conductor = new WindowConductor(rootModel, view); + + await conductor.InitialiseAsync(); + + return view; + } + + /// + /// Makes sure the view is a window is is wrapped by one. + /// + /// The view model. + /// The view. + /// Whethor or not the window is being shown as a dialog. + /// The window. + protected virtual Window EnsureWindow(object model, object view, bool isDialog) + { + + if (view is Window window) + { + var owner = InferOwnerOf(window); + if (owner != null && isDialog) + { + //TODO: (Avalonia) Set window owner + //window.Owner = owner; + } + } + else + { + window = new Window + { + Content = view, + SizeToContent = SizeToContent.WidthAndHeight + }; + + window.SetValue(View.IsGeneratedProperty, true); + + var owner = InferOwnerOf(window); + if (owner != null) + { + window.WindowStartupLocation = WindowStartupLocation.CenterOwner; + //TODO: (Avalonia) Set window owner + //window.Owner = owner; + } + else + { + window.WindowStartupLocation = WindowStartupLocation.CenterScreen; + } + } + + return window; + } + + /// + /// Infers the owner of the window. + /// + /// The window to whose owner needs to be determined. + /// The owner. + protected virtual Window InferOwnerOf(Window window) + { + var application = Application.Current; + if (application == null) + { + return null; + } + + Window active = null; + if (application.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime c) + { + active = c.Windows.OfType().FirstOrDefault(x => x.IsActive); + active = active ?? c.MainWindow; //(PresentationSource.FromVisual(application.MainWindow) == null ? null : application.MainWindow); + } + + return active == window ? null : active; + } + + /* + /// + /// Creates the page. + /// + /// The root model. + /// The context. + /// The optional popup settings. + /// The page. + public virtual async Task CreatePageAsync(object rootModel, object context, IDictionary settings) + { + var view = EnsurePage(rootModel, ViewLocator.LocateForModel(rootModel, null, context)); + ViewModelBinder.Bind(rootModel, view, context); + + var haveDisplayName = rootModel as IHaveDisplayName; + if (string.IsNullOrEmpty(view.Title) && haveDisplayName != null && !ConventionManager.HasBinding(view, Page.TitleProperty)) + { + var binding = new Binding("DisplayName") { Mode = BindingMode.TwoWay }; + view.SetBinding(Page.TitleProperty, binding); + } + + ApplySettings(view, settings); + + if (rootModel is IActivate activator) + { + await activator.ActivateAsync(); + } + + if (rootModel is IDeactivate deactivatable) + { + view.Unloaded += async (s, e) => await deactivatable.DeactivateAsync(true); + } + + return view; + } + */ + + /* + /// + /// Ensures the view is a page or provides one. + /// + /// The model. + /// The view. + /// The page. + protected virtual Page EnsurePage(object model, object view) + { + if (view is Page page) + { + return page; + } + + page = new Page { Content = view }; + page.SetValue(View.IsGeneratedProperty, true); + + return page; + } + */ + + private bool ApplySettings(object target, IEnumerable> settings) + { + if (settings != null) + { + var type = target.GetType(); + + foreach (var pair in settings) + { + var propertyInfo = type.GetProperty(pair.Key); + + if (propertyInfo != null) + { + propertyInfo.SetValue(target, pair.Value, null); + } + } + + return true; + } + + return false; + } + } +} diff --git a/src/Caliburn.Micro.Core/Caliburn.Micro.Core.csproj b/src/Caliburn.Micro.Core/Caliburn.Micro.Core.csproj index e42533612..4be452cc1 100644 --- a/src/Caliburn.Micro.Core/Caliburn.Micro.Core.csproj +++ b/src/Caliburn.Micro.Core/Caliburn.Micro.Core.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net461;netstandard2.0 Caliburn.Micro.Core Caliburn.Micro Core Caliburn.Micro diff --git a/src/Caliburn.Micro.Platform.Core/Caliburn.Micro.Platform.Core.csproj b/src/Caliburn.Micro.Platform.Core/Caliburn.Micro.Platform.Core.csproj index 7a015e398..08afd3954 100644 --- a/src/Caliburn.Micro.Platform.Core/Caliburn.Micro.Platform.Core.csproj +++ b/src/Caliburn.Micro.Platform.Core/Caliburn.Micro.Platform.Core.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net5.0-windows;net6.0-windows + netstandard2.0;net6.0-windows Caliburn.Micro.Platform.Core Caliburn.Micro .\..\Caliburn.Micro.snk diff --git a/src/Caliburn.Micro.Platform/Action.cs b/src/Caliburn.Micro.Platform/Action.cs index a306c5439..ac6105e73 100644 --- a/src/Caliburn.Micro.Platform/Action.cs +++ b/src/Caliburn.Micro.Platform/Action.cs @@ -13,6 +13,12 @@ namespace Caliburn.Micro using FrameworkElement = global::Xamarin.Forms.VisualElement; using DependencyProperty = global::Xamarin.Forms.BindableProperty; using DependencyObject = global::Xamarin.Forms.BindableObject; +#elif AVALONIA + using System; + using Avalonia; + using DependencyObject = Avalonia.IAvaloniaObject; + using DependencyPropertyChangedEventArgs = Avalonia.AvaloniaPropertyChangedEventArgs; + using FrameworkElement = Avalonia.Controls.Control; #else using System.Windows; #endif @@ -26,26 +32,45 @@ public static class Action { /// /// A property definition representing the target of an . The DataContext of the element will be set to this instance. /// +#if AVALONIA + public static readonly AvaloniaProperty TargetProperty = + AvaloniaProperty.RegisterAttached("Target", typeof(Action)); +#else public static readonly DependencyProperty TargetProperty = DependencyPropertyHelper.RegisterAttached( "Target", typeof(object), typeof(Action), - null, + null, OnTargetChanged ); +#endif /// /// A property definition representing the target of an . The DataContext of the element is not set to this instance. /// +#if AVALONIA + public static readonly AvaloniaProperty TargetWithoutContextProperty = + AvaloniaProperty.RegisterAttached("TargetWithoutContext", typeof(Action)); +#else public static readonly DependencyProperty TargetWithoutContextProperty = DependencyPropertyHelper.RegisterAttached( "TargetWithoutContext", typeof(object), typeof(Action), - null, + null, OnTargetWithoutContextChanged ); +#endif + +#if AVALONIA + static Action() + { + TargetProperty.Changed.Subscribe(args => OnTargetChanged(args.Sender, args)); + TargetWithoutContextProperty.Changed.Subscribe(args => OnTargetWithoutContextChanged(args.Sender, args)); + } + +#endif /// /// Sets the target of the . diff --git a/src/Caliburn.Micro.Platform/ActionExecutionContext.cs b/src/Caliburn.Micro.Platform/ActionExecutionContext.cs index 57df00c96..e82331ba8 100644 --- a/src/Caliburn.Micro.Platform/ActionExecutionContext.cs +++ b/src/Caliburn.Micro.Platform/ActionExecutionContext.cs @@ -14,6 +14,9 @@ namespace Caliburn.Micro using DependencyObject = global::Xamarin.Forms.BindableObject; using DependencyProperty = global::Xamarin.Forms.BindableProperty; using FrameworkElement = global::Xamarin.Forms.VisualElement; +#elif AVALONIA + using FrameworkElement = Avalonia.Controls.Control; + using DependencyObject = Avalonia.IAvaloniaObject; #else using System.Windows; #endif diff --git a/src/Caliburn.Micro.Platform/ActionMessage.cs b/src/Caliburn.Micro.Platform/ActionMessage.cs index fcbe834cd..41c3ccb96 100644 --- a/src/Caliburn.Micro.Platform/ActionMessage.cs +++ b/src/Caliburn.Micro.Platform/ActionMessage.cs @@ -14,6 +14,21 @@ using Microsoft.Xaml.Interactivity; using TriggerBase = Microsoft.Xaml.Interactivity.IBehavior; using EventTrigger = Microsoft.Xaml.Interactions.Core.EventTriggerBehavior; +#elif AVALONIA + using Avalonia; + using Avalonia.Data; + using Avalonia.Data.Core; + using Avalonia.Interactivity; + using Avalonia.Xaml.Interactivity; + using Avalonia.VisualTree; + using Avalonia.Xaml.Interactions.Core; + using DependencyObject = Avalonia.IAvaloniaObject; + using XamlReader = Avalonia.Markup.Xaml.AvaloniaRuntimeXamlLoader; + using UIElement = Avalonia.Input.InputElement; + using DependencyPropertyChangedEventArgs = Avalonia.AvaloniaPropertyChangedEventArgs; + using DependencyProperty = Avalonia.AvaloniaProperty; + using EventTrigger = Avalonia.Xaml.Interactions.Core.EventTriggerBehavior; + using FrameworkElement = Avalonia.Controls.Control; #else using System.Windows; using System.Windows.Controls.Primitives; @@ -22,6 +37,7 @@ using Microsoft.Xaml.Behaviors; using EventTrigger = Microsoft.Xaml.Behaviors.EventTrigger; #endif + #if NET5_0_WINDOWS || NET6_0_WINDOWS using System.IO; using System.Xml; @@ -32,22 +48,28 @@ /// #if WINDOWS_UWP [ContentProperty(Name = "Parameters")] -#else +#elif !AVALONIA [ContentProperty("Parameters")] [DefaultTrigger(typeof(FrameworkElement), typeof(EventTrigger), "MouseLeftButtonDown")] [DefaultTrigger(typeof(ButtonBase), typeof(EventTrigger), "Click")] [TypeConstraint(typeof(FrameworkElement))] #endif - public class ActionMessage : TriggerAction, IHaveParameters { + public class ActionMessage : TriggerAction, IHaveParameters + { static readonly ILog Log = LogManager.GetLog(typeof(ActionMessage)); ActionExecutionContext context; - internal static readonly DependencyProperty HandlerProperty = DependencyProperty.RegisterAttached( + internal static readonly DependencyProperty HandlerProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("Handler", typeof(ActionMessage)); +#else + DependencyProperty.RegisterAttached( "Handler", typeof(object), typeof(ActionMessage), new PropertyMetadata(null, HandlerPropertyChanged) ); +#endif /// /// Causes the action invocation to "double check" if the action should be invoked by executing the guard immediately before hand. @@ -65,28 +87,44 @@ public class ActionMessage : TriggerAction, IHaveParameters { /// Represents the method name of an action message. /// public static readonly DependencyProperty MethodNameProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("MethodName", typeof(ActionMessage)); +#else DependencyProperty.Register( "MethodName", typeof(string), typeof(ActionMessage), null ); +#endif /// /// Represents the parameters of an action message. /// public static readonly DependencyProperty ParametersProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached>("Parameters", typeof(ActionMessage)); +#else DependencyProperty.Register( "Parameters", typeof(AttachedCollection), typeof(ActionMessage), null ); +#endif + +#if AVALONIA + static ActionMessage() + { + HandlerProperty.Changed.Subscribe(args => HandlerPropertyChanged(args.Sender, args)); + } +#endif /// /// Creates an instance of . /// - public ActionMessage() { + public ActionMessage() + { SetValue(ParametersProperty, new AttachedCollection()); } @@ -97,7 +135,8 @@ public ActionMessage() { #if !WINDOWS_UWP [Category("Common Properties")] #endif - public string MethodName { + public string MethodName + { get { return (string)GetValue(MethodNameProperty); } set { SetValue(MethodNameProperty, value); } } @@ -109,7 +148,8 @@ public string MethodName { #if !WINDOWS_UWP [Category("Common Properties")] #endif - public AttachedCollection Parameters { + public AttachedCollection Parameters + { get { return (AttachedCollection)GetValue(ParametersProperty); } } @@ -147,14 +187,23 @@ void ElementUnloaded(object sender, RoutedEventArgs e) OnDetaching(); } #else - protected override void OnAttached() { - if (!View.InDesignMode) { + protected override void OnAttached() + { + if (!View.InDesignMode) + { Parameters.Attach(AssociatedObject); Parameters.Apply(x => x.MakeAwareOf(this)); - if (View.ExecuteOnLoad(AssociatedObject, ElementLoaded)) { + if (View.ExecuteOnLoad(AssociatedObject, ElementLoaded)) + { +#if AVALONIA + var trigger = Interaction.GetBehaviors(AssociatedObject) + .OfType() + .FirstOrDefault(t => t.Actions.Contains(this)) as EventTriggerBehavior; +#else var trigger = Interaction.GetTriggers(AssociatedObject) - .FirstOrDefault(t => t.Actions.Contains(this)) as EventTrigger; + .FirstOrDefault(t => t.Actions.Contains(this)) as EventTrigger; +#endif if (trigger != null && trigger.EventName == "Loaded") Invoke(new RoutedEventArgs()); } @@ -164,39 +213,70 @@ protected override void OnAttached() { } #endif - static void HandlerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { + static void HandlerPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { ((ActionMessage)d).UpdateContext(); } /// /// Called when the action is being detached from its AssociatedObject, but before it has actually occurred. /// - protected override void OnDetaching() { - if (!View.InDesignMode) { + protected override void OnDetaching() + { + if (!View.InDesignMode) + { Detaching(this, EventArgs.Empty); +#if AVALONIA + //TODO: (Avalonia) Remove the ElementLoaded handler added in OnAttached +#else + //TODO: Fix this: cannot remove ElementLoaded here because a wrapper handler was added instead (in View.ExecuteOnLoad() called from this.OnAttached()) AssociatedObject.Loaded -= ElementLoaded; +#endif Parameters.Detach(); } base.OnDetaching(); } - void ElementLoaded(object sender, RoutedEventArgs e) { +#if AVALONIA + void ElementLoaded(object sender, EventArgs e) + { +#else + void ElementLoaded(object sender, RoutedEventArgs e) + { +#endif UpdateContext(); DependencyObject currentElement; - if (context.View == null) { + if (context.View == null) + { currentElement = AssociatedObject; - while (currentElement != null) { + while (currentElement != null) + { if (Action.HasTargetSet(currentElement)) break; +#if AVALONIA + currentElement = ((IVisual)currentElement).GetVisualParent() as IAvaloniaObject; +#else currentElement = BindingScope.GetVisualParent(currentElement); +#endif } } - else currentElement = context.View; + else + currentElement = context.View; -#if NET || NETCORE +#if AVALONIA + var binding = new Binding + { + Path = "(cal:Message.Handler)", + TypeResolver = (s, s1) => + { + return typeof(Message); + }, + Source = currentElement + }; +#elif (NET || NETCORE) var binding = new Binding { Path = new PropertyPath(Message.HandlerProperty), Source = currentElement @@ -217,14 +297,20 @@ void ElementLoaded(object sender, RoutedEventArgs e) { var binding = (Binding)XamlReader.Load(bindingText); binding.Source = currentElement; #endif +#if AVALONIA + this.Bind(HandlerProperty, binding); +#else BindingOperations.SetBinding(this, HandlerProperty, binding); +#endif } - void UpdateContext() { + void UpdateContext() + { if (context != null) context.Dispose(); - context = new ActionExecutionContext { + context = new ActionExecutionContext + { Message = this, Source = AssociatedObject }; @@ -237,16 +323,20 @@ void UpdateContext() { /// Invokes the action. /// /// The parameter to the action. If the action does not require a parameter, the parameter may be set to a null reference. - protected override void Invoke(object eventArgs) { + protected override void Invoke(object eventArgs) + { Log.Info("Invoking {0}.", this); - if (context == null) { + if (context == null) + { UpdateContext(); } - if (context.Target == null || context.View == null) { + if (context.Target == null || context.View == null) + { PrepareContext(context); - if (context.Target == null) { + if (context.Target == null) + { var ex = new Exception(string.Format("No target found for method {0}.", context.Message.MethodName)); Log.Error(ex); @@ -255,12 +345,14 @@ protected override void Invoke(object eventArgs) { throw ex; } - if (!UpdateAvailabilityCore()) { + if (!UpdateAvailabilityCore()) + { return; } } - if (context.Method == null) { + if (context.Method == null) + { var ex = new Exception(string.Format("Method {0} not found on target of type {1}.", context.Message.MethodName, context.Target.GetType())); Log.Error(ex); @@ -271,7 +363,8 @@ protected override void Invoke(object eventArgs) { context.EventArgs = eventArgs; - if (EnforceGuardsDuringInvocation && context.CanExecute != null && !context.CanExecute()) { + if (EnforceGuardsDuringInvocation && context.CanExecute != null && !context.CanExecute()) + { return; } @@ -282,7 +375,8 @@ protected override void Invoke(object eventArgs) { /// /// Forces an update of the UI's Enabled/Disabled state based on the the preconditions associated with the method. /// - public virtual void UpdateAvailability() { + public virtual void UpdateAvailability() + { if (context == null) return; @@ -292,7 +386,8 @@ public virtual void UpdateAvailability() { UpdateAvailabilityCore(); } - bool UpdateAvailabilityCore() { + bool UpdateAvailabilityCore() + { Log.Info("{0} availability update.", this); return ApplyAvailabilityEffect(context); } @@ -303,34 +398,40 @@ bool UpdateAvailabilityCore() { /// /// A that represents the current . /// - public override string ToString() { + public override string ToString() + { return "Action: " + MethodName; } /// /// Invokes the action using the specified /// - public static Action InvokeAction = context => { + public static Action InvokeAction = context => + { var values = MessageBinder.DetermineParameters(context, context.Method.GetParameters()); var returnValue = context.Method.Invoke(context.Target, values); var task = returnValue as System.Threading.Tasks.Task; - if (task != null) { + if (task != null) + { returnValue = task.AsResult(); } - + var result = returnValue as IResult; - if (result != null) { + if (result != null) + { returnValue = new[] { result }; } var enumerable = returnValue as IEnumerable; - if (enumerable != null) { + if (enumerable != null) + { returnValue = enumerable.GetEnumerator(); } var enumerator = returnValue as IEnumerator; - if (enumerator != null) { + if (enumerator != null) + { Coroutine.BeginExecute(enumerator, new CoroutineExecutionContext { @@ -345,14 +446,16 @@ public override string ToString() { /// Applies an availability effect, such as IsEnabled, to an element. /// /// Returns a value indicating whether or not the action is available. - public static Func ApplyAvailabilityEffect = context => { + public static Func ApplyAvailabilityEffect = context => + { #if WINDOWS_UWP var source = context.Source as Control; #else var source = context.Source; #endif - if (source == null) { + if (source == null) + { return true; } @@ -361,7 +464,8 @@ public override string ToString() { #else var hasBinding = ConventionManager.HasBinding(source, UIElement.IsEnabledProperty); #endif - if (!hasBinding && context.CanExecute != null) { + if (!hasBinding && context.CanExecute != null) + { source.IsEnabled = context.CanExecute(); } @@ -372,7 +476,8 @@ public override string ToString() { /// Finds the method on the target matching the specified message. /// /// The matching method, if available. - public static Func GetTargetMethod = (message, target) => { + public static Func GetTargetMethod = (message, target) => + { #if WINDOWS_UWP return (from method in target.GetType().GetRuntimeMethods() where method.Name == message.MethodName @@ -391,23 +496,29 @@ public override string ToString() { /// /// Sets the target, method and view on the context. Uses a bubbling strategy by default. /// - public static Action SetMethodBinding = context => { + public static Action SetMethodBinding = context => + { var source = context.Source; DependencyObject currentElement = source; - while (currentElement != null) { - if (Action.HasTargetSet(currentElement)) { + while (currentElement != null) + { + if (Action.HasTargetSet(currentElement)) + { var target = Message.GetHandler(currentElement); - if (target != null) { + if (target != null) + { var method = GetTargetMethod(context.Message, target); - if (method != null) { + if (method != null) + { context.Method = method; context.Target = target; context.View = currentElement; return; } } - else { + else + { context.View = currentElement; return; } @@ -416,11 +527,13 @@ public override string ToString() { currentElement = BindingScope.GetVisualParent(currentElement); } - if (source != null && source.DataContext != null) { + if (source != null && source.DataContext != null) + { var target = source.DataContext; var method = GetTargetMethod(context.Message, target); - if (method != null) { + if (method != null) + { context.Target = target; context.Method = method; context.View = source; @@ -431,7 +544,8 @@ public override string ToString() { /// /// Prepares the action execution context for use. /// - public static Action PrepareContext = context => { + public static Action PrepareContext = context => + { SetMethodBinding(context); if (context.Target == null || context.Method == null) { @@ -453,17 +567,20 @@ public override string ToString() { { matchingGuardName = possibleGuardName; guard = GetMethodInfo(targetType, "get_" + matchingGuardName); - if (guard != null) break; + if (guard != null) + break; } if (guard == null) return; PropertyChangedEventHandler handler = null; - handler = (s, e) => { + handler = (s, e) => + { if (string.IsNullOrEmpty(e.PropertyName) || e.PropertyName == matchingGuardName) { - Caliburn.Micro.Execute.OnUIThread(() => { + Caliburn.Micro.Execute.OnUIThread(() => + { var message = context.Message; if (message == null) { @@ -476,8 +593,10 @@ public override string ToString() { }; inpc.PropertyChanged += handler; - context.Disposing += delegate { inpc.PropertyChanged -= handler; }; - context.Message.Detaching += delegate { inpc.PropertyChanged -= handler; }; + context.Disposing += delegate + { inpc.PropertyChanged -= handler; }; + context.Message.Detaching += delegate + { inpc.PropertyChanged -= handler; }; } context.CanExecute = () => (bool)guard.Invoke( @@ -495,23 +614,30 @@ public override string ToString() { /// The execution context /// Method names to look for. ///A MethodInfo, if found; null otherwise - static MethodInfo TryFindGuardMethod(ActionExecutionContext context, IEnumerable possibleGuardNames) { + static MethodInfo TryFindGuardMethod(ActionExecutionContext context, IEnumerable possibleGuardNames) + { var targetType = context.Target.GetType(); MethodInfo guard = null; foreach (string possibleGuardName in possibleGuardNames) { guard = GetMethodInfo(targetType, possibleGuardName); - if (guard != null) break; + if (guard != null) + break; } - if (guard == null) return null; - if (guard.ContainsGenericParameters) return null; - if (!typeof(bool).Equals(guard.ReturnType)) return null; + if (guard == null) + return null; + if (guard.ContainsGenericParameters) + return null; + if (!typeof(bool).Equals(guard.ReturnType)) + return null; var guardPars = guard.GetParameters(); var actionPars = context.Method.GetParameters(); - if (guardPars.Length == 0) return guard; - if (guardPars.Length != actionPars.Length) return null; + if (guardPars.Length == 0) + return guard; + if (guardPars.Length != actionPars.Length) + return null; var comparisons = guardPars.Zip( context.Method.GetParameters(), @@ -529,7 +655,8 @@ static MethodInfo TryFindGuardMethod(ActionExecutionContext context, IEnumerable /// /// Returns the list of possible names of guard methods / properties for the given method. /// - public static Func> BuildPossibleGuardNames = method => { + public static Func> BuildPossibleGuardNames = method => + { var guardNames = new List(); @@ -541,7 +668,8 @@ static MethodInfo TryFindGuardMethod(ActionExecutionContext context, IEnumerable const string AsyncMethodSuffix = "Async"; - if (methodName.EndsWith(AsyncMethodSuffix, StringComparison.OrdinalIgnoreCase)) { + if (methodName.EndsWith(AsyncMethodSuffix, StringComparison.OrdinalIgnoreCase)) + { guardNames.Add(GuardPrefix + methodName.Substring(0, methodName.Length - AsyncMethodSuffix.Length)); } diff --git a/src/Caliburn.Micro.Platform/Bind.cs b/src/Caliburn.Micro.Platform/Bind.cs index 3bfd04d36..202baef53 100644 --- a/src/Caliburn.Micro.Platform/Bind.cs +++ b/src/Caliburn.Micro.Platform/Bind.cs @@ -14,6 +14,13 @@ namespace Caliburn.Micro using FrameworkElement = global::Xamarin.Forms.VisualElement; using DependencyProperty = global::Xamarin.Forms.BindableProperty; using DependencyObject =global::Xamarin.Forms.BindableObject; +#elif AVALONIA + using Avalonia; + using Avalonia.Data; + using DependencyObject = Avalonia.IAvaloniaObject; + using DependencyPropertyChangedEventArgs = Avalonia.AvaloniaPropertyChangedEventArgs; + using FrameworkElement = Avalonia.Controls.Control; + using DependencyProperty = Avalonia.AvaloniaProperty; #else using System.Windows; using System.Windows.Data; @@ -27,31 +34,52 @@ public static class Bind { /// Allows binding on an existing view. Use this on root UserControls, Pages and Windows; not in a DataTemplate. /// public static DependencyProperty ModelProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("Model", typeof(Bind)); +#else DependencyPropertyHelper.RegisterAttached( "Model", typeof(object), typeof(Bind), - null, + null, ModelChanged); +#endif /// /// Allows binding on an existing view without setting the data context. Use this from within a DataTemplate. /// public static DependencyProperty ModelWithoutContextProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("Handler", typeof(Bind)); +#else DependencyPropertyHelper.RegisterAttached( "ModelWithoutContext", typeof(object), typeof(Bind), - null, + null, ModelWithoutContextChanged); +#endif internal static DependencyProperty NoContextProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("Handler", typeof(Bind)); +#else DependencyPropertyHelper.RegisterAttached( "NoContext", typeof(bool), typeof(Bind), false); +#endif +#if AVALONIA + static Bind() + { + ModelProperty.Changed.Subscribe(args => ModelChanged(args.Sender, args)); + ModelWithoutContextProperty.Changed.Subscribe(args => ModelWithoutContextChanged(args.Sender, args)); + DataContextProperty.Changed.Subscribe(args => DataContextChanged(args.Sender, args)); + AtDesignTimeProperty.Changed.Subscribe(args => AtDesignTimeChanged(args.Sender, args)); + } +#endif /// /// Gets the model to bind to. /// @@ -89,7 +117,7 @@ public static void SetModel(DependencyObject dependencyObject, object value) { } static void ModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (View.InDesignMode || e.NewValue == null || e.NewValue == e.OldValue) { + if (Caliburn.Micro.View.InDesignMode || e.NewValue == null || e.NewValue == e.OldValue) { return; } @@ -98,10 +126,10 @@ static void ModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs return; } - View.ExecuteOnLoad(fe, delegate { + Caliburn.Micro.View.ExecuteOnLoad(fe, delegate { var target = e.NewValue; - d.SetValue(View.IsScopeRootProperty, true); + d.SetValue(Caliburn.Micro.View.IsScopeRootProperty, true); #if XFORMS var context = fe.Id.ToString("N"); @@ -116,7 +144,7 @@ static void ModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs } static void ModelWithoutContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (View.InDesignMode || e.NewValue == null || e.NewValue == e.OldValue) { + if (Caliburn.Micro.View.InDesignMode || e.NewValue == null || e.NewValue == e.OldValue) { return; } @@ -125,9 +153,9 @@ static void ModelWithoutContextChanged(DependencyObject d, DependencyPropertyCha return; } - View.ExecuteOnLoad(fe, delegate { + Caliburn.Micro.View.ExecuteOnLoad(fe, delegate { var target = e.NewValue; - d.SetValue(View.IsScopeRootProperty, true); + d.SetValue(Caliburn.Micro.View.IsScopeRootProperty, true); #if XFORMS var context = fe.Id.ToString("N"); @@ -146,19 +174,23 @@ static void ModelWithoutContextChanged(DependencyObject d, DependencyPropertyCha /// Allows application of conventions at design-time. /// public static DependencyProperty AtDesignTimeProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("AtDesignTime", typeof(Bind)); +#else DependencyPropertyHelper.RegisterAttached( "AtDesignTime", typeof(bool), typeof(Bind), - false, + false, AtDesignTimeChanged); +#endif /// /// Gets whether or not conventions are being applied at design-time. /// /// The ui to apply conventions to. /// Whether or not conventions are applied. -#if NET || NETCORE +#if (NET || NETCORE) && !AVALONIA // not sure this is right [AttachedPropertyBrowsableForTypeAttribute(typeof(DependencyObject))] #endif public static bool GetAtDesignTime(DependencyObject dependencyObject) { @@ -175,7 +207,7 @@ public static void SetAtDesignTime(DependencyObject dependencyObject, bool value } static void AtDesignTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (!View.InDesignMode) + if (!Caliburn.Micro.View.InDesignMode) return; var atDesignTime = (bool) e.NewValue; @@ -183,20 +215,26 @@ static void AtDesignTimeChanged(DependencyObject d, DependencyPropertyChangedEve return; #if XFORMS d.SetBinding(DataContextProperty, String.Empty); +#elif AVALONIA + d.Bind(DataContextProperty, new Binding()); #else BindingOperations.SetBinding(d, DataContextProperty, new Binding()); #endif } static readonly DependencyProperty DataContextProperty = +#if AVALONIA + AvaloniaProperty.RegisterAttached("DataContext", typeof(Bind)); +#else DependencyPropertyHelper.RegisterAttached( "DataContext", typeof(object), typeof(Bind), null, DataContextChanged); +#endif static void DataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - if (!View.InDesignMode) + if (!Caliburn.Micro.View.InDesignMode) return; var enable = d.GetValue(AtDesignTimeProperty); diff --git a/src/Caliburn.Micro.Platform/BindingScope.cs b/src/Caliburn.Micro.Platform/BindingScope.cs index a5ca157fd..baf393910 100644 --- a/src/Caliburn.Micro.Platform/BindingScope.cs +++ b/src/Caliburn.Micro.Platform/BindingScope.cs @@ -8,6 +8,14 @@ using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Media; +#elif AVALONIA + using Avalonia; + using Avalonia.Diagnostics; + using Avalonia.VisualTree; + using Avalonia.Controls; + using Avalonia.Controls.Presenters; + using DependencyObject = Avalonia.IAvaloniaObject; + using FrameworkElement = Avalonia.Controls.Control; #else using System.Windows; using System.Windows.Controls; @@ -26,7 +34,7 @@ static BindingScope() { AddChildResolver(e => new[] { e.Content as DependencyObject }); AddChildResolver(e => e.Items.OfType().ToArray() ); -#if !WINDOWS_UWP +#if !WINDOWS_UWP && !AVALONIA AddChildResolver(e => new[] { e.Header as DependencyObject }); AddChildResolver(e => new[] { e.Header as DependencyObject }); #endif @@ -130,8 +138,11 @@ public static bool RemoveChildResolver(ChildResolver resolver) { /// Gets the parent of the given object in the Visual Tree. /// /// The parent of the given object in the Visual Tree +#if AVALONIA + public static Func GetVisualParent = e => ((IVisual)e).GetVisualParent() as DependencyObject; +#else public static Func GetVisualParent = e => VisualTreeHelper.GetParent(e); - +#endif /// /// Finds a set of named instances in each hop in a . /// @@ -172,6 +183,22 @@ public static bool RemoveChildResolver(ChildResolver resolver) { continue; } +#if AVALONIA + var visual = current as IVisual; + var childCount = visual != null + ? visual.GetVisualChildren().Count() : 0; + + if (childCount > 0) + { + foreach (var childDo in visual.GetVisualChildren().OfType()) + { + queue.Enqueue(childDo); + } + + } +#else + + #if NET || NETCORE var childCount = (current is Visual || current is Visual3D) ? VisualTreeHelper.GetChildrenCount(current) : 0; @@ -179,8 +206,10 @@ public static bool RemoveChildResolver(ChildResolver resolver) { var childCount = (current is UIElement) ? VisualTreeHelper.GetChildrenCount(current) : 0; #endif - if (childCount > 0) { - for (var i = 0; i < childCount; i++) { + if (childCount > 0) + { + for (var i = 0; i < childCount; i++) + { var childDo = VisualTreeHelper.GetChild(current, i); queue.Enqueue(childDo); } @@ -197,6 +226,7 @@ public static bool RemoveChildResolver(ChildResolver resolver) { } #endif } +#endif else { var currentType = current.GetType(); @@ -292,10 +322,12 @@ private static IEnumerable ResolveHub(Hub hub) { if (root is UserControl) break; +#if !AVALONIA if (root is Page) { root = ((Page) root).Content as DependencyObject ?? root; break; } +#endif if ((bool) root.GetValue(View.IsScopeRootProperty)) break; diff --git a/src/Caliburn.Micro.Platform/ChildResolver.cs b/src/Caliburn.Micro.Platform/ChildResolver.cs index 58185ce5d..ac03d8067 100644 --- a/src/Caliburn.Micro.Platform/ChildResolver.cs +++ b/src/Caliburn.Micro.Platform/ChildResolver.cs @@ -6,6 +6,9 @@ #else using System.Windows; #endif +#if AVALONIA +using DependencyObject = Avalonia.IAvaloniaObject; +#endif namespace Caliburn.Micro { diff --git a/src/Caliburn.Micro.Platform/ConventionManager.cs b/src/Caliburn.Micro.Platform/ConventionManager.cs index 79760ca4c..c5a529ea9 100644 --- a/src/Caliburn.Micro.Platform/ConventionManager.cs +++ b/src/Caliburn.Micro.Platform/ConventionManager.cs @@ -1,4 +1,5 @@ -namespace Caliburn.Micro { +namespace Caliburn.Micro +{ using System; using System.Collections.Generic; using System.Linq; @@ -11,6 +12,21 @@ using Windows.UI.Xaml.Markup; using EventTrigger = Microsoft.Xaml.Interactions.Core.EventTriggerBehavior; using Windows.UI.Xaml.Shapes; +#elif AVALONIA + using System.Collections.Specialized; + using Avalonia.Xaml.Interactions.Core; + using Avalonia; + using Avalonia.Data; + using Avalonia.Controls; + using Avalonia.Controls.Primitives; + using Avalonia.Controls.Shapes; + using Avalonia.Markup.Xaml.Templates; + using XamlReader = Avalonia.Markup.Xaml.AvaloniaRuntimeXamlLoader; + using FrameworkElement = Avalonia.Controls.Control; + using DependencyObject = Avalonia.AvaloniaObject; + using DependencyProperty = Avalonia.AvaloniaProperty; + using ButtonBase = Avalonia.Controls.Button; + using Selector = Avalonia.Controls.Primitives.SelectingItemsControl; #else using System.ComponentModel; using System.Windows; @@ -18,24 +34,26 @@ using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Markup; - using System.Windows.Shapes; + using System.Windows.Shapes; using EventTrigger = Microsoft.Xaml.Behaviors.EventTrigger; #endif -#if !WINDOWS_UWP +#if !WINDOWS_UWP && !AVALONIA using System.Windows.Documents; #endif /// /// Used to configure the conventions used by the framework to apply bindings and create actions. /// - public static class ConventionManager { + public static class ConventionManager + { static readonly ILog Log = LogManager.GetLog(typeof(ConventionManager)); +#if !AVALONIA /// /// Converters to/from . /// public static IValueConverter BooleanToVisibilityConverter = new BooleanToVisibilityConverter(); - +#endif /// /// Indicates whether or not static properties should be included during convention name matching. /// @@ -61,6 +79,10 @@ public static class ConventionManager { "" + "" + "" +#elif AVALONIA + " " + + "" #else " " + @@ -78,8 +100,12 @@ public static class ConventionManager { #else XamlReader.Parse( #endif +#if AVALONIA + "" +#else "" - ); +#endif +); static readonly Dictionary ElementConventions = new Dictionary(); @@ -121,8 +147,12 @@ public static class ConventionManager { ApplyValidation(binding, viewModelType, property); ApplyUpdateSourceTrigger(bindableProperty, element, binding, property); +#if AVALONIA + element.Bind(bindableProperty, binding); +#else BindingOperations.SetBinding(element, bindableProperty, binding); - }; +#endif +}; /// /// Applies the appropriate binding mode to the binding. @@ -147,7 +177,7 @@ public static class ConventionManager { binding.ValidatesOnExceptions = true; } #endif -#if !WINDOWS_UWP +#if !WINDOWS_UWP && !AVALONIA if (typeof(IDataErrorInfo).IsAssignableFrom(viewModelType)) { binding.ValidatesOnDataErrors = true; binding.ValidatesOnExceptions = true; @@ -159,9 +189,11 @@ public static class ConventionManager { /// Determines whether a value converter is is needed and applies one to the binding. /// public static Action ApplyValueConverter = (binding, bindableProperty, property) => { +#if !AVALONIA if (bindableProperty == UIElement.VisibilityProperty && typeof(bool).IsAssignableFrom(property.PropertyType)) binding.Converter = BooleanToVisibilityConverter; - }; +#endif +}; /// /// Determines whether a custom string format is needed and applies it to the binding. @@ -177,12 +209,19 @@ public static class ConventionManager { /// Determines whether a custom update source trigger should be applied to the binding. /// public static Action ApplyUpdateSourceTrigger = (bindableProperty, element, binding, info) => { -#if WINDOWS_UWP || NET || NETCORE +#if (WINDOWS_UWP || NET || NETCORE) && !AVALONIA binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged; #endif }; static ConventionManager() { +#if AVALONIA + var itemsControlSourceProperty = ItemsControl.ItemsProperty; + var loadedEvent = "AttachedToLogicalTree"; +#else + var itemsControlSourceProperty = ItemsControl.ItemsSourceProperty; + var loadedEvent = "Loaded"; +#endif #if WINDOWS_UWP AddElementConvention(SplitView.ContentProperty, "IsPaneOpen", "PaneClosing").GetBindableProperty = delegate (DependencyObject foundControl) @@ -227,20 +266,34 @@ static ConventionManager() { AddElementConvention(HyperlinkButton.ContentProperty, "DataContext", "Click"); AddElementConvention(PasswordBox.PasswordProperty, "Password", "PasswordChanged"); #else - AddElementConvention(DocumentViewer.DocumentProperty, "DataContext", "Loaded"); +#if !AVALONIA + AddElementConvention(DocumentViewer.DocumentProperty, "DataContext", loadedEvent); AddElementConvention(null, "Password", "PasswordChanged"); AddElementConvention(Hyperlink.DataContextProperty, "DataContext", "Click"); AddElementConvention(RichTextBox.DataContextProperty, "DataContext", "TextChanged"); + AddElementConvention(Menu.ItemsSourceProperty,"DataContext", "Click"); AddElementConvention(MenuItem.ItemsSourceProperty, "DataContext", "Click"); +#else + AddElementConvention(Menu.ItemsProperty, "DataContext", "Click"); + AddElementConvention(MenuItem.ItemsProperty, "DataContext", "Click"); +#endif AddElementConvention