WPF TextAlignmentProperty.OverrideMetadata not working in inheritor of TextBox

Viewed 130

I have created a custom control, which inherits TextBox. It basically has an extra property 'Seconds' and set a binding on 'Text', to shown the 'Seconds' formatted, as eg. 2m 5s, using a converter.

I now want to default right-align the text. From other custom controls I know we sometimes will want to set/override values using styles. If I set the value directly in the constructor I will not be able to do this.

I would usually something like this:

TextAlignmentProperty.OverrideMetadata(typeof(DurationTextBox), new FrameworkPropertyMetadata(TextAlignment.Right));

But this does NOT appear to work:

First two have the default alignment, then they are Left, Center and Right aligned directly on the controls. Seconds row has a style setting alignment to Center

First two have the default alignment, then they are Left, Center and Right aligned directly on the controls. Seconds row has a style setting alignment to Center

I have bound a TextBlock to the TextAlignment of the first DurationTextBox, and this states that the aligment is 'Right', but this is not how it is shown!

Can anyone explain:

A. Why this is not working?

B. How to do this correctly, or something with the same end effect? (default aligned Right, but possible to override from Style)

C# class :

Please note that this is a simplified version. The complete one has Min, Max, option of confirming value changed and option for out of range action, which is the reason for the structure of the class. Please keep focus on the TextAlignment issue! (The SecondsToDurationStringConverter and DurationStringValidator can be removed to make the example compile with the same effect)

public class DurationTextBox : TextBox
{
    #region Dependency properties

    /// <summary>
    /// Property for <see cref="Seconds"/>
    /// </summary>
    [NotNull] public static readonly DependencyProperty SecondsProperty = DependencyProperty.Register(nameof(Seconds), typeof(double), typeof(Demo.DurationTextBox), new FrameworkPropertyMetadata(default(double), SecondsChangedCallback) { BindsTwoWayByDefault = true });

    /// <summary>
    /// Seconds to show as duration string
    /// </summary>
    public double Seconds
    {
        // ReSharper disable once PossibleNullReferenceException
        get { return (double)GetValue(SecondsProperty); }
        set { SetValue(SecondsProperty, value); }
    }

    /// <summary>
    /// Property for <see cref="EditValue"/>
    /// </summary>
    [NotNull] public static readonly DependencyProperty EditValueProperty = DependencyProperty.Register(nameof(EditValue), typeof(double), typeof(Demo.DurationTextBox), new FrameworkPropertyMetadata(default(double), EditValueChangedCallback) { BindsTwoWayByDefault = true });

    /// <summary>
    /// Number being edited by the actual text box. Transferred to <see cref="Seconds"/>.
    /// <para>Do NOT bind to this property from outside this control</para>
    /// </summary>
    public double EditValue
    {
        // ReSharper disable once PossibleNullReferenceException
        get { return (double)GetValue(EditValueProperty); }
        set { SetValue(EditValueProperty, value); }
    }
    #endregion Dependency properties

    private bool _isLocked;

    static DurationTextBox()
    {
        // TextAlignment
        TextAlignmentProperty.OverrideMetadata(typeof(Demo.DurationTextBox), new FrameworkPropertyMetadata(TextAlignment.Right));
    }

    /// <inheritdoc />
    public DurationTextBox()
    {
        SecondsToDurationStringConverter secondsToDurationStringConverter = new SecondsToDurationStringConverter();

        // Text
        Binding binding = new Binding(nameof(EditValue)) { Source = this, Converter = secondsToDurationStringConverter, NotifyOnValidationError = true };
        binding.ValidationRules.Add(new DurationStringValidation());
        SetBinding(TextProperty, binding);

    }

    

    private static void SecondsChangedCallback([CanBeNull] DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Demo.DurationTextBox durationTextBox = d as Demo.DurationTextBox;
        if (durationTextBox == null) return;
        if (!durationTextBox._isLocked)
        {
            durationTextBox._isLocked = true;
            durationTextBox.SetCurrentValue(EditValueProperty, durationTextBox.Seconds);
            durationTextBox._isLocked = false;
        }
    }


    private static void EditValueChangedCallback([CanBeNull] DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Demo.DurationTextBox durationTextBox = d as Demo.DurationTextBox;
        if (durationTextBox == null) return;
        if (!durationTextBox._isLocked)
        {
            durationTextBox._isLocked = true;
            durationTextBox.SetCurrentValue(SecondsProperty, durationTextBox.EditValue);
            durationTextBox._isLocked = false;
        }
    }

  
}

XAML code:

    <Label Content="Demo.DurationTextBox" FontWeight="Bold"/>
    <WrapPanel>
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" x:Name="DemoDurationTextBox"/>
        <TextBlock Text="{Binding ElementName=DemoDurationTextBox, Path=TextAlignment}"/>
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}"  />
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" TextAlignment="Left"/>
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" TextAlignment="Center"/>
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" TextAlignment="Right"/>
    </WrapPanel>
    <WrapPanel>
        <WrapPanel.Resources>
            <Style TargetType="demo:DurationTextBox">
                <Setter Property="TextAlignment" Value="Center"/>
            </Style>
        </WrapPanel.Resources>
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}"/>
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}"  />
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" TextAlignment="Left" />
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" TextAlignment="Center" />
        <demo:DurationTextBox MinWidth="150" Seconds="{Binding ElementName=Duration1, Path=Text}" TextAlignment="Right" />
    </WrapPanel>
2 Answers

A: This isn't working because TextBox.TextAlignment is inherited, so by default the active value is determined by the parent control at runtime. It doesn't make sense for a class to override the default value of the static DependencyProperty object itself.

The fact that the debug binding you added displays "Right" is strange, and probably an internal WPF optimisation glitch.

B: The correct solution is to set TextBox.TextAlignment="Right" (note the type name qualifier) on a parent control, e.g. your wrap panel. That value will then be automatically applied to all child text blocks unless they or an intermediate parent override it further, including via a style.

C: I would add that the code you posted seems to be an attempt to re-invent DataGrid. That control supports transactional editing out of the box and might save you a lot of time if you switch to it!

While it is not the solution I would have preferred, I have found a way that allows me to have the default alignment of my control to Right, while being able to overwrite it locally by using a style, or directly on the control, without affecting other TextBoxes

I made a default style:

<Style TargetType="controls:DurationTextBox">
    <Setter Property="TextAlignment" Value="Right"/>
</Style>

As I have other resource dictionaries which must be included anyway this will work in my situation.

I would have preferred simply setting the default value of my control, but according to Artfunkel that is sadly not possible.

The issue with my chosen approach of using a default style is if a different default style is set for TextBox in a project, the DurationTextBox will not use/inherit this style, because it has its own default style, thus someone using the library has to also set a similar style/override the style for the DurationTextBox for them not to appear different.

If the DurationTextBox had not needed a different default style, containing the text alignment, it would have been possible to have the default style be the same as a TextBox, but this is not a possibility now.

As we do have a different default style for TextBox I have added a BasedOn to my default style:

<Style TargetType="controls:DurationTextBox" BasedOn="{StaticResource {x:Type TextBox}}">
    <Setter Property="TextAlignment" Value="Right"/>
</Style>
Related