WPF get RichTextBox document block on text changed - c#

I have a custom WPF control that inherits from RichTextBox. I'd like to be able to modify the FlowDocument of the richtextbox whenever the text is changed. For demonstration, let's say that we have this:
<MyCustomRichTextbox>
<FlowDocument>
<Paragraph>This is the first paragraph</Paragraph>
<Paragraph>And this is the second</Paragraph>
</FlowDocument>
</MyCustomRichTextbox>
and whenever the text is changed (e.g. someone types in the control), the entire containing paragraph is colored red.
It seems to me that two things need to happen in order to achieve this:
I need to somehow get the block that contains the changed text out of MyCustomRichTextbox.Document.Blocks
I need to modify that block
Unfortunately, the OnTextChanged method doesn't provide a way to get the Block that changed. I was able to use LINQ and the TextChangedEventArgs.Offset to get the block, but I'm concerned that this approach will yield unacceptable slowdowns with larger documents (since it must enumerate each block every time a character is typed). Is there a better way to get the containing paragraph?
I know I could cache a reference to the "Last modified block" and check if it's still the one being modified, but that wouldn't really help in a random access scenario.

If I understand your problem correctly, you want to highlight the containing Paragraph of the current selection (the current position of the caret). So it's obviously that you have to get the containing Paragraph each time the Selection changes. Then you can just change the Foreground to Brushes.Red. Fortunately that the containing Paragraph seems to be referenced by the TextPointer and the process of finding it is nearly immediate. (a TextPointer has a property called Paragraph). Here is the detailed code:
Paragraph p = null;
//Suppose rtb is the name of your RichtTextBox
private void UpdateContainingBlockState() {
if (p != rtb.Selection.Start.Paragraph){
if (p != null) p.Foreground = Brushes.Black;
p = rtb.Selection.Start.Paragraph;
if (p != null) p.Foreground = Brushes.Red;
}
}
//The SelectionChanged event handler for your RichTextBox
private void selectionChangedHandler(object sender, RoutedEventArgs e){
UpdateContainingBlockState();
}
The frequency of changing the Selection is fairly high (each time you press almost keys which can cause the selection changing). So if your document is large and you realize some poor performance while typing, it's time to switch to the next more complex code. You can also try using Threading approach (or using Task) to put the UpdateContainingBlockState() call in another thread but be careful about cross-thread access. Here I use a different approach, the idea is call the UpdateContainingBlockState() at the right time, that is when the actual selection change can jump between paragraphs. While typing normal printable characters, the selection will be always in the current paragraph (so we don't need to call UpdateContainingBlockState()) unless when you type the Enter key. Generally we will call the method when user typing a control key (arrow keys, home, end, Enter, ...). We should also call the method if the RichTextBox gets focused and if user clicks mouse on the RichTextBox. You can see that almost the typed characters won't trigger calling the method so it will improve the performance much more than the code above (of course it may be realizable only when the document is large). Here is the detailed code:
//should add this using at the beginning
using System.Runtime.InteropServices;
[DllImport("user32")]
private static extern int MapVirtualKey(int ucode, int mapType);
//The KeyUp event handler for your RichTextBox
private void keyUp_Handler(object sender, KeyEventArgs e){
if (char.IsControl((char) MapVirtualKey(KeyInterop.VirtualKeyFromKey(e.Key),0x2)))
UpdateContainingBlockState();
}
//The PreviewMouseUp event handler for your RichTextBox
private void previewMouseUp_Handler(object sender, MouseButtonEventArgs e){
//UpdateContainingBlockState();
//Calling UpdateContainingBlockState() directly will cause a small issue
//So we use this instead
Task.Run(() => Dispatcher.Invoke( () => UpdateContainingBlockState()));
}
//The GotKeyboardFocus event handler for your RichTextBox
private void gotKeyboardFocus_Handler(object sender,
KeyboardFocusChangedEventArgs e){
UpdateContainingBlockState();
}

Related

Setting description of an item in Visual Studio -and- changing what "this" refers to

