Massimo
Massimo

Reputation: 702

WPF Validation depending on Required/Not required field

I'm new to WPF's developing but I was thinking about how to kill 3 birds with one stone. Example: I've a form with 2 TextBox and 2 TextBlocks. The first 'bird' would be to be able to "enrich" some textblock with an asterisk if they refer to required fields:

<TextBlock Grid.Row="0" Grid.Column="0" Text="Age" customProperty="Required" /> <TextBlock Grid.Row="1" Grid.Column="0" Text="Foot Size/>

Then the TextBlocks would show their text differently, the first will have an asterisk, while the one with no customproperty defined would have not.

The second bird would be to have some kind of validation on the value of the textbox, which If I understood correctly is done by using a CustomValidationRule, for which I implemented a class:

class AgeController: ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (value == null)
            return new ValidationResult(false, "Null value");

        int temp = 1;
        Boolean noIllegalChars = int.TryParse(value.ToString(), out temp);
        if (temp >= 1)
            return new ValidationResult(true, null);
        else
            return new ValidationResult(false, "Correggi");
    }
}

By adding this to the textBlox XAML code:

<TextBox.Text>
     <Binding Path="blabla" UpdateSourceTrigger="PropertyChanged"  ValidatesOnDataErrors="True">
         <Binding.ValidationRules>
              <local:AgeController ValidationStep="RawProposedValue" />
         </Binding.ValidationRules>
     </Binding>
</TextBox.Text>

And this works BUT the validation process should be different for required and not required fields: if it's required a blank input is not valid but if it's optional a blank field is OK. How do I achieve this without specifying two different ValidationRule while referencing the textblock linked to the textbox?

/tldr: I'm trying to find a way to enrich a textblock with an attribute that adds a style to its text (asterisk or whatever the client wants, I modify how the enrichment modifies the text in just one place), the textbox's validation referring to the enriched textblock would then behave differently based on the value of the enrichment.

I hope I didn't mess up the explanation.

Upvotes: 16

Views: 20464

Answers (4)

Emmanuel DURIN
Emmanuel DURIN

Reputation: 4913

1. TextBlock has no ControlTemplate property. So the (*) required can't be added to TextBlock

Label has a controltemplate and can give focus to an input field. Let's use it.

Usage of Target property for passing focus to TextBox when Alt+F is pressed:

<!-- Prefixing Firstname with _ allows the user to give focus
     to the textbox (Target) by pressing Alt + F-->

    <local:LabelWithRequiredInfo  Content="_Firstname" 
                                  IsRequired="false" 
                                  Target="{Binding ElementName=textboxFirstname,
                                  Mode=OneWay}" ... />

Creation of a sub class of Label : LabelWithRequiredInfo, so a IsRequired property can be added.
(Use VS Add New Item/WPF Custom Control).

2. Creation of IsRequired dependency property to the control so binding will work - we need it !

public class LabelWithRequiredInfo : Label
{
    public bool IsRequired
    {
        get { return (bool)GetValue(IsRequiredProperty); }
        set { SetValue(IsRequiredProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsRequired.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsRequiredProperty =
        DependencyProperty.Register("IsRequired", typeof(bool), typeof(LabelWithRequiredInfo), new PropertyMetadata(false));
    static LabelWithRequiredInfo()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LabelWithRequiredInfo), new FrameworkPropertyMetadata(typeof(LabelWithRequiredInfo)));
    }
}

3. Let's fill the LabelWithRequiredInfo template in Themes\Generic.xaml

(But the template is first designed in MainWindow.xaml rigth clicking on a label/Edit template/Copy - so it can visualized - then the template content is copied in Generic.xaml)

<Style TargetType="{x:Type local:LabelWithRequiredInfo}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:LabelWithRequiredInfo}">
                <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="true">
                    <!-- A grid has been added to the template content to have multiple content.  -->
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="30"/>
                        </Grid.ColumnDefinitions>
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        <!-- The Visibility  property has to be converted because it's not a bool but has a Visibility type
                             The converter (pretty classical) can be found in the attached solution, and is declared in the resource section
                             The binding is made on a property of the component : IsRequired
                        -->
                        <TextBlock  Text="(*)" 
                                    Visibility="{TemplateBinding IsRequired,Converter={StaticResource booleanToVisibilityConverter}}"
                                    Foreground="Red"
                                    Grid.Column="1"
                                    Margin="5 0"/>
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>

            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

