0

The problem is that the KeyDown event is triggered twice, the first one comes from the CustomTextBox named textSource inside the Style; the second, from control in the MainWindow named "CTbox".

The linked question provides a solution where you filter on the EventHandler OnKeyDown the source e.Source != "textSource" which is working fine:

if (e.Source is CustomTextBox sourceTextBox && sourceTextBox.Name.Equals("textSource"))
{
    return;
}

Basically I would like to know if there is any better solution to this and if someone can explain the reason why is this happening and how can be avoid it.

The style is mean to create a Hint Text or WaterMark in the CustomTextBox without relaying in the Focus events

Thanks in advance.

Following the code to create a Minimal, Complete, and Verifiable example of this behaviour

CustomTextBox Class:

public class CustomTextBox : TextBox
{
    protected override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);
    }
}


MainWindow:

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp2"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style x:Key="CustomTextBoxStyle"
           TargetType="{x:Type local:CustomTextBox}">
            <Setter Property="Foreground"
                Value="#FF414042" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:CustomTextBox}">
                        <Border Name="Border"
                            BorderBrush="#FF348781"
                            BorderThickness="0,0,0,4"
                            CornerRadius="2">
                            <ScrollViewer x:Name="PART_ContentHost"
                                      Margin="0" />
                            <VisualStateManager.VisualStateGroups>
                                <VisualStateGroup x:Name="CommonStates">
                                    <VisualState x:Name="Normal" />
                                    <VisualState x:Name="Disabled" />
                                    <VisualState x:Name="ReadOnly" />
                                    <VisualState x:Name="MouseOver" />
                                </VisualStateGroup>
                            </VisualStateManager.VisualStateGroups>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

        <Style TargetType="{x:Type local:CustomTextBox}"
           BasedOn="{StaticResource CustomTextBoxStyle}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:CustomTextBox}">
                        <Grid>
                            <local:CustomTextBox
                            Text="{TemplateBinding Text}"
                            x:Name="textSource"
                            Background="Transparent"
                            Panel.ZIndex="2"
                            Style="{StaticResource CustomTextBoxStyle}"/>
                            <TextBlock Text="{TemplateBinding Tag}">
                                <TextBlock.Style>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <Setter Property="Foreground"
                                            Value="Transparent" />
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Path=Text, Source={x:Reference textSource}}"
                                                     Value="">
                                                <Setter Property="Foreground"
                                                    Value="Gray" />
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            </TextBlock>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <Grid>
        <local:CustomTextBox x:Name="CTbox" Tag="Hint Text Example" Height="25" Width="258"/>
    </Grid>
</Window>
Nekeniehl
  • 1,633
  • 18
  • 35

1 Answers1

1

So you're getting two executions of your OnKeyDown handler because your XAML essentially builds a CustomTextBox within a CustomTextBox. With the routing strategy of WPF events and your visual tree looking like this:

enter image description here

the KeyDown event naturally will fire in both places (textsource, then bubble up to CustomTextBox). Side note, if you overrode OnPreviewKeyDown, you should get the reverse order - CTbox tunneling down to textSource.

To answer the second part of your question - how to avoid this, I think perhaps you should rethink your implementation here. My questions would be:

  1. Should you even use an editable control (your CustomTextBox in this case) for your hinttext overlay in your control template? Since the hinttext should only be readable, perhaps a TextBlock would suffice.
  2. Should you override the ControlTemplate for customtextbox to get this HintText functionality? Perhaps the better way is just to create a UserControl that provides this functionality. i.e. Grid containing your custom textbox and a textblock overlaying it. This will prevent you from having a visual tree with nested CustomTextBox's.
  3. If you need a ControlTemplate so you can reuse it, why not make it the default control template of your CustomTextBox? If your "Tag" property is not bound, the hinttext will just naturally not show. This way you won't have nested CustomTextBoxes causing duplicate execution of OnKeyDown as well!
  4. Why do you even need a CustomTextBox? Do you have other override code and behavior that you're not showing that requires this? I'm guessing this is because you showed a minimal sample and there's more - but just thought I'd ask :)

EDIT for comment Given your clarifications/questions, I would've approached solving this via a custom control. I know what you have is technically a custom control, but I'm talking about the kind that comes with a themes\generic.xaml file :). If you're not familiar, I recommend creating a new VS project and making it of the "WPF custom control library" template. Then you should be able to add a new class of the template "Custom Control (WPF)". You'll see that VS has generated a themes\generic.xaml file for you - this is where you would hold the controltemplate for your CustomTextBox. I would get the default control template of a TextBox, and add in a TextBlock that's not hit test visible (so a user can click through to your editable area) and set a TemplateBinding on it for the HintText. You'll then be able to reuse this custom control everywhere(as it's compiled in a separate dll... you can also opt to keep it within your current project too), get the default behaviors of textbox that you didn't override, and won't have nested CustomTextBoxes.

Rowbear
  • 1,619
  • 12
  • 15
  • Thanks a lot for your clear answer, the main problem and the main reason is that I'm pretty new in WPF and I have a lot to learn. 1. I actually do not know how I could do the same with a TextBlock instead. 2. I might give a try to put everything in a `UserControl` instead, so I can reuse it for later projects. I didn't expect that would be such complicate task to put a Watermark `Textbox`. 3. Not sure if I understand you on this point, Is there any other way to set up the `ControlTemplate` beside the `Style`? 4. Yes,there are lot of stuff involved mainly because I have a Virtual Keyboard – Nekeniehl Feb 22 '18 at 09:14