読者です 読者をやめる 読者になる 読者になる

空談録

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

Slack APIのスレッドの扱いについて

進捗…ガハッ…

Slackにスレッド機能が追加されたらしいですね。

slackhq.com

www.itmedia.co.jp

見てたらdevcussionのSlackには来てたのでAPIではどう扱うのかのさわり的なところを書いておきます。
Slack APIを触っている人間がどれだけいるのかは知りませんが…

スレッドのリプライメッセージについて

まず、Slackのスレッドは親となる通常メッセージにリプライメッセージを子としてつけていくことで実現しています。
この親メッセージとリプライメッセージが一つのスレッドとなります。

実際のデータを見ながら追っていきましょう。
(ただし内容については架空のものです)

まずチャンネルに流れるメッセージは通常次のような形となっています

{
    "type": "message",
    "user": "Uxxxxxxxx",
    "text": "ねむい",
    "ts": "99999999.000001"
}

このメッセージにスレッド機能をつかってリプライをつけてみます。
つけたリプライは次のようなデータとなります。

{
    "type": "message",
    "user": "Uyyyyyyyy",
    "text": "ビャーーーッ",
    "thread_ts": "99999999.000001",
    "parent_user_id": "Uxxxxxxxx",
    "ts": "99999999.000333"
}

このとき、スレッドとなった親メッセージは次のような値に変化しています。

{
    "type": "message",
    "user": "Uxxxxxxxx",
    "text": "ねむい",
    "thread_ts": "99999999.000001",
    "reply_count": 1,
    "replies": [
        {
            "user": "Uyyyyyyyy",
            "ts": "99999999.000333"
        }
    ],
    "subscribed": false,
    "ts": "99999999.000001"
}

このように基本的にはスレッドを構成するメッセージにはthread_tsフィールドが存在します。
同じthread_tsを持つメッセージをスレッドとして管理できればスレッド機能への対応は可能です。

…え? スレッドメッセージがリプライかどうか見分ける方法って? フィールドがあるかないかじゃないですかね…

スレッドメッセージの取得方法は?

これは別に対応する必要がなくて、普通にx.history系で取れます。x.repliesで取ってくる必要はないです。
またRealTime Messagingでも普通に流れてくるはずです。

さらに言えばスレッドメッセージはthread_tsで基本的には実現しているので、クライアントが対応していなければありがたいことに普通にチャンネルに流れてきます。
つまるところ、スレッドしてリプライメッセージをまとめるか、そのまま表示するか程度の差でしかないです。公式SlackはUIでスレッドとして別にメッセージがあるように見せていますが、実際は他のメッセージと同じ扱いです。

クライアントを作るのが大変そうって? わかる

スレッドにポストする方法は?

これは普通にスレッドの親メッセージのtsを"thread_ts"パラメータに入れるだけです。
リプライしたいメッセージにthread_tsがついててもついてなくても関係なく、chat.postMessageでthread_tsのパラメータを指定すれば勝手にスレッドのリプライとしてやってくれます。

ところで一つ注意点があります。(1/19時点) thread_tsに不正な値を入れてもchat.postMessageは通ります。tsフォーマットさえ守ればおそらく通るはずです。
これの問題点はリプライ扱いなので公式クライアントでは見れなくなります。というかなんだこれ。

ちなみにAPIレスポンスではちゃんと拾えています。
f:id:fantasticswallow:20170119203621p:plain

im.historyによるレスポンスと公式クライアントの見え方です。少なくともuuuuというメッセージは見えません。
そして世界のどこのSlackにもなさそうな"1484824875.990999"とかいうふざけたthread_tsですが通っています。
ウーーン

reply_broadcastとかいうやつ

殺意しか湧かないですね!

Slackのスレッド機能にはもう一つ機能があります。リプライメッセージをチャンネルにも通知するという機能です。
"Also send to {チャンネル名}"とかそういうチェックがリプライの所にあると思います。それですそれ。

さっきの画像の"Also send as direct message"もそうです。
これらはAPIではreply_broadcastとして扱われます。

この機能を使う、つまりチェックを入れて投稿するにはchat.postMessageの"reply_broadcast"パラメータをtrueにするだけです。
問題はやってくるレスポンスデータですよええ…

こんなんが来ます。

{
    "channel_id": "00000000",
    "channel_type": "D",
    "timestamp": "1484826106000008",
    "is_multiteam": false,
    "attachments": [
        {
            "from_url": "https:\/\/devcussion.slack.com\/archives",
            "fallback": "[January 19th, 2017 3:21 AM] fantasticswallow: \u3042\u3042\u3042\u3042",
            "ts": "1484824875.000002",
            "author_subname": "fantasticswallow",
            "channel_id": "D1A25G6E8",
            "channel_name": "Direct Message",
            "is_msg_unfurl": true,
            "text": "\u3042\u3042\u3042\u3042",
            "author_link": "https:\/\/devcussion.slack.com\/team\/fantasticswallow",
            "author_icon": "https:\/\/avatars.slack-edge.com\/2016-01-16\/18649887269_b1c2847c8a63b9a64a8c_48.png",
            "mrkdwn_in": [
                "text"
            ],
            "id": 1,
            "footer": "2 replies"
        },
        {
            "fallback": "Foo\uff01",
            "author_subname": "fantasticswallow",
            "text": "Foo\uff01",
            "mrkdwn_in": [
                "text"
            ],
            "author_link": "https:\/\/devcussion.slack.com\/team\/fantasticswallow",
            "from_url": "https:\/\/devcussion.slack.com\/archives",
            "ts": "1484826106.000008",
            "author_icon": "https:\/\/avatars.slack-edge.com\/2016-01-16\/18649887269_b1c2847c8a63b9a64a8c_48.png",
            "id": 2
        }
    ],
    "text": "",
    "type": "message",
    "subtype": "reply_broadcast",
    "user": "U0JK7N9D5",
    "ts": "1484826106.000010"
}

