web-dev-qa-db-fra.com

Validation WPF en fonction du champ Required/Not required

Je suis nouveau dans le développement de WPF mais je pensais à la façon de tuer 3 oiseaux avec une pierre . Exemple: j'ai un formulaire avec 2 TextBox et 2 TextBlocks . Le premier "oiseau" serait capable "d'enrichir" un bloc de texte avec un astérisque s'ils font référence à des champs obligatoires:

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

Ensuite, les TextBlocks afficheront leur texte différemment, le premier aura un astérisque, tandis que celui sans propriété personnalisée définie ne l’aura pas.

Le deuxième oiseau serait d'avoir une sorte de validation sur la valeur de la zone de texte, ce qui, si j'ai bien compris, est fait en utilisant un CustomValidationRule, pour lequel j'ai implémenté une classe:

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");
    }
}

En ajoutant ceci au code XAML de textBlox:

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

Et cela fonctionne MAIS le processus de validation devrait être différent pour les champs obligatoires et non obligatoires: si c'est obligatoire, une entrée vide n'est pas valide, mais si elle est facultative, un champ vide est OK . référençant le bloc de texte lié à la zone de texte?

/ tldr: J'essaie de trouver un moyen d'enrichir un bloc de texte avec un attribut qui ajoute un style à son texte (astérisque ou tout ce que le client veut, je modifie la manière dont l'enrichissement modifie le texte à un seul endroit), la validation de la zone de texte se référer au bloc de texte enrichi se comporterait alors différemment en fonction de la valeur de l'enrichissement.

J'espère que je n'ai pas gâché l'explication.

16
Massimo

1. TextBlock n'a pas de propriété ControlTemplate. Donc, le (*) requis ne peut pas être ajouté à TextBlock 

Label a un modèle de contrôle et peut donner le focus à un champ de saisie. Utilisons-le. 

Utilisation de la propriété Target pour passer le focus à TextBox lorsque les touches Alt + F sont enfoncées: 

<!-- 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}" ... />

Création d'une sous-classe de Label: LabelWithRequiredInfo, permettant d'ajouter une propriété IsRequired.
(Utilisez VS Ajouter un nouvel élément/Contrôle personnalisé WPF).

2. Création de la propriété de dépendance IsRequired sur le contrôle afin que la liaison fonctionne - nous en avons besoin!

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. Remplissons le modèle LabelWithRequiredInfo dans Themes\Generic.xaml 

(Mais le modèle est d'abord conçu dans MainWindow.xaml en cliquant sur une étiquette/Modifier le modèle/Copier - afin qu'il puisse être visualisé - puis le contenu du modèle est copié dans 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. Déclaration du convertisseur dans 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. Déclaration d'une règle de validation tenant compte du comportement IsRequired: 

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. Et utilisez-le dans votre reliure comme vous l'avez trouvé:

<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>

Vous trouverez la solution complète ici: 

http://1drv.ms/1igpsyb

9
Emmanuel DURIN

Pour permettre la réutilisation et la manière dont vous avez décrit l'exigence, un contrôle global peut être nécessaire. Je pense qu'un UserControl + certains DependencyProperty sont parfaits pour cela.

Pour mon UserControl, j'aurais aimé ceci ..

<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>

Puis sur sa classe partielle (j'ai ajouté un nombre de propriétés, voir la partie utilisation):

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); }
    }

Et je peux utiliser le contrôle comme ceci:

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

En ce qui concerne la validation, je préférerais utiliser IDataErrorInfo. Cela peut aller de pair avec votre ViewModel puisque nous pouvons maintenant lier la propriété Required.

4
tgpdyk

Premier point : vous pouvez définir un modèle personnalisé pour vos contrôles, dans lequel vous ajouteriez les éléments visuels souhaités (l'astérisque, etc.). Vous pouvez contrôler leur visibilité à l'aide de déclencheurs. (vous pouvez poser une question séparée pour plus de détails à ce sujet)

Second/Third : Vous pouvez définir une propriété booléenne IsRequired sur AgeController et la définir sur TRUE/FALSE lors de la définition de la 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>

Ensuite, cette valeur sera disponible lors de la mise en œuvre de la validation:

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

Voici une deuxième réponse qui n’est pas exactement la question de Massimo. 

Je l’ai fait, pensant que cela pourrait être plus facile à utiliser pour le concepteur XAML 

Le but est d’avoir une étiquette (la sous-classe ayant en fait le symbole rouge requis (*)) plus facile à utiliser. 

Il donne le focus à un TexBlock grâce à la propriété habituelle Target 

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

Et puisqu'il y a une cible, LabelWithRequiredInfo peut vérifier la présence de RequiredValidationRule dans TextBox.TextProperty

Donc, la plupart du temps, pas besoin d'une propriété IsRequired. 

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;
        }
    }
}

Et si une légende requise doit être fournie dans une case à cocher ou dans une liste déroulante ou dans un autre élément, il existe toujours une propriété IsRequired sur le LabelWithRequiredInfo

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

Et il est toujours possible d'ajouter d'autres règles de validation sur la zone de texte (ou sur n'importe quel contrôle) pour rechercher un nombre, une expression régulière, ...

Et dernier bonus, définissez RequiredLegend en tant que propriété de dépendance dans 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));

Pour que le modèle de LabelWithRequiredInfo puisse l’utiliser pour afficher du texte: 

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

Ou quelque chose de plus XAML-ish:

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

Il suffit de changer le modèle de contrôle dans themes\Generic.xaml

Il utilise maintenant un ContentControl pour afficher du texte ou un contrôle: 

<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>

Voici le lien vers la solution de travail complète: http://1drv.ms/1MxltVZ

Meilleur codage

0
Emmanuel DURIN