Thursday, May 31, 2007

Fully binded validation by using Dependency and Attached properties

Yesterday, I got a request from one of my clients to create validation rule with range, binded to source or any other data. Well, that's easy - I though. However, there are a couple of tricks, you should know, to perform it.

Let's start from the beginning. Custom validation rule have to inherit from ValidationRule class, this means, that it can not be dependency object. But how said, that it can not have dependency object member? It can. So, first of all we need a validation rule.

    public class MinMaxValidationRule:ValidationRule
{
private Int32RangeChecker validRange;

public Int32RangeChecker ValidRange
{
get { return validRange; }
set { validRange = value; }
}

public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
int res = int.MinValue;
bool isNumber = int.TryParse(value.ToString(), out res);
bool isValidRange = true;
if (validRange == null)
{
isValidRange =
true;
}
else
{
isValidRange = (res > validRange.Minimum & res < validRange.Maximum);
}
string errorString = !isNumber ? "The string is in incorrect format" : !isValidRange ? "The input integer is out of range" : string.Empty;
return new ValidationResult(isNumber && isValidRange, errorString);

}
}

 


Well, I have not explain what is doing, right? :) Next we'll create DependencyObject Int32RangeChecker. That's simple as well.


    public class Int32RangeChecker : DependencyObject
{
public int Minimum
{
get { return (int)GetValue(MinimumProperty); }
set { SetValue(MinimumProperty, value); }
}
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(int), typeof(Int32RangeChecker), new UIPropertyMetadata(int.MinValue));


public int Maximum
{
get { return (int)GetValue(MaximumProperty); }
set { SetValue(MaximumProperty, value); }
}
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(int), typeof(Int32RangeChecker), new UIPropertyMetadata(int.MaxValue));

}

 


Now, let's XAML it. I'm using converter-validator explained earlier. You really do not have to do it.


    <TextBox>
<
TextBox.Text>
<
Binding Path="[1]" UpdateSourceTrigger="PropertyChanged">
<
Binding.Converter>
<
local:MyInt32Converter xmlns:local="clr-namespace:BindedValidator" />
</
Binding.Converter>
<
Binding.ValidationRules>
<
local:MinMaxValidationRule/>
<
local:MinMaxValidationRule.ValidRange>
<local:Int32RangeChecker
Minimum="{Binding Source={StaticResource dataSource}, Path=MinValue}"
Maximum="{Binding Source={StaticResource dataSource}, Path=MaxValue}"/>
</local:MinMaxValidationRule.ValidRange>
</local:MinMaxValidationRule
>
</
Binding.ValidationRules>
</
Binding>
</
TextBox.Text>
</
TextBox>

 


There are a couple of tricks here. First, I'm using explicitly set UpdateSourceTrigger. This one, will force the validation on source change, rather then on focus lost. The other trick, is that if you even have data context, or you want to set control as data source for internal DP - this will not work. Why? 'cos our dependency object is not part of logical tree, so you can not use ElementName or DataContext as source for internal data binding.


So far, so good. What to do if I want to bind to control (e.g. slider) in my page? You have to put local:Int32RangeChecker DependencyObject in your resources and use it following way.


<Slider Minimum="0" Maximum="20" Value="{Binding Source={StaticResource rangeConverter}, Path=Minimum, Mode=OneWayToSource}" Name="minVal"/>
<
Slider Minimum="0" Maximum="20" Value="{Binding Source={StaticResource rangeConverter}, Path=Maximum, Mode=OneWayToSource}" Name="maxVal"/>
<
TextBox>
<
TextBox.Text>
<
Binding Path="[1]" UpdateSourceTrigger="PropertyChanged">
<
Binding.Converter>
<
local:MyInt32Converter xmlns:local="clr-namespace:TreeViewItemEdit" />
</
Binding.Converter>
<
Binding.ValidationRules>
<
local:MinMaxValidationRule ValidRange="{StaticResource rangeConverter}"/>
</Binding.ValidationRules>
</
Binding>
</
TextBox.Text>
</
TextBox>

 


Well. That's one of methods to do data binding inside validation rule. The other approach is by using Attached Properties. You can create static class with properties, can be attached to your validation range source. The XAML will looks like this


