Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Rxui5 validation #225

Closed
wants to merge 11 commits into from

3 participants

@wendazhou
Owner

Took a stab at implemeting a validation framework for ReactiveUI as per discussed in #217

In essence, it implements two mechanisms:

  • On one hand, we have IBindingErrorProvider, that provides a stream of errors for a given object. An implementation exist for both IDataErrorInfo and INotifyDataErrorInfo, and more can be added (similarly to the extensibility of the binding mechanism).
  • On the other hand, we have the IBindingDisplayProvider, that displays the current error on the view. An implementation is provided for WPF that basically hacks System.ComponentModel.Validation to display the error. In the future, we should look to move towards a more general system for all the xaml frameworks. This is also extensible and pluggable.

The validation framework then takes care of wiring these two components up, so that everything is handled correctly (I hope). The syntax looks as follows:

public class MyView : Window, IViewFor<ViewModel>
{
  public MyView()
  {
    InitializeComponent();

    this.Bind(ViewModel,  x => x.SomeProperty, v => v.MyTextBox.Text);
    this.DisplayValidationFor(x => x.MyTextBox.Text) // enables validation
  }
}

and a sample can be found at https://github.com/wendazhou/ReactiveUI/tree/rxui5-validation-sample

It compiles on most platforms (could not test WP8), but will not work on any except WPF as there is no implementation of IBindingDisplayProvider on both.

Also, the code makes it very clear moving to portable libraries #217 will be very beneficial, and figuring out a way to sort out the RxApp and introducing some sort of dependency injection would be really great (as per #219).

There is one known issue for the moment: in order to be able to find all the bindings on a given view, I register a IPropertyBindingHook that keeps track of all the bindings ever created... Not only does this probably leak, it is not very efficient, and hard to configure (we need it on by default so that it registers all bindings, but then if the validation is not used, it consumes memory purposelessly).

So, in conclusion, I'd like to gather some feedback on the design, and adress the following:

  • Provide implementations of IBindingDisplayProvider on all platforms
  • Figure out how to disable the registration hook if user is not using the framework
  • Test the whole thing, but will be easier with better DI
  • reduce the huge mess of #if by using portable libraries and better DI

@jlaanstra I saw that you rebuilt ReactiveValidatedObject for for INotifyErrorInfo, do you have any input on that?

In any case, thanks a lot @xpaulbettsx for the great work!

@jlaanstra
Owner

I'll check this out and see if we can somehow combine our work to get at something awesome. Hopefully this week or in the weekend.

@wendazhou
Owner

I don't know if you did anything more that what you pushed, but if not, it should just work when you merge, as I have implemented an error provider for INotifyDataErrorInfo.

@paulcbetts
Owner

This is totally killer. @wendazhou, I've added you to the Owners team on ReactiveUI, you should move this directly to a branch on reactiveui/reactiveui. Here's how to do the Git Magic™:

git remote add upstream https://github.com/reactiveui/ReactiveUI.git
git push -u upstream rxui5-validation
@paulcbetts
Owner

It compiles on most platforms (could not test WP8), but will not work on any except WPF as there is no implementation of IBindingDisplayProvider on both.

I'm okay with that - this is similar to the UserError framework, where we don't provide the UX (though having a default UX for WPF is good)

this.DisplayValidationFor(x => x.MyTextBox.Text) // enables validation

I like this syntax, and I also like that it's in the View code-behind. Would it be better if you enabled it via the ViewModel instead?

this.DisplayValidationFor(x => x.SomeProperty); // enables validation

Also, the code makes it very clear moving to portable libraries #217 will be very beneficial, and figuring out a way to sort out the RxApp and introducing some sort of dependency injection would be really great (as per #219).

Definitely - right now anything that uses Service Location in tests randomly fails because the state of Service Location is constantly being mutated.

@wendazhou
Owner

I like this syntax, and I also like that it's in the View code-behind. Would it be better if you enabled it via the ViewModel instead?

This is actually a very interesting idea, we could handshake with ReactiveObject and have it store all the active bindings instead of using the global hook I have right now. It just seemed more natural at the time for it to be on the view, but thinking right now, it is a totally symmetric (in terms of view vs view-model) feature. I'll take a look in a few hours.

Concerning the UX, it shouldn't be hard to provide some default adorner system for all Xaml-based platforms, I just need to understand how adorners actually work.

@wendazhou wendazhou referenced this pull request
Closed

Rxui5 validation #227

