From c3f142af2e75c83c57262e9ab27efa7c25e02043 Mon Sep 17 00:00:00 2001 From: artyom Date: Wed, 22 May 2019 18:35:47 +0300 Subject: [PATCH] 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