Update Text of FlowDocument in the CodeBehind - wpf

I need to alter the text of a FlowDocument without changing the existing formatting and am having trouble doing so.
My thought was to do a foreach of Blocks in the document. Then for any Paragraph do a foreach of the Inlines like this;
foreach (var x in par.Inlines)
{
if (x.GetType() == typeof(Run))
{
Run r = (Run)x;
r.Text = r.Text.Replace("#", "$");
}
}
Problem is that this returns the following error message;
System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'
What is the correct way of doing this?

The usual solution is to call ToList() on the collection and iterate through the new collection returned by ToList().
var runs =
flowdoc.Blocks.OfType<Paragraph>()
.SelectMany(par => par.Inlines).OfType<Run>()
.ToList();
foreach (var r in runs)
{
r.Text = r.Text.Replace("#", "$");
}

Your error comes from trying to use the foreach loop to enumerate through a collection while also modifying the collection. Use a for loop.
For changing text in a flow document, try a TextPointer + TextRange, here's an example (this one changes the text background but you can change text just as easily).
private void ClearTextHighlight(FlowDocument haystack)
{
TextPointer text = haystack.ContentStart;
TextPointer tpnext = text.GetNextContextPosition(LogicalDirection.Forward);
while (tpnext != null){
TextRange txt = new TextRange(text, tpnext);
//access text via txt.Text
//apply changes like:
var backgroundProp = txt.GetPropertyValue(TextElement.BackgroundProperty) as SolidColorBrush;
if(backgroundProp != null && backgroundProp.Equals(Setting_HighlightColor)){
//change is here
txt.ApplyPropertyValue(TextElement.BackgroundProperty, Setting_DefaultColor);
}
text = tpnext;
tpnext = text.GetNextContextPosition(LogicalDirection.Forward);
}
}

Related

Weird incident with DownloadStringAsync