ウウウーーーーン……なんだこれ…
なんかもう頑張って対応…って無理じゃね…

RealTime Messagingにおけるスレッドイベント

最後にRTMでもらえるスレッド周りのイベントを紹介しましょう。
リプライメッセージそのものは普通にメッセージと同じでやってくるので特別に用意されていません。

まずmessage_repliedっていうのがあります。
https://api.slack.com/events/message/message_replied
これは簡単で、「すでにあるメッセージがスレッドになったぜ!」って通知をしてくれます。それだけです。

次にreply_broadcastがあります
https://api.slack.com/events/message/reply_broadcast
上のreply_broadcastがポストされたら飛んできます。



というわけでSlack APIのスレッド周りのお話でした。
reply_broadcastに殺意が湧くのとUIどうしろと感が半端ないこと以外は平和でしたね!

基本的にスレッドも普通のメッセージと同じフィールドを持っているのでスレッドガン無視も一つの手かなぁとは思います。
それはそれでどうなのよという感じですが、かといって対応しようにもそう簡単に実装できるものでもない気が…

この辺で

2016年を振り返るらしい

どちらかというと明日が冬コミ3日目という情報の方が信じられない…

毎年目標もなく気が向いたらブログを書くだけの人間ですが、振り返りは書いておきましょう。

プヨグラミング

思った以上に何も進歩がなかった…
最近こそNereidでひさびさにおうちプログラミングの機会は増えましたが、7月から11月とかマジでVisual Studioを起動した記憶がない…

それもこれもラボ畜がわるい。わたしはらぼちくにくるうけもの…。

来年はもう少し書きたい、せめてNereidは完成させたい。せめてSlackアプリはつくりおえ…ウッ

ゲーム

気が付いたらSteamが入っていた。

地球防衛して大復活の5面で焼かれていました。大復活難しい…。
地球防衛については割と頑張りましたが武器コンプリートをする前に心が折れてしまった悲しみ。マジで後半は天の兵団と竜の宴ループでしかなかったしなぁ。グングニルが早めに出たのでまだなんとかなったのですがとはいえやはり作業量がつらい。
なおフェンサーは最後まで使いこなすことはできませんでした。エアレイダーのリムペットガンで戦ってる方が戦力になる程度には才能が足りない。

そさげ

ぱどどらは今年は耐えきりましたが、ティターニアの8コンボ教室が突破できなくて残っていたモチベーション?使命感?みたいなのは死にました。
もう無理…転生ウズメでたら起こしてください…。いや起こさなくていいや…。
転生ミネルヴァでやるのは割と楽しかったのですが、なんか完全に時代に置いていかれた感じがあるのでもう無理かなぁという。

ゴ魔乙はギルドバトルで急速にやる気が墜落した気がします。
ギルドへの参加をやめて適当にプレイしてた頃のモチベーションを取り戻した方がいい気がする。
それはそうとしてソードが楽しいですね。使いこなせないけど。

10月くらいから新しくシャドウバーーーースを始めました。
私ふぁぼツバメ!しゃどうばーす知ってる? 本格アグロバトルがスマホでこんなに簡単に以下略。
違うんだ、私は宴冥府ルナちゃんとかノノパメラとかで遊びたいだけなんだ…なんだこの超越とかいうゴリラは…
宴冥府はその辺の鷹が飛んできて死ぬしパメラちゃんはテミス撃たれるしどっちも大体超越の前には無力だし……
とりあえず対戦する超越の8割が4ターン目までに0コス導きを使うの反則だと思いますううーーーー。

いやでもニンジンとかネクロアサシンとかでめっちゃ楽しい動きができるようになったのでルナちゃんは楽しい。勝てるとは言ってない。
なんだかんだで楽しんではいます。でもえいらは5コスでいいし運命は手札消滅でいいし超越はPP10以降じゃないと撃てなくなってほしい。
超越のセラフよりもどうにもならない感じはなんなんだ。

というか土にもふぇありーーさーくるみたいに1コススペルで土の印2枚とかひとつで3回くらい使える土の印とかください…。

お花

闇の組織の陰謀によりFlower Knight Girlを始めてしまった感じです。
巫女アネモネかわいい。

f:id:fantasticswallow:20161230104923j:plain

イベントはそこそこ楽です。ガチャ?知らんなぁ

f:id:fantasticswallow:20161230104809j:plain

パーティはそこそこそろってきたのですがまだ足りない気はします。
とりあえず虹が欲しい……。
総合力の割には善戦してくれることが多いのですがさすがにパーティ1に全振りしすぎて他のパーティでボスいくと簡単に全滅するのがつらい。

スノードロップほしいなぁと思いながら続けていますが果たしてどうなることやら…

大学その他もろもろ

無事にけんきゅーーーしつとかいうところでらぼちくになりました。
研究というのが大変につらいことだけわかりました。というか私は研究が向いてないのではという感じがしました。
普段から大変適当な人間なのでしっかりしたあれそれができないんだなぁという。
(もしかして:仕事も向いてない)

発想力もなければ論文執筆能力もなくすぐれたプレゼン能力もないのでもうだめです。

