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); } } ```
This commit is contained in:
Родитель
e06abc28a0
Коммит
16cd48a941
|
@ -134,14 +134,14 @@ public class DefaultPropertyBindingTests
|
||||||
var vm = new FakeWinformViewModel();
|
var vm = new FakeWinformViewModel();
|
||||||
var view = new FakeWinformsView { ViewModel = vm };
|
var view = new FakeWinformsView { ViewModel = vm };
|
||||||
|
|
||||||
var disp = new CompositeDisposable(new[]
|
var disp = new CompositeDisposable(
|
||||||
{
|
[
|
||||||
view.Bind(vm, x => x.Property1, x => x.Property1.Text),
|
view.Bind(vm, x => x.Property1, x => x.Property1.Text),
|
||||||
view.Bind(vm, x => x.Property2, x => x.Property2.Text),
|
view.Bind(vm, x => x.Property2, x => x.Property2.Text),
|
||||||
view.Bind(vm, x => x.Property3, x => x.Property3.Text),
|
view.Bind(vm, x => x.Property3, x => x.Property3.Text),
|
||||||
view.Bind(vm, x => x.Property4, x => x.Property4.Text),
|
view.Bind(vm, x => x.Property4, x => x.Property4.Text),
|
||||||
view.Bind(vm, x => x.BooleanProperty, x => x.BooleanProperty.Checked),
|
view.Bind(vm, x => x.BooleanProperty, x => x.BooleanProperty.Checked),
|
||||||
});
|
]);
|
||||||
|
|
||||||
vm.Property1 = "FOOO";
|
vm.Property1 = "FOOO";
|
||||||
Assert.Equal(vm.Property1, view.Property1.Text);
|
Assert.Equal(vm.Property1, view.Property1.Text);
|
||||||
|
|
|
@ -116,6 +116,12 @@ namespace ReactiveUI
|
||||||
Bounce = 4,
|
Bounce = 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static class ValidationBindingMixins
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
where TViewModel : class
|
||||||
|
where TView : class, ReactiveUI.IViewFor { }
|
||||||
|
}
|
||||||
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
|
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
|
||||||
{
|
{
|
||||||
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
||||||
|
|
|
@ -116,6 +116,12 @@ namespace ReactiveUI
|
||||||
Bounce = 4,
|
Bounce = 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static class ValidationBindingMixins
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
where TViewModel : class
|
||||||
|
where TView : class, ReactiveUI.IViewFor { }
|
||||||
|
}
|
||||||
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
|
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
|
||||||
{
|
{
|
||||||
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
||||||
|
|
|
@ -114,6 +114,12 @@ namespace ReactiveUI
|
||||||
Bounce = 4,
|
Bounce = 4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static class ValidationBindingMixins
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
where TViewModel : class
|
||||||
|
where TView : class, ReactiveUI.IViewFor { }
|
||||||
|
}
|
||||||
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
|
public class ViewModelViewHost : ReactiveUI.TransitioningContentControl, ReactiveUI.IActivatableView, ReactiveUI.IViewFor, Splat.IEnableLogger
|
||||||
{
|
{
|
||||||
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
public static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
// Copyright (c) 2024 .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.Linq.Expressions;
|
||||||
|
using System.Windows;
|
||||||
|
using ReactiveUI.Wpf.Binding;
|
||||||
|
|
||||||
|
namespace ReactiveUI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ValidationBindingMixins.
|
||||||
|
/// </summary>
|
||||||
|
public static class ValidationBindingMixins
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Binds the validation.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TViewModel">The type of the view model.</typeparam>
|
||||||
|
/// <typeparam name="TView">The type of the view.</typeparam>
|
||||||
|
/// <typeparam name="TVProp">The type of the v property.</typeparam>
|
||||||
|
/// <typeparam name="TType">The type of the type.</typeparam>
|
||||||
|
/// <param name="view">The view.</param>
|
||||||
|
/// <param name="viewModel">The view model.</param>
|
||||||
|
/// <param name="viewModelPropertySelector">The view model property selector.</param>
|
||||||
|
/// <param name="frameworkElementSelector">The framework element selector.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// An instance of <see cref="IDisposable"/> that, when disposed,
|
||||||
|
/// disconnects the binding.
|
||||||
|
/// </returns>
|
||||||
|
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)
|
||||||
|
where TView : class, IViewFor
|
||||||
|
where TViewModel : class
|
||||||
|
{
|
||||||
|
if (viewModelPropertySelector == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(viewModelPropertySelector));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameworkElementSelector == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(frameworkElementSelector));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValidationBindingWpf<TView, TViewModel, TVProp, TType>(view, viewModel, viewModelPropertySelector, frameworkElementSelector);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright (c) 2024 .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.Linq.Expressions;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Data;
|
||||||
|
using System.Windows.Markup.Primitives;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using DynamicData;
|
||||||
|
|
||||||
|
namespace ReactiveUI.Wpf.Binding;
|
||||||
|
|
||||||
|
internal class ValidationBindingWpf<TView, TViewModel, TVProp, TVMProp> : IReactiveBinding<TView, TVMProp>
|
||||||
|
where TView : class, IViewFor
|
||||||
|
where TViewModel : class
|
||||||
|
{
|
||||||
|
private const string DotValue = ".";
|
||||||
|
private readonly FrameworkElement _control;
|
||||||
|
private readonly DependencyProperty? _dpPropertyName;
|
||||||
|
private readonly TViewModel _viewModel;
|
||||||
|
private readonly string? _vmPropertyName;
|
||||||
|
private IDisposable? _inner;
|
||||||
|
|
||||||
|
public ValidationBindingWpf(
|
||||||
|
TView view,
|
||||||
|
TViewModel viewModel,
|
||||||
|
Expression<Func<TViewModel, TVMProp?>> vmProperty,
|
||||||
|
Expression<Func<TView, TVProp>> viewProperty)
|
||||||
|
{
|
||||||
|
// Get the ViewModel details
|
||||||
|
_viewModel = viewModel;
|
||||||
|
ViewModelExpression = Reflection.Rewrite(vmProperty.Body);
|
||||||
|
var vmet = ViewModelExpression.GetExpressionChain();
|
||||||
|
var vmFullName = vmet.Select(x => x.GetMemberInfo()?.Name).Aggregate(new StringBuilder(), (sb, x) => sb.Append(x).Append('.')).ToString();
|
||||||
|
if (vmFullName.EndsWith(DotValue))
|
||||||
|
{
|
||||||
|
vmFullName = vmFullName.Substring(0, vmFullName.Length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
_vmPropertyName = vmFullName;
|
||||||
|
|
||||||
|
// Get the View details
|
||||||
|
View = view;
|
||||||
|
ViewExpression = Reflection.Rewrite(viewProperty.Body);
|
||||||
|
var vet = ViewExpression.GetExpressionChain().ToArray();
|
||||||
|
var controlName = string.Empty;
|
||||||
|
var index = vet.IndexOf(vet.Last()!);
|
||||||
|
if (vet != null && index > 0)
|
||||||
|
{
|
||||||
|
controlName = vet[vet.IndexOf(vet.Last()!) - 1]!.GetMemberInfo()?.Name
|
||||||
|
?? throw new ArgumentException($"Control name not found on {typeof(TView).Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_control = FindControlsByName(view as DependencyObject, controlName).FirstOrDefault()!;
|
||||||
|
var controlDpPropertyName = vet?.Last().GetMemberInfo()?.Name;
|
||||||
|
_dpPropertyName = GetDependencyProperty(_control, controlDpPropertyName) ?? throw new ArgumentException($"Dependency property not found on {typeof(TVProp).Name}");
|
||||||
|
|
||||||
|
var somethingChanged = Reflection.ViewModelWhenAnyValue(viewModel, view, ViewModelExpression).Select(tvm => (TVMProp?)tvm).Merge(
|
||||||
|
view.WhenAnyDynamic(ViewExpression, x => (TVProp?)x.Value).Select(p => default(TVMProp)));
|
||||||
|
Changed = somethingChanged;
|
||||||
|
Direction = BindingDirection.TwoWay;
|
||||||
|
Bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
public System.Linq.Expressions.Expression ViewModelExpression { get; }
|
||||||
|
|
||||||
|
public TView View { get; }
|
||||||
|
|
||||||
|
public System.Linq.Expressions.Expression ViewExpression { get; }
|
||||||
|
|
||||||
|
public IObservable<TVMProp?> Changed { get; }
|
||||||
|
|
||||||
|
public BindingDirection Direction { get; }
|
||||||
|
|
||||||
|
public IDisposable Bind()
|
||||||
|
{
|
||||||
|
_control.SetBinding(_dpPropertyName, new System.Windows.Data.Binding()
|
||||||
|
{
|
||||||
|
Source = _viewModel,
|
||||||
|
Path = new(_vmPropertyName),
|
||||||
|
Mode = BindingMode.TwoWay,
|
||||||
|
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
_inner = Disposable.Create(() => BindingOperations.ClearBinding(_control, _dpPropertyName));
|
||||||
|
|
||||||
|
return _inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_inner?.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<DependencyProperty> EnumerateDependencyProperties(object element)
|
||||||
|
{
|
||||||
|
if (element != null)
|
||||||
|
{
|
||||||
|
var markupObject = MarkupWriter.GetMarkupObjectFor(element);
|
||||||
|
if (markupObject != null)
|
||||||
|
{
|
||||||
|
foreach (var mp in markupObject.Properties)
|
||||||
|
{
|
||||||
|
if (mp.DependencyProperty != null)
|
||||||
|
{
|
||||||
|
yield return mp.DependencyProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<DependencyProperty> EnumerateAttachedProperties(object element)
|
||||||
|
{
|
||||||
|
if (element != null)
|
||||||
|
{
|
||||||
|
var markupObject = MarkupWriter.GetMarkupObjectFor(element);
|
||||||
|
if (markupObject != null)
|
||||||
|
{
|
||||||
|
foreach (var mp in markupObject.Properties)
|
||||||
|
{
|
||||||
|
if (mp.IsAttached)
|
||||||
|
{
|
||||||
|
yield return mp.DependencyProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DependencyProperty? GetDependencyProperty(object element, string? name) =>
|
||||||
|
EnumerateDependencyProperties(element).Concat(EnumerateAttachedProperties(element)).FirstOrDefault(x => x.Name == name);
|
||||||
|
|
||||||
|
private static IEnumerable<FrameworkElement> FindControlsByName(DependencyObject? parent, string? name)
|
||||||
|
{
|
||||||
|
if (parent == null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name == null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var childCount = VisualTreeHelper.GetChildrenCount(parent);
|
||||||
|
|
||||||
|
for (var i = 0; i < childCount; i++)
|
||||||
|
{
|
||||||
|
var child = VisualTreeHelper.GetChild(parent, i);
|
||||||
|
|
||||||
|
if (child is FrameworkElement element && element.Name == name)
|
||||||
|
{
|
||||||
|
yield return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var descendant in FindControlsByName(child, name))
|
||||||
|
{
|
||||||
|
yield return descendant;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче