空談録

http://artfulplace.net/blogs/ からひっこしつつ

UWPとかでRichTextBlockにリッチなテキストをバインドしたい

Surface RTでも動かしたいからWindows 8.1に対応するんだ!!→いますぐ捨てよう??→ぐわああああっ

というわけで(??)RichTextBlockじゃなくても動くと思うんですけど(挙動に差があるけど)、TextBlockでURLを出したいっていうお話です
自分で解決できなかったのでその辺も含めて書いときます

@okazukiさんと@tmytさんにはこの件について本当にお世話になりました。ありがとうございますありがとうございます

せっせと話を進めていきましょう

RichTextBlockってプロパティ生えてないの?

ないの

RichTextBlock class - Windows app development を見ればわかりますが、RichTextBlockにはTextとかContentみたいなプロパティはいません
ちょっと見てればわかりますが、コンテンツを表示する場所はRichTextBlock.Blocksであることがわかります。しかし! このプロパティはReadOnly! バインドどころかBlockCollectionをコードから突っ込むなんてこともできないわけですね

ついでにBlockCollectionのコンストラクタもないのでどうにかRichTextBlock.Blocks.Addで追加する必要があります。でもバインドはしたい。ぐぬぬ
コードビハインドって言っても、DataTemplate内のRichTextBlockとかだと面倒ですし…

UserControlで対応しよう

というわけでUserControlを作りましょう。こんなやつ

<UserControl
    x:Class="HogeFuga.BindableRichTextBlockControl"
    ...
    >
    
    <Grid>
        <RichTextBlock Name="MainTextBlock" />
    </Grid>
</UserControl>

UserControlでRichTextBlockを囲ってみました!みたいなやつですね
作り方としては、UserControlを追加してRichTextBlockの行だけコピーして貼り付けると完成です。お手軽

で、問題はこの後ですね
今から失敗例を書いていくのでコピペしないでおきましょう

とりあえずBindingしたいしDependancyPropertyを生やすぞ!って言って生やした結果がこれ

public static readonly DependencyProperty ContentParagraphProperty
    = DependencyProperty.Register("ContentParagraph", typeof(Paragraph), typeof(BindableRichTextBlockControl), PropertyMetadata.Create(new Paragraph(), (d, e) => (d as BindableRichTextBlockControl).ContentParagraph = (Paragraph)e.NewValue));

public Paragraph ContentParagraph
{
    get { return (Paragraph)this.GetValue(ContentParagraphProperty); }
    set
    {
        this.SetValue(ContentParagraphProperty, value);
        MainTextBlock.Blocks.Clear();
        if (value != null)
        {
            try
            {
                MainTextBlock.Blocks.Add(value);
            }
            catch (Exception ex)
            {
                // Debug.WriteLine(ex.ToString());
            }
        }
    }
}

どう見てもクソコードです、本当にありがとうございました
そもそもtry~catchで対応してる時点で先行きが怪しすぎる

バインド周りは適当に書いて実行しましょう。たぶんおそらくページやUserControlに1つだけ設置したRichTextBlockだったら動くはずです(試してない)
ただしDataTemplateでListViewとかに使っていると2つの問題が出てきます
・値が全く違う要素の値になってる場合がある
・"element is already the child of another element" って例外が出てくる

どっちにしても問題がありすぎます。というわけで優秀な開発者が集まっていると噂の devcussion で相談してきました
devcussionについてはこちら→ devcussion | Doorkeeper

まず値が異なる場合について質問した結果プロパティのsetterでやるのをやめようといわれたのでこんなかんじに

public static readonly DependencyProperty ContentParagraphProperty
    = DependencyProperty.Register("ContentParagraph", typeof(Paragraph), typeof(BindableRichTextBlockControl), PropertyMetadata.Create(new Paragraph(), (d, e) => {
        (d as BindableRichTextBlockControl).MainTextBlock.Blocks.Clear();
        (d as BindableRichTextBlockControl).MainTextBlock.Blocks.Add((Paragraph)e.NewValue);
    }));

public Paragraph ContentParagraph
{
    get { return (Paragraph)this.GetValue(ContentParagraphProperty); }
    set
    {
        this.SetValue(ContentParagraphProperty, value);
    }
}

これで値が別のに入れ替わる問題が解決できました。ですがもう片方の例外が残ったのでそっちについて聞いていると「1つのParagraphが瞬間的に2か所に追加されるタイミングが存在している」ということを知ったのでここでParagraphをこねることにしました

ちなみにこんなことをして再生成を回避するのはできませんでした。GUNUNU

public static readonly DependencyProperty ContentParagraphProperty
    = DependencyProperty.Register("ContentParagraph", typeof(Span), typeof(BindableRichTextBlockControl), PropertyMetadata.Create(new Paragraph(), (d, e) => {
        var paragraph = new Paragraph();
        var span = e.NewValue as Span;
        if (span != null)
        {
            paragraph.Inlines.Add(span);
        }
        (d as BindableRichTextBlockControl).MainTextBlock.Blocks.Clear();
        (d as BindableRichTextBlockControl).MainTextBlock.Blocks.Add(paragraph);
    }));

public Span ContentParagraph
{
    get { return (Span)this.GetValue(ContentParagraphProperty); }
    set
    {
        this.SetValue(ContentParagraphProperty, value);
    }
}

Spanなら…Spanならうまいこと回避してくれ…なかったよ… そもそもParagraphで分けるとかそういう問題じゃない感じでDocuments領域のやつらを使いまわすなって感じですね…

最終的なコード

一応わけないとどれコピペしていいかわからない気がした

public string Text
{
    get { return (string)GetValue(TextProperty); }
    set { SetValue(TextProperty, value); }
}

public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register("Text", typeof(string), typeof(BindableRichTextBlockControl), new PropertyMetadata("", (s, e) => 
    {
        // TODO : Create Paragraph from string.
        // var paragraph = CreateParagraph(e.NewValue as string);
        var self = s as BindableRichTextBlockControl;
        self.MainTextBlock.Blocks.Clear();
        self.MainTextBlock.Blocks.Add(paragraph);
    }));

これで行けます。毎回パースするのは遅くねという気もしなくもないので何か手がないかなぁとは思ったり
(Paragraph生成時にstringを解析する処理を私の場合しているのでそこはどうにかしたいというだけですが)


失敗コードが多すぎてよくわからん記事になってしまった。まあPVなんてないし問題ない
そもそもどうしてこのコードがダメなのかみたいなの書いてないし…まあ優秀じゃない開発者だからその辺は優秀な人がきっとやってくれるはず…

なかなか体調がよくならない時期なので平日外に出たくない

追記:まったく関係ないのですが、Office Insiderだとバージョンの部分の文字がOffice Insiderになってるんですけど、人によっては"現在の分岐"になってるようです
Current Branchの意味だと思うんですけど何が違うんですかね。Office Insiderの配信されてるブランチは"First Release for Current Branch"だった気が…?

画像の青で囲んだ部分の話です

この辺で