そんなこんなで来年からしゅーしょくです。内定という何かをもらえたのでお仕事にはありつけそうです。
問題は何か月もつんでしょうかというあれ。
お金をもらいつつ無心に働ければいいんですけどねぇ…。


というわけでなんかうつうつしてきたところで終わりです。
なんかだいがく関係でめっちゃ余裕のない一年だったなぁという気分です。来年からもそうなのかなぁ…せめてTLは追えるくらいの余裕がほしいなぁ…。
まず帰ってきて何もやる気が出ないの時点でダメさを感じる。

来年は死んだ目で日々を過ごさないようにだけ気を付けたいなぁと思います。

この辺で

追記:あれげは?と聞かれましたがばるどはーとしか買ってなかったので書くことがなかったというあれです。
来年の予定は未定です。オトメドメインは買いたいかなぁ程度。

ObservableCollectionでAddRangeする

2016年の霊圧がどんどん小さくなっていく…

XAMLな世界でItemsSourceといえばObservableCollection!という感じですが3回に1回くらいAddRangeがほしくなります。
foreachでAddしてるとなんか残念な気分になるというかこちらでいちいちforeachしたくないという。

// collection = IEnumerable<T>
// observableCollection = ObservableCollection<T>

foreach (var item in collection)
{
    observableCollection.Add(item);
}

別に4行だしいいじゃんといわれればそんな気分にもなりますが何度も書くのは嫌になります。
というわけで拡張メソッドでAddRangeを作ってみましょう。
もちろん上のやつを拡張メソッドにしてもうれしいかもしれませんがうれしくないのでそこそこパフォーマンスの向上を狙ってみます。

完成したソースコード

解説しても9割は不要な情報でしかないので今回のコードを先に貼っておきます

public static class ObservableCollectionExtensions
{
    public static void AddRange<T>(this ObservableCollection<T> source, IEnumerable<T> collection)
    {
        if (ValidateCollectionCount(source, collection))
        {
            return;
        }

        var itProperty = typeof(ObservableCollection<T>).GetProperty("Items", BindingFlags.NonPublic | BindingFlags.Instance);
        var colResetMethod = typeof(ObservableCollection<T>).GetMethod("OnCollectionReset", BindingFlags.NonPublic | BindingFlags.Instance);

        var list = itProperty.GetValue(source) as List<T>;
        if (list != null)
        {
            list.AddRange(collection);
            colResetMethod.Invoke(source, null);
        }
    }

    private const int switchForeachThresold = 2;
    private static bool ValidateCollectionCount<T>(ObservableCollection<T> source, IEnumerable<T> collection)
    {
        var count = collection.Count();
        if (count <= switchForeachThresold)
        {
            foreach (var item in collection)
            {
                source.Add(item);
            }
            return true;
        }

        return false;
    }
}

パフォーマンスについては後で書きますがforeachで追加するよりは10 ~ 20倍くらい早くはなります。
ただし素数1のコレクションをAddRangeで追加する場合、foreachの2倍ほど遅くなります。
collectionの要素数見てもいいかもしれませんが閾値までは調べてないので対応はしてません。
とはいえ要素数1のコレクションを100万回AddRangeで追加しての情報なので基本は問題ないとはおもいます? 気になるなら対応するとよいとおもいます。

追記(12/30):↑の問題にとりあえず対応してみました。ぶっちゃけ閾値1でいい気はします。(大幅なパフォーマンス改善が行えるのは1のみ)
閾値の変更についてはswitchForeachThresoldとかいう定数をいじってください。
そもそもforeachは絶対に使いたくない場合はif (ValidateCollectionCount)のブロックを削除してください。

ObservableCollectionの内部コレクションについて

AddRange処理には次の手順が必要になります。
・ObservableCollectionの内部コレクションに対して要素の追加
・NotifyCollectionChangedの実行

ところでObservableCollectionの内部コレクションの型はなんでしょうか? という疑問が出ます。
これについてはReferenceSourceから拾ってきましょう。ObservableCollectionはCollection<T>を継承しているためCollection<T>の内部コレクションがそのままObservableCollectionの内部コレクションになります。

Collectionのコンストラクタは次の通り。
referencesource/collection.cs at master · Microsoft/referencesource · GitHub

public class Collection<T>: IList<T>, IList, IReadOnlyList<T>
{
    IList<T> items;

    public Collection() {
        items = new List<T>();
    }

    // ...
}

まあわかりやすい。List<T>だそうです。
List<T>ということはList<T>.AddRangeで内部コレクションへの追加が行えます。知らない型だったらそれこそAddRangeの内部ソースについても調べる必要が出そうでしたがこれなら問題ないですね。

ObservableCollectionから先ほどのitemsにアクセスするには Collection(T).Items プロパティ (System.Collections.ObjectModel) が使えます。
しかしこの子はprotectedです。ぐぬぬ

ついでにNotifyCollectionChangedも見ておきましょう。

ObservableCollection.NotifyCollectionChangedの呼び出し

これはMSDNに載ってるのでReferenceSourceを見なくても余裕です。
ObservableCollection(T).OnCollectionChanged メソッド (NotifyCollectionChangedEventArgs) (System.Collections.ObjectModel)

アッアッ…これもprotected…

というわけでAddRangeするためには
・Reflectionでごり押し
・ObservableCollectionを継承したクラスを作る
のどちらかで対応する必要があります。グヌヌ

実際にAddRangeを追加しよう

というわけでReflectionでごり押ししたのが上の完成したなんたらです。
えっ?OnCollectionChanged呼び出してないじゃんって? これはこれでまた理由があります。ありますがいったん放置。

継承パターンについてはこんな感じになります。すっきりしてます。

public class ExtendObservableCollection<T> : ObservableCollection<T>
{
    public void AddRange(IEnumerable<T> collection)
    {
        ((List<T>)this.Items).AddRange(collection);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

問題は通常のObservableCollectionが使えないという点ですがそれを許容できるなら…?
(ただしパフォーマンスはReflectionと大差ない)

ところでNotifyCollectionChangedEventArgsではIListを用いて複数アイテムの変更を通知できます。
じゃあAddRangeでも使えばいいじゃん!っていうと確かに使えますが今度はListCollectionViewの制約にひっかかります。
どういうことかというと、"ListBoxにバインドした状態でAddRangeすると落ちる"ようになります。

この辺については
ListCollectionView/CollectionView doesn't support NotifyCollectionChanged with multiple items
とかで対応する方法とかは書いてあるようです。ObservableCollectionでAddRangeしたいがためにListCollectionViewとかいじるのは嫌なのでResetで対応しています。

(もちろんResetなのでUIをRefreshするのとほぼ同義の処理です。そのため1件とかでAddRangeしまくるとパフォーマンスが強烈に低下します。)

パフォーマンスを見てみる

とまあだらだら書いてきましたが、これでforeachと大差なかったらforeachでよくね!って感じなので見てみます。

比較用のAddRangeTestは次のようなものを用意。

public static void AddRangeTest<T>(this ObservableCollection<T> source, IEnumerable<T> collection)
{
    foreach (var item in collection)
    {
        source.Add(item);
    }
}

普通のforeachですね。ふつう。

計測ですがWindow.LoadedでObservableCollectionに追加して測定します。このときObservableCollectionはListBoxにバインドしておきます。

こんな感じ

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    var sw = new Stopwatch();
    sw.Start();
    // Test Method #1
    col.AddRange(Enumerable.Range(0, 1000000).Select(x => x.ToString()).ToArray());
    col.AddRange(Enumerable.Range(1000000, 1000000).Select(x => x.ToString()).ToArray());

    // Test Method #2
    for (var i = 0; i < 100; i++)
    {
        col.AddRange(Enumerable.Range(10000 *i, 10000).Select(x => x.ToString()).ToArray());
    }

    sw.Stop();
    Debug.WriteLine(sw.ElapsedMilliseconds);
}

LINQ使ってるあたりでなんか遅い気はしますが全部同じ条件だし問題ないでしょう。
5回測定した平均が下の通りです。値はミリ秒。
AddTestがforeach、AddRngがReflection、ExtAddが継承ベースのAddRangeです。

AddTest AddRng ExtAdd
test#1 12589.4 833.8 839.4
test#2 6076.8 424.0 383.8

基本的にforeachの場合、件数に対して一定の時間だけかかります。私の環境で100万件だとほぼ常に6000msですね。
逆に追加したReflectionなAddRangeだとcollectionの件数が少ないほど遅いですが大体3 ~ 5個要素があるとforeachより早くなります。
また200個くらいからそこまで実行時間が変化しなくなります。
AddRangeに対して、コレクションの要素数を変化させつつ100万件の要素を追加したときの時間を下に書いておきます。こちらもミリ秒。
(elementが2の場合、2個の要素を持つコレクションをAddRangeで50万回追加した。)

element time
1 12967
2 6707
4 4118
5 3040
10 2063
20 1078
50 715
100 582
200 484
1000 380
10000 389

もちろん常に一定数追加することはないと思いますので理論値として見といてほしいのですが、まあそこそこなパフォーマンスかなぁという感想です。
collection.Count < 3とかのときはAddで追加とかにしてもいいのですが単純に面倒だった…。


というわけでObservableCollectionにAddRangeする拡張メソッドの話でした。
これで楽していきたい…

そろそろ今年も終わるしまた今年の感想的な記事を書いて今年のブログは終わりかなぁ。

この辺で

DependencyObjectに独自DataContextを追加してBindingしたいと思った

思った。思ったができるとは言っていない。

世の中の奇特なふぁぼツバメとかいうやつが言いました。
「Bindingするのにx:Staticでソース指定するのだるくね?」
ええそうですね。どう考えても対応できないことが分かりますがクレーマーを放置してもしょうがないので対処します。

実は知らなかった「なぜSourceに初期値を入れないとDataContextをSourceに使用するのか」という話も含めてやっていきましょう。

そもそも独自DataContextって有効なの?

DataContextっていうDependencyPropertyを追加したらBindingのデフォルトのソースになるのか?という点からやっていきます。
FrameworkElementのDataContextを参考にしてこんなものを追加します。

public abstract class DataContextBase : DependencyObject
{
    public object DataContext
    {
        get { return (object)GetValue(DataContextPropert
        set { SetValue(DataContextProperty, value); }
    }


    // Using a DependencyProperty as the backing store for DataContext.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty DataContextProperty =
        DependencyProperty.Register("DataContext", typeof(object), typeof(DataContextBase), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.Inherits, (d, e) => ((DataContextBase)d).DataContext = e.NewValue));
}

で、これで hoge={Binding x}とか書いたら解決してくれる! なんていうことはありませんでした。
このオレオレDataContextちゃんを反映させるべく、BindingのデフォルトにDataContextを使ってくれるのはなぜ~っていうのを追っていきます

DataContextとBindingExpressionの関係

なぜも何もDataContextを使うようにしているからだろ!って言われそうですが、ではDataContextの利用はどうやっているのでしょうか。Reflection? って話については全く知りません。
BindingのデフォルトパラメータとかでDataContextを指定してるならそれを追従すれば独自DataContext生やせるよね!ということで追ってみましょう。

ざっくりいくと、実際はBindingExpressionでこのDataContextへの対応付けを行っています。
BindingでもFrameworkElementでもないです。ここがいまいちわかりづらい。

BindingExpressionにDataSourceというプロパティが存在します。これがSourceオブジェクトを返すプロパティです。
https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Data/BindingExpression.cs,166

ReferenceSourceが開けないとかのためにソースの一部をコピペするとこんな感じ。

/// <summary>The data source actually used by this BindingExpression</summary>
internal object DataSource
{
    get
    {
        // ~~~~~

        // if we're using DataContext, find the source for the DataContext
        if (_ctxElement != null)
            return GetDataSourceForDataContext(ContextElement);
 
        // otherwise use the explicit source
    }
}

ContextElementはFrameworkElementであることが保証されている?感じだと思います。詳しくは追えなかった…
ContextElementがnullでなければGetDataSourceForDataContextとかいうそれっぽい名前の関数にいきます。

https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Data/BindingExpression.cs,2694

object GetDataSourceForDataContext(DependencyObject d)
{
    // look for ancestor that contributed the inherited value
    DependencyObject ancestor;
    BindingExpression b = null;
 
    for (ancestor = d;
         ancestor != null;
         ancestor = FrameworkElement.GetFrameworkParent(ancestor))
    {
        if (HasLocalDataContext(ancestor))
        {
            b = BindingOperations.GetBindingExpression(ancestor, FrameworkElement.DataContextProperty) as BindingExpression;
            break;
        }
    }
 
    if (b != null)
        return b.DataSource;
 
    return null;
}

FrameworkElement.DataContextProperty

グエーーーッ

というわけでDataContextが使われる理由は、.NETのコード内でFrameworkElement.DataContextPropertyを基にBindingOperations.GetBindingExpressionsを利用したソース解決を行うコードがあるからのようです。

つまりどういうことだってばよ? というと、オレオレDataContextちゃんがFrameworkElement.DataContextでない限り、既定の処理では解決してくれないということです。

なんとしてでもDataContextを解決させる

ぶっちゃけデフォルトのBindingでは不可能です。
しかしどうしてもDataContextを自動でやってくれる夢が見たい!

ならばBindingそのものをいじればいいじゃない!ということでBindingを変えましょう。私のように完全にPresentationFrameworkなコントロール群と独立した環境であれば、既存のBindingを使う強いメリットも存在しません。
(デフォルトのxmlnsとpresentationが別になるので、普通のBindingを使おうとするとp:Bindingとかになる。)

なのでBindingを派生してBindingクラスを作ります。コードはこんな感じ。

public class Binding : System.Windows.Data.Binding
{
    public Binding()
        : base()
    {
        if (Source == null && RelativeSource == null && string.IsNullOrEmpty(ElementName))
        {
            this.RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self);
            if (this.Path == null)
            {
                this.Path = new System.Windows.PropertyPath("DataContext");
            }
            else
            {
                this.Path = new System.Windows.PropertyPath("DataContext." + this.Path.Path);
            }
        }
    }

    public Binding(string path)
        :base(path)
    {
        if (Source == null && RelativeSource == null && string.IsNullOrEmpty(ElementName))
        {
            this.RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self);
            if (this.Path == null)
            {
                this.Path = new System.Windows.PropertyPath("DataContext");
            }
            else
            {
                this.Path = new System.Windows.PropertyPath("DataContext." + this.Path.Path);
            }
        }
    }
}

Sourceに何も入っていない場合は、RelativeSourceで自分自身を参照して、DataContextを見に行くようにします。
Pathがすでにある場合はDataContext.Pathとなるように文字列結合で対応します。力技だ!

これをすることで今まではこう書いていたXAML

<CustomUI x:Class="NereidTestAddin.RibbonData"
             xmlns="clr-namespace:artfulplace.Nereid;assembly=artfulplace.Nereid"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NereidTestAddin">
    <Ribbon>
        <Tabs>
            <Tab IdMso="TabAddIns">
                <Group Id="MyGroup" Label="My Group">
                    <Button Id="aaaa" Label="{p:Binding TestButton1Label, Source={x:Static local:MainViewModel.Instance}}" />
                </Group>
            </Tab>
        </Tabs>
    </Ribbon>
</CustomUI>

こうできます!(DataContextはコードビハインドで設定)

<CustomUI x:Class="NereidTestAddin.RibbonData"
             xmlns="clr-namespace:artfulplace.Nereid;assembly=artfulplace.Nereid"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NereidTestAddin">
    <Ribbon>
        <Tabs>
            <Tab IdMso="TabAddIns">
                <Group Id="MyGroup" Label="My Group">
                    <Button Id="aaaa" Label="{Binding TestButton1Label}" />
                </Group>
            </Tab>
        </Tabs>
    </Ribbon>
</CustomUI>

やべえ!画期的だ!という誰も得しない解決方法でなんとか対応します。

ところで実は最初のDataContextだと動かなかったりします。XAMLでDataContextを設定していればいいのですが、設定していない場合は動きません。
というわけでこんなあれそれをDataContextの値が変更されたら呼び出します。

