简体   繁体   中英

C# WPF DataGrid: how to color paint text inside DataGrid cells without breaking the DataGrid iteslf?

I'm developing an application for searching some text in a batch of bilingual (source/translation) XML files (Trados SDLXLIFF for that matter). Since I want to be able to do quick edit in the translation text from search results, I have chosen WPF DataGrid to present the search results.

Among other things, I also want to highlight search phrase inside the search results with yellow background, and also to highlight with red font internal tags/text formatting placeholders that source/translation text may contain. After googling it I found this post and implemented the suggestion in my code.

At first glance everything worked fine, but then I noticed that with a large number of search results when DataGrid needs to be scrolled it begins to display some random text from my search results, and each time when I scroll the DataGrid up and down it shows different text in cells where color painting was applied. Essentially, applying color painting to DataGrid cells breaks visual consistency of the DataGrid.

To illustrate the issue I created a simple WFP application.

XAML:

<Window x:Class="WPF.Tutorial.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=system"
        Title="MainWindow" Height="480" Width="640" WindowStartupLocation="CenterScreen">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>            
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <DataGrid x:Name="testGrid" Grid.Row="0" AutoGenerateColumns="False" Background="White" CanUserAddRows="False" CanUserDeleteRows="False" Margin="2" SelectionUnit="FullRow" SelectionMode="Single">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Path=ID}" Header="ID" Width="30" IsReadOnly="True" />
                <DataGridTemplateColumn Header="Source" Width="*" IsReadOnly="True">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock x:Name="sourceTextBlock" Text="{Binding Path=SourceText}" TextWrapping="Wrap" Loaded="onTextLoaded"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>                    
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Target" Width="*">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock x:Name="targetTextBlock" Text="{Binding Path=TargetText}" TextWrapping="Wrap" Loaded="onTextLoaded"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox x:Name="targetTextBox" Text="{Binding Path=TargetText}" TextWrapping="Wrap" FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>        
        <Label x:Name="statusLabel" Grid.Row="1" />
    </Grid>
</Window>

C#:

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.ComponentModel;

namespace WPF.Tutorial
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();            
            for (int i = 1; i <= 100; i++)
            {
                testGrid.Items.Add(new Segment() {  ID = i.ToString(),
                                                    SourceText = String.Format("Segment <b>{0}</b>", i),
                                                    TargetText = String.Format("Сегмент <b>{0}</b>", i) });
            }
            statusLabel.Content = String.Format("Items: {0}", testGrid.Items.Count);           
        }

        // Text highlighting
        private void HighlightText(TextBlock tb)
        {   
            // The search pattern we need to highlight               
            string searchText = "сегмент";            
            var regex = new Regex("(" + searchText + ")", RegexOptions.IgnoreCase);
            // We want to highlight tags inside text
            var tagRegex = new Regex("(<[^>]*>)", RegexOptions.IgnoreCase);                            
            string[] pieces = tagRegex.Split(tb.Text);
            var subpieces = new List<string>();
            foreach (var piece in pieces)
            {
                subpieces.AddRange(regex.Split(piece));
            }            
            tb.Inlines.Clear();
            foreach (var item in subpieces)
            {
                // We don't want to highlight search patterns inside tags
                if (regex.Match(item).Success && !tagRegex.Match(item).Success)
                {
                    Run runx = new Run(item);
                    runx.Background = Brushes.Yellow;
                    tb.Inlines.Add(runx);
                }
                else if (tagRegex.Match(item).Success)
                {
                    Run runx = new Run(item);
                    runx.Foreground = Brushes.Red;
                    tb.Inlines.Add(runx);
                }
                else
                {
                    tb.Inlines.Add(item);
                }
            }           
        }

        private void onTextLoaded(object sender, EventArgs e)
        {          
            var tb = sender as TextBlock;
            if (tb != null)
            {
                HighlightText(tb);
            }                
        }
    }

    class Segment : IEditableObject
    {
        string targetBackup = null;

        public string ID { get; set; }
        public string SourceText { get; set; }
        public string TargetText { get; set; }

        public void BeginEdit()
        {
            if (targetBackup == null)
                targetBackup = TargetText;
        }

        public void CancelEdit()
        {
            if (targetBackup != null)
            {
                TargetText = targetBackup;
                targetBackup = null;
            }
        }

        public void EndEdit()
        {
            if (targetBackup != null)
                targetBackup = null;
        }
    }
}

Launch the application and then scroll the DataGrid repeatedly up and down, and you will see that with each scroll the DataGrid shows random text in cells that have been painted.

I've done some experiments and can assure you that it does not matter how you add data to the DataGrid: either directly as in this example or via a bound data collection. It does not matter how you apply text painting: either via connected "Loaded" events of TextBlocks (as in this example) or first adding data to the DataGrid and then walking over cells and applying painting to each cell individually. (For traversing the DataGrid cell by cell I used the code from here ). As soon as the DataGrid is color-painted, it is broken.

UPD : I found out that the DataGrid contents get broken even if I just replace TextBlock.Inlines contents with a new Run object containing the same text with no coloring at all. So essentially a bound TextBlock in a DataGrid gets broken if we try to manipulate with its Inlines collection.

So my question is: how to apply color painting to text in WPF DataGrid cells without breaking visual consistency of this DataGrid?

After some googling I've found another solution for color-painting text inside a TextBlock. I subclassed TexbBlock to make its InlineCollection accessible via a new RichText bindable property and then wrote a value converter to convert plain text to a collection of rich text inlines based on regular expressions.