0 of 2 tasks complete
@wendazhou
Owner

Closing this thread, now tracked in #227.

@wendazhou wendazhou closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
65 ReactiveUI.Xaml/ManualValidationHelper.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+
+namespace ReactiveUI.Xaml
+{
+ /// <summary>
+ /// Provides helpers to manually toggle validation errors.
+ /// </summary>
+ static class ManualValidation
+ {
+ // this dummy attached property is used as a source
+ // of the binding.
+ static readonly DependencyProperty dummyProperty =
+ DependencyProperty.RegisterAttached("DummyProperty", typeof(object), typeof(ManualValidation), new PropertyMetadata(null));
+
+ // this class implements a dummy validation rule without behaviour.
+ private class DummyValidationRule : ValidationRule
+ {
+ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ /// <summary>
+ /// Manually mark a validation error on the given framework element.
+ /// </summary>
+ /// <param name="element">The instance of <see cref="FrameworkElement"/> on which to mark the validation error.</param>
+ /// <param name="errorContent">An object representing the content of the error.</param>
+ public static void MarkInvalid(FrameworkElement element, object errorContent)
+ {
+ // create a dummy binding. Conveniently, we bind to the tag of the FrameworkElement,
+ // so as to minimise the potential interaction with other code.
+ var binding = new Binding("Tag") { Source = element, Mode = BindingMode.OneWayToSource };
+
+ // set the binding on to our dummy property.
+ BindingOperations.SetBinding(element, dummyProperty, binding);
+
+ // we now get the live binding expression.
+ var bindingExpression = element.GetBindingExpression(dummyProperty);
+
+ // create a dummy binding error, with the specified error content.
+ var validationError = new ValidationError(new DummyValidationRule(), binding, errorContent, null);
+
+ // and manually set the validation error on the binding.
+ Validation.MarkInvalid(bindingExpression, validationError);
+ }
+
+ /// <summary>
+ /// Clears all manually assigned errors on the given <paramref name="element"/>.
+ /// </summary>
+ /// <param name="element">The instance of <see cref="FrameworkElement"/> on which to clear the validation.</param>
+ public static void ClearValidation(FrameworkElement element)
+ {
+ // to clear an error, we simply remove all bindings to our dummy property.
+ BindingOperations.ClearBinding(element, dummyProperty);
+ }
+
+ }
+}
View
2  ReactiveUI.Xaml/ReactiveUI.Xaml.csproj
@@ -134,6 +134,7 @@
<Compile Include="DependencyObjectObservableForProperty.cs" />
<Compile Include="Errors.cs" />
<Compile Include="Interfaces.cs" />
+ <Compile Include="ManualValidationHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReactiveAsyncCommand.cs" />
<Compile Include="ReactiveCommand.cs" />
@@ -142,6 +143,7 @@
<Compile Include="TransitioningContentControl.cs" />
<Compile Include="WaitForDispatcherScheduler.cs" />
<Compile Include="XamlDefaultPropertyBinding.cs" />
+ <Compile Include="XamlValidationDisplayProvider.cs" />
</ItemGroup>
<ItemGroup>
<Page Include="Themes\Generic.xaml">
View
2  ReactiveUI.Xaml/ReactiveUI.Xaml_Net45.csproj
@@ -134,8 +134,10 @@
<Compile Include="CommandBinding.cs" />
<Compile Include="CreatesCommandBinding.cs" />
<Compile Include="DependencyObjectObservableForProperty.cs" />
+ <Compile Include="XamlValidationDisplayProvider.cs" />
<Compile Include="Errors.cs" />
<Compile Include="Interfaces.cs" />
+ <Compile Include="ManualValidationHelper.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReactiveAsyncCommand.cs" />
<Compile Include="ReactiveCommand.cs" />
View
4 ReactiveUI.Xaml/ServiceLocationRegistration.cs
@@ -27,6 +27,10 @@ public void Register()
RxApp.Register(typeof (CreatesCommandBindingViaCommandParameter), typeof(ICreatesCommandBinding));
RxApp.Register(typeof (CreatesCommandBindingViaEvent), typeof(ICreatesCommandBinding));
RxApp.Register(typeof (BooleanToVisibilityTypeConverter), typeof (IBindingTypeConverter));
+
+#if !WINRT && !SILVERLIGHT
+ RxApp.Register(typeof (XamlValidationDisplayProvider), typeof (IBindingDisplayProvider));
+#endif
#endif
#if WINRT
View
82 ReactiveUI.Xaml/XamlValidationDisplayProvider.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Text;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+
+namespace ReactiveUI.Xaml
+{
+ class XamlValidationDisplayProvider : IBindingDisplayProvider, IEnableLogger
+ {
+ static int lastFrameworkIndex(BindingInfo binding, string[] propNames)
+ {
+ var types = Reflection.GetTypesForPropChain(binding.Target.GetType(), propNames);
+
+ var lastFramework =
+ types.Select((x, i) => new KeyValuePair<int, Type>(i, x))
+ .LastOrDefault(x => typeof (FrameworkElement).IsAssignableFrom(x.Value));
+
+
+ if (lastFramework.Value == null) {
+ // default value
+ return -1;
+ }
+
+ var lastFrameworkIndex = lastFramework.Key;
+ return lastFrameworkIndex;
+ }
+
+ public int GetAffinityForBinding(BindingInfo binding)
+ {
+ var propNames = binding.TargetPath.ToArray();
+
+ var lastFrameworkIndex = XamlValidationDisplayProvider.lastFrameworkIndex(binding, propNames);
+
+ if (lastFrameworkIndex == -1) {
+ // no framework element, can't bind.
+ return 0;
+ }
+
+ if (lastFrameworkIndex == propNames.Length -1) {
+ return 15; // it is the one before last, best case.
+ }
+ else {
+ return 10; // we can still handle that.
+ }
+ }
+
+
+ public void SetBindingError(BindingInfo binding, IEnumerable<object> errors)
+ {
+ var lastFrameworkIndex = XamlValidationDisplayProvider.lastFrameworkIndex(binding,
+ binding.TargetPath.ToArray());
+
+ FrameworkElement element;
+
+ var frameworkElementPropertyPath = binding.TargetPath.Take(lastFrameworkIndex + 1).ToArray();
+ Reflection.TryGetValueForPropertyChain(out element, binding.Target,
+ frameworkElementPropertyPath);
+
+ if (element == null) {
+ this.Log()
+ .Info("Attempted to set error on null FrameworkElement, property path: {0}",
+ string.Join(".", frameworkElementPropertyPath));
+ }
+
+ var error = errors.FirstOrDefault(x => x != null);
+
+ if (error != null) {
+ ManualValidation.MarkInvalid(element, error);
+ }
+ else {
+ ManualValidation.ClearValidation(element);
+ }
+ }
+ }
+}
View
189 ReactiveUI/BindingErrorProviders.cs
@@ -0,0 +1,189 @@
+using System;
+using System.ComponentModel;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+
+namespace ReactiveUI
+{
+#if NET_45 || SILVERLIGHT5
+ /// <summary>
+ /// This class provides an implementation of <see cref="IBindingErrorProvider"/>
+ /// for view models implementing the <see cref="INotifyDataErrorInfo"/> interface.
+ /// </summary>
+ class NotifyDataErrorInfoBindingProvider : IBindingErrorProvider
+ {
+ static int maxNotifyDataErrorInfoIndex(BindingInfo binding, string[] propNames)
+ {
+ IObservedChange<object, object>[] values;
+
+ Reflection.TryGetAllValuesForPropertyChain(out values, binding.Source, propNames);
+
+ var maxNotifyDataErrorInfo = -1;
+
+ if (binding.Source is INotifyDataErrorInfo) {
+ maxNotifyDataErrorInfo = 0;
+ }
+
+ for (int i = 0; i < values.Length; i++) {
+ if (values[i] == null || values[i].Value == null) {
+ break;
+ }
+
+ if (values[i].Value is INotifyDataErrorInfo) {
+ maxNotifyDataErrorInfo = i + 1;
+ }
+ }
+ return maxNotifyDataErrorInfo;
+ }
+
+ public int GetAffinityForBinding(BindingInfo binding)
+ {
+ var propNames = binding.SourcePath.ToArray();
+ var maxNotifyDataErrorInfo = maxNotifyDataErrorInfoIndex(binding, propNames);
+
+ if (maxNotifyDataErrorInfo == -1) {
+ // no NotifyDataErrorInfo, we cannot bind.
+ return 0;
+ }
+
+ if (maxNotifyDataErrorInfo == propNames.Length - 1) {
+ // bind more tightly if it is the one before last;
+ return 25;
+ }
+ else {
+ return 20;
+ }
+ }
+
+ public IObservable<IDataError> GetErrorsForBinding(BindingInfo binding)
+ {
+ var propNames = binding.SourcePath.ToArray();
+ var index = maxNotifyDataErrorInfoIndex(binding, propNames);
+
+ var validationPropertyName = propNames.Skip(index).FirstOrDefault();
+
+ IObservable<INotifyDataErrorInfo> source;
+
+ if (propNames.Length > 1) {
+ source =
+ binding.Source.SubscribeToExpressionChain<object, INotifyDataErrorInfo>(propNames.Take(index),
+ skipInitial: false).Value();
+ }
+ else {
+ source = Observable.Return(binding.Source as INotifyDataErrorInfo);
+ }
+
+ return
+ source
+ .SelectMany(x =>
+ Observable.Return( // return one initial value to reset the errors
+ new EventPattern<DataErrorsChangedEventArgs>(x,
+ new DataErrorsChangedEventArgs(x.HasErrors ? validationPropertyName : null)))
+ .Concat(x == null // no errors on a null object
+ ? Observable.Empty<EventPattern<DataErrorsChangedEventArgs>>()
+ : Observable.FromEventPattern<DataErrorsChangedEventArgs>(
+ h => x.ErrorsChanged += h,
+ h => x.ErrorsChanged -= h)))
+ .Where(
+ x => x.EventArgs.PropertyName == null || x.EventArgs.PropertyName == validationPropertyName)
+ .Select(x =>
+ new DataError
+ {
+ Errors =
+ ((INotifyDataErrorInfo) x.Sender).GetErrors(x.EventArgs.PropertyName)
+ .Cast<object>(),
+ PropertyName = x.EventArgs.PropertyName,
+ Sender = x.Sender
+ });
+ }
+ }
+#endif
+
+#if !WINRT
+ class DataErrorInfoBindingProvider : IBindingErrorProvider
+ {
+ static int maxDataErrorInfoIndex(BindingInfo binding, string[] propNames)
+ {
+ IObservedChange<object, object>[] values;
+
+ Reflection.TryGetAllValuesForPropertyChain(out values, binding.Source, propNames);
+
+ var maxDataErrorInfoIndex = -1;
+
+ if (binding.Source is IDataErrorInfo) {
+ maxDataErrorInfoIndex = 0;
+ }
+
+ for (int i = 0; i < values.Length; i++) {
+ if (values[i] == null || values[i].Value == null) {
+ break;
+ }
+
+ if (values[i].Value is IDataErrorInfo) {
+ maxDataErrorInfoIndex = i + 1;
+ }
+ }
+ return maxDataErrorInfoIndex;
+ }
+
+ public int GetAffinityForBinding(BindingInfo binding)
+ {
+ var propNames = binding.SourcePath.ToArray();
+ var errorInfoIndex = maxDataErrorInfoIndex(binding, propNames);
+
+ if (errorInfoIndex == -1) {
+ // no NotifyDataErrorInfo, we cannot bind.
+ return 0;
+ }
+
+ if (errorInfoIndex == propNames.Length - 1) {
+ // bind more tightly if it is the one before last;
+ return 15;
+ }
+ else {
+ return 10;
+ }
+ }
+
+ public IObservable<IDataError> GetErrorsForBinding(BindingInfo binding)
+ {
+ var propNames = binding.SourcePath.ToArray();
+ var index = maxDataErrorInfoIndex(binding, propNames);
+
+ var validationPropertyName = propNames.Skip(index).FirstOrDefault();
+
+ IObservable<IDataErrorInfo> source;
+
+ if (propNames.Length > 1) {
+ source =
+ binding.Source.SubscribeToExpressionChain<object, IDataErrorInfo>(propNames.Take(index),
+ skipInitial: false).Value();
+ }
+ else {
+ source = Observable.Return(binding.Source as IDataErrorInfo);
+ }
+
+ return
+ source
+ .SelectMany(x => SubscribeToDataErrors(x, validationPropertyName));
+ }
+
+ IObservable<IDataError> SubscribeToDataErrors(IDataErrorInfo instance, string property)
+ {
+ return instance
+ .ObservableForProperty(new[] {property}, skipInitial: false)
+ .Select(x =>
+ {
+ var instanceError = x.Sender[property];
+
+ var errors = string.IsNullOrEmpty(instanceError)
+ ? Enumerable.Empty<object>()
+ : new object[] {instanceError};
+
+ return (IDataError)new DataError {Errors = errors, PropertyName = property, Sender = x.Sender};
+ });
+ }
+ }
+#endif
+}
View
128 ReactiveUI/BindingTrackerBindingHook.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive.Subjects;
+using System.Reflection;
+using System.Text;
+
+namespace ReactiveUI
+{
+ public class BindingInfo
+ {
+ readonly ReadOnlyCollection<string> sourcePath;
+ readonly ReadOnlyCollection<string> targetPath;
+ readonly object source;
+ readonly object target;
+
+ public BindingInfo(object source, object target, IEnumerable<string> sourcePath, IEnumerable<string> targetPath)
+ {
+ this.sourcePath = new ReadOnlyCollection<string>(sourcePath.ToList());
+ this.targetPath = new ReadOnlyCollection<string>(targetPath.ToList());
+
+ this.source = source;
+ this.target = target;
+ }
+
+ public IEnumerable<string> SourcePath
+ {
+ get { return sourcePath; }
+ }
+
+ public IEnumerable<string> TargetPath
+ {
+ get { return targetPath; }
+ }
+
+ public object Source
+ {
+ get { return source; }
+ }
+
+ public object Target
+ {
+ get { return target; }
+ }
+ }
+
+ /// <summary>
+ /// This class implements a method of tracking all the bindings that are set on a given object.
+ /// </summary>
+ internal class BindingTrackerBindingHook : IPropertyBindingHook, IBindingRegistry, IEnableLogger
+ {
+ static readonly Dictionary<object, ReplaySubject<BindingInfo>> allBindings = new Dictionary<object, ReplaySubject<BindingInfo>>();
+ static bool monitor = true;
+
+ public bool ExecuteHook(object source, object target,
+ Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
+ Func<IObservedChange<object, object>[]> getCurrentViewProperties,
+ BindingDirection direction)
+ {
+ if (!Monitor) {
+ // this is not active, don't do anything.
+ return true;
+ }
+
+ var sourcePath = getCurrentViewModelProperties().Select(x => x.PropertyName);
+ var targetPath = getCurrentViewProperties().Select(x => x.PropertyName);
+
+ var bindingInfo = new BindingInfo(source, target, sourcePath, targetPath);
+
+ lock (allBindings) {
+ ReplaySubject<BindingInfo> bindings;
+
+ if (!allBindings.TryGetValue(target, out bindings)) {
+ bindings = new ReplaySubject<BindingInfo>();
+ }
+
+ bindings.OnNext(bindingInfo);
+
+ allBindings[target] = bindings;
+ }
+
+ return true;
+ }
+
+ public IObservable<BindingInfo> GetBindingForView(object view)
+ {
+ if (!Monitor) {
+ this.Log().Warn("You are tryong to get bindings for a view object, " +
+ "but since monitoring is not enabled, they might be out of date!");
+ }
+
+ lock (allBindings) {
+ ReplaySubject<BindingInfo> bindings;
+
+ if (!allBindings.TryGetValue(view, out bindings)) {
+ bindings = new ReplaySubject<BindingInfo>();
+
+ allBindings.Add(view, bindings);
+ }
+
+ return bindings;
+ }
+ }
+
+ public bool Monitor
+ {
+ get { return monitor; }
+ set { monitor = value; }
+ }
+ }
+
+ public interface IBindingRegistry
+ {
+ /// <summary>
+ /// Gets a collection of all the bindings applied to a given <see cref="view"/>.
+ /// </summary>
+ /// <param name="view">The target object for which to get all the bindings for.</param>
+ /// <returns>An enumerable containing all the bindings applied to the <paramref name="view"/>.</returns>
+ IObservable<BindingInfo> GetBindingForView(object view);
+
+ /// <summary>
+ /// Sets a value indicating whether this instance of <see cref="IBindingRegistry"/>
+ /// should monitor the bindings.
+ /// </summary>
+ bool Monitor { get; set; }
+ }
+}
View
188 ReactiveUI/DisplayValidationMixin.cs
@@ -0,0 +1,188 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reactive.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ReactiveUI
+{
+ /// <summary>
+ /// This interface contains information on the errors of a given entity
+ /// </summary>
+ public interface IDataError
+ {
+ /// <summary>
+ /// The entity described by this instance of <see cref="IDataError"/>.
+ /// </summary>
+ object Sender { get; }
+
+ /// <summary>
+ /// The name of the property described by this instance of <see cref="IDataError"/>.
+ /// </summary>
+ string PropertyName { get; }
+
+ /// <summary>
+ /// A sequence representing the errors on the property described by this instance of <see cref="IDataError"/>.
+ /// If there are no errors, the collection is empty.
+ /// </summary>
+ IEnumerable<object> Errors { get; }
+ }
+
+ class DataError : IDataError
+ {
+ public object Sender { get; set; }
+ public string PropertyName { get; set; }
+ public IEnumerable<object> Errors { get; set; }
+ }
+
+ /// <summary>
+ /// This interface represents an entity that is capable of
+ /// conveying to the user information about an error on a given binding.
+ /// </summary>
+ public interface IBindingDisplayProvider
+ {
+ /// <summary>
+ /// Gets an integer representing the affinity of this instance of <see cref="IBindingDisplayProvider"/>
+ /// for the given <paramref name="binding"/>. A positive value indicates that this instance of
+ /// <see cref="IBindingDisplayProvider"/> can work with the <paramref name="binding"/> to some extent.
+ /// </summary>
+ /// <param name="binding">The instance of <see cref="BindingInfo"/> to display error information for.</param>
+ /// <returns>An integer representing the affinity of the display provider with the binding.</returns>
+ int GetAffinityForBinding(BindingInfo binding);
+
+ /// <summary>
+ /// Sets the given <paramref name="errors"/> on the given <paramref name="binding"/>,
+ /// or clears all errors if <paramref name="errors"/> is empty.
+ /// </summary>
+ /// <param name="binding">The instance of <see cref="BindingInfo"/> to display information for.</param>
+ /// <param name="errors">
+ /// An IEnumerable containing the errors in the binding.
+ /// It may be empty if the binding has no errors, in which case the error display should be reset.
+ /// </param>
+ void SetBindingError(BindingInfo binding, IEnumerable<object> errors);
+ }
+
+ /// <summary>
+ /// This interface represents an entity that is capable of providing
+ /// error information for a given binding.
+ /// </summary>
+ public interface IBindingErrorProvider
+ {
+ /// <summary>
+ /// Gets an integer representing the affinity of this instance of <see cref="IBindingErrorProvider"/>
+ /// for the given <paramref name="binding"/>./ A positive value indicates that this instance of
+ /// <see cref="IBindingErrorProvider"/> can work with the <paramref name="binding"/> to a certain extent.
+ /// </summary>
+ /// <param name="binding">The binding object to test.</param>
+ /// <returns>
+ /// An integer representing the affinity of this instance of <see cref="IBindingErrorProvider"/>
+ /// with the given <paramref name="binding"/>
+ /// </returns>
+ int GetAffinityForBinding(BindingInfo binding);
+
+ IObservable<IDataError> GetErrorsForBinding(BindingInfo binding);
+ }
+
+ public static class DisplayValidationMixin
+ {
+ class ValidationObserver : IObserver<IDataError>, IEnableLogger
+ {
+ readonly BindingInfo binding;
+ readonly List<IBindingDisplayProvider> displayProviders;
+
+ IBindingDisplayProvider lastProvider;
+
+ public ValidationObserver(BindingInfo binding, List<IBindingDisplayProvider> displayProviders)
+ {
+ this.binding = binding;
+ this.displayProviders = displayProviders;
+ }
+
+ public void OnNext(IDataError value)
+ {
+ var provider =
+ displayProviders.OrderByDescending(p => p.GetAffinityForBinding(binding)).FirstOrDefault();
+
+ if (provider == null) {
+ this.Log().Warn("No display provider for binding {0}", binding);
+ return;
+ }
+
+ if (lastProvider != null && provider != lastProvider) {
+ lastProvider.SetBindingError(binding, Enumerable.Empty<object>());
+ }
+
+ lastProvider = provider;
+
+ provider.SetBindingError(binding, value.Errors);
+ }
+
+ public void OnError(Exception error)
+ {
+ throw error;
+ }
+
+ public void OnCompleted()
+ {
+ if (lastProvider != null) {
+ lastProvider.SetBindingError(binding, Enumerable.Empty<object>());
+ }
+
+ lastProvider = null;
+ }
+ }
+
+ static readonly IBindingRegistry registry;
+ static readonly List<IBindingErrorProvider> bindingErrorProviders;
+ static readonly List<IBindingDisplayProvider> bindingDisplayProviders;
+
+ static DisplayValidationMixin()
+ {
+ registry = RxApp.GetService<IBindingRegistry>();
+ registry.Monitor = true;
+
+ bindingErrorProviders = RxApp.GetAllServices<IBindingErrorProvider>().ToList();
+ bindingDisplayProviders = RxApp.GetAllServices<IBindingDisplayProvider>().ToList();
+ }
+
+ public static void DisplayValidationFor<TView, TProp>(this TView view, Expression<Func<TView, TProp>> property)
+ where TView : IViewFor
+ {
+ var bindings = registry.GetBindingForView(view);
+
+ var propertyNames = Reflection.ExpressionToPropertyNames(property);
+
+ // filter the bindings.
+ var elementBindings = bindings
+ .Where(x =>
+#if WP7
+ {
+ int index = 0;
+ return x.TargetPath.All(pathSegment => propertyNames[index++] == pathSegment);
+ }
+#else
+ Enumerable.Zip(x.TargetPath, propertyNames, EqualityComparer<string>.Default.Equals)
+ .All(_ => _)
+#endif
+ );
+
+ elementBindings
+ .Subscribe(b =>
+ {
+ var errorProvider = bindingErrorProviders
+ .OrderByDescending(x => x.GetAffinityForBinding(b))
+ .FirstOrDefault();
+
+ if (errorProvider == null) {
+ LogHost.Default.Info("No BindingErrorProvider for binding {0}", b);
+ return;
+ }
+
+ errorProvider.GetErrorsForBinding(b)
+ .Subscribe(new ValidationObserver(b, bindingDisplayProviders));
+ });
+ }
+ }
+}
View
21 ReactiveUI/Interfaces.cs
@@ -436,8 +436,29 @@ public enum BindingDirection
AsyncOneWay,
}
+ /// <summary>
+ /// This interface allows objects to hook into the binding system,
+ /// and take action before the binding between a source and a target is established.
+ ///
+ /// It can optionally prevent the binding from actually enabling itself.
+ /// </summary>
public interface IPropertyBindingHook
{
+ /// <summary>
+ /// Executes the hook when the binding is being set up.
+ /// </summary>
+ /// <param name="source">The source object, usually the view model.</param>
+ /// <param name="target">The target object, usually the view.</param>
+ /// <param name="getCurrentViewModelProperties">
+ /// A function, that when invoked, returns the current values of the property chain
+ /// bound to on the <paramref name="source"/> object.
+ /// </param>
+ /// <param name="getCurrentViewProperties">
+ /// A function, that when invoked, returns the current values of the property chain
+ /// bound to on the <paramref name="target"/> object.
+ /// </param>
+ /// <param name="direction">An enumeration indicating the direction of the binding.</param>
+ /// <returns>True if the binding is allowed to succeed; otherwise false to prevent the binding from activating.</returns>
bool ExecuteHook(object source, object target, Func<IObservedChange<object, object>[]> getCurrentViewModelProperties, Func<IObservedChange<object, object>[]> getCurrentViewProperties, BindingDirection direction);
}
View
2  ReactiveUI/PropertyBinding.cs
@@ -824,7 +824,7 @@ public class PropertyBinderImplementation : IPropertyBinderImplementation
/// <returns>
/// An instance of <see cref="IDisposable"/> that, when disposed,
/// disconnects the binding.
- /// </returns
+ /// </returns>
public IDisposable Bind<TViewModel, TView, TVMProp, TVProp, TDontCare>(
TViewModel viewModel,
TView view,
View
3  ReactiveUI/ReactiveUI.csproj
@@ -124,9 +124,12 @@
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="BindingErrorProviders.cs" />
+ <Compile Include="BindingTrackerBindingHook.cs" />
<Compile Include="BindingTypeConverters.cs" />
<Compile Include="CompatMixins.cs" />
<Compile Include="DefaultPropertyBinding.cs" />
+ <Compile Include="DisplayValidationMixin.cs" />
<Compile Include="INPCObservableForProperty.cs" />
<Compile Include="Interfaces.cs">
<SubType>Code</SubType>
View
3  ReactiveUI/ReactiveUI_Net45.csproj
@@ -119,10 +119,13 @@
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="BindingErrorProviders.cs" />
+ <Compile Include="BindingTrackerBindingHook.cs" />
<Compile Include="BindingTypeConverters.cs" />
<Compile Include="CollectionDebugView.cs" />
<Compile Include="CompatMixins.cs" />
<Compile Include="DefaultPropertyBinding.cs" />
+ <Compile Include="DisplayValidationMixin.cs" />
<Compile Include="INPCObservableForProperty.cs" />
<Compile Include="Interfaces.cs">
<SubType>Code</SubType>
View
3  ReactiveUI/ReactiveUI_SL5.csproj
@@ -103,11 +103,14 @@
<Reference Include="System.Windows.Browser" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="BindingErrorProviders.cs" />
+ <Compile Include="BindingTrackerBindingHook.cs" />
<Compile Include="BindingTypeConverters.cs" />
<Compile Include="CollectionDebugView.cs" />
<Compile Include="CompatMixins.cs" />
<Compile Include="ContractStubs.cs" />
<Compile Include="DefaultPropertyBinding.cs" />
+ <Compile Include="DisplayValidationMixin.cs" />
<Compile Include="INPCObservableForProperty.cs" />
<Compile Include="Interfaces.cs" />
<Compile Include="IRNPCObservableForProperty.cs" />
View
3  ReactiveUI/ReactiveUI_WP7.csproj
@@ -112,11 +112,14 @@
<Reference Include="System.Net" />
</ItemGroup>
<ItemGroup>
+ <Compile Include="BindingErrorProviders.cs" />
+ <Compile Include="BindingTrackerBindingHook.cs" />
<Compile Include="BindingTypeConverters.cs" />
<Compile Include="CollectionDebugView.cs" />
<Compile Include="CompatMixins.cs" />
<Compile Include="ContractStubs.cs" />
<Compile Include="DefaultPropertyBinding.cs" />
+ <Compile Include="DisplayValidationMixin.cs" />
<Compile Include="INPCObservableForProperty.cs" />
<Compile Include="Interfaces.cs" />
<Compile Include="IRNPCObservableForProperty.cs" />
View
3  ReactiveUI/ReactiveUI_WinRT.csproj
@@ -36,11 +36,14 @@
<DocumentationFile>bin\Release\WinRT45\ReactiveUI_WinRT.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
+ <Compile Include="BindingErrorProviders.cs" />
+ <Compile Include="BindingTrackerBindingHook.cs" />
<Compile Include="BindingTypeConverters.cs" />
<Compile Include="CollectionDebugView.cs" />
<Compile Include="CompatMixins.cs" />
<Compile Include="ContractStubs.cs" />
<Compile Include="DefaultPropertyBinding.cs" />
+ <Compile Include="DisplayValidationMixin.cs" />
<Compile Include="INPCObservableForProperty.cs" />
<Compile Include="Interfaces.cs" />
<Compile Include="IRNPCObservableForProperty.cs" />
View
9 ReactiveUI/Reflection.cs
@@ -262,6 +262,15 @@ public static bool TryGetValueForPropertyChain<TValue>(out TValue changeValue, o
return true;
}
+ /// <summary>
+ /// Gets a list of all the values set along a property chain.
+ /// For example, given a property chain <c>current.Foo.Bar.Baz</c>
+ /// it attempts to get the value of <c>Foo</c>, <c>Foo.Bar</c> and <c>Foo.Bar.Baz</c>.
+ /// </summary>
+ /// <param name="changeValues">The values of the properties in the property chain.</param>
+ /// <param name="current">The root of the property chain.</param>
+ /// <param name="propNames">An array of <see cref="System.String"/> describing the property chain.</param>
+ /// <returns>True if all the property were successfully accessed; otherwise false (e.g. one property was null).</returns>
public static bool TryGetAllValuesForPropertyChain(out IObservedChange<object, object>[] changeValues, object current, string[] propNames)
{
int currentIndex = 0;
View
11 ReactiveUI/RxApp.cs
@@ -94,6 +94,17 @@ static RxApp()
RxApp.Register(typeof(EqualityTypeConverter), typeof(IBindingTypeConverter));
RxApp.Register(typeof(StringConverter), typeof(IBindingTypeConverter));
+ RxApp.Register(typeof(BindingTrackerBindingHook), typeof(IPropertyBindingHook));
+ RxApp.Register(typeof(BindingTrackerBindingHook), typeof(IBindingRegistry));
+
+#if NET_45 || SILVERLIGHT5
+ RxApp.Register(typeof(NotifyDataErrorInfoBindingProvider), typeof(IBindingErrorProvider));
+#endif
+
+#if !WINRT
+ RxApp.Register(typeof(DataErrorInfoBindingProvider), typeof(IBindingErrorProvider));
+#endif
+
#if !SILVERLIGHT && !WINRT
RxApp.Register(typeof(ComponentModelTypeConverter), typeof(IBindingTypeConverter));
#endif
Something went wrong with that request. Please try again.