Manish Dalal's blog

Exploring .net!

Prevention : The first line of defense, with Attach Property Pixie dust!

In the Building Business Application with Silverlight series we have seen how to validate data in various conditions. While it is necessary to validate data after user has entered it, it will be even better if we can prevent user from  entering invalid data in the first place. Consider for example the Age field. User should only be allowed to enter positive numbers. Negative numbers or alpha characters should not be allowed. A simple way to ensure this rule is to check data as part of validation, if it is negative or non numeric, we can throw exception or set some error state. This typically occurs after user has entered data, in setter of data fields. User will then reenter the new data, resulting in duplicate work.

Instead of waiting for user to complete the data entry and then validating, we can trap user keystrokes as data is being entered and prevent invalid keys. This can carried out by subscribing to the KeyDown event and rejecting any keys that are not proper.

KeyDown event provides information on the Key pressed as part of KeyEventArgs. The KeyDown event is a bubbling event and can be canceled by setting Handled property of KeyEventArgs to true. This prevents it from routing to further objects along the event route, effectively canceling the event.

We will implement  text filtering feature as a service utilizing attached property to provide value for the filter. Another option is to inherit from TextBox and create a new control. However since we are just modifying existing control behavior and only working with with public properties, it is simpler to use attached property to add functionality rather then creating new control.

Attached Property Basics

An attached property is a dependency property that is defined by one object but settable on other object. Attached properties are defined as a specialized form of dependency property that do not have the conventional property wrapper on the type defining the property. In addition, storage is provided by the type on which the property is set and not the defining type.

In XAML, you set attached properties by using the syntax AttachedPropertyProvider.PropertyName. Attached property value itself is stored by the object on which it is set. Think of the object as providing a name value pair dictionary, where property is the key and points to the value, to be used by property definer or even other objects.

ParentPropertyProvider.Propertyname

The most typical use of attached property is where parent type defines property to be set on the child elements. The parent type then iterates its child elements, obtains the values, and acts on those values in some manner. For instance when you layout using Grid, you specify Row and Column as attached property on children elements. The Grid.Row property is created as an attached property because it is designed to be set on elements that are contained within a Grid, rather than on Grid itself. Grid then uses Row and Column values for layout.

ServicePropertyProvider.PropertyName

However another alternate use is to impart functionality, where by the type that defines the attached property represents a service. Then when the element that set the attached property is evaluated in the context of the service, the attached property values are obtained by the service class and desired behavior carried out. An example of this is TooltipService with attached property Tooltip. In this scenario, AttachedPropertyProvider represents a Service and attached property provides value for the service to act on.

We will use this alternate way to define TextBoxFilterService with attached property Filter, to represent various type of filters. There is also another use of attached property to store arbitrary data, that we will explore later.

TextBoxFilterService

Create a new Silverlight application project. Also create the corresponding web application to host and test the Silverlight application. In the Silverlight project, add new class, called TextBoxFilterService.cs. Define attached property Filter as:

// Filter Attached Dependency Property
public static readonly DependencyProperty FilterProperty =
    DependencyProperty.RegisterAttached("Filter", typeof(TextBoxFilterType), typeof(TextBoxFilterService),
                                        new PropertyMetadata(OnFilterChanged));
// Gets the Filter property. 
public static TextBoxFilterType GetFilter(DependencyObject d) {
    return (TextBoxFilterType)d.GetValue(FilterProperty);
}
// Sets the Filter property.
public static void SetFilter(DependencyObject d, TextBoxFilterType value) {
    d.SetValue(FilterProperty, value);
}

What enables use of attached property for service provider scenario is the delegate OnFilterChanged. This delegate is called whenever attached property value changes. As part the call, you get access to the object on with attached property is defined. Once you have handle to the object, you can attach event handlers to modify the default behavior.

Add OnFilterChanged delegate as shown

// Handles changes to the Filter property.
private static void OnFilterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
    TextBox textBox = d as TextBox;
    if (TextBoxFilterType.None != (TextBoxFilterType)e.OldValue) {
        textBox.KeyDown -= new KeyEventHandler(textBox_KeyDown);
    }
    if (TextBoxFilterType.None != (TextBoxFilterType)e.NewValue) {
        textBox.KeyDown += new KeyEventHandler(textBox_KeyDown);
    }
}

In the delegate above we are attaching to KeyDown event. We will implement our filtering logic in the KeyDown event handler.

// Handles the KeyDown event of the textBox control. private static void textBox_KeyDown(object sender, KeyEventArgs e) { // bypass other keys! if (IsValidOtherKey(e.Key)) { return; } TextBoxFilterType filterType = GetFilter((DependencyObject)sender); TextBox textBox = sender as TextBox;

