如何使用mvvm在wpf中创建带省略号的路径修剪文本框?

时间:2017-12-25 15:00:12

标签: wpf mvvm trim

我想创建一个texbox,里面有一个目录/文件路径。如果目录路径太长,文本应该看起来用省略号修剪,我希望省略号出现在路径字符串的中间,例如,D:\Directory1\Directory2\Directory3可以修剪为D:\...\Directory3。 路径本身应绑定到ViewModel,以便可以在MVVM模型中使用。

1 个答案:

答案 0 :(得分:-1)

我最近遇到过这个问题,所以我决定在这里分享我的解决方案。 首先受到这个帖子的启发How to create a file path Trimming TextBlock with Ellipsis我决定创建我的自定义TextBlock,它将用省略号修剪其文本,这是实现,我写了注释以便代码清晰:

using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace PathTrimming.Controls
{
    public class PathTrimmingTextBlock : TextBlock, INotifyPropertyChanged
    {
        #region Dependency properties
        //This property represents the Text of this textblock that can be bound to another viewmodel property, 
        //whenever this property is updated the Text property will be updated too.
        //We cannot bind to Text property directly because once we update Text, e.g., Text = "NewValue", the binding will be broken
        public string BoundedText
        {
            get { return GetValue(BoundedTextProperty).ToString(); }
            set { SetValue(BoundedTextProperty, value); }
        }

        public static readonly DependencyProperty BoundedTextProperty = DependencyProperty.Register(
            nameof(BoundedText), typeof(string), typeof(PathTrimmingTextBlock),
            new PropertyMetadata(string.Empty, new PropertyChangedCallback(BoundedTextProperty_Changed)));

        //Every time the property BoundedText is updated two things should be done:
        //1) Text should be updated to be equal to new BoundedText
        //2) New path should be trimmed again
        private static void BoundedTextProperty_Changed(object sender, DependencyPropertyChangedEventArgs e)
        {
            var pathTrimmingTextBlock = (PathTrimmingTextBlock)sender;
            pathTrimmingTextBlock.OnPropertyChanged(nameof(BoundedText));
            pathTrimmingTextBlock.Text = pathTrimmingTextBlock.BoundedText;
            pathTrimmingTextBlock.TrimPathAsync();
        }
        #endregion

        private const string Ellipsis = "...";


        public PathTrimmingTextBlock()
        {
            // This will make sure if the directory name is too long it will be trimmed with ellipsis on the right side
            TextTrimming = TextTrimming.CharacterEllipsis;

            //setting the event handler for every time this PathTrimmingTextBlock is rendered
            Loaded += new RoutedEventHandler(PathTrimmingTextBox_Loaded);
        }

        private void PathTrimmingTextBox_Loaded(object sender, RoutedEventArgs e)
        {
            //asynchronously update Text, so that the window won't be frozen
            TrimPathAsync();
        }

        private void TrimPathAsync()
        {
            Task.Run(() => Dispatcher.Invoke(() => TrimPath()));
        }

        private void TrimPath()
        {
            var isWidthOk = false; //represents if the width of the Text is short enough and should not be trimmed 
            var widthChanged = false; //represents if the width of Text was changed, if the text is short enough at the begging it should not be trimmed
            var wasTrimmed = false; //represents if Text was trimmed at least one time

            //in this loop we will be checking the current width of textblock using FormattedText at every iteration,
            //if the width is not short enough to fit textblock it will be shrinked by one character, and so on untill it fits
            do
            {
                //widthChanged? Text + Ellipsis : Text - at first iteration we have to check if Text is not already short enough to fit textblock,
                //after widthChanged = true, we will have to measure the width of Text + Ellipsis, because ellipsis will be added to Text
                var formattedText = new FormattedText(widthChanged ? Text + Ellipsis : Text,
                    CultureInfo.CurrentCulture,
                    FlowDirection.LeftToRight,
                    new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
                    FontSize,
                    Foreground);

                //check if width fits textblock RenderSize.Width, (cannot use Width here because it's not set during rendering,
                //and cannot use ActualWidth either because it is the initial width of Text not textblock itself)
                isWidthOk = formattedText.Width < RenderSize.Width;

                //if it doesn't fit trim it by one character
                if (!isWidthOk)
                {
                    wasTrimmed = TrimPathByOneChar();
                    widthChanged = true;
                }
                //continue loop
            } while (!isWidthOk && wasTrimmed);

            //Format Text with ellipsis, if width was changed (after previous loop we may have gotten a path like this "D:\Dire\Directory" 
            //it should be formatted to "D:\...\Directory")
            if (widthChanged)
            {
                FormatWithEllipsis();
            }
        }

        //Trim Text by one character before last slash, if Text doesn't have slashes it won't be trimmed with ellipsis in the middle,
        //instead it will be trimmed with ellipsis at the end due to having TextTrimming = TextTrimming.CharacterEllipsis; in the constructor
        private bool TrimPathByOneChar()
        {
            var lastSlashIndex = Text.LastIndexOf('\\');
            if (lastSlashIndex > 0)
            {
                Text = Text.Substring(0, lastSlashIndex - 1) + Text.Substring(lastSlashIndex);
                return true;
            }
            return false;
        }

        //"\Directory will become "...\Directory"
        //"Dire\Directory will become "...\Directory"\
        //"D:\Dire\Directory" will become "D:\...\Directory"
        private void FormatWithEllipsis()
        {
            var lastSlashIndex = Text.LastIndexOf('\\');
            if (lastSlashIndex == 0)
            {
                Text = Ellipsis + Text;
            }
            else if (lastSlashIndex > 0)
            {
                var secondastSlashIndex = Text.LastIndexOf('\\', lastSlashIndex - 1);
                if (secondastSlashIndex < 0)
                {
                    Text = Ellipsis + Text.Substring(lastSlashIndex);
                }
                else
                {
                    Text = Text.Substring(0, secondastSlashIndex + 1) + Ellipsis + Text.Substring(lastSlashIndex);
                }
            }
        }

        //starndard implementation of INotifyPropertyChanged to be able to notify BoundedText property change 
        #region INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
    }
}