public static void UpdateBindingAll(this DependencyObject target)
{
    LocalValueEnumerator localValueEnumerator = target.GetLocalValueEnumerator();
    while (localValueEnumerator.MoveNext())
    {
        LocalValueEntry current = localValueEnumerator.Current;
        if (System.Windows.Data.BindingOperations.IsDataBound(target, current.Property))
        {
            var bind = System.Windows.Data.BindingOperations.GetBinding(target, current.Property);
            if (bind.Source == null)
            {
                System.Windows.Data.BindingOperations.GetBindingExpression(target, current.Property).UpdateTarget();
            }
        }
    }

    // if target has children, TODO: update children's Bindings.
}

UpdateTargetをしないとDataContextを変更したタイミングで値が書き換わらないことへの対処です。

まとめ

FrameworkElementを継承した方がいい


独自DataContextについて誰かやってないのかなぁと思って調べていたのですが、誰もやってなかったのはなんでだろうなーと思ったらこれはできないわ…という感じでした。
結局BindingそのものはできますがオレオレBindingだしなぁ…

もう少しRibbon XAMLネタは続きそうです。ひさびさにブログが書きまくれている気がする

この辺で

Officeのリボンコントロールの値をデータバインディングで変更する

すげえ!誰一人として検索してこなそうなタイトル!!

というわけで、前回DependencyObjectでなんか作っていましたが、WPFやぞ!?データバインディングできなくてどうする!ってことで対応しました。
さすがに全部対応はしてませんが同じようなコードを書けばあとはうまるはず…

ついでにRibbon XMLについても学習していきましょう。

リボンの動的な値変更

そもそもリボンのXMLは値の動的変更に対応しているの?というと対応はしています。
IRibbonUI.Invalidate method (Microsoft.Office.Core) とか
IRibbonUI.InvalidateControl method (Microsoft.Office.Core) を呼ぶとリボンの初期化を行います。
とはいえInvalidateしてもXMLを作り直すとかそんなことはしません。(これについてはGetCustomUIを呼ばれる回数を見て確認)
リボンXMLでは値を直接入力する方法 (普通にlabel="ああああ"とか)と値をコールバックから取得する方法の二つがあります。このうち後者のコールバックをひたすら連打することで初期化を行うのがInvalidateです。
つまり、値の変更に応じてInvalidateかInvalidateControlを呼ぶことで値の動的変更が行えるわけです。

というわけで
・リボンXMLにおけるコールバックの設定
・実際にデータバインディングの値を通知
の流れで見ていきます。

リボンXMLのコールバックとIRibbonExtensibility

リボンXMLとかいつも値で入れてたからコールバックとかわからん!っていう人がいるかと思います。私でs

f:id:fantasticswallow:20161203200825p:plain

xsdで見るとわかりやすいのですが、大体一つおきに"hoge"プロパティと"getHoge"デリゲートが並んでいることが分かります。
(labelであればlabel : ST_StringとgetLabel : ST_Delegate)
(idやinsertBeforeX系のように動的変更を許容しないものにはgetデリゲートはありません)
これらはhogeプロパティの値をgetHogeデリゲートでも設定できることを示しています。
つまりhogeプロパティの値にgetHogeで取得できる値を使います。このときgetHogeの返す値は一定である必要はもちろんありません。

というわけでこんなRibbon XML(ここはXAMLではない)(一部抜粋)を書いて

<group id="MyGroup" label="My Group">
  <button id="aaaa" getLabel="aaaa_GetLabel" />
</group>

こんなC#のコードを書くといいわけですよ。

public string aaaa_GetLabel(Office.IRibbonControl arg)
{
    return "test";
}

こうするとaaaaのボタンはtestというラベルで生成されます。

これで動的変更とか楽勝じゃん!って思うのですが一つ壁が存在します。それはリボン側から呼ばれるコールバックは使用しているIRibbonExtensibilityのメンバーである必要があります。
もちろんCreateRibbonExtensibilityObjectで渡しているIRibbonExtensibilityです。この中のメンバーでないものは呼べません。

普通にやる場合はそこまで困ることはなくて、ユニークな名前をつけていればOKなのですが、XAMLでやろうとするアホにとっては障壁です。オブジェクトの数だけGetLabelのコードを書いておくとかそういう手段はとれません。
ライブラリとしてとれる手段としては二つあって
・IRibbonExtensibilityを継承してもらって、継承先にコールバックのメソッドを追加してもらう。
・こちら側で何らかの手段でどのオブジェクトのコールバックかを特定し、対応するメソッドを呼び出す。
とかではないかなぁとおもいます。

今回はXAMLで書けるんだぜ!?ってことで当然後者一択です。
継承してコードビハインドで書けるなら前者もいいのですが、前回の記事でDependencyObjectとIRibbonExtensibilityは共存できないことはわかっているので後者で頑張りましょう。

頑張るといっても、実は各getコールバックの引数でやってくるIRibbonControlはIdプロパティを持っています。
このIdはXMLに書いてあるidと対応します。なので対応するオブジェクトを発見して、そのオブジェクトのコールバックメソッドの値をIRibbonExtensibility側のメソッドで返してあげればいいわけです。

文章で説明することへの限界を感じました。コードで書いていきましょう。

IRibbonExtensibilityとコントロールとの連携

GitHub見てください!って言えばいいんですけどなんか違うのでこっちに書きましょう。

まずボタンコントロールに対応するButtonクラスを以下のように定義します。

public class Button : DependencyObject
{
    // id dependency property and more...

    public string GetLabel()
    {
        return Label;
    }

