Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 769
Massimo Avatar asked Oct 21 '15 09:10

Massimo


2 Answers

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

like image 57
Emmanuel DURIN Avatar answered Nov 10 '22 06:11

Emmanuel DURIN


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.

like image 4
tgpdyk Avatar answered Nov 10 '22 06:11

tgpdyk