I'm working in Visual Studio 2010 and I'm dealing with C#; I've made a statusStrip that I intend to use as my tool-tip viewer, its .text attribute changing depending on the control the mouse has entered. I've got two textBoxes and I'm trying to make it such that entering the control fires a function called tooltipEnter, and leaving it fires a function called tooltipLeave. Here's my code for those two functions:
private void tooltipEnter(object sender, EventArgs e)
{
toolStripStatusLabel1.Text = this.AccessibleDescription;
}
private void tooltipLeave(object sender, EventArgs e)
{
toolStripStatusLabel1.Text = "Look here for tool-tips regarding the form!";
}
The problem with this is that, first, I'm not sure AccessibleDescription is the right attribute to saddle the description to, and I'm not sure of the most elegant way to do the toolStripStatusLabel1.Text assignment in the first place. Second, this in the program's frame of reference refers to the form on which these controls lay, not the controls themselves; How do I refer uniformly to "the control that just got entered" in a way that allows me to have just the one function for all entries, without having to make different ones for each control?
The problem with this is that, first, I'm not sure
AccessibleDescription is the right attribute to saddle the description
to, and I'm not sure of the most elegant way to do the
toolStripStatusLabel1.Text assignment in the first place.
AccessibleDescription is just some string instance referenced in your form, from this code. The text assignment is done in the only way possible. I'm not sure what your question is regarding this.
Second, this in the program's frame of reference refers to the form on
which these controls lay, not the controls themselves; How do I refer
uniformly to "the control that just got entered" in a way that allows
me to have just the one function for all entries, without having to
make different ones for each control?
sender is always the object from which the event was raised in the EventHandler delegate: msdn.microsoft.com/en-us/library/system.eventhandler.aspx

Detect when a control on design surface is selected

I'm writing an Expression Blend 4 Extension and I want to detect (in my extension) when a Control or Element on the design surface is selected. Can someone tell me how I can detect it? Thanks, Tim
I've continued a bit on my tutorial on writing extensions. When you look at the sample code of this project the code below should be clear.
The first method below is called when the active document is changed. This method handles the ActiveDocumentChanged event of the IDocumentService. First it gets the content of TimelinePane from the palette registry. In this content lives the ActiveSceneViewModel. The ActiveSceneViewModel is the viewmodel that containse the active scene (= the current xaml file being edited). The ActiveSceneViewModel contains a set of the selected elements, the ElementSelectionSet. Which has an event(Changed) that is fired when it is changed. Handle this event.
In this eventhandler you'll have access to the selection set, directly after it is changed.
private void ActiveDocumentChanged(object sender, DocumentChangedEventArgs e)
{
var timelinePane =
(TimelinePane)WindowService.PaletteRegistry["Designer_TimelinePane"].Content;
_activeSceneViewModel = timelinePane.ActiveSceneViewModel;
_activeSceneViewModel.ElementSelectionSet.Changed +=
new System.EventHandler(ElementSelectionSet_Changed);
//some other goes here....
}
void ElementSelectionSet_Changed(object sender, System.EventArgs e)
{
SceneElementSelectionSet selectionSet
= sender as SceneElementSelectionSet;
// get the selected elements from the selection set
}

When Control.ParentChanged Event occurs?

I read about Control.ParentChanged Event on MSDN
http://msdn.microsoft.com/en-us/library/system.windows.forms.control.parentchanged(VS.71).aspx
But I don't understand the sample code: there's no ParentChanged at all appearing in source code ?
private void currencyTextBox_TextChanged(object sender, EventArgs e)
{
try
{
// Convert the text to a Double and determine if it is a negative number.
if(double.Parse(currencyTextBox.Text) < 0)
{
// If the number is negative, display it in Red.
currencyTextBox.ForeColor = Color.Red;
}
else
{
// If the number is not negative, display it in Black.
currencyTextBox.ForeColor = Color.Black;
}
}
catch
{
// If there is an error, display the text using the system colors.
currencyTextBox.ForeColor = SystemColors.ControlText;
}
}
So I don't understand what Control.ParentChanged Event is or does.
Hehe, they just couldn't come up with a good example. And punted by showing a generic FooChanged event handler instead. Yeah, useless.
It is quite unusual to implement a ParentChanged event handler yourself. It's a big deal in the Winforms internals, properties like BackColor, ForeColor, Font are 'ambient' properties. If they are not overridden from the default then they'll get the value of the Parent. Which of course means that it is really important to notice that the parent changed. The winforms code already takes care of it, you very rarely have to worry about it. Unless you create your own ambient property of course.
There would be another piece of code elsewhere that registers this as an event handler:
currencyTextBox.ParentChanged += new EventHandler(currencyTextBox_TextChanged);
However, I agree - the method name is misleading.
This event handler will fire when you change the parent control of this control to a different parent control.
You may want to read up on raising and consuming events.

