Notes on Software

Posts Tagged ‘Validation’

Using BindingGroup in custom controls

Posted by K. M. on September 25, 2008

I have a WPF custom control to display and edit a double value. The control exposes a dependency property called Value, along with a Format, Converter and ConverterParameter properties. The control template has a TextBox whose Text property is bound to the Value property with a custom converter.

private void SetValueBinding()
{
    
if (textbox == null) return;
    
textbox.SetBinding(TextBox.TextProperty, new Binding(“Value”)
    {
        Source =
this,
        Converter =
new CustomConverter() { Converter = Converter, Format = Format },
        ConverterParameter = ConverterParameter,
        Mode =
BindingMode.TwoWay,
        ValidatesOnExceptions =
true
    });
}

(Here textbox is the instance of the TextBox that must be a part of the ControlTemplate, and the SetValueBinding method is called in the OnApplyTemplate override after obtaining the textbox instance)
This works well except for validation. If the user types in some text in the custom control that cannot be converted to a double, the Binding shown above fails. But this binding error does not participate in the validation mechanism (look at my earlier post on validation for a pattern for implementing validation). The solution is to find a BindingGroup on a parent of the custom control and add the BindingExpression to the BindingGroup.

private BindingGroup GetParentBindingGroup()
{
    
FrameworkElement element = this;
    
while (element != null)
    {
        
BindingGroup grp = element.BindingGroup;
        
if (grp != null)
            
return grp;
        element =
VisualTreeHelper.GetParent(element) as FrameworkElement;
    }
    
return null;
}

This can now be used in the SetValueBinding method

private void SetValueBinding()
{
    
if (textbox == null) return;
    
BindingGroup group = GetParentBindingGroup();
    
BindingExpressionBase expr =
        
BindingOperations.GetBindingExpressionBase(textbox, TextBox.TextProperty);
    
if (group != null && expr != null)
        group.BindingExpressions.Remove(expr);
    textbox.SetBinding(
TextBox.TextProperty, new Binding(“Value”)
    {
        Source =
this,
        Converter =
new CustomConverter() { Converter = Converter, Format = Format },
        ConverterParameter = ConverterParameter,
        Mode =
BindingMode.TwoWay,
        ValidatesOnExceptions =
true
    });
    expr =
BindingOperations.GetBindingExpressionBase(textbox, TextBox.TextProperty);
    
if (group != null)
        group.BindingExpressions.Add(expr);
}
Advertisements

Posted in WPF | Tagged: , | Leave a Comment »

Validation in WPF with .NET 3.5 SP1

Posted by K. M. on August 20, 2008

With the new BindingGroup support in WPF, validation in WPF can be handled much more cleanly than it was possible in the past. Here is a pattern that I believe I will be using fairly often.

In the data layer:

Make all domain objects implement IEditableObject and IDataErrorInfo and INotifyPropertyChanged.

Apply property level validation logic in the property setters of the domain objects. Throw exceptions when the logic is violated. (Example: Throw an exception if a name is null or empty or too long, Throw an exception if a number is negative etc.)

Apply object level validation logic in the implementation of IDataErrorInfo.Error. Do not throw exceptions when object level validation logic is violated.

Return null in the implementation of IDataErrorInfo.this[string ColumnName].

In the WPF layer:

Use a BindingGroup on the container that contains the editable UI elements

Use an instance of the following ValidationRule on the BindingGroup

public class DataErrorValidationRule : ValidationRule
{
    
public override ValidationResult Validate(object value,
        System.Globalization.
CultureInfo cultureInfo)
    {
        
BindingGroup group = (BindingGroup)value;
        
// if any expression is in error, the object level validation
        // cannot be trusted, since atleast some properties have not been
        // set. Better to skip object level validation than report
        // spurious errors
        if (group.BindingExpressions.Any(be => be.HasError))
            
return ValidationResult.ValidResult;
        
StringBuilder sb = null;
        
foreach (var item in group.Items)
        {
// Unlikely to have more than one item, but loop through anyway
            IDataErrorInfo info = item as IDataErrorInfo;
            
if (info != null)
            {
                
string error = info.Error;
                
if (!string.IsNullOrEmpty(error))
                {
                    
if (sb == null) sb = new StringBuilder();
                    
if (sb.Length != 0) sb.AppendLine();
                    sb.Append(error);
                }
            }
        }
        
if (sb != null)
            
return new ValidationResult(false, sb.ToString());
        
return ValidationResult.ValidResult;
    }
}

