Show WPF validation error message in a fixed place

13,398

Solution 1

Thanks to http://www.scottlogic.com/blog/2008/11/28/using-bindinggroups-for-greater-control-over-input-validation.html I was able to solve this with a BindingGroup and without ValidationAdornerSite.

<Window x:Class="BindingAndValidation.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:BindingAndValidation"
        Title="Binding and Validation" Height="110" Width="425"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        >
    <Grid x:Name="RootElement">
        <Grid.Resources>
            <Style TargetType="{x:Type TextBox}">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                    </Trigger>
                </Style.Triggers>
            </Style>
        </Grid.Resources>
        <Grid.BindingGroup>
            <BindingGroup Name="LocalBindingGroup">
                <BindingGroup.ValidationRules>
                    <local:RuleGroup />
                </BindingGroup.ValidationRules>
            </BindingGroup>
        </Grid.BindingGroup>
        <TextBox 
            HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" LostFocus="TextBox_LostFocus">
            <TextBox.Text>
                <Binding>
                    <Binding.Path>Box1</Binding.Path>
                    <Binding.BindingGroupName>LocalBindingGroup</Binding.BindingGroupName>
                    <Binding.ValidationRules>
                        <local:RuleA />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBox 
            HorizontalAlignment="Left" Height="23" Margin="10,38,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
            <TextBox.Text>
                <Binding>
                    <Binding.Path>Box2</Binding.Path>
                    <Binding.BindingGroupName>LocalBindingGroup</Binding.BindingGroupName>
                    <Binding.ValidationRules>
                        <local:RuleA />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <ItemsControl ItemsSource="{Binding Path=(Validation.Errors), ElementName=RootElement}" MinWidth="100" MinHeight="16" Margin="230,10,0,0" Background="AntiqueWhite" Foreground="Red">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Label Foreground="Red" Content="{Binding Path=ErrorContent}"/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Content="Button" HorizontalAlignment="Left" Margin="150,0,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click" />
    </Grid>
</Window>

The validation only occurs when you call CommitEdit. If you want to have it immediatly like I wished here you can use LostFocus

    private void TextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        this.RootElement.BindingGroup.CommitEdit();
    }

Of course for a greater project an attached property might help.

Solution 2

You can use a BindingGroup in combination with the Validation.ValidationAdornerSite and Validation.ValidationAdornerSiteFor properties.

This blog post shows you an example of how to do this.


<StackPanel x:Name="FormRoot"
            Validation.ValidationAdornerSite="{Binding ElementName=ErrorDisplay}">
  <FrameworkElement.BindingGroup>
    <BindingGroup Name="FormBindingGroup" />
  </FrameworkElement.BindingGroup>

  <TextBox>
    <TextBox.Text>
      <Binding BindingGroupName="FormBindingGroup"
               UpdateSourceTrigger="LostFocus"
               Path="Box1">
        <Binding.ValidationRules>
          <l:RuleA />
        </Binding.ValidationRules>
      </Binding>
    </TextBox.Text>
  </TextBox>

<TextBox>
    <TextBox.Text>
      <Binding BindingGroupName="FormBindingGroup"
               UpdateSourceTrigger="LostFocus"
               Path="Box2">
        <Binding.ValidationRules>
          <l:RuleA />
        </Binding.ValidationRules>
      </Binding>
    </TextBox.Text>
  </TextBox>

  <ItemsControl x:Name="ErrorDisplay"
                Background="AntiqueWhite"
                Foreground="Red"
                ItemsSource="{Binding RelativeSource={RelativeSource Self},
                                      Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)}"
                DisplayMemberPath="ErrorContent" />
</StackPanel>

To commit the values as the user types, change the UpdateSourceTrigger values to PropertyChanged. Note that it isn't strictly necessary to use ValidationAdornerSite here; you could simply point the ErrorDisplay binding directly to the owner of the BindingGroup:

ItemsSource="{Binding ElementName=FormRoot, Path=(Validation.Errors)}"
Share:
13,398
ZoolWay
Author by

ZoolWay

Academic grade in business computer science (Dipl. Wirts.-Informatiker (FH)), more than ten years work experience in web and windows development, .NET framework and other programming languages.

Updated on June 06, 2022

Comments

  • ZoolWay
    ZoolWay almost 2 years

    I got WPF validation running (added ValidationRules to the binding) and with the template I can create nice adorners. There are many posting out there.

    But I cannot find a way to display the error message outside of the adorned control in a fixed place like a TextBlock in a corner of the window e.g.

    How could I achieve this? Can I bind all of my validation error messages to my DataContext (ViewModel here)?


    Update: Thanks to an answer I got it partly working. The validation messages are now displayed in another label. As all the textboxes with their validation rules are created on the fly by code, the binding to do so is done this way:

    Binding bindSite = new Binding();
    bindSite.Source = this.validationErrorDisplayLabel;
    BindingOperations.SetBinding(textBox, Validation.ValidationAdornerSiteProperty, bindSite);
    

    But the validation messages are only forwarded to the adornersite for the last textbox for which this code was executed.


    I reproduced the problem in this small example.

    XAML:

    <Grid>
        <TextBox 
            Validation.ValidationAdornerSite="{Binding ElementName=ErrorDisplay}"
            HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
            <TextBox.Text>
                <Binding>
                    <Binding.Path>Box1</Binding.Path>
                    <Binding.ValidationRules>
                        <local:RuleA />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBox 
            Validation.ValidationAdornerSite="{Binding ElementName=ErrorDisplay}"
            HorizontalAlignment="Left" Height="23" Margin="10,38,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
            <TextBox.Text>
                <Binding>
                    <Binding.Path>Box2</Binding.Path>
                    <Binding.ValidationRules>
                        <local:RuleA />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBlock 
            x:Name="ErrorDisplay"
            Background="AntiqueWhite"
            Foreground="Red"
            Text="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)[0].ErrorContent}"
            HorizontalAlignment="Left" Margin="230,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" RenderTransformOrigin="2.218,-4.577" Width="177" Height="51"/>
    </Grid>
    

    The class RuleA produces a validation error when the value equals the string "A". The errors in the 2nd textbox are displayed in the TextBlock, the errors of the first one not (instead it uses the default template and gets a red border).

    How can it work for both? The textblock does not need to sum up all errors but display the very first error.