Skip to content

Commit 9db0036

Browse files
committed
Add convention-based property binding for common controls
1 parent 296c7a4 commit 9db0036

12 files changed

Lines changed: 1047 additions & 221 deletions

ReactiveUI.Tests/ObservedChangedMixinTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public void BindToSmokeTest()
108108
var input = new ScheduledSubject<string>(sched);
109109
var fixture = new HostTestFixture() {Child = new TestFixture()};
110110

111-
input.OneWayBind(fixture, x => x.Child.IsNotNullString);
111+
input.BindTo(fixture, x => x.Child.IsNotNullString);
112112

113113
Assert.Null(fixture.Child.IsNotNullString);
114114

@@ -129,7 +129,7 @@ public void DisposingDisconnectsTheBindTo()
129129
var input = new ScheduledSubject<string>(sched);
130130
var fixture = new HostTestFixture() {Child = new TestFixture()};
131131

132-
var subscription = input.OneWayBind(fixture, x => x.Child.IsNotNullString);
132+
var subscription = input.BindTo(fixture, x => x.Child.IsNotNullString);
133133

134134
Assert.Null(fixture.Child.IsNotNullString);
135135

@@ -152,7 +152,7 @@ public void BindToIsNotFooledByIntermediateObjectSwitching()
152152
var input = new ScheduledSubject<string>(sched);
153153
var fixture = new HostTestFixture() {Child = new TestFixture()};
154154

155-
var subscription = input.OneWayBind(fixture, x => x.Child.IsNotNullString);
155+
var subscription = input.BindTo(fixture, x => x.Child.IsNotNullString);
156156

157157
Assert.Null(fixture.Child.IsNotNullString);
158158

ReactiveUI.Xaml/ReactiveUI.Xaml.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
<Compile Include="ReactiveCommand.cs" />
128128
<Compile Include="ServiceLocationRegistration.cs" />
129129
<Compile Include="TransitioningContentControl.cs" />
130+
<Compile Include="XamlDefaultPropertyBinding.cs" />
130131
</ItemGroup>
131132
<ItemGroup>
132133
<ProjectReference Include="..\ReactiveUI\ReactiveUI.csproj">

ReactiveUI.Xaml/ServiceLocationRegistration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class ServiceLocationRegistration : IWantsToRegisterStuff
1818
public void Register()
1919
{
2020
RxApp.Register(typeof (DependencyObjectObservableForProperty), typeof (ICreatesObservableForProperty));
21+
RxApp.Register(typeof (XamlDefaultPropertyBinding), typeof (IDefaultPropertyBindingProvider));
2122
}
2223

2324
static bool? inDesignMode;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Windows;
6+
using System.Windows.Controls;
7+
using System.Windows.Controls.Primitives;
8+
using System.Windows.Documents;
9+
10+
namespace ReactiveUI.Xaml
11+
{
12+
public class XamlDefaultPropertyBinding : IDefaultPropertyBindingProvider
13+
{
14+
public Tuple<string, int> GetPropertyForControl(object control)
15+
{
16+
// NB: These are intentionally arranged in priority order from most
17+
// specific to least specific.
18+
var items = new[] {
19+
new { Type = typeof(RichTextBox), Property = "Document" },
20+
new { Type = typeof(Slider), Property = "Value" },
21+
new { Type = typeof(Expander), Property = "IsExpanded" },
22+
new { Type = typeof(ToggleButton), Property = "IsChecked" },
23+
new { Type = typeof(TextBox), Property = "Text" },
24+
new { Type = typeof(TextBlock), Property = "Text" },
25+
new { Type = typeof(ProgressBar), Property = "Value" },
26+
new { Type = typeof(ItemsControl), Property = "ItemsSource" },
27+
new { Type = typeof(Image), Property = "Source" },
28+
new { Type = typeof(FrameworkContentElement), Property = "Content" },
29+
new { Type = typeof(FrameworkElement), Property = "Visibility" },
30+
};
31+
32+
var type = control.GetType();
33+
var kvp = items.FirstOrDefault(x => x.Type.IsAssignableFrom(type));
34+
35+
return kvp != null ? Tuple.Create(kvp.Property, 5) : null;
36+
}
37+
}
38+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
6+
namespace ReactiveUI
7+
{
8+
public class DefaultPropertyBinding
9+
{
10+
public static string GetPropertyForControl(object control)
11+
{
12+
return RxApp.GetAllServices<IDefaultPropertyBindingProvider>()
13+
.Select(x => x.GetPropertyForControl(control))
14+
.Where(x => x != null)
15+
.OrderByDescending(x => x.Item2)
16+
.Select(x => x.Item1)
17+
.FirstOrDefault();
18+
}
19+
}
20+
}

ReactiveUI/Interfaces.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ public interface IViewForViewModel<T> : IViewForViewModel
335335
T ViewModel { get; set; }
336336
}
337337

