WPF TextBox – 启用ValidationRule、TabIndex和Placeholder/Text Hint

huangapple go评论65阅读模式
英文:

WPF TextBox - enable ValidationRule, TabIndex, & Paceholder/Text Hint

问题

I have a group of TextBox input fields using WPF.

My wishlist

I would like to apply all three features to the input fields. I can get each feature to work individually, but I run into issues when all three are configured.

  1. Tab Order - have the ability to keyboard tab thru each field to input value
  2. Validation Rule - On LostFocus, validate text is alphanumeric only
  3. Placeholder - Add text placeholder/text hint to textbox. on focus, field is cleared. Style is different for placeholder vs user input

with below code, i can get tab index and my validation rule to work as expected, but when i add placeholder portion to my control template, validation rule does not trigger and Keyboard tab no longer works. But, my placeholder works great..... i remove the placeholder section and tab index / validation start working again.

view.xaml

<TextBox x:Name="SurveyInputProject" TabIndex="0" Tag="Project Name" Style="{StaticResource SurveyInputText}"> 
<TextBox.Text> 
<Binding Path="SurveyInput.Name" UpdateSourceTrigger="LostFocus" Mode="TwoWay"> 
<Binding.ValidationRules> 
<validate:SurveyProjectValidate ValidationStep="RawProposedValue"/> 
</Binding.ValidationRules> 
</Binding> 
</TextBox.Text> 
</TextBox>

style.xaml

<Style x:Key="SurveyInputText" TargetType="TextBox"> 
<Setter Property="FontSize" Value="20"/> 
<Setter Property="Padding" Value="10,0,0,0"/> 
<Setter Property="Background" Value="Transparent"/> 
<Setter Property="Foreground" Value="{StaticResource cCharcoal}"/> 
<Setter Property="BorderBrush" Value="{StaticResource cGray}"/> 
<Setter Property="BorderThickness" Value="0,0,0,1"/> 
<Setter Property="VerticalContentAlignment" Value="Bottom"/> 
<Setter Property="Template" Value="{StaticResource SurveyTextBoxDefault}"/> 
<Setter Property="Validation.ErrorTemplate"> 
<Setter.Value> 
<ControlTemplate> 
<AdornedElementPlaceholder Name="Error" /> 
</ControlTemplate> 
</Setter.Value> 
</Setter> 
<Style.Triggers> 
<Trigger Property="Validation.HasError" Value="True"> 
<Setter Property="BorderBrush" Value="Red"/> 
<Setter Property="BorderThickness" Value="0,0,0,1"/> 
</Trigger> 
</Style.Triggers> 
</Style>

To enable my placeholder - In my control template, i remove the ScrollView element and add the following Grid/TextBlock

<Grid> 
<TextBlock Text="{TemplateBinding Tag}" Margin="10,0,0,0" Opacity=".5" Foreground="{StaticResource cGray}" VerticalAlignment="Bottom"> 
<TextBlock.Visibility> 
<MultiBinding Converter="{StaticResource TexBoxPlaceholder}"> 
<Binding ElementName="InputField" Path="Text.IsEmpty"/> 
<Binding ElementName="InputField" Path="IsFocused"/> 
</MultiBinding> 
</TextBlock.Visibility> 
</TextBlock> 
<TextBox x:Name="InputField" Background="Transparent" BorderThickness="0" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> 
</Grid>

Template Modified

<ControlTemplate x:Key="SurveyTextBoxDefault" TargetType="{x:Type TextBox}"> 
<Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> 
<Grid> 
<TextBlock Text="{TemplateBinding Tag}" Margin="10,0,0,0" Opacity=".5" Foreground="{StaticResource cGray}" VerticalAlignment="Bottom"> 
<TextBlock.Visibility> 
<MultiBinding Converter="{StaticResource TexBoxPlaceholder}"> 
<Binding ElementName="InputField" Path="Text.IsEmpty"/> 
<Binding ElementName="InputField" Path="IsFocused"/> 
</MultiBinding> 
</TextBlock.Visibility> 
</TextBlock> 
<TextBox x:Name="InputField" Background="Transparent" BorderThickness="0" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/> 
</Grid> 
</Border> 
<ControlTemplate.Triggers> 
<Trigger Property="IsMouseOver" Value="true"> 
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/> 
</Trigger> 
<Trigger Property="IsKeyboardFocused" Value="true"> 
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/> 
</Trigger> 
<Trigger Property="IsFocused" Value="true"> 
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/> 
</Trigger> 
</ControlTemplate.Triggers> 
</ControlTemplate>