Why is my ListBox throwing an exception?

Okay, I was able to create a simple Windows Forms project that reproduces some strange behavior I found. In the designer, make a form with a ListBox (named lbx) anchored Top, Left, Right, and Bottom, and a button (button1). Now, the Form's code is here:
using System;
using System.Windows.Forms;
namespace ListBoxKaboom
{
public partial class Form1 : Form
{
private bool _initFinished = false;
public Form1()
{
InitializeComponent();
this._initFinished = true;
this.Height += 100;
this.Height -= 50;
this.Height += 50;
}
private void lbx_SelectedIndexChanged(object sender, EventArgs e)
{
this.button1.Enabled = (this.lbx.SelectedItem != null);
}
protected override void OnLayout(LayoutEventArgs e)
{
if (_initFinished)
{
int lines = (this.lbx.Height - 4) / this.lbx.ItemHeight;
this.SuspendLayout();
while (lines < this.lbx.Items.Count)
{
this.lbx.Items.RemoveAt(this.lbx.Items.Count - 1);
}
while (lines > this.lbx.Items.Count)
{
this.lbx.Items.Add("Item " + (this.lbx.Items.Count + 1).ToString());
}
this.ResumeLayout();
}
base.OnLayout(e);
}
}
}
PLEASE NOTE THE FOLLOWING INSTRUCTIONS:
Run this, click any of the items in the list box, and use the arrow keys to move down far enough to cause the list box to scroll. Kaboom.
Exception (sometimes NullReferenceException and sometimes IndexOutOfBoundsException). Any ideas why? Also, I would think that the items would be in order, but they're not. Is this just a goofy corner case that didn't get handled properly by Windows Forms, or am I doing something wrong?
Stack trace:
at System.Windows.Forms.ListBox.NativeUpdateSelection()
at System.Windows.Forms.ListBox.SelectedObjectCollection.EnsureUpToDate()
at System.Windows.Forms.ListBox.SelectedObjectCollection.get_InnerArray()
at System.Windows.Forms.ListBox.SelectedObjectCollection.get_Item(Int32 index)
at System.Windows.Forms.ListBox.get_SelectedItem()
I Copy/Pasted it to an empty Form and get a StackOverflow exception. Looking at it, with manipulation of the Items inside a Layout event I would say you deserve little better.
I realize this may be a simplification of something else, but there simply are limits to what you can do in a EDS.
My best guess: The ResumeLayout triggers a recursive layout operation. You could try to stem it with a sibling to _initFinished but I would suggest rethinking tour design here.
I copy/pasted wrong, my bad (used layout event).
Second try:
based on the two while-loops I would expect the Item strings to be in order, and no vertical scrollbar. It is clear that the listbox is confused, showing vertical scroll range and with the items out-of-order. So some 'error' is already present in the internals of the Listbox, waiting for a Scroll. I can also reproduce it with the mouse.
A workaround: You should be able to get the desired effect using the Resize event.
Attempt to an explanation: The (unmanaged part of the) Listbox gets confused by (multiple) Add/RemoveAt operations with suspended Layout. The last items are drawn at the wrong place, and the Listbox can't compute pixel-to-item.
You should not manipulate any GUI elements in the
constructor, e.g. this.Height += 100; in your example.
Strange things can happen. I have been bitten by this
several times in legacy code.
Wait until form load time - handle the base.Load event and do the
height manipulation there.
From "When does Form.Load event get raised?":
Q: "... I need to find out basically
what the difference is between
putting code in Load event's handler,
versus putting code in the Form's
constructor after the
InitializeComponents() line ..."
A: "The Load event is fired once the
control/form has been fully
initialized and has a window handle
created. Therefore once this event
has fired it is a fully usable user
interface control. Remember that
inside the constructor the actual
window handle for the control/form has
not yet been created, you are only
creating C# objects here and inside
the InitializeComponent call."

