Skip to content

Commit 16cd48a

Browse files
authored
Feature Add Wpf Validation Binding (#3874)
<!-- 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 **What is the current behavior?** <!-- You can also link to an open issue here. --> No code bindings exist supporting Validation **What is the new behavior?** <!-- If this is a feature change --> Added Wpf Bind which uses ReactiveUI ReactiveProperty with validation as a validation source **What might this PR break?** New Feature **Please check if the PR fulfills these requirements** - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) **Other information**: Example ```c# public partial class MainWindow : ReactiveWindow<MainWindowViewModel> { public MainWindow() { InitializeComponent(); ViewModel = new MainWindowViewModel(); this.WhenActivated(cleanup => { this.BindWithValidation( ViewModel, vm => vm.NoSymbolsTextProperty.Value, view => view.NoSymbolsEntry.Text, .DisposeWith(cleanup); }); } } using System.ComponentModel.DataAnnotations; using ReactiveUI; public class MainWindowViewModel : ReactiveObject { [RegularExpression(@"^[^!@#$%^&*()]*$", ErrorMessage = "Symbols not allowed!")] public ReactiveProperty<string> NoSymbolsTextProperty { get; } public MainWindowViewModel() { NoSymbolsTextProperty = new ReactiveProperty<string>().AddValidation(() => NoSymbolsTextProperty); } } ```
1 parent e06abc2 commit 16cd48a

6 files changed

+239
-6
lines changed

src/ReactiveUI.Tests/Platforms/winforms/DefaultPropertyBindingTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,14 @@ public void SmokeTestWinformControls()
134134
var vm = new FakeWinformViewModel();
135135
var view = new FakeWinformsView { ViewModel = vm };
136136

137-
var disp = new CompositeDisposable(new[]
138-
{
137+
var disp = new CompositeDisposable(
138+
[
139139
view.Bind(vm, x => x.Property1, x => x.Property1.Text),
140140
view.Bind(vm, x => x.Property2, x => x.Property2.Text),
141141
view.Bind(vm, x => x.Property3, x => x.Property3.Text),
142142
view.Bind(vm, x => x.Property4, x => x.Property4.Text),
143143
view.Bind(vm, x => x.BooleanProperty, x => x.BooleanProperty.Checked),
144-
});
144+
]);
145145

146146
vm.Property1 = "FOOO";
147147
Assert.Equal(vm.Property1, view.Property1.Text);

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ namespace ReactiveUI
116116
Bounce = 4,
117117
}
118118
}
119+
public static class ValidationBindingMixins
120+
{
121+
public static ReactiveUI.IReactiveBinding<TView, TType> BindWithValidation<TViewModel, TView, TVProp, TType>(this TView view, TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TType?>> viewModelPropertySelector, System.Linq.Expressions.Expression<System.Func<TView, TVProp>> frameworkElementSelector)
122+
where TViewModel : class
123+
where TView : class, ReactiveUI.IViewFor { }
124+
}
119125
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
120126
{
121127
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
@@ -139,4 +145,4 @@ namespace ReactiveUI.Wpf
139145
public Registrations() { }
140146
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
141147
}
142-
}
148+
}

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ namespace ReactiveUI
116116
Bounce = 4,
117117
}
118118
}
119+
public static class ValidationBindingMixins
120+
{
121+
public static ReactiveUI.IReactiveBinding<TView, TType> BindWithValidation<TViewModel, TView, TVProp, TType>(this TView view, TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TType?>> viewModelPropertySelector, System.Linq.Expressions.Expression<System.Func<TView, TVProp>> frameworkElementSelector)
122+
where TViewModel : class
123+
where TView : class, ReactiveUI.IViewFor { }
124+
}
119125
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
120126
{
121127
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
@@ -139,4 +145,4 @@ namespace ReactiveUI.Wpf
139145
public Registrations() { }
140146
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
141147
}
142-
}
148+
}

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