338+
public interface IDefaultPropertyBindingProvider
339+
{
340+
Tuple<string, int> GetPropertyForControl(object control);
341+
}
342+
338343
internal interface IWantsToRegisterStuff
339344
{
340345
void Register();

ReactiveUI/PropertyBinding.cs

Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,44 @@ public static IDisposable Bind<TViewModel, TView, TProp>(
2626
Expression<Func<TViewModel, TProp>> vmProperty,
2727
Expression<Func<TView, TProp>> viewProperty)
2828
where TViewModel : class
29-
where TView : class, IViewForViewModel<TViewModel>
29+
where TView : IViewForViewModel<TViewModel>
3030
{
3131
return binderImplementation.Bind(viewModel, view, vmProperty, viewProperty);
3232
}
3333

34+
public static IDisposable Bind<TViewModel, TView, TProp>(
35+
this TView view,
36+
TViewModel viewModel,
37+
Expression<Func<TViewModel, TProp>> vmProperty)
38+
where TViewModel : class
39+
where TView : IViewForViewModel<TViewModel>
40+
{
41+
return binderImplementation.Bind(viewModel, view, vmProperty, null);
42+
}
43+
3444
public static IDisposable OneWayBind<TViewModel, TView, TProp>(
3545
this TView view,
3646
TViewModel viewModel,
3747
Expression<Func<TViewModel, TProp>> vmProperty,
3848
Expression<Func<TView, TProp>> viewProperty,
3949
Func<TProp> fallbackValue = null)
4050
where TViewModel : class
41-
where TView : class, IViewForViewModel<TViewModel>
51+
where TView : IViewForViewModel<TViewModel>
4252
{
4353
return binderImplementation.OneWayBind(viewModel, view, vmProperty, viewProperty, fallbackValue);
4454
}
4555

56+
public static IDisposable OneWayBind<TViewModel, TView, TProp>(
57+
this TView view,
58+
TViewModel viewModel,
59+
Expression<Func<TViewModel, TProp>> vmProperty,
60+
Func<TProp> fallbackValue = null)
61+
where TViewModel : class
62+
where TView : IViewForViewModel<TViewModel>
63+
{
64+
return binderImplementation.OneWayBind(viewModel, view, vmProperty, null, fallbackValue);
65+
}
66+
4667
public static IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
4768
this TView view,
4869
TViewModel viewModel,
@@ -51,11 +72,23 @@ public static IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
5172
Func<TProp, TOut> selector,
5273
Func<TOut> fallbackValue = null)
5374
where TViewModel : class
54-
where TView : class, IViewForViewModel<TViewModel>
75+
where TView : IViewForViewModel<TViewModel>
5576
{
5677
return binderImplementation.OneWayBind(viewModel, view, vmProperty, viewProperty, selector, fallbackValue);
5778
}
5879