The demonstration code is below.

XAML:

<Window x:Class="WPF.Tutorial.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=system"
        xmlns:local="clr-namespace:WPF.Tutorial"
        Title="MainWindow" Height="480" Width="640" WindowStartupLocation="CenterScreen">
    <Window.Resources>
        <local:RichTextValueConverter x:Key="RichTextValueConverter" />
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>            
            <RowDefinition Height="30"/>
        </Grid.RowDefinitions>
        <DataGrid x:Name="testGrid" Grid.Row="0" AutoGenerateColumns="False" Background="White" CanUserAddRows="False" CanUserDeleteRows="False" Margin="2" SelectionUnit="FullRow" SelectionMode="Single">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Path=ID}" Header="ID" Width="30" IsReadOnly="True" />
                <DataGridTemplateColumn Header="Source" Width="*" IsReadOnly="True">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <local:RichTextBlock x:Name="sourceTextBlock" TextWrapping="Wrap" 
                                                 RichText="{Binding Path=SourceText, Converter={StaticResource RichTextValueConverter}}"  />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>                    
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Target" Width="*">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <local:RichTextBlock x:Name="targetTextBlock" TextWrapping="Wrap" 
                                                 RichText="{Binding Path=TargetText, Converter={StaticResource RichTextValueConverter}}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox x:Name="targetTextBox" Text="{Binding Path=TargetText}" TextWrapping="Wrap" FocusManager.FocusedElement="{Binding RelativeSource={RelativeSource Self}}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>        
        <Label x:Name="statusLabel" Grid.Row="1" />
    </Grid>
</Window>

C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.ComponentModel;

namespace WPF.Tutorial
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public static Regex SearchRegex = new Regex("(segment)", RegexOptions.IgnoreCase);
        public static Regex TagRegex = new Regex("(<[^>]*>)", RegexOptions.IgnoreCase);

        ObservableCollection<Segment> segments;

        public MainWindow()
        {
            InitializeComponent();
            segments = new ObservableCollection<Segment>();
            testGrid.ItemsSource = segments;
            for (int i = 1; i <= 100; i++)
            {
                segments.Add(new Segment()
                {
                    ID = i.ToString(),
                    SourceText = String.Format("Segment <b>{0}</b>", i),
                    TargetText = String.Format("Сегмент <b>{0}</b>", i)
                });
            }
            statusLabel.Content = String.Format("Items: {0}", testGrid.Items.Count);
        }


        public class Segment : IEditableObject
        {
            string targetBackup = null;

            public string ID { get; set; }
            public string SourceText { get; set; }
            public string TargetText { get; set; }

            public void BeginEdit()
            {
                if (targetBackup == null)
                    targetBackup = TargetText;
            }

            public void CancelEdit()
            {
                if (targetBackup != null)
                {
                    TargetText = targetBackup;
                    targetBackup = null;
                }
            }

            public void EndEdit()
            {
                if (targetBackup != null)
                    targetBackup = null;
            }
        }
    }


    public class RichTextBlock : TextBlock
    {
        public static DependencyProperty InlineProperty;

        static RichTextBlock()
        {
            //OverrideMetadata call tells the system that this element wants to provide a style that is different than in base class
            DefaultStyleKeyProperty.OverrideMetadata(typeof(RichTextBlock), new FrameworkPropertyMetadata(
                                typeof(RichTextBlock)));
            InlineProperty = DependencyProperty.Register("RichText", typeof(List<Inline>), typeof(RichTextBlock),
                            new PropertyMetadata(null, new PropertyChangedCallback(OnInlineChanged)));
        }
        public List<Inline> RichText
        {
            get { return (List<Inline>)GetValue(InlineProperty); }
            set { SetValue(InlineProperty, value); }
        }

        public static void OnInlineChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (e.NewValue == e.OldValue)
                return;
            RichTextBlock r = sender as RichTextBlock;
            List<Inline> i = e.NewValue as List<Inline>;
            if (r == null || i == null)
                return;
            r.Inlines.Clear();
            foreach (Inline inline in i)
            {
                r.Inlines.Add(inline);
            }
        }
    }


    class RichTextValueConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string text = value as string;
            var inlines = new List<Inline>();
            if (text != null)
            {
                string[] pieces = MainWindow.TagRegex.Split(text);
                var subpieces = new List<string>();
                foreach (var piece in pieces)
                {
                    subpieces.AddRange(MainWindow.SearchRegex.Split(piece));
                }
                foreach (var item in subpieces)
                {
                    if (MainWindow.SearchRegex.Match(item).Success && !MainWindow.TagRegex.Match(item).Success)
                    {
                        Run runx = new Run(item);
                        runx.Background = Brushes.Yellow;
                        inlines.Add(runx);
                    }
                    else if (MainWindow.TagRegex.Match(item).Success)
                    {
                        Run runx = new Run(item);
                        runx.Foreground = Brushes.Red;
                        inlines.Add(runx);
                    }
                    else
                    {
                        inlines.Add(new Run(item));
                    }
                }
            }
            return inlines;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException("Back conversion is not supported!");
        }
    }
}

Now color painting works fine on a DataGrid with a large numbers of rows.

PS. Some guy from MSDN forums also suggested to set VirtualizingPanel.IsVirtualizing property of a DataGrid to false . This solution works with the original code but, as I understand, this variant is not very good performance-wise.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM