Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/ReactiveUI.Tests/Routing/Mocks/TestView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Splat;

namespace ReactiveUI.Tests
{
public class TestView : ReactiveUserControl<TestViewModel>, IScreen
{
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

public TestView()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
}

public TestView(IScreen? screen = null)
{
Router = screen?.Router ?? Locator.Current.GetService<RoutingState>()!;
}

public RoutingState Router { get; }
}
}
134 changes: 134 additions & 0 deletions src/ReactiveUI.Tests/Routing/RoutedViewHostTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Reactive.Concurrency;
using System.Windows;
using DynamicData;
using ReactiveUI.Tests.Wpf;
using Splat;
using Xunit;

namespace ReactiveUI.Tests
{
public class RoutedViewHostTests
{
[StaFact]
public void RoutedViewHostDefaultContentNotNull()
{
var uc = new RoutedViewHost
{
DefaultContent = new System.Windows.Controls.Label()
};
var window = new WpfTestWindow();
window.RootGrid.Children.Add(uc);

var activation = new ActivationForViewFetcher();

activation.GetActivationForView(window).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var windowActivated).Subscribe();

activation.GetActivationForView(uc).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var controlActivated).Subscribe();

var loaded = new RoutedEventArgs
{
RoutedEvent = FrameworkElement.LoadedEvent
};

window.RaiseEvent(loaded);
uc.RaiseEvent(loaded);

new[] { true }.AssertAreEqual(windowActivated);
new[] { true }.AssertAreEqual(controlActivated);

Assert.NotNull(uc.Content);

window.Dispatcher.InvokeShutdown();
}

[StaFact]
public void RoutedViewHostDefaultContentNotNullWithViewModelAndActivated()
{
Locator.CurrentMutable.Register<RoutingState>(() => new(ImmediateScheduler.Instance));
Locator.CurrentMutable.Register<TestViewModel>(() => new());
Locator.CurrentMutable.Register<IViewFor<TestViewModel>>(() => new TestView());

var uc = new RoutedViewHost
{
DefaultContent = new System.Windows.Controls.Label(),
Router = Locator.Current.GetService<RoutingState>()!
};
var window = new WpfTestWindow();
window.RootGrid.Children.Add(uc);

var activation = new ActivationForViewFetcher();

activation.GetActivationForView(window).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var windowActivated).Subscribe();

activation.GetActivationForView(uc).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var controlActivated).Subscribe();

var loaded = new RoutedEventArgs
{
RoutedEvent = FrameworkElement.LoadedEvent
};

window.RaiseEvent(loaded);
uc.RaiseEvent(loaded);

new[] { true }.AssertAreEqual(windowActivated);
new[] { true }.AssertAreEqual(controlActivated);

// Default Content
Assert.IsType<System.Windows.Controls.Label>(uc.Content);

// Test Navigation after activated
uc.Router.Navigate.Execute(Locator.Current.GetService<TestViewModel>()!);
Assert.IsType<TestView>(uc.Content);

window.Dispatcher.InvokeShutdown();
}

[StaFact]
public void RoutedViewHostDefaultContentNotNullWithViewModelAndNotActivated()
{
Locator.CurrentMutable.Register<RoutingState>(() => new(ImmediateScheduler.Instance));
Locator.CurrentMutable.Register<TestViewModel>(() => new());
Locator.CurrentMutable.Register<IViewFor<TestViewModel>>(() => new TestView());

var uc = new RoutedViewHost
{
DefaultContent = new System.Windows.Controls.Label(),
Router = Locator.Current.GetService<RoutingState>()!
};
var window = new WpfTestWindow();
window.RootGrid.Children.Add(uc);

var activation = new ActivationForViewFetcher();

activation.GetActivationForView(window).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var windowActivated).Subscribe();

activation.GetActivationForView(uc).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var controlActivated).Subscribe();

var loaded = new RoutedEventArgs
{
RoutedEvent = FrameworkElement.LoadedEvent
};

// Test navigation before Activation.
uc.Router.Navigate.Execute(Locator.Current.GetService<TestViewModel>()!);

// Activate
window.RaiseEvent(loaded);
uc.RaiseEvent(loaded);

new[] { true }.AssertAreEqual(windowActivated);
new[] { true }.AssertAreEqual(controlActivated);

// Test Navigation before activated
Assert.IsType<TestView>(uc.Content);

window.Dispatcher.InvokeShutdown();
}
}
}
93 changes: 93 additions & 0 deletions src/ReactiveUI.Tests/Routing/ViewModelViewHostTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using System;
using System.Reactive.Concurrency;
using System.Windows;
using DynamicData;
using ReactiveUI.Tests.Wpf;
using Splat;
using Xunit;

