Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,14 @@ public void SmokeTestWinformControls()
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -139,4 +145,4 @@ namespace ReactiveUI.Wpf
public Registrations() { }
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -139,4 +145,4 @@ namespace ReactiveUI.Wpf
public Registrations() { }
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -137,4 +143,4 @@ namespace ReactiveUI.Wpf
public Registrations() { }
public void Register(System.Action<System.Func<object>, System.Type> registerFunction) { }
}
}
}
48 changes: 48 additions & 0 deletions src/ReactiveUI.Wpf/Binding/ValidationBindingMixins.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
167 changes: 167 additions & 0 deletions src/ReactiveUI.Wpf/Binding/ValidationBindingWpf.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}