    public string Label
    {
        get { return (string)GetValue(LabelProperty); }
        set { SetValue(LabelProperty, value); }
    }
    // Using a DependencyProperty as the backing store for Label.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty LabelProperty =
        DependencyProperty.Register("Label", typeof(string), typeof(Button), new PropertyMetadata(""));
}

このGetLabelをIRibbonExtensibility側から適切に呼び出すことができればいいわけです。

次にIRibbonExtensibility側にGetLabelで呼び出されるメソッドを作ります。

using Office = Microsoft.Office.Core;

// ~~~

[ComVisible(true)]
public class RibbonExtensibility : Office.IRibbonExtensibility
{
    // GetCustomUI(string) etc...

    private Dictionary<string, Button> ItemsDictionary { get; } = new Dictionary<string, Button>();
    
    public string NereidControl_GetLabel(Office.IRibbonControl arg)
    {
        return ItemsDictionary[arg.Id].GetLabel();
    }
}

Idは重複すると読み込めない制約があるのでDictionaryで管理できます。何らかの方法でButtonをIdをキーとしてDictionaryに入れて置き、RibbonExtensibility側で呼ばれたら対応するButtonを引っ張ってきます。

で、Buttonの返すXMLは次のようにするだけです

<button id="aaaa" getLabel="NereidControl_GetLabel" />

getLabelは常に一定です。labelの値に依存しないため結構きれい。
これでコールバック周りの準備は終わりです。あとはInvalidateControlを呼ぶだけです。

RibbonExtensibilityに次のコードを追加します。

internal static Action<string> NereidControls_PropertyChanged { get; set; }

public void Ribbon_Load(Office.IRibbonUI ribbonUI)
{
    this.ribbon = ribbonUI;
    NereidControls_PropertyChanged = id => ribbon.InvalidateControl(id);
}

private Office.IRibbonUI ribbon { get; set; }

で、Button側に次のコードをまず追加します。

private void NotifyChanged()
{
    RibbonExtensibility.NereidControls_PropertyChanged?.Invoke(Id);
}

internal static void DependencyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e, string propertyName)
{
    var obj = (Button)d;
    d.GetType().GetProperty(propertyName).SetValue(d, e.NewValue);
    obj.NotifyChanged();
}

そしてButtonにあるLabelPropertyのPropertyMetadataをDataBindingに対応させます。

// Using a DependencyProperty as the backing store for Label.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty LabelProperty =
    DependencyProperty.Register("Label", typeof(string), typeof(Button), new PropertyMetadata("", (d, e) => DependencyPropertyChanged(d, e, "Label")));

これで対応自体は完了です。これをすべてのプロパティに対して作れば終了です。頑張りましょう(誰がやるんだ)

実際に動くんです?

では試してみましょう。

まずViewModelとして次のものを作ります。

public class MainViewModel : INotifyPropertyChanged
{
    private MainViewModel()
    {
    }

    public static MainViewModel Instance { get; } = new MainViewModel();

    private string _testButton1Label = "";
    public string TestButton1Label
    {
        get { return _testButton1Label; }
        set
        {
            _testButton1Label = value;
            NotifyChanged();
        }
    }

    // NotifyChanged() => PropertyChanged(name) and PropertyChanged event;
}

次にXAMLに次のように書きます。

<CustomUI x:Class="NereidTestAddin.RibbonData"
             xmlns="clr-namespace:artfulplace.Nereid;assembly=artfulplace.Nereid"
             xmlns:p="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NereidTestAddin">
    <!-- Ribbon etc...-->
        <Button Id="aaaa" Label="{p:Binding TestButton1Label, Source={x:Static local:MainViewModel.Instance}}" />
        <Button Id="bbbb" Label="bbbb" Click="Button_Click_1" />
    <!-- Ribbon etc...-->
</CustomUI>

また、RibbonDataのコードビハインドには次のものを追加しておきます

public partial class RibbonData : CustomUI
{

    private void Button_Click_1(object arg1, RibbonEventArgs arg2)
    {
        MainViewModel.Instance.TestButton1Label = "bcbc";
    }
}

これでリボンを読み込んでみると次の画像のようになります。
f:id:fantasticswallow:20161203221458p:plain
Labelがないのは初期値が""だからですね…

これでbbbbを押すと次の画像のようになります。
f:id:fantasticswallow:20161203221533p:plain
すごい!ViewModelの値を変えたらリボンの値も変わってる!!


というわけでクソ長い一発ネタ感のある記事でした。
頑張って作っているので実際に使えるようになるのはまだ先です、年内に終わらせたいなぁ…
でも頭を使う作業はもうないはずだしあとはコピペでがりがり進むはず…?

github.com

↑に今回の記事のコードと実行サンプルがあるのでどんな感じで動いているのかは見ていただければなぁとは思います。

この辺で

OfficeのリボンXMLを別アセンブリのクラスから拡張する

ぐぬぬぬぬ、月一更新はしたいと思っていたのに…

OfficeのリボンカスタマイズにはXMLを書く場合とWinFormsもどきで頑張っていじる場合があると思います。
しかしXMLで書くのは面倒…インテリセンスがそこそこ効くけど…

というあれそれを解決するためにxamlで書けるようにしたい!というライブラリを作ろうとした話です。
完成?いつかするかなって…一応根本部分はできたしあとは生やせば……

なおあまりxaml関係ない記事です。IRibbonExtensibilityとかいうやつの話です

XAMLで扱えるクラスでリボンを拡張する