80+
public static IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
81+
this TView view,
82+
TViewModel viewModel,
83+
Expression<Func<TViewModel, TProp>> vmProperty,
84+
Func<TProp, TOut> selector,
85+
Func<TOut> fallbackValue = null)
86+
where TViewModel : class
87+
where TView : IViewForViewModel<TViewModel>
88+
{
89+
return binderImplementation.OneWayBind(viewModel, view, vmProperty, null, selector, fallbackValue);
90+
}
91+
5992
public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
6093
this TView view,
6194
TViewModel viewModel,
@@ -64,10 +97,22 @@ public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
6497
Func<TProp, IObservable<TOut>> selector,
6598
Func<TOut> fallbackValue = null)
6699
where TViewModel : class
67-
where TView : class, IViewForViewModel<TViewModel>
100+
where TView : IViewForViewModel<TViewModel>
68101
{
69102
return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, viewProperty, selector, fallbackValue);
70103
}
104+
105+
public static IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
106+
this TView view,
107+
TViewModel viewModel,
108+
Expression<Func<TViewModel, TProp>> vmProperty,
109+
Func<TProp, IObservable<TOut>> selector,
110+
Func<TOut> fallbackValue = null)
111+
where TViewModel : class
112+
where TView : IViewForViewModel<TViewModel>
113+
{
114+
return binderImplementation.AsyncOneWayBind(viewModel, view, vmProperty, null, selector, fallbackValue);
115+
}
71116
}
72117

73118
public interface IPropertyBinderImplementation
@@ -78,7 +123,7 @@ IDisposable Bind<TViewModel, TView, TProp>(
78123
Expression<Func<TViewModel, TProp>> vmProperty,
79124
Expression<Func<TView, TProp>> viewProperty)
80125
where TViewModel : class
81-
where TView : class, IViewForViewModel<TViewModel>;
126+
where TView : IViewForViewModel<TViewModel>;
82127

83128
IDisposable OneWayBind<TViewModel, TView, TProp>(
84129
TViewModel viewModel,
@@ -87,7 +132,7 @@ IDisposable OneWayBind<TViewModel, TView, TProp>(
87132
Expression<Func<TView, TProp>> viewProperty,
88133
Func<TProp> fallbackValue = null)
89134
where TViewModel : class
90-
where TView : class, IViewForViewModel<TViewModel>;
135+
where TView : IViewForViewModel<TViewModel>;
91136

92137
IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
93138
TViewModel viewModel,
@@ -97,7 +142,7 @@ IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
97142
Func<TProp, TOut> selector,
98143
Func<TOut> fallbackValue = null)
99144
where TViewModel : class
100-
where TView : class, IViewForViewModel<TViewModel>;
145+
where TView : IViewForViewModel<TViewModel>;
101146

102147
IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
103148
TViewModel viewModel,
@@ -107,7 +152,7 @@ IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
107152
Func<TProp, IObservable<TOut>> selector,
108153
Func<TOut> fallbackValue = null)
109154
where TViewModel : class
110-
where TView : class, IViewForViewModel<TViewModel>;
155+
where TView : IViewForViewModel<TViewModel>;
111156
}
112157

113158
class PropertyBinderImplementation : IPropertyBinderImplementation
@@ -118,23 +163,31 @@ public IDisposable Bind<TViewModel, TView, TProp>(
118163
Expression<Func<TViewModel, TProp>> vmProperty,
119164
Expression<Func<TView, TProp>> viewProperty)
120165
where TViewModel : class
121-
where TView : class, IViewForViewModel<TViewModel>
166+
where TView : IViewForViewModel<TViewModel>
122167
{
123168
var ret = new CompositeDisposable();
169+
170+
var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
171+
string[] viewPropChain;
172+
173+
if (viewProperty == null) {
174+
viewPropChain = getDefaultViewPropChain(view, vmPropChain);
175+
} else {
176+
viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
177+
}
178+
124179
var somethingChanged = Observable.Merge(
125180
Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty),
126-
view.WhenAny(viewProperty, x => x.Value)
181+
view.WhenAnyDynamic(viewPropChain, x => (TProp)x.Value)
127182
).Multicast(new Subject<TProp>());
128183

129-
var vmPropChain = Reflection.ExpressionToPropertyNames(vmProperty);
130184
ret.Add(somethingChanged.Where(x => {
131185
TProp result;
132186
if (!Reflection.TryGetValueForPropertyChain(out result, view.ViewModel, vmPropChain))
133187
return false;
134188
return EqualityComparer<TProp>.Default.Equals(result, x) != true;
135189
}).Subscribe(x => Reflection.SetValueToPropertyChain(view.ViewModel, vmPropChain, x, false)));
136190

137-
var viewPropChain = Reflection.ExpressionToPropertyNames(viewProperty);
138191
ret.Add(somethingChanged.Where(x => {
139192
TProp result;
140193
if (!Reflection.TryGetValueForPropertyChain(out result, view, viewPropChain))
@@ -160,11 +213,17 @@ public IDisposable OneWayBind<TViewModel, TView, TProp>(
160213
Expression<Func<TView, TProp>> viewProperty,
161214
Func<TProp> fallbackValue = null)
162215
where TViewModel : class
163-
where TView : class, IViewForViewModel<TViewModel>
216+
where TView : IViewForViewModel<TViewModel>
164217
{
165-
218+
if (viewProperty == null) {
219+
var viewPropChain = getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
220+
221+
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
222+
.Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
223+
}
224+
166225
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
167-
.OneWayBind(view, viewProperty, fallbackValue);
226+
.BindTo(view, viewProperty, fallbackValue);
168227
}
169228

170229
public IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
@@ -175,11 +234,19 @@ public IDisposable OneWayBind<TViewModel, TView, TProp, TOut>(
175234
Func<TProp, TOut> selector,
176235
Func<TOut> fallbackValue = null)
177236
where TViewModel : class
178-
where TView : class, IViewForViewModel<TViewModel>
237+
where TView : IViewForViewModel<TViewModel>
179238
{
239+
if (viewProperty == null) {
240+
var viewPropChain = getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
241+
242+
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
243+
.Select(selector)
244+
.Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
245+
}
246+
180247
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
181248
.Select(selector)
182-
.OneWayBind(view, viewProperty, fallbackValue);
249+
.BindTo(view, viewProperty, fallbackValue);
183250
}
184251

185252
public IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
@@ -190,11 +257,36 @@ public IDisposable AsyncOneWayBind<TViewModel, TView, TProp, TOut>(
190257
Func<TProp, IObservable<TOut>> selector,
191258
Func<TOut> fallbackValue = null)
192259
where TViewModel : class
193-
where TView : class, IViewForViewModel<TViewModel>
260+
where TView : IViewForViewModel<TViewModel>
194261
{
262+
if (viewProperty == null) {
263+
var viewPropChain = getDefaultViewPropChain(view, Reflection.ExpressionToPropertyNames(vmProperty));
264+
265+
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
266+
.SelectMany(selector)
267+
.Subscribe(x => Reflection.SetValueToPropertyChain(view, viewPropChain, x, false));
268+
}
269+
195270
return Reflection.ViewModelWhenAnyValue(viewModel, view, vmProperty)
196271
.SelectMany(selector)
197-
.OneWayBind(view, viewProperty, fallbackValue);
272+
.BindTo(view, viewProperty, fallbackValue);
273+
}
274+
275+
static string[] getDefaultViewPropChain(object view, string[] vmPropChain)
276+
{
277+
var vmPropertyName = vmPropChain.First();
278+
var control = Reflection.GetValueFetcherForProperty(view.GetType(), vmPropertyName)(view);
279+
280+
if (control == null) {
281+
throw new Exception(String.Format("Tried to bind to control but it was null: {0}.{1}", view.GetType().FullName,
282+
vmPropertyName));
283+
}
284+
285+
var defaultProperty = DefaultPropertyBinding.GetPropertyForControl(control);
286+
if (defaultProperty == null) {
287+
throw new Exception(String.Format("Couldn't find a default property for type {0}", control.GetType()));
288+
}
289+
return new[] {vmPropertyName, defaultProperty};
198290
}
199291
}
200292

@@ -210,12 +302,11 @@ public static class ObservableBindingMixins
210302
/// <param name="property">An expression representing the target
211303
/// property to set. This can be a child property (i.e. x.Foo.Bar.Baz).</param>
212304
/// <returns>An object that when disposed, disconnects the binding.</returns>
213-
public static IDisposable OneWayBind<TTarget, TValue>(
305+
public static IDisposable BindTo<TTarget, TValue>(
214306
this IObservable<TValue> This,
215307
TTarget target,
216308
Expression<Func<TTarget, TValue>> property,
217309
Func<TValue> fallbackValue = null)
218-
where TTarget : class
219310
{
220311
var pn = Reflection.ExpressionToPropertyNames(property);
221312

0 commit comments

Comments
 (0)