Explicitly set UpdateSourceTrigger to PropertyChanged or LostFocus on all bindings in the BindingGroup.

Set ValidatesOnExceptions to True on all bindings in the BindingGroup.

Define Edit, Accept and Cancel RoutedUICommands. Call BeginEdit, CommitEdit and CancelEdit on the BindingGroup when the commands are executed. Call ValidateWithoutUpdate and set CanExecuteRoutedEventArgs.CanExecute to the Validation.HasError property on the container element in the CanExecute event handler for the Accept command.

Use the following code to extract the error message from a ValidationError for display in the UI.

internal static string ExtractErrorMessage(ValidationError error)
{
    
if (error.Exception is TargetInvocationException &&
        error.Exception.Message == (
string)error.ErrorContent)
        
return error.Exception.InnerException.Message;
    
else
        return System.Convert.ToString(error.ErrorContent);
}

The code for all the work in the WPF layer can be abstracted into a static class with attached properties for use in XAML. This class provides the following features

  • IsEditableContainer attached property (Set to True on the container containing all the editing UI) This can be set both on a stand alone container or a container in an ItemsControl such as a ListBoxItem via a Style. When used in an ItemsControl, the code calls EditItemCommitEdit and CancelEdit on the IEditableCollectionView used by the ItemsControl.
  • IsEditingEnabled attached property (Set to False to disable editing. Can be bound to ListBoxItem.IsSelected for example)
  • IsDeletingEnabled attached property (Set to True to allow deleting the data item. Only relevant if the data item is in a collection and is being displayed in an ItemsControl)
  • EditGesture, AcceptGesture, CancelGesture and DeleteGesture attached properties. Specify KeyGestures for the Edit, Accept, Cancel and Delete commands
  • Deleting routed event which bubbles up the element tree. Use to cancel (possibly with a confirm dialog box) the execution of the Delete command
public static class DataEditing
{
    
public static bool GetIsEditableContainer(DependencyObject obj)
    {
        
return (bool)obj.GetValue(IsEditableContainerProperty);
    }
    
public static void SetIsEditableContainer(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEditableContainerProperty, value);
    }
    
public static readonly DependencyProperty IsEditableContainerProperty =
        
DependencyProperty.RegisterAttached(“IsEditableContainer”, typeof(bool), typeof(DataEditing),
            
new FrameworkPropertyMetadata(IsEditableContainer_PropertyChanged));
    
private static void IsEditableContainer_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        
FrameworkElement element = d as FrameworkElement;
        
if (element == null) return;
        
bool editable = (bool)e.NewValue;
        
if (editable)
        {
            
BindingGroup group = new BindingGroup();
            group.ValidationRules.Add(
new DataErrorValidationRule());
            element.BindingGroup = group;
            element.CommandBindings.Add(
new CommandBinding(Edit, OnExecuteEditCommand, CanExecuteEditCommand));
            element.CommandBindings.Add(
new CommandBinding(Accept, OnExecuteAcceptCommand, CanExecuteAcceptCommand));
            element.CommandBindings.Add(
new CommandBinding(Cancel, OnExecuteCancelCommand, CanExecuteCancelCommand));
            element.CommandBindings.Add(
new CommandBinding(Delete, OnExecuteDeleteCommand, CanExecuteDeleteCommand));
        }
        
else
        {
            element.BindingGroup =
null;
            element.CommandBindings.Remove((
CommandBinding b) => b.Command == Edit);
            element.CommandBindings.Remove((
CommandBinding b) => b.Command == Accept);
            element.CommandBindings.Remove((
CommandBinding b) => b.Command == Cancel);
            element.CommandBindings.Remove((
CommandBinding b) => b.Command == Delete);
        }
    }

    public static bool GetIsEditingEnabled(DependencyObject obj)
    {
        
return (bool)obj.GetValue(IsEditingEnabledProperty);
    }
    
public static void SetIsEditingEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEditingEnabledProperty, value);
    }
    
public static readonly DependencyProperty IsEditingEnabledProperty =
        