4. Declaration of the converter in Generic.xaml :

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TextboxRequiredMandatoryInput">
    <local:BooleanToVisibilityConverter  x:Key="booleanToVisibilityConverter"/>

5. Declaration of a ValidationRule taking into account IsRequired behavior :

class RequiredValidationRule : ValidationRule
{
    public bool IsRequired { get; set; }
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        var content = value as String;
        if (content != null)
        {
            if (IsRequired && String.IsNullOrWhiteSpace(content))
                return new ValidationResult(false, "Required content");
        }
        return ValidationResult.ValidResult;
    }
}

6. And use it in your binding as you found :

<TextBox x:Name="textboxFirstname" HorizontalAlignment="Left" Height="23" Margin="236,94,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
    <TextBox.Text>
        <Binding Path="Firstname" UpdateSourceTrigger="PropertyChanged"  ValidatesOnDataErrors="True">
            <Binding.ValidationRules>
                <local:RequiredValidationRule IsRequired="true" ValidationStep="RawProposedValue" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

You will find the complete solution here :

http://1drv.ms/1igpsyb

Upvotes: 10

Emmanuel DURIN
Emmanuel DURIN

Reputation: 4913

Here is a second answer that is not exactly Massimo's question.

I did it, thinking it could be easier to use for the XAML designer

The goal is to have a Label (subclass actually to have the required red symbol (*)) easier to use.

It gives focus to a TexBlock thanks to the usual Target property

<local:LabelWithRequiredInfo  Content="_Firstname" 
                              Target="{Binding ElementName=textboxFirstname, Mode=OneWay}" 
                              ... />  

And because there is a target, the LabelWithRequiredInfo can check for the presence of the RequiredValidationRule in the TextBox.TextProperty.

So most of the time no need for a IsRequired property.

public LabelWithRequiredInfo()
{
    var dpd = DependencyPropertyDescriptor.FromProperty(Label.TargetProperty, typeof(Label));
    dpd.AddValueChanged(this, SearchForRequiredValidationRule);
}
private void SearchForRequiredValidationRule(object sender, EventArgs e)
{
    var textbox = Target as TextBox;
    if (textbox != null)
    {
        Binding binding = BindingOperations.GetBinding(textbox, TextBox.TextProperty);
        var requiredValidationRule = binding.ValidationRules
                                            .OfType<RequiredValidationRule>()
                                            .FirstOrDefault();
        if (requiredValidationRule != null)
        {
            // makes the required legend (red (*) for instance) to appear
            IsRequired = true;
        }
    }
}

And if a required legend must be provided on a checkbox or a combobox or whatever there is still a IsRequired property on the LabelWithRequiredInfo

<local:LabelWithRequiredInfo  Content="_I agree with the terms of contract" 
                              Target="{Binding ElementName=checkboxIAgree}"
                              IsRequired='"true"                                  
                              ... />

And it is still possible to add other validation rules on the textbox (or any control) to check for a number, regex, ...

And last bonus, set the RequiredLegend as a dependency property in the LabelWithRequiredInfo:

public Object RequiredLegend
{
    get { return (Object)GetValue(RequiredLegendProperty); }
    set { SetValue(RequiredLegendProperty, value); }
}

// Using a DependencyProperty as the backing store for RequiredLegend.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty RequiredLegendProperty =
    DependencyProperty.Register("RequiredLegend", typeof(Object), typeof(LabelWithRequiredInfo), new PropertyMetadata(null));

So that the template of LabelWithRequiredInfo can use it to display some text :

<local:LabelWithRequiredInfo RequiredLegend="(*)" ... />

Or something more XAML-ish :

<local:LabelWithRequiredInfo ... >
    <local:LabelWithRequiredInfo.RequiredLegend>
        <TextBlock Text="(*)" Foreground="Red" />
    </local:LabelWithRequiredInfo.RequiredLegend>

Just need to change the template of control in themes\Generic.xaml.

It now uses a ContentControl to display text or a control :

<Style TargetType="{x:Type local:LabelWithRequiredInfo}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:LabelWithRequiredInfo}">
                <Border ...>
                    <Grid>
                        <Grid.ColumnDefinitions ... />
                        <ContentPresenter ... />
                        **<ContentControl Content="{TemplateBinding RequiredLegend}" Visibility="{TemplateBinding IsRequired,Converter={StaticResource booleanToVisibilityConverter}}" Grid.Column="1" /> **
                    </Grid>

