Browse Source

Add ViewContract to ViewModelViewHost

Sergey Volkov 4 years ago
parent
commit
fe42fbfe6b

+ 42 - 10
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@@ -3,7 +3,7 @@ using System.Reactive.Disposables;
 using ReactiveUI;
 using Splat;
 
-namespace Avalonia.ReactiveUI 
+namespace Avalonia.ReactiveUI
 {
     /// <summary>
     /// This content control will automatically load the View associated with
@@ -18,6 +18,12 @@ namespace Avalonia.ReactiveUI
         public static readonly AvaloniaProperty<object?> ViewModelProperty =
             AvaloniaProperty.Register<ViewModelViewHost, object?>(nameof(ViewModel));
 
+        /// <summary>
+        /// <see cref="AvaloniaProperty"/> for the <see cref="ViewContract"/> property.
+        /// </summary>
+        public static readonly StyledProperty<string?> ViewContractProperty =
+            AvaloniaProperty.Register<ViewModelViewHost, string?>(nameof(ViewContract));
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
         /// </summary>
@@ -25,8 +31,8 @@ namespace Avalonia.ReactiveUI
         {
             this.WhenActivated(disposables =>
             {
-                this.WhenAnyValue(x => x.ViewModel)
-                    .Subscribe(NavigateToViewModel)
+                this.WhenAnyValue(x => x.ViewModel, x => x.ViewContract)
+                    .Subscribe(tuple => NavigateToViewModel(tuple.Item1, tuple.Item2))
                     .DisposeWith(disposables);
             });
         }
@@ -39,7 +45,16 @@ namespace Avalonia.ReactiveUI
             get => GetValue(ViewModelProperty);
             set => SetValue(ViewModelProperty, value);
         }
-        
+
+        /// <summary>
+        /// Gets or sets the view contract.
+        /// </summary>
+        public string? ViewContract
+        {
+            get => GetValue(ViewContractProperty);
+            set => SetValue(ViewContractProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the view locator.
         /// </summary>
@@ -49,7 +64,8 @@ namespace Avalonia.ReactiveUI
         /// 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)
+        /// <param name="contract">The contract for view resolution.</param>
+        private void NavigateToViewModel(object? viewModel, string? contract)
         {
             if (viewModel == null)
             {
@@ -57,17 +73,33 @@ namespace Avalonia.ReactiveUI
                 Content = DefaultContent;
                 return;
             }
-    
+
             var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
-            var viewInstance = viewLocator.ResolveView(viewModel);
+            var viewInstance = viewLocator.ResolveView(viewModel, contract);
             if (viewInstance == null)
             {
-                this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
+                if (contract == null)
+                {
+                    this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
+                }
+                else
+                {
+                    this.Log().Warn($"Couldn't find view with contract '{contract}' for '{viewModel}'. Is it registered? Falling back to default content.");
+                }
+
                 Content = DefaultContent;
                 return;
             }
-    
-            this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
+
+            if (contract == null)
+            {
+                this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
+            }
+            else
+            {
+                this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel} and contract '{contract}'.");
+            }
+
             viewInstance.ViewModel = viewModel;
             if (viewInstance is IStyledElement styled)
                 styled.DataContext = viewModel;

+ 71 - 1
tests/Avalonia.ReactiveUI.UnitTests/ViewModelViewHostTest.cs

@@ -12,15 +12,23 @@ namespace Avalonia.ReactiveUI.UnitTests
 
         public class FirstView : ReactiveUserControl<FirstViewModel> { }
 
+        public class AlternativeFirstView : ReactiveUserControl<FirstViewModel> { }
+
         public class SecondViewModel : ReactiveObject { }
 
         public class SecondView : ReactiveUserControl<SecondViewModel> { }
 
+        public class AlternativeSecondView : ReactiveUserControl<SecondViewModel> { }
+
+        public static string AlternativeViewContract => "AlternativeView";
+
         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>));
+            Locator.CurrentMutable.Register(() => new AlternativeFirstView(), typeof(IViewFor<FirstViewModel>), AlternativeViewContract);
+            Locator.CurrentMutable.Register(() => new AlternativeSecondView(), typeof(IViewFor<SecondViewModel>), AlternativeViewContract);
         }
 
         [Fact]
@@ -67,5 +75,67 @@ namespace Avalonia.ReactiveUI.UnitTests
             Assert.Equal(first, ((FirstView)host.Content).DataContext);
             Assert.Equal(first, ((FirstView)host.Content).ViewModel);
         }
+
+        [Fact]
+        public void ViewModelViewHost_View_Should_Stay_In_Sync_With_ViewModel_And_Contract()
+        {
+            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;
+
+            host.ViewContract = null;
+            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);
+
+            host.ViewContract = AlternativeViewContract;
+            Assert.NotNull(host.Content);
+            Assert.Equal(typeof(AlternativeFirstView), host.Content.GetType());
+            Assert.Equal(first, ((AlternativeFirstView)host.Content).DataContext);
+            Assert.Equal(first, ((AlternativeFirstView)host.Content).ViewModel);
+
+            var second = new SecondViewModel();
+            host.ViewModel = second;
+
+            host.ViewContract = null;
+            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.ViewContract = AlternativeViewContract;
+            Assert.NotNull(host.Content);
+            Assert.Equal(typeof(AlternativeSecondView), host.Content.GetType());
+            Assert.Equal(second, ((AlternativeSecondView)host.Content).DataContext);
+            Assert.Equal(second, ((AlternativeSecondView)host.Content).ViewModel);
+
+            host.ViewModel = null;
+
+            host.ViewContract = null;
+            Assert.NotNull(host.Content);
+            Assert.Equal(typeof(TextBlock), host.Content.GetType());
+            Assert.Equal(defaultContent, host.Content);
+
+            host.ViewContract = AlternativeViewContract;
+            Assert.NotNull(host.Content);
+            Assert.Equal(typeof(TextBlock), host.Content.GetType());
+            Assert.Equal(defaultContent, host.Content);
+        }
     }
-}
+}