Browse Source

Merge pull request #2560 from worldbeater/master

Add ViewModelViewHost and AutoDataTemplateBindingHook
rtb-gl-fixes
Nikita Tsukanov 7 years ago
committed by GitHub
parent
commit
627999cd6f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  2. 55
      src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
  3. 143
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  4. 75
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  5. 80
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  6. 8
      src/Avalonia.Visuals/Animation/CrossFade.cs
  7. 9
      tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs
  8. 116
      tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs
  9. 5
      tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs
  10. 34
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveUserControlTest.cs
  11. 37
      tests/Avalonia.ReactiveUI.UnitTests/ReactiveWindowTest.cs
  12. 8
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
  13. 63
      tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs
  14. 74
      tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

5
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));
});
}
}

55
src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs

@ -0,0 +1,55 @@
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 Avalonia.ReactiveUI
{
/// <summary>
/// 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.
/// </summary>
public class AutoDataTemplateBindingHook : IPropertyBindingHook
{
private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate<object>(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);
/// <inheritdoc/>
public bool ExecuteHook(
object source, object target,
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
Func<IObservedChange<object, object>[]> 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;
itemsControl.ItemTemplate = DefaultItemTemplate;
return true;
}
}
}

143
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI
/// ReactiveUI routing documentation website</see> for more info.
/// </para>
/// </remarks>
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger
{
/// <summary>
/// The router dependency property.
/// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
/// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </summary>
@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
/// <exception cref="Exception">
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
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);
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
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;
}
}
}
}

75
src/Avalonia.ReactiveUI/TransitioningContentControl.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 System;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// A ContentControl that animates the transition when its content is changed.
/// </summary>
public class TransitioningContentControl : ContentControl, IStyleable
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
/// </summary>
public static readonly AvaloniaProperty<IPageTransition> PageTransitionProperty =
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition),
new CrossFade(TimeSpan.FromSeconds(0.5)));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent));
/// <summary>
/// Gets or sets the animation played when content appears and disappears.
/// </summary>
public IPageTransition PageTransition
{
get => GetValue(PageTransitionProperty);
set => SetValue(PageTransitionProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the content with animation.
/// </summary>
public new object Content
{
get => base.Content;
set => UpdateContentWithTransition(value);
}
/// <summary>
/// TransitioningContentControl uses the default ContentControl
/// template from Avalonia default theme.
/// </summary>
Type IStyleable.StyleKey => typeof(ContentControl);
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="content">New content to set.</param>
private async void UpdateContentWithTransition(object content)
{
if (PageTransition != null)
await PageTransition.Start(this, null, true);
base.Content = content;
if (PageTransition != null)
await PageTransition.Start(null, this, true);
}
}
}

80
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -0,0 +1,80 @@
// 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;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// 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.
/// </summary>
public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel));
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary>
public ViewModelViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.ViewModel)
.Subscribe(NavigateToViewModel)
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the ViewModel to display.
/// </summary>
public object ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>
/// Gets or sets the view locator.
/// </summary>
public IViewLocator ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
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;
}
}
}

8
src/Avalonia.Visuals/Animation/CrossFade.cs

@ -14,8 +14,8 @@ namespace Avalonia.Animation
/// </summary>
public class CrossFade : IPageTransition
{
private Animation _fadeOutAnimation;
private Animation _fadeInAnimation;
private readonly Animation _fadeOutAnimation;
private readonly Animation _fadeInAnimation;
/// <summary>
/// Initializes a new instance of the <see cref="CrossFade"/> 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)
}
}

9
tests/Avalonia.ReactiveUI.UnitTests/Attributes.cs

@ -0,0 +1,9 @@
// 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
// from Splat.MemoizingMRUCache.cs which is not thread-safe.
// Thrown when trying to access WhenActivated concurrently.
[assembly: CollectionBehavior(DisableTestParallelization = true)]

116
tests/Avalonia.ReactiveUI.UnitTests/AutoDataTemplateBindingHookTest.cs

@ -0,0 +1,116 @@
// 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 Avalonia.Controls.Templates;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.VisualTree;
using Avalonia.Controls.Presenters;
using Splat;
using System.Threading.Tasks;
using System;
namespace Avalonia.ReactiveUI.UnitTests
{
public class AutoDataTemplateBindingHookTest
{
public class NestedViewModel : ReactiveObject { }
public class NestedView : ReactiveUserControl<NestedViewModel> { }
public class ExampleViewModel : ReactiveObject
{
public ObservableCollection<NestedViewModel> Items { get; } = new ObservableCollection<NestedViewModel>();
}
public class ExampleView : ReactiveUserControl<ExampleViewModel>
{
public ItemsControl List { get; } = new ItemsControl();
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.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new NestedView(), typeof(IViewFor<NestedViewModel>));
}
[Fact]
public void Should_Apply_Data_Template_Binding_When_No_Template_Is_Set()
{
var view = new ExampleView();
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<ViewModelViewHost>(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<NestedViewModel>(host.ViewModel);
Assert.IsType<NestedViewModel>(host.DataContext);
host.DataContext = "changed context";
Assert.IsType<string>(host.ViewModel);
Assert.IsType<string>(host.DataContext);
}
private FuncControlTemplate GetTemplate()
{
return new FuncControlTemplate<ItemsControl>(parent =>
{
return new Border
{
Background = new Media.SolidColorBrush(0xffffffff),
Child = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
MemberSelector = parent.MemberSelector,
[~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty],
}
};
});
}
}
}

5
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;
@ -13,7 +16,7 @@ using Splat;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Avalonia
namespace Avalonia.ReactiveUI.UnitTests
{
public class AvaloniaActivationForViewFetcherTest
{

34
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<ExampleViewModel> { }
[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);
}
}
}

37
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<ExampleViewModel> { }
[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);
}
}
}
}

8
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;
@ -16,7 +19,7 @@ using System.Threading.Tasks;
using System.Reactive;
using Avalonia.ReactiveUI;
namespace Avalonia
namespace Avalonia.ReactiveUI.UnitTests
{
public class RoutedViewHostTest
{
@ -59,8 +62,7 @@ namespace Avalonia
{
Router = screen.Router,
DefaultContent = defaultContent,
FadeOutAnimation = null,
FadeInAnimation = null
PageTransition = null
};
var root = new TestRoot

63
tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs

@ -0,0 +1,63 @@
// 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
{
PageTransition = null,
Template = GetTemplate(),
Content = "Foo"
};
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
var child = ((IVisual)target).VisualChildren.Single();
Assert.IsType<Border>(child);
child = child.VisualChildren.Single();
Assert.IsType<ContentPresenter>(child);
child = child.VisualChildren.Single();
Assert.IsType<TextBlock>(child);
}
private FuncControlTemplate GetTemplate()
{
return new FuncControlTemplate<ContentControl>(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],
}
};
});
}
}
}

74
tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

@ -0,0 +1,74 @@
// 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 ViewModelViewHostTest
{
public class FirstViewModel { }
public class FirstView : ReactiveUserControl<FirstViewModel> { }
public class SecondViewModel : ReactiveObject { }
public class SecondView : ReactiveUserControl<SecondViewModel> { }
public ViewModelViewHostTest()
{
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.Register(() => new FirstView(), typeof(IViewFor<FirstViewModel>));
Locator.CurrentMutable.Register(() => new SecondView(), typeof(IViewFor<SecondViewModel>));
}
[Fact]
public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel()
{
var defaultContent = new TextBlock();
var host = new ViewModelViewHost
{
DefaultContent = defaultContent,
PageTransition = 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);
}
}
}
Loading…
Cancel
Save