Skip to content

Commit 632b33d

Browse files
fix: Allow Developers Bypass the Default Fallback Behavior (resolves #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 <[email protected]>
1 parent e28392d commit 632b33d

11 files changed

+370
-46
lines changed

src/ReactiveUI.Maui/Common/ViewModelViewHost.cs

+26-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableL
3232
public static readonly DependencyProperty ViewContractObservableProperty =
3333
DependencyProperty.Register(nameof(ViewContractObservable), typeof(IObservable<string>), typeof(ViewModelViewHost), new PropertyMetadata(Observable<string>.Default));
3434

35+
/// <summary>
36+
/// The ContractFallbackByPass dependency property.
37+
/// </summary>
38+
public static readonly DependencyProperty ContractFallbackByPassProperty =
39+
DependencyProperty.Register("ContractFallbackByPass", typeof(bool), typeof(ViewModelViewHost), new PropertyMetadata(false));
40+
3541
private string? _viewContract;
3642

3743
/// <summary>
@@ -119,12 +125,26 @@ public string? ViewContract
119125
set => ViewContractObservable = Observable.Return(value);
120126
}
121127

128+
/// <summary>
129+
/// Gets or sets a value indicating whether should bypass the default contract fallback behavior.
130+
/// </summary>
131+
public bool ContractFallbackByPass
132+
{
133+
get => (bool)GetValue(ContractFallbackByPassProperty);
134+
set => SetValue(ContractFallbackByPassProperty, value);
135+
}
136+
122137
/// <summary>
123138
/// Gets or sets the view locator.
124139
/// </summary>
125140
public IViewLocator? ViewLocator { get; set; }
126141

127-
private void ResolveViewForViewModel(object? viewModel, string? contract)
142+
/// <summary>
143+
/// resolve view for view model with respect to contract.
144+
/// </summary>
145+
/// <param name="viewModel">ViewModel.</param>
146+
/// <param name="contract">contract used by ViewLocator.</param>
147+
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract)
128148
{
129149
if (viewModel is null)
130150
{
@@ -133,7 +153,11 @@ private void ResolveViewForViewModel(object? viewModel, string? contract)
133153
}
134154

135155
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
136-
var viewInstance = viewLocator.ResolveView(viewModel, contract) ?? viewLocator.ResolveView(viewModel);
156+
var viewInstance = viewLocator.ResolveView(viewModel, contract);
157+
if (viewInstance is null && !ContractFallbackByPass)
158+
{
159+
viewInstance = viewLocator.ResolveView(viewModel);
160+
}
137161

138162
if (viewInstance is null)
139163
{

src/ReactiveUI.Maui/ViewModelViewHost.cs

+55-15
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ public class ViewModelViewHost : ContentView, IViewFor
4040
typeof(ViewModelViewHost),
4141
Observable<string>.Never);
4242

43+
/// <summary>
44+
/// The ContractFallbackByPass dependency property.
45+
/// </summary>
46+
public static readonly BindableProperty ContractFallbackByPassProperty = BindableProperty.Create(
47+
nameof(ContractFallbackByPass),
48+
typeof(bool),
49+
typeof(ViewModelViewHost),
50+
false);
51+
4352
private string? _viewContract;
4453

4554
/// <summary>
@@ -66,21 +75,7 @@ public ViewModelViewHost()
6675
{
6776
_viewContract = x.Contract;
6877

69-
if (x.ViewModel is null)
70-
{
71-
Content = DefaultContent;
72-
return;
73-
}
74-
75-
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
76-
var view = (viewLocator.ResolveView(x.ViewModel, x.Contract) ?? viewLocator.ResolveView(x.ViewModel)) ?? throw new Exception($"Couldn't find view for '{x.ViewModel}'.");
77-
if (view is not View castView)
78-
{
79-
throw new Exception($"View '{view.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
80-
}
81-
82-
view.ViewModel = x.ViewModel;
83-
Content = castView;
78+
ResolveViewForViewModel(x.ViewModel, x.Contract);
8479
})
8580
});
8681
}
@@ -124,8 +119,53 @@ public string? ViewContract
124119
set => ViewContractObservable = Observable.Return(value);
125120
}
126121

