I have TextBlock that has Inlines dynamicly added to it (basically bunch of Run objects that are either italic or bold).
In my application I have search function.
I want to be able to highlight TextBlock's text that is in being searched for.
By highlighting I mean changing certain parts of TextBlock text's color (keeping in mind that it may highlight several different Run objects at a time).
I have tried this example http://blogs.microsoft.co.il/blogs/tamir/archive/2008/05/12/search-and-highlight-any-text-on-wpf-rendered-page.aspx
But it seams very unstable :(
Is there easy way to solve this problem?
This question is similar to How to display search results in a WPF items control with highlighted query terms
In answer to that question, I came up with an approach that uses an IValueConverter. The converter takes a text snippet, formats it into valid XAML markup, and uses a XamlReader to instantiate the markup into framework objects.
The full explanation is rather long, so I've posted it to my blog: Highlighting Query Terms in a WPF TextBlock
I took dthrasers answer and took out the need for an XML parser. He does a great job explaining each of the pieces in his blog, However this didn't require me to add any extra libraries, here's how I did it.
Step one, make a converter class:
class StringToXamlConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string input = value as string;
if (input != null)
{
var textBlock = new TextBlock();
textBlock.TextWrapping = TextWrapping.Wrap;
string escapedXml = SecurityElement.Escape(input);
while (escapedXml.IndexOf("|~S~|") != -1) {
//up to |~S~| is normal
textBlock.Inlines.Add(new Run(escapedXml.Substring(0, escapedXml.IndexOf("|~S~|"))));
//between |~S~| and |~E~| is highlighted
textBlock.Inlines.Add(new Run(escapedXml.Substring(escapedXml.IndexOf("|~S~|") + 5,
escapedXml.IndexOf("|~E~|") - (escapedXml.IndexOf("|~S~|") + 5)))
{ FontWeight = FontWeights.Bold, Background= Brushes.Yellow });
//the rest of the string (after the |~E~|)
escapedXml = escapedXml.Substring(escapedXml.IndexOf("|~E~|") + 5);
}
if (escapedXml.Length > 0)
{
textBlock.Inlines.Add(new Run(escapedXml));
}
return textBlock;
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException("This converter cannot be used in two-way binding.");
}
}
Step two:
Instead of a TextBlock use a ContentBlock. Pass in the string (you would of used for your textBlock) to the content block, like so:
<ContentControl Margin="7,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{Binding Description, Converter={StaticResource CONVERTERS_StringToXaml}, Mode=OneTime}">
</ContentControl>
Step three:
Make sure the text you pass includes |~S~| before and |~E~| after the text part you want to be highlighted. For example in this string "my text |~S~|is|~E~| good" the is will be highlighted in yellow.
Notes:
You can change the style in the run to determine what and how your text is highlighted
Make sure you add your Converter class to your namespace and resources. This might also require a rebuild to get working.
Differences to other solutions
easier to reuse -> attached behavior instead of custom control
MVVM friendly -> no code behind
works BOTH ways! -> Changing the term to be highlighted OR the text, both updates the highlight in the textblock. The other solutions i checked had the problem, that changing the text does not reapply the highlighting. Only changing the highlighted term/search text worked.
How to use
IMPORTANT: do NOT use the regular Text="blabla" property of the TextBlock anymore. Instead bind your text to HighlightTermBehavior.Text="blabla".
Add the attached properties to your TextBlock like that
<TextBlock local:HighlightTermBehavior.TermToBeHighlighted="{Binding MyTerm}"
local:HighlightTermBehavior.Text="{Binding MyText}" />
or hardcoded
<TextBlock local:HighlightTermBehavior.TermToBeHighlighted="highlight this"
local:HighlightTermBehavior.Text="bla highlight this bla" />
Add this class
To change the kind of highlighting, just change these Methods:
AddPartToTextBlock() for non highlighted text
AddHighlightedPartToTextBlock() for the highlighted text.
At the moment highlighted is FontWeights.ExtraBold and non highlighted text is FontWeights.Light.
probably hard to read without an IDE, sorry.
public static class HighlightTermBehavior
{
public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
"Text",
typeof(string),
typeof(HighlightTermBehavior),
new FrameworkPropertyMetadata("", OnTextChanged));
public static string GetText(FrameworkElement frameworkElement) => (string) frameworkElement.GetValue(TextProperty);
public static void SetText(FrameworkElement frameworkElement, string value) => frameworkElement.SetValue(TextProperty, value);
public static readonly DependencyProperty TermToBeHighlightedProperty = DependencyProperty.RegisterAttached(
"TermToBeHighlighted",
typeof(string),
typeof(HighlightTermBehavior),
new FrameworkPropertyMetadata("", OnTextChanged));
public static string GetTermToBeHighlighted(FrameworkElement frameworkElement)
{
return (string) frameworkElement.GetValue(TermToBeHighlightedProperty);
}
public static void SetTermToBeHighlighted(FrameworkElement frameworkElement, string value)
{
frameworkElement.SetValue(TermToBeHighlightedProperty, value);
}
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBlock textBlock)
SetTextBlockTextAndHighlightTerm(textBlock, GetText(textBlock), GetTermToBeHighlighted(textBlock));
}
private static void SetTextBlockTextAndHighlightTerm(TextBlock textBlock, string text, string termToBeHighlighted)
{
textBlock.Text = string.Empty;
if (TextIsEmpty(text))
return;
if (TextIsNotContainingTermToBeHighlighted(text, termToBeHighlighted))
{
AddPartToTextBlock(textBlock, text);
return;
}
var textParts = SplitTextIntoTermAndNotTermParts(text, termToBeHighlighted);
foreach (var textPart in textParts)
AddPartToTextBlockAndHighlightIfNecessary(textBlock, termToBeHighlighted, textPart);
}
private static bool TextIsEmpty(string text)
{
return text.Length == 0;
}
private static bool TextIsNotContainingTermToBeHighlighted(string text, string termToBeHighlighted)
{
return text.Contains(termToBeHighlighted, StringComparison.Ordinal) == false;
}
private static void AddPartToTextBlockAndHighlightIfNecessary(TextBlock textBlock, string termToBeHighlighted, string textPart)
{
if (textPart == termToBeHighlighted)
AddHighlightedPartToTextBlock(textBlock, textPart);
else
AddPartToTextBlock(textBlock, textPart);
}
private static void AddPartToTextBlock(TextBlock textBlock, string part)
{
textBlock.Inlines.Add(new Run {Text = part, FontWeight = FontWeights.Light});
}
private static void AddHighlightedPartToTextBlock(TextBlock textBlock, string part)
{
textBlock.Inlines.Add(new Run {Text = part, FontWeight = FontWeights.ExtraBold});
}
public static List<string> SplitTextIntoTermAndNotTermParts(string text, string term)
{
if (text.IsNullOrEmpty())
return new List<string>() {string.Empty};
return Regex.Split(text, $#"({Regex.Escape(term)})")
.Where(p => p != string.Empty)
.ToList();
}
}
By strange coincidence, I have recently written an article that solves the very same problem. It is a custom control that has the same properties as a TextBlock (so you can swap is out for a TextBlock wherever you need it), and it has an extra Property that you can bind to called HighLightText, and wherever the value of HighLightText is found in the main Text property (case insensitive), it is highlighted.
It was a fairly straight-forward control to create, and you can find the full code as a solution here:
SearchMatchTextblock(GitHub)
Here is what I came up with by building off of the exisiting TextBlock and adding a new dependency property named SearchText:
public class SearchHightlightTextBlock : TextBlock
{
public SearchHightlightTextBlock() : base() { }
public String SearchText { get { return (String)GetValue(SearchTextProperty); }
set { SetValue(SearchTextProperty, value); } }
private static void OnDataChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
TextBlock tb = (TextBlock)source;
if (tb.Text.Length == 0)
return;
string textUpper = tb.Text.ToUpper();
String toFind = ((String) e.NewValue).ToUpper();
int firstIndex = textUpper.IndexOf(toFind);
String firstStr = tb.Text.Substring(0, firstIndex);
String foundStr = tb.Text.Substring(firstIndex, toFind.Length);
String endStr = tb.Text.Substring(firstIndex + toFind.Length,
tb.Text.Length - (firstIndex + toFind.Length));
tb.Inlines.Clear();
var run = new Run();
run.Text = firstStr;
tb.Inlines.Add(run);
run = new Run();
run.Background = Brushes.Yellow;
run.Text = foundStr;
tb.Inlines.Add(run);
run = new Run();
run.Text = endStr;
tb.Inlines.Add(run);
}
public static readonly DependencyProperty SearchTextProperty =
DependencyProperty.Register("SearchText",
typeof(String),
typeof(SearchHightlightTextBlock),
new FrameworkPropertyMetadata(null, OnDataChanged));
}
And in your view, this:
<view:SearchHightlightTextBlock SearchText="{Binding TextPropertyContainingTextToSearch}"
Text="{Binding YourTextProperty}"/>
Here I present another Approach for highlighting text. I had a use case where I needed to decorate a bunch of C# Code in WPF, however I did not want to use textBlock.Inlines.Add type of syntax, instead I wanted to generate the highlighting XAML on the fly and then dynamically add it to a Canvas or some other container in WPF.
So suppose you want to colorize the following piece of code and also highlight a part of it:
public static void TestLoop(int count)
{
for(int i=0;i<count;i++)
Console.WriteLine(i);
}
Suppose the above code is found in a file called Test.txt .
Suppose you want to colorize all the C# keywords (public, static, void etc..) and simple types(int, string) in Blue, and Console.WriteLine highlight in yellow.
Step 0. Create a new WPF Application and include some sample code similar to above in a file called Test.txt
Step 1. Create a Code Highlighter class:
using System.IO;
using System.Text;
public enum HighLightType
{
Type = 0,
Keyword = 1,
CustomTerm = 2
}
public class CodeHighlighter
{
public static string[] KeyWords = { "public", "static", "void", "return", "while", "for", "if" };
public static string[] Types = { "string", "int", "double", "long" };
private string FormatCodeInXaml(string code, bool withLineBreak)
{
string[] mapAr = { "<","<" , //Replace less than sign
">",">" }; //Replace greater than sign
StringBuilder sb = new StringBuilder();
using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(code))))
{
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
line = line.Replace("\t", " "); //Replace tabs
line = line.Replace(" ", " "); //Replace spaces
for (int i = 0; i < mapAr.Length; i += 2)
line = line.Replace(mapAr[i], mapAr[i + 1]);
if (withLineBreak)
sb.AppendLine(line + "<LineBreak/>"); //Replace line breaks
else
sb.AppendLine(line);
}
}
return sb.ToString();
}
private string BuildForegroundTag(string highlightText, string color)
{
return "<Span Foreground=\"" + color + "\">" + highlightText + "</Span>";
}
private string BuildBackgroundTag(string highlightText, string color)
{
return "<Span Background=\"" + color + "\">" + highlightText + "</Span>";
}
private string HighlightTerm(HighLightType type, string term, string line)
{
if (term == string.Empty)
return line;
string keywordColor = "Blue";
string typeColor = "Blue";
string statementColor = "Yellow";
if (type == HighLightType.Type)
return line.Replace(term, BuildForegroundTag(term, typeColor));
if (type == HighLightType.Keyword)
return line.Replace(term, BuildForegroundTag(term, keywordColor));
if (type == HighLightType.CustomTerm)
return line.Replace(term, BuildBackgroundTag(term, statementColor));
return line;
}
public string ApplyHighlights(string code, string customTerm)
{
code = FormatCodeInXaml(code, true);
customTerm = FormatCodeInXaml(customTerm, false).Trim();
StringBuilder sb = new StringBuilder();
using (StreamReader sr = new StreamReader(new MemoryStream(Encoding.UTF8.GetBytes(code))))
{
while (!sr.EndOfStream)
{
string line = sr.ReadLine();
line = HighlightTerm(HighLightType.CustomTerm, customTerm, line);
foreach (string keyWord in KeyWords)
line = HighlightTerm(HighLightType.Keyword, keyWord, line);
foreach (string type in Types)
line = HighlightTerm(HighLightType.Type, type, line);
sb.AppendLine(line);
}
}
return sb.ToString();
}
}
Step 2. Add a Canvas XAML tag to your MainWindow.xaml
<Window x:Class="TestCodeVisualizer.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:TestCodeVisualizer"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Canvas Name="canvas" />
</Window>
Step 3. In Your WPF Application add the following code: (make sure that test.txt is in the correct location) :
using System.Text;
using System.IO;
using System.Windows;
using System.Windows.Markup;
namespace TestCodeVisualizer
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
string testText = File.ReadAllText("Test.txt");
FrameworkElement fe = GenerateHighlightedTextBlock(testText, "Console.WriteLine");
this.canvas.Children.Add(fe);
}
private FrameworkElement GenerateHighlightedTextBlock(string code, string term)
{
CodeHighlighter ch = new CodeHighlighter();
string uc = "<UserControl xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>[CONTENT]</UserControl>";
string content = "<TextBlock>" + ch.ApplyHighlights(code, term) + "</TextBlock>";
uc = uc.Replace("[CONTENT]", content);
FrameworkElement fe = XamlReader.Load(new System.IO.MemoryStream(Encoding.UTF8.GetBytes(uc))) as FrameworkElement;
return fe;
}
}
}
I had a similar problem - trying to implement a text search over a load of presenters that basically represent a report. The report was originally written into a string and we were leveraging FlowDocumentViewer's built in ctrl-F - it's not very good and has some wierd options but was sufficient.
If you just want something like that you can do the following:
<FlowDocumentScrollViewer>
<FlowDocument>
<Paragraph FontFamily="Lucida Console" FontSize="12">
<Run Text="{Binding Content, Mode=OneWay}"/>
</Paragraph>
</FlowDocument>
</FlowDocumentScrollViewer>
We decided to go for a rewrite as the report is kept in sync with the rest of the program and basically every edit changes it, having to recreate the entire report everytime means that this is quite slow. We wanted to improve this by moving to a update-the-bits-you-need-to model but needed to have view model (rather than just a string) to be able to do that in a sane way! We wanted to preserve the searching functionality before swapping out the report however and go one better and have highlighting of the 'current' search position in one colour and other search hits in another.
Here's a simplified version of my solution; a class that derives from TextBlock that adds a dependency property of Type HighlightingInformation. I've not included the namespace and usings as they are sensitive.
public class HighlightingTextBlock : TextBlock
{
public static readonly DependencyProperty HighlightingProperty =
DependencyProperty.Register("Highlighting", typeof (HighlightingInformation), typeof (HighlightingTextBlock));
public HighlightingInformation Highlighting
{
get { return (HighlightingInformation)GetValue(HighlightingProperty); }
set { SetValue(HighlightingProperty, value); }
}
public HighlightingTextBlock()
{
AddValueChangedCallBackTo(HighlightingProperty, UpdateText);
}
private void AddValueChangedCallBackTo(DependencyProperty property, Action updateAction)
{
var descriptor = DescriptorFor(property);
descriptor.AddValueChanged(this, (src, args) => updateAction());
}
private DependencyPropertyDescriptor DescriptorFor(DependencyProperty property)
{
return DependencyPropertyDescriptor.FromProperty(property, GetType());
}
private void UpdateText()
{
var highlighting = Highlighting;
if (highlighting == null)
return;
highlighting.SetUpdateMethod(UpdateText);
var runs = highlighting.Runs;
Inlines.Clear();
Inlines.AddRange(runs);
}
}
The type this class can be bound to uses the update method when it's text and list of highlights are changed to update the list of Runs. The highlights themselves look something like this:
public class Highlight
{
private readonly int _length;
private readonly Brush _colour;
public int Start { get; private set; }
public Highlight(int start, int length,Brush colour)
{
Start = start;
_length = length;
_colour = colour;
}
private string TextFrom(string currentText)
{
return currentText.Substring(Start, _length);
}
public Run RunFrom(string currentText)
{
return new Run(TextFrom(currentText)){Background = _colour};
}
}
To produce the correct collection of highlights is a seperate problem, which I basically solved by treating the collection of presenters as a Tree that you recursively search for content - leaf nodes are those that have content and other nodes just have children. If you search depth-first you get the order you'd expect. You can then basically write a wrapper around the list of results to keep track of the position. Im not going to post all the code for this - my response here it is to document how you can make wpf do multi-coloured highlighting in MVP style.
I haven't used INotifyPropertyChanged or CollectionChanged here as we didn't need the changes to be multi-cast (eg one presenter has multiple views). Initially I tried to do that by adding an event changed notification for Text and one for a list (which you also have to manually subscribe to the INotifyCollectionChanged event on). I had concerns about memory leaks from the event subcriptions however and the fact that the updates for the text and the highlights didn't come at the same time made it problematic.
The one drawback of this approach is that people shouldn't bind to the Text property of this control. In the real version I have added some checking + exception throwing to stop people from doing this but ommitted it from the example for clarity's sake!
Ended up writing following code
At moment has few bugs, but solves the problem
if (Main.IsFullTextSearch)
{
for (int i = 0; i < runs.Count; i++)
{
if (runs[i] is Run)
{
Run originalRun = (Run)runs[i];
if (Main.SearchCondition != null && originalRun.Text.ToLower()
.Contains(Main.SearchCondition.ToLower()))
{
int pos = originalRun.Text.ToLower()
.IndexOf(Main.SearchCondition.ToLower());
if (pos > 0)
{
Run preRun = CloneRun(originalRun);
Run postRun = CloneRun(originalRun);
preRun.Text = originalRun.Text.Substring(0, pos);
postRun.Text = originalRun.Text
.Substring(pos + Main.SearchCondition.Length);
runs.Insert(i - 1 < 0 ? 0 : i - 1, preRun);
runs.Insert(i + 1, new Run(" "));
runs.Insert(i + 2, postRun);
originalRun.Text = originalRun.Text
.Substring(pos, Main.SearchCondition.Length);
SolidColorBrush brush = new SolidColorBrush(Colors.Yellow);
originalRun.Background = brush;
i += 3;
}
}
}
}
}
If you are handling ContainerContentChanging for your ListViewBase, you can take the following approach: TextBlock highlighting for WinRT/ContainerContentChanging
Please note that this code is for Windows RT. The WPF syntax will be slightly different. Also note that if you are using binding to populate the TextBlock.Text property, the text generated by my approach will be overwritten. I use ContainerContentChanging to populate target fields because of radically-increased performance and improvements in memory usage, vs. normal binding. I use binding only to manage the source data, not the data view.
The following highlight search method takes your TextBlock and search term then returns your block with this term or words which contain this term highlighted purple.
private TextBlock HighlightSearch(TextBlock textBlock, string searchTerm)
{
string[] words = textBlock.Text.Split(' ');
textBlock.Text = string.Empty;
foreach (string word in words)
{
if (!string.IsNullOrEmpty(searchTerm) &&
word.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0)
{
textBlock.Inlines.Add(new Run($"{word} ") { Foreground = Brushes.Purple, FontWeight = FontWeights.DemiBold });
}
else
{
textBlock.Inlines.Add($"{word} ");
}
}
return textBlock;
}
`
The requirement I had was highlighting must be fully style-able beyond just a few pre-defined options:
public partial class HighlightTextBlock : UserControl
{
public HighlightTextBlock()
{
InitializeComponent();
}
public static readonly DependencyProperty TextBlockStyleProperty = DependencyProperty.Register(
nameof(TextBlockStyle), typeof(Style), typeof(HighlightTextBlock), new PropertyMetadata(default(Style)));
public Style TextBlockStyle
{
get { return (Style)GetValue(TextBlockStyleProperty); }
set { SetValue(TextBlockStyleProperty, value); }
}
public static readonly DependencyProperty HighlightTextElementStyleProperty = DependencyProperty.Register(
nameof(HighlightTextElementStyle), typeof(Style), typeof(HighlightTextBlock), new PropertyMetadata(default(Style)));
public Style HighlightTextElementStyle
{
get { return (Style)GetValue(HighlightTextElementStyleProperty); }
set { SetValue(HighlightTextElementStyleProperty, value); }
}
public static readonly DependencyProperty NormalTextElementStyleProperty = DependencyProperty.Register(
nameof(NormalTextElementStyle), typeof(Style), typeof(HighlightTextBlock), new PropertyMetadata(default(Style)));
public Style NormalTextElementStyle
{
get { return (Style)GetValue(NormalTextElementStyleProperty); }
set { SetValue(NormalTextElementStyleProperty, value); }
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
nameof(Text), typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(default(string), PropertyChangedCallback));
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty HighlightProperty = DependencyProperty.Register(
nameof(Highlight), typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(default(string), PropertyChangedCallback));
public string Highlight
{
get { return (string)GetValue(HighlightProperty); }
set { SetValue(HighlightProperty, value); }
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.EnsureType<HighlightTextBlock>().update();
}
private void update()
{
var highlightLength = this.Highlight?.Length ?? 0;
if (highlightLength > 0)
{
var highlightOffset = this.Text?.IndexOf(this.Highlight, StringComparison.InvariantCultureIgnoreCase) ?? -1;
if (highlightOffset > -1)
{
PrefixRun.Text = this.Text.Substring(0, highlightOffset);
HighlightRun.Text = this.Text.Substring(highlightOffset, highlightLength);
SuffixRun.Text = this.Text.Substring(highlightOffset + highlightLength);
return;
}
}
PrefixRun.Text = this.Text;
HighlightRun.Text = null;
SuffixRun.Text = null;
}
}
Mind PropertyChangedCallback used by HighlightProperty and TextProperty.
XAML:
<UserControl x:Class="Example.HighlightTextBlock"
x:Name="self"
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">
<Grid>
<TextBlock DataContext="{Binding ElementName=self}" Style="{Binding TextBlockStyle}">
<!-- NOTE: TO avoid whitespaces when rendering Inlines, avoid them in markup (e.g. between Run tags)-->
<TextBlock.Inlines><Run
x:Name="PrefixRun" x:FieldModifier="private" Style="{Binding NormalTextElementStyle}"/><Run
x:Name="HighlightRun" x:FieldModifier="private" Style="{Binding HighlightTextElementStyle}"/><Run
x:Name="SuffixRun" x:FieldModifier="private" Style="{Binding NormalTextElementStyle}"/></TextBlock.Inlines>
</TextBlock>
</Grid>
</UserControl>
DataTemplate:
<DataTemplate x:Key="ExampleDataTemplate">
<DataTemplate.Resources>
<Style x:Key="HighlightTextElementStyle" TargetType="{x:Type Inline}">
<Setter Property="Foreground" Value="DarkGray"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="TextDecorations" Value="Underline"/>
</Style>
<Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}" BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="TextAlignment" Value="Left"/>
</Style>
</DataTemplate.Resources>
<controls1:HighlightTextBlock Text="{Binding ExampleText}"
Highlight="{Binding ExampleHighlight}"
TextBlockStyle="{StaticResource TextBlockStyle}"
HighlightTextElementStyle="{StaticResource HighlightTextElementStyle}"/>
</DataTemplate>
Related
I have xaml with button and FlowDocumentScrollViewer:
<Button Content="My Button" Command="{Binding SomeButton}"/>
<FlowDocumentScrollViewer Document="{Binding FlowDocument}"/>
Now I want into VM add some logic:
private ICommand m_SomeButtonCommand ;
public ICommand SomeButton => m_SomeButtonCommand ?? (m_SomeButtonCommand = new RelayCommand(RunSM, true));
private void RunSM
{
FlowDocument flowDocument = new FlowDocument();
Paragraph paragraph = new Paragraph(new Run("some new Text"));
paragraph.Background = Brushes.White;
paragraph.Foreground = Brushes.Black;
paragraph.FontSize = 14;
m_flowDocumentScrollViewer = new FlowDocumentScrollViewer();
m_flowDocumentScrollViewer.Document = flowDocument;
}
private FlowDocumentScrollViewer m_flowDocumentScrollViewer;
public FlowDocumentScrollViewer FlowDocumentScrollViewer
{
get
{
return m_flowDocumentScrollViewer;
}
set
{
if (m_flowDocumentScrollViewer == value)
return;
m_flowDocumentScrollViewer = value;
OnPropertyChanged();
}
}
private FlowDocument m_flowDocument;
public FlowDocument FlowDocument
{
get
{
return m_flowDocument;
}
set
{
if (m_flowDocument == value)
return;
m_flowDocument = value;
OnPropertyChanged();
}
}
But nothing is display.
First of all, you should not declare property for FlowDocumentScrollViever inside your ViewModel - it is already backed with a field inside compiler generated CodeBehind partial class (your xaml.cs sibling).
Second, instead of instantiating local variable in your RunSM() method, you could directly instantiate your FlowDocument property, like this:
private void RunSM()
{
Paragraph paragraph = new Paragraph(new Run("some new Text"));
paragraph.Background = Brushes.White;
paragraph.Foreground = Brushes.Black;
paragraph.FontSize = 14;
///note that im passing previously created paragraph, because the ONLY way to do it is through the constructor
FlowDocument = new FlowDocument(paragraph);
}
Now, assuming your INotifyPropertyChanged is implemented properly (RaisePropertyChanged() call in FlowDocument setter), it should automatically notify UI with the changes, because FlowDocument property is already bound with your FlowDocumentScrollViewer:
<FlowDocumentScrollViewer Document="{Binding FlowDocument}"/>
Third, you should never set a value to your property's backing field directly when outside of it's (property) setter!
So, instead of m_flowDocument = foo;, you should rather write FlowDocument = foo;.
P.S. Correct me if im wrong, but the prefixed syntax (e.g. m_, s_) is no longer up to current C# naming conventions, so it's recommended not to use this, except in case when project convention enforces it.
I have a collection of items with a string property. That string property contains text which includes 6 digit numbers in various places like so:
this string 123456 is an example of a set of links 884555 to the following numbers
401177
155879
998552
I want to turn those 6 digit numbers into hyperlinks that when clicked will run a command on the ViewModel passing themselves as parameters. For example if I click 401177 I want to run HyperlinkCommand on the VM with the string parameter "401177". I still want to keep the formatting of the original text.
I figured the best way to do it would be with a custom control based on TextBlock. Below is the rough structure of my view, the UserControl is bound to the ViewModel, I use a ContentControl to bind to a collection of items with the property "detail", and that is templated with the custom text block bound to the "detail" property of my items.
<UserControl.DataContext>
<VM:HdViewModel/>
</UserControl.DataContext>
<UserControl.Resources>
<DataTemplate x:Key="DetailTemplate">
<StackPanel Margin="30,15">
<helpers:CustomTextBlock FormattedText="{Binding detail}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentControl Content="{Binding ItemListing}" ContentTemplate="{StaticResource DetailTemplate}" />
</Grid>
I used the code from this question and edited it slightly to generate the following custom control:
public class CustomTextBlock : TextBlock
{
static Regex _regex = new Regex(#"[0-9]{6}", RegexOptions.Compiled);
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached("FormattedText", typeof(string), typeof(CustomTextBlock), new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.AffectsMeasure, FormattedTextPropertyChanged));
public static void SetFormattedText(DependencyObject textBlock, string value)
{
textBlock.SetValue(FormattedTextProperty, value);
}
public static string GetFormattedText(DependencyObject textBlock)
{ return (string)textBlock.GetValue(FormattedTextProperty); }
static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is TextBlock textBlock)) return;
var formattedText = (string)e.NewValue ?? string.Empty;
string fullText =
$"<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{formattedText}</Span>";
textBlock.Inlines.Clear();
using (var xmlReader1 = XmlReader.Create(new StringReader(fullText)))
{
try
{
var result = (Span)XamlReader.Load(xmlReader1);
RecognizeHyperlinks(result);
textBlock.Inlines.Add(result);
}
catch
{
formattedText = System.Security.SecurityElement.Escape(formattedText);
fullText =
$"<Span xml:space=\"preserve\" xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">{formattedText}</Span>";
using (var xmlReader2 = XmlReader.Create(new StringReader(fullText)))
{
try
{
dynamic result = (Span)XamlReader.Load(xmlReader2);
textBlock.Inlines.Add(result);
}
catch
{
//ignored
}
}
}
}
}
static void RecognizeHyperlinks(Inline originalInline)
{
if (!(originalInline is Span span)) return;
var replacements = new Dictionary<Inline, List<Inline>>();
var startInlines = new List<Inline>(span.Inlines);
foreach (Inline i in startInlines)
{
switch (i)
{
case Hyperlink _:
continue;
case Run run:
{
if (!_regex.IsMatch(run.Text)) continue;
var newLines = GetHyperlinks(run);
replacements.Add(run, newLines);
break;
}
default:
RecognizeHyperlinks(i);
break;
}
}
if (!replacements.Any()) return;
var currentInlines = new List<Inline>(span.Inlines);
span.Inlines.Clear();
foreach (Inline i in currentInlines)
{
if (replacements.ContainsKey(i)) span.Inlines.AddRange(replacements[i]);
else span.Inlines.Add(i);
}
}
static List<Inline> GetHyperlinks(Run run)
{
var result = new List<Inline>();
var currentText = run.Text;
do
{
if (!_regex.IsMatch(currentText))
{
if (!string.IsNullOrEmpty(currentText)) result.Add(new Run(currentText));
break;
}
var match = _regex.Match(currentText);
if (match.Index > 0)
{
result.Add(new Run(currentText.Substring(0, match.Index)));
}
var hyperLink = new Hyperlink();
hyperLink.Command = ;
hyperLink.CommandParameter = match.Value;
hyperLink.Inlines.Add(match.Value);
result.Add(hyperLink);
currentText = currentText.Substring(match.Index + match.Length);
} while (true);
return result;
}
}
This is showing the links properly, however I dont know how to bind to the command on my ViewModel. I tested the command and the parameter using a button previously, and the binding was
Command="{Binding DataContext.HyperlinkCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
So what I am hoping is that I can convert this XAML into C# and attach it to hyperLink.Command = in my custom control. I can't figure out how to access the DataContext of the UserControl that the CustomTextBlock will be placed in.
I am not under any illusion that what I am doing is the best or right way of doing things so I welcome any suggestions
This is an interesting challenge, which I have solved with new code - coming at the problem in a slightly different way:
The code can be found here:
https://github.com/deanchalk/InlineNumberLinkControl
I'm using MVVM pattern to bind text to TextBlock.
The text is defined in database with <Subscript> tag to define when the text is Subscript. "Some<Subscript>subscript</Subscript>text."
I tried using Unicode subscripts and superscripts, but the characters appears too small, and hard to read.
I couldn't find a direct way to do this. any suggestions?
When you know that there is a subscription Tag you may use several Runs within your TextBlock.
<TextBlock>
<Run />
<Run />
</TextBlock>
I think you do not know the exact position of the subscripted text, right? So, why not just analyze your input and creating a new Run programatically? The Runs with the normal text have another size than Runs with subscripted text.
If you need help with adding Runs programatically just have a look at this StackOverflow post:
How to assign a Run to a text property, programmatically?
I know this is not the best way in MVVM to define XAML controls in your ViewModel, but thats the fastest way to reach better legibility.
Using Attached properties fixed my problem in the most MVVM friendly way.
You get the text and add to the TextBlock Inlines as you want.
Attached property c#:
public static class TextBlockAp {
public static readonly DependencyProperty SubscriptTextProperty = DependencyProperty.RegisterAttached(
"SubscriptText", typeof(string), typeof(TextboxAttachedProperty), new PropertyMetadata(OnSubscriptTextPropertyChanged));
public static string GetSubscriptText(DependencyObject obj) {
return (string)obj.GetValue(SubscriptTextProperty);
}
public static void SetSubscriptText(DependencyObject obj, string value) {
obj.SetValue(SubscriptTextProperty, value);
}
private static void OnSubscriptTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
try {
var value = e.NewValue as string;
if (String.IsNullOrEmpty(value)) return;
var textBlock = (TextBlock)d;
var startTag = "<Subscript>";
var endTag = "</Subscript>";
var subscript = String.Empty;
if (value.Contains(startTag) && value.Contains(endTag)) {
int index = value.IndexOf(startTag) + startTag.Length;
subscript = value.Substring(index, value.IndexOf(endTag) - index);
}
var text = value.Split(new[] { startTag }, StringSplitOptions.None);
textBlock.Inlines.Add(text[0]);
Run run = new Run($" {subscript}") { BaselineAlignment = BaselineAlignment.Subscript, FontSize = 9 };
textBlock.Inlines.Add(run);
} catch (Exception ex) {
if (ExceptionUtilities.UiPolicyException(ex)) throw;
}
}
}
xaml
<TextBlock ap:TextBlockAp.SubscriptText="{Binding MyProperty}" />
It needs more refactoring to work correctly, but it's a start.
I would like to format specific words in TextBlock dynamically, because it is binded to my object. I was thinking about using Converter but using following solution add only tags directly to the text (instead of showing it formatted).
public class TextBlockFormatter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
string regexp = #"\p{L}+#\d{4}";
if (value != null) {
return Regex.Replace(value as string, regexp, m => string.Format("<Bold>{0}</Bold>", m.Value));
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return null;
}
}
This is not an attempt to answer this question... it is a demonstration in response to a question in the question comments.
Yes #Blam, you can format individual words, or even characters in a TextBlock... you need to use the Run (or you could replace Run with TextBlock) class. Either way, you can also data bind to the Text property on either of them:
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
<Run Text="This" FontWeight="Bold" Foreground="Red" />
<Run Text="text" FontSize="18" />
<Run Text="is" FontStyle="Italic" />
<Run Text="in" FontWeight="SemiBold" Background="LightGreen" />
<Run Text="one" FontFamily="Candara" FontSize="20" />
<Run Text="TextBlock" FontWeight="Bold" Foreground="Blue" />
</TextBlock>
UPDATE >>>
Regarding this question now, I suppose that it would be possible to use these Run elements in a DataTemplate and have them generated from the data... the data in this case would have to be classes with (obviously) a Text property, but also formatting properties that you could use to data bind to the Run style properties.
It would be awkward though because the TextBlock has no ItemsSource property that you could bind your collection of word classes to... maybe you could use a Converter for that part... just thinking aloud here... I'm going to stop now.
UPDATE >>>
#KrzysztofBzowski, unfortunately the TextBlock.Inlines property is not a DependencyProperty, so you won't be able to data bind to it. However, it got me thinking and I did another search and found the Binding text containing tags to TextBlock inlines using attached property in Silverlight for Windows Phone 7 article on Jevgeni Tsaikin's .NET laboratory.
It would involve you declaring an Attached Property and a Converter, but it looks promising... give it a go. And don't worry that it's for Silverlight... if it works in Silverlight, then it'll work in WPF.
I recently had to solve this problem which I was able to do by writing a Blend behaviour for TextBlocks.
It can be declared in XAML with a list of Highlight elements where you specify the text to highlight, the colour you want that text to be and it's font weight (can easily add more formatting properties as required).
It works by looping though the desired highlights, scanning the TextBlock for each phrase starting at the TextBlock.ContentStart TextPointer. Once the phrase is found it can build a TextRange which can have the formatting options applied to it.
It should work if the TextBlock Text property is data bound too because I attach to the bindings Target updated event.
See below for the behaviour code and an example in XAML
public class TextBlockHighlightBehaviour : Behavior<TextBlock>
{
private EventHandler<DataTransferEventArgs> targetUpdatedHandler;
public List<Highlight> Highlights { get; set; }
public TextBlockHighlightBehaviour()
{
this.Highlights = new List<Highlight>();
}
#region Behaviour Overrides
protected override void OnAttached()
{
base.OnAttached();
targetUpdatedHandler = new EventHandler<DataTransferEventArgs>(TextBlockBindingUpdated);
Binding.AddTargetUpdatedHandler(this.AssociatedObject, targetUpdatedHandler);
// Run the initial behaviour logic
HighlightTextBlock(this.AssociatedObject);
}
protected override void OnDetaching()
{
base.OnDetaching();
Binding.RemoveTargetUpdatedHandler(this.AssociatedObject, targetUpdatedHandler);
}
#endregion
#region Private Methods
private void TextBlockBindingUpdated(object sender, DataTransferEventArgs e)
{
var textBlock = e.TargetObject as TextBlock;
if (textBlock == null)
return;
if(e.Property.Name == "Text")
HighlightTextBlock(textBlock);
}
private void HighlightTextBlock(TextBlock textBlock)
{
foreach (var highlight in this.Highlights)
{
foreach (var range in FindAllPhrases(textBlock, highlight.Text))
{
if (highlight.Foreground != null)
range.ApplyPropertyValue(TextElement.ForegroundProperty, highlight.Foreground);
if(highlight.FontWeight != null)
range.ApplyPropertyValue(TextElement.FontWeightProperty, highlight.FontWeight);
}
}
}
private List<TextRange> FindAllPhrases(TextBlock textBlock, string phrase)
{
var result = new List<TextRange>();
var position = textBlock.ContentStart;
while (position != null)
{
var range = FindPhrase(position, phrase);
if (range != null)
{
result.Add(range);
position = range.End;
}
else
position = null;
}
return result;
}
// This method will search for a specified phrase (string) starting at a specified position.
private TextRange FindPhrase(TextPointer position, string phrase)
{
while (position != null)
{
if (position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
string textRun = position.GetTextInRun(LogicalDirection.Forward);
// Find the starting index of any substring that matches "phrase".
int indexInRun = textRun.IndexOf(phrase);
if (indexInRun >= 0)
{
TextPointer start = position.GetPositionAtOffset(indexInRun);
TextPointer end = start.GetPositionAtOffset(phrase.Length);
return new TextRange(start, end);
}
}
position = position.GetNextContextPosition(LogicalDirection.Forward);
}
// position will be null if "phrase" is not found.
return null;
}
#endregion
}
public class Highlight
{
public string Text { get; set; }
public Brush Foreground { get; set; }
public FontWeight FontWeight { get; set; }
}
Example usage in XAML:
<TextBlock Text="Here is some text">
<i:Interaction.Behaviors>
<behaviours:TextBlockHighlightBehaviour>
<behaviours:TextBlockHighlightBehaviour.Highlights>
<behaviours:Highlight Text="some" Foreground="{StaticResource GreenBrush}" FontWeight="Bold" />
</behaviours:TextBlockHighlightBehaviour.Highlights>
</behaviours:TextBlockHighlightBehaviour>
</i:Interaction.Behaviors>
</TextBlock>
You'll need to import the Blend interactivity namespace and your behaviour's namespace:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:behaviours="clr-namespace:YourProject.Behviours"
I had a similar use case where I needed to build a text editor with RichTextBox. However, all text formatted changes: Font, Color, Italic, Bold must reflect on the TextBlock dynamically. I found few articles pointing me to Textblock.inlines.Add() which seemed helpful but only allow one change at a time or appending to the existing text.
However, Textblock.inlines.ElementAt(index of the existing text to format) can be utilized to apply the desired text format to the text located at that index. Below is my pseudo approach to resolving this issue. I hope this helps:
For RichTextBox:
selectedText.ApplyPropertyValue(TextElement.FontFamilyProperty, cbFontFamily.SelectedValue.ToString());
However, for the Textblock formatting to work I had to use Run run = new Run() concept which allows and works with:
Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(SelectedColorText))
FontFamily = new FontFamily(cbFontFamily.SelectedValue.ToString())
FontStyle = FontStyles.Italic
TextDecorations = TextDecorations.Underline
TextDecorations = TextDecorations.Strikethrough
FontWeight = (FontWeight)new FontWeightConverter().ConvertFromString(cbFontWeightBox.SelectedItem.ToString())
Additionally, I created a class with various fields and constructors. I also created a custom based class dictionary to capture all the changes made in the RichTextbbox. Finally, I applied all the captured information in the dictionary via a forloop.
TextBlock.Inlines.ElementAt(mItemIndex).Foreground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(dictionaryItem.Value._applyColor.ToString()));
TextBlock.Inlines.ElementAt(mItemIndex).FontFamily = new FontFamily(ftItem.Value._applyFontFamily.ToString());
TextBlock.Inlines.ElementAt(mItemIndex).FontStyle = FontStyles.Italic;
TextBlock.Inlines.ElementAt(mItemIndex).TextDecorations = TextDecorations.Underline;
TextBlock.Inlines.ElementAt(mItemIndex).TextDecorations = TextDecorations.Strikethrough;
I am here with another problem.
I have setup my comboBox such that it accepts only those characters which matches with the name of any items in the comboBoxItems.
Now here I am stuck with a problem. Please have a look at my code then I will explain you the problem :
private void myComboBox_KeyUp(object sender, KeyEventArgs e)
{
// Get the textbox part of the combobox
TextBox textBox = cbEffectOn.Template.FindName("PART_EditableTextBox", cbEffectOn) as TextBox;
// holds the list of combobox items as strings
List<String> items = new List<String>();
// indicates whether the new character added should be removed
bool shouldRemoveLastChar = true;
for (int i = 0; i < cbEffectOn.Items.Count; i++)
{
items.Add(cbEffectOn.Items.GetItemAt(i).ToString());
}
for (int i = 0; i < items.Count; i++)
{
// legal character input
if (textBox.Text != "" && items.ElementAt(i).StartsWith(textBox.Text))
{
shouldRemoveLastChar = false;
break;
}
}
// illegal character input
if (textBox.Text != "" && shouldRemoveLastChar)
{
textBox.Text = textBox.Text.Remove(textBox.Text.Length - 1);
textBox.CaretIndex = textBox.Text.Length;
}
}
In the last if condition I am removing the last character from the combobox. But user can use arrow keys or mouse to change the position of the cursor and enter the text at the middle of the text.
So if by entering a character at the middle of the text if the text becomes invalid I mean if it does not match the Items in the ComboBox then I should remove the last entered character. Can anybody suggest me how to get the last inserted character and remove it?
Update :
string OldValue = "";
private void myComboBox_KeyDown(object sender, KeyEventArgs e)
{
TextBox textBox = cbEffectOn.Template.FindName("PART_EditableTextBox", cbEffectOn) as TextBox;
List<String> items = new List<String>();
for (int i = 0; i < cbEffectOn.Items.Count; i++)
{
items.Add(cbEffectOn.Items.GetItemAt(i).ToString());
}
OldValue = textBox.Text;
bool shouldReplaceWithOldValue = true;
string NewValue = textBox.Text.Insert(textBox.CaretIndex,e.Key.ToString()).Remove(textBox.CaretIndex + 1,textBox.Text.Length - textBox.CaretIndex);
for (int i = 0; i < items.Count; i++)
{
// legal character input
if (NewValue != "" && items.ElementAt(i).StartsWith(NewValue, StringComparison.InvariantCultureIgnoreCase))
{
shouldReplaceWithOldValue = false;
break;
}
}
//// illegal character input
if (NewValue != "" && shouldReplaceWithOldValue)
{
e.Handled = true;
}
}
Here I have tried to move all the code in KeyDown event to solve the above problem. This code works just fine but have 1 problem.
If I have any item named Birds & Animals then After typing Birds and a space I cannot type &.
I know what is the problem but don't know the solution.
The Problem is : To type & I have to press shift key and then press the 7 key. But both are sent as different keys.
Solutions that I think about :
1) I should move my code to KeyUp event. But here the problem of long press and fast typing will arise.
2) I think I should replace e.Key with something. But don't know what.
I'm not sure if this is what you're trying to do but I feel like you're trying to do what we typically see in visual studio Intellisense fitlering out results as we type.
Instead of removing the keystrokes, you should be using the validation mechanisms that WPF provides. Here's a sample of how this could work.
Scenarios covered:
Input matches a combox item completely: TypedInput & SelectedItem both show full match.
Input matches some element partially: TypedInput shortlists the popup list. The binding shows the matching text while SelectedItem remains null.
Input doesn't match any item in list either from start or at some
random point: user is visually given feedback (with possibility to
add additional feedback information) with typical red outline. The
TypedInput remains at last valid entry, SelectedItem may or may
not be null depending on if the last TypedInput matched any item or not.
Full Code:
MainWindow.xaml
<Window x:Class="Sample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:Sample"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding Source={x:Static l:MainWindowViewModel.CurrentInstance}}">
<StackPanel>
<TextBlock>
<Run Text="Typed valid text" />
<Run Text="{Binding TypedText}"/>
</TextBlock>
<TextBlock>
<Run Text="Valid SelectedItem" />
<Run Text="{Binding SelectedItem}"/>
</TextBlock>
<ComboBox ItemsSource="{Binding FilteredItems}" IsEditable="True" IsTextSearchEnabled="False" SelectedItem="{Binding SelectedItem}">
<ComboBox.Text>
<Binding Path="TypedText" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<l:ContainsValidationRule />
</Binding.ValidationRules>
</Binding>
</ComboBox.Text>
</ComboBox>
</StackPanel>
</Window>
MainWindow.xaml.cs
namespace Sample
{
public partial class MainWindow { public MainWindow() { InitializeComponent(); } }
}
ContainsValidationRule.cs -- Meat of the solution
namespace Sample
{
using System.Globalization;
using System.Linq;
using System.Windows.Controls;
public class ContainsValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
var result = MainWindowViewModel.CurrentInstance.Items.Any(x => x.ToLower(cultureInfo).Contains((value as string).ToLower(cultureInfo)));
return new ValidationResult(result, "No Reason");
}
}
}
MainWindowViewModel - Supporting ViewModel Singleton
namespace Sample
{
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
public sealed class MainWindowViewModel : INotifyPropertyChanged
{
private string _typedText;
private string _selectedItem;
private static readonly MainWindowViewModel Instance = new MainWindowViewModel();
private MainWindowViewModel()
{
Items = new[] { "Apples", "Apples Green", "Bananas", "Bananas & Oranges", "Oranges", "Grapes" };
}
public static MainWindowViewModel CurrentInstance { get { return Instance; } }
public string SelectedItem
{
get { return _selectedItem; }
set
{
if (value == _selectedItem) return;
_selectedItem = value;
OnPropertyChanged();
}
}
public string TypedText
{
get { return _typedText; }
set
{
if (value == _typedText) return;
_typedText = value;
OnPropertyChanged();
OnPropertyChanged("FilteredItems");
}
}
public IEnumerable<string> Items { get; private set; }
public IEnumerable<string> FilteredItems
{
get
{
return Items == null || TypedText == null ? Items : Items.Where(x => x.ToLowerInvariant().Contains(TypedText.ToLowerInvariant()));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Instead of KeyUp event, subscribe to TextChanged event on your ComboBox Textbox. In event handler you can get the offset where the change has occured. You can use your validation logic inside the hanlder and delete the character at the offset if it makes Text invalid.
private void Window_Loaded(object sender, RoutedEventArgs e)
{
TextBox textBox = cbEffectOn.Template.FindName("PART_EditableTextBox", cbEffectOn) as TextBox;
textBox.TextChanged += new TextChangedEventHandler(textBox_TextChanged);
}
void textBox_TextChanged(object sender, TextChangedEventArgs e)
{
int index = e.Changes.First().Offset;
}
Have you considered using a string variable to hold the last legal text value in the text box portion of the combo box?
Initially, this string would be empty, as the user has not typed anything yet, then as each KeyUp event is handled, if an invalid character is input, then the previous string value is used to replace the text of the text box; otherwise the previous string value is now updated with the new complete string; awaiting anymore input by the user.