WPF TextBox with ellipsis trimming and validation

2019-09-11 17:34发布

I am currently working on an user control to add some more functionality to a TextBox:

  • Ellipsis trimming if the text is to large and the TextBox has lost the focus
  • Label in front of the TextBox
  • Validation on error

I found an example for ellipsis. This example stores the current value of the TextBox in a dependency property and sets the TextBox.Text property if the element got/lost the focus.

Here my xaml-code of my user control:

<UserControl x:Class="WpfTextBoxEllipsis.EditUC"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300"
         Name="EDIT"
         Loaded="EditUC_OnLoaded">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <TextBlock Text="Label" Margin="10,0" Grid.Column="0" VerticalAlignment="Center"/>
    <TextBox Name="Box" Grid.Column="1"
             LostFocus="BoxLostFocus"
             GotFocus="BoxGotFocus"
             LayoutUpdated="BoxOnLayoutUpdated"/>
</Grid>

The code behind of my user control:

public partial class EditUC : UserControl
{
    private string textvalue;

    public EditUC()
    {
        InitializeComponent();
    }

    internal UpdateSourceTrigger UpdateTrigger { get; set; }

    #region Text property

    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        "Text",
        typeof(string),
        typeof(EditUC),
        new FrameworkPropertyMetadata(string.Empty)
        {
            DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            BindsTwoWayByDefault = true
        });

    public string Text
    {
        get
        {
            return (string)GetValue(TextProperty);
        }

        set
        {
            SetValue(TextProperty, value);
        }
    }

    #endregion

    #region Ellisped TextBox

    public string TextValue
    {
        get
        {
            return this.textvalue;
        }

        set
        {
            if (this.textvalue == value)
            {
                return;
            }

            this.textvalue = value;
            this.Box.Text = this.CutTextToWidth(this.textvalue);
        }
    }

    private string CutTextToWidth(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return value;
        }

        var width = this.Box.ActualWidth - 25;
        var validArea = false;
        var shortText = value;
        var lastlong = value.Length;
        var lastfit = 0;

        if (this.StringWidth(value) < width)
        {
            shortText = value;
        }
        else
        {
            while (!validArea)
            {
                if (width < this.StringWidth(shortText + "\u2026"))
                {
                    lastlong = shortText.Length;
                }
                else
                {
                    lastfit = shortText.Length;
                }

                int newLen = (lastfit + lastlong) / 2;

                if (shortText.Length != newLen)
                {
                    shortText = value.Substring(0, newLen);
                }
                else
                {
                    shortText = value.Substring(0, lastfit);
                    break;
                }

                var w = this.StringWidth(shortText + "\u2026");
                validArea = (width - 10 < w) && (w < width);
            }

            shortText += "\u2026";
        }

        return shortText;
    }

    private void BoxGotFocus(object sender, RoutedEventArgs e)
    {
        int index = this.Box.SelectionStart;
        this.Box.Text = this.textvalue;
        this.Box.SelectionStart = index;
        this.Box.TextChanged += this.BoxOnTextChanged;
    }

    private void BoxOnTextChanged(object sender, TextChangedEventArgs args)
    {
        if (sender != this.Box || args.Changes.Count <= 0 || this.UpdateTrigger != UpdateSourceTrigger.PropertyChanged)
        {
            return;
        }

        this.UpdateVM();
    }

    private void UpdateVM()
    {
        this.Text = this.Box.Text;

        ////var exp = BindingOperations.GetBindingExpression(this, TextProperty);
        ////if (exp != null)
        ////{
        ////    exp.UpdateSource();
        ////    bool he = exp.HasError;
        ////}
    }

    private void BoxLostFocus(object sender, RoutedEventArgs e)
    {
        this.Box.TextChanged -= this.BoxOnTextChanged;
        this.UpdateVM();
        this.TextValue = this.Box.Text;
        ToolTipService.SetToolTip(this.Box, this.textvalue);
    }

    private double StringWidth(string s)
    {
        if (s == " ")
        {
            s = "\u00a0";
        }

        var formattedText = new FormattedText(
                s,
                CultureInfo.CurrentUICulture,
                FlowDirection.LeftToRight,
                new Typeface(this.Box.FontFamily, this.Box.FontStyle, this.Box.FontWeight, this.Box.FontStretch),
                this.Box.FontSize,
                Brushes.Black);

        return formattedText.Width;

    }

    private void BoxOnLayoutUpdated(object sender, EventArgs e)
    {
        if (!this.Box.IsFocused)
        {
            var width = this.StringWidth(this.Box.Text);
            if (width > this.Box.ActualWidth || (width + 10 < this.Box.ActualWidth && this.Box.Text != this.TextValue))
            {
                this.Box.Text = this.CutTextToWidth(this.TextValue);
            }
        }
    }

    #endregion

    private void EditUC_OnLoaded(object sender, RoutedEventArgs e)
    {
        this.TextValue = this.Text;

        var exp = BindingOperations.GetBindingExpression(this, TextProperty);
        var parent = exp != null ? exp.ParentBinding : null;
        this.UpdateTrigger = parent != null ? parent.UpdateSourceTrigger : UpdateSourceTrigger.Default;

        if (this.UpdateTrigger == UpdateSourceTrigger.Default)
        {
            var def = TextProperty.GetMetadata(this) as FrameworkPropertyMetadata;
            if (def != null)
            {
                this.UpdateTrigger = def.DefaultUpdateSourceTrigger;
            }
        }

        this.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(OnErrorEvent));
    }

    private void OnErrorEvent(object sender, RoutedEventArgs routedEventArgs)
    {
        if (sender == null)
        {
            return;
        }
    }
}

With the following code I get a red border around the whole user control in case of validation errors.

<local:EditUC Text="{Binding Text, ValidatesOnDataErrors=True}"/>

But I only want to have an error adorner around the TextBox.

  1. Has anyone a solution for my problem?

I set the content of the TextBox "Box" manually.

  1. Is there a possibility to use a binding TextProperty to Box.Text?
  2. Will this help to

Last but not least my viewmodel:

public class MainVM : INotifyPropertyChanged, IDataErrorInfo
{
    private string text;

    public string Text
    {
        get
        {
            return this.text;
        }

        set
        {
            if (this.text == value)
            {
                return;
            }

            this.text = value;
            this.OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public string this[string columnName]
    {
        get
        {
            return this.Validate(columnName);
        }
    }

    public string Error
    {
        get
        {
            return "asd";
        }
    }

    private string Validate(string properyName)
    {
        string msg = string.Empty;
        switch (properyName)
        {
            case "Text":
                msg = this.Text == "Valid" ? string.Empty : "Error";
                break;
        }
        return msg;
    }
}

Thanks a lot for your help.

Best regards Christian

0条回答
登录 后发表回答