DependencyProperty.RegisterAttached(“IsEditingEnabled”, typeof(bool), typeof(DataEditing),
            
new FrameworkPropertyMetadata(true));

    public static bool GetIsDeletingEnabled(DependencyObject obj)
    {
        
return (bool)obj.GetValue(IsDeletingEnabledProperty);
    }
    
public static void SetIsDeletingEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsDeletingEnabledProperty, value);
    }
    
public static readonly DependencyProperty IsDeletingEnabledProperty =
        
DependencyProperty.RegisterAttached(“IsDeletingEnabled”, typeof(bool), typeof(DataEditing),
            
new FrameworkPropertyMetadata(false));

    public static KeyGesture GetEditGesture(DependencyObject obj)
    {
        
return (KeyGesture)obj.GetValue(EditGestureProperty);
    }
    
public static void SetEditGesture(DependencyObject obj, KeyGesture value)
    {
        obj.SetValue(EditGestureProperty, value);
    }
    
public static readonly DependencyProperty EditGestureProperty =
        
DependencyProperty.RegisterAttached(“EditGesture”, typeof(KeyGesture), typeof(DataEditing),
            
new FrameworkPropertyMetadata(CommandGesture_PropertyChanged));

    public static KeyGesture GetAcceptGesture(DependencyObject obj)
    {
        
return (KeyGesture)obj.GetValue(AcceptGestureProperty);
    }
    
public static void SetAcceptGesture(DependencyObject obj, KeyGesture value)
    {
        obj.SetValue(AcceptGestureProperty, value);
    }
    
public static readonly DependencyProperty AcceptGestureProperty =
        
DependencyProperty.RegisterAttached(“AcceptGesture”, typeof(KeyGesture), typeof(DataEditing),
            
new FrameworkPropertyMetadata(CommandGesture_PropertyChanged));

    public static KeyGesture GetCancelGesture(DependencyObject obj)
    {
        
return (KeyGesture)obj.GetValue(CancelGestureProperty);
    }
    
public static void SetCancelGesture(DependencyObject obj, KeyGesture value)
    {
        obj.SetValue(CancelGestureProperty, value);
    }
    
public static readonly DependencyProperty CancelGestureProperty =
        
DependencyProperty.RegisterAttached(“CancelGesture”, typeof(KeyGesture), typeof(DataEditing),
            
new FrameworkPropertyMetadata(CommandGesture_PropertyChanged));

    public static KeyGesture GetDeleteGesture(DependencyObject obj)
    {
        
return (KeyGesture)obj.GetValue(DeleteGestureProperty);
    }
    
public static void SetDeleteGesture(DependencyObject obj, KeyGesture value)
    {
        obj.SetValue(DeleteGestureProperty, value);
    }
    
public static readonly DependencyProperty DeleteGestureProperty =
        
DependencyProperty.RegisterAttached(“DeleteGesture”, typeof(KeyGesture), typeof(DataEditing),
            
new FrameworkPropertyMetadata(CommandGesture_PropertyChanged));

    private static void CommandGesture_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        
DependencyProperty dp = e.Property;
        
RoutedUICommand command =
            dp == EditGestureProperty ? Edit :
            dp == AcceptGestureProperty ? Accept :
            dp == CancelGestureProperty ? Cancel :
            dp == DeleteGestureProperty ? Delete :
            
null;
        
if (command != null)
        {
            
KeyGesture gesture = (KeyGesture)e.OldValue;
            
FrameworkElement element = (FrameworkElement)d;
            
if (gesture != null)
                element.InputBindings.Remove((
InputBinding ib) => ib.Command == command && ib.Gesture == gesture);
            gesture = (
KeyGesture)e.NewValue;
            
if (gesture != null)
                element.InputBindings.Add(
new InputBinding(command, gesture));
        }
    }

    private static bool GetViewAndItem(FrameworkElement element, out IEditableCollectionView view, out object dataItem)
    {
        
ItemsControl ic = ItemsControl.ItemsControlFromItemContainer(element);
        
if (ic != null)
        {
            
object item = ic.ItemContainerGenerator.ItemFromContainer(element);
            
if (item != null && item != DependencyProperty.UnsetValue)
            {
                view = (
IEditableCollectionView)ic.Items;
                dataItem = item;
                
return true;
            }
        }
        view =
null;
        dataItem =
null;
        
return false;
    }

    private static void CanExecuteEditCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;
        e.CanExecute = GetIsEditingEnabled(element) && GetIsReadOnly(element);
    }
    
private static void OnExecuteEditCommand(object sender, ExecutedRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;

        IEditableCollectionView view;
        
object item;
        
if (GetViewAndItem(element, out view, out item))
            view.EditItem(item);
        element.BindingGroup.BeginEdit();

        SetIsReadOnly(element, false);
    }

    private static void CanExecuteAcceptCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;
        element.BindingGroup.ValidateWithoutUpdate();
        e.CanExecute = !GetIsReadOnly(element) && !
Validation.GetHasError(element);
    }
    
private static void OnExecuteAcceptCommand(object sender, ExecutedRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;
        element.BindingGroup.CommitEdit();

        IEditableCollectionView view;
        
object item;
        
if (GetViewAndItem(element, out view, out item))
            view.CommitEdit();

        SetIsReadOnly(element, true);
    }

    private static void CanExecuteCancelCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;
        e.CanExecute = !GetIsReadOnly(element);
    }
    
private static void OnExecuteCancelCommand(object sender, ExecutedRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;
        element.BindingGroup.CancelEdit();

        IEditableCollectionView view;
        
object item;
        
if (GetViewAndItem(element, out view, out item))
            view.CancelEdit();

        SetIsReadOnly(element, true);
    }

    private static void CanExecuteDeleteCommand(object sender, CanExecuteRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;
        e.CanExecute = GetIsDeletingEnabled(element);
    }
    
private static void OnExecuteDeleteCommand(object sender, ExecutedRoutedEventArgs e)
    {
        
FrameworkElement element = sender as FrameworkElement;

        IEditableCollectionView view;
        
object item;
        
if (GetViewAndItem(element, out view, out item))
        {
            
CancelRoutedEventArgs cancelArgs = new CancelRoutedEventArgs() { RoutedEvent = DeletingEvent, Source = element };
            element.RaiseEvent(cancelArgs);
            
if (!cancelArgs.Cancel)
                view.Remove(item);
        }
    }

    public static bool GetIsReadOnly(DependencyObject obj)
    {
        
return (bool)obj.GetValue(IsReadOnlyProperty);
    }
    
private static void SetIsReadOnly(DependencyObject obj, bool value)
    {
        obj.SetValue(IsReadOnlyPropertyKey, value);
    }
    
public static readonly DependencyPropertyKey IsReadOnlyPropertyKey =
        
DependencyProperty.RegisterAttachedReadOnly(“IsReadOnly”, typeof(bool), typeof(DataEditing),
        
new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits));
    
public static readonly DependencyProperty IsReadOnlyProperty = IsReadOnlyPropertyKey.DependencyProperty;

    public static readonly RoutedUICommand Edit = new RoutedUICommand(“Edit”, “Edit”, typeof(DataEditing));
    
public static readonly RoutedUICommand Accept = new RoutedUICommand(“Accept”, “Accept”, typeof(DataEditing));
    
public static readonly RoutedUICommand Cancel = new RoutedUICommand(“Cancel”, “Cancel”, typeof(DataEditing));
    
public static readonly RoutedUICommand Delete = new RoutedUICommand(“Delete”, “Delete”, typeof(DataEditing));

    public static void AddDeletingHandler(DependencyObject element, CancelRoutedEventHandler handler)
    {
        ((
FrameworkElement)element).AddHandler(DeletingEvent, handler);
    }
    
public static void RemoveDeletingHandler(DependencyObject element, CancelRoutedEventHandler handler)
    {
        ((
FrameworkElement)element).RemoveHandler(DeletingEvent, handler);
    }
    
public static readonly RoutedEvent DeletingEvent =
        
EventManager.RegisterRoutedEvent(“Deleting”, RoutingStrategy.Bubble,
            
typeof(CancelRoutedEventHandler), typeof(DataEditing));

    private static void Remove<T>(this System.Collections.IList list, Func<T, bool> predicate)
    {
        T item = list.OfType<T>().FirstOrDefault(predicate);
        list.Remove(item);
    }
}

public delegate void CancelRoutedEventHandler(object sender, CancelRoutedEventArgs e);

public class CancelRoutedEventArgs : RoutedEventArgs
{
    
public bool Cancel{get; set;}
}

Posted in WPF | Tagged: , | 4 Comments »