My guess is it is somewhere in my TextBlock and Focus values, but I cannot find the right combo to work. I also tried to remove textblock by moving the value converter for the placeholder to my view (textbox element) and use a multib

英文:

I have a group of TextBox input fields using WPF.

My wishlist

I would like to apply all three features to the input fields. I can get each feature to work individually, but I run into issues when all three are configured.

  1. Tab Order - have the ability to keyboard tab thru each field to input value
  2. Validation Rule - On LostFocus, validate text is alphanumeric only
  3. Placeholder - Add text placeholder/text hint to textbox. on focus, field is cleared. Style is different for placeholder vs user input

with below code, i can get tab index and my validation rule to work as expected, but when i add placeholder portion to my control template, validation rule does not trigger and Keyboard tab no longer works. But, my placeholder works great..... i remove the placeholder section and tab index / validation start working again.

view.xaml

<TextBox x:Name="SurveyInputProject" TabIndex="0" Tag="Project Name" Style="{StaticResource SurveyInputText}">
    <TextBox.Text>
        <Binding Path="SurveyInput.Name" UpdateSourceTrigger="LostFocus" Mode="TwoWay">
            <Binding.ValidationRules>
                <validate:SurveyProjectValidate ValidationStep="RawProposedValue"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

style.xaml

<Style x:Key="SurveyInputText" TargetType="TextBox">
    <Setter Property="FontSize" Value="20"/>
    <Setter Property="Padding" Value="10,0,0,0"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="{StaticResource cCharcoal}"/>
    <Setter Property="BorderBrush" Value="{StaticResource cGray}"/>
    <Setter Property="BorderThickness" Value="0,0,0,1"/>
    <Setter Property="VerticalContentAlignment" Value="Bottom"/>
    <Setter Property="Template" Value="{StaticResource SurveyTextBoxDefault}"/>
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <AdornedElementPlaceholder Name="Error" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="BorderBrush" Value="Red"/>
            <Setter Property="BorderThickness" Value="0,0,0,1"/>
        </Trigger>
    </Style.Triggers>
</Style>

<ControlTemplate x:Key="SurveyTextBoxDefault" TargetType="{x:Type TextBox}">
    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
        <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/>
        </Trigger>
        <Trigger Property="IsKeyboardFocused" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/>
        </Trigger>
        <Trigger Property="IsFocused" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

To enable my placeholder

  • In my control template, i remove the ScrollView element and add the following Grid/TextBlock
<Grid>
    <TextBlock Text="{TemplateBinding Tag}" Margin="10,0,0,0" Opacity=".5" Foreground="{StaticResource cGray}" VerticalAlignment="Bottom">
        <TextBlock.Visibility>
            <MultiBinding Converter="{StaticResource TexBoxPlaceholder}">
                <Binding ElementName="InputField" Path="Text.IsEmpty"/>
                <Binding ElementName="InputField" Path="IsFocused"/>
            </MultiBinding>
        </TextBlock.Visibility>
    </TextBlock>
    <TextBox x:Name="InputField" Background="Transparent" BorderThickness="0" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>

Template Modified

<ControlTemplate x:Key="SurveyTextBoxDefault" TargetType="{x:Type TextBox}">
    <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
        <Grid>
            <TextBlock Text="{TemplateBinding Tag}" Margin="10,0,0,0" Opacity=".5" Foreground="{StaticResource cGray}" VerticalAlignment="Bottom">
            <TextBlock.Visibility>
                <MultiBinding Converter="{StaticResource TexBoxPlaceholder}">
                    <Binding ElementName="InputField" Path="Text.IsEmpty"/>
                    <Binding ElementName="InputField" Path="IsFocused"/>
                </MultiBinding>
            </TextBlock.Visibility>
            </TextBlock>
            <TextBox x:Name="InputField" Background="Transparent" BorderThickness="0" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
        </Grid>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/>
        </Trigger>
        <Trigger Property="IsKeyboardFocused" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/>
        </Trigger>
        <Trigger Property="IsFocused" Value="true">
            <Setter Property="BorderBrush" TargetName="border" Value="{StaticResource cGray}"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

My guess is it is somewhere in my TextBlock and Focus values, but I cannot find the right combo to work. I also tried to remove textblock by moving the value converter for the placeholder to my view (textbox element) and use a multibind for both validation and placeholder, but i then get errors about no having my data binding (path) on the parent element.

答案1

得分: 0

