1

I try to create a textbox which have multi level validation, so notify the user if there is some fields which should be filled but not required. I created a control, which is works really good, I only have one problem. I want to use this new textbox in an other usercontrol. I have a save button there, and I want this button to be enabled when this textbox has no validation error, but it seems the validation error is inside my custom texbox control, so the button is always enabled. Here is the control xaml code:

MultiLevelValidationTextBox.xaml

<Style x:Key="MultiLevelValidationTextBoxStyle"  TargetType="local:MultiLevelValidationTextBoxControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MultiLevelValidationTextBoxControl">
                <Grid>
                    <Grid.Resources>
                        <local:IsNullConverter x:Key="IsNullConverter" />
                    </Grid.Resources>
                    <TextBox
                        x:Name="PART_TextBox"
                        Text="{Binding Path=BindingText, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay}">
                        <TextBox.Style>
                            <Style TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
                                <Style.Triggers>
                                    <MultiDataTrigger>
                                        <MultiDataTrigger.Conditions>
                                            <Condition Binding="{Binding Path=Recommended, RelativeSource={RelativeSource Mode=TemplatedParent}}" Value="True"/>
                                            <Condition Binding="{Binding Path=BindingText, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource IsNullConverter}}" Value="True"/>
                                        </MultiDataTrigger.Conditions>
                                        <Setter Property="BorderBrush" Value="#fcba03" />
                                    </MultiDataTrigger>
                                    <DataTrigger Binding="{Binding Path=Required, RelativeSource={RelativeSource Mode=TemplatedParent}}" Value="True">
                                        <Setter Property="Text">
                                            <Setter.Value>
                                                <Binding Path="BindingText" RelativeSource="{RelativeSource Mode=TemplatedParent}" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
                                                    <Binding.ValidationRules>
                                                        <local:TextBoxTextValidation ValidatesOnTargetUpdated="True"/>
                                                    </Binding.ValidationRules>
                                                </Binding>
                                            </Setter.Value>
                                        </Setter>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </TextBox.Style>
                    </TextBox>
                    <Polygon x:Name="PART_Polygon" Points="0,0 5,0 0,5 0,0" Margin="0,3,2,0" HorizontalAlignment="Right" FlowDirection="RightToLeft" ToolTip="A mező kitöltése ajánlott!">
                        <Polygon.Style>
                            <Style TargetType="Polygon">
                                <Style.Triggers>
                                    <MultiDataTrigger>
                                        <MultiDataTrigger.Conditions>
                                            <Condition Binding="{Binding Path=Recommended, RelativeSource={RelativeSource Mode=TemplatedParent}}" Value="True"/>
                                            <Condition Binding="{Binding Path=BindingText, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource IsNullConverter}}" Value="True"/>
                                        </MultiDataTrigger.Conditions>
                                        <Setter Property="Fill" Value="#fcba03" />
                                    </MultiDataTrigger>
                                    <MultiDataTrigger>
                                        <MultiDataTrigger.Conditions>
                                            <Condition Binding="{Binding Path=Recommended, RelativeSource={RelativeSource Mode=TemplatedParent}}" Value="True"/>
                                            <Condition Binding="{Binding Path=BindingText, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource IsNullConverter}}" Value="False"/>
                                        </MultiDataTrigger.Conditions>
                                        <Setter Property="Fill" Value="Transparent" />
                                    </MultiDataTrigger>
                                    <MultiDataTrigger>
                                        <MultiDataTrigger.Conditions>
                                            <Condition Binding="{Binding Path=Recommended, RelativeSource={RelativeSource Mode=TemplatedParent}}" Value="False"/>
                                        </MultiDataTrigger.Conditions>
                                        <Setter Property="Fill" Value="Transparent" />
                                    </MultiDataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Polygon.Style>
                    </Polygon>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And here is the MultiLevelValidationTextBoxControl.cs

    [TemplatePart(Name = PART_TextBox, Type = typeof(TextBox))]
