WPFのRichTextBoxにBindingする

# 概要
WPFで単純な文字列ではなく、複数の書式（文字色, Font, etc.）が入り混じった文字表示をしたい場合はRichTextBoxを使用します。
しかし、このRichTextBoxの中身`Document`プロパティは依存関係プロパティではないので、そのままではBindingできず、ViewModelから変更できません。

そこでこれを解決するためのいくつかの方法を紹介します。

# 解決方法
## デモアプリ
解決方法の具体例として、以下のようなデモアプリを考えます。
1つのRichTextBox内に、文字色と内容が固定の文字列("FixText_")と、ViewModelから文字色と内容が変更される文字列があるとします。

![RichTextBoxBinding.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/120077/11e9859c-52f3-8fc8-1351-bd8faaa096b8.gif)

このデモアプリでは、各行ごとに以降の方法を使って、ViewModelからRichTextBoxの中身を書き換えています。

## 方法1 Run内のプロパティ単位でBindingする
そもそもRichTextBoxの中身全体をBindingするのではなくて、その一部だけをBindingしてしまう、という方法です。
Runのプロパティは依存関係プロパティなので、問題なくBindingできます。
表題とはずれますが、ViewModelから変更したい内容が、一部分の文字列だけとかであれば、これで十分でしょう。

```xml:MainWindow.xaml（部分）
<RichTextBox>
   <FlowDocument>
      <Paragraph>
         <Run  Text="FixText_" />
         <Run
            FontStyle="Italic"
            Foreground="{Binding TextColor}"
            Text="{Binding NormalText}" />
      </Paragraph>
   </FlowDocument>
</RichTextBox>
```

```csharp:MainWindowViewModel.cs（部分）
private string _NormalText = "NormalText in VM";
public string NormalText
{
    get => _NormalText;
    set
    {
        _NormalText = value;
        RaisePropertyChanged();
    }
}
```

## 方法2 添付プロパティでFlowDocumentまるごとBindingする
もっと柔軟に、RichTextBoxの中身の`FlowDocument`をまるごと変更したいということであれば、この方法を使用します。
ただしViewModelにゴリゴリのView情報（RichTextBox）が入っているのでMVVM的にはイマイチです。

Bindingできない`Document`プロパティにBindingするための添付プロパティを用意します。
この添付プロパティに`FlowDocument`がBindingで渡されると、添付対象のRichTextBoxの本来の`Document`プロパティに対して、その`FlowDocument`が設定されます。
その際に`FlowDocument`が複数のRichTextBoxに所属するとエラーしてしまうので、必要であればコピーを作成して設定します。

```csharp
public class RichTextBoxHelper : DependencyObject
{
    public static FlowDocument GetDocument(DependencyObject obj) => (FlowDocument)obj.GetValue(DocumentProperty);
    public static void SetDocument(DependencyObject obj, FlowDocument value) => obj.SetValue(DocumentProperty, value);

    public static readonly DependencyProperty DocumentProperty = DependencyProperty.RegisterAttached(
        "Document", typeof(FlowDocument), typeof(RichTextBoxHelper),
        new FrameworkPropertyMetadata(null, Document_Changed));

    private static void Document_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (!(d is RichTextBox richTextBox))
            return;

        var attachedDocument = GetDocument(richTextBox);

        //FlowDocumentは1つのRichTextBoxにしか設定できない。
        //すでに他のRichTextBoxに所属しているなら、コピーを作成・設定する
        richTextBox.Document = attachedDocument.Parent == null
            ? attachedDocument
            : CopyFlowDocument(attachedDocument);
    }

    private static FlowDocument CopyFlowDocument(FlowDocument sourceDoc)
    {
        //もとのFlowDocumentをMemoryStream上に一度Serializeする
        var sourceRange = new TextRange(sourceDoc.ContentStart, sourceDoc.ContentEnd);
        using var stream = new MemoryStream();
        XamlWriter.Save(sourceRange, stream);
        sourceRange.Save(stream, DataFormats.XamlPackage);

        //新しくFlowDocumentを作成
        var copyDoc = new FlowDocument();
        var copyRange = new TextRange(copyDoc.ContentStart, copyDoc.ContentEnd);
        //MemoryStreamからDesirializeして書き込む
        copyRange.Load(stream, DataFormats.XamlPackage);

        return copyDoc;
    }
}
```

この添付プロパティをRichTextBoxに対して使用します。

```xml:MainWindow.xaml（部分）
<RichTextBox local:RichTextBoxHelper.Document="{Binding Document}" />
```

ViewModelではコードでFlowDocumentを組み立てます。

```csharp:MainWindowViewModel.cs（部分）
private FlowDocument _Document = CreateFlowDoc("FlowDocument in VM");
public FlowDocument Document
{
    get => _Document;
    set
    {
        _Document = value;
        RaisePropertyChanged();
    }
}

private static FlowDocument CreateFlowDoc(string innerText)
{
    var paragraph = new Paragraph();
    paragraph.Inlines.Add(new Run("FixText_"));
    paragraph.Inlines.Add(new Run(innerText) { Foreground = new SolidColorBrush(Colors.BlueViolet) });
    return new FlowDocument(paragraph);
}
```

## 方法3 添付プロパティとConverterで柔軟にBindingする
柔軟に変更したいが、ViewModelにViewの情報を入れたくない・適度に抽象化したい、という場合はこの方法です。

まず、RichTextBoxの`Document`に対応するViewModelクラスを作成します。
ここでは色と文字列だけ変更するとします。

```csharp
public class RichTextViewModel
{
    public string Text { get; set; }
    public Color Color { get; set; }
}
```

それをMainWindowViewModelではプロパティとして公開します。

```csharp:MainWindowViewModel.cs（部分）
private RichTextViewModel _RichVM = new RichTextViewModel() { Text = "Original Text in VM", Color = Colors.Indigo };
public RichTextViewModel RichVM
{
    get => _RichVM;
    set
    {
        _RichVM = value;
        RaisePropertyChanged();
    }
}
```

このプロパティをそのままBindingすることはできませんので、ViewModel→FlowDocumentへの変換のためのConverterを作成します。

```csharp
public class RichTextVmToFlowDocumentConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (!(value is RichTextViewModel richVM))
            return Binding.DoNothing;

        var paragraph = new Paragraph();
        paragraph.Inlines.Add(new Run("FixText_"));
        paragraph.Inlines.Add(new Run()
        {
            Text = richVM.Text,
            Foreground = new SolidColorBrush(richVM.Color),
            FontStyle = FontStyles.Italic,
        });
        return new FlowDocument(paragraph);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
```

このConverterと方法2の添付プロパティを使用して、ViewModelのプロパティとBindingします。

```xml:MainWindow.xaml（部分）
<RichTextBox local:RichTextBoxHelper.Document="{Binding RichVM, Converter={StaticResource RichTextVmToFlowDocumentConverter}}" />
```

## デモアプリコード全体

デモアプリのコード全体はGithubにおいておきます。

https://github.com/soi013/RichTextBoxBinding

# 注意点
Viewからの変更をViewModelで取得するのは難しいです。基本的に`OneWay`のみ。
方法2で、内部で`FlowDocument`がコピーされていなければViewModel側にも反映されていますが、`Document`プロパティ自体は変更されていないので、変更通知などは発生しません。

# 参考
[RichTextBox とデータバインディングする](http://kisuke0303.sakura.ne.jp/blog/?p=165)

https://stackoverflow.com/questions/343468/richtextbox-wpf-binding

https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.forms.richtextbox?view=netframework-4.8

# 環境
VisualStudio2019
.NET Core 3.1
C#8