Here is link to full working solution : http://1drv.ms/1MxltVZ

Best coding

Upvotes: 0

tgpdyk
tgpdyk

Reputation: 1233

For re-usability and the way you described the requirement, an aggregate control may be needed. I think a UserControl + some DependencyProperty are a perfect one for this.

For my UserControl, I would have like this..

<UserControl x:Class="WpfApplication1.InputFieldControl"
         x:Name="InputUserCtrl"
         ...             >
<StackPanel x:Name="MainPanel">
    <TextBlock x:Name="Label">
        <TextBlock.Text>
            <MultiBinding StringFormat="{}{0}{1}:">
                <Binding Path="InputLabel" ElementName="InputUserCtrl"/>
                <Binding Path="RequiredStringSymbol" ElementName="InputUserCtrl"/>
            </MultiBinding>
        </TextBlock.Text>
    </TextBlock>
    <TextBox x:Name="Value" Text="{Binding DataContext, ElementName=InputUserCtrl}"/>
</StackPanel>

Then on its partial class (I have added number of properties, refer to the usage part):

public partial class InputFieldControl : UserControl
{
    // Required property
    public static readonly DependencyProperty RequiredProperty =
                   DependencyProperty.Register("Required", 
                   typeof(bool), typeof(InputFieldControl), 
                   new PropertyMetadata(true, OnRequiredChanged));
    private static void OnRequiredChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){
        InputFieldControl ctrl = d as InputFieldControl;
        // symbol is voided
        if ((bool)e.NewValue == false)
            ctrl.RequiredStringSymbol = string.Empty;
    }
    public bool Required {
        get { return (bool)GetValue(RequiredProperty); }
        set { SetValue(RequiredProperty, value); }
    }
    // Required string symbol
    public static readonly DependencyProperty RequiredStringSymbolProperty =
                  DependencyProperty.Register("RequiredStringSymbol",
                  typeof(string), typeof(InputFieldControl),
                  new PropertyMetadata("*"));
    public string RequiredStringSymbol{
        get { return (string)GetValue(RequiredStringSymbolProperty); }
        set { SetValue(RequiredStringSymbolProperty, value); }
    }
    // Input Label
    public static readonly DependencyProperty InputLabelProperty =
                  DependencyProperty.Register("InputLabel",
                  typeof(string), typeof(InputFieldControl),
                  new PropertyMetadata(string.Empty));
    public string InputLabel{
        get { return (string)GetValue(InputLabelProperty); }
        set { SetValue(InputLabelProperty, value); }
    }

And I can use the control like this:

<StackPanel>
    <customCtrl:InputFieldControl Required="True"
                                  RequiredStringSymbol="+" 
                                  InputLabel="RequiredField"/>
    <customCtrl:InputFieldControl Required="False"
                                  InputLabel="NormalField"/>
    <customCtrl:InputFieldControl Required="True"
                                  RequiredStringSymbol="*" 
                                  InputLabel="AnotherRequiredField">
    </customCtrl:InputFieldControl>
</StackPanel>

As to the validation part, I would rather use IDataErrorInfo. This can go hand in hand with your ViewModel since we can now bind the Required property.

Upvotes: 4

Andy
Andy

Reputation: 3743

First point: you can define a custom template for your controls, in which you would add the visual elements you want ( the asterisk, etc. ) . You can control their visibility using triggers. ( you can ask a separate question for more details on this)

Second/Third: You can define a boolean property IsRequired on AgeController, and you can set it to TRUE/FALSE when defining the validation:

<TextBox.Text>
     <Binding Path="blabla" UpdateSourceTrigger="PropertyChanged"  ValidatesOnDataErrors="True">
         <Binding.ValidationRules>
              <local:AgeController ValidationStep="RawProposedValue" 
                                                   IsRequired="**True**" />
                                               OR: IsRequired="**False**" />

         </Binding.ValidationRules>
     </Binding>
</TextBox.Text>

Then, this value would be available to you when implementing the validation:

public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (IsRequired)
        {
           ...
        }
        else
        {
           ...
        }
    }

Upvotes: 3

Related Questions