When you add and remove text from a FlowDocument, all text pointers adjust their position based on several heuristics designed to keep them as close to the same "place" as possible.
To delete it is simple: if TextPointer is in the deleted text, it ends between the characters that surrounded the deleted text. But for attachments, this is not so simple: when text or other elements are inserted into the FlowDocument exactly into the existing TextPointer, should the TextPointer be before or after the inserted text? TextPointer has a “LogicalDirection” property that controls this.
What happens in your case is that the “caretBefore” position that you grab is exactly TextPosition, where the typed character is entered, and in your test cases your logical direction is LogicalDirection.Forward. That way, when a character is inserted, your "caretBefore" ends after the inserted character, which matches the TextPosition, which gives you an empty TextRange.
How does TextPointer get its logical direction? If you click on a RichTextBox to set the caret position, the click is interpreted as between two characters. If the actual point you clicked was on the second character, LogicalDirection is Forward, but if the actual point you clicked was the first character, LogicalDirection is Backward.
Try this experiment:
- Set FontSize = "40" and pre-fill the RichTextBox with the text "ABCD" in the constructor
- Click on the right side of B and type “X” between B and C. LogicalDirection Backward, so your “beforeCaret” ends with “X” and your MessageBox shows “X”.
- Click on the left side of C and type "X" between B and C. LogicalDirection - Forward, so your "beforeCaret" ends after "X" and your MessageBox is empty.
This behavior is inconsistent: if you do not know that LogicalDirection exists, you might think that pressing the right side of B or the left side of C will give you exactly the same carriage position.
Note. An easy way to visualize what is happening is to issue the MessageBox.Show command and instead do caretBefore.InsertTextInRun("^");
How do you achieve the result you need? LogicalDirection is read-only. One way is to use TextRange to force the TextPointer construct to logically reverse:
caretBefore = new TextRange(caretBefore, caretBefore.DocumentEnd).Start;
Do it in PreviewKeyDown. If you wait until PreviewKeyUp is too late: caretBefore has moved. This works because, as far as I can tell, the start of a non-empty TextRange always has a logical backward direction.
Another option is to keep the character offset from the beginning of the document (note that this is not a character offset!). In this case, you can save the offset in PreviewKeyDown:
caretBeforeOffset = caretBefore.DocumentStart.OffsetToPosition(caretBefore);
and reset caretBefore to the same character offset in PreviewKeyUp:
caretBefore = caretBefore.DocumentStart.GetPositionAtOffset(caretBeforeOffset, LogicalDirection.Forward);
Although this works, it’s not as usual as making TextPointer have a logical reverse direction: any text that was changed earlier in the document between PreviewKeyDown and PreviewKeyUp will cause the character offset calculation to find the wrong location, which is what TextPointers were designed to fix in the first place.
I don’t know any good resources for learning TextPointers other than reading documentation and playing with them, which is exactly what you already did.