[TemplatePart(Name = PART_Polygon, Type = typeof(Polygon))]
public class MultiLevelValidationTextBoxControl : TextBox
{

    private const string PART_TextBox = "PART_TextBox";
    private const string PART_Polygon = "PART_Polygon";

    private TextBox _textBox = null;
    private Polygon _polygon = null;

    static MultiLevelValidationTextBoxControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiLevelValidationTextBoxControl), new FrameworkPropertyMetadata(typeof(MultiLevelValidationTextBoxControl)));
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        _textBox = GetTemplateChild(PART_TextBox) as TextBox;
        _polygon = GetTemplateChild(PART_Polygon) as Polygon;
    }

    public static readonly DependencyProperty RequiredProperty = DependencyProperty.Register("Required", typeof(bool), typeof(MultiLevelValidationTextBoxControl), new UIPropertyMetadata(false));

    public bool Required
    {
        get
        {
            return (bool)GetValue(RequiredProperty);
        }
        set
        {
            SetValue(RequiredProperty, value);
        }
    }

    public static readonly DependencyProperty RecommendedProperty = DependencyProperty.Register("Recommended", typeof(bool), typeof(MultiLevelValidationTextBoxControl), new UIPropertyMetadata(false));

    public bool Recommended
    {
        get
        {
            return (bool)GetValue(RecommendedProperty);
        }
        set
        {
            SetValue(RecommendedProperty, value);
        }
    }

    public static readonly DependencyProperty BindingTextProperty = DependencyProperty.Register("BindingText", typeof(string), typeof(MultiLevelValidationTextBoxControl), new UIPropertyMetadata(string.Empty));

    public string BindingText
    {
        get
        {
            return (string)GetValue(BindingTextProperty);
        }
        set
        {
            SetValue(BindingTextProperty, value);
        }
    }

    public static readonly DependencyProperty LabelTextProperty = DependencyProperty.Register("LabelText", typeof(string), typeof(MultiLevelValidationTextBoxControl), new UIPropertyMetadata(string.Empty));

    public string LabelText
    {
        get
        {
            return (string)GetValue(LabelTextProperty);
        }
        set
        {
            SetValue(LabelTextProperty, value);
        }
    }
}

And I have an other window, where I want to use this, and there is a button which should be disabled if the texbox is required:

<Button
                         Grid.Row="1"
            Grid.Column="6"
            Margin="10,0,0,0" 
            MinWidth="80" 
            Height="30">

So is there any way to pass the validation errors to the parent? Everything is works fine, only this part fails.

                    <Button.Style>
                        <Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}">
                            <Setter Property="IsEnabled" Value="False"/>
                            <Style.Triggers>
                                <MultiDataTrigger>
                                    <MultiDataTrigger.Conditions>
                                        <Condition Binding="{Binding ElementName=CustomTextBoxName, Path=(Validation.HasError), UpdateSourceTrigger=PropertyChanged}" Value="False" />
                                    </MultiDataTrigger.Conditions>
                                    <Setter Property="IsEnabled" Value="True"/>
                                </MultiDataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Button.Style>

                    Save
                </Button>
KoB81
  • 47
  • 7
  • what kind of validation ? – Sats Feb 01 '20 at 08:58
  • At first glance you are doing something wrong. Why does your TextBox style has a `TextBox` in its `ControlTemplate`? What are you trying to do? – BionicCode Feb 01 '20 at 14:49
  • @BionicCode I have a form, and there are lot of fields, textboxes, comboboxes etc. And I want to mark required and recommended fields. Required fields is easy, because I can use validation. But there are some fields which is only recommended, but the user don't have to fill it. And there will be an admin, who can set which fields are required and which is only recommended. So I can't create some fields whit required style, and some with recommended style, because the admin can dynamically change that the field is required, recommended or neither. – KoB81 Feb 01 '20 at 16:31
  • How do you know which fields are marked as required by an admin? – BionicCode Feb 02 '20 at 13:39

1 Answers1

2

First of all your code is too complicated and therefore difficult to understand. You don't have to put a TextBox inside the ControlTemplate of a TextBox. That's absurd. If you don't know how a control's tree is designed, always take a look at Microsoft Docs: Control Styles and Templates and lookup the required control.
In your case it's the TextBox Styles and Templates. Here you can learn that the content of a TextBox is hosted within a ScrollViewer, which is actually a ContentControl that offers content scrolling:

This is a reduced ControlTemplate of a TextBox (visit the previous link to see the full code):

<Style TargetType="{x:Type TextBox}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type TextBoxBase}">
        <Border Name="Border"
                CornerRadius="2"
                Padding="2"
                BorderThickness="1">
          <VisualStateManager.VisualStateGroups>
            ...
          </VisualStateManager.VisualStateGroups>

          <!-- The host of the TextBox's content -->
          <ScrollViewer Margin="0"
                        x:Name="PART_ContentHost" />
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

But since WPF provides out-of-the-box data validation and error feedback, you don't need to override the default ControlTemplate. Since your custom MultiLevelValidationTextBoxControl doesn't add any additional features, you can go with the plain vanilla TextBox.

This is how to implement simple data validation using Binding Validation
and visual error feedback by using the attached property Validation.ErrorTemplate:

The TextBox definition

<TextBox Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
         Style="{StaticResource TextBoxStyle}">
  <TextBox.Text>
    <Binding Path="BindingText" 
             RelativeSource="{RelativeSource TemplatedParent}"
             UpdateSourceTrigger="PropertyChanged">
      <Binding.ValidationRules>
        <local:TextBoxTextValidation ValidatesOnTargetUpdated="True"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

The ControlTemplate that is shown when the validation fails

<ControlTemplate x:Key="ValidationErrorTemplate">
  <DockPanel>
    <TextBlock Text="!"
               Foreground="#FCBA03" 
               FontSize="20" />
    <Border BorderThickness="2" 
            BorderBrush="#FCBA03"
            VerticalAlignment="Top">
      <AdornedElementPlaceholder/>
    </Border>
  </DockPanel>
</ControlTemplate>

The Styleto add additional behavior e.g. showing error ToolTip

<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" Value="true">
      <Setter Property="ToolTip"
        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors)/ErrorContent}"/>
    </Trigger>
  </Style.Triggers>
</Style>

To bind this TextBox validation errors to a different control (in case of using Binding Validation) simply go:

<StackPanel>

  <!-- Button will be disabled when the TextBox has validation errors -->
  <Button Content="Press Me" Height="40">
    <Button.Style>
      <Style TargetType="Button">
        <Style.Triggers>
          <DataTrigger Binding="{Binding ElementName=ValidatedTextBox, Path=(Validation.HasError)}" 
                       Value="True">
            <Setter Property="IsEnabled" Value="False" />
          </DataTrigger>
        </Style.Triggers>
      </Style>
    </Button.Style>
  </Button>
  <TextBox x:Name="ValidatedTextBox" 
           Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
           Style="{StaticResource TextBoxStyle}">
      <TextBox.Text>
        <Binding Path="BindingText" 
                 RelativeSource="{RelativeSource TemplatedParent}"
                 UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
            <local:TextBoxTextValidation ValidatesOnTargetUpdated="True"/>
          </Binding.ValidationRules>
        </Binding>
      </TextBox.Text>
    </TextBox>
</StackPanel>

This is a working example. You just have to extend it by adding the conditional validation based on some Required property. Just add a trigger to the TextBoxStyle. No need to derive from TextBox and create a custom control.


Remarks

I highly recommend to implement data validation in a view model by implementing INotifyDataErrorInfo (example). This adds more flexibility and easier UI design as binding paths will be simplified and UI bloat removed. It also enforces encapsulation (improves design) and enables unit testing of the complete validation logic.

BionicCode
  • 1
  • 4
  • 28
  • 44