+7-1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ namespace ReactiveUI
114114
Bounce = 4,
115115
}
116116
}
117+
public static class ValidationBindingMixins
118+
{
119+
public static ReactiveUI.IReactiveBinding<TView, TType> BindWithValidation<TViewModel, TView, TVProp, TType>(this TView view, TViewModel viewModel, System.Linq.Expressions.Expression<System.Func<TViewModel, TType?>> viewModelPropertySelector, System.Linq.Expressions.Expression<System.Func<TView, TVProp>> frameworkElementSelector)
120+
where TViewModel : class
121+
where TView : class, ReactiveUI.IViewFor { }
122+
}
117123
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
118124
{
119125
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
@@ -137,4 +143,4 @@ namespace ReactiveUI.Wpf
137143
public Registrations() { }
138144
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
139145
}
140-
}
146+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) 2024 .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.Linq.Expressions;
7+
using System.Windows;
8+
using ReactiveUI.Wpf.Binding;
9+
10+
namespace ReactiveUI;
11+
12+
/// <summary>
13+
/// ValidationBindingMixins.
14+
/// </summary>
15+
public static class ValidationBindingMixins
16+
{
17+
/// <summary>
18+
/// Binds the validation.
19+
/// </summary>
20+
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
21+
/// <typeparam name="TView">The type of the view.</typeparam>
22+
/// <typeparam name="TVProp">The type of the v property.</typeparam>
23+
/// <typeparam name="TType">The type of the type.</typeparam>
24+
/// <param name="view">The view.</param>
25+
/// <param name="viewModel">The view model.</param>
26+
/// <param name="viewModelPropertySelector">The view model property selector.</param>
27+
/// <param name="frameworkElementSelector">The framework element selector.</param>
28+
/// <returns>
29+
/// An instance of <see cref="IDisposable"/> that, when disposed,
30+
/// disconnects the binding.
31+
/// </returns>
32+
public static IReactiveBinding<TView, TType> BindWithValidation<TViewModel, TView, TVProp, TType>(this TView view, TViewModel viewModel, Expression<Func<TViewModel, TType?>> viewModelPropertySelector, Expression<Func<TView, TVProp>> frameworkElementSelector)
33+
where TView : class, IViewFor
34+
where TViewModel : class
35+
{
36+
if (viewModelPropertySelector == null)
37+
{
38+
throw new ArgumentNullException(nameof(viewModelPropertySelector));
39+
}
40+
41+
if (frameworkElementSelector == null)
42+
{
43+
throw new ArgumentNullException(nameof(frameworkElementSelector));
44+
}
45+
46+
return new ValidationBindingWpf<TView, TViewModel, TVProp, TType>(view, viewModel, viewModelPropertySelector, frameworkElementSelector);
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright (c) 2024 .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.Linq.Expressions;
7+
using System.Text;
8+
using System.Windows;
9+
using System.Windows.Data;
10+
using System.Windows.Markup.Primitives;
11+
using System.Windows.Media;
12+
using DynamicData;
13+
14+
namespace ReactiveUI.Wpf.Binding;
15+
16+
internal class ValidationBindingWpf<TView, TViewModel, TVProp, TVMProp> : IReactiveBinding<TView, TVMProp>
17+
where TView : class, IViewFor
18+
where TViewModel : class
19+
{
20+
private const string DotValue = ".";
21+
private readonly FrameworkElement _control;
22+
private readonly DependencyProperty? _dpPropertyName;
23+
private readonly TViewModel _viewModel;
24+
private readonly string? _vmPropertyName;
25+
private IDisposable? _inner;
26+
27+
public ValidationBindingWpf(
28+
TView view,
29+
TViewModel viewModel,
30+
Expression<Func<TViewModel, TVMProp?>> vmProperty,
31+
Expression<Func<TView, TVProp>> viewProperty)
32+
{
33+
// Get the ViewModel details
34+
_viewModel = viewModel;
35+
ViewModelExpression = Reflection.Rewrite(vmProperty.Body);
36+
var vmet = ViewModelExpression.GetExpressionChain();
37+
var vmFullName = vmet.Select(x => x.GetMemberInfo()?.Name).Aggregate(new StringBuilder(), (sb, x) => sb.Append(x).Append('.')).ToString();
38+
if (vmFullName.EndsWith(DotValue))
39+
{
40+
vmFullName = vmFullName.Substring(0, vmFullName.Length - 1);
41+
}
42+
43+
_vmPropertyName = vmFullName;
44+
45+
// Get the View details
46+
View = view;
47+
ViewExpression = Reflection.Rewrite(viewProperty.Body);
48+
var vet = ViewExpression.GetExpressionChain().ToArray();
49+
var controlName = string.Empty;
50+
var index = vet.IndexOf(vet.Last()!);
51+
if (vet != null && index > 0)
52+
{
53+
controlName = vet[vet.IndexOf(vet.Last()!) - 1]!.GetMemberInfo()?.Name
54+
?? throw new ArgumentException($"Control name not found on {typeof(TView).Name}");
55+
}
56+
57+
_control = FindControlsByName(view as DependencyObject, controlName).FirstOrDefault()!;
58+
var controlDpPropertyName = vet?.Last().GetMemberInfo()?.Name;
59+
_dpPropertyName = GetDependencyProperty(_control, controlDpPropertyName) ?? throw new ArgumentException($"Dependency property not found on {typeof(TVProp).Name}");
60+
61+
var somethingChanged = Reflection.ViewModelWhenAnyValue(viewModel, view, ViewModelExpression).Select(tvm => (TVMProp?)tvm).Merge(
62+
view.WhenAnyDynamic(ViewExpression, x => (TVProp?)x.Value).Select(p => default(TVMProp)));
63+
Changed = somethingChanged;
64+
Direction = BindingDirection.TwoWay;
65+
Bind();
66+
}
67+
68+
public System.Linq.Expressions.Expression ViewModelExpression { get; }
69+
70+
public TView View { get; }
71+
72+
public System.Linq.Expressions.Expression ViewExpression { get; }
73+
74+
public IObservable<TVMProp?> Changed { get; }
75+
76+
public BindingDirection Direction { get; }
77+
78+
public IDisposable Bind()
79+
{
80+
_control.SetBinding(_dpPropertyName, new System.Windows.Data.Binding()
81+
{
82+
Source = _viewModel,
83+
Path = new(_vmPropertyName),
84+
Mode = BindingMode.TwoWay,
85+
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
86+
});
87+
88+
_inner = Disposable.Create(() => BindingOperations.ClearBinding(_control, _dpPropertyName));
89+
90+
return _inner;
91+
}
92+
93+
public void Dispose()
94+
{
95+
_inner?.Dispose();
96+
GC.SuppressFinalize(this);
97+
}
98+
99+
private static IEnumerable<DependencyProperty> EnumerateDependencyProperties(object element)
100+
{
101+
if (element != null)
102+
{
103+
var markupObject = MarkupWriter.GetMarkupObjectFor(element);
104+
if (markupObject != null)
105+
{
106+
foreach (var mp in markupObject.Properties)
107+
{
108+
if (mp.DependencyProperty != null)
109+
{
110+
yield return mp.DependencyProperty;
111+
}
112+
}
113+
}
114+
}
115+
}
116+
117+
private static IEnumerable<DependencyProperty> EnumerateAttachedProperties(object element)
118+
{
119+
if (element != null)
120+
{
121+
var markupObject = MarkupWriter.GetMarkupObjectFor(element);
122+
if (markupObject != null)
123+
{
124+
foreach (var mp in markupObject.Properties)
125+
{
126+
if (mp.IsAttached)
127+
{
128+
yield return mp.DependencyProperty;
129+
}
130+
}
131+
}
132+
}
133+
}
134+
135+
private static DependencyProperty? GetDependencyProperty(object element, string? name) =>
136+
EnumerateDependencyProperties(element).Concat(EnumerateAttachedProperties(element)).FirstOrDefault(x => x.Name == name);
137+
138+
private static IEnumerable<FrameworkElement> FindControlsByName(DependencyObject? parent, string? name)
139+
{
140+
if (parent == null)
141+
{
142+
yield break;
143+
}
144+
145+
if (name == null)
146+
{
147+
yield break;
148+
}
149+
150+
var childCount = VisualTreeHelper.GetChildrenCount(parent);
151+
152+
for (var i = 0; i < childCount; i++)
153+
{
154+
var child = VisualTreeHelper.GetChild(parent, i);
155+
156+
if (child is FrameworkElement element && element.Name == name)
157+
{
158+
yield return element;
159+
}
160+
161+
foreach (var descendant in FindControlsByName(child, name))
162+
{
163+
yield return descendant;
164+
}
165+
}
166+
}
167+
}

0 commit comments

Comments
 (0)