I am surely not the first in this case to notice that Silverlight does not perform in the same way as WPF in certain aspects. One of those niceties that I relied on in WPF that does not appear within Silverlight is the ability to receive an event what the contents of a Textbox have be updated. This event is central in the validation process, especially in the reactive validation. For example it is not possible in Silverlight to validate character input via the IErrorInfo process but only to validate the complete content when navigating away from the Textbox. In a great number of real-life situations the validation is directly on input allowing the UI to reactively interact with the user providing instant feedback over the status of the input.
So, what can be done to enhance the Textbox control to support reactive validation? Well, there are three options available for the control designer. Firstly, the designer can subclass the Textbox class and provide an internal handler for the TextChanged event to check and update the binding source. The second option is to provide an attached property that attaches itself to the TextChanged event of the Textbox and updates the binding source. Finally, an Expression Blend Behaviour can be developed that has a similar effect to the attached property previous. It is worth noting here that the ideal solution should also be compatible with the philosophy behind WPF and Silverlight such that there is a separation between business logic and the visual representation i.e. the control is “skin-able”.
Sub-classing the Textbox
In the days of yore this was the only option available to developers and as the development process was fraught with difficulties and customising GUI was rarely if at all undertaken. However, those who did understand the intricacies of such development were forced to into sub-classing the control. As this experience took time and hence embedded itself into the developer as the process it is easy to see how this mentality is extended into the WPF/Silverlight world.
Ultimately these old tricks result in the following code:
using System.Windows.Controls;
using System.Windows.Data;
namespace DatabindingAttachedProperty
{
/// <summary>
/// The <see cref="TextChangedSubClass"/> class.
/// </summary>
public class TextChangedSubClass : TextBox
{
/// <summary>
/// Initializes a new instance of the <see cref="TextChangedSubClass"/> class.
/// </summary>
public TextChangedSubClass()
{
TextChanged += TextChangedSubClassTextChanged;
}
/// <summary>
/// Handles the TextChanged event of the TextChangedSubClass control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.Windows.Controls.TextChangedEventArgs"/> instance containing the event data.</param>
private static void TextChangedSubClassTextChanged(object sender, TextChangedEventArgs e)
{
if (!(sender is TextBox))
{
return;
}
var tb = sender as TextBox;
BindingExpression bindingExpression = tb.GetBindingExpression(TextProperty);
if (bindingExpression != null && bindingExpression.ParentBinding.Mode == BindingMode.TwoWay)
{
bindingExpression.UpdateSource();
}
}
}
}
The “one trick Pony” development mentality results in awkward code, extension difficulties and maintenance problems – today as it did back then. However, things have changed (unfortunately this development mentality has not) and today we have better ways of solving the problem of the missing binding update.
TextChanged Dependency Property
The Dependency Property provides a simple programming extension that augments a control with a property directly on the class itself, i.e. the class owns the dependency property and do not have much significance outside the class.
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DatabindingAttachedProperty
{
/// <summary>
/// The <see cref="TextChangedBindDependencyProperty"/> class.
/// </summary>
public class TextChangedBindDependencyProperty : TextBox
{
public static readonly DependencyProperty TextChangedBindProperty =
DependencyProperty.RegisterAttached("TextChangedBind", typeof (bool),
typeof (TextChangedBindDependencyProperty),
new PropertyMetadata(false, TextChangedBindCallBack));
/// <summary>
/// Gets or sets a value indicating whether text changed binding is active.
/// </summary>
/// <value><c>true</c> if text changed binding is active; otherwise, <c>false</c>.</value>
[Description("Enable or disable TextChanged checking for this control.")]
[Category("TextChangedBindDependencyProperty")]
public bool TextChangedBind
{
set { SetValue(TextChangedBindProperty, value); }
get { return (bool) GetValue(TextChangedBindProperty); }
}
/// <summary>
/// Texts the changed call back.
/// </summary>
/// <param name = "d">The d.</param>
/// <param name = "e">The <see cref = "System.Windows.DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
private static void TextChangedBindCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is TextBox)) return;
var tb = d as TextBox;
var isb = (bool) e.NewValue;
if (isb)
{
tb.TextChanged += TbTextChangedBind;
}
else
{
tb.TextChanged -= TbTextChangedBind;
}
}
/// <summary>
/// Handles the TextChangedBind event of the tb control.
/// </summary>
/// <param name = "sender">The source of the event.</param>
/// <param name = "e">The <see cref = "System.Windows.Controls.TextChangedBindEventArgs" /> instance containing the event data.</param>
private static void TbTextChangedBind(object sender, TextChangedEventArgs e)
{
if (!(sender is TextBox))
{
return;
}
var tb = sender as TextBox;
BindingExpression bindingExpression = tb.GetBindingExpression(TextProperty);
if (bindingExpression != null && bindingExpression.ParentBinding.Mode == BindingMode.TwoWay)
{
bindingExpression.UpdateSource();
}
}
/// <summary>
/// Gets the text changed bind.
/// </summary>
/// <param name="obj">The obj.</param>
/// <returns></returns>
public static bool GetTextChangedBind(DependencyObject obj)
{
return (bool) obj.GetValue(TextChangedBindProperty);
}
/// <summary>
/// Sets the text changed bind.
/// </summary>
/// <param name="obj">The obj.</param>
/// <param name="value">if set to <c>true</c> [value].</param>
public static void SetTextChangedBind(DependencyObject obj, bool value)
{
obj.SetValue(TextChangedBindProperty, value);
}
}
}
However, the disadvantage of the Attached Property is that it extends by sub-classing the original control. Hence a conscious decision has to be made in using the sub-classed control over the original. However, the designer will need to have a little mode control, something more flexible that can be use with base classes, third-party classes as well as the classes developed within the project.
TextChanged Attached Property
The Attached Property solution is the first that leaves the TextBox control intact and provides the binding update outside of the implementation. The great benefit of attached properties is that they allow you to extend the functionality of existing controls without having the source code or using extension methods. The attached property can be addressed directly in XAML as well as from the code behind.
namespace DatabindingAttachedProperty
{
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
/// <summary>
/// Definition of the <see cref = "TextChangedInteractor" /> class.
/// </summary>
public class TextChangedInteractor : DependencyObject
{
/// <summary>
/// Using a DependencyProperty as the backing store for TextChanged. This enables animation, styling, binding, etc…
/// </summary>
public static readonly DependencyProperty TextChangedProperty =
DependencyProperty.RegisterAttached("TextChanged", typeof (bool), typeof (TextChangedInteractor),
new PropertyMetadata(false, OnTextChangedChanged));
/// <summary>
/// The validation Regex.
/// </summary>
public static readonly DependencyProperty ExpressionProperty = DependencyProperty
.RegisterAttached(
"Expression",
typeof (string),
typeof (TextChangedInteractor),
new PropertyMetadata(string.Empty));
/// <summary>
/// Runs test for get Regex.
/// </summary>
/// <param name = "obj">
/// The obj parameter.
/// </param>
/// <returns>
/// The get Regex entry.
/// </returns>
public static bool GetTextChanged(DependencyObject obj)
{
return (bool) obj.GetValue(TextChangedProperty);
}
/// <summary>
/// Runs test for set Regex.
/// </summary>
/// <param name = "obj">
/// The obj parameter.
/// </param>
/// <param name = "value">
/// If set to <c>true</c> if value.
/// </param>
public static void SetTextChanged(DependencyObject obj, bool value)
{
obj.SetValue(TextChangedProperty, value);
}
/// <summary>
/// Runs test for on Regex entry.
/// </summary>
/// <param name="textChanged">The TextChanged parameter.</param>
/// <param name="e">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
private static void OnTextChangedChanged(DependencyObject textChanged, DependencyPropertyChangedEventArgs e)
{
if (!(textChanged is TextBox))
{
return; // support only Regex entry in a TextBox
}
var textBox = textChanged as TextBox;
// add the event handles for key press
if ((bool) e.NewValue)
{
textBox.TextChanged += TextBoxPreviewTextInput;
}
else
{
textBox.TextChanged -= TextBoxPreviewTextInput;
}
}
/// <summary>
/// Handles the PreviewTextInput event of the textBox control.
/// </summary>
/// <param name = "sender">The source of the event.</param>
/// <param name = "textChangedEventArgs">The <see cref = "System.Windows.Controls.TextChangedEventArgs" /> instance containing the event data.</param>
private static void TextBoxPreviewTextInput(object sender, TextChangedEventArgs textChangedEventArgs)
{
if (!(sender is TextBox))
{
return;
}
var tb = sender as TextBox;
var bindingExpression = tb.GetBindingExpression(TextBox.TextProperty);
if (bindingExpression != null && bindingExpression.ParentBinding.Mode == BindingMode.TwoWay)
{
bindingExpression.UpdateSource();
}
}
}
}
Expression Blend Behaviour
Expression Blend is the only design tool for the successful development of rich UIs in both WPF and Silverlight. Primarily a designer’s tool it can still be enhanced to support technical extensions. Blend’s extension mechanism is achieved through Behaviours. Similar in effect to the Attached Property solution, Blend Behaviours leave the base class intact and
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Interactivity;
namespace DatabindingAttachedProperty
{
/// <summary>
/// The <see cref="TextChangedBehavior"/> class.
/// </summary>
public class TextChangedBehavior : Behavior<TextBox>
{
/// <summary>
/// Called after the behavior is attached to an AssociatedObject.
/// </summary>
/// <remarks>Override this to hook up functionality to the AssociatedObject.</remarks>
protected override void OnAttached()
{
base.OnAttached();
// Insert code that you would want run when the Behavior is attached to an object.
AssociatedObject.TextChanged += TextChanged;
}
/// <summary>
/// Handles the TextChanged event of the AssociatedObject control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.Windows.Controls.TextChangedEventArgs"/> instance containing the event data.</param>
protected void TextChanged(object sender, TextChangedEventArgs e)
{
if (!(sender is TextBox))
{
return;
}
var tb = sender as TextBox;
BindingExpression bindingExpression = tb.GetBindingExpression(TextBox.TextProperty);
if (bindingExpression != null && bindingExpression.ParentBinding.Mode == BindingMode.TwoWay)
{
bindingExpression.UpdateSource();
}
}
/// <summary>
/// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
/// </summary>
/// <remarks>Override this to unhook functionality from the AssociatedObject.</remarks>
protected override void OnDetaching()
{
base.OnDetaching();
// Insert code that you would want run when the Behavior is removed from an object.
AssociatedObject.TextChanged -= TextChanged;
}
}
}
The test program consists of the four TextBox options and associated error processing. Entering a character in one text boxes results in the character being shown in each of the other text boxes. This shows that the binding is taking place on every keystroke and not just on the focus changing as is normal in Silverlight.
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:DatabindingAttachedProperty="clr-namespace:DatabindingAttachedProperty"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
x:Class="DatabindingAttachedProperty.MainPage"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="604">
<Grid
x:Name="LayoutRoot"
Background="White"
DataContext="{Binding Source={StaticResource Locator}}">
<StackPanel Margin="100,30,300,0" Orientation="Vertical" d:LayoutOverrides="Height">
<DatabindingAttachedProperty:TextChangedBindDependencyProperty
x:Name="textChangedDependencyProperty"
Height="36"
TextWrapping="Wrap"
Text="{Binding Main.TextDetails, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
TextChangedBind="True"
Margin="0,0,0,10" />
<TextBox
x:Name="textBox1"
Height="36"
TextWrapping="Wrap"
Text="{Binding Main.TextDetails, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
Margin="0,0,0,10"
/>
<TextBox
x:Name="textBox"
TextWrapping="Wrap"
Text="{Binding Main.TextDetails, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
Height="36"
d:LayoutOverrides="Height"
Margin="0,0,0,10">
<i:Interaction.Behaviors>
<DatabindingAttachedProperty:TextChangedBehavior />
</i:Interaction.Behaviors>
</TextBox>
<DatabindingAttachedProperty:TextChangedSubClass
x:Name="textChangedSubClass"
TextWrapping="Wrap"
Text="{Binding Main.TextDetails, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
DatabindingAttachedProperty:TextChangedInteractor.TextChanged="true"
Height="36"
Margin="0,0,0,10" />
</StackPanel>
</Grid>
</UserControl>
The ViewModel contains validation for itself as well as the Model.
using System.ComponentModel;
using System.Linq;
using DatabindingAttachedProperty.Model;
using DatabindingAttachedProperty.Validation;
using FluentValidation.Results;
using GalaSoft.MvvmLight;
namespace DatabindingAttachedProperty.ViewModel
{
/// <summary>
/// The <see cref="MainViewModel"/> class.
/// </summary>
public class MainViewModel : ViewModelBase, IDataErrorInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="MainViewModel"/> class.
/// </summary>
public MainViewModel()
{
Model = new MainModel();
}
/// <summary>
/// Gets or sets the text details.
/// </summary>
/// <value>The text details.</value>
public string TextDetails
{
get { return Model.TextDetails; }
set
{
if (Model.TextDetails == value)
{
return;
}
Model.TextDetails = value;
RaisePropertyChanged("TextDetails");
}
}
/// <summary>
/// Gets a value indicating whether this instance is valid.
/// </summary>
/// <value><c>true</c> if this instance is valid; otherwise, <c>false</c>.</value>
public bool IsValid
{
get { return SelfValidate().IsValid; }
}
/// <summary>
/// Gets or sets the model.
/// </summary>
/// <value>The model.</value>
protected MainModel Model { get; set; }
#region Implementation of IDataErrorInfo
/// <summary>
/// Gets a message that describes any validation errors for the specified property or column name.
/// </summary>
/// <returns>
/// The validation error on the specified property, or null or <see cref="F:System.String.Empty"/> if there are no errors present.
/// </returns>
/// <param name="columnName">The name of the property or column to retrieve validation errors for.</param>
public string this[string columnName]
{
get
{
ValidationResult validationResults = SelfValidate();
if (validationResults == null) return string.Empty;
ValidationFailure columnResults =
validationResults.Errors.FirstOrDefault(x => string.Compare(x.PropertyName, columnName) == 0);
return columnResults != null ? columnResults.ErrorMessage : string.Empty;
}
}
/// <summary>
/// Gets a message that describes any validation errors for the object.
/// </summary>
/// <returns>
/// The validation error on the object, or null or <see cref="F:System.String.Empty"/> if there are no errors present.
/// </returns>
public string Error
{
get { return ValidationHelper.GetError(SelfValidate()); }
}
#endregion
/// <summary>
/// Validates the ViewModel and Model.
/// </summary>
/// <returns></returns>
public ValidationResult SelfValidate()
{
ValidationResult vr1 = ValidationHelper.Validate<MainViewModelValidator, MainViewModel>(this);
ValidationResult vr2 = ValidationHelper.Validate<MainModelValidator, MainModel>(Model);
foreach (ValidationFailure failure in vr2.Errors)
{
vr1.Errors.Add(failure);
}
return vr1;
}
}
}
The Model contains only one property.
namespace DatabindingAttachedProperty.Model
{
/// <summary>
/// The <see cref="MainModel"/> class.
/// </summary>
public class MainModel
{
/// <summary>
/// Gets or sets the text details.
/// </summary>
/// <value>The text details.</value>
public string TextDetails { get; set; }
}
}
The validation relies on Fluent Validation package and the validators for both the ViewModel and the Model are shown below.
using DatabindingAttachedProperty.ViewModel;
using FluentValidation;
namespace DatabindingAttachedProperty.Validation
{
/// <summary>
/// The <see cref="MainViewModelValidator"/> class.
/// </summary>
public class MainViewModelValidator : AbstractValidator<MainViewModel>
{
/// <summary>
/// Initializes a new instance of the <see cref="MainViewModelValidator"/> class.
/// </summary>
public MainViewModelValidator()
{
RuleFor(model => model.TextDetails).NotNull().Matches(@"^[0-9]+$").WithMessage("Not a number.");
}
}
}
using DatabindingAttachedProperty.Model;
using FluentValidation;
namespace DatabindingAttachedProperty.Validation
{
/// <summary>
/// The <see cref="MainModelValidator"/> class.
/// </summary>
public class MainModelValidator : AbstractValidator<MainModel>
{
/// <summary>
/// Initializes a new instance of the <see cref="MainModelValidator"/> class.
/// </summary>
public MainModelValidator()
{
RuleFor(model => model.TextDetails).NotNull().WithMessage("Must hold a value.");
}
}
}
In conclusion, four options have been put forward as successful in solving the missing property changed binding that exists in WPF but not in Silverlight4. The first two methods of extending the base class invasively are not recommended as the full design flexibility are non-existent. The Attached Property solution as well as the Blend Behaviour solution are, by far, the correct way to approach a solution. Personally, I find that only the latter two options should be considered – but then I am a believer in the WPF and Silverlight design philosophy and hope to rid myself of the burden of the VB control development word someday.
That’s all Folks…
Posted in Attached property, Behaviour, Blend 4, Design, MVVM, Silverlight, VS2010, WPF, XAML
You must be logged in to post a comment.