Notes on Software

Archive for August, 2008

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

Advertisements

Posted in WPF | Tagged: , | 4 Comments »

Connection pooling is good

Posted by K. M. on August 9, 2008

I was writing a project which involved accessing user instances of a SQL Server database from two separate processes in turn. This failed with “Login failed” error messages. Connection pooling meant that the process which attached the database first did not close its connection even after its use of the database was over. A simple solution seemed to be to disable connection pooling on both processes. This worked in the sense that there were no exceptions. But performance took a huge hit. Finally I changed the design so that all the work was done in a single process. I could afford to do it since there was not much code involved. This is something to keep in mind when using user instances in SQL Server. Disabling connection pooling is an unacceptable solution.

Posted in SQL Server | Tagged: | Leave a Comment »

Routed Events in WPF

Posted by K. M. on August 9, 2008

Unlike the concept of dependency properties, the concept of routed events is a little difficult to understand. It is clear what purposes dependency properties serve. But routed events? Why would anyone want events to route up or down the element tree? There is a common example given of a Panel with multiple Buttons in it where one can attach an event handler for Button.Click on the Panel instead of on the Buttons and have it called when any Button is clicked. But that does not seem to be a good enough reason to architect an entire feature. And indeed it is not. Event routing becomes much more useful in the context of commands and control templating. It would be much more difficult to write controls like the ScrollBar or the Slider without routed commands (which work via routed events) while allowing them to be easily templatable. The real purpose of routed events is not to attach handlers on some node of a static tree of elements to handle events on the children. It is to enable parent elements to define behavior when a particular event occurs on a child element that it does not statically know of.

Posted in WPF | Tagged: | Leave a Comment »

Checkboxes in WPF TreeView

Posted by K. M. on August 3, 2008

Josh Smith recently posted an interesting article on CodeProject about using CheckBoxes in a WPF TreeView.

“The fundamental problem is that as you navigate the tree via arrow keys, a TreeViewItem will first take input focus, and then the CheckBox it contains will take focus upon the next keystroke. Both the TreeViewItem and CheckBox controls are focusable. The result is that you must press an arrow key twice to navigate from item to item in the tree.”

His solution is to set Focusable to false on the CheckBox and make the TreeViewItem a “virtual ToggleButton” by applying attached properties.

Since the problem has to do with focus, it can also be solved by setting focus on the CheckBox when the TreeViewItem receives focus. The class shown below defines an attached property – FocusedChildName – that sets focus on a named element in the visual tree of the target element. Adding a Setter to the Style for the TreeViewItem

<Setter Property=”local:FocusHelper.FocusedChildName” Value=”checkbox”/>

and setting up a binding on the Focusable property of the CheckBox

<CheckBox x:Name=”checkbox”
    Focusable=”{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TreeViewItem}}, Path=IsSelected}”

produces the desired behavior

public static class FocusHelper
{
    
public static string GetFocusedChildName(DependencyObject obj)
    {
        
return (string)obj.GetValue(FocusedChildNameProperty);
    }

    public static void SetFocusedChildName(DependencyObject obj, string value)
    {
        obj.SetValue(FocusedChildNameProperty, value);
    }
    
public static readonly DependencyProperty FocusedChildNameProperty =
        
DependencyProperty.RegisterAttached(“FocusedChildName”, typeof(string), typeof(FocusHelper),
            
new UIPropertyMetadata(FocusedChildName_PropertyChanged));

    private static void FocusedChildName_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        
UIElement element = d as UIElement;
        
if (element != null && !string.IsNullOrEmpty((string)e.NewValue))
            element.GotKeyboardFocus += element_GotKeyboardFocus;
        
if (element != null && string.IsNullOrEmpty((string)e.NewValue))
            element.GotKeyboardFocus -= element_GotKeyboardFocus;
    }

    private static void element_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
    {
        
if (e.OriginalSource == sender)
        {
            (sender
as DispatcherObject).Dispatcher.BeginInvoke(DispatcherPriority.Input,
                
new Action(() =>
                {
                    
DependencyObject obj = sender as DependencyObject;
                    
string name = (string)obj.GetValue(FocusHelper.FocusedChildNameProperty);
                    
IInputElement element = FindNamedElementInVisualTree(obj, name) as IInputElement;
                    
if (element != null)
                        element.Focus();
                }));
        }
    }

    private static DependencyObject FindNamedElementInVisualTree(DependencyObject parent, string name)
    {
        
int count = VisualTreeHelper.GetChildrenCount(parent);
        
for (int i = 0; i < count; i++)
        {
            
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            
if ((string)child.GetValue(FrameworkElement.NameProperty) == name) return child;
            child = FindNamedElementInVisualTree(child, name);
            
if (child != null) return child;
        }
        
return null;
    }
}

Posted in WPF | Tagged: | Leave a Comment »