namespace ReactiveUI.Tests
{
public class ViewModelViewHostTests
{
[StaFact]
public void ViewModelViewHostDefaultContentNotNull()
{
var uc = new ViewModelViewHost
{
DefaultContent = new System.Windows.Controls.Label()
};
var window = new WpfTestWindow();
window.RootGrid.Children.Add(uc);

var activation = new ActivationForViewFetcher();

activation.GetActivationForView(window)
.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
.Bind(out var windowActivated)
.Subscribe();

activation.GetActivationForView(uc)
.ToObservableChangeSet(scheduler: ImmediateScheduler.Instance)
.Bind(out var controlActivated)
.Subscribe();

var loaded = new RoutedEventArgs
{
RoutedEvent = FrameworkElement.LoadedEvent
};

window.RaiseEvent(loaded);
uc.RaiseEvent(loaded);

new[] { true }.AssertAreEqual(windowActivated);
new[] { true }.AssertAreEqual(controlActivated);

Assert.NotNull(uc.Content);

window.Dispatcher.InvokeShutdown();
}

[StaFact]
public void ViewModelViewHostContentNotNullWithViewModelAndActivated()
{
Locator.CurrentMutable.Register<TestViewModel>(() => new());
Locator.CurrentMutable.Register<IViewFor<TestViewModel>>(() => new TestView());

var uc = new ViewModelViewHost
{
DefaultContent = new System.Windows.Controls.Label(),
ViewModel = Locator.Current.GetService<TestViewModel>()
};
var window = new WpfTestWindow();
window.RootGrid.Children.Add(uc);

var activation = new ActivationForViewFetcher();

activation.GetActivationForView(window).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var windowActivated).Subscribe();

activation.GetActivationForView(uc).ToObservableChangeSet(scheduler: ImmediateScheduler.Instance).Bind(out var controlActivated).Subscribe();

var loaded = new RoutedEventArgs
{
RoutedEvent = FrameworkElement.LoadedEvent
};

window.RaiseEvent(loaded);
uc.RaiseEvent(loaded);

new[] { true }.AssertAreEqual(windowActivated);
new[] { true }.AssertAreEqual(controlActivated);

// Test IViewFor<ViewModel> after activated
Assert.IsType<TestView>(uc.Content);

window.Dispatcher.InvokeShutdown();
}
}
}
78 changes: 45 additions & 33 deletions src/ReactiveUI/Platforms/windows-common/RoutedViewHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
#else

using System.Windows.Controls;

#endif

#if HAS_UNO
namespace ReactiveUI.Uno
#else

namespace ReactiveUI
#endif
{
Expand Down Expand Up @@ -55,6 +58,8 @@ class RoutedViewHost : TransitioningContentControl, IActivatableView, IEnableLog
public static readonly DependencyProperty ViewContractObservableProperty =
DependencyProperty.Register("ViewContractObservable", typeof(IObservable<string>), typeof(RoutedViewHost), new PropertyMetadata(Observable<string>.Default));

private string? _viewContract;

/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
/// </summary>
Expand All @@ -66,14 +71,8 @@ public RoutedViewHost()
HorizontalContentAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;

if (ModeDetector.InUnitTestRunner())
{
ViewContractObservable = Observable<string>.Never;
return;
}

var platform = Locator.Current.GetService<IPlatformOperations>();
Func<string?> platformGetter = () => default!;
Func<string?> platformGetter = () => default;

if (platform == null)
{
Expand All @@ -86,47 +85,31 @@ public RoutedViewHost()
platformGetter = () => platform.GetOrientation();
}

ViewContractObservable = Observable.FromEvent<SizeChangedEventHandler, string?>(
ViewContractObservable = ModeDetector.InUnitTestRunner()
? Observable<string>.Never
: Observable.FromEvent<SizeChangedEventHandler, string?>(
eventHandler =>
{
void Handler(object sender, SizeChangedEventArgs e) => eventHandler(platformGetter());
return Handler;
},
x => SizeChanged += x,
x => SizeChanged -= x)
.DistinctUntilChanged()
.StartWith(platformGetter())
.Select(x => x);
.StartWith(platformGetter())
.DistinctUntilChanged();

var vmAndContract = this.WhenAnyObservable(x => x.Router.CurrentViewModel!).CombineLatest(
this.WhenAnyObservable(x => x.ViewContractObservable),
IRoutableViewModel? currentViewModel = null;
var vmAndContract = this.WhenAnyObservable(x => x.Router.CurrentViewModel).Do(x => currentViewModel = x).StartWith(currentViewModel).CombineLatest(
this.WhenAnyObservable(x => x.ViewContractObservable).Do(x => _viewContract = x).StartWith(ViewContract),
(viewModel, contract) => (viewModel, contract));

this.WhenActivated(d =>
{
// NB: The DistinctUntilChanged is useful because most views in
// WinRT will end up getting here twice - once for configuring
// the RoutedViewHost's ViewModel, and once on load via SizeChanged
d(vmAndContract.DistinctUntilChanged().Subscribe(
x =>
{
if (x.viewModel == null)
{
Content = DefaultContent;
return;
}

var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(x.viewModel, x.contract) ?? viewLocator.ResolveView(x.viewModel);

if (view == null)
{
throw new Exception($"Couldn't find view for '{x.viewModel}'.");
}

view.ViewModel = x.viewModel;
Content = view;
},
d(vmAndContract.DistinctUntilChanged<(IRoutableViewModel? viewModel, string? contract)>().Subscribe(
ResolveViewForViewModel,
ex => RxApp.DefaultExceptionHandler.OnNext(ex)));
});
}
Expand Down Expand Up @@ -162,12 +145,41 @@ public IObservable<string?> ViewContractObservable
set => SetValue(ViewContractObservableProperty, value);
}

/// <summary>
/// Gets or sets the view contract.
/// </summary>
public string? ViewContract
{
get => _viewContract;
set => ViewContractObservable = Observable.Return(value);
}

/// <summary>
/// Gets or sets the view locator.
/// </summary>
/// <value>
/// The view locator.
/// </value>
public IViewLocator? ViewLocator { get; set; }

private void ResolveViewForViewModel((IRoutableViewModel? viewModel, string? contract) x)
{
if (x.viewModel == null)
{
Content = DefaultContent;
return;
}

var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(x.viewModel, x.contract) ?? viewLocator.ResolveView(x.viewModel);

if (view == null)
{
throw new Exception($"Couldn't find view for '{x.viewModel}'.");
}

view.ViewModel = x.viewModel;
Content = view;
}
}
}
Loading