リボンの拡張にはIRibbonExtensibility (Microsoft.Office.Core)を使います。これさえ実装していればおっけー!という感じだと思います
ついでにXAMLで読み込むためにDependencyObjectを継承しておきましょう。

サクサク実装するとこんなんでしょうか

using System.Windows;
using System.Windows.Markup;
using Office = Microsoft.Office.Core;

// ~~~~~~

[ComVisible(true)]
public class RibbonExtensibility : DependencyObject, Office.IRibbonExtensibility
{
    public string GetCustomUI(string RibbonID)
    {
        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?> ...";
    }
}

とまあいつも通りダメコードなのですが、ひとまずこれで作ってみる感じかなと
DependencyObjectを継承してXAMLで扱えるようにして、IRibbonExtensibilityを実装してリボンを読み込んでもらえるようにします。

ちなみにIDTExtensibility2がいるとかどこかにあるんですけどVSTOアドインの場合は不要で、COMアドインの場合は普通に必須です。
(というよりはCOMアドインそのものの読み込みにIDTExtensibility2が必要なのでリボン関係ないのでは…とは思う)

GetCustomUIの中身は最初は動くXMLを入れておきましょう。結構検証で詰まりますし。

で、読み込み部分はThisAddin.csに次のコードをコピペします。
読み込み部分は単純で、オーバーライドしてIRibbonExtensibilityを返せばいい感じです

protected override Microsoft.Office.Core.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
    return new RibbonExtensibility();
}

やべえ!XAML全然関係ない!

で、アドインを実行するともれなく次のようなメッセージが出ます。

A QueryInterface call was made requesting the class interface of COM visible managed class 'RibbonExtensibility'. However since this class derives from non COM visible class 'DependencyObject', the QueryInterface call will fail. This is done to prevent the non COM visible base class from being constrained by the COM versioning rules.

DependencyObjectがCOMで参照できないんですけど!!って言われます。
参照できなくてもいいんだけど…と思って続行していると読み込んでくれません。つらい。

ちなみにDependencyObjectを継承するアホなことをしていなければ別ライブラリでIRibbonExtensibilityだけ実装で読み込めるはずです。なんと!このブログ記事の存在価値やいかに

というわけで答えを書いてしまいましたがComVisibleじゃないクラスを継承していなければいいだけなのでRibbonExtensibilityからDependencyObjectを消します。
代わりにRibbonExtensibilityを使っていた部分を置き換える別のクラスを作って、こちらに「DependencyObjectのみ」を継承します。IRibbonExtensibilityは不要です。

今回だと次のようなコードで対処できます。

[ComVisible(true)]
public class RibbonExtensibility : Office.IRibbonExtensibility
{
    public RibbonExtensibility(CustomUI ui)
    {
        customUI = ui.GetRibbonXml();
    }
    private string customUI { get; set; }
    public string GetCustomUI(string RibbonID)
    {
        return customUI;
    }
}

public class CustomUI : DependencyObject
{
    public string GetRibbonXml() { ... }
}

つまるところ適当に継承して手抜きをするなって話ですね。
別ライブラリとか関係なくね?って感じがしていますが、実質その通りでどこにあっても動きます。

XAML読み込みはどうなったのよ

こちらの顛末を書いておきます。

先ほどのCustomUIみたいなクラスを大量にはやすことでこんなxamlが記述できます。

<CustomUI x:Class="NereidTestAddin.RibbonData"
             xmlns="clr-namespace:artfulplace.Nereid;assembly=artfulplace.Nereid"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:NereidTestAddin">
    <Ribbon>
        <Tabs>
            <Tab IdMso="TabAddIns">
                <Group Id="MyGroup" Label="My Group">
                    <Button Id="aaaa" Label="aaaa" />
                </Group>
            </Tab>
        </Tabs>
    </Ribbon>
</CustomUI>

で、これを普通にXMLとして作るだけです。
作るとこんな感じになります。

<?xml version="1.0" encoding="UTF-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="Ribbon_Load">
  <ribbon>
    <tabs>
      <tab idMso="TabAddIns">
        <group id="MyGroup" label="My Group">
          <button id="aaaa" label="aaaa" />
        </group>
      </tab>
    </tabs>
  </ribbon>
</customUI>

ええまあいろいろ大変でしたが、読み込めない理由がただのタイプミスだったとかそんな話しかあとは残ってない…
上のを読み込むと普通にaaaaってボタンが増えます。もう画像はいいよね…


というわけでXAMLで書ける何かを作っている話でした。
文章が崩壊しまくっていてブログ書かなすぎの弊害を受けている…
それ以上にこれブログに書く意味あったのか大変に怪しい…

github.com
上の何かでそれっぽい作業をしてます。心優しい方はDependencyPropertyをはやしてくれると大変助かります。。。。。
ぶっちゃけすでに飽きかけているのもあり、それ以上にDataContextがFrameworkElement以上にしかないことを知ったり、UIの更新にはInvalidateでリボンそのものを読み込み直すしかなかったりでXAMLでやる意味あるのか?という疑問が出てきている…

この辺で

Baldr Heartの感想とか

久々に書く記事がこれで申し訳ない感じがあるけどここ最近ゲームしかしてないし…

というわけで先週にはBaldr Heartクリアしてましたが適当に感想とか書いとこうかなぁと
products.web-giga.com

特にそういう要素について書きませんけど18歳未満はあれなげな感じなので、そういうのに興味がないとか年齢的にあれとかあれとかだったら閉じましょう。ネタばれへの考慮もしません
あとそんなにいい感想を書かないのであれ

続きを読む