if (null == textBox) {
    textBox = e.Source as TextBox;
}

switch (filterType) { case TextBoxFilterType.PositiveInteger: e.Handled = !IsValidIntegerKey(textBox, e.Key, e.PlatformKeyCode, false); break; case TextBoxFilterType.Integer: e.Handled = !IsValidIntegerKey(textBox, e.Key, e.PlatformKeyCode, true); break; case TextBoxFilterType.PositiveDecimal: e.Handled = !IsValidDecmialKey(textBox, e.Key, e.PlatformKeyCode, false); break; case TextBoxFilterType.Decimal: e.Handled = !IsValidDecmialKey(textBox, e.Key, e.PlatformKeyCode, true); break; case TextBoxFilterType.Alpha: e.Handled = !IsValidAlphaKey(e.Key); break; } } // Determines whether the specified key is valid other key. private static bool IsValidOtherKey(Key key) { // allow control keys if ((Keyboard.Modifiers & ModifierKeys.Control) != 0) { return true; } // allow // Back, Tab, Enter, Shift, Ctrl, Alt, CapsLock, Escape, PageUp, PageDown // End, Home, Left, Up, Right, Down, Insert, Delete // except for space! // allow all Fx keys if ( (key < Key.D0 && key != Key.Space) || (key > Key.Z && key < Key.NumPad0)) { return true; } // we need to examine all others! return false; } // Determines whether the specified key is valid integer key for the specified text box. private static bool IsValidIntegerKey(TextBox textBox, Key key, int platformKeyCode, bool negativeAllowed) { if ((Keyboard.Modifiers & ModifierKeys.Shift) != 0) { return false; } if (Key.D0 <= key && key <= Key.D9) { return true; } if (Key.NumPad0 <= key && key <= Key.NumPad9) { return true; } if (negativeAllowed && (key == Key.Subtract || (key == Key.Unknown && platformKeyCode == 189))) { return 0 == textBox.Text.Length; } return false; } // Determines whether the specified key is valid decmial key for the specified text box. private static bool IsValidDecmialKey(TextBox textBox, Key key, int platformKeyCode, bool negativeAllowed) { if (IsValidIntegerKey(textBox, key, platformKeyCode, negativeAllowed)) { return true; } if (key == Key.Decimal || (key == Key.Unknown && platformKeyCode == 190)) { return !textBox.Text.Contains("."); } return false; } // Determines whether the specified key is valid alpha key. private static bool IsValidAlphaKey(Key key) { if (Key.A <= key && key <= Key.Z) { return true; } return false; }

When we receive the KeyDown notification, first thing we do is to check for valid keys such as Backspace, Enter and other non alpha numeric keys. If key is one of the allowed ones, we just return, and let the normal processing occur. Next we look for type of filter that has been set and try to check for valid keys for that filter. For instance if Filter has been set to PositiveInterger, only numeric keys are allowed. If key is not valid for given filter, we set Handled to true and stop processing, preventing key from having any effect.

We also need to add TextBoxFilterType enum that defines type of filter we support.

public enum TextBoxFilterType {
    None,
    PositiveInteger,
    PositiveDecimal,
    Integer,
    Decimal,
    Alpha,
}

To test TextBoxFilterService, first add namespace declaration as

xmlns:src="clr-namespace:Silverlight"

to the UserControl opening tag in the Page.xaml. Next add following XAML markup to Page.xaml Grid element.

<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
    <RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="Auto"/>
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Only Positive Integer TextBox"/>
<TextBox Grid.Column="1" src:TextBoxFilterService.Filter="PositiveInteger"/>
<TextBlock Grid.Column="0" Grid.Row="2" Text="Only Integer TextBox"/>
<TextBox Grid.Column="1" Grid.Row="2" src:TextBoxFilterService.Filter="Integer"/>
<TextBlock Grid.Column="0" Grid.Row="3" Text="Only Positive Decimal TextBox"/>
<TextBox Grid.Column="1" Grid.Row="3" src:TextBoxFilterService.Filter="PositiveDecimal"/>
<TextBlock Grid.Column="0" Grid.Row="4" Text="Only Positive Decimal TextBox"/>
<TextBox Grid.Column="1" Grid.Row="4" src:TextBoxFilterService.Filter="Decimal"/>
<TextBlock Grid.Column="0" Grid.Row="5" Text="Only Alphabets TextBox"/>
<TextBox Grid.Column="1" Grid.Row="5" src:TextBoxFilterService.Filter="Alpha"/>

Finally run the application

image

Try out various of the options. Note that only valid keys are allowed, others are simply ignored. You can extend code to handle other scenarios that might be common in your business domain.

DataPropertyProvider.PropertyName