<Grid>
    <TextBox x:Name="SurveyInputProject" TabIndex="0" Tag="Project Name" Style="{StaticResource SurveyInputText}">
        <TextBox.Text>
            <Binding Path="SurveyInput.Name" 
                 UpdateSourceTrigger="LostFocus"        
                 Mode="TwoWay">
                <Binding.ValidationRules>
                    <validate:SurveyProjectValidate ValidationStep="RawProposedValue"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    <TextBlock Style="{StaticResource SurveyInputPlaceholder}" Text="Project Name" IsHitTestVisible="False"/>
</Grid>
<Style x:Key="SurveyInputPlaceholder" TargetType="TextBlock">
    <Setter Property="Opacity" Value=".5"/>
    <Setter Property="Visibility" Value="Hidden"/>
    <Setter Property="Padding" Value="10,0,0,0"/>
    <Setter Property="FontSize" Value="20"/>
    <Setter Property="VerticalAlignment" Value="Bottom"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="{StaticResource cGray}"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding SurveyInput.Name}" Value="{x:Null}">
            <Setter Property="Visibility" Value="Visible"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding ElementName=SurveyInputProject, Path=(Validation.HasError)}" Value="True">
            <Setter Property="Visibility" Value="Hidden"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding IsFocused, ElementName=SurveyInputProject }" Value="True">
            <Setter Property="Visibility" Value="Hidden"/>
        </DataTrigger>
    </Style.Triggers>
</Style>
英文:

I found a solution for my placeholder.

  1. removed placeholder config from my control template
  2. wrapped my textbox in a grid with textblock element
  3. created a new style for textblock with style triggers to toggle visibility on focus, text input, etc.

now all three features work as expected.

view

 <Grid>
    <TextBox x:Name="SurveyInputProject" TabIndex="0" Tag="Project Name" Style="{StaticResource SurveyInputText}">
        <TextBox.Text>
            <Binding Path="SurveyInput.Name" 
                 UpdateSourceTrigger="LostFocus"        
                 Mode="TwoWay">
                <Binding.ValidationRules>
                    <validate:SurveyProjectValidate ValidationStep="RawProposedValue"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
    <TextBlock Style="{StaticResource SurveyInputPlaceholder}" Text="Project Name" IsHitTestVisible="False"/>
</Grid>

style


<Style x:Key="SurveyInputPlaceholder" TargetType="TextBlock">
    <Setter Property="Opacity" Value=".5"/>
    <Setter Property="Visibility" Value="Hidden"/>
    <Setter Property="Padding" Value="10,0,0,0"/>
    <Setter Property="FontSize" Value="20"/>
    <Setter Property="VerticalAlignment" Value="Bottom"/>
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="Foreground" Value="{StaticResource cGray}"/>
    <Style.Triggers>
        <DataTrigger Binding="{Binding SurveyInput.Name}" Value="{x:Null}">
            <Setter Property="Visibility" Value="Visible"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding ElementName=SurveyInputProject, Path=(Validation.HasError)}" Value="True">
            <Setter Property="Visibility" Value="Hidden"/>
        </DataTrigger>
        <DataTrigger Binding="{Binding IsFocused, ElementName=SurveyInputProject }" Value="True">
            <Setter Property="Visibility" Value="Hidden"/>
        </DataTrigger>
    </Style.Triggers>

</Style>

答案2

得分: 0

以下是代码部分的翻译:

另一种创建带有占位符的解决方案:

