Skip to content

Commit

Permalink
fix: Allow Developers Bypass the Default Fallback Behavior (resolves #…
Browse files Browse the repository at this point in the history
…3713) (#3718)

This changes applys to the Maui/Wpf/XamarinForm platform.

<!-- Please be sure to read the
[Contribute](https://github.com/reactiveui/reactiveui#contribute)
section of the README -->

**What kind of change does this PR introduce?**
<!-- Bug fix, feature, docs update, ... -->

- Feature Request. See  #3713 

**What is the current behavior?**
<!-- You can also link to an open issue here. -->

1. The ViewModelViewHost resolves view by the ViewContract property.
Currently ignores the `ViewContract` condition if nothing found.

**What is the new behavior?**

1. Add a property of `ContractFallbackByPass` so that we can bypass this
fallback behavior.
2. Expose a virtual method , i.e. `protected virtual void
ResolveViewForViewModel(object? viewModel, string? contract)` , which
allows developers override this behavior.


**What might this PR break?**

As far as I can see, it does not break anying.

**Please check if the PR fulfills these requirements**
- [x] Tests for the changes have been added (for bug fixes / features)
- [X] Docs have been added / updated (for bug fixes / features)

**Other information**:


For WPF/MAUI/XamForms/WinUI, the `ContractFallbackByPass` is set to
false by default. So it won't breaking existing apps.

However, I find the [current WinForms
implementation](https://github.com/reactiveui/ReactiveUI/blob/9c36b0f0701ee7005556ccafaeb503a96ff6b75f/src/ReactiveUI.Winforms/ViewModelViewHost.cs#L210-L211)
has no default fallback behaivor as same as WPF

```c#
   var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
   var view = viewLocator.ResolveView(x.ViewModel, x.Contract);
   if (view is not null)
   {
       view.ViewModel = x.ViewModel;
       Content = view;
   }
```

So I didn't add such a property for WinForms. Is it better to add such a
property that is set to true by default ?

---------

Co-authored-by: Chris Pulman <chris.pulman@yahoo.com>
  • Loading branch information
newbienewbie and ChrisPulman committed Jan 27, 2024
1 parent e28392d commit 632b33d
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 46 deletions.
28 changes: 26 additions & 2 deletions src/ReactiveUI.Maui/Common/ViewModelViewHost.cs
Expand Up @@ -32,6 +32,12 @@ public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableL
public static readonly DependencyProperty ViewContractObservableProperty =
DependencyProperty.Register(nameof(ViewContractObservable), typeof(IObservable<string>), typeof(ViewModelViewHost), new PropertyMetadata(Observable<string>.Default));

/// <summary>
/// The ContractFallbackByPass dependency property.
/// </summary>
public static readonly DependencyProperty ContractFallbackByPassProperty =
DependencyProperty.Register("ContractFallbackByPass", typeof(bool), typeof(ViewModelViewHost), new PropertyMetadata(false));

private string? _viewContract;

/// <summary>
Expand Down Expand Up @@ -119,12 +125,26 @@ public object DefaultContent
set => ViewContractObservable = Observable.Return(value);
}

/// <summary>
/// Gets or sets a value indicating whether should bypass the default contract fallback behavior.
/// </summary>
public bool ContractFallbackByPass
{
get => (bool)GetValue(ContractFallbackByPassProperty);
set => SetValue(ContractFallbackByPassProperty, value);
}

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

private void ResolveViewForViewModel(object? viewModel, string? contract)
/// <summary>
/// resolve view for view model with respect to contract.
/// </summary>
/// <param name="viewModel">ViewModel.</param>
/// <param name="contract">contract used by ViewLocator.</param>
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract)
{
if (viewModel is null)
{
Expand All @@ -133,7 +153,11 @@ private void ResolveViewForViewModel(object? viewModel, string? contract)
}

var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel, contract) ?? viewLocator.ResolveView(viewModel);
var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance is null && !ContractFallbackByPass)
{
viewInstance = viewLocator.ResolveView(viewModel);
}

if (viewInstance is null)
{
Expand Down
70 changes: 55 additions & 15 deletions src/ReactiveUI.Maui/ViewModelViewHost.cs
Expand Up @@ -40,6 +40,15 @@ public class ViewModelViewHost : ContentView, IViewFor
typeof(ViewModelViewHost),
Observable<string>.Never);

/// <summary>
/// The ContractFallbackByPass dependency property.
/// </summary>
public static readonly BindableProperty ContractFallbackByPassProperty = BindableProperty.Create(
nameof(ContractFallbackByPass),
typeof(bool),
typeof(ViewModelViewHost),
false);

private string? _viewContract;

/// <summary>
Expand All @@ -66,21 +75,7 @@ public ViewModelViewHost()
{
_viewContract = x.Contract;
if (x.ViewModel is null)
{
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
var view = (viewLocator.ResolveView(x.ViewModel, x.Contract) ?? viewLocator.ResolveView(x.ViewModel)) ?? throw new Exception($"Couldn't find view for '{x.ViewModel}'.");
if (view is not View castView)
{
throw new Exception($"View '{view.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
}
view.ViewModel = x.ViewModel;
Content = castView;
ResolveViewForViewModel(x.ViewModel, x.Contract);
})
});
}
Expand Down Expand Up @@ -124,8 +119,53 @@ public View DefaultContent
set => ViewContractObservable = Observable.Return(value);
}

/// <summary>
/// Gets or sets a value indicating whether should bypass the default contract fallback behavior.
/// </summary>
public bool ContractFallbackByPass
{
get => (bool)GetValue(ContractFallbackByPassProperty);
set => SetValue(ContractFallbackByPassProperty, value);
}

/// <summary>
/// Gets or sets the override for the view locator to use when resolving the view. If unspecified, <see cref="ViewLocator.Current"/> will be used.
/// </summary>
public IViewLocator? ViewLocator { get; set; }

/// <summary>
/// resolve view for view model with respect to contract.
/// </summary>
/// <param name="viewModel">ViewModel.</param>
/// <param name="contract">contract used by ViewLocator.</param>
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract)
{
if (viewModel is null)
{
Content = DefaultContent;
return;
}

var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;

var viewInstance = viewLocator.ResolveView(viewModel, contract);
if (viewInstance is null && !ContractFallbackByPass)
{
viewInstance = viewLocator.ResolveView(viewModel);
}

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

if (viewInstance is not View castView)
{
throw new Exception($"View '{viewInstance.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
}

viewInstance.ViewModel = viewModel;

Content = castView;
}
}
Expand Up @@ -118,15 +118,18 @@ namespace ReactiveUI
}
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
{
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
public static readonly System.Windows.DependencyProperty ViewModelProperty;
public ViewModelViewHost() { }
public bool ContractFallbackByPass { get; set; }
public object DefaultContent { get; set; }
public string? ViewContract { get; set; }
public System.IObservable<string?> ViewContractObservable { get; set; }
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
public object? ViewModel { get; set; }
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
}
}
namespace ReactiveUI.Wpf
Expand All @@ -136,4 +139,4 @@ namespace ReactiveUI.Wpf
public Registrations() { }
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
}
}
}
Expand Up @@ -118,15 +118,18 @@ namespace ReactiveUI
}
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
{
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
public static readonly System.Windows.DependencyProperty ViewModelProperty;
public ViewModelViewHost() { }
public bool ContractFallbackByPass { get; set; }
public object DefaultContent { get; set; }
public string? ViewContract { get; set; }
public System.IObservable<string?> ViewContractObservable { get; set; }
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
public object? ViewModel { get; set; }
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
}
}
namespace ReactiveUI.Wpf
Expand Down
Expand Up @@ -118,15 +118,18 @@ namespace ReactiveUI
}
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
{
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
public static readonly System.Windows.DependencyProperty ViewModelProperty;
public ViewModelViewHost() { }
public bool ContractFallbackByPass { get; set; }
public object DefaultContent { get; set; }
public string? ViewContract { get; set; }
public System.IObservable<string?> ViewContractObservable { get; set; }
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
public object? ViewModel { get; set; }
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
}
}
namespace ReactiveUI.Wpf
Expand All @@ -136,4 +139,4 @@ namespace ReactiveUI.Wpf
public Registrations() { }
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
}
}
}
Expand Up @@ -116,15 +116,18 @@ namespace ReactiveUI
}
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
{
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
public static readonly System.Windows.DependencyProperty ViewModelProperty;
public ViewModelViewHost() { }
public bool ContractFallbackByPass { get; set; }
public object DefaultContent { get; set; }
public string? ViewContract { get; set; }
public System.IObservable<string?> ViewContractObservable { get; set; }
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
public object? ViewModel { get; set; }
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
}
}
namespace ReactiveUI.Wpf
Expand All @@ -134,4 +137,4 @@ namespace ReactiveUI.Wpf
public Registrations() { }
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
}
}
}
@@ -0,0 +1,85 @@
// Copyright (c) 2023 .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.Windows;
using System.Windows.Controls;

namespace ReactiveUI.Tests.Wpf;

public static class FakeViewWithContract
{
internal const string ContractA = "ContractA";
internal const string ContractB = "ContractB";

public class MyViewModel : ReactiveObject
{
}

/// <summary>
/// Used as the default view with no contracted.
/// </summary>
public class View0 : UserControl, IViewFor<MyViewModel>
{

// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(MyViewModel), typeof(View0), new PropertyMetadata(null));

/// <summary>
/// Gets or sets the ViewModel.
/// </summary>
public MyViewModel? ViewModel
{
get { return (MyViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}

object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel?)value; }
}

/// <summary>
/// the view with ContractA.
/// </summary>
public class ViewA : UserControl, IViewFor<MyViewModel>
{

// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(MyViewModel), typeof(ViewA), new PropertyMetadata(null));

/// <summary>
/// Gets or sets the ViewModel.
/// </summary>
public MyViewModel? ViewModel
{
get { return (MyViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}

object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel?)value; }
}

/// <summary>
/// the view as ContractB.
/// </summary>
public class ViewB : UserControl, IViewFor<MyViewModel>
{

// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register("ViewModel", typeof(MyViewModel), typeof(ViewB), new PropertyMetadata(null));

/// <summary>
/// Gets or sets the ViewModel.
/// </summary>
public MyViewModel? ViewModel
{
get { return (MyViewModel)GetValue(ViewModelProperty); }
set { SetValue(ViewModelProperty, value); }
}

object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel?)value; }
}
}

0 comments on commit 632b33d

Please sign in to comment.