From e8e7615abe0e4c4d13816d0db94e4398a50548d8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 4 Dec 2024 23:20:01 -0800 Subject: [PATCH 1/8] Move Avalonia project --- .../AppBuilderExtensions.cs | 29 +++ src/ReactiveUI.Avalonia/Attributes.cs | 4 + .../AutoDataTemplateBindingHook.cs | 59 ++++++ src/ReactiveUI.Avalonia/AutoSuspendHelper.cs | 89 +++++++++ .../AvaloniaActivationForViewFetcher.cs | 75 +++++++ .../AvaloniaObjectReactiveExtensions.cs | 110 +++++++++++ src/ReactiveUI.Avalonia/AvaloniaScheduler.cs | 92 +++++++++ .../ReactiveUI.Avalonia.csproj | 14 ++ .../ReactiveUserControl.cs | 66 +++++++ src/ReactiveUI.Avalonia/ReactiveWindow.cs | 66 +++++++ src/ReactiveUI.Avalonia/RoutedViewHost.cs | 184 ++++++++++++++++++ src/ReactiveUI.Avalonia/ViewModelViewHost.cs | 129 ++++++++++++ src/ReactiveUI.sln | 18 ++ src/RxUI.DotSettings | 5 +- 14 files changed, 939 insertions(+), 1 deletion(-) create mode 100644 src/ReactiveUI.Avalonia/AppBuilderExtensions.cs create mode 100644 src/ReactiveUI.Avalonia/Attributes.cs create mode 100644 src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs create mode 100644 src/ReactiveUI.Avalonia/AutoSuspendHelper.cs create mode 100644 src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs create mode 100644 src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs create mode 100644 src/ReactiveUI.Avalonia/AvaloniaScheduler.cs create mode 100644 src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj create mode 100644 src/ReactiveUI.Avalonia/ReactiveUserControl.cs create mode 100644 src/ReactiveUI.Avalonia/ReactiveWindow.cs create mode 100644 src/ReactiveUI.Avalonia/RoutedViewHost.cs create mode 100644 src/ReactiveUI.Avalonia/ViewModelViewHost.cs diff --git a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs new file mode 100644 index 0000000000..915e7cdea1 --- /dev/null +++ b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs @@ -0,0 +1,29 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using ReactiveUI; +using Splat; + +namespace ReactiveUI.Avalonia +{ + public static class AppBuilderExtensions + { + /// + /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia + /// scheduler, an activation for view fetcher, a template binding hook. Remember + /// to call this method if you are using ReactiveUI in your application. + /// + public static AppBuilder UseReactiveUI(this AppBuilder builder) => + builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => + { + if (Locator.CurrentMutable is null) + { + return; + } + + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + })); + } +} diff --git a/src/ReactiveUI.Avalonia/Attributes.cs b/src/ReactiveUI.Avalonia/Attributes.cs new file mode 100644 index 0000000000..b44ec9b824 --- /dev/null +++ b/src/ReactiveUI.Avalonia/Attributes.cs @@ -0,0 +1,4 @@ +using System.Reflection; +using Avalonia.Metadata; + +[assembly: XmlnsDefinition("http://reactiveui.net", "ReactiveUI.Avalonia")] diff --git a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..f1286526ef --- /dev/null +++ b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Layout; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace ReactiveUI.Avalonia +{ + /// + /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls + /// that don't have DataTemplates, and assigns a default DataTemplate that + /// loads the View associated with each ViewModel. + /// + public class AutoDataTemplateBindingHook : IPropertyBindingHook + { + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate((x, _) => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(Control.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); + + /// + public bool ExecuteHook( + object? source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + var itemsControl = lastViewProperty?.Sender as ItemsControl; + if (itemsControl == null) + return true; + + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + return true; + + if (itemsControl.ItemTemplate != null) + return true; + + if (itemsControl.DataTemplates != null && + itemsControl.DataTemplates.Count > 0) + return true; + + itemsControl.ItemTemplate = DefaultItemTemplate; + return true; + } + } +} diff --git a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs new file mode 100644 index 0000000000..7f069287ab --- /dev/null +++ b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs @@ -0,0 +1,89 @@ +using Avalonia; +using Avalonia.VisualTree; +using Avalonia.Controls; +using System.Threading; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using System.Reactive.Linq; +using ReactiveUI; +using System; +using Avalonia.Controls.ApplicationLifetimes; +using Splat; + +namespace ReactiveUI.Avalonia +{ + /// + /// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for + /// Avalonia applications. Call its constructor in your app's composition root, + /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. + /// + public sealed class AutoSuspendHelper : IEnableLogger, IDisposable + { + private readonly Subject _shouldPersistState = new Subject(); + private readonly Subject _isLaunchingNew = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + /// Pass in the Application.ApplicationLifetime property. + public AutoSuspendHelper(IApplicationLifetime lifetime) + { + RxApp.SuspensionHost.IsResuming = Observable.Never(); + RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; + + if (Avalonia.Controls.Design.IsDesignMode) + { + this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state."); + RxApp.SuspensionHost.ShouldPersistState = Observable.Never(); + } + else if (lifetime is IControlledApplicationLifetime controlled) + { + this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); + controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); + RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; + } + else if (lifetime != null) + { + var type = lifetime.GetType().FullName; + var message = $"Don't know how to detect app exit event for {type}."; + throw new NotSupportedException(message); + } + else + { + var message = "ApplicationLifetime is null. " + + "Ensure you are initializing AutoSuspendHelper " + + "after Avalonia application initialization is completed."; + throw new ArgumentNullException(message); + } + + var errored = new Subject(); + AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); + RxApp.SuspensionHost.ShouldInvalidateState = errored; + } + + /// + /// Call this method in your App.OnFrameworkInitializationCompleted method. + /// + public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); + + /// + /// Disposes internally stored observers. + /// + public void Dispose() + { + _shouldPersistState.Dispose(); + _isLaunchingNew.Dispose(); + } + + private void OnControlledApplicationLifetimeExit() + { + this.Log().Debug("Received IControlledApplicationLifetime exit event."); + var manual = new ManualResetEvent(false); + _shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); + + manual.WaitOne(); + this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); + } + } +} diff --git a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs new file mode 100644 index 0000000000..05005423b1 --- /dev/null +++ b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs @@ -0,0 +1,75 @@ +using System; +using System.Reactive.Linq; +using Avalonia.VisualTree; +using Avalonia.Controls; +using Avalonia.Interactivity; +using ReactiveUI; + +namespace ReactiveUI.Avalonia +{ + /// + /// Determines when Avalonia IVisuals get activated. + /// + public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher + { + /// + /// Returns affinity for view. + /// + public int GetAffinityForView(Type view) + { + return typeof(Visual).IsAssignableFrom(view) ? 10 : 0; + } + + /// + /// Returns activation observable for activatable Avalonia view. + /// + public IObservable GetActivationForView(IActivatableView view) + { + if (!(view is Visual visual)) return Observable.Return(false); + if (view is Control control) return GetActivationForControl(control); + return GetActivationForVisual(visual); + } + + /// + /// Listens to Loaded and Unloaded + /// events for Avalonia Control. + /// + private IObservable GetActivationForControl(Control control) + { + var controlLoaded = Observable + .FromEventPattern( + x => control.Loaded += x, + x => control.Loaded -= x) + .Select(args => true); + var controlUnloaded = Observable + .FromEventPattern( + x => control.Unloaded += x, + x => control.Unloaded -= x) + .Select(args => false); + return controlLoaded + .Merge(controlUnloaded) + .DistinctUntilChanged(); + } + + /// + /// Listens to AttachedToVisualTree and DetachedFromVisualTree + /// events for Avalonia IVisuals. + /// + private IObservable GetActivationForVisual(Visual visual) + { + var visualLoaded = Observable + .FromEventPattern( + x => visual.AttachedToVisualTree += x, + x => visual.AttachedToVisualTree -= x) + .Select(args => true); + var visualUnloaded = Observable + .FromEventPattern( + x => visual.DetachedFromVisualTree += x, + x => visual.DetachedFromVisualTree -= x) + .Select(args => false); + return visualLoaded + .Merge(visualUnloaded) + .DistinctUntilChanged(); + } + } +} diff --git a/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs b/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs new file mode 100644 index 0000000000..8a5131748b --- /dev/null +++ b/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs @@ -0,0 +1,110 @@ +using System.Reactive; +using System.Reactive.Subjects; +using Avalonia.Data; + +namespace ReactiveUI.Avalonia; + +public static class AvaloniaObjectReactiveExtensions +{ + /// + /// Gets a subject for an . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); + } + + /// + /// Gets a subject for an . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject GetSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } + + /// + /// Gets a subject for a . + /// + /// The property type. + /// The object. + /// The property. + /// + /// The priority with which binding values are written to the object. + /// + /// + /// An which can be used for two-way binding to/from the + /// property. + /// + public static ISubject> GetBindingSubject( + this AvaloniaObject o, + AvaloniaProperty property, + BindingPriority priority = BindingPriority.LocalValue) + { + return Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); + } +} diff --git a/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs b/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs new file mode 100644 index 0000000000..f598491b77 --- /dev/null +++ b/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs @@ -0,0 +1,92 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Threading; + +namespace ReactiveUI.Avalonia +{ + /// + /// A reactive scheduler that uses Avalonia's . + /// + public class AvaloniaScheduler : LocalScheduler + { + /// + /// Users can schedule actions on the dispatcher thread while being on the correct thread already. + /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases. + /// To prevent this we are limiting amount of reentrant calls to before we will + /// schedule on a dispatcher anyway. + /// + private const int MaxReentrantSchedules = 32; + + private int _reentrancyGuard; + + /// + /// The instance of the . + /// + public static readonly AvaloniaScheduler Instance = new AvaloniaScheduler(); + + /// + /// Initializes a new instance of the class. + /// + private AvaloniaScheduler() + { + } + + /// + public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + IDisposable PostOnDispatcher() + { + var composite = new CompositeDisposable(2); + + var cancellation = new CancellationDisposable(); + + Dispatcher.UIThread.Post(() => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, DispatcherPriority.Background); + + composite.Add(cancellation); + + return composite; + } + + if (dueTime == TimeSpan.Zero) + { + if (!Dispatcher.UIThread.CheckAccess()) + { + return PostOnDispatcher(); + } + else + { + if (_reentrancyGuard >= MaxReentrantSchedules) + { + return PostOnDispatcher(); + } + + try + { + _reentrancyGuard++; + + return action(this, state); + } + finally + { + _reentrancyGuard--; + } + } + } + else + { + var composite = new CompositeDisposable(2); + + composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + + return composite; + } + } + } +} diff --git a/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj b/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj new file mode 100644 index 0000000000..eea1a05d9b --- /dev/null +++ b/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj @@ -0,0 +1,14 @@ + + + $(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0 + false + + + + + + + + + + diff --git a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs new file mode 100644 index 0000000000..0f828a1608 --- /dev/null +++ b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia.Controls; +using ReactiveUI; + +namespace ReactiveUI.Avalonia +{ + /// + /// A ReactiveUI that implements the interface and + /// will activate your ViewModel automatically if the view model implements . + /// When the DataContext property changes, this class will update the ViewModel property with the new DataContext + /// value, and vice versa. + /// + /// ViewModel type. + public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty + .Register, TViewModel?>(nameof(ViewModel)); + + /// + /// Initializes a new instance of the class. + /// + public ReactiveUserControl() + { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + } + + /// + /// The ViewModel. + /// + public TViewModel? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DataContextProperty) + { + if (ReferenceEquals(change.OldValue, ViewModel) + && change.NewValue is null or TViewModel) + { + SetCurrentValue(ViewModelProperty, change.NewValue); + } + } + else if (change.Property == ViewModelProperty) + { + if (ReferenceEquals(change.OldValue, DataContext)) + { + SetCurrentValue(DataContextProperty, change.NewValue); + } + } + } + } +} diff --git a/src/ReactiveUI.Avalonia/ReactiveWindow.cs b/src/ReactiveUI.Avalonia/ReactiveWindow.cs new file mode 100644 index 0000000000..7faa1c09d3 --- /dev/null +++ b/src/ReactiveUI.Avalonia/ReactiveWindow.cs @@ -0,0 +1,66 @@ +using System; +using Avalonia.Controls; +using ReactiveUI; + +namespace ReactiveUI.Avalonia +{ + /// + /// A ReactiveUI that implements the interface and will + /// activate your ViewModel automatically if the view model implements . When + /// the DataContext property changes, this class will update the ViewModel property with the new DataContext value, + /// and vice versa. + /// + /// ViewModel type. + public class ReactiveWindow : Window, IViewFor where TViewModel : class + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty + .Register, TViewModel?>(nameof(ViewModel)); + + /// + /// Initializes a new instance of the class. + /// + public ReactiveWindow() + { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + } + + /// + /// The ViewModel. + /// + public TViewModel? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == DataContextProperty) + { + if (ReferenceEquals(change.OldValue, ViewModel) + && change.NewValue is null or TViewModel) + { + SetCurrentValue(ViewModelProperty, change.NewValue); + } + } + else if (change.Property == ViewModelProperty) + { + if (ReferenceEquals(change.OldValue, DataContext)) + { + SetCurrentValue(DataContextProperty, change.NewValue); + } + } + } + } +} diff --git a/src/ReactiveUI.Avalonia/RoutedViewHost.cs b/src/ReactiveUI.Avalonia/RoutedViewHost.cs new file mode 100644 index 0000000000..193b3bae82 --- /dev/null +++ b/src/ReactiveUI.Avalonia/RoutedViewHost.cs @@ -0,0 +1,184 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia; +using ReactiveUI; +using Splat; + +namespace ReactiveUI.Avalonia +{ + /// + /// This control hosts the View associated with ReactiveUI RoutingState, + /// and will display the View and wire up the ViewModel whenever a new + /// ViewModel is navigated to. Nested routing is also supported. + /// + /// + /// + /// ReactiveUI routing consists of an IScreen that contains current + /// RoutingState, several IRoutableViewModels, and a platform-specific + /// XAML control called RoutedViewHost. + /// + /// + /// RoutingState manages the ViewModel navigation stack and allows + /// ViewModels to navigate to other ViewModels. IScreen is the root of + /// a navigation stack; despite the name, its views don't have to occupy + /// the whole screen. RoutedViewHost monitors an instance of RoutingState, + /// responding to any changes in the navigation stack by creating and + /// embedding the appropriate view. + /// + /// + /// Place this control to a view containing your ViewModel that implements + /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// See + /// ReactiveUI routing documentation website for more info. + /// + /// + public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger + { + /// + /// for the property. + /// + public static readonly StyledProperty RouterProperty = + AvaloniaProperty.Register(nameof(Router)); + + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + + /// + /// for the property. + /// + public static readonly StyledProperty DefaultContentProperty = + ViewModelViewHost.DefaultContentProperty.AddOwner(); + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() + { + this.WhenActivated(disposables => + { + var routerRemoved = this + .WhenAnyValue(x => x.Router) + .Where(router => router == null)! + .Cast(); + + var viewContract = this.WhenAnyValue(x => x.ViewContract); + + this.WhenAnyValue(x => x.Router) + .Where(router => router != null) + .SelectMany(router => router!.CurrentViewModel) + .Merge(routerRemoved) + .CombineLatest(viewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second)) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState? Router + { + get => GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object? DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator? ViewLocator { get; set; } + + protected override Type StyleKeyOverride => typeof(TransitioningContentControl); + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) + { + if (Router == null) + { + this.Log().Warn("Router property is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel, contract); + if (viewInstance == null) + { + if (contract == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + } + else + { + this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + } + + Content = DefaultContent; + return; + } + + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + + viewInstance.ViewModel = viewModel; + if (viewInstance is IDataContextProvider provider) + provider.DataContext = viewModel; + Content = viewInstance; + } + } +} diff --git a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs new file mode 100644 index 0000000000..6c15f29efa --- /dev/null +++ b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs @@ -0,0 +1,129 @@ +using System; +using System.Reactive.Disposables; + +using Avalonia.Controls; +using Avalonia.Styling; +using ReactiveUI; +using Splat; + +namespace ReactiveUI.Avalonia +{ + /// + /// This content control will automatically load the View associated with + /// the ViewModel property and display it. This control is very useful + /// inside a DataTemplate to display the View associated with a ViewModel. + /// + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + + /// + /// for the property. + /// + public static readonly StyledProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2)) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ViewModel to display. + /// + public object? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object? DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator? ViewLocator { get; set; } + + protected override Type StyleKeyOverride => typeof(TransitioningContentControl); + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel, contract); + if (viewInstance == null) + { + if (contract == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + } + else + { + this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + } + + Content = DefaultContent; + return; + } + + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + + viewInstance.ViewModel = viewModel; + if (viewInstance is StyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} diff --git a/src/ReactiveUI.sln b/src/ReactiveUI.sln index e60b06e689..4b3c496f51 100644 --- a/src/ReactiveUI.sln +++ b/src/ReactiveUI.sln @@ -49,6 +49,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.WinUI", "Reactiv EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.AndroidX", "ReactiveUI.AndroidX\ReactiveUI.AndroidX.csproj", "{3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Avalonia", "ReactiveUI.Avalonia\ReactiveUI.Avalonia.csproj", "{4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -279,6 +281,22 @@ Global {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x64.Build.0 = Release|Any CPU {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x86.ActiveCfg = Release|Any CPU {3F642A3D-CBA6-4E17-903C-672FDBE9D9E8}.Release|x86.Build.0 = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|arm64.ActiveCfg = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|arm64.Build.0 = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|x64.Build.0 = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Debug|x86.Build.0 = Debug|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|Any CPU.Build.0 = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|arm64.ActiveCfg = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|arm64.Build.0 = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|x64.ActiveCfg = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|x64.Build.0 = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|x86.ActiveCfg = Release|Any CPU + {4E50CFE5-26F2-4083-9809-6FF11CBFEE2F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/RxUI.DotSettings b/src/RxUI.DotSettings index 51718dc68a..314a4a12e0 100644 --- a/src/RxUI.DotSettings +++ b/src/RxUI.DotSettings @@ -15,7 +15,9 @@ NEVER <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Static, Instance" AccessRightKinds="Private" Description="Private Fields/Methods/Properties"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /><Kind Name="CONSTANT_FIELD" /><Kind Name="METHOD" /><Kind Name="PROPERTY" /><Kind Name="EVENT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></Policy> True True True @@ -24,4 +26,5 @@ True True True - True \ No newline at end of file + True + True \ No newline at end of file From 4f1e6caf4f3782028a648d7e34de05548fc835a8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 4 Dec 2024 23:31:42 -0800 Subject: [PATCH 2/8] Add file header, fix namespaces --- .../AppBuilderExtensions.cs | 9 ++++++--- src/ReactiveUI.Avalonia/Attributes.cs | 6 +++++- .../AutoDataTemplateBindingHook.cs | 9 ++++++--- src/ReactiveUI.Avalonia/AutoSuspendHelper.cs | 18 ++++++++++-------- .../AvaloniaActivationForViewFetcher.cs | 8 ++++++-- .../AvaloniaObjectReactiveExtensions.cs | 8 +++++++- .../ReactiveUI.Avalonia.csproj | 15 +++++++-------- src/ReactiveUI.Avalonia/ReactiveUserControl.cs | 8 ++++++-- src/ReactiveUI.Avalonia/ReactiveWindow.cs | 8 ++++++-- src/ReactiveUI.Avalonia/RoutedViewHost.cs | 10 ++++++---- src/ReactiveUI.Avalonia/ViewModelViewHost.cs | 9 ++++++--- 11 files changed, 71 insertions(+), 37 deletions(-) diff --git a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs index 915e7cdea1..0abbcc3d89 100644 --- a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs +++ b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs @@ -1,6 +1,9 @@ -using Avalonia.Controls; -using Avalonia.Threading; -using ReactiveUI; +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia; using Splat; namespace ReactiveUI.Avalonia diff --git a/src/ReactiveUI.Avalonia/Attributes.cs b/src/ReactiveUI.Avalonia/Attributes.cs index b44ec9b824..16debf28bb 100644 --- a/src/ReactiveUI.Avalonia/Attributes.cs +++ b/src/ReactiveUI.Avalonia/Attributes.cs @@ -1,4 +1,8 @@ -using System.Reflection; +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + using Avalonia.Metadata; [assembly: XmlnsDefinition("http://reactiveui.net", "ReactiveUI.Avalonia")] diff --git a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs index f1286526ef..e0b489daca 100644 --- a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs +++ b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs @@ -1,11 +1,14 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + using System; using System.Linq; +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Layout; -using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.Templates; -using ReactiveUI; namespace ReactiveUI.Avalonia { diff --git a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs index 7f069287ab..4c04e25609 100644 --- a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs +++ b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs @@ -1,13 +1,15 @@ -using Avalonia; -using Avalonia.VisualTree; -using Avalonia.Controls; -using System.Threading; +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; using System.Reactive; using System.Reactive.Disposables; -using System.Reactive.Subjects; using System.Reactive.Linq; -using ReactiveUI; -using System; +using System.Reactive.Subjects; +using System.Threading; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Splat; @@ -32,7 +34,7 @@ public AutoSuspendHelper(IApplicationLifetime lifetime) RxApp.SuspensionHost.IsResuming = Observable.Never(); RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; - if (Avalonia.Controls.Design.IsDesignMode) + if (Design.IsDesignMode) { this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state."); RxApp.SuspensionHost.ShouldPersistState = Observable.Never(); diff --git a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs index 05005423b1..9d6c5dbd2c 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs @@ -1,9 +1,13 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + using System; using System.Reactive.Linq; -using Avalonia.VisualTree; +using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; -using ReactiveUI; namespace ReactiveUI.Avalonia { diff --git a/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs b/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs index 8a5131748b..65e3443cd2 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs @@ -1,5 +1,11 @@ -using System.Reactive; +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive; using System.Reactive.Subjects; +using Avalonia; using Avalonia.Data; namespace ReactiveUI.Avalonia; diff --git a/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj b/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj index eea1a05d9b..56a859ed3e 100644 --- a/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj +++ b/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj @@ -1,14 +1,13 @@  - $(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0 - false + $(BaseTargetFrameworks) + Contains the ReactiveUI platform specific extensions for Avalonia + mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;avalonia;net;netcore - + + + + - - - - - diff --git a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs index 0f828a1608..09167620cd 100644 --- a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs +++ b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs @@ -1,6 +1,10 @@ -using System; +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia; using Avalonia.Controls; -using ReactiveUI; namespace ReactiveUI.Avalonia { diff --git a/src/ReactiveUI.Avalonia/ReactiveWindow.cs b/src/ReactiveUI.Avalonia/ReactiveWindow.cs index 7faa1c09d3..f01b42723f 100644 --- a/src/ReactiveUI.Avalonia/ReactiveWindow.cs +++ b/src/ReactiveUI.Avalonia/ReactiveWindow.cs @@ -1,6 +1,10 @@ -using System; +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia; using Avalonia.Controls; -using ReactiveUI; namespace ReactiveUI.Avalonia { diff --git a/src/ReactiveUI.Avalonia/RoutedViewHost.cs b/src/ReactiveUI.Avalonia/RoutedViewHost.cs index 193b3bae82..f64cc3cf7a 100644 --- a/src/ReactiveUI.Avalonia/RoutedViewHost.cs +++ b/src/ReactiveUI.Avalonia/RoutedViewHost.cs @@ -1,11 +1,13 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + using System; using System.Reactive.Disposables; using System.Reactive.Linq; -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Styling; using Avalonia; -using ReactiveUI; +using Avalonia.Controls; using Splat; namespace ReactiveUI.Avalonia diff --git a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs index 6c15f29efa..cfe411f9d2 100644 --- a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs +++ b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs @@ -1,9 +1,12 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + using System; using System.Reactive.Disposables; - +using Avalonia; using Avalonia.Controls; -using Avalonia.Styling; -using ReactiveUI; using Splat; namespace ReactiveUI.Avalonia From 674bd2689c494ac3157810f4e2dbd609350e1c44 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 4 Dec 2024 23:31:51 -0800 Subject: [PATCH 3/8] Add PackageVersion for Avalonia --- src/Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 75e38c472c..e00fc99908 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -43,6 +43,7 @@ + From 48bfcd4b95aa743cb3d41d027d0022eb222a0cf3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 4 Dec 2024 23:58:16 -0800 Subject: [PATCH 4/8] Reformat code, add missing docs, fix build warnigns-as-errors --- .../AppBuilderExtensions.cs | 23 ++++-- .../AutoDataTemplateBindingHook.cs | 39 +++++++--- src/ReactiveUI.Avalonia/AutoSuspendHelper.cs | 10 +-- .../AvaloniaActivationForViewFetcher.cs | 73 ++++++++++--------- .../AvaloniaObjectReactiveExtensions.cs | 70 +++++++++--------- src/ReactiveUI.Avalonia/AvaloniaScheduler.cs | 71 ++++++++++-------- .../ReactiveUI.Avalonia.csproj | 4 +- .../ReactiveUserControl.cs | 14 +++- src/ReactiveUI.Avalonia/ReactiveWindow.cs | 16 ++-- src/ReactiveUI.Avalonia/RoutedViewHost.cs | 18 +++-- src/ReactiveUI.Avalonia/ViewModelViewHost.cs | 10 ++- 11 files changed, 204 insertions(+), 144 deletions(-) diff --git a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs index 0abbcc3d89..955ca655a4 100644 --- a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs +++ b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs @@ -3,20 +3,32 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System; using Avalonia; using Splat; namespace ReactiveUI.Avalonia { + /// + /// Avalonia AppBuilder setup extensions. + /// public static class AppBuilderExtensions { /// - /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia - /// scheduler, an activation for view fetcher, a template binding hook. Remember - /// to call this method if you are using ReactiveUI in your application. + /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia scheduler, + /// an activation for view fetcher, a template binding hook. + /// Remember to call this method if you are using ReactiveUI in your application. /// - public static AppBuilder UseReactiveUI(this AppBuilder builder) => - builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => + /// This builder. + /// The builder. + public static AppBuilder UseReactiveUI(this AppBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => { if (Locator.CurrentMutable is null) { @@ -28,5 +40,6 @@ public static AppBuilder UseReactiveUI(this AppBuilder builder) => Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); })); + } } } diff --git a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs index e0b489daca..16470b1d64 100644 --- a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs +++ b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs @@ -19,41 +19,56 @@ namespace ReactiveUI.Avalonia /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate((x, _) => - { - var control = new ViewModelViewHost(); - var context = control.GetObservable(Control.DataContextProperty); - control.Bind(ViewModelViewHost.ViewModelProperty, context); - control.HorizontalContentAlignment = HorizontalAlignment.Stretch; - control.VerticalContentAlignment = VerticalAlignment.Stretch; - return control; - }, - true); + private static readonly FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate( + (_, _) => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(StyledElement.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); /// public bool ExecuteHook( - object? source, object target, + object? source, + object target, Func[]> getCurrentViewModelProperties, Func[]> getCurrentViewProperties, BindingDirection direction) { + if (getCurrentViewProperties is null) + { + throw new ArgumentNullException(nameof(getCurrentViewProperties)); + } + var viewProperties = getCurrentViewProperties(); var lastViewProperty = viewProperties.LastOrDefault(); var itemsControl = lastViewProperty?.Sender as ItemsControl; if (itemsControl == null) + { return true; + } var propertyName = viewProperties.Last().GetPropertyName(); if (propertyName != "Items" && propertyName != "ItemsSource") + { return true; - + } + if (itemsControl.ItemTemplate != null) + { return true; + } if (itemsControl.DataTemplates != null && itemsControl.DataTemplates.Count > 0) + { return true; + } itemsControl.ItemTemplate = DefaultItemTemplate; return true; diff --git a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs index 4c04e25609..948784b012 100644 --- a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs +++ b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs @@ -24,7 +24,7 @@ public sealed class AutoSuspendHelper : IEnableLogger, IDisposable { private readonly Subject _shouldPersistState = new Subject(); private readonly Subject _isLaunchingNew = new Subject(); - + /// /// Initializes a new instance of the class. /// @@ -54,11 +54,11 @@ public AutoSuspendHelper(IApplicationLifetime lifetime) else { var message = "ApplicationLifetime is null. " - + "Ensure you are initializing AutoSuspendHelper " - + "after Avalonia application initialization is completed."; + + "Ensure you are initializing AutoSuspendHelper " + + "after Avalonia application initialization is completed."; throw new ArgumentNullException(message); } - + var errored = new Subject(); AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); RxApp.SuspensionHost.ShouldInvalidateState = errored; @@ -83,7 +83,7 @@ private void OnControlledApplicationLifetimeExit() this.Log().Debug("Received IControlledApplicationLifetime exit event."); var manual = new ManualResetEvent(false); _shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); - + manual.WaitOne(); this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); } diff --git a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs index 9d6c5dbd2c..8e568aa51f 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs @@ -16,64 +16,65 @@ namespace ReactiveUI.Avalonia /// public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher { - /// - /// Returns affinity for view. - /// - public int GetAffinityForView(Type view) - { - return typeof(Visual).IsAssignableFrom(view) ? 10 : 0; - } + /// + public int GetAffinityForView(Type view) => typeof(Visual).IsAssignableFrom(view) ? 10 : 0; - /// - /// Returns activation observable for activatable Avalonia view. - /// + /// public IObservable GetActivationForView(IActivatableView view) { - if (!(view is Visual visual)) return Observable.Return(false); - if (view is Control control) return GetActivationForControl(control); + if (view is not Visual visual) + { + return Observable.Return(false); + } + + if (view is Control control) + { + return GetActivationForControl(control); + } + return GetActivationForVisual(visual); } /// - /// Listens to Loaded and Unloaded + /// Listens to Loaded and Unloaded /// events for Avalonia Control. /// - private IObservable GetActivationForControl(Control control) + private static IObservable GetActivationForControl(Control control) { var controlLoaded = Observable - .FromEventPattern( - x => control.Loaded += x, - x => control.Loaded -= x) - .Select(args => true); + .FromEventPattern( + x => control.Loaded += x, + x => control.Loaded -= x) + .Select(args => true); var controlUnloaded = Observable - .FromEventPattern( - x => control.Unloaded += x, - x => control.Unloaded -= x) - .Select(args => false); + .FromEventPattern( + x => control.Unloaded += x, + x => control.Unloaded -= x) + .Select(args => false); return controlLoaded - .Merge(controlUnloaded) - .DistinctUntilChanged(); + .Merge(controlUnloaded) + .DistinctUntilChanged(); } /// - /// Listens to AttachedToVisualTree and DetachedFromVisualTree + /// Listens to AttachedToVisualTree and DetachedFromVisualTree /// events for Avalonia IVisuals. /// - private IObservable GetActivationForVisual(Visual visual) + private static IObservable GetActivationForVisual(Visual visual) { var visualLoaded = Observable - .FromEventPattern( - x => visual.AttachedToVisualTree += x, - x => visual.AttachedToVisualTree -= x) - .Select(args => true); + .FromEventPattern( + x => visual.AttachedToVisualTree += x, + x => visual.AttachedToVisualTree -= x) + .Select(args => true); var visualUnloaded = Observable - .FromEventPattern( - x => visual.DetachedFromVisualTree += x, - x => visual.DetachedFromVisualTree -= x) - .Select(args => false); + .FromEventPattern( + x => visual.DetachedFromVisualTree += x, + x => visual.DetachedFromVisualTree -= x) + .Select(args => false); return visualLoaded - .Merge(visualUnloaded) - .DistinctUntilChanged(); + .Merge(visualUnloaded) + .DistinctUntilChanged(); } } } diff --git a/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs b/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs index 65e3443cd2..7546d40247 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaObjectReactiveExtensions.cs @@ -10,6 +10,9 @@ namespace ReactiveUI.Avalonia; +/// +/// Avalonia reactive extensions. +/// public static class AvaloniaObjectReactiveExtensions { /// @@ -21,7 +24,7 @@ public static class AvaloniaObjectReactiveExtensions /// The priority with which binding values are written to the object. /// /// - /// An which can be used for two-way binding to/from the + /// An which can be used for two-way binding to/from the /// property. /// public static ISubject GetSubject( @@ -30,8 +33,8 @@ public static class AvaloniaObjectReactiveExtensions BindingPriority priority = BindingPriority.LocalValue) { return Subject.Create( - Observer.Create(x => o.SetValue(property, x, priority)), - o.GetObservable(property)); + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); } /// @@ -44,18 +47,15 @@ public static class AvaloniaObjectReactiveExtensions /// The priority with which binding values are written to the object. /// /// - /// An which can be used for two-way binding to/from the - /// property. + /// An which can be used for two-way binding to/from the property. /// public static ISubject GetSubject( this AvaloniaObject o, AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create( - Observer.Create(x => o.SetValue(property, x, priority)), - o.GetObservable(property)); - } + BindingPriority priority = BindingPriority.LocalValue) => + Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), + o.GetObservable(property)); /// /// Gets a subject for a . @@ -66,24 +66,22 @@ public static ISubject GetSubject( /// The priority with which binding values are written to the object. /// /// - /// An which can be used for two-way binding to/from the + /// An which can be used for two-way binding to/from the /// property. /// public static ISubject> GetBindingSubject( this AvaloniaObject o, AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create>( - Observer.Create>(x => - { - if (x.HasValue) - { - o.SetValue(property, x.Value, priority); - } - }), - o.GetBindingObservable(property)); - } + BindingPriority priority = BindingPriority.LocalValue) => + Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); /// /// Gets a subject for a . @@ -95,22 +93,20 @@ public static ISubject GetSubject( /// The priority with which binding values are written to the object. /// /// - /// An which can be used for two-way binding to/from the + /// An which can be used for two-way binding to/from the /// property. /// public static ISubject> GetBindingSubject( this AvaloniaObject o, AvaloniaProperty property, - BindingPriority priority = BindingPriority.LocalValue) - { - return Subject.Create>( - Observer.Create>(x => - { - if (x.HasValue) - { - o.SetValue(property, x.Value, priority); - } - }), - o.GetBindingObservable(property)); - } + BindingPriority priority = BindingPriority.LocalValue) => + Subject.Create>( + Observer.Create>(x => + { + if (x.HasValue) + { + o.SetValue(property, x.Value, priority); + } + }), + o.GetBindingObservable(property)); } diff --git a/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs b/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs index f598491b77..6aac9550fe 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs @@ -1,3 +1,8 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; @@ -10,6 +15,11 @@ namespace ReactiveUI.Avalonia /// public class AvaloniaScheduler : LocalScheduler { + /// + /// The instance of the . + /// + public static readonly AvaloniaScheduler Instance = new(); + /// /// Users can schedule actions on the dispatcher thread while being on the correct thread already. /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases. @@ -20,11 +30,6 @@ public class AvaloniaScheduler : LocalScheduler private int _reentrancyGuard; - /// - /// The instance of the . - /// - public static readonly AvaloniaScheduler Instance = new AvaloniaScheduler(); - /// /// Initializes a new instance of the class. /// @@ -33,21 +38,29 @@ private AvaloniaScheduler() } /// - public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + public override IDisposable Schedule( + TState state, TimeSpan dueTime, Func action) { + if (action is null) + { + throw new ArgumentNullException(nameof(action)); + } + IDisposable PostOnDispatcher() { var composite = new CompositeDisposable(2); var cancellation = new CancellationDisposable(); - Dispatcher.UIThread.Post(() => - { - if (!cancellation.Token.IsCancellationRequested) - { - composite.Add(action(this, state)); - } - }, DispatcherPriority.Background); + Dispatcher.UIThread.Post( + () => + { + if (!cancellation.Token.IsCancellationRequested) + { + composite.Add(action(this, state)); + } + }, + DispatcherPriority.Background); composite.Add(cancellation); @@ -60,26 +73,24 @@ IDisposable PostOnDispatcher() { return PostOnDispatcher(); } - else + + if (_reentrancyGuard >= MaxReentrantSchedules) { - if (_reentrancyGuard >= MaxReentrantSchedules) - { - return PostOnDispatcher(); - } - - try - { - _reentrancyGuard++; - - return action(this, state); - } - finally - { - _reentrancyGuard--; - } + return PostOnDispatcher(); + } + + try + { + _reentrancyGuard++; + + return action(this, state); + } + finally + { + _reentrancyGuard--; } } - else + { var composite = new CompositeDisposable(2); diff --git a/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj b/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj index 56a859ed3e..e9827cd38d 100644 --- a/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj +++ b/src/ReactiveUI.Avalonia/ReactiveUI.Avalonia.csproj @@ -5,9 +5,9 @@ mvvm;reactiveui;rx;reactive extensions;observable;LINQ;events;frp;avalonia;net;netcore - + - + diff --git a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs index 09167620cd..12992db3d9 100644 --- a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs +++ b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs @@ -15,8 +15,12 @@ namespace ReactiveUI.Avalonia /// value, and vice versa. /// /// ViewModel type. - public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class + public class ReactiveUserControl : UserControl, IViewFor + where TViewModel : class { + /// + /// The dependency property. + /// [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel?>(nameof(ViewModel)); @@ -31,26 +35,28 @@ public ReactiveUserControl() this.WhenActivated(disposables => { }); } - /// - /// The ViewModel. - /// + /// public TViewModel? ViewModel { get => GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } + /// object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TViewModel?)value; } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); +#pragma warning disable CA1062 if (change.Property == DataContextProperty) +#pragma warning restore CA1062 { if (ReferenceEquals(change.OldValue, ViewModel) && change.NewValue is null or TViewModel) diff --git a/src/ReactiveUI.Avalonia/ReactiveWindow.cs b/src/ReactiveUI.Avalonia/ReactiveWindow.cs index f01b42723f..53d7ead6e9 100644 --- a/src/ReactiveUI.Avalonia/ReactiveWindow.cs +++ b/src/ReactiveUI.Avalonia/ReactiveWindow.cs @@ -15,8 +15,12 @@ namespace ReactiveUI.Avalonia /// and vice versa. /// /// ViewModel type. - public class ReactiveWindow : Window, IViewFor where TViewModel : class + public class ReactiveWindow : Window, IViewFor + where TViewModel : class { + /// + /// The dependency property. + /// [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] public static readonly StyledProperty ViewModelProperty = AvaloniaProperty .Register, TViewModel?>(nameof(ViewModel)); @@ -30,27 +34,29 @@ public ReactiveWindow() // block if the ViewModel implements IActivatableViewModel. this.WhenActivated(disposables => { }); } - - /// - /// The ViewModel. - /// + + /// public TViewModel? ViewModel { get => GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } + /// object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (TViewModel?)value; } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); +#pragma warning disable CA1062 if (change.Property == DataContextProperty) +#pragma warning restore CA1062 { if (ReferenceEquals(change.OldValue, ViewModel) && change.NewValue is null or TViewModel) diff --git a/src/ReactiveUI.Avalonia/RoutedViewHost.cs b/src/ReactiveUI.Avalonia/RoutedViewHost.cs index f64cc3cf7a..a961a03b4d 100644 --- a/src/ReactiveUI.Avalonia/RoutedViewHost.cs +++ b/src/ReactiveUI.Avalonia/RoutedViewHost.cs @@ -80,9 +80,9 @@ public RoutedViewHost() this.WhenActivated(disposables => { var routerRemoved = this - .WhenAnyValue(x => x.Router) - .Where(router => router == null)! - .Cast(); + .WhenAnyValue(x => x.Router) + .Where(router => router == null)! + .Cast(); var viewContract = this.WhenAnyValue(x => x.ViewContract); @@ -128,8 +128,9 @@ public object? DefaultContent /// public IViewLocator? ViewLocator { get; set; } + /// protected override Type StyleKeyOverride => typeof(TransitioningContentControl); - + /// /// Invoked when ReactiveUI router navigates to a view model. /// @@ -157,11 +158,13 @@ private void NavigateToViewModel(object? viewModel, string? contract) { if (contract == null) { - this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + this.Log() + .Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); } else { - this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + this.Log() + .Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); } Content = DefaultContent; @@ -179,7 +182,10 @@ private void NavigateToViewModel(object? viewModel, string? contract) viewInstance.ViewModel = viewModel; if (viewInstance is IDataContextProvider provider) + { provider.DataContext = viewModel; + } + Content = viewInstance; } } diff --git a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs index cfe411f9d2..b47c53f5d2 100644 --- a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs +++ b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs @@ -81,6 +81,7 @@ public object? DefaultContent /// public IViewLocator? ViewLocator { get; set; } + /// protected override Type StyleKeyOverride => typeof(TransitioningContentControl); /// @@ -103,11 +104,13 @@ private void NavigateToViewModel(object? viewModel, string? contract) { if (contract == null) { - this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + this.Log() + .Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); } else { - this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); + this.Log() + .Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); } Content = DefaultContent; @@ -125,7 +128,10 @@ private void NavigateToViewModel(object? viewModel, string? contract) viewInstance.ViewModel = viewModel; if (viewInstance is StyledElement styled) + { styled.DataContext = viewModel; + } + Content = viewInstance; } } From cbe4cb43443e02a2d5da91d7d573ae678459f868 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 5 Dec 2024 00:40:09 -0800 Subject: [PATCH 5/8] Implement AvaloniaObjectObservableForProperty --- .../AppBuilderExtensions.cs | 1 + .../AvaloniaObjectObservableForProperty.cs | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/ReactiveUI.Avalonia/AvaloniaObjectObservableForProperty.cs diff --git a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs index 955ca655a4..b913850a42 100644 --- a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs +++ b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs @@ -39,6 +39,7 @@ public static AppBuilder UseReactiveUI(this AppBuilder builder) RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); })); } } diff --git a/src/ReactiveUI.Avalonia/AvaloniaObjectObservableForProperty.cs b/src/ReactiveUI.Avalonia/AvaloniaObjectObservableForProperty.cs new file mode 100644 index 0000000000..0d02709f35 --- /dev/null +++ b/src/ReactiveUI.Avalonia/AvaloniaObjectObservableForProperty.cs @@ -0,0 +1,78 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Reactive.Linq; +using System.Reflection; +using Avalonia; +using Splat; + +namespace ReactiveUI.Avalonia; + +/// +/// Creates an observable for a property if available that is based on a DependencyProperty. +/// +internal class AvaloniaObjectObservableForProperty : ICreatesObservableForProperty +{ + /// + public int GetAffinityForObject(Type type, string propertyName, bool beforeChanged = false) + { + if (!typeof(AvaloniaObject).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + return 0; + } + + return GetAvaloniaProperty(type, propertyName) is not null ? 4 : 0; + } + + /// + public IObservable> GetNotificationForProperty(object sender, System.Linq.Expressions.Expression expression, string propertyName, bool beforeChanged = false, bool suppressWarnings = false) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(sender); +#else + if (sender is null) + { + throw new ArgumentNullException(nameof(sender)); + } +#endif + + if (sender is not AvaloniaObject avaloniaObject) + { + throw new InvalidOperationException("The sender is not a AvaloniaObject"); + } + + var type = sender.GetType(); + + var avaloniaProperty = GetAvaloniaProperty(type, propertyName); + + if (avaloniaProperty is null) + { + if (!suppressWarnings) + { + this.Log().Error("Couldn't find avalonia property " + propertyName + " on " + type.Name); + } + + throw new NullReferenceException("Couldn't find avalonia property " + propertyName + " on " + type.Name); + } + + return avaloniaObject + .GetPropertyChangedObservable(avaloniaProperty) + .Select(args => new ObservedChange(args.Sender, expression, args.NewValue)); + } + + private static AvaloniaProperty? GetAvaloniaProperty(Type type, string propertyName) + { + foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(type)) + { + if (string.Equals(property.Name, propertyName, StringComparison.Ordinal)) + { + return property; + } + } + + return null; + } +} From dbfd7e04443b900c1e32377ea4b13e5c7ea00b87 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 5 Dec 2024 00:40:28 -0800 Subject: [PATCH 6/8] Use file-scoped namespaces --- .../AppBuilderExtensions.cs | 53 ++- .../AutoDataTemplateBindingHook.cs | 109 +++--- src/ReactiveUI.Avalonia/AutoSuspendHelper.cs | 127 ++++--- .../AvaloniaActivationForViewFetcher.cs | 119 ++++--- src/ReactiveUI.Avalonia/AvaloniaScheduler.cs | 139 ++++---- .../ReactiveUserControl.cs | 103 +++--- src/ReactiveUI.Avalonia/ReactiveWindow.cs | 103 +++--- src/ReactiveUI.Avalonia/RoutedViewHost.cs | 311 +++++++++--------- src/ReactiveUI.Avalonia/ViewModelViewHost.cs | 207 ++++++------ 9 files changed, 631 insertions(+), 640 deletions(-) diff --git a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs index b913850a42..9150bb04a3 100644 --- a/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs +++ b/src/ReactiveUI.Avalonia/AppBuilderExtensions.cs @@ -7,40 +7,39 @@ using Avalonia; using Splat; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// Avalonia AppBuilder setup extensions. +/// +public static class AppBuilderExtensions { /// - /// Avalonia AppBuilder setup extensions. + /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia scheduler, + /// an activation for view fetcher, a template binding hook. + /// Remember to call this method if you are using ReactiveUI in your application. /// - public static class AppBuilderExtensions + /// This builder. + /// The builder. + public static AppBuilder UseReactiveUI(this AppBuilder builder) { - /// - /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia scheduler, - /// an activation for view fetcher, a template binding hook. - /// Remember to call this method if you are using ReactiveUI in your application. - /// - /// This builder. - /// The builder. - public static AppBuilder UseReactiveUI(this AppBuilder builder) + if (builder is null) { - if (builder is null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new ArgumentNullException(nameof(builder)); + } - return builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => + return builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => + { + if (Locator.CurrentMutable is null) { - if (Locator.CurrentMutable is null) - { - return; - } + return; + } - PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); - RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); - Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - Locator.CurrentMutable.RegisterConstant(new AvaloniaObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); - })); - } + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); + RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaObjectObservableForProperty(), typeof(ICreatesObservableForProperty)); + })); } } diff --git a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs index 16470b1d64..5196439377 100644 --- a/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs +++ b/src/ReactiveUI.Avalonia/AutoDataTemplateBindingHook.cs @@ -10,68 +10,67 @@ using Avalonia.Controls.Templates; using Avalonia.Layout; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls +/// that don't have DataTemplates, and assigns a default DataTemplate that +/// loads the View associated with each ViewModel. +/// +public class AutoDataTemplateBindingHook : IPropertyBindingHook { - /// - /// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls - /// that don't have DataTemplates, and assigns a default DataTemplate that - /// loads the View associated with each ViewModel. - /// - public class AutoDataTemplateBindingHook : IPropertyBindingHook - { - private static readonly FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate( - (_, _) => - { - var control = new ViewModelViewHost(); - var context = control.GetObservable(StyledElement.DataContextProperty); - control.Bind(ViewModelViewHost.ViewModelProperty, context); - control.HorizontalContentAlignment = HorizontalAlignment.Stretch; - control.VerticalContentAlignment = VerticalAlignment.Stretch; - return control; - }, - true); + private static readonly FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate( + (_, _) => + { + var control = new ViewModelViewHost(); + var context = control.GetObservable(StyledElement.DataContextProperty); + control.Bind(ViewModelViewHost.ViewModelProperty, context); + control.HorizontalContentAlignment = HorizontalAlignment.Stretch; + control.VerticalContentAlignment = VerticalAlignment.Stretch; + return control; + }, + true); - /// - public bool ExecuteHook( - object? source, - object target, - Func[]> getCurrentViewModelProperties, - Func[]> getCurrentViewProperties, - BindingDirection direction) + /// + public bool ExecuteHook( + object? source, + object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + if (getCurrentViewProperties is null) { - if (getCurrentViewProperties is null) - { - throw new ArgumentNullException(nameof(getCurrentViewProperties)); - } - - var viewProperties = getCurrentViewProperties(); - var lastViewProperty = viewProperties.LastOrDefault(); - var itemsControl = lastViewProperty?.Sender as ItemsControl; - if (itemsControl == null) - { - return true; - } + throw new ArgumentNullException(nameof(getCurrentViewProperties)); + } - var propertyName = viewProperties.Last().GetPropertyName(); - if (propertyName != "Items" && - propertyName != "ItemsSource") - { - return true; - } + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + var itemsControl = lastViewProperty?.Sender as ItemsControl; + if (itemsControl == null) + { + return true; + } - if (itemsControl.ItemTemplate != null) - { - return true; - } + var propertyName = viewProperties.Last().GetPropertyName(); + if (propertyName != "Items" && + propertyName != "ItemsSource") + { + return true; + } - if (itemsControl.DataTemplates != null && - itemsControl.DataTemplates.Count > 0) - { - return true; - } + if (itemsControl.ItemTemplate != null) + { + return true; + } - itemsControl.ItemTemplate = DefaultItemTemplate; + if (itemsControl.DataTemplates != null && + itemsControl.DataTemplates.Count > 0) + { return true; } + + itemsControl.ItemTemplate = DefaultItemTemplate; + return true; } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs index 948784b012..ff4890be3f 100644 --- a/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs +++ b/src/ReactiveUI.Avalonia/AutoSuspendHelper.cs @@ -13,79 +13,78 @@ using Avalonia.Controls.ApplicationLifetimes; using Splat; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for +/// Avalonia applications. Call its constructor in your app's composition root, +/// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. +/// +public sealed class AutoSuspendHelper : IEnableLogger, IDisposable { + private readonly Subject _shouldPersistState = new Subject(); + private readonly Subject _isLaunchingNew = new Subject(); + /// - /// A ReactiveUI AutoSuspendHelper which initializes suspension hooks for - /// Avalonia applications. Call its constructor in your app's composition root, - /// before calling the RxApp.SuspensionHost.SetupDefaultSuspendResume method. + /// Initializes a new instance of the class. /// - public sealed class AutoSuspendHelper : IEnableLogger, IDisposable + /// Pass in the Application.ApplicationLifetime property. + public AutoSuspendHelper(IApplicationLifetime lifetime) { - private readonly Subject _shouldPersistState = new Subject(); - private readonly Subject _isLaunchingNew = new Subject(); + RxApp.SuspensionHost.IsResuming = Observable.Never(); + RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; - /// - /// Initializes a new instance of the class. - /// - /// Pass in the Application.ApplicationLifetime property. - public AutoSuspendHelper(IApplicationLifetime lifetime) + if (Design.IsDesignMode) { - RxApp.SuspensionHost.IsResuming = Observable.Never(); - RxApp.SuspensionHost.IsLaunchingNew = _isLaunchingNew; - - if (Design.IsDesignMode) - { - this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state."); - RxApp.SuspensionHost.ShouldPersistState = Observable.Never(); - } - else if (lifetime is IControlledApplicationLifetime controlled) - { - this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); - controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); - RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; - } - else if (lifetime != null) - { - var type = lifetime.GetType().FullName; - var message = $"Don't know how to detect app exit event for {type}."; - throw new NotSupportedException(message); - } - else - { - var message = "ApplicationLifetime is null. " - + "Ensure you are initializing AutoSuspendHelper " - + "after Avalonia application initialization is completed."; - throw new ArgumentNullException(message); - } - - var errored = new Subject(); - AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); - RxApp.SuspensionHost.ShouldInvalidateState = errored; + this.Log().Debug("Design mode detected. AutoSuspendHelper won't persist app state."); + RxApp.SuspensionHost.ShouldPersistState = Observable.Never(); } - - /// - /// Call this method in your App.OnFrameworkInitializationCompleted method. - /// - public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); - - /// - /// Disposes internally stored observers. - /// - public void Dispose() + else if (lifetime is IControlledApplicationLifetime controlled) { - _shouldPersistState.Dispose(); - _isLaunchingNew.Dispose(); + this.Log().Debug("Using IControlledApplicationLifetime events to handle app exit."); + controlled.Exit += (sender, args) => OnControlledApplicationLifetimeExit(); + RxApp.SuspensionHost.ShouldPersistState = _shouldPersistState; } - - private void OnControlledApplicationLifetimeExit() + else if (lifetime != null) { - this.Log().Debug("Received IControlledApplicationLifetime exit event."); - var manual = new ManualResetEvent(false); - _shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); - - manual.WaitOne(); - this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); + var type = lifetime.GetType().FullName; + var message = $"Don't know how to detect app exit event for {type}."; + throw new NotSupportedException(message); + } + else + { + var message = "ApplicationLifetime is null. " + + "Ensure you are initializing AutoSuspendHelper " + + "after Avalonia application initialization is completed."; + throw new ArgumentNullException(message); } + + var errored = new Subject(); + AppDomain.CurrentDomain.UnhandledException += (o, e) => errored.OnNext(Unit.Default); + RxApp.SuspensionHost.ShouldInvalidateState = errored; + } + + /// + /// Call this method in your App.OnFrameworkInitializationCompleted method. + /// + public void OnFrameworkInitializationCompleted() => _isLaunchingNew.OnNext(Unit.Default); + + /// + /// Disposes internally stored observers. + /// + public void Dispose() + { + _shouldPersistState.Dispose(); + _isLaunchingNew.Dispose(); + } + + private void OnControlledApplicationLifetimeExit() + { + this.Log().Debug("Received IControlledApplicationLifetime exit event."); + var manual = new ManualResetEvent(false); + _shouldPersistState.OnNext(Disposable.Create(() => manual.Set())); + + manual.WaitOne(); + this.Log().Debug("Completed actions on IControlledApplicationLifetime exit event."); } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs index 8e568aa51f..e8053c09d0 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaActivationForViewFetcher.cs @@ -9,72 +9,71 @@ using Avalonia.Controls; using Avalonia.Interactivity; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// Determines when Avalonia IVisuals get activated. +/// +public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher { - /// - /// Determines when Avalonia IVisuals get activated. - /// - public class AvaloniaActivationForViewFetcher : IActivationForViewFetcher - { - /// - public int GetAffinityForView(Type view) => typeof(Visual).IsAssignableFrom(view) ? 10 : 0; + /// + public int GetAffinityForView(Type view) => typeof(Visual).IsAssignableFrom(view) ? 10 : 0; - /// - public IObservable GetActivationForView(IActivatableView view) + /// + public IObservable GetActivationForView(IActivatableView view) + { + if (view is not Visual visual) { - if (view is not Visual visual) - { - return Observable.Return(false); - } - - if (view is Control control) - { - return GetActivationForControl(control); - } - - return GetActivationForVisual(visual); + return Observable.Return(false); } - /// - /// Listens to Loaded and Unloaded - /// events for Avalonia Control. - /// - private static IObservable GetActivationForControl(Control control) + if (view is Control control) { - var controlLoaded = Observable - .FromEventPattern( - x => control.Loaded += x, - x => control.Loaded -= x) - .Select(args => true); - var controlUnloaded = Observable - .FromEventPattern( - x => control.Unloaded += x, - x => control.Unloaded -= x) - .Select(args => false); - return controlLoaded - .Merge(controlUnloaded) - .DistinctUntilChanged(); + return GetActivationForControl(control); } - /// - /// Listens to AttachedToVisualTree and DetachedFromVisualTree - /// events for Avalonia IVisuals. - /// - private static IObservable GetActivationForVisual(Visual visual) - { - var visualLoaded = Observable - .FromEventPattern( - x => visual.AttachedToVisualTree += x, - x => visual.AttachedToVisualTree -= x) - .Select(args => true); - var visualUnloaded = Observable - .FromEventPattern( - x => visual.DetachedFromVisualTree += x, - x => visual.DetachedFromVisualTree -= x) - .Select(args => false); - return visualLoaded - .Merge(visualUnloaded) - .DistinctUntilChanged(); - } + return GetActivationForVisual(visual); + } + + /// + /// Listens to Loaded and Unloaded + /// events for Avalonia Control. + /// + private static IObservable GetActivationForControl(Control control) + { + var controlLoaded = Observable + .FromEventPattern( + x => control.Loaded += x, + x => control.Loaded -= x) + .Select(args => true); + var controlUnloaded = Observable + .FromEventPattern( + x => control.Unloaded += x, + x => control.Unloaded -= x) + .Select(args => false); + return controlLoaded + .Merge(controlUnloaded) + .DistinctUntilChanged(); + } + + /// + /// Listens to AttachedToVisualTree and DetachedFromVisualTree + /// events for Avalonia IVisuals. + /// + private static IObservable GetActivationForVisual(Visual visual) + { + var visualLoaded = Observable + .FromEventPattern( + x => visual.AttachedToVisualTree += x, + x => visual.AttachedToVisualTree -= x) + .Select(args => true); + var visualUnloaded = Observable + .FromEventPattern( + x => visual.DetachedFromVisualTree += x, + x => visual.DetachedFromVisualTree -= x) + .Select(args => false); + return visualLoaded + .Merge(visualUnloaded) + .DistinctUntilChanged(); } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs b/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs index 6aac9550fe..255d2b8c37 100644 --- a/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs +++ b/src/ReactiveUI.Avalonia/AvaloniaScheduler.cs @@ -8,96 +8,95 @@ using System.Reactive.Disposables; using Avalonia.Threading; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// A reactive scheduler that uses Avalonia's . +/// +public class AvaloniaScheduler : LocalScheduler { /// - /// A reactive scheduler that uses Avalonia's . + /// The instance of the . + /// + public static readonly AvaloniaScheduler Instance = new(); + + /// + /// Users can schedule actions on the dispatcher thread while being on the correct thread already. + /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases. + /// To prevent this we are limiting amount of reentrant calls to before we will + /// schedule on a dispatcher anyway. + /// + private const int MaxReentrantSchedules = 32; + + private int _reentrancyGuard; + + /// + /// Initializes a new instance of the class. /// - public class AvaloniaScheduler : LocalScheduler + private AvaloniaScheduler() + { + } + + /// + public override IDisposable Schedule( + TState state, TimeSpan dueTime, Func action) { - /// - /// The instance of the . - /// - public static readonly AvaloniaScheduler Instance = new(); - - /// - /// Users can schedule actions on the dispatcher thread while being on the correct thread already. - /// We are optimizing this case by invoking user callback immediately which can lead to stack overflows in certain cases. - /// To prevent this we are limiting amount of reentrant calls to before we will - /// schedule on a dispatcher anyway. - /// - private const int MaxReentrantSchedules = 32; - - private int _reentrancyGuard; - - /// - /// Initializes a new instance of the class. - /// - private AvaloniaScheduler() + if (action is null) { + throw new ArgumentNullException(nameof(action)); } - /// - public override IDisposable Schedule( - TState state, TimeSpan dueTime, Func action) + IDisposable PostOnDispatcher() { - if (action is null) - { - throw new ArgumentNullException(nameof(action)); - } + var composite = new CompositeDisposable(2); - IDisposable PostOnDispatcher() - { - var composite = new CompositeDisposable(2); + var cancellation = new CancellationDisposable(); - var cancellation = new CancellationDisposable(); - - Dispatcher.UIThread.Post( - () => + Dispatcher.UIThread.Post( + () => + { + if (!cancellation.Token.IsCancellationRequested) { - if (!cancellation.Token.IsCancellationRequested) - { - composite.Add(action(this, state)); - } - }, - DispatcherPriority.Background); + composite.Add(action(this, state)); + } + }, + DispatcherPriority.Background); - composite.Add(cancellation); + composite.Add(cancellation); - return composite; - } + return composite; + } - if (dueTime == TimeSpan.Zero) + if (dueTime == TimeSpan.Zero) + { + if (!Dispatcher.UIThread.CheckAccess()) { - if (!Dispatcher.UIThread.CheckAccess()) - { - return PostOnDispatcher(); - } - - if (_reentrancyGuard >= MaxReentrantSchedules) - { - return PostOnDispatcher(); - } - - try - { - _reentrancyGuard++; - - return action(this, state); - } - finally - { - _reentrancyGuard--; - } + return PostOnDispatcher(); } + if (_reentrancyGuard >= MaxReentrantSchedules) { - var composite = new CompositeDisposable(2); + return PostOnDispatcher(); + } - composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + try + { + _reentrancyGuard++; - return composite; + return action(this, state); } + finally + { + _reentrancyGuard--; + } + } + + { + var composite = new CompositeDisposable(2); + + composite.Add(DispatcherTimer.RunOnce(() => composite.Add(action(this, state)), dueTime)); + + return composite; } } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs index 12992db3d9..1adda422f1 100644 --- a/src/ReactiveUI.Avalonia/ReactiveUserControl.cs +++ b/src/ReactiveUI.Avalonia/ReactiveUserControl.cs @@ -6,71 +6,70 @@ using Avalonia; using Avalonia.Controls; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// A ReactiveUI that implements the interface and +/// will activate your ViewModel automatically if the view model implements . +/// When the DataContext property changes, this class will update the ViewModel property with the new DataContext +/// value, and vice versa. +/// +/// ViewModel type. +public class ReactiveUserControl : UserControl, IViewFor + where TViewModel : class { /// - /// A ReactiveUI that implements the interface and - /// will activate your ViewModel automatically if the view model implements . - /// When the DataContext property changes, this class will update the ViewModel property with the new DataContext - /// value, and vice versa. + /// The dependency property. /// - /// ViewModel type. - public class ReactiveUserControl : UserControl, IViewFor - where TViewModel : class - { - /// - /// The dependency property. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] - public static readonly StyledProperty ViewModelProperty = AvaloniaProperty - .Register, TViewModel?>(nameof(ViewModel)); + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty + .Register, TViewModel?>(nameof(ViewModel)); - /// - /// Initializes a new instance of the class. - /// - public ReactiveUserControl() - { - // This WhenActivated block calls ViewModel's WhenActivated - // block if the ViewModel implements IActivatableViewModel. - this.WhenActivated(disposables => { }); - } + /// + /// Initializes a new instance of the class. + /// + public ReactiveUserControl() + { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + } - /// - public TViewModel? ViewModel - { - get => GetValue(ViewModelProperty); - set => SetValue(ViewModelProperty, value); - } + /// + public TViewModel? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } - /// - object? IViewFor.ViewModel - { - get => ViewModel; - set => ViewModel = (TViewModel?)value; - } + /// + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } - /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); #pragma warning disable CA1062 - if (change.Property == DataContextProperty) + if (change.Property == DataContextProperty) #pragma warning restore CA1062 + { + if (ReferenceEquals(change.OldValue, ViewModel) + && change.NewValue is null or TViewModel) { - if (ReferenceEquals(change.OldValue, ViewModel) - && change.NewValue is null or TViewModel) - { - SetCurrentValue(ViewModelProperty, change.NewValue); - } + SetCurrentValue(ViewModelProperty, change.NewValue); } - else if (change.Property == ViewModelProperty) + } + else if (change.Property == ViewModelProperty) + { + if (ReferenceEquals(change.OldValue, DataContext)) { - if (ReferenceEquals(change.OldValue, DataContext)) - { - SetCurrentValue(DataContextProperty, change.NewValue); - } + SetCurrentValue(DataContextProperty, change.NewValue); } } } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/ReactiveWindow.cs b/src/ReactiveUI.Avalonia/ReactiveWindow.cs index 53d7ead6e9..e81a9ecde7 100644 --- a/src/ReactiveUI.Avalonia/ReactiveWindow.cs +++ b/src/ReactiveUI.Avalonia/ReactiveWindow.cs @@ -6,71 +6,70 @@ using Avalonia; using Avalonia.Controls; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// A ReactiveUI that implements the interface and will +/// activate your ViewModel automatically if the view model implements . When +/// the DataContext property changes, this class will update the ViewModel property with the new DataContext value, +/// and vice versa. +/// +/// ViewModel type. +public class ReactiveWindow : Window, IViewFor + where TViewModel : class { /// - /// A ReactiveUI that implements the interface and will - /// activate your ViewModel automatically if the view model implements . When - /// the DataContext property changes, this class will update the ViewModel property with the new DataContext value, - /// and vice versa. + /// The dependency property. /// - /// ViewModel type. - public class ReactiveWindow : Window, IViewFor - where TViewModel : class - { - /// - /// The dependency property. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] - public static readonly StyledProperty ViewModelProperty = AvaloniaProperty - .Register, TViewModel?>(nameof(ViewModel)); + [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1002", Justification = "Generic avalonia property is expected here.")] + public static readonly StyledProperty ViewModelProperty = AvaloniaProperty + .Register, TViewModel?>(nameof(ViewModel)); - /// - /// Initializes a new instance of the class. - /// - public ReactiveWindow() - { - // This WhenActivated block calls ViewModel's WhenActivated - // block if the ViewModel implements IActivatableViewModel. - this.WhenActivated(disposables => { }); - } + /// + /// Initializes a new instance of the class. + /// + public ReactiveWindow() + { + // This WhenActivated block calls ViewModel's WhenActivated + // block if the ViewModel implements IActivatableViewModel. + this.WhenActivated(disposables => { }); + } - /// - public TViewModel? ViewModel - { - get => GetValue(ViewModelProperty); - set => SetValue(ViewModelProperty, value); - } + /// + public TViewModel? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } - /// - object? IViewFor.ViewModel - { - get => ViewModel; - set => ViewModel = (TViewModel?)value; - } + /// + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } - /// - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); #pragma warning disable CA1062 - if (change.Property == DataContextProperty) + if (change.Property == DataContextProperty) #pragma warning restore CA1062 + { + if (ReferenceEquals(change.OldValue, ViewModel) + && change.NewValue is null or TViewModel) { - if (ReferenceEquals(change.OldValue, ViewModel) - && change.NewValue is null or TViewModel) - { - SetCurrentValue(ViewModelProperty, change.NewValue); - } + SetCurrentValue(ViewModelProperty, change.NewValue); } - else if (change.Property == ViewModelProperty) + } + else if (change.Property == ViewModelProperty) + { + if (ReferenceEquals(change.OldValue, DataContext)) { - if (ReferenceEquals(change.OldValue, DataContext)) - { - SetCurrentValue(DataContextProperty, change.NewValue); - } + SetCurrentValue(DataContextProperty, change.NewValue); } } } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/RoutedViewHost.cs b/src/ReactiveUI.Avalonia/RoutedViewHost.cs index a961a03b4d..c7af1e7cf2 100644 --- a/src/ReactiveUI.Avalonia/RoutedViewHost.cs +++ b/src/ReactiveUI.Avalonia/RoutedViewHost.cs @@ -10,183 +10,182 @@ using Avalonia.Controls; using Splat; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// This control hosts the View associated with ReactiveUI RoutingState, +/// and will display the View and wire up the ViewModel whenever a new +/// ViewModel is navigated to. Nested routing is also supported. +/// +/// +/// +/// ReactiveUI routing consists of an IScreen that contains current +/// RoutingState, several IRoutableViewModels, and a platform-specific +/// XAML control called RoutedViewHost. +/// +/// +/// RoutingState manages the ViewModel navigation stack and allows +/// ViewModels to navigate to other ViewModels. IScreen is the root of +/// a navigation stack; despite the name, its views don't have to occupy +/// the whole screen. RoutedViewHost monitors an instance of RoutingState, +/// responding to any changes in the navigation stack by creating and +/// embedding the appropriate view. +/// +/// +/// Place this control to a view containing your ViewModel that implements +/// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. +/// +/// +/// +/// +/// +/// +/// ]]> +/// +/// +/// +/// See +/// ReactiveUI routing documentation website for more info. +/// +/// +public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger { /// - /// This control hosts the View associated with ReactiveUI RoutingState, - /// and will display the View and wire up the ViewModel whenever a new - /// ViewModel is navigated to. Nested routing is also supported. + /// for the property. /// - /// - /// - /// ReactiveUI routing consists of an IScreen that contains current - /// RoutingState, several IRoutableViewModels, and a platform-specific - /// XAML control called RoutedViewHost. - /// - /// - /// RoutingState manages the ViewModel navigation stack and allows - /// ViewModels to navigate to other ViewModels. IScreen is the root of - /// a navigation stack; despite the name, its views don't have to occupy - /// the whole screen. RoutedViewHost monitors an instance of RoutingState, - /// responding to any changes in the navigation stack by creating and - /// embedding the appropriate view. - /// - /// - /// Place this control to a view containing your ViewModel that implements - /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. - /// - /// - /// - /// - /// - /// - /// ]]> - /// - /// - /// - /// See - /// ReactiveUI routing documentation website for more info. - /// - /// - public class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLogger + public static readonly StyledProperty RouterProperty = + AvaloniaProperty.Register(nameof(Router)); + + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + + /// + /// for the property. + /// + public static readonly StyledProperty DefaultContentProperty = + ViewModelViewHost.DefaultContentProperty.AddOwner(); + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() { - /// - /// for the property. - /// - public static readonly StyledProperty RouterProperty = - AvaloniaProperty.Register(nameof(Router)); - - /// - /// for the property. - /// - public static readonly StyledProperty ViewContractProperty = - AvaloniaProperty.Register(nameof(ViewContract)); - - /// - /// for the property. - /// - public static readonly StyledProperty DefaultContentProperty = - ViewModelViewHost.DefaultContentProperty.AddOwner(); - - /// - /// Initializes a new instance of the class. - /// - public RoutedViewHost() + this.WhenActivated(disposables => { - this.WhenActivated(disposables => - { - var routerRemoved = this - .WhenAnyValue(x => x.Router) - .Where(router => router == null)! - .Cast(); - - var viewContract = this.WhenAnyValue(x => x.ViewContract); - - this.WhenAnyValue(x => x.Router) - .Where(router => router != null) - .SelectMany(router => router!.CurrentViewModel) - .Merge(routerRemoved) - .CombineLatest(viewContract) - .Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second)) - .DisposeWith(disposables); - }); - } + var routerRemoved = this + .WhenAnyValue(x => x.Router) + .Where(router => router == null)! + .Cast(); + + var viewContract = this.WhenAnyValue(x => x.ViewContract); + + this.WhenAnyValue(x => x.Router) + .Where(router => router != null) + .SelectMany(router => router!.CurrentViewModel) + .Merge(routerRemoved) + .CombineLatest(viewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.First, tuple.Second)) + .DisposeWith(disposables); + }); + } - /// - /// Gets or sets the of the view model stack. - /// - public RoutingState? Router - { - get => GetValue(RouterProperty); - set => SetValue(RouterProperty, value); - } + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState? Router + { + get => GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } - /// - /// Gets or sets the view contract. - /// - public string? ViewContract - { - get => GetValue(ViewContractProperty); - set => SetValue(ViewContractProperty, value); - } + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object? DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object? DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } - /// - /// Gets or sets the ReactiveUI view locator used by this router. - /// - public IViewLocator? ViewLocator { get; set; } + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator? ViewLocator { get; set; } - /// - protected override Type StyleKeyOverride => typeof(TransitioningContentControl); + /// + protected override Type StyleKeyOverride => typeof(TransitioningContentControl); - /// - /// Invoked when ReactiveUI router navigates to a view model. - /// - /// ViewModel to which the user navigates. - /// The contract for view resolution. - private void NavigateToViewModel(object? viewModel, string? contract) + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) + { + if (Router == null) { - if (Router == null) - { - this.Log().Warn("Router property is null. Falling back to default content."); - Content = DefaultContent; - return; - } - - if (viewModel == null) - { - this.Log().Info("ViewModel is null. Falling back to default content."); - Content = DefaultContent; - return; - } + this.Log().Warn("Router property is null. Falling back to default content."); + Content = DefaultContent; + return; + } - var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var viewInstance = viewLocator.ResolveView(viewModel, contract); - if (viewInstance == null) - { - if (contract == null) - { - this.Log() - .Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); - } - else - { - this.Log() - .Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); - } - - Content = DefaultContent; - return; - } + if (viewModel == null) + { + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel, contract); + if (viewInstance == null) + { if (contract == null) { - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + this.Log() + .Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); } else { - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + this.Log() + .Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); } - viewInstance.ViewModel = viewModel; - if (viewInstance is IDataContextProvider provider) - { - provider.DataContext = viewModel; - } + Content = DefaultContent; + return; + } - Content = viewInstance; + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); } + + viewInstance.ViewModel = viewModel; + if (viewInstance is IDataContextProvider provider) + { + provider.DataContext = viewModel; + } + + Content = viewInstance; } -} +} \ No newline at end of file diff --git a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs index b47c53f5d2..72871d9a35 100644 --- a/src/ReactiveUI.Avalonia/ViewModelViewHost.cs +++ b/src/ReactiveUI.Avalonia/ViewModelViewHost.cs @@ -9,130 +9,129 @@ using Avalonia.Controls; using Splat; -namespace ReactiveUI.Avalonia +namespace ReactiveUI.Avalonia; + +/// +/// This content control will automatically load the View associated with +/// the ViewModel property and display it. This control is very useful +/// inside a DataTemplate to display the View associated with a ViewModel. +/// +public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger { /// - /// This content control will automatically load the View associated with - /// the ViewModel property and display it. This control is very useful - /// inside a DataTemplate to display the View associated with a ViewModel. + /// for the property. /// - public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// for the property. + /// + public static readonly StyledProperty ViewContractProperty = + AvaloniaProperty.Register(nameof(ViewContract)); + + /// + /// for the property. + /// + public static readonly StyledProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() { - /// - /// for the property. - /// - public static readonly AvaloniaProperty ViewModelProperty = - AvaloniaProperty.Register(nameof(ViewModel)); - - /// - /// for the property. - /// - public static readonly StyledProperty ViewContractProperty = - AvaloniaProperty.Register(nameof(ViewContract)); - - /// - /// for the property. - /// - public static readonly StyledProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Initializes a new instance of the class. - /// - public ViewModelViewHost() + this.WhenActivated(disposables => { - this.WhenActivated(disposables => - { - this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract) - .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2)) - .DisposeWith(disposables); - }); - } + this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract) + .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2)) + .DisposeWith(disposables); + }); + } - /// - /// Gets or sets the ViewModel to display. - /// - public object? ViewModel - { - get => GetValue(ViewModelProperty); - set => SetValue(ViewModelProperty, value); - } + /// + /// Gets or sets the ViewModel to display. + /// + public object? ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } - /// - /// Gets or sets the view contract. - /// - public string? ViewContract - { - get => GetValue(ViewContractProperty); - set => SetValue(ViewContractProperty, value); - } + /// + /// Gets or sets the view contract. + /// + public string? ViewContract + { + get => GetValue(ViewContractProperty); + set => SetValue(ViewContractProperty, value); + } - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object? DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object? DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } - /// - /// Gets or sets the view locator. - /// - public IViewLocator? ViewLocator { get; set; } + /// + /// Gets or sets the view locator. + /// + public IViewLocator? ViewLocator { get; set; } - /// - protected override Type StyleKeyOverride => typeof(TransitioningContentControl); + /// + protected override Type StyleKeyOverride => typeof(TransitioningContentControl); - /// - /// Invoked when ReactiveUI router navigates to a view model. - /// - /// ViewModel to which the user navigates. - /// The contract for view resolution. - private void NavigateToViewModel(object? viewModel, string? contract) + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// The contract for view resolution. + private void NavigateToViewModel(object? viewModel, string? contract) + { + if (viewModel == null) { - if (viewModel == null) - { - this.Log().Info("ViewModel is null. Falling back to default content."); - Content = DefaultContent; - return; - } - - var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var viewInstance = viewLocator.ResolveView(viewModel, contract); - if (viewInstance == null) - { - if (contract == null) - { - this.Log() - .Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); - } - else - { - this.Log() - .Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); - } - - Content = DefaultContent; - return; - } + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; + return; + } + var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; + var viewInstance = viewLocator.ResolveView(viewModel, contract); + if (viewInstance == null) + { if (contract == null) { - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + this.Log() + .Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); } else { - this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + this.Log() + .Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content."); } - viewInstance.ViewModel = viewModel; - if (viewInstance is StyledElement styled) - { - styled.DataContext = viewModel; - } + Content = DefaultContent; + return; + } - Content = viewInstance; + if (contract == null) + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); } + else + { + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'."); + } + + viewInstance.ViewModel = viewModel; + if (viewInstance is StyledElement styled) + { + styled.DataContext = viewModel; + } + + Content = viewInstance; } -} +} \ No newline at end of file From 4a1d68c6a3252c0cf39c988242de88e1e55c214c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 5 Dec 2024 01:09:23 -0800 Subject: [PATCH 7/8] Add IntegrationTests.Avalonia login sample --- integrationtests/IntegrationTests.All.sln | 101 ++++++++++++++++++ .../IntegrationTests.Avalonia/App.axaml | 8 ++ .../IntegrationTests.Avalonia/App.axaml.cs | 28 +++++ .../IntegrationTests.Avalonia.csproj | 18 ++++ .../LoginControl.axaml | 45 ++++++++ .../LoginControl.axaml.cs | 63 +++++++++++ .../MainWindow.axaml | 13 +++ .../MainWindow.axaml.cs | 17 +++ .../IntegrationTests.Avalonia/Program.cs | 27 +++++ .../UserControlExtensions.cs | 37 +++++++ 10 files changed, 357 insertions(+) create mode 100644 integrationtests/IntegrationTests.Avalonia/App.axaml create mode 100644 integrationtests/IntegrationTests.Avalonia/App.axaml.cs create mode 100644 integrationtests/IntegrationTests.Avalonia/IntegrationTests.Avalonia.csproj create mode 100644 integrationtests/IntegrationTests.Avalonia/LoginControl.axaml create mode 100644 integrationtests/IntegrationTests.Avalonia/LoginControl.axaml.cs create mode 100644 integrationtests/IntegrationTests.Avalonia/MainWindow.axaml create mode 100644 integrationtests/IntegrationTests.Avalonia/MainWindow.axaml.cs create mode 100644 integrationtests/IntegrationTests.Avalonia/Program.cs create mode 100644 integrationtests/IntegrationTests.Avalonia/UserControlExtensions.cs diff --git a/integrationtests/IntegrationTests.All.sln b/integrationtests/IntegrationTests.All.sln index 89df742cb8..ddfbbae310 100644 --- a/integrationtests/IntegrationTests.All.sln +++ b/integrationtests/IntegrationTests.All.sln @@ -52,6 +52,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Uwp", "..\src\Re EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.AndroidX", "..\src\ReactiveUI.AndroidX\ReactiveUI.AndroidX.csproj", "{824088E4-A1D2-4B71-843E-873D351073C8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTests.Avalonia", "IntegrationTests.Avalonia\IntegrationTests.Avalonia.csproj", "{556E134B-BA44-4885-A5F9-FB2D668F30A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveUI.Avalonia", "..\src\ReactiveUI.Avalonia\ReactiveUI.Avalonia.csproj", "{67F3CF51-1448-468E-BF86-4769F0EDF581}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1088,6 +1092,102 @@ Global {824088E4-A1D2-4B71-843E-873D351073C8}.Release|x64.Build.0 = Release|Any CPU {824088E4-A1D2-4B71-843E-873D351073C8}.Release|x86.ActiveCfg = Release|Any CPU {824088E4-A1D2-4B71-843E-873D351073C8}.Release|x86.Build.0 = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|ARM.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|iPhone.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|x64.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|x64.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|x86.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.AppStore|x86.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|ARM.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|ARM.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|iPhone.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|x64.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Debug|x86.Build.0 = Debug|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|Any CPU.Build.0 = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|ARM.ActiveCfg = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|ARM.Build.0 = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|iPhone.ActiveCfg = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|iPhone.Build.0 = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|x64.ActiveCfg = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|x64.Build.0 = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|x86.ActiveCfg = Release|Any CPU + {556E134B-BA44-4885-A5F9-FB2D668F30A2}.Release|x86.Build.0 = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|ARM.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|iPhone.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|x64.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|x64.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|x86.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.AppStore|x86.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|ARM.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|ARM.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|iPhone.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|x64.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|x64.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|x86.ActiveCfg = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Debug|x86.Build.0 = Debug|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|Any CPU.Build.0 = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|ARM.ActiveCfg = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|ARM.Build.0 = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|iPhone.ActiveCfg = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|iPhone.Build.0 = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|x64.ActiveCfg = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|x64.Build.0 = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|x86.ActiveCfg = Release|Any CPU + {67F3CF51-1448-468E-BF86-4769F0EDF581}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1101,6 +1201,7 @@ Global {E6284535-0B6E-42AA-9113-5230AB09E8E6} = {34A7ABF0-30C6-48EA-94F0-8B116E995464} {7B5A4E3B-8706-4D5B-B979-C5E39289CE17} = {34A7ABF0-30C6-48EA-94F0-8B116E995464} {824088E4-A1D2-4B71-843E-873D351073C8} = {34A7ABF0-30C6-48EA-94F0-8B116E995464} + {67F3CF51-1448-468E-BF86-4769F0EDF581} = {34A7ABF0-30C6-48EA-94F0-8B116E995464} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EDE6B0FD-3EF7-49C4-AF1D-E7485C1FEFAF} diff --git a/integrationtests/IntegrationTests.Avalonia/App.axaml b/integrationtests/IntegrationTests.Avalonia/App.axaml new file mode 100644 index 0000000000..a3760c31bb --- /dev/null +++ b/integrationtests/IntegrationTests.Avalonia/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/integrationtests/IntegrationTests.Avalonia/App.axaml.cs b/integrationtests/IntegrationTests.Avalonia/App.axaml.cs new file mode 100644 index 0000000000..7abbc5bdcc --- /dev/null +++ b/integrationtests/IntegrationTests.Avalonia/App.axaml.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2024 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace IntegrationTests.Avalonia; + +internal partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/integrationtests/IntegrationTests.Avalonia/IntegrationTests.Avalonia.csproj b/integrationtests/IntegrationTests.Avalonia/IntegrationTests.Avalonia.csproj new file mode 100644 index 0000000000..085a5f70c1 --- /dev/null +++ b/integrationtests/IntegrationTests.Avalonia/IntegrationTests.Avalonia.csproj @@ -0,0 +1,18 @@ + + + WinExe + net8.0 + Avalonia specific example of IntegrationTests + IntegrationTests.Avalonia + true + + + + + + + + + + + diff --git a/integrationtests/IntegrationTests.Avalonia/LoginControl.axaml b/integrationtests/IntegrationTests.Avalonia/LoginControl.axaml new file mode 100644 index 0000000000..355e841647 --- /dev/null +++ b/integrationtests/IntegrationTests.Avalonia/LoginControl.axaml @@ -0,0 +1,45 @@ + + + + + + + + +