KoB81
KoB81

Reputation: 47

C# wpf - How to pass textbox error to parent in custom control

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>

Upvotes: 1

Views: 1214

Answers (1)

BionicCode
BionicCode

Reputation: 28988

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.

Upvotes: 2

Related Questions