概要
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><RunText="FixText_"/><RunFontStyle="Italic"Foreground="{Binding TextColor}"Text="{Binding NormalText}"/></Paragraph></FlowDocument></RichTextBox>privatestring_NormalText="NormalText in VM";publicstringNormalText{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に所属するとエラーしてしまうので、必要であればコピーを作成して設定します。
publicclassRichTextBoxHelper:DependencyObject{publicstaticFlowDocumentGetDocument(DependencyObjectobj)=>(FlowDocument)obj.GetValue(DocumentProperty);publicstaticvoidSetDocument(DependencyObjectobj,FlowDocumentvalue)=>obj.SetValue(DocumentProperty,value);publicstaticreadonlyDependencyPropertyDocumentProperty=DependencyProperty.RegisterAttached("Document",typeof(FlowDocument),typeof(RichTextBoxHelper),newFrameworkPropertyMetadata(null,Document_Changed));privatestaticvoidDocument_Changed(DependencyObjectd,DependencyPropertyChangedEventArgse){if(!(disRichTextBoxrichTextBox))return;varattachedDocument=GetDocument(richTextBox);//FlowDocumentは1つのRichTextBoxにしか設定できない。//すでに他のRichTextBoxに所属しているなら、コピーを作成・設定するrichTextBox.Document=attachedDocument.Parent==null?attachedDocument:CopyFlowDocument(attachedDocument);}privatestaticFlowDocumentCopyFlowDocument(FlowDocumentsourceDoc){//もとのFlowDocumentをMemoryStream上に一度SerializeするvarsourceRange=newTextRange(sourceDoc.ContentStart,sourceDoc.ContentEnd);usingvarstream=newMemoryStream();XamlWriter.Save(sourceRange,stream);sourceRange.Save(stream,DataFormats.XamlPackage);//新しくFlowDocumentを作成varcopyDoc=newFlowDocument();varcopyRange=newTextRange(copyDoc.ContentStart,copyDoc.ContentEnd);//MemoryStreamからDesirializeして書き込むcopyRange.Load(stream,DataFormats.XamlPackage);returncopyDoc;}}この添付プロパティをRichTextBoxに対して使用します。
<RichTextBoxlocal:RichTextBoxHelper.Document="{Binding Document}"/>ViewModelではコードでFlowDocumentを組み立てます。
privateFlowDocument_Document=CreateFlowDoc("FlowDocument in VM");publicFlowDocumentDocument{get=>_Document;set{_Document=value;RaisePropertyChanged();}}privatestaticFlowDocumentCreateFlowDoc(stringinnerText){varparagraph=newParagraph();paragraph.Inlines.Add(newRun("FixText_"));paragraph.Inlines.Add(newRun(innerText){Foreground=newSolidColorBrush(Colors.BlueViolet)});returnnewFlowDocument(paragraph);}方法3 添付プロパティとConverterで柔軟にBindingする
柔軟に変更したいが、ViewModelにViewの情報を入れたくない・適度に抽象化したい、という場合はこの方法です。
まず、RichTextBoxのDocumentに対応するViewModelクラスを作成します。
ここでは色と文字列だけ変更するとします。
publicclassRichTextViewModel{publicstringText{get;set;}publicColorColor{get;set;}}それをMainWindowViewModelではプロパティとして公開します。
privateRichTextViewModel_RichVM=newRichTextViewModel(){Text="Original Text in VM",Color=Colors.Indigo};publicRichTextViewModelRichVM{get=>_RichVM;set{_RichVM=value;RaisePropertyChanged();}}このプロパティをそのままBindingすることはできませんので、ViewModel→FlowDocumentへの変換のためのConverterを作成します。
publicclassRichTextVmToFlowDocumentConverter:IValueConverter{publicobjectConvert(objectvalue,TypetargetType,objectparameter,System.Globalization.CultureInfoculture){if(!(valueisRichTextViewModelrichVM))returnBinding.DoNothing;varparagraph=newParagraph();paragraph.Inlines.Add(newRun("FixText_"));paragraph.Inlines.Add(newRun(){Text=richVM.Text,Foreground=newSolidColorBrush(richVM.Color),FontStyle=FontStyles.Italic,});returnnewFlowDocument(paragraph);}publicobjectConvertBack(objectvalue,TypetargetType,objectparameter,System.Globalization.CultureInfoculture){thrownewNotImplementedException();}}このConverterと方法2の添付プロパティを使用して、ViewModelのプロパティとBindingします。
<RichTextBoxlocal:RichTextBoxHelper.Document="{Binding RichVM, Converter={StaticResource RichTextVmToFlowDocumentConverter}}"/>デモアプリコード全体
デモアプリのコード全体はGithubにおいておきます。
https://github.com/soi013/RichTextBoxBinding
注意点
Viewからの変更をViewModelで取得するのは難しいです。基本的にOneWayのみ。
方法2で、内部でFlowDocumentがコピーされていなければViewModel側にも反映されていますが、Documentプロパティ自体は変更されていないので、変更通知などは発生しません。
参考
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
