Home > Mobile >  How to add Paragraph in the middle of existing Paraghaph programmaticaly in FlowDocument WPF?
How to add Paragraph in the middle of existing Paraghaph programmaticaly in FlowDocument WPF?

Time:04-05

I have a test app where i'm trying to insert an editable paragraph so user can write info there (maybe it can be realized just with Run, i took paragraph just for example, if you know how to add Run into Run, that will be great). I DON'T WANT to use richtextbox for it for two main reasons:

  1. User can't edit any other parts of document

  2. Flowdocument has pagination For what i've done now, i have this: textbox and flowdocument with one paragraph (aaaaa bbb cccc) created by xaml and one created by code

    My editable paragraph going to the end of document. What i want is to put it instead of "bbb" for examle. So it must somehow find "bbb" from all document, replace it, and put in that place my paragraph

I've tried to:

  • Run through all blocks, find text that i need and remove it from paragraph, but no use because i can't replace string with paragraph or with run
  • Find index of text i want but i still can't do nothing with it because i need a TextPointer
  • Convert int to TextPointer but documentation said i'm going to a wrong and unsave direction
  • Find cursor controller for FlowDocument and set it to index i need but it still needs a TextPointer So i really need help because i can't see no other options

Here is my xaml

<Grid x:Name="grid">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <FlowDocumentReader Grid.Row="1">
        <FlowDocument x:Name="DocumentReader"/>
    </FlowDocumentReader>

</Grid>

And here is my xaml.cs without any bad code with my attempts to set paragrahp inside a paragraph - just textbox and editable paragraph

Dictionary<string, Paragraph> paragraphs = new Dictionary<string, Paragraph>();
        private string text = $"{{\\rtf1\\ansi\\ansicpg1252\\uc1\\htmautsp\\deff2{{\\fonttbl{{\\f0\\fcharset0 Times New Roman;}}{{\\f2\\fcharset0 Palatino Linotype;}}}}{{\\colortbl\\red0\\green0\\blue0;\\red255\\green255\\blue255;}}\\loch\\hich\\dbch\\pard\\plain\\ltrpar\\itap0{{\\lang1033\\fs21\\f2\\cf0 \\cf0\\ql{{\\f2 {{\\ltrch aaaaa bbb ccc}}\\li0\\ri0\\sa0\\sb0\\fi0\\ql\\par}}\r\n}}\r\n}}";
        public MainWindow()
        {
            InitializeComponent();
            
            //this is how data loads in flowdocument in my actual programm
            TextRange textRange = new TextRange(DocumentReader.ContentStart, DocumentReader.ContentEnd);
            using (MemoryStream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(text)))
            {
                textRange.Load(stream, DataFormats.Rtf);
            }

            //this is what i was testing
            var parag = new Paragraph { Name = "paragName" };
            parag.Inlines.Add(new Run("as"));
            paragraphs.Add("paragName", parag);
            DocumentReader.Blocks.Add(parag);

            var txt = new TextBox{Tag = "paragName" };
            txt.TextChanged  = (sender, args) =>
            {
                paragraphs.First(x => (string)x.Key == txt.Tag).Value.Inlines.Clear();
                paragraphs.First(x => (string)x.Key == txt.Tag).Value.Inlines.Add(new Run((sender as TextBox).Text));
            };
            grid.Children.Add(txt);
        }

It is super raw, i was just testing it, but i can't resolve how to do it, please help

CodePudding user response:

A quite simple solution would be to use a TextBlock and then inline a TextBox at the position you like to edit.

The following example lets EditableTextBlock extend TextBlock to extend the TextBlock behavior.
Setting a EditableTextBlock.EditableTextRange property defines the position and range in the text which should be made editable. The complete displayed text can be obtained by accessing the inherited EditableTextBlock.Text property as usual.
Setting the EditableTextBlock.EditableTextRange property will trigger a TextBox to appear at the specified position. The edited value is then committed by pressing the Enter key or by clicking the Button next to the TextBox.
The TextBox will then disappear and the edited text will become read-only again.
To simplify the content handling, the EditableTextBlock maintains a single Run to display the content.
The implementation is very simple and should serve you as a useful starting point.

TextRange.cs

public readonly struct TextRange : IEquatable<TextRange>
{
  public TextRange(int index, int length)
  {
    // TODO::Throw ArgumentException if values are out of range (e.g. < 0)

    this.Index = index;
    this.Length = length;
  }

  public bool Equals(TextRange other) => this.Index.Equals(other.Index) && this.Length.Equals(other.Length);
  public int Index { get; }
  public int Length { get; }
}

EditableTextBlock.cs

public class EditableTextBlock : TextBlock
{
  public TextRange EditableTextRange
  {
    get => (TextRange)GetValue(EditableTextRangeProperty);
    set => SetValue(EditableTextRangeProperty, value);
  }

  public static readonly DependencyProperty EditableTextRangeProperty = DependencyProperty.Register(
    "EditableTextRange", 
    typeof(TextRange), 
    typeof(EditableTextBlock), 
    new PropertyMetadata(default(TextRange), OnTextRangeChanged));

  public static void SetText(UIElement attachedElement, string value)
    => attachedElement.SetValue(TextProperty, value);

  public static string GetText(UIElement attachedElement)
    => attachedElement.GetValue(TextProperty) as string;

  public static readonly DependencyProperty TextProperty = DependencyProperty.RegisterAttached(
    "Text",
    typeof(string),
    typeof(EditableTextBlock),
    new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
  public static RoutedUICommand CommitChangesCommand { get; }

  static EditableTextBlock()
  {
    DefaultStyleKeyProperty.OverrideMetadata(typeof(EditableTextBlock), new FrameworkPropertyMetadata(typeof(EditableTextBlock)));
    CommitChangesCommand = new RoutedUICommand(
      "Commit edit changes",
      nameof(CommitChangesCommand),
      typeof(EditableTextBlock),
      new InputGestureCollection() 
        { 
          new KeyGesture(Key.Enter) 
        });
  }

  public EditableTextBlock()
  {
    var editableElementContentTemplate = Application.Current.Resources["EditableElementTemplate"] as DataTemplate;
    if (editableElementContentTemplate == null)
    {
      throw new InvalidOperationException("Define a DataTemplate named "EditableElementTemplate" in App.xaml");
    }

    var editableContent = new ContentPresenter() { ContentTemplate = editableElementContentTemplate };
    this.EditableElement = new InlineUIContainer(editableContent);

    this.CommandBindings.Add(new CommandBinding(CommitChangesCommand, ExecuteCommitChangesCommand));
  }

  private static void OnTextRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    => (d as EditableTextBlock).OnTextRangeChanged((TextRange)e.OldValue, (TextRange)e.NewValue);

  private void ExecuteCommitChangesCommand(object sender, ExecutedRoutedEventArgs e)
  {
    var documentTextBuilder = new StringBuilder();
    foreach (Inline documentElement in this.Inlines)
    {
      documentTextBuilder.Append(documentElement is Run run ? run.Text : GetText((documentElement as InlineUIContainer).Child));
    }
    var readOnlyDocument = new Run(documentTextBuilder.ToString());
    this.Inlines.Clear();
    this.Inlines.Add(readOnlyDocument);
  }

  protected virtual void OnTextRangeChanged(TextRange oldTextRange, TextRange newTextRange)
  {
    Inline documentContent = this.Inlines.FirstInline;
    if (documentContent is Run run && newTextRange.Index < run.Text.Length)
    {
      string newPreceedingReadOnlyRangeText = run.Text.Substring(0, newTextRange.Index);
      var newReadOnlyElement = new Run(newPreceedingReadOnlyRangeText);
      this.Inlines.InsertBefore(documentContent, newReadOnlyElement);

      string newEditableRangeText = run.Text.Substring(newTextRange.Index, newTextRange.Length);
      SetText(this.EditableElement.Child, newEditableRangeText);
      this.Inlines.InsertAfter(documentContent, this.EditableElement);
      this.Inlines.Remove(documentContent);

      string remainingReadOnlyRangeText = run.Text.Substring(newTextRange.Index   newTextRange.Length);
      var remainingReadOnlyElement = new Run(remainingReadOnlyRangeText);
      this.Inlines.InsertAfter(this.EditableElement, remainingReadOnlyElement);
    }
    else // Append
    {
      string newEditableRangeText = String.Empty;
      SetText(this.EditableElement.Child, newEditableRangeText);
      this.Inlines.Add(this.EditableElement);
    }
  }

  private InlineUIContainer EditableElement { get; }
}

App.xaml

<Application xmlns:system="clr-namespace:System;assembly=netstandard">
  <Application.Resources>
    <DataTemplate x:Key="EditableElementTemplate"
                  DataType="{x:Type system:String}">
      <StackPanel Orientation="Horizontal">
        <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=ContentPresenter}, Path=(local:EditableTextBlock.Text), UpdateSourceTrigger=PropertyChanged}" />
        <Button Command="{x:Static local:EditableTextBlock.CommitChangesCommand}"
                Content="Ok" />
      </StackPanel>
    </DataTemplate>
  </Application.Resources>
</Application>

Usage example

MainWindow.xaml

<Window>
  <local:EditableTextBlock x:Name="Document" />
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();

    this.Loaded #= onl oaded;
  }

  private void onl oaded(object sender, EventArgs e)
  {
    var documentText = "This is some random text.";
    this.Document.Text = documentText;

    int editableTextIndex = this.Document.Text.IndexOf("random");
    int editableTextLength = "random".Length;
    this.Document.EditableTextRange = new TextRange(editableTextIndex, editableTextLength);
  }
}
  • Related