现在我们创建了texblock后,我们必须以某种方式&#34; wire&#34;它位于TextBox的{​​{1}},可以使用XAML完成。这是完整的ControlTemplate代码,我再次撰写评论,因此应该很容易理解:

XAML

现在最后剩下的就是编写负责将数据提供给<Window x:Class="PathTrimming.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:viewmodel = "clr-namespace:PathTrimming.ViewModel" xmlns:controls="clr-namespace:PathTrimming.Controls" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <!-- Assigning datacontext to the window --> <Window.DataContext> <viewmodel:MainViewModel/> </Window.DataContext> <Window.Resources> <ResourceDictionary> <!--This is the most important part, if TextBox is not in focused, it will be rendered as PathTrimmingTextBlock, if it is focused it shouldn't be trimmed and will be rendered as default textbox. To achieve this I'm using DataTrigger and ControlTemplate--> <Style x:Key="TextBoxDefaultStyle" TargetType="{x:Type TextBox}"> <Style.Triggers> <DataTrigger Binding="{Binding IsKeyboardFocused, RelativeSource={RelativeSource Self}}" Value="False"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="TextBox"> <Border BorderThickness="1" BorderBrush="#000"> <controls:PathTrimmingTextBlock BoundedText="{TemplateBinding Text}"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </DataTrigger> </Style.Triggers> </Style> </ResourceDictionary> </Window.Resources> <!--Grid with two textboxes and button that updates the textboxes with new pathes from a random path pool--> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBox Grid.Row="0" Grid.Column="0" Width="100" Text="{Binding Path1}" Style="{StaticResource TextBoxDefaultStyle}"/> <TextBox Grid.Row="1" Grid.Column="0" Width="100" Text="{Binding Path2}" Style="{StaticResource TextBoxDefaultStyle}"/> <Button Grid.Row="2" Content="Update pathes" Command="{Binding UpdatePathesCmd}"/> </Grid> </Window> 的{​​{1}}。在这里,我使用ViewModel库来简化代码,但这并不重要,使用任何其他方法应该可以正常工作。 这是带注释的代码,无论如何应该是非常自我解释的:

View