{{ message }}
Repository navigation
Expand file tree
/
Copy pathViewModelViewHost.cs
More file actions
333 lines (280 loc) · 12.2 KB
/
Copy pathViewModelViewHost.cs
File metadata and controls
333 lines (280 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
// Copyright (c) 2009-2026 .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.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using ReactiveUI.Internal;
#if REACTIVE_SHIM
using static ReactiveUI.Binding.Reactive.ViewLocator;
#else
using static ReactiveUI.Binding.ViewLocator;
#endif
#if REACTIVE_SHIM
namespace ReactiveUI.Reactive.Winforms;
#else
namespace ReactiveUI.Winforms;
#endif
/// <summary>A view model control host which will find and host the View for a ViewModel.</summary>
/// <remarks>
/// The host asks the view locator for the view by the view model's run-time type, without building any type at run
/// time. The default locator checks the view lookup the source generator writes, then the views the app added with
/// <c>Map</c>. A view registered only with the service locator needs <see cref="ViewModelControlHostUnsafe"/>, which
/// also asks the service locator for <see cref="IViewFor{T}"/> closed over the view model's run-time type.
/// </remarks>
[DefaultProperty("ViewModel")]
[DebuggerDisplay("{CurrentView}, {DefaultContent}")]
public partial class ViewModelControlHost : UserControl, IReactiveObject, IViewFor
{
/// <summary>Holds the subscriptions created during setup so they can be disposed together.</summary>
private readonly MultipleDisposable _disposables = [];
/// <summary>Asks a view locator for the view of a view model under a contract.</summary>
private readonly Func<IViewLocator, object, string?, IViewFor?> _resolveView;
/// <summary>Backing field for the currently displayed content.</summary>
private object? _content;
/// <summary>Backing field indicating whether resolved views are cached and reused.</summary>
private bool _cacheViews;
/// <summary>The <see cref="INotifyPropertyChanging.PropertyChanging"/> handlers; subscribing enables the classic event.</summary>
private PropertyChangingEventHandler? _propertyChanging;
/// <summary>The <see cref="INotifyPropertyChanged.PropertyChanged"/> handlers; subscribing enables the classic event.</summary>
private PropertyChangedEventHandler? _propertyChanged;
/// <summary>Initializes a new instance of the <see cref="ViewModelControlHost"/> class.</summary>
public ViewModelControlHost()
: this(ViewHostResolution.ResolveViewWithoutReflection)
{
}
/// <summary>Initializes a new instance of the <see cref="ViewModelControlHost"/> class with its view lookup.</summary>
/// <param name="resolveView">Asks a view locator for the view of a view model under a contract.</param>
[SuppressMessage(
"Design",
"SST2403:'this' escapes before construction finishes",
Justification = "'this' is passed to BindingSink; the WinForms designer-mandated constructor wires this control's bindings; the single-threaded control is never published elsewhere.")]
private protected ViewModelControlHost(Func<IViewLocator, object, string?, IViewFor?> resolveView)
{
_resolveView = resolveView;
InitializeComponent();
_cacheViews = DefaultCacheViewsEnabled;
_disposables.Add(new BindingSink(this));
}
/// <inheritdoc/>
public event PropertyChangingEventHandler? PropertyChanging
{
add
{
this.SubscribePropertyChangingEvents();
_propertyChanging += value;
}
remove => _propertyChanging -= value;
}
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged
{
add
{
this.SubscribePropertyChangedEvents();
_propertyChanged += value;
}
remove => _propertyChanged -= value;
}
/// <summary>Gets or sets a value indicating whether [default cache views enabled].</summary>
public static bool DefaultCacheViewsEnabled { get; set; }
/// <summary>Gets the current view.</summary>
public Control? CurrentView => _content as Control;
/// <summary>Gets or sets the default content.</summary>
[Category("ReactiveUI")]
[Description("The default control when no viewmodel is specified")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public Control? DefaultContent
{
get => field;
set => this.RaiseAndSetIfChanged(ref field, value);
}
/// <summary>Gets or sets the view contract observable.</summary>
/// <value>
/// The view contract observable.
/// </value>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IObservable<string>? ViewContractObservable
{
get;
set => this.RaiseAndSetIfChanged(ref field, value);
}
/// <summary>Gets or sets the view locator.</summary>
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public IViewLocator? ViewLocator { get; set; }
/// <inheritdoc/>
[Category("ReactiveUI")]
[Description("The viewmodel to host.")]
[Bindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public object? ViewModel
{
get;
set => this.RaiseAndSetIfChanged(ref field, value);
}
/// <summary>Gets the content.</summary>
[Category("ReactiveUI")]
[Description("The Current View")]
[Bindable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public object? Content
{
get => _content;
protected set => this.RaiseAndSetIfChanged(ref _content, value);
}
/// <summary>Gets or sets a value indicating whether to cache views.</summary>
[Category("ReactiveUI")]
[Description("Cache Views")]
[Bindable(true)]
[DefaultValue(true)]
public bool CacheViews
{
get => _cacheViews;
set => this.RaiseAndSetIfChanged(ref _cacheViews, value);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => _propertyChanging?.Invoke(this, args);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => _propertyChanged?.Invoke(this, args);
/// <summary>Clean up any resources being used.</summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && components is not null)
{
components.Dispose();
_disposables.Dispose();
}
base.Dispose(disposing);
}
/// <summary>Replaces the currently visible control with the supplied view, docked to fill the host.</summary>
/// <param name="view">The control to display.</param>
private void SwapHostedView(Control view)
{
// change the view in the ui
SuspendLayout();
// clear out existing visible control view
foreach (Control? c in Controls)
{
c?.Dispose();
Controls.Remove(c);
}
view.Dock = DockStyle.Fill;
Controls.Add(view);
ResumeLayout();
}
/// <summary>Resolves and displays the content for the supplied view model and contract.</summary>
/// <param name="viewModel">The current view model, or null.</param>
/// <param name="contract">The view contract.</param>
private void UpdateContentForViewModel(object? viewModel, string contract)
{
// set content to default when viewmodel is null
if (viewModel is null)
{
if (DefaultContent is not null)
{
Content = DefaultContent;
}
return;
}
if (TryReuseCachedView(viewModel))
{
return;
}
var view = _resolveView(ViewLocator ?? GetCurrent(), viewModel, contract);
if (view is null)
{
return;
}
view.ViewModel = viewModel;
Content = view;
}
/// <summary>Reuses the cached view for the supplied view model when caching is enabled and the types match.</summary>
/// <param name="viewModel">The current view model.</param>
/// <returns>true if the cached view was reused; otherwise, false.</returns>
private bool TryReuseCachedView(object viewModel)
{
if (!CacheViews)
{
return false;
}
// when caching views, check the current viewmodel and type
var c = _content as IViewFor;
if (c?.ViewModel is null || c.ViewModel.GetType() != viewModel.GetType())
{
return false;
}
// setting the viewmodel allows the view to update its bindings
c.ViewModel = viewModel;
return true;
}
/// <summary>
/// A single fused sink that owns every subscription the host needs and drives view presentation directly. It
/// folds the four sources — <see cref="Content"/>, <see cref="DefaultContent"/>, <see cref="ViewModel"/> and the
/// <see cref="ViewContractObservable"/> — into one object, performing the view-model/contract <c>CombineLatest</c>
/// inline instead of allocating a separate combine operator and per-source observers.
/// </summary>
private sealed class BindingSink : IDisposable
{
/// <summary>The host whose presentation this sink drives.</summary>
private readonly ViewModelControlHost _host;
/// <summary>The <see cref="Content"/> subscription.</summary>
private readonly IDisposable _contentSubscription;
/// <summary>The <see cref="DefaultContent"/> subscription.</summary>
private readonly IDisposable _defaultContentSubscription;
/// <summary>The view-model/contract combine that drives content resolution.</summary>
private readonly CombineLatestSink<object?, string> _viewModelContract;
/// <summary>Initializes a new instance of the <see cref="BindingSink"/> class and wires every source.</summary>
/// <param name="host">The host to drive.</param>
public BindingSink(ViewModelControlHost host)
{
_host = host;
// Content -> swap the hosted control (the type check replaces WhereNotNull().OfType<Control>()).
_contentSubscription = host.WhenAnyValue(static x => x.Content)
.Subscribe(new DelegateObserver<object?>(OnContentChanged));
// DefaultContent -> show it as the current content once it is set.
_defaultContentSubscription = host.WhenAnyValue(static x => x.DefaultContent)
.Subscribe(new DelegateObserver<Control?>(OnDefaultContentChanged));
host.ViewContractObservable = Signal.Emit(string.Empty);
// ViewModel + ViewContractObservable -> resolve and show the matching view.
_viewModelContract = new(
host.WhenAnyValue(static x => x.ViewModel),
host.WhenAnyObservable(x => x.ViewContractObservable!),
host.UpdateContentForViewModel,
RxState.DefaultExceptionHandler.OnNext);
}
/// <inheritdoc/>
public void Dispose()
{
_contentSubscription.Dispose();
_defaultContentSubscription.Dispose();
_viewModelContract.Dispose();
}
/// <summary>Swaps the hosted view when the content is a control.</summary>
/// <param name="content">The new content.</param>
private void OnContentChanged(object? content)
{
if (content is not Control control)
{
return;
}
_host.SwapHostedView(control);
}
/// <summary>Shows the default content once it is assigned.</summary>
/// <param name="defaultContent">The new default content.</param>
private void OnDefaultContentChanged(Control? defaultContent)
{
if (defaultContent is null)
{
return;
}
_host.Content = _host.DefaultContent;
}
}
}
You can’t perform that action at this time.
