From 8cfa7c175c4e9094d52448137db7731532a50c26 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Fri, 17 May 2019 15:58:36 +0200 Subject: [PATCH 01/23] Add blank cursor --- src/Avalonia.Input/Cursors.cs | 1 + src/Avalonia.X11/X11CursorFactory.cs | 22 ++++++++++++++++++++- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 7 ++++--- src/Gtk/Avalonia.Gtk3/GdkCursor.cs | 1 + src/Windows/Avalonia.Win32/CursorFactory.cs | 5 +++-- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Input/Cursors.cs b/src/Avalonia.Input/Cursors.cs index d3618f30f3..8139af1659 100644 --- a/src/Avalonia.Input/Cursors.cs +++ b/src/Avalonia.Input/Cursors.cs @@ -38,6 +38,7 @@ namespace Avalonia.Input DragMove, DragCopy, DragLink, + None, // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax direclty from theme with fallback image diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 40b01117e3..c5566318a2 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -8,6 +8,8 @@ namespace Avalonia.X11 { class X11CursorFactory : IStandardCursorFactory { + private static IntPtr _nullCursor; + private readonly IntPtr _display; private Dictionary _cursors; @@ -42,16 +44,34 @@ namespace Avalonia.X11 public X11CursorFactory(IntPtr display) { _display = display; + _nullCursor = GetNullCursor(display); _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } public IPlatformHandle GetCursor(StandardCursorType cursorType) { - var handle = s_mapping.TryGetValue(cursorType, out var shape) + IntPtr handle; + if (cursorType == StandardCursorType.None) + { + handle = _nullCursor; + } + else + { + handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; + } return new PlatformHandle(handle, "XCURSOR"); } + + private static IntPtr GetNullCursor(IntPtr display) + { + XColor color = new XColor(); + byte[] data = new byte[] { 0 }; + IntPtr window = XLib.XRootWindow(display, 0); + IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index a28b1cbb1a..f36e6b986d 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -12,7 +12,8 @@ namespace Avalonia.Gtk3 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.None, CursorType.Blank}, + { StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -36,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = @@ -80,4 +81,4 @@ namespace Avalonia.Gtk3 return rv; } } -} \ No newline at end of file +} diff --git a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs index 4fad8208b3..aa0f8cde0d 100644 --- a/src/Gtk/Avalonia.Gtk3/GdkCursor.cs +++ b/src/Gtk/Avalonia.Gtk3/GdkCursor.cs @@ -2,6 +2,7 @@ { enum GdkCursorType { + Blank = -2, CursorIsPixmap = -1, XCursor = 0, Arrow = 2, diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index e582b5fb82..df413addef 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -41,7 +41,8 @@ namespace Avalonia.Win32 private static readonly Dictionary CursorTypeMapping = new Dictionary { - {StandardCursorType.AppStarting, 32650}, + {StandardCursorType.None, 0}, + { StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -69,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From 4ad1acd24eeb836ff3b4fb06cb83011ed635d1e0 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:05:49 +0200 Subject: [PATCH 02/23] Fix X11 NullCursor --- src/Avalonia.X11/X11CursorFactory.cs | 8 ++++---- src/Avalonia.X11/XLib.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c5566318a2..c020c44662 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -51,7 +51,7 @@ namespace Avalonia.X11 public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -66,12 +66,12 @@ namespace Avalonia.X11 } private static IntPtr GetNullCursor(IntPtr display) - { + { XColor color = new XColor(); byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); - IntPtr pixmap = XLib.XCreatePixmapFromBitmapData(display, window, data, 1, 1, IntPtr.Zero, IntPtr.Zero, 0); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 8a146f922d..3c41f7bdde 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -321,6 +321,9 @@ namespace Avalonia.X11 public static extern IntPtr XCreatePixmapCursor(IntPtr display, IntPtr source, IntPtr mask, ref XColor foreground_color, ref XColor background_color, int x_hot, int y_hot); + [DllImport(libX11)] + public static extern IntPtr XCreateBitmapFromData(IntPtr display, IntPtr drawable, byte[] data, int width, int height); + [DllImport(libX11)] public static extern IntPtr XCreatePixmapFromBitmapData(IntPtr display, IntPtr drawable, byte[] data, int width, int height, IntPtr fg, IntPtr bg, int depth); From 3860fafa000e9127f436745b998f6d8994cdb715 Mon Sep 17 00:00:00 2001 From: MarkusKgit <13160892+MarkusKgit@users.noreply.github.com> Date: Mon, 20 May 2019 12:10:49 +0200 Subject: [PATCH 03/23] Fix formatting --- src/Avalonia.X11/X11CursorFactory.cs | 10 +++++----- src/Gtk/Avalonia.Gtk3/CursorFactory.cs | 4 ++-- src/Windows/Avalonia.Win32/CursorFactory.cs | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index c020c44662..0a8b1ee9c4 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -9,7 +9,7 @@ namespace Avalonia.X11 class X11CursorFactory : IStandardCursorFactory { private static IntPtr _nullCursor; - + private readonly IntPtr _display; private Dictionary _cursors; @@ -48,10 +48,10 @@ namespace Avalonia.X11 _cursors = Enum.GetValues(typeof(CursorFontShape)).Cast() .ToDictionary(id => id, id => XLib.XCreateFontCursor(_display, id)); } - + public IPlatformHandle GetCursor(StandardCursorType cursorType) { - IntPtr handle; + IntPtr handle; if (cursorType == StandardCursorType.None) { handle = _nullCursor; @@ -61,7 +61,7 @@ namespace Avalonia.X11 handle = s_mapping.TryGetValue(cursorType, out var shape) ? _cursors[shape] : _cursors[CursorFontShape.XC_top_left_arrow]; - } + } return new PlatformHandle(handle, "XCURSOR"); } @@ -71,7 +71,7 @@ namespace Avalonia.X11 byte[] data = new byte[] { 0 }; IntPtr window = XLib.XRootWindow(display, 0); IntPtr pixmap = XLib.XCreateBitmapFromData(display, window, data, 1, 1); - return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); + return XLib.XCreatePixmapCursor(display, pixmap, pixmap, ref color, ref color, 0, 0); } } } diff --git a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs index f36e6b986d..95fa3ba9e3 100644 --- a/src/Gtk/Avalonia.Gtk3/CursorFactory.cs +++ b/src/Gtk/Avalonia.Gtk3/CursorFactory.cs @@ -13,7 +13,7 @@ namespace Avalonia.Gtk3 { {StandardCursorType.None, CursorType.Blank}, - { StandardCursorType.AppStarting, CursorType.Watch}, + {StandardCursorType.AppStarting, CursorType.Watch}, {StandardCursorType.Arrow, CursorType.LeftPtr}, {StandardCursorType.Cross, CursorType.Cross}, {StandardCursorType.Hand, CursorType.Hand1}, @@ -37,7 +37,7 @@ namespace Avalonia.Gtk3 {StandardCursorType.BottomRightCorner, CursorType.BottomRightCorner}, {StandardCursorType.DragCopy, CursorType.CenterPtr}, {StandardCursorType.DragMove, CursorType.Fleur}, - {StandardCursorType.DragLink, CursorType.Cross}, + {StandardCursorType.DragLink, CursorType.Cross}, }; private static readonly Dictionary Cache = diff --git a/src/Windows/Avalonia.Win32/CursorFactory.cs b/src/Windows/Avalonia.Win32/CursorFactory.cs index df413addef..f1fd74f931 100644 --- a/src/Windows/Avalonia.Win32/CursorFactory.cs +++ b/src/Windows/Avalonia.Win32/CursorFactory.cs @@ -42,7 +42,7 @@ namespace Avalonia.Win32 { {StandardCursorType.None, 0}, - { StandardCursorType.AppStarting, 32650}, + {StandardCursorType.AppStarting, 32650}, {StandardCursorType.Arrow, 32512}, {StandardCursorType.Cross, 32515}, {StandardCursorType.Hand, 32649}, @@ -70,7 +70,7 @@ namespace Avalonia.Win32 // Fallback, should have been loaded from ole32.dll {StandardCursorType.DragMove, 32516}, {StandardCursorType.DragCopy, 32516}, - {StandardCursorType.DragLink, 32516}, + {StandardCursorType.DragLink, 32516}, }; private static readonly Dictionary Cache = From c3f142af2e75c83c57262e9ab27efa7c25e02043 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 18:35:47 +0300 Subject: [PATCH 04/23] Add ViewModelViewHost and TransitioningUserControl --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 143 +++--------------- .../TransitioningUserControl.cs | 127 ++++++++++++++++ src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 75 +++++++++ 3 files changed, 219 insertions(+), 126 deletions(-) create mode 100644 src/Avalonia.ReactiveUI/TransitioningUserControl.cs create mode 100644 src/Avalonia.ReactiveUI/ViewModelViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 4bd86a67c0..bf295aafca 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningUserControl, IActivatable, IEnableLogger { /// - /// The router dependency property. + /// for the property. /// public static readonly AvaloniaProperty RouterProperty = AvaloniaProperty.Register(nameof(Router)); - - /// - /// The default content property. - /// - public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - /// - /// Fade in animation property. - /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// Fade out animation property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI set => SetValue(RouterProperty, 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 animation played when page appears. - /// - public IAnimation FadeInAnimation - { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); - } - - /// - /// Gets or sets the animation played when page disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - - /// - /// Duplicates the Content property with a private setter. - /// - public new object Content - { - get => base.Content; - private set => base.Content = value; - } - /// /// Gets or sets the ReactiveUI view locator used by this router. /// @@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI /// Invoked when ReactiveUI router navigates to a view model. /// /// ViewModel to which the user navigates. - /// - /// Thrown when ViewLocator is unable to find the appropriate view. - /// - private void NavigateToViewModel(IRoutableViewModel viewModel) + private void NavigateToViewModel(object viewModel) { if (viewModel == null) { - this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + this.Log().Info("ViewModel is null. Falling back to default content."); + Content = DefaultContent; return; } var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current; - var view = viewLocator.ResolveView(viewModel); - if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + var viewInstance = viewLocator.ResolveView(viewModel); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } - this.Log().Info($"Ready to show {view} with autowired {viewModel}."); - view.ViewModel = viewModel; - if (view is IStyledElement styled) + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) styled.DataContext = viewModel; - UpdateContent(view); - } - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContent(object newContent) - { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); - Content = newContent; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + Content = viewInstance; } } -} +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs new file mode 100644 index 0000000000..fb5258f2e4 --- /dev/null +++ b/src/Avalonia.ReactiveUI/TransitioningUserControl.cs @@ -0,0 +1,127 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; + +namespace Avalonia.ReactiveUI +{ + /// + /// A ContentControl that animates the transition when its content is changed. + /// + public class TransitioningUserControl : UserControl + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + + /// + /// for the property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + /// + /// Gets or sets the animation played when content appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when content disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, 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 with animation. + /// + public new object Content + { + get => base.Content; + set => UpdateContentWithTransition(value); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContentWithTransition(object content) + { + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); + base.Content = content; + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs new file mode 100644 index 0000000000..84108a9b52 --- /dev/null +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using ReactiveUI; +using Splat; + +namespace Avalonia.ReactiveUI +{ + /// + /// 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 : TransitioningUserControl, IViewFor, IEnableLogger + { + /// + /// for the property. + /// + public static readonly AvaloniaProperty ViewModelProperty = + AvaloniaProperty.Register(nameof(ViewModel)); + + /// + /// Gets or sets the ViewModel to display. + /// + public object ViewModel + { + get => GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the view locator. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Updates the Content when ViewModel changes. + /// + /// Property changed event arguments. + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); + base.OnPropertyChanged(e); + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + private void NavigateToViewModel(object viewModel) + { + 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); + if (viewInstance == null) + { + this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content."); + Content = DefaultContent; + return; + } + + this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}."); + viewInstance.ViewModel = viewModel; + if (viewInstance is IStyledElement styled) + styled.DataContext = viewModel; + Content = viewInstance; + } + } +} \ No newline at end of file From 97b44c02d25d07c0e2adfecb842e016c54d2e434 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:09:51 +0300 Subject: [PATCH 05/23] Add a unit test for ViewModelViewHost --- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 27 ++++--- .../Attributes.cs | 6 ++ .../AvaloniaActivationForViewFetcherTest.cs | 2 +- .../RoutedViewHostTest.cs | 2 +- .../ViewModelViewHostTest.cs | 72 +++++++++++++++++++ 5 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 84108a9b52..6f80bff4b8 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Reactive.Disposables; using ReactiveUI; using Splat; @@ -18,7 +20,20 @@ namespace Avalonia.ReactiveUI /// public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty.Register(nameof(ViewModel)); - + + /// + /// Initializes a new instance of the class. + /// + public ViewModelViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.ViewModel) + .Subscribe(NavigateToViewModel) + .DisposeWith(disposables); + }); + } + /// /// Gets or sets the ViewModel to display. /// @@ -33,16 +48,6 @@ namespace Avalonia.ReactiveUI /// public IViewLocator ViewLocator { get; set; } - /// - /// Updates the Content when ViewModel changes. - /// - /// Property changed event arguments. - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) - { - if (e.Property.Name == nameof(ViewModel)) NavigateToViewModel(e.NewValue); - base.OnPropertyChanged(e); - } - /// /// Invoked when ReactiveUI router navigates to a view model. /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs new file mode 100644 index 0000000000..79e58f63e9 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -0,0 +1,6 @@ +using Xunit; + +// Required to avoid InvalidOperationException sometimes thrown +// from Splat.MemoizingMRUCache.cs which is not thread-safe. +// Thrown when trying to access WhenActivated concurrently. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index d9f1ce47dd..f4dffdc0c3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -13,7 +13,7 @@ using Splat; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class AvaloniaActivationForViewFetcherTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index 401d169896..c6017d3f5f 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; using System.Reactive; using Avalonia.ReactiveUI; -namespace Avalonia +namespace Avalonia.ReactiveUI.UnitTests { public class RoutedViewHostTest { diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs new file mode 100644 index 0000000000..5d5d15358e --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -0,0 +1,72 @@ +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ViewModelViewHostTest + { + public class FirstViewModel { } + + public class FirstView : ReactiveUserControl { } + + public class SecondViewModel : ReactiveObject { } + + public class SecondView : ReactiveUserControl { } + + public ViewModelViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor)); + } + + [Fact] + public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel() + { + var defaultContent = new TextBlock(); + var host = new ViewModelViewHost + { + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + var first = new FirstViewModel(); + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + + var second = new SecondViewModel(); + host.ViewModel = second; + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondView), host.Content.GetType()); + Assert.Equal(second, ((SecondView)host.Content).DataContext); + Assert.Equal(second, ((SecondView)host.Content).ViewModel); + + host.ViewModel = null; + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + host.ViewModel = first; + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstView), host.Content.GetType()); + Assert.Equal(first, ((FirstView)host.Content).DataContext); + Assert.Equal(first, ((FirstView)host.Content).ViewModel); + } + } +} \ No newline at end of file From 724a5da8963ea83d616fce8691a9ded211ccb4ef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:10:40 +0300 Subject: [PATCH 06/23] Add license headers --- tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs | 3 +++ .../AvaloniaActivationForViewFetcherTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs | 3 +++ tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs index 79e58f63e9..a79647d355 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Xunit; // Required to avoid InvalidOperationException sometimes thrown diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index f4dffdc0c3..2c81e8fea3 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index c6017d3f5f..e873c60e36 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index 5d5d15358e..a21bf34ef5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using Avalonia.Controls; using Avalonia.UnitTests; using ReactiveUI; From e8c0012c318522529f0e572a7535ab60d628d96e Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 19:26:29 +0300 Subject: [PATCH 07/23] Unit tests for ReactiveWindow and ReactiveUserControl --- .../ReactiveUserControlTest.cs | 34 +++++++++++++++++ .../ReactiveWindowTest.cs | 37 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs new file mode 100644 index 0000000000..328b749ba1 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveUserControlTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleView : ReactiveUserControl { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_User_Control_View_Model() + { + var view = new ExampleView(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs new file mode 100644 index 0000000000..ff77de8d28 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Controls; +using Avalonia.UnitTests; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class ReactiveWindowTest + { + public class ExampleViewModel : ReactiveObject { } + + public class ExampleWindow : ReactiveWindow { } + + [Fact] + public void Data_Context_Should_Stay_In_Sync_With_Reactive_Window_View_Model() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var view = new ExampleWindow(); + var viewModel = new ExampleViewModel(); + Assert.Null(view.ViewModel); + + view.DataContext = viewModel; + Assert.Equal(view.ViewModel, viewModel); + Assert.Equal(view.DataContext, viewModel); + + view.DataContext = null; + Assert.Null(view.ViewModel); + Assert.Null(view.DataContext); + } + } + } +} \ No newline at end of file From fe7e7054fb7012b8f901de4d7abc8664bc9f40e9 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 20:40:26 +0300 Subject: [PATCH 08/23] Add initial AutoDataTemplateBindingHook implementation --- .../AutoDataTemplateBindingHook.cs | 60 +++++++++++++++++++ .../AutoDataTemplateBindingHookTest.cs | 56 +++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs new file mode 100644 index 0000000000..bd156a5884 --- /dev/null +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.Templates; +using ReactiveUI; + +namespace Avalonia.ReactiveUI +{ + /// + /// 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 Lazy DefaultItemTemplate { get; } = new Lazy(() => + { + var template = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + return (DataTemplate)loader.Load(template); + }); + + /// + public bool ExecuteHook( + object source, object target, + Func[]> getCurrentViewModelProperties, + Func[]> getCurrentViewProperties, + BindingDirection direction) + { + var viewProperties = getCurrentViewProperties(); + var lastViewProperty = viewProperties.LastOrDefault(); + if (lastViewProperty == null) + return true; + + 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; + + itemsControl.ItemTemplate = DefaultItemTemplate.Value; + return true; + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs new file mode 100644 index 0000000000..2391daece2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -0,0 +1,56 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Xunit; +using ReactiveUI; +using Avalonia.ReactiveUI; +using Avalonia.UnitTests; +using Avalonia.Controls; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Splat; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class AutoDataTemplateBindingHookTest + { + public class NestedViewModel : ReactiveObject { } + + public class NestedView : ReactiveUserControl { } + + public class ExampleViewModel : ReactiveObject + { + public ObservableCollection Items { get; } = new ObservableCollection(); + } + + public class ExampleView : ReactiveUserControl + { + public ListBox List { get; } = new ListBox(); + + public ExampleView() + { + Content = List; + ViewModel = new ExampleViewModel(); + this.OneWayBind(ViewModel, x => x.Items, x => x.List.Items); + } + } + + public AutoDataTemplateBindingHookTest() + { + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); + Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + } + + [Fact] + public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() + { + var view = new ExampleView(); + var root = new TestRoot + { + Child = view + }; + Assert.NotNull(view.List.ItemTemplate); + } + } +} \ No newline at end of file From b486112d296354f0d9d46f4fea72ea8bdb5c2bef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:06:31 +0300 Subject: [PATCH 09/23] Use null propagation --- src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bd156a5884..bb660fd76a 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -38,10 +38,7 @@ namespace Avalonia.ReactiveUI { var viewProperties = getCurrentViewProperties(); var lastViewProperty = viewProperties.LastOrDefault(); - if (lastViewProperty == null) - return true; - - var itemsControl = lastViewProperty.Sender as ItemsControl; + var itemsControl = lastViewProperty?.Sender as ItemsControl; if (itemsControl == null) return true; From 72fe95272a09644f72fd34c7d14acf42ef576819 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:08:52 +0300 Subject: [PATCH 10/23] Register auto data template binding hook --- src/Avalonia.ReactiveUI/AppBuilderExtensions.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index f67cb7f40a..ced26a3004 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI return builder.AfterSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; - Locator.CurrentMutable.Register( - () => new AvaloniaActivationForViewFetcher(), - typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); }); } } From d2574b383a1880bdd3aebfc94b9ee26ce653629d Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 22:47:18 +0300 Subject: [PATCH 11/23] Use FuncDataTemplate --- .../AutoDataTemplateBindingHook.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index bb660fd76a..d7cfebf1d8 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -1,6 +1,8 @@ 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; @@ -14,19 +16,18 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static Lazy DefaultItemTemplate { get; } = new Lazy(() => { - var template = @" - - -"; - - var loader = new AvaloniaXamlLoader(); - return (DataTemplate)loader.Load(template); + return 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); }); /// From 65f5f58a07f29c5baf4fe80218cc2a83deba5dc6 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 00:36:53 +0300 Subject: [PATCH 12/23] ViewModelViewHost type resolution assertion --- .../AutoDataTemplateBindingHookTest.cs | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 2391daece2..4ff3e1fed6 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -6,9 +6,12 @@ using ReactiveUI; using Avalonia.ReactiveUI; using Avalonia.UnitTests; using Avalonia.Controls; +using Avalonia.Controls.Templates; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Avalonia.VisualTree; +using Avalonia.Controls.Presenters; using Splat; namespace Avalonia.ReactiveUI.UnitTests @@ -26,7 +29,7 @@ namespace Avalonia.ReactiveUI.UnitTests public class ExampleView : ReactiveUserControl { - public ListBox List { get; } = new ListBox(); + public ItemsControl List { get; } = new ItemsControl(); public ExampleView() { @@ -40,17 +43,48 @@ namespace Avalonia.ReactiveUI.UnitTests { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); } [Fact] public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set() { var view = new ExampleView(); - var root = new TestRoot - { - Child = view - }; Assert.NotNull(view.List.ItemTemplate); } + + [Fact] + public void Should_Use_View_Model_View_Host_As_Data_Template() + { + var view = new ExampleView(); + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + Assert.IsType(container.Child); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + MemberSelector = parent.MemberSelector, + [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], + } + }; + }); + } } } \ No newline at end of file From 8a2d1662fdcd92057d39aa39a2e8296e134ab0bd Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 23 May 2019 01:20:36 +0300 Subject: [PATCH 13/23] Remove LazyT, test new template resolver --- .../AutoDataTemplateBindingHook.cs | 23 +++++++-------- .../AutoDataTemplateBindingHookTest.cs | 28 ++++++++++++++++++- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index d7cfebf1d8..3f41f54363 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -16,19 +16,16 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static Lazy DefaultItemTemplate { get; } = new Lazy(() => + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => { - return 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); - }); + 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( @@ -51,7 +48,7 @@ namespace Avalonia.ReactiveUI if (itemsControl.ItemTemplate != null) return true; - itemsControl.ItemTemplate = DefaultItemTemplate.Value; + itemsControl.ItemTemplate = DefaultItemTemplate; return true; } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs index 4ff3e1fed6..667462eb91 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs @@ -13,6 +13,8 @@ using System.Linq; using Avalonia.VisualTree; using Avalonia.Controls.Presenters; using Splat; +using System.Threading.Tasks; +using System; namespace Avalonia.ReactiveUI.UnitTests { @@ -42,8 +44,8 @@ namespace Avalonia.ReactiveUI.UnitTests public AutoDataTemplateBindingHookTest() { Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - Locator.CurrentMutable.Register(() => new ExampleView(), typeof(IViewFor)); Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor)); } [Fact] @@ -70,6 +72,30 @@ namespace Avalonia.ReactiveUI.UnitTests Assert.IsType(container.Child); } + [Fact] + public void Should_Resolve_And_Embedd_Appropriate_View_Model() + { + var view = new ExampleView(); + var root = new TestRoot { Child = view }; + view.ViewModel.Items.Add(new NestedViewModel()); + + view.List.Template = GetTemplate(); + view.List.ApplyTemplate(); + view.List.Presenter.ApplyTemplate(); + + var child = view.List.Presenter.Panel.Children[0]; + var container = (ContentPresenter) child; + container.UpdateChild(); + + var host = (ViewModelViewHost) container.Child; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + + host.DataContext = "changed context"; + Assert.IsType(host.ViewModel); + Assert.IsType(host.DataContext); + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate(parent => From 6360de1921e157fde03f6e9c1d25e4094ec3fcb7 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 29 May 2019 01:28:37 +0300 Subject: [PATCH 14/23] Derive transitioning control from ContentControl --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 2 +- ...trol.cs => TransitioningContentControl.cs} | 14 ++-- src/Avalonia.ReactiveUI/ViewModelViewHost.cs | 2 +- .../TransitioningContentControlTest.cs | 64 +++++++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) rename src/Avalonia.ReactiveUI/{TransitioningUserControl.cs => TransitioningContentControl.cs} (87%) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index bf295aafca..05edeea683 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -53,7 +53,7 @@ namespace Avalonia.ReactiveUI /// ReactiveUI routing documentation website for more info. /// /// - public class RoutedViewHost : TransitioningUserControl, IActivatable, IEnableLogger + public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger { /// /// for the property. diff --git a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs similarity index 87% rename from src/Avalonia.ReactiveUI/TransitioningUserControl.cs rename to src/Avalonia.ReactiveUI/TransitioningContentControl.cs index fb5258f2e4..db8f3c964c 100644 --- a/src/Avalonia.ReactiveUI/TransitioningUserControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -11,27 +11,27 @@ namespace Avalonia.ReactiveUI /// /// A ContentControl that animates the transition when its content is changed. /// - public class TransitioningUserControl : UserControl + public class TransitioningContentControl : ContentControl, IStyleable { /// /// for the property. /// public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), + AvaloniaProperty.Register(nameof(DefaultContent), CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); /// /// for the property. /// public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), + AvaloniaProperty.Register(nameof(DefaultContent), CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// for the property. /// public static readonly AvaloniaProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); + AvaloniaProperty.Register(nameof(DefaultContent)); /// /// Gets or sets the animation played when content appears. @@ -68,6 +68,12 @@ namespace Avalonia.ReactiveUI get => base.Content; set => UpdateContentWithTransition(value); } + + /// + /// TransitioningContentControl uses the default ContentControl + /// template from Avalonia default theme. + /// + Type IStyleable.StyleKey => typeof(ContentControl); /// /// Updates the content with transitions. diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 6f80bff4b8..5cfa464c37 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -13,7 +13,7 @@ namespace Avalonia.ReactiveUI /// 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 : TransitioningUserControl, IViewFor, IEnableLogger + public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger { /// /// for the property. diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs new file mode 100644 index 0000000000..5ffcede5dd --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs @@ -0,0 +1,64 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using ReactiveUI; +using Splat; +using Xunit; + +namespace Avalonia.ReactiveUI.UnitTests +{ + public class TransitioningContentControlTest + { + [Fact] + public void Transitioning_Control_Should_Derive_Template_From_Content_Control() + { + var target = new TransitioningContentControl(); + var stylable = (IStyledElement)target; + Assert.Equal(typeof(ContentControl),stylable.StyleKey); + } + + [Fact] + public void Transitioning_Control_Template_Should_Be_Instantiated() + { + var target = new TransitioningContentControl + { + FadeInAnimation = null, + FadeOutAnimation = null, + Template = GetTemplate(), + Content = "Foo" + }; + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + var child = ((IVisual)target).VisualChildren.Single(); + Assert.IsType(child); + child = child.VisualChildren.Single(); + Assert.IsType(child); + child = child.VisualChildren.Single(); + Assert.IsType(child); + } + + private FuncControlTemplate GetTemplate() + { + return new FuncControlTemplate(parent => + { + return new Border + { + Background = new Media.SolidColorBrush(0xffffffff), + Child = new ContentPresenter + { + Name = "PART_ContentPresenter", + [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], + [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], + } + }; + }); + } + } +} \ No newline at end of file From f0574cbf1a570f6530eb0349967ce5740dccf9ef Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 29 May 2019 02:10:03 +0300 Subject: [PATCH 15/23] Use PageTransition, fix possible bug with CrossFade --- .../TransitioningContentControl.cs | 82 +++---------------- src/Avalonia.Visuals/Animation/CrossFade.cs | 8 +- .../RoutedViewHostTest.cs | 3 +- .../TransitioningContentControlTest.cs | 3 +- .../ViewModelViewHostTest.cs | 3 +- 5 files changed, 19 insertions(+), 80 deletions(-) diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index db8f3c964c..68d69bc95f 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -14,18 +14,11 @@ namespace Avalonia.ReactiveUI public class TransitioningContentControl : ContentControl, IStyleable { /// - /// for the property. + /// for the property. /// - public static readonly AvaloniaProperty FadeInAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); - - /// - /// for the property. - /// - public static readonly AvaloniaProperty FadeOutAnimationProperty = - AvaloniaProperty.Register(nameof(DefaultContent), - CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); + public static readonly AvaloniaProperty PageTransitionProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + new CrossFade(TimeSpan.FromSeconds(0.5))); /// /// for the property. @@ -34,23 +27,14 @@ namespace Avalonia.ReactiveUI AvaloniaProperty.Register(nameof(DefaultContent)); /// - /// Gets or sets the animation played when content appears. + /// Gets or sets the animation played when content appears and disappears. /// - public IAnimation FadeInAnimation + public IPageTransition PageTransition { - get => GetValue(FadeInAnimationProperty); - set => SetValue(FadeInAnimationProperty, value); + get => GetValue(PageTransitionProperty); + set => SetValue(PageTransitionProperty, value); } - /// - /// Gets or sets the animation played when content disappears. - /// - public IAnimation FadeOutAnimation - { - get => GetValue(FadeOutAnimationProperty); - set => SetValue(FadeOutAnimationProperty, value); - } - /// /// Gets or sets the content displayed whenever there is no page currently routed. /// @@ -81,53 +65,11 @@ namespace Avalonia.ReactiveUI /// New content to set. private async void UpdateContentWithTransition(object content) { - if (FadeOutAnimation != null) - await FadeOutAnimation.RunAsync(this, Clock); + if (PageTransition != null) + await PageTransition.Start(this, null, true); base.Content = content; - if (FadeInAnimation != null) - await FadeInAnimation.RunAsync(this, Clock); - } - - /// - /// Creates opacity animation for this routed view host. - /// - /// Opacity to start from. - /// Opacity to finish with. - /// Duration of the animation. - /// Animation object instance. - private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) - { - return new Avalonia.Animation.Animation - { - Duration = duration, - Children = - { - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = from - } - }, - Cue = new Cue(0d) - }, - new KeyFrame - { - Setters = - { - new Setter - { - Property = OpacityProperty, - Value = to - } - }, - Cue = new Cue(1d) - } - } - }; + if (PageTransition != null) + await PageTransition.Start(null, this, true); } } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 6b8cb8b755..614d828259 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -14,8 +14,8 @@ namespace Avalonia.Animation /// public class CrossFade : IPageTransition { - private Animation _fadeOutAnimation; - private Animation _fadeInAnimation; + private readonly Animation _fadeOutAnimation; + private readonly Animation _fadeInAnimation; /// /// Initializes a new instance of the class. @@ -61,10 +61,10 @@ namespace Avalonia.Animation new Setter { Property = Visual.OpacityProperty, - Value = 0d + Value = 1d } }, - Cue = new Cue(0d) + Cue = new Cue(1d) } } diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs index e873c60e36..8c5b5083e8 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -62,8 +62,7 @@ namespace Avalonia.ReactiveUI.UnitTests { Router = screen.Router, DefaultContent = defaultContent, - FadeOutAnimation = null, - FadeInAnimation = null + PageTransition = null }; var root = new TestRoot diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs index 5ffcede5dd..f09eea5ec5 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs @@ -28,8 +28,7 @@ namespace Avalonia.ReactiveUI.UnitTests { var target = new TransitioningContentControl { - FadeInAnimation = null, - FadeOutAnimation = null, + PageTransition = null, Template = GetTemplate(), Content = "Foo" }; diff --git a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs index a21bf34ef5..8bed5adcd4 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs @@ -33,8 +33,7 @@ namespace Avalonia.ReactiveUI.UnitTests var host = new ViewModelViewHost { DefaultContent = defaultContent, - FadeOutAnimation = null, - FadeInAnimation = null + PageTransition = null }; var root = new TestRoot From 12ebd106cbed16c77c64d53b16def9bc929fa7dc Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 29 May 2019 02:12:03 +0300 Subject: [PATCH 16/23] Typo fix --- src/Avalonia.ReactiveUI/TransitioningContentControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs index 68d69bc95f..1bec5fc365 100644 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs @@ -17,7 +17,7 @@ namespace Avalonia.ReactiveUI /// for the property. /// public static readonly AvaloniaProperty PageTransitionProperty = - AvaloniaProperty.Register(nameof(DefaultContent), + AvaloniaProperty.Register(nameof(PageTransition), new CrossFade(TimeSpan.FromSeconds(0.5))); /// From 36d24bcea207254b1323670dd333ab3ba64a4218 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 31 May 2019 17:15:25 +0100 Subject: [PATCH 17/23] improved notification colours. --- src/Avalonia.Themes.Default/Accents/BaseDark.xaml | 10 +++++----- src/Avalonia.Themes.Default/Accents/BaseLight.xaml | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml index f84e09510b..8f7d56dbc6 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseDark.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1,1,1,1 0.5 diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 18c32b02bc..666596d710 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -46,11 +46,11 @@ - - - - - + + + + + 1 0.5 From bf8e23457172aeb73d4d54aea8ed711ddc200cd9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 31 May 2019 17:36:04 +0100 Subject: [PATCH 18/23] allow notification card size to be controlled with styles. --- src/Avalonia.Themes.Default/NotificationCard.xaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Default/NotificationCard.xaml b/src/Avalonia.Themes.Default/NotificationCard.xaml index e94cb33d1e..bcc7bb9a29 100644 --- a/src/Avalonia.Themes.Default/NotificationCard.xaml +++ b/src/Avalonia.Themes.Default/NotificationCard.xaml @@ -13,7 +13,7 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Margin="8,8,0,0"> - + @@ -40,6 +40,10 @@ + +