Browse Source

Merge pull request #1 from AvaloniaUI/Validation

Implemented Validation
pull/2109/head
sdoroff 8 years ago
committed by GitHub
parent
commit
307b09baf9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1052
      src/Avalonia.DataGrid/DataGrid.cs
  2. 184
      src/Avalonia.DataGrid/DataGridBoundColumn.cs
  3. 12
      src/Avalonia.DataGrid/DataGridColumn.cs
  4. 3
      src/Avalonia.DataGrid/DataGridColumnHeader.cs
  5. 4
      src/Avalonia.DataGrid/DataGridFillerColumn.cs
  6. 2
      src/Avalonia.DataGrid/DataGridRow.cs
  7. 160
      src/Avalonia.DataGrid/Utilities/CellEditBinding.cs
  8. 168
      src/Avalonia.DataGrid/Utilities/ValidationUtil.cs

1052
src/Avalonia.DataGrid/DataGrid.cs

File diff suppressed because it is too large

184
src/Avalonia.DataGrid/DataGridBoundColumn.cs

@ -95,9 +95,11 @@ namespace Avalonia.Controls
}
//TODO Rename
protected sealed override Control GenerateEditingElement(DataGridCell cell, object dataItem)
//TODO Validation
protected sealed override Control GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding editBinding)
{
Control element = GenerateEditingElementDirect(cell, dataItem);
editBinding = null;
if (Binding != null)
//if (Binding != null || !DesignerProperties.IsInDesignTool)
@ -105,7 +107,7 @@ namespace Avalonia.Controls
//var t1 = Binding.Initiate(textBox, BindingTarget, anchor: null, enableDataValidation: true);
//BindingOperations.Apply(textBox, BindingTarget, t1, null);
BindEditingElement(element, BindingTarget, Binding);
editBinding = BindEditingElement(element, BindingTarget, Binding);
//element.Bind(BindingTarget, Binding);
//textBox.SetBinding(BindingTarget, Binding);
}
@ -113,93 +115,27 @@ namespace Avalonia.Controls
return element;
}
private static IDisposable BindEditingElement(IAvaloniaObject target, AvaloniaProperty property, IBinding binding)
private static ICellEditBinding BindEditingElement(IAvaloniaObject target, AvaloniaProperty property, IBinding binding)
{
var result = binding.Initiate(target, property, enableDataValidation: true);
if (result != null)
{
//if(result.Subject != null)
//{
// var watcher = new BindingWatcher(result.Subject, result);
// result = watcher.InstancedBinding;
//}
return BindingOperations.Apply(target, property, result, null);
}
else
{
return Disposable.Empty;
}
}
public class LightweightSubject<T> : LightweightObservableBase<T>, ISubject<T>
{
public void OnCompleted()
{
PublishCompleted();
}
public void OnError(Exception error)
{
PublishError(error);
}
public void OnNext(T value)
{
PublishNext(value);
}
protected override void Deinitialize()
{ }
protected override void Initialize()
{ }
protected override void Subscribed(IObserver<T> observer, bool first)
{
base.Subscribed(observer, first);
}
}
class BindingWatcher
{
ISubject<object> _innerSubject;
ISubject<object> _wrappedSubject;
InstancedBinding _instancedBinding;
bool _isWriting = false;
public BindingWatcher(ISubject<object> innerSubject, InstancedBinding innerBinding)
{
_innerSubject = innerSubject;
_wrappedSubject = new LightweightSubject<object>();
_instancedBinding = new InstancedBinding(_wrappedSubject, innerBinding.Mode, innerBinding.Priority);
_innerSubject.Subscribe(InnerSubjectOnNext);
_wrappedSubject.Subscribe(WrappedSubjectOnNext);
}
private void InnerSubjectOnNext(object value)
{
Debug.WriteLine($"InnerSubject: {value} ({value?.GetType().Name})");
if (!_isWriting)
if(result.Subject != null)
{
_isWriting = true;
_wrappedSubject.OnNext(value);
_isWriting = false;
}
}
private void WrappedSubjectOnNext(object value)
{
Debug.WriteLine($"WrappedSubject: {value} ({value?.GetType().Name})");
if (!_isWriting)
{
_isWriting = true;
_innerSubject.OnNext(value);
_isWriting = false;
var bindingHelper = new CellEditBinding(result.Subject);
var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority);
BindingOperations.Apply(target, property, instanceBinding, null);
return bindingHelper;
}
BindingOperations.Apply(target, property, result, null);
}
public InstancedBinding InstancedBinding => _instancedBinding;
return null;
}
/*
public static IDisposable Apply(
IAvaloniaObject target,
@ -252,7 +188,6 @@ namespace Avalonia.Controls
protected abstract Control GenerateEditingElementDirect(DataGridCell cell, object dataItem);
internal AvaloniaProperty BindingTarget { get; set; }
internal void SetHeaderFromBinding()
@ -268,86 +203,6 @@ namespace Avalonia.Controls
}
}
}
private class AdvancedBinding
{
class SubjectWrapper : LightweightObservableBase<object>, ISubject<object>, IDisposable
{
private readonly ISubject<object> _sourceSubject;
private IDisposable _subscription;
private object _controlValue;
private bool _isControlValueSet = false;
public SubjectWrapper(ISubject<object> bindingSourceSubject)
{
_sourceSubject = bindingSourceSubject;
}
private void SetSourceValue(object value)
{
_sourceSubject.OnNext(value);
}
private void SetControlValue(object value)
{
PublishNext(value);
}
private void OnValidationError(BindingNotification notification)
{
}
private void OnControlValueUpdated(object value)
{
_controlValue = value;
_isControlValueSet = true;
}
private void OnSourceValueUpdated(object value)
{
if(value is BindingNotification notification)
{
if (notification.ErrorType != BindingErrorType.None)
OnValidationError(notification);
else
SetControlValue(value);
}
else
{
SetControlValue(value);
}
}
protected override void Deinitialize()
{
_subscription?.Dispose();
_subscription = null;
}
protected override void Initialize()
{
_subscription = _sourceSubject.Subscribe(OnSourceValueUpdated);
}
void IObserver<object>.OnCompleted()
{
throw new NotImplementedException();
}
void IObserver<object>.OnError(Exception error)
{
throw new NotImplementedException();
}
void IObserver<object>.OnNext(object value)
{
OnControlValueUpdated(value);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
}
}
}
}
/*
@ -398,14 +253,7 @@ namespace Avalonia.Controls
#endregion
#region ClipBoard
#endregion
#region Styles
//TODO Styles

12
src/Avalonia.DataGrid/DataGridColumn.cs

@ -37,6 +37,7 @@ namespace Avalonia.Controls
private object _header;
private DataGridColumnHeader _headerCell;
private Control _editingElement;
private ICellEditBinding _editBinding;
private IBinding _clipboardContentBinding;
private readonly Classes _cellStyleClasses = new Classes();
@ -174,6 +175,11 @@ namespace Avalonia.Controls
private set;
}
internal ICellEditBinding CellEditBinding
{
get => _editBinding;
}
/// <summary>
/// Determines whether or not this column is visible.
/// </summary>
@ -588,8 +594,6 @@ namespace Avalonia.Controls
return content;
}
public Control GetCellContent(DataGridRow dataGridRow)
{
Contract.Requires<ArgumentNullException>(dataGridRow != null);
@ -672,7 +676,7 @@ namespace Avalonia.Controls
/// <returns>
/// A new editing element that is bound to the column's <see cref="P:Avalonia.Controls.DataGridBoundColumn.Binding" /> property value.
/// </returns>
protected abstract Control GenerateEditingElement(DataGridCell cell, object dataItem);
protected abstract Control GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding binding);
/// <summary>
/// When overridden in a derived class, gets a read-only element that is bound to the column's
@ -988,7 +992,7 @@ namespace Avalonia.Controls
{
if (_editingElement == null)
{
_editingElement = GenerateEditingElement(cell, dataItem);
_editingElement = GenerateEditingElement(cell, dataItem, out _editBinding);
}
//if (_inputBindings == null && _editingElement != null)
//{

3
src/Avalonia.DataGrid/DataGridColumnHeader.cs

@ -286,7 +286,6 @@ namespace Avalonia.Controls
internal void OnMouseLeftButtonUp_Click(InputModifiers inputModifiers, ref bool handled)
{
Debug.WriteLine(nameof(OnMouseLeftButtonUp_Click));
// completed a click without dragging, so we're sorting
InvokeProcessSort(inputModifiers);
handled = true;
@ -458,7 +457,6 @@ namespace Avalonia.Controls
internal void OnMouseLeftButtonDown(ref bool handled, PointerEventArgs args, Point mousePosition)
{
Debug.WriteLine(nameof(OnMouseLeftButtonDown));
IsPressed = true;
if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
@ -503,7 +501,6 @@ namespace Avalonia.Controls
internal void OnMouseLeftButtonUp(ref bool handled, PointerEventArgs args, Point mousePosition, Point mousePositionHeaders)
{
IsPressed = false;
Debug.WriteLine(nameof(OnMouseLeftButtonUp));
if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
{

4
src/Avalonia.DataGrid/DataGridFillerColumn.cs

@ -4,6 +4,7 @@
// All other rights reserved.
using Avalonia.Interactivity;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
@ -55,8 +56,9 @@ namespace Avalonia.Controls
return null;
}
protected override Control GenerateEditingElement(DataGridCell cell, object dataItem)
protected override Control GenerateEditingElement(DataGridCell cell, object dataItem, out ICellEditBinding editBinding)
{
editBinding = null;
return null;
}

2
src/Avalonia.DataGrid/DataGridRow.cs

@ -100,7 +100,7 @@ namespace Avalonia.Controls
public bool IsValid
{
get { return _isValid; }
private set { SetAndRaise(IsValidProperty, ref _isValid, value); }
internal set { SetAndRaise(IsValidProperty, ref _isValid, value); }
}

160
src/Avalonia.DataGrid/Utilities/CellEditBinding.cs

@ -0,0 +1,160 @@
using Avalonia.Data;
using Avalonia.Reactive;
using System;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Text;
namespace Avalonia.Utilities
{
public interface ICellEditBinding
{
bool IsValid { get; }
IEnumerable<Exception> ValidationErrors { get; }
IObservable<bool> ValidationChanged { get; }
bool CommitEdit();
}
internal class CellEditBinding : ICellEditBinding
{
private readonly Subject<bool> _changedSubject = new Subject<bool>();
private readonly List<Exception> _validationErrors = new List<Exception>();
private readonly SubjectWrapper _inner;
public bool IsValid => _validationErrors.Count <= 0;
public IEnumerable<Exception> ValidationErrors => _validationErrors;
public IObservable<bool> ValidationChanged => _changedSubject;
public ISubject<object> InternalSubject => _inner;
public CellEditBinding(ISubject<object> bindingSourceSubject)
{
_inner = new SubjectWrapper(bindingSourceSubject, this);
}
private void AlterValidationErrors(Action<List<Exception>> action)
{
var wasValid = IsValid;
action(_validationErrors);
var isValid = IsValid;
if(!isValid || !wasValid)
{
_changedSubject.OnNext(isValid);
}
}
public bool CommitEdit()
{
_inner.CommitEdit();
return IsValid;
}
class SubjectWrapper : LightweightObservableBase<object>, ISubject<object>, IDisposable
{
private readonly ISubject<object> _sourceSubject;
private readonly CellEditBinding _editBinding;
private IDisposable _subscription;
private object _controlValue;
private bool _isControlValueSet = false;
private bool _settingSourceValue = false;
public SubjectWrapper(ISubject<object> bindingSourceSubject, CellEditBinding editBinding)
{
_sourceSubject = bindingSourceSubject;
_editBinding = editBinding;
}
private void SetSourceValue(object value)
{
_settingSourceValue = true;
_sourceSubject.OnNext(value);
_settingSourceValue = false;
}
private void SetControlValue(object value)
{
PublishNext(value);
}
private void OnValidationError(BindingNotification notification)
{
if(notification.Error != null)
{
_editBinding.AlterValidationErrors(errors =>
{
errors.Clear();
var unpackedErrors = ValidationUtil.UnpackException(notification.Error);
if(unpackedErrors != null)
errors.AddRange(unpackedErrors);
});
}
}
private void OnControlValueUpdated(object value)
{
_controlValue = value;
_isControlValueSet = true;
if(!_editBinding.IsValid)
{
SetSourceValue(value);
}
}
private void OnSourceValueUpdated(object value)
{
void OnValidValue(object val)
{
SetControlValue(val);
_editBinding.AlterValidationErrors(errors => errors.Clear());
}
if (value is BindingNotification notification)
{
if (notification.ErrorType != BindingErrorType.None)
OnValidationError(notification);
else
OnValidValue(value);
}
else
{
OnValidValue(value);
}
}
protected override void Deinitialize()
{
_subscription?.Dispose();
_subscription = null;
}
protected override void Initialize()
{
_subscription = _sourceSubject.Subscribe(OnSourceValueUpdated);
}
void IObserver<object>.OnCompleted()
{
throw new NotImplementedException();
}
void IObserver<object>.OnError(Exception error)
{
throw new NotImplementedException();
}
void IObserver<object>.OnNext(object value)
{
OnControlValueUpdated(value);
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
}
public void CommitEdit()
{
if(_isControlValueSet)
SetSourceValue(_controlValue);
}
}
}
}

168
src/Avalonia.DataGrid/Utilities/ValidationUtil.cs

@ -0,0 +1,168 @@
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
using System.Threading;
using System.Linq;
using Avalonia.Data;
namespace Avalonia.Utilities
{
internal static class ValidationUtil
{
/// <summary>
/// Searches a ValidationResult for the specified target member name. If the target is null
/// or empty, this method will return true if there are no member names at all.
/// </summary>
/// <param name="validationResult">ValidationResult to search.</param>
/// <param name="target">Member name to search for.</param>
/// <returns>True if found.</returns>
public static bool ContainsMemberName(this ValidationResult validationResult, string target)
{
int memberNameCount = 0;
foreach (string memberName in validationResult.MemberNames)
{
if (string.Equals(target, memberName))
{
return true;
}
memberNameCount++;
}
return (memberNameCount == 0 && string.IsNullOrEmpty(target));
}
/// <summary>
/// Finds an equivalent ValidationResult if one exists.
/// </summary>
/// <param name="collection">ValidationResults to search through.</param>
/// <param name="target">ValidationResult to find.</param>
/// <returns>Equal ValidationResult if found, null otherwise.</returns>
public static ValidationResult FindEqualValidationResult(this ICollection<ValidationResult> collection, ValidationResult target)
{
foreach (ValidationResult oldValidationResult in collection)
{
if (oldValidationResult.ErrorMessage == target.ErrorMessage)
{
bool movedOld = true;
bool movedTarget = true;
IEnumerator<string> oldEnumerator = oldValidationResult.MemberNames.GetEnumerator();
IEnumerator<string> targetEnumerator = target.MemberNames.GetEnumerator();
while (movedOld && movedTarget)
{
movedOld = oldEnumerator.MoveNext();
movedTarget = targetEnumerator.MoveNext();
if (!movedOld && !movedTarget)
{
return oldValidationResult;
}
if (movedOld != movedTarget || oldEnumerator.Current != targetEnumerator.Current)
{
break;
}
}
}
}
return null;
}
public static bool IsValid(this ValidationResult result)
{
return result == null || result == ValidationResult.Success;
}
public static IEnumerable<Exception> UnpackException(Exception exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
(IEnumerable<Exception>)new[] { exception } :
aggregate.InnerExceptions;
var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList();
if (filtered.Count > 0)
{
return filtered;
}
}
return null;
}
/// <summary>
/// Determines whether the collection contains an equivalent ValidationResult
/// </summary>
/// <param name="collection">ValidationResults to search through</param>
/// <param name="target">ValidationResult to search for</param>
/// <returns></returns>
public static bool ContainsEqualValidationResult(this ICollection<ValidationResult> collection, ValidationResult target)
{
return (collection.FindEqualValidationResult(target) != null);
}
/// <summary>
/// Adds a new ValidationResult to the collection if an equivalent does not exist.
/// </summary>
/// <param name="collection">ValidationResults to search through</param>
/// <param name="value">ValidationResult to add</param>
public static void AddIfNew(this ICollection<ValidationResult> collection, ValidationResult value)
{
if (!collection.ContainsEqualValidationResult(value))
{
collection.Add(value);
}
}
private static bool ExceptionsMatch(Exception e1, Exception e2)
{
return e1.Message == e2.Message;
}
public static void AddExceptionIfNew(this ICollection<Exception> collection, Exception value)
{
if(!collection.Any(e => ExceptionsMatch(e, value)))
{
collection.Add(value);
}
}
/// <summary>
/// Performs an action and catches any non-critical exceptions.
/// </summary>
/// <param name="action">Action to perform</param>
public static void CatchNonCriticalExceptions(Action action)
{
try
{
action();
}
catch (Exception exception)
{
if (IsCriticalException(exception))
{
throw;
}
// Catch any non-critical exceptions
}
}
/// <summary>
/// Determines if the specified exception is un-recoverable.
/// </summary>
/// <param name="exception">The exception.</param>
/// <returns>True if the process cannot be recovered from the exception.</returns>
public static bool IsCriticalException(Exception exception)
{
return (exception is OutOfMemoryException) ||
(exception is StackOverflowException) ||
(exception is AccessViolationException) ||
(exception is ThreadAbortException);
}
}
}
Loading…
Cancel
Save