I'm using the following code to download the source code of an HTML page
String search = $"<search url>";
String result = "";
using (WebClient wc = new WebClient())
{
var searchURL = new Uri(search);
wc.DownloadStringCompleted += (s, e) =>
{
result = e.Result;
};
wc.DownloadStringAsync(searchURL);
}
return result.Trim();
This exists in a static method
public static Scrape(String arg)
of a class Scraper.
However, when I do
txtResult.Text = Scraper.Scrape(arg);
in the code behing of a form, nothing appears. Using breakpoints, I see that e.Result indeed contains the expected data, but something happens and the data is "lost" in the way. Anyone has any idea what I'm missing;
Because the download is asynchronous it hasn't updated the value of result before you reach the line return "result.Trim()" as a result you are returning the empty string.
You need to wait for WC to complete, if you use the task method then accessing the result property will cause you to wait for the response.
String search = $"<search url>";
String result = "";
using (WebClient wc = new WebClient())
{
var searchURL = new Uri(search);
result = wc.DownloadStringTaskAsync(searchURL).Result;
}
return result.Trim();
However if you do this in the current method as it stands you will end up hanging your UI as the wait happens on the foreground thread.
You can move the update to the background by changing your call to use a task with a continuation.
Instead of
txtResult.Text = Scraper.Scrape(arg);
Add a using using System.Threading.Tasks;
then you can use the line
Task.Run(() => Scraper.Scrape(arg))
.ContinueWith(t => txtResult.Text = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
You may also want to precede this with a line txtResult.Text = "Please Wait Fetching....";

Copy list to Flowdocument messes up 1st listitem

I'm writing an extension that allows a user to merge multiple notes into a single note and provides some features like adding periods onto the end of the original notes. I'm writing the code that copies the parts of one flowdocument to another and inserts the periods as it goes.
I am having problems copying lists to the new document. For some reason the first listitem always ends up in the paragraph PREceeding the list instead of in the list.
My code:
foreach (Block b in tempDoc.Blocks)
{
thisBlock++;
if (b is List)
{
pkhCommon.WPF.Helpers.AddBlock(b, mergedDocument);
}
else
{
Paragraph p = b as Paragraph;
foreach (Inline inl in p.Inlines)
{
if (!(inl is LineBreak))
pkhCommon.WPF.Helpers.AddInline(inl, mergedDocument);
}
if (thisElement != lastElement || thisBlock != lastBlock)
if ((bool)cb_AddPeriods.IsChecked)
pkhCommon.WPF.Helpers.AddInline(new Run(". "), mergedDocument);
else
pkhCommon.WPF.Helpers.AddInline(new Run(" "), mergedDocument);
}
}
Below is the function to merge the blocks. The AddIline function works the same way.
public static void AddBlock(Block from, FlowDocument to)
{
if (from != null)
{
using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
{
TextRange range = new TextRange(from.ContentStart, from.ContentEnd);
System.Windows.Markup.XamlWriter.Save(range, stream);
range.Save(stream, DataFormats.XamlPackage);
TextRange textRange2 = new TextRange(to.ContentEnd, to.ContentEnd);
textRange2.Load(stream, DataFormats.XamlPackage);
}
}
}
I can't understand why the flowdocument is deciding to put the listitem into the PREceeding paragraph.
Adding a block to the FlowDocument's block collection should put it at the end.
This works for for me.
Document.Blocks.Add(blockToAdd);
You are doing the save/load to clone the block right? Can you just try adding it this way at the end instead of inserting in the text range?
var blockToAdd = XamlReader.Load(stream) as Block;
Document.Blocks.Add(blockToAdd);
Part of your issue is that after saving, the stream position is at the end of the stream, so there is nothing to Load. There is probably a better way to fix this but I am out of time to help. Setting position to 0 feels wrong. This has no parse exception.
var from = new System.Windows.Documents.List(new ListItem(new Paragraph(new Run("Blah"))));
using (var stream = new MemoryStream())
{
System.Windows.Markup.XamlWriter.Save(from, stream);
stream.Position = 0;
Block b = System.Windows.Markup.XamlReader.Load(stream) as Block;
}

how do i select the node's index?

I have a xml populated combobox. If builderemail (from parsing of text using streamreader) is equals to the any one value found in the xml file, the combobox will select the index. how do i go about selecting it?
if (line.StartsWith("Builder_Email:"))
{
bool IsNodeExists = false;
string[] fields = line.Split('\t');
string builderemail = fields[3];
XmlDocument emailparse = new XmlDocument();
emailparse.Load(#"C:\GUI\buildermanageremail.xml");
XmlNodeList emailnode = emailparse.GetElementsByTagName("value");
if (string.IsNullOrEmpty(builderemail))
comboBox1.SelectedIndex = -1;
else
foreach (XmlNode node in emailnode)
{
if (builderemail == node.InnerText)
{
// how do i get the combobox selection right?
// need some code here
IsNodeExists = true;
break;
}
}
if(!IsNodeExists)
{
//create main node
XmlNode abc = emailparse.CreateNode(XmlNodeType.Element, "builder", null);
//create the first child node
XmlNode value = emailparse.CreateElement("value");
//set the value
value.InnerText = builderemail;
// add childes to father
//node.AppendChild(id);
abc.AppendChild(value);
// find the node we want to add the new node to
XmlNodeList l = emailparse.GetElementsByTagName("builderemail");
// append the new node
l[0].AppendChild(abc);
// save the file
emailparse.Save(#"C:\GUI\buildermanageremail.xml");
//then we populate the new updated xml file into the drop down list:
PopulateDDLFromXMLFile();
int count = emailparse.SelectNodes("email/builderemail/builder").Count;
count--;
comboBox1.SelectedIndex = count;
}
}
the place to look at is here:
foreach (XmlNode node in emailnode)
{
if (builderemail == node.InnerText)
{
// how do i get the combobox selection right?
// need some code here
IsNodeExists = true;
break;
}
}
I believe this code does everything that you want your code to do. This code is certainly not perfect, and may not even work, but if you compare it to yours you should find that it employs probably half a dozen practices that you're not following in your code and probably should be. If you cut out all the assertions, you'll find that it's only 10 lines of code (not counting the refactored method).
if (line.StartsWith("Builder_email:"))
{
Debug.Assert(
line.Where(x => x == '\t').Count() > 2),
"Can't parse input line.");
string builderEmail = line.Split('\t')[3];
Debug.Assert(
builderEmail != null && builderEmail == builderEmail.Trim(),
"Input data is bad.");
string filename = #"C:\GUI\buildermanageremail.xml"
Debug.Assert(
File.Exists(filename),
"Expected XML file does not exist.");
XmlDocument emailXml = new XmlDocument();
emailXml.Load(filename);
// In your real application, you know the name of the document element, so you
// should replace * with it in the XPath below.
string xpath = string.Format(
"/*/builderemail/builder/value[.='{0}']",
builderEmail);
if (emailXml.SelectSingleNode(xpath) == null)
{
CreateBuilderEmailElement(emailXml, builderEmail);
emailXml.Save(filename);
// I've changed the name of this method, which is problematic for several
// reasons - not least of which is that in my world, at least, "DDL" means
// "Data Definition Language."
//
// This also assumes that you've created an overload of the method that
// takes an XmlDocument argument.
PopulateEmailComboBox(emailXml);
}
// I'm assuming that the builderEmail is the actual text value stored in the
// combo box items, in which case all you need to do is find the item with that
// value and set SelectedItem, which will automatically set SelectedIndex. Also,
// if the value isn't found, setting SelectedItem to null automatically sets
// SelectedIndex to -1.
builderEmailComboBox.SelectedItem = builderEmailComboBox.Items
.Where(x => x.ToString() == builderEmail)
.FirstOrNull();
}
And here's the method for creating the builderemail element - which, by the way, should be named builderEmail if you have any say over it:
// this is refactored out as its own function, and made internal so that you
// can unit test it.
internal void CreateBuilderEmailElement(XmlDocument emailXml, string builderEmail)
{
XmlElement builder = emailXml.CreateNode("builder");
XmlElement value = emailXml.CreateNode("value");
builder.AppendChild(valueElm);
value.InnerText = builderEmail;
// again, you know the name of the document element in your application,
// so replace the * below with it.
Debug.Assert(
emailXml.SelectSingleNode("/*/builderemail") != null,
"No builderemail element found under the document element.");
emailXml.SelectSingleNode("/*/builderemail").AppendChild(builder);
}
Also, is there a reason that your XML has a separate value element under builderEmail, instead of builderEmail just containing the value?

WPF RichTextbox remove Foreground information from TextRange

sorry for my bad english... The default for a RichTextBox content is to inherit the Foreground color from the RichTextBox itself. That's nice, but if I set a specific Foreground color to some part of my text, that part does not inherit the Foreground anymore, obviously. How can I make my "colored" text inherit the Foreground again? I'm trying to do something like the "Automatic" color from Office Word but after I have set a specific color to a TextRange, I do not know how to unset it :/
TextRange.ClearAllProperties() does what I need, but also erases other properties like FontSize and FontFamily...
TextRange.ApplyPropertyValue(ForegroundProperty, DependencyProperty.UnsetValue) also does not do the trick...
You can also unset it by setting the property to null (this worked for me clearing out the background, for example removing highlighting)
TextRange.ApplyPropertyValue(TextElement.BackgroundProperty, null);
This seemed almost impossible to achieve since there is no "RemovePropertyValue" method. I also tried with span and got the same exception as you did so I made a method that collects all the Paragraphs within the TextRange and made a span for each separetly.. less than ideal, I know.. Anyway, it works for a small example but might be pretty hard to work with for something more complex.
private List<Span> m_spanList = new List<Span>();
private void c_setForegroundButton_Click(object sender, RoutedEventArgs e)
{
TextPointer textPointerStart = c_richTextBox1.Selection.Start;
TextPointer textPointerEnd = c_richTextBox1.Selection.End;
TextRange textRange = new TextRange(textPointerStart, textPointerEnd);
SetForeground(textRange);
}
private void c_clearForegroundButton_Click(object sender, RoutedEventArgs e)
{
foreach (Span span in m_spanList)
{
span.ClearValue(Span.ForegroundProperty);
}
}
public void SetForeground(TextRange textRange)
{
List<Paragraph> spannedParagraphs = new List<Paragraph>();
if (textRange.Start.Paragraph != null)
{
TextRange curRange = null;
Block cur = textRange.Start.Paragraph;
do
{
spannedParagraphs.Add(cur as Paragraph);
// Get next range
curRange = new TextRange(cur.ContentStart, cur.ContentEnd);
} while ((textRange.End.Paragraph == null || !curRange.Contains(textRange.End.Paragraph.ContentEnd)) && (cur = cur.NextBlock) != null);
}
if (spannedParagraphs.Count == 1)
{
Span span = new Span(c_richTextBox1.Selection.Start, c_richTextBox1.Selection.End);
span.Foreground = Brushes.Red;
m_spanList.Add(span);
}
else
{
for (int i = 0; i < spannedParagraphs.Count; i++)
{
if (i == spannedParagraphs.Count - 1)
{
Paragraph paragraph = spannedParagraphs[i];
// For some reason I get an exception here when I try this..
//m_span = new Span(paragraph.ElementStart, c_richTextBox1.Selection.End);
c_richTextBox1.Selection.Select(paragraph.ElementStart, c_richTextBox1.Selection.End);
Span span = new Span(c_richTextBox1.Selection.Start, c_richTextBox1.Selection.End);
span.Foreground = Brushes.Red;
m_spanList.Add(span);
}
else if (i == 0)
{
Paragraph paragraph = spannedParagraphs[i];
Span span = new Span(c_richTextBox1.Selection.Start, paragraph.ElementEnd);
span.Foreground = Brushes.Red;
m_spanList.Add(span);
}
else
{
Paragraph paragraph = spannedParagraphs[i];
Span span = new Span(paragraph.ElementStart, paragraph.ElementEnd);
span.Foreground = Brushes.Red;
m_spanList.Add(span);
}
}
}
}
If you look at the code of method TextRange.ApplyPropertyValue in the .NET Reference Source, you'll see that in the end it calls DependencyObject.SetValue on a collection of Inlines and Blocks, and DependencyObject.SetValue treats the value DependencyProperty.UnsetValue specially by effectively clearing the local value for the property.
The problem is that they didn't think of that when implementing TextRange.ApplyPropertyValue: it checks the passed property value against the property type, and in case of a reference type, it makes sure the passed value is either null or inherits from the same class, thus preventing us from passing DependencyProperty.UnsetValue.
One solution I found to implement a way of clearing local values of a TextRange for dependency properties of a reference type is the following:
// We declare a marker brush to be detected later in the TextRange.
var markerBrush = new SolidColorBrush();
// First we ask the TextRange implementation to set our marker brush on its content.
// Using ApplyPropertyValue here takes care of splitting inlines when necessary to make
// sure that only the selected text gets affected.
range.ApplyPropertyValue(TextElement.ForegroundProperty, markerBrush);
// Now, we search the text range for every Inline that has our brush set as the foreground
// brush, and we clear the Foreground dependency property.
var position = range.Start;
while (position != null && range.Contains(position))
{
if (position.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart &&
position.Parent is Inline inline &&
inline.ReadLocalValue(TextElement.ForegroundProperty) == _foregroundClearBrush)
inline.ClearValue(TextElement.ForegroundProperty);
position = position.GetNextContextPosition(LogicalDirection.Forward);
}

Remove images from RichTextBox FlowDocument

I have a WPF application on which the user can paste some data from Word inside a RichTextBox... but if that word data has an image, I need to remove it, how can I accomplish that?
Since the FlowDocument is xml, maybe doing some linq magic could do it, but I don't know how.
There is a tool called WordtoXAML Converter (http://wordtoxaml.codeplex.com). You can use that to convert your Word document contents to XAML, use regular expression matching to identify the images and then strip them out.
The following code will do what you want. While it can be a bit wasteful (it looks through the entire document instead of just the bit that was just pasted), it is the only way to do it, as sometimes the RichTextBox is inaccurate when it indicates the recently painted range:
public class MyTextBox : RichTextBox
{
public MyTextBox()
{
CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, Paste));
}
protected virtual void Paste(object sender, ExecutedRoutedEventArgs e)
{
Paste();
foreach (var image in FindImages())
{
if (image.SiblingInlines != null)
{
image.SiblingInlines.Remove(image);
}
}
}
IEnumerable<InlineUIContainer> FindImages()
{
var result = new List<InlineUIContainer>();
var blocks = Document.Blocks;
for (TextPointer position = blocks.FirstBlock.ElementStart; position != null && position.CompareTo(blocks.LastBlock.ElementEnd) != 1; position = position.GetNextContextPosition(LogicalDirection.Forward))
{
InlineUIContainer element = position.Parent as InlineUIContainer;
if (element != null && element.Child is Image)
{
result.Add(element);
}
}
return result;
}
}

Resources