Use of attached property to implement service is very powerful concept and can be leveraged in many other situations. Another equally useful but lesser know use of attached property is to save arbitrary data. For instance, Tag property found on many elements allows user to save custom strings. However if the element does not provide one, like say ColumnDefinition, it is easy to add your own Tag. Define a custom tag property, MyTag in page.xaml.cs as

public static readonly DependencyProperty MyTagProperty =
    DependencyProperty.RegisterAttached("MyTag", typeof(string), typeof(Page),
                                        new PropertyMetadata(null));

public static string GetMyTag(DependencyObject d) {
    return (string)d.GetValue(MyTagProperty);
}

public static void SetMyTag(DependencyObject d, string value) {
    d.SetValue(MyTagProperty, value);
}

You can now used MyTag property on ColumnDefinition as

<ColumnDefinition Width="Auto" src:Page.MyTag="CustomTag"/>

Add new RowDefinition to LayoutRoot grid and add new Button (call it myTagButton) to test functionality. Add following code for the Button click event

void myTagButton_Click(object sender, RoutedEventArgs e) {
    HtmlPage.Window.Alert(GetMyTag(this.LayoutRoot.ColumnDefinitions[0]));
}

Test the MyTag functionality

image

Actually there is not need for the custom GetMyTag and SetMyTag helper methods as well, you can get data directly from object on which you define attached property using GetValue and SetValue methods. The only requirement to use of attach property is that, the object on which you are setting attached property must derive from DependencyObject. It is the DependencyObject that provides GetValue and SetValue methods.

Replace code in button click with following

void myTagButton_Click(object sender, RoutedEventArgs e) {
    //HtmlPage.Window.Alert(GetMyTag(this.LayoutRoot.ColumnDefinitions[0]));
    HtmlPage.Window.Alert(this.LayoutRoot.ColumnDefinitions[0].GetValue(MyTagProperty).ToString());
}

If you the test application again, you will get the same results.

Hopefully you have found new appreciation for the attached property and gained knowledge to start using in you own service provider or to store you own data.

Source Code: FilterService.zip

Silverlight Business Application Part 7: Beyond Validation, Prevention.

If you have been following the Building Business Application with Silverlight series, this is essentially Part 7, going beyond validation. We have the Age filed in the Person class that is currently being validated by PersonValidator using following code

public void ValidateAge(int newValue) {
    InvalidAge = (newValue < 0 || newValue > 200);
}

If you enter negative number in the Age filed, it is caught by the validator and user is displayed the error

image

Let's use the TextBoxFilterService to prevent entry of negative numbers. First add code for TextBoxFilterService to the Silverlight project. Next modify page.xaml and wire up age TextBox to the filter service

<TextBox
    Text="{Binding Path=Age,Mode=TwoWay,NotifyOnValidationError=true,ValidatesOnExceptions=true}"
    Background="{Binding Path=Validator.InvalidAge, Converter={StaticResource brushConverter}}"
    ToolTipService.ToolTip="{Binding Path=Validator, Converter={StaticResource messageConverter},ConverterParameter=Age}" 
    src:TextBoxFilterService.Filter="PositiveInteger"
    Tag="Age"
    >
</TextBox>

Now when you run the application, it will only allow positive numbers, all other entries are ignored. Note that validation is still required since user can paste invalid data.

Source Code for the Business Application:BusinessApp1.zip

Comments

... said:

Nice site you have!

# January 27, 2009 11:16 AM

Andrew said:

Thanks for the original code.  Works well.  I added an additional number type called a 2 decimal place positive decimal, so I have added the additional code fragments below in case anyone else find it useful

......

case TextBoxFilterType.PositiveDecimal2DP:

  e.Handled = !IsValidDecmialKey(textBox, e.Key, e.PlatformKeyCode, false);

  if (!e.Handled)

      e.Handled = !IsValid2DPKey(textBox, e.Key, e.PlatformKeyCode, false);

  break;

.....

// Determines whether the specified key is valid for a 2 Decimal place text box.

private static bool IsValid2DPKey(TextBox textBox, Key key, int platformKeyCode, bool negativeAllowed)

{

   if (!textBox.Text.Contains("."))

       return true;

   else

       if (textBox.SelectionStart <= textBox.Text.LastIndexOf("."))

           return true;

       else

           return (textBox.Text.Length - 2 <= textBox.Text.LastIndexOf("."));

}

# April 15, 2009 12:34 AM

Sharif said:

Thanks dude, its a great article.

# August 18, 2009 12:21 AM

John Scully said:

This is a great article, and describes exactly what I was starting to design. Fortunately, I googled first, to see if any one else had done it first (they usually have!) and I found your work. Really excellent, and thank you for taking the time to publish it.

# September 18, 2009 5:49 AM