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 thePathBoxitself. This correctly applied the:errorpseudoclass to thePathBoxcontrol. - Template Isolation: The
TextBoxinside theControlTemplateis a separate logical entity. While it is wrapped in aDataValidationErrorscontainer, it is binding its text to theTemplatedParent. - Property Target Discrepancy: Validation errors are being attached to the
PathBoxinstance, but theTextBoxexpects the validation error to be attached to its ownTextproperty or its own instance to trigger the internal visual state changes (like the red border). - Pseudoclass Propagation: In Avalonia, visual states like
:errordo not automatically “tunnel” from a parentTemplatedControldown to the specific properties of an internalTextBoxunless 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 TemplatedParentcreates a bridge. If the error is set on the parent, the parent knows it is in an error state, but the child (theTextBox) 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
- 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
:errorpseudoclass. - Align Error Attachment: Ensure that
DataValidationErrors.SetErroris called on the object that theDataValidationErrorscontainer in the template is actually monitoring. - Use TemplatePart Patterns: Instead of relying solely on
TemplatedParentbindings for everything, senior engineers often useOnApplyTemplateto findPART_elements and manually synchronize states if the standard binding engine fails to propagate visual pseudoclasses. - Verify Theme Consistency: Check if the custom control’s style is actually triggering the
DataValidationErrorsstyle 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
:errorclass in the DevTools on thePathBoxand conclude “the code is working,” failing to realize that theTextBox(the part actually responsible for the border) is missing the state.