What is the last event to fire when loading a new WPF/C# window?

I am trying to load a preferences window for my application and I would like the apply button to initially be disabled, then when a preference is updated, the apply button gets enabled again. I have some controls data bound to a preferences object and what happens is that after the window loads, the combobox events get triggered. Is there any event that is guaranteed to happen dead last after everything is stable?
Here is what my code looks like (the apply button is always enabled after the window loads):
private void Window_Loaded(object sender, RoutedEventArgs e)
{
_preferencesData = new PreferencesDataContext();
LayoutRoot.DataContext = _preferencesData;
ButtonApply.IsEnabled = false;
}
private void ComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
ButtonApply.IsEnabled = true;
}
Is it also interesting to note that this only happens with textboxes and comboboxes, not checkboxes or radiobuttons.
Best solution for simple need
Joseph's answer is the best solution by far for your simple need: Just use data binding and let the data model handle it.
Answer to question as posed
There are more complex scenarios when you really do need control after absolutely everything has finished loading and all events have fired. There is no single event that occurs "dead last", but it is easy to effectively roll your own using the Dispatcher queue.
This is how to do it:
Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(() =>
{
var x = ComputeSomething(1, 2, 3);
DoSomething(x, "Test");
}));
Everything inside the { } will be executed when WPF finishes everything at a higher priority than ContextIdle, which includes all event handlers, loaded events, input events, rendering, etc.
Sequence of events when a Window is created and shown
As requested, here is the sequence of major events in WPF when a window is created and shown:
Constructors and getters/setters are called as objects are created, including PropertyChangedCallback, ValidationCallback, etc on the objects being updated and any objects that inherit from them
As each element gets added to a visual or logical tree its Intialized event is fired, which causes Styles and Triggers to be found applied in addition to any element-specific initialization you may define [note: Initialized event not fired for leaves in a logical tree if there is no PresentationSource (eg Window) at its root]
The window and all non-collapsed Visuals on it are Measured, which causes an ApplyTemplate at each Control, which causes additional object tree construction including more constructors and getters/setters
The window and all non-collapsed Visuals on it are Arranged
The window and its descendants (both logical and visual) receive a Loaded event
Any data bindings that failed when they were first set are retried
The window and its descendants are given an opportunity to render their content visually
Steps 1-2 are done when the Window is created, whether or not it is shown. The other steps generally don't happen until a Window is shown, but they can happen earlier if triggered manually.
The Window.ContentRendered event fulfilled my requirements.
I just did kind of the same thing behaviorly in a systray WPF app.
However, I didn't do it using event handling. I simply bound the Enabled property of my button to a property in my ViewModel, and had the property updated whenever I needed the behavior.
You can use ManagedSpy to figure this out on your own.
http://msdn.microsoft.com/en-us/magazine/cc163617.aspx
Setting the DataContext will likely fire the SelectionChanged event, and you can't rely on when exactly it's fired. Some logic checking on what exactly is selected would be more reliable:
private void ComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
if (myComboBox.SelectedItem == null)
{
buttonApply.IsEnabled = false;
}
else
{
buttonApply.IsEnabled = true;
}
}
The reason it's happening afterwards with your code as-is is because the event gets queued on the thread for the UI, so it's up to Windows if it will execute the next line of code in Load, or to handle the other events on the queue.
Not to throw a whole lot of stuff at you that you may or may not be familiar with, but if this is a relatively new codebase, you may want to consider using the MVVM pattern and use Commands instead of the archaic (emphasis mine) eventing model.
Order of Events in Windows Forms
Control.HandleCreated
Control.BindingContextChanged
Form.Load
Control.VisibleChanged
Form.Activated
Form.Shown

Categories

Resources