Data validation error text appears inside TemplatedControl, but inner TextBox does not get a border

Summary

A custom TemplatedControl in Avalonia failed to render visual error states (specifically the red border) despite correctly capturing and propagating validation errors. While the error text was visible via the DataValidationErrors container in the template, the inner TextBox lacked the necessary pseudoclass state to trigger the theme’s error styling. This resulted in a broken UI where users received textual feedback but lost the immediate, high-contrast visual cue provided by the border.

Root Cause

The issue stems from a decoupling of the validation state between the custom control and its internal template parts.

  • State Mismatch: The developer manually called DataValidationErrors.SetError(this, error) on the PathBox itself. This correctly applied the :error pseudoclass to the PathBox control.
  • Template Isolation: The TextBox inside the ControlTemplate is a separate logical entity. While it is wrapped in a DataValidationErrors container, it is binding its text to the TemplatedParent.
  • Property Target Discrepancy: Validation errors are being attached to the PathBox instance, but the TextBox expects the validation error to be attached to its own Text property or its own instance to trigger the internal visual state changes (like the red border).
  • Pseudoclass Propagation: In Avalonia, visual states like :error do not automatically “tunnel” from a parent TemplatedControl down to the specific properties of an internal TextBox unless the binding and the error container are explicitly aligned.

Why This Happens in Real Systems

In complex UI frameworks, there is a fundamental distinction between the Logical Tree and the Visual Tree.

  • Encapsulation Overreach: When building “wrapper” controls, engineers often try to manage the state of the wrapper to provide a unified API. However, the styling engine often looks for errors on the leaf nodes (the actual input elements) rather than the container.
  • Binding Complexity: In a TemplatedControl, RelativeSource TemplatedParent creates a bridge. If the error is set on the parent, the parent knows it is in an error state, but the child (the TextBox) only knows that its source has an error—it doesn’t necessarily inherit the visual pseudoclass required by the XAML theme.

Real-World Impact

  • Reduced Accessibility: Users with visual impairments or those operating in high-glare environments rely on color-coded borders to identify input errors quickly.
  • UX Friction: Textual error messages appearing without the accompanying border look like “glitches” or unstyled text, leading to a perception of a low-quality or broken application.
  • Increased Debugging Time: Since the error is present in the DevTools (the pseudoclass exists), developers often waste hours looking at CSS/XAML styling instead of the property inheritance logic.

Example or Code

The following demonstrates the correct way to ensure the inner TextBox acknowledges the validation state by ensuring the DataValidationErrors container is correctly positioned and the error is propagated to the element responsible for the visual state.

using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;

namespace Dashavalanche.UI.Controls;

public class PathBox : TemplatedControl
{
    public static readonly DirectProperty PathProperty =
        AvaloniaProperty.RegisterDirect(
            nameof(Path), 
            o => o.Path, 
            (o, v) => o.Path = v, 
            defaultBindingMode: BindingMode.TwoWay, 
            enableDataValidation: true);

    private string? _path;
    public string? Path
    {
        get => _path;
        set => SetAndRaise(PathProperty, ref _path, value);
    }

    public static readonly StyledProperty WatermarkProperty =
        AvaloniaProperty.Register(nameof(Watermark));

    public string? Watermark
    {
        get => GetValue(WatermarkProperty);
        set => SetValue(WatermarkProperty, value);
    }

    protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
    {
        base.UpdateDataValidation(property, state, error);

        // To fix the visual border, we ensure the error is set on the control 
        // so the DataValidationErrors container in the template can pick it up.
        if (property == PathProperty)
        {
            DataValidationErrors.SetError(this, error);
        }
    }
}

  
    
      
      
        
      
    
  

How Senior Engineers Fix It

  1. Identify the Visual Target: Determine exactly which element in the visual tree is responsible for drawing the border. In Avalonia, this is usually the element that reacts to the :error pseudoclass.
  2. Align Error Attachment: Ensure that DataValidationErrors.SetError is called on the object that the DataValidationErrors container in the template is actually monitoring.
  3. Use TemplatePart Patterns: Instead of relying solely on TemplatedParent bindings for everything, senior engineers often use OnApplyTemplate to find PART_ elements and manually synchronize states if the standard binding engine fails to propagate visual pseudoclasses.
  4. Verify Theme Consistency: Check if the custom control’s style is actually triggering the DataValidationErrors style defined in the default theme.

Why Juniors Miss It

  • Focusing on Data, Not Visuals: Juniors often see the error text appearing and assume the “validation works,” ignoring the fact that the visual affordance (the border) is missing.
  • Misunderstanding Pseudoclasses: There is a common misconception that if a parent has a state (like :error), all children automatically inherit that visual state. In modern retained-mode UI frameworks, visual states are local to the element.
  • Over-reliance on DevTools: A junior might see the :error class in the DevTools on the PathBox and conclude “the code is working,” failing to realize that the TextBox (the part actually responsible for the border) is missing the state.

Leave a Comment