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:
Chris Pulman 2024-08-10 08:23:34 -07:00 коммит произвёл GitHub
Родитель e06abc28a0
Коммит 16cd48a941
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
6 изменённых файлов: 239 добавлений и 6 удалений

Просмотреть файл

@ -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;

Просмотреть файл

@ -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;

Просмотреть файл

@ -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;

Просмотреть файл

@ -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;
}
}
}
}