122+
/// <summary>
123+
/// Gets or sets a value indicating whether should bypass the default contract fallback behavior.
124+
/// </summary>
125+
public bool ContractFallbackByPass
126+
{
127+
get => (bool)GetValue(ContractFallbackByPassProperty);
128+
set => SetValue(ContractFallbackByPassProperty, value);
129+
}
130+
127131
/// <summary>
128132
/// Gets or sets the override for the view locator to use when resolving the view. If unspecified, <see cref="ViewLocator.Current"/> will be used.
129133
/// </summary>
130134
public IViewLocator? ViewLocator { get; set; }
135+
136+
/// <summary>
137+
/// resolve view for view model with respect to contract.
138+
/// </summary>
139+
/// <param name="viewModel">ViewModel.</param>
140+
/// <param name="contract">contract used by ViewLocator.</param>
141+
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract)
142+
{
143+
if (viewModel is null)
144+
{
145+
Content = DefaultContent;
146+
return;
147+
}
148+
149+
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current;
150+
151+
var viewInstance = viewLocator.ResolveView(viewModel, contract);
152+
if (viewInstance is null && !ContractFallbackByPass)
153+
{
154+
viewInstance = viewLocator.ResolveView(viewModel);
155+
}
156+
157+
if (viewInstance is null)
158+
{
159+
throw new Exception($"Couldn't find view for '{viewModel}'.");
160+
}
161+
162+
if (viewInstance is not View castView)
163+
{
164+
throw new Exception($"View '{viewInstance.GetType().FullName}' is not a subclass of '{typeof(View).FullName}'.");
165+
}
166+
167+
viewInstance.ViewModel = viewModel;
168+
169+
Content = castView;
170+
}
131171
}

src/ReactiveUI.Tests/Platforms/wpf/API/WpfApiApprovalTests.Wpf.DotNet6_0.verified.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,18 @@ namespace ReactiveUI
118118
}
119119
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
120120
{
121+
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
121122
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
122123
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
123124
public static readonly System.Windows.DependencyProperty ViewModelProperty;
124125
public ViewModelViewHost() { }
126+
public bool ContractFallbackByPass { get; set; }
125127
public object DefaultContent { get; set; }
126128
public string? ViewContract { get; set; }
127129
public System.IObservable<string?> ViewContractObservable { get; set; }
128130
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
129131
public object? ViewModel { get; set; }
132+
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
130133
}
131134
}
132135
namespace ReactiveUI.Wpf
@@ -136,4 +139,4 @@ namespace ReactiveUI.Wpf
136139
public Registrations() { }
137140
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
138141
}
139-
}
142+
}

src/ReactiveUI.Tests/Platforms/wpf/API/WpfApiApprovalTests.Wpf.DotNet7_0.verified.txt

+3
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,18 @@ namespace ReactiveUI
118118
}
119119
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
120120
{
121+
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
121122
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
122123
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
123124
public static readonly System.Windows.DependencyProperty ViewModelProperty;
124125
public ViewModelViewHost() { }
126+
public bool ContractFallbackByPass { get; set; }
125127
public object DefaultContent { get; set; }
126128
public string? ViewContract { get; set; }
127129
public System.IObservable<string?> ViewContractObservable { get; set; }
128130
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
129131
public object? ViewModel { get; set; }
132+
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
130133
}
131134
}
132135
namespace ReactiveUI.Wpf

src/ReactiveUI.Tests/Platforms/wpf/API/WpfApiApprovalTests.Wpf.DotNet8_0.verified.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,18 @@ namespace ReactiveUI
118118
}
119119
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
120120
{
121+
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
121122
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
122123
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
123124
public static readonly System.Windows.DependencyProperty ViewModelProperty;
124125
public ViewModelViewHost() { }
126+
public bool ContractFallbackByPass { get; set; }
125127
public object DefaultContent { get; set; }
126128
public string? ViewContract { get; set; }
127129
public System.IObservable<string?> ViewContractObservable { get; set; }
128130
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
129131
public object? ViewModel { get; set; }
132+
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
130133
}
131134
}
132135
namespace ReactiveUI.Wpf
@@ -136,4 +139,4 @@ namespace ReactiveUI.Wpf
136139
public Registrations() { }
137140
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
138141
}
139-
}
142+
}

src/ReactiveUI.Tests/Platforms/wpf/API/WpfApiApprovalTests.Wpf.Net4_7.verified.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,18 @@ namespace ReactiveUI
116116
}
117117
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
118118
{
119+
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
119120
public static readonly System.Windows.DependencyProperty DefaultContentProperty;
120121
public static readonly System.Windows.DependencyProperty ViewContractObservableProperty;
121122
public static readonly System.Windows.DependencyProperty ViewModelProperty;
122123
public ViewModelViewHost() { }
124+
public bool ContractFallbackByPass { get; set; }
123125
public object DefaultContent { get; set; }
124126
public string? ViewContract { get; set; }
125127
public System.IObservable<string?> ViewContractObservable { get; set; }
126128
public ReactiveUI.IViewLocator? ViewLocator { get; set; }
127129
public object? ViewModel { get; set; }
130+
protected virtual void ResolveViewForViewModel(object? viewModel, string? contract) { }
128131
}
129132
}
130133
namespace ReactiveUI.Wpf
@@ -134,4 +137,4 @@ namespace ReactiveUI.Wpf
134137
public Registrations() { }
135138
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
136139
}
137-
}
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using System.Windows;
7+
using System.Windows.Controls;
8+
9+
namespace ReactiveUI.Tests.Wpf;
10+
11+
public static class FakeViewWithContract
12+
{
13+
internal const string ContractA = "ContractA";
14+
internal const string ContractB = "ContractB";
15+
16+
public class MyViewModel : ReactiveObject
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Used as the default view with no contracted.
22+
/// </summary>
23+
public class View0 : UserControl, IViewFor<MyViewModel>
24+
{
25+
26+
// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
27+
public static readonly DependencyProperty ViewModelProperty =
28+
DependencyProperty.Register("ViewModel", typeof(MyViewModel), typeof(View0), new PropertyMetadata(null));
29+
30+
/// <summary>
31+
/// Gets or sets the ViewModel.
32+
/// </summary>
33+
public MyViewModel? ViewModel
34+
{
35+
get { return (MyViewModel)GetValue(ViewModelProperty); }
36+
set { SetValue(ViewModelProperty, value); }
37+
}
38+
39+
object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel?)value; }
40+
}
41+
42+
/// <summary>
43+
/// the view with ContractA.
44+
/// </summary>
45+
public class ViewA : UserControl, IViewFor<MyViewModel>
46+
{
47+
48+
// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
49+
public static readonly DependencyProperty ViewModelProperty =
50+
DependencyProperty.Register("ViewModel", typeof(MyViewModel), typeof(ViewA), new PropertyMetadata(null));
51+
52+
/// <summary>
53+
/// Gets or sets the ViewModel.
54+
/// </summary>
55+
public MyViewModel? ViewModel
56+
{
57+
get { return (MyViewModel)GetValue(ViewModelProperty); }
58+
set { SetValue(ViewModelProperty, value); }
59+
}
60+
61+
object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel?)value; }
62+
}
63+
64+
/// <summary>
65+
/// the view as ContractB.
66+
/// </summary>
67+
public class ViewB : UserControl, IViewFor<MyViewModel>
68+
{
69+
70+
// Using a DependencyProperty as the backing store for ViewModel. This enables animation, styling, binding, etc...
71+
public static readonly DependencyProperty ViewModelProperty =
72+
DependencyProperty.Register("ViewModel", typeof(MyViewModel), typeof(ViewB), new PropertyMetadata(null));
73+
74+
/// <summary>
75+
/// Gets or sets the ViewModel.
76+
/// </summary>
77+
public MyViewModel? ViewModel
78+
{
79+
get { return (MyViewModel)GetValue(ViewModelProperty); }
80+
set { SetValue(ViewModelProperty, value); }
81+
}
82+
83+
object? IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MyViewModel?)value; }
84+
}
85+
}

0 commit comments

Comments
 (0)