When you add and remove text from a FlowDocument, all the TextPointers adjust their position based on a number of heuristics designed to make them stay in as close to the same "place" as possible.
For deletions this is simple: If the TextPointer is in the deleted text, it ends up betweem the characters that had been surrounding the deleted text. But for insertions it is not so simple: When text or other elements are inserted into a FlowDocument exactly at an existing TextPointer, should the TextPointer end up before or after the inserted text? TextPointer has a property called "LogicalDirection" that controls this.
What is happening in your case is that the "caretBefore" position you are capturing is exactly the TextPosition where the typed character is inserted, and in your test cases your LogicalDirection is LogicalDirection.Forward. Thus when the character is inserted, your "caretBefore" is ending up after the inserted character, which coincides with TextPosition giving you an empty TextRange.
How does a TextPointer get a LogicalDirection assigned to it? If you click on a the RichTextBox to set the caret position, the click is interpreted as being between two characters. If the actual point you clicked was on the second character, LogicalDirection is set to Forward, but if the actual point you clicked on was the first character, LogicalDirection is set to Backward.
Try this experiment:
- Set your FontSize="40" and prepopulate the RichTextBox with the text "ABCD" in the constructor
- Click on the right side of the B and type an "X" between the B and the C. LogicalDirection is Backward, so your "beforeCaret" ends up before the "X" and your MessageBox shows the "X".
- Click on the left side of the C and type an "X" between the B and the C. LogicalDirection is Forward, so your "beforeCaret" ends up after the "X" and your MessageBox is empty.
This behaviour is counterintuitive: When you don't know that LogicalDirection exists, you would think that clicking on the right-hand side of the B or the left-hand side of the C would give you exactly the same caret position.
Note: An easy way to visualize what is going on is to command out your MessageBox.Show and instead do a caretBefore.InsertTextInRun("^");
How do you achieve the result you need? LogicalDirection is read-only. One way is to use TextRange to force the construction of a TextPointer with a LogicalDirection of Backward:
caretBefore = new TextRange(caretBefore, caretBefore.DocumentEnd).Start;
Do this in PreviewKeyDown. If you wait until PreviewKeyUp it is already too late: caretBefore has moved. This works because as far as I can tell, the Start of a non-empty TextRange always has a LogicalDirection of Backward.
Another option is to save the symbol offset from the beginning of the document (note that this is not a character offset!). In this case you could store the offset in PreviewKeyDown:
caretBeforeOffset = caretBefore.DocumentStart.OffsetToPosition(caretBefore);
and reset caretBefore to the same symbol offset in PreviewKeyUp:
caretBefore = caretBefore.DocumentStart.GetPositionAtOffset(caretBeforeOffset,
LogicalDirection.Forward);
Although this works it is not as general as forcing your TextPointer to have a LogicalDirection of Backward: Any text changes earlier in the document between the PreviewKeyDown and PreviewKeyUp will cause the symbol offset calculation to find the wrong location, which is what TextPointers were designed to fix in the first place.
I don't know of any good resources for learning about TextPointers except for reading the documentation and playing with them, which is exactly what you have already been doing.