class TextBoxWithPlaceHolder.cs

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace TextBoxWithPlaceHolderExample
{
    public class TextBoxWithPlaceHolder : TextBox
    {
        #region Properties
        public string Label
        {
            get { return (string)GetValue(LabelProperty); }
            set { SetValue(LabelProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Label. This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LabelProperty =
            DependencyProperty.Register("Label", typeof(string), typeof(TextBoxWithPlaceHolder), new UIPropertyMetadata("Label"));

        public Style LabelStyle
        {
            get { return (Style)GetValue(LabelStyleProperty); }
            set { SetValue(LabelStyleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for LabelStyle. This enables animation, styling, binding, etc...
        public static readonly DependencyProperty LabelStyleProperty =
            DependencyProperty.Register("LabelStyle", typeof(Style), typeof(TextBoxWithPlaceHolder), new UIPropertyMetadata(null));


        public bool HasText
        {
            get { return (bool)GetValue(HasTextProperty); }
            private set { SetValue(HasTextPropertyKey, value); }
        }

        private static readonly DependencyPropertyKey HasTextPropertyKey =
            DependencyProperty.RegisterReadOnly("HasText", typeof(bool), typeof(TextBoxWithPlaceHolder), new PropertyMetadata(false));
        public static readonly DependencyProperty HasTextProperty = HasTextPropertyKey.DependencyProperty;

        #endregion

        public TextBoxWithPlaceHolder()
            : base()
        {
        }

        AdornerLayer myAdornerLayer;
        AdornerLabel myAdornerLabel;
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            myAdornerLayer = AdornerLayer.GetAdornerLayer(this);
            myAdornerLabel = new AdornerLabel(this, Label, LabelStyle);
            UpdateAdorner(this);

            DependencyPropertyDescriptor focusProp = DependencyPropertyDescriptor.FromProperty(FrameworkElement.IsFocusedProperty, typeof(FrameworkElement));
            if (focusProp is not null)
            {
                focusProp.AddValueChanged(this, delegate
                {
                    UpdateAdorner(this);
                });
            }

            DependencyPropertyDescriptor containsTextProp = DependencyPropertyDescriptor.FromProperty(TextBoxWithPlaceHolder.HasTextProperty, typeof(TextBoxWithPlaceHolder));
            if (containsTextProp is not null)
            {
                containsTextProp.AddValueChanged(this, delegate
                {
                    UpdateAdorner(this);
                });
            }
        }

        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            HasText = this.Text != "";
            base.OnTextChanged(e);
        }

        protected override void OnDragEnter(DragEventArgs e)
        {
            myAdornerLayer.RemoveAdorners<AdornerLabel>(this); // 需要 AdornerExtensions.cs
            base.OnDragEnter(e);
        }

        protected override void OnDragLeave(DragEventArgs e)
        {
            UpdateAdorner(this);
            base.OnDragLeave(e);
        }

        private void UpdateAdorner(FrameworkElement elem)
        {
            if (((TextBoxWithPlaceHolder)elem).HasText || elem.IsFocused)
            {
                // 隐藏阴影标签
                this.ToolTip = this.Label;
                myAdornerLayer.RemoveAdorners<AdornerLabel>(elem); // 需要 AdornerExtensions.cs
            }
            else
            {
                // 显示阴影标签
                this.ToolTip = null;
                if (!myAdornerLayer.Contains<AdornerLabel>(elem)) // 需要 AdornerExtensions.cs
                    myAdornerLayer.Add(myAdornerLabel);
            }
        }
    }
}
class AdornerLabel.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace TextBoxWithPlaceHolderExample
{
    // 修饰层必须继承抽象基类 Adorner。
    public class AdornerLabel : Adorner
    {
        private readonly TextBlock _textBlock;

        // 确保调用基类构造函数。
        public AdornerLabel(UIElement adornedElement, string label, Style labelStyle)
            : base(adornedElement)
        {
            _textBlock = new TextBlock
            {
                Style = labelStyle,
                Text = label
            };
        }

        // 确保布局系统知道元素
        protected override Size MeasureOverride(Size constraint)
        {
            _textBlock.Measure(constraint);
            return _textBlock.DesiredSize;
        }

        // 确保布局系统知道元素
        protected override Size ArrangeOverride(Size finalSize)
        {
            _textBlock.Arrange(new Rect(finalSize));
            return finalSize;
        }

        // 返回要显示的可视元素
        protected override System.Windows.Media.Visual GetVisualChild(int index)
        {
            return _textBlock;
        }

        // 返回可视元素的数量
        protected override int VisualChildrenCount
        {
            get { return 1; }
        }
    }
}
一些 AdornerExtension

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace TextBoxWithPlaceHolderExample
{
    public static class AdornerExtensions
    {
        public static void RemoveAdorners<T>(this AdornerLayer adr, UIElement elem)
        {
            Adorner[] adorners = adr.GetAdorners(elem);

            if (adorners == null) return;

            for (int i = adorners.Length - 1; i >= 0; i--)
            {
                if (adorners[i] is T)
                    adr.Remove(adorners[i]);
            }
        }

        public static bool Contains<T>(this AdornerLayer adr, UIElement elem)
        {
            Adorner[] adorners = adr.GetAdorners(elem);

            if (adorners == null) return false;

            for (int i = adorners.Length - 1; i >= 0; i--)
            {
                if (adorners[i] is T)
                    return true;
            }
            return false;
        }

        public static void RemoveAll(this AdornerLayer adr, UIElement elem)
        {
            try
            {
                Adorner[] adorners = adr.GetAdorners(elem);

                if (adorners == null) return;

                foreach (Adorner toRemove in adorners)
                    adr.Remove(toRemove);
            }
            catch { }
        }

        public static void RemoveAllRecursive(this AdornerLayer adr, UIElement element)
        {
            try
            {
                Action<UIElement> recurse = null;
                recurse = ((Action<UIElement>)delegate (UIElement elem)
                {
                    adr.RemoveAll(elem);
                    if (elem is Panel panel)
                    {
                        foreach (UIElement e in panel.Children)
                            recurse(e);
                    }
                    else if (elem is Decorator decorator)
                    {
                        recurse(decorator.Child);


<details>
<summary>英文:</summary>

Another solution  to create a placeholder with adoner:

    class TextBoxWithPlaceHolder.cs
    
    using System.ComponentModel;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    
    namespace TextBoxWithPlaceHolderExample
    {
        public class TextBoxWithPlaceHolder : TextBox
        {
            #region Properties
            public string Label
            {
                get { return (string)GetValue(LabelProperty); }
                set { SetValue(LabelProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for Label.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty LabelProperty =
                DependencyProperty.Register(&quot;Label&quot;, typeof(string), typeof(TextBoxWithPlaceHolder), new UIPropertyMetadata(&quot;Label&quot;));
    
            public Style LabelStyle
            {
                get { return (Style)GetValue(LabelStyleProperty); }
                set { SetValue(LabelStyleProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for LabelStyle.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty LabelStyleProperty =
                DependencyProperty.Register(&quot;LabelStyle&quot;, typeof(Style), typeof(TextBoxWithPlaceHolder), new UIPropertyMetadata(null));
    
    
            public bool HasText
            {
                get { return (bool)GetValue(HasTextProperty); }
                private set { SetValue(HasTextPropertyKey, value); }
            }
    
            private static readonly DependencyPropertyKey HasTextPropertyKey =
                DependencyProperty.RegisterReadOnly(&quot;HasText&quot;, typeof(bool), typeof(TextBoxWithPlaceHolder), new PropertyMetadata(false));
            public static readonly DependencyProperty HasTextProperty = HasTextPropertyKey.DependencyProperty;
    
            #endregion
    
            public TextBoxWithPlaceHolder()
                : base()
            {
            }
    
            AdornerLayer myAdornerLayer;
            AdornerLabel myAdornerLabel;
            public override void OnApplyTemplate()
            {
                base.OnApplyTemplate();
    
                myAdornerLayer = AdornerLayer.GetAdornerLayer(this);
                myAdornerLabel = new AdornerLabel(this, Label, LabelStyle);
                UpdateAdorner(this);
    
                DependencyPropertyDescriptor focusProp = DependencyPropertyDescriptor.FromProperty(FrameworkElement.IsFocusedProperty, typeof(FrameworkElement));
                if (focusProp != null)
                {
                    focusProp.AddValueChanged(this, delegate
                    {
                        UpdateAdorner(this);
                    });
                }
    
                DependencyPropertyDescriptor containsTextProp = DependencyPropertyDescriptor.FromProperty(TextBoxWithPlaceHolder.HasTextProperty, typeof(TextBoxWithPlaceHolder));
                if (containsTextProp != null)
                {
                    containsTextProp.AddValueChanged(this, delegate
                    {
                        UpdateAdorner(this);
                    });
                }
            }
    
            protected override void OnTextChanged(TextChangedEventArgs e)
            {
                HasText = this.Text != &quot;&quot;;
    
                base.OnTextChanged(e);
            }
    
            protected override void OnDragEnter(DragEventArgs e)
            {
                myAdornerLayer.RemoveAdorners&lt;AdornerLabel&gt;(this); // requires AdornerExtensions.cs
    
                base.OnDragEnter(e);
            }
    
            protected override void OnDragLeave(DragEventArgs e)
            {
                UpdateAdorner(this);
    
                base.OnDragLeave(e);
            }
    
            private void UpdateAdorner(FrameworkElement elem)
            {
                if (((TextBoxWithPlaceHolder)elem).HasText || elem.IsFocused)
                {
                    // Hide the Shadowed Label
                    this.ToolTip = this.Label;
                    myAdornerLayer.RemoveAdorners&lt;AdornerLabel&gt;(elem);  // requires AdornerExtensions.cs
                }
                else
                {
                    // Show the Shadowed Label
                    this.ToolTip = null;
                    if (!myAdornerLayer.Contains&lt;AdornerLabel&gt;(elem))  // requires AdornerExtensions.cs
                        myAdornerLayer.Add(myAdornerLabel);
                }
            }
        }
    }
___
class AdornerLabel.cs

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    
    namespace TextBoxWithPlaceHolderExample
    {
        // Adorners must subclass the abstract base class Adorner.
        public class AdornerLabel : Adorner
        {
            private readonly TextBlock _textBlock;
    
            // Be sure to call the base class constructor.
            public AdornerLabel(UIElement adornedElement, string label, Style labelStyle)
                : base(adornedElement)
            {
                _textBlock = new TextBlock
                {
                    Style = labelStyle,
                    Text = label
                };
            }
    
            //make sure that the layout system knows of the element
            protected override Size MeasureOverride(Size constraint)
            {
                _textBlock.Measure(constraint);
                return _textBlock.DesiredSize;
            }
            
            //make sure that the layout system knows of the element
            protected override Size ArrangeOverride(Size finalSize)
            {
                _textBlock.Arrange(new Rect(finalSize));
                return finalSize;
            }
    
            //return the visual that we want to display
            protected override System.Windows.Media.Visual GetVisualChild(int index)
            {
                return _textBlock;
            }
    
            //return the count of the visuals
            protected override int VisualChildrenCount
            {
                get { return 1; }
            }
        }
    }
___
some AdornerExtension

    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Documents;
    
    namespace TextBoxWithPlaceHolderExample
    {
        public static class AdornerExtensions
        {
            public static void RemoveAdorners&lt;T&gt;(this AdornerLayer adr, UIElement elem)
            {
                Adorner[] adorners = adr.GetAdorners(elem);
    
                if (adorners == null) return;
    
                for (int i = adorners.Length - 1; i &gt;= 0; i--)
                {
                    if (adorners[i] is T)
                        adr.Remove(adorners[i]);   
                }
            }
    
            public static bool Contains&lt;T&gt;(this AdornerLayer adr, UIElement elem)
            {
                Adorner[] adorners = adr.GetAdorners(elem);
    
                if (adorners == null) return false;
    
                for (int i = adorners.Length - 1; i &gt;= 0; i--)
                {
                    if (adorners[i] is T)
                        return true;
                }
                return false;
            }
    
            public static void RemoveAll(this AdornerLayer adr, UIElement elem)
            {
                try
                {
                    Adorner[] adorners = adr.GetAdorners(elem);
    
                    if (adorners == null) return;
    
                    foreach (Adorner toRemove in adorners)
                        adr.Remove(toRemove);
                }
                catch { }
            }
    
            public static void RemoveAllRecursive(this AdornerLayer adr, UIElement element)
            {
                try
                {
                    Action&lt;UIElement&gt; recurse = null;
                    recurse = ((Action&lt;UIElement&gt;)delegate(UIElement elem)
                    {
                        adr.RemoveAll(elem);
                        if (elem is Panel panel)
                        {
                            foreach (UIElement e in panel.Children)
                                recurse(e);
                        }
                        else if (elem is Decorator decorator)
                        {
                            recurse(decorator.Child);
                        }
                        else if (elem is ContentControl control)
                        {
                            if (control.Content is UIElement)
                                recurse(control.Content as UIElement);
                        }
    
                    });
    
                    recurse(element);
                }
                catch { }
            }
        }
    }
___
how to use it: in xaml file

    &lt;Window x:Class=&quot;TextBoxWithPlaceHolderExample.Window1&quot;
        xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
        xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
        xmlns:local=&quot;clr-namespace:TextBoxWithPlaceHolderExample&quot;
        Title=&quot;Window1&quot; Height=&quot;500&quot; Width=&quot;700&quot; Style=&quot;{StaticResource dataWindow}&quot;&gt;
        &lt;Window.Resources&gt;
            
            &lt;Style x:Key=&quot;PlaceHolderLabelStyle&quot;&gt;
                &lt;Setter Property=&quot;TextBlock.Foreground&quot; Value=&quot;{x:Static SystemColors.ControlDarkBrush}&quot; /&gt;
                &lt;Setter Property=&quot;FrameworkElement.Opacity&quot; Value=&quot;0.8&quot; /&gt;
                &lt;Setter Property=&quot;TextBlock.FontSize&quot; Value=&quot;12&quot; /&gt;
                &lt;Setter Property=&quot;TextBlock.FontStyle&quot; Value=&quot;Italic&quot; /&gt;
                &lt;Setter Property=&quot;TextBlock.Margin&quot; Value=&quot;8,4,4,4&quot; /&gt;
            &lt;/Style&gt;
    
            &lt;Style TargetType=&quot;{x:Type local:TextBoxWithPlaceHolder}&quot;&gt;
                &lt;Setter Property=&quot;FontSize&quot; Value=&quot;14&quot; /&gt;
                &lt;Setter Property=&quot;Margin&quot; Value=&quot;5,2,2,2&quot; /&gt;
                &lt;Setter Property=&quot;LabelStyle&quot; Value=&quot;{StaticResource PlaceHolderLabelStyle}&quot; /&gt;
            &lt;/Style&gt;
    
        &lt;/Window.Resources&gt;
        &lt;Border Style=&quot;{StaticResource dataBorder}&quot;&gt;
            &lt;Grid DataContext=&quot;{x:Static local:Customer.GetHomerSimpson}&quot;&gt;
                &lt;Grid.RowDefinitions&gt;
                    &lt;RowDefinition Height=&quot;28&quot; /&gt;
                    &lt;RowDefinition Height=&quot;10&quot; /&gt;
                    &lt;RowDefinition Height=&quot;28&quot; /&gt;
                    &lt;RowDefinition Height=&quot;28&quot; /&gt;
                    &lt;RowDefinition Height=&quot;10&quot; /&gt;
                    &lt;RowDefinition Height=&quot;28&quot; /&gt;
                    &lt;RowDefinition Height=&quot;28&quot; /&gt;
                    &lt;RowDefinition Height=&quot;28&quot; /&gt;
                    &lt;RowDefinition Height=&quot;*&quot; /&gt;
                &lt;/Grid.RowDefinitions&gt;
    
                &lt;Grid&gt;
                    &lt;Grid.ColumnDefinitions&gt;
                        &lt;ColumnDefinition Width=&quot;4*&quot; /&gt;
                        &lt;ColumnDefinition Width=&quot;2*&quot; /&gt;
                        &lt;ColumnDefinition Width=&quot;4*&quot; /&gt;
                    &lt;/Grid.ColumnDefinitions&gt;
                    &lt;local:TextBoxWithPlaceHolder Label=&quot;First Name&quot; Text=&quot;{Binding FirstName}&quot; /&gt;
                    &lt;local:TextBoxWithPlaceHolder Label=&quot;Middle Name&quot; Text=&quot;{Binding MiddleName}&quot; Grid.Column=&quot;1&quot; /&gt;
                    &lt;local:TextBoxWithPlaceHolder Label=&quot;Last Name&quot; Text=&quot;{Binding LastName}&quot; Grid.Column=&quot;2&quot; /&gt;
                &lt;/Grid&gt;
    
                &lt;local:TextBoxWithPlaceHolder Label=&quot;Job Title&quot; Text=&quot;{Binding JobTitle}&quot; Grid.Row=&quot;2&quot; /&gt;
    
                &lt;local:TextBoxWithPlaceHolder Label=&quot;Company&quot; Text=&quot;{Binding Company}&quot; Grid.Row=&quot;3&quot; /&gt;
    
                &lt;local:TextBoxWithPlaceHolder Label=&quot;Address1&quot; Text=&quot;{Binding Address1}&quot; Grid.Row=&quot;5&quot; /&gt;
    
                &lt;local:TextBoxWithPlaceHolder Label=&quot;Address2&quot; Text=&quot;{Binding Address2}&quot; Grid.Row=&quot;6&quot; /&gt;
    
                &lt;Grid Grid.Row=&quot;7&quot;&gt;
                    &lt;Grid.ColumnDefinitions&gt;
                        &lt;ColumnDefinition Width=&quot;5*&quot; /&gt;
                        &lt;ColumnDefinition Width=&quot;2*&quot; /&gt;
                        &lt;ColumnDefinition Width=&quot;3*&quot; /&gt;
                    &lt;/Grid.ColumnDefinitions&gt;
                    &lt;local:TextBoxWithPlaceHolder Label=&quot;City&quot; Text=&quot;{Binding City}&quot; /&gt;
                    &lt;local:TextBoxWithPlaceHolder Label=&quot;State&quot; Text=&quot;{Binding State}&quot; Grid.Column=&quot;1&quot; /&gt;
                    &lt;local:TextBoxWithPlaceHolder Label=&quot;Zip&quot; Text=&quot;{Binding Zip}&quot; Grid.Column=&quot;2&quot; /&gt;
                &lt;/Grid&gt;
    
            &lt;/Grid&gt;
        &lt;/Border&gt;
    &lt;/Window&gt;
____

one good thing i see with this method is to simplify the way to add the placeholder and the simplification of the style used

Result:

[![enter image description here][1]][1]


  [1]: https://i.stack.imgur.com/jECfc.png

</details>



# 答案3
**得分**: 0

Your `ControlTemplate` is wrong. You can't add a `TextBox` into the template of a `TextBox`. This doesn't make sense either. The problem you are facing is that you are typing into the inner `TextBox`. This inner `TextBox` has not the desired tab index and it is not bound to any source and therefore not validating. The `ValidationRule` is actually applied to the outer `TextBox` (the templated parent).

The `TextBox` *requires* a text site element named `PART_ContentHost` (for example a `Border`). Elements that are found in templates and that are named with the `PART_` prefix can be considered as mandatory. Otherwise the templated control won't behave as intended.
Just put the `ScrollViewer` back (to replace the inner `TextBox`) and bind the placeholder `TextBlock` to the templated parent (the `TextBox` the template is applied to) using the `{RelativeSource TemplatedParent}` markup extension in your `MultiBinding`.

```xaml
<ControlTemplate x:Key="SurveyTextBoxDefault" TargetType="{x:Type TextBox}">
  <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
    <Grid>
      <TextBlock Text="{TemplateBinding Tag}" Margin="10,0,0,0" Opacity=".5" VerticalAlignment="Bottom">
        <TextBlock.Visibility>
          <MultiBinding Converter="{StaticResource TextBoxPlaceholder}">
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Text.IsEmpty" />
            <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="IsFocused" />
          </MultiBinding>
        </TextBlock.Visibility>
      </TextBlock>

      <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" />
    </Grid>
  </Border>

  <ControlTemplate.Triggers>
   ...
  </ControlTemplate.Triggers>
</ControlTemplate>
英文:

Your ControlTemplate is wrong. You can't add a TextBox into the template of a TextBox. This doesn't make sense either. The problem you are facing is that you are typing into the inner TextBox. This inner TextBox has not the desired tab index and it is not bound to any source and therefore not validating. The ValidationRule is actually applied to the outer TextBox (the templated parent).

The TextBox requires a text site element named PART_ContentHost (for example a Border). Elements that are found in templates and that are named with the PART_ prefix can be considered as mandatory. Otherwise the templated control won't behave as intended.
Just put the ScrollViewer back (to replace the inner TextBox) and bind the placeholder TextBlock to the templated parent (the TextBox the template is applied to) using the {RelativeSource TemplatedParent} markup extension in your MultiBinding.

&lt;ControlTemplate x:Key=&quot;SurveyTextBoxDefault&quot;
TargetType=&quot;{x:Type TextBox}&quot;&gt;
&lt;Border x:Name=&quot;border&quot;
Background=&quot;{TemplateBinding Background}&quot;
BorderBrush=&quot;{TemplateBinding BorderBrush}&quot;
BorderThickness=&quot;{TemplateBinding BorderThickness}&quot;&gt;
&lt;Grid&gt;
&lt;TextBlock Text=&quot;{TemplateBinding Tag}&quot;
Margin=&quot;10,0,0,0&quot;
Opacity=&quot;.5&quot;
VerticalAlignment=&quot;Bottom&quot;&gt;
&lt;TextBlock.Visibility&gt;
&lt;MultiBinding Converter=&quot;{StaticResource TexBoxPlaceholder}&quot;&gt;
&lt;Binding RelativeSource=&quot;{RelativeSource TemplatedParent}&quot;
Path=&quot;Text.IsEmpty&quot; /&gt;
&lt;Binding RelativeSource=&quot;{RelativeSource TemplatedParent}&quot;
Path=&quot;IsFocused&quot; /&gt;
&lt;/MultiBinding&gt;
&lt;/TextBlock.Visibility&gt;
&lt;/TextBlock&gt;
&lt;ScrollViewer x:Name=&quot;PART_ContentHost&quot;
Focusable=&quot;false&quot;
HorizontalScrollBarVisibility=&quot;Hidden&quot;
VerticalScrollBarVisibility=&quot;Hidden&quot; /&gt;
&lt;/Grid&gt;
&lt;/Border&gt;
&lt;ControlTemplate.Triggers&gt;
...
&lt;/ControlTemplate.Triggers&gt;
&lt;/ControlTemplate&gt;

huangapple
  • 本文由 发表于 2023年4月6日 21:53:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/75950332.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定