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 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.Property2, x => x.Property2.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.BooleanProperty, x => x.BooleanProperty.Checked),
|
||||
});
|
||||
]);
|
||||
|
||||
vm.Property1 = "FOOO";
|
||||
Assert.Equal(vm.Property1, view.Property1.Text);
|
||||
|
|
|
@ -116,6 +116,12 @@ namespace ReactiveUI
|
|||
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 static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
||||
|
@ -139,4 +145,4 @@ namespace ReactiveUI.Wpf
|
|||
public Registrations() { }
|
||||
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -116,6 +116,12 @@ namespace ReactiveUI
|
|||
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 static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
||||
|
@ -139,4 +145,4 @@ namespace ReactiveUI.Wpf
|
|||
public Registrations() { }
|
||||
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -114,6 +114,12 @@ namespace ReactiveUI
|
|||
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 static readonly System.Windows.DependencyProperty ContractFallbackByPassProperty;
|
||||
|
@ -137,4 +143,4 @@ namespace ReactiveUI.Wpf
|
|||
public Registrations() { }
|
||||
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче