概要
WPFで単純な文字列ではなく、複数の書式(文字色, Font, etc.)が入り混じった文字表示をしたい場合はRichTextBoxを使用します。
しかし、このRichTextBoxの中身Document
プロパティは依存関係プロパティではないので、そのままではBindingできず、ViewModelから変更できません。
そこでこれを解決するためのいくつかの方法を紹介します。
解決方法
デモアプリ
解決方法の具体例として、以下のようなデモアプリを考えます。
1つのRichTextBox内に、文字色と内容が固定の文字列("FixText_")と、ViewModelから文字色と内容が変更される文字列があるとします。
このデモアプリでは、各行ごとに以降の方法を使って、ViewModelからRichTextBoxの中身を書き換えています。
方法1 Run内のプロパティ単位でBindingする
そもそもRichTextBoxの中身全体をBindingするのではなくて、その一部だけをBindingしてしまう、という方法です。
Runのプロパティは依存関係プロパティなので、問題なくBindingできます。
表題とはずれますが、ViewModelから変更したい内容が、一部分の文字列だけとかであれば、これで十分でしょう。
<RichTextBox>
<FlowDocument>
<Paragraph>
<Run Text="FixText_" />
<Run
FontStyle="Italic"
Foreground="{Binding TextColor}"
Text="{Binding NormalText}" />
</Paragraph>
</FlowDocument>
</RichTextBox>
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に所属するとエラーしてしまうので、必要であればコピーを作成して設定します。
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に対して使用します。
<RichTextBox local:RichTextBoxHelper.Document="{Binding Document}" />
ViewModelではコードでFlowDocumentを組み立てます。
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クラスを作成します。
ここでは色と文字列だけ変更するとします。
public class RichTextViewModel
{
public string Text { get; set; }
public Color Color { get; set; }
}
それをMainWindowViewModelではプロパティとして公開します。
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を作成します。
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します。
<RichTextBox local:RichTextBoxHelper.Document="{Binding RichVM, Converter={StaticResource RichTextVmToFlowDocumentConverter}}" />
デモアプリコード全体
デモアプリのコード全体はGithubにおいておきます。
注意点
Viewからの変更をViewModelで取得するのは難しいです。基本的にOneWay
のみ。
方法2で、内部でFlowDocument
がコピーされていなければViewModel側にも反映されていますが、Document
プロパティ自体は変更されていないので、変更通知などは発生しません。
参考
環境
VisualStudio2019
.NET Core 3.1
C#8