<TextBox 
l:MinMaxValidator.Minimum="{Binding Source={StaticResource rangeConverter}, Path=Minimum}"
l:MinMaxValidator.Maximum="{Binding Source={StaticResource rangeConverter}, Path=Maximum}">
<
TextBox.Text>
<
Binding Path="[1]" UpdateSourceTrigger="PropertyChanged">
<
Binding.Converter>
<
local:MyInt32Converter xmlns:local="clr-namespace:TreeViewItemEdit" />
</
Binding.Converter>
</
Binding>
</
TextBox.Text>
</
TextBox>

 


That's looks much better, isn't it? But how to evaluate validation, while you are in completely different class. How to know where to do it and create "on-the-fly" binding from code. Look here


    public static class MinMaxValidator
{
public static int GetMinimum(DependencyObject obj)
{
return (int)obj.GetValue(MinimumProperty);
}

public static void SetMinimum(DependencyObject obj, int value)
{
obj.SetValue(MinimumProperty, value);
}

public static readonly DependencyProperty MinimumProperty =
DependencyProperty.RegisterAttached("Minimum", typeof(int), typeof(MinMaxValidator), new UIPropertyMetadata(int.MinValue,OnAttachedPropertyChanged));

public static int GetMaximum(DependencyObject obj)
{
return (int)obj.GetValue(MaximumProperty);
}

public static void SetMaximum(DependencyObject obj, int value)
{
obj.SetValue(MaximumProperty, value);
}

public static readonly DependencyProperty MaximumProperty =
DependencyProperty.RegisterAttached("Maximum", typeof(int), typeof(MinMaxValidator), new UIPropertyMetadata(int.MaxValue,OnAttachedPropertyChanged));


static void OnAttachedPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
TextBox tb = obj as TextBox;
if (tb != null)
{
if (!tb.IsInitialized)
{
EventHandler callback = null;
callback =
delegate
{
tb.Initialized -= callback;
Validate(tb);
};
tb.Initialized += callback;
}
}
}

static void Validate(TextBox tb)
{
BindingExpression exp = tb.GetBindingExpression(TextBox.TextProperty);
if (exp != null && exp.ParentBinding != null)
{
MinMaxValidationRule myRule = null;
foreach (ValidationRule rule in exp.ParentBinding.ValidationRules)
{
if (rule != null && rule is MinMaxValidationRule)
{
myRule = rule
as MinMaxValidationRule;
}
}
if (myRule == null)
{
myRule =
new MinMaxValidationRule();
exp.ParentBinding.ValidationRules.Add(myRule);
}

myRule.ValidRange =
new Int32RangeChecker();
Binding minBinding = new Binding();
minBinding.Source = tb;
minBinding.Path =
new PropertyPath(MinMaxValidator.MinimumProperty);

myRule.ValidRange.Maximum = (
int)tb.GetValue(MinMaxValidator.MaximumProperty);
myRule.Validate(tb.Text, System.Globalization.
CultureInfo.CurrentCulture);
}
}
}

 


Looks scary, don't it? No fear. That's really simple. The only place to look deeper is Validate and OnPropertyChange method. What we're doing there?


First of all, we have to be sure, that the property attached to textbox. Then we should check if the textbox initialized. Actually, binding and property attachment occures before initialization, so we have to subscribe to initialized event and run our code there. What we are doing in Validate method?


The target is get updated values and assign to the validation. It can be done by databinding or by regular setter. Now, let's take a binding expression from the data source, my properties were attached to. Next, we have to tear it's parent binding and find the validatorule from validator rules collection, that we need. If this one not found, we'll just create new and assign values to its range.


That's all, folks. We can bind range (or any other properties) of our validator and be quiet about triggers, data freshments and other nasty things, we suffer before Windows Presentation Foundation.

1 comment:

Krzysiek said...

Hi,
Thanks for the really nice example, but I don't understand how did you manage to connect ValidationRule parameter to control/window data context. As you wrote - it's not possible to do it directly because the ValidationRule parameter does not exist in logical tree. You have used StaticResource to solve that, but in your code there is no declaration of dataSource resource used in the example:

Minimum="{Binding Source={StaticResource dataSource}, Path=MinValue}"

If you declare this object in control resources, it still won't be included in logical tree and won't be able to bind to DataContext values.

Can you clarify how did you declared dataSource and bound it to control's data context?

Cheers