OneNoteのページをC#のコードでコピーする
私は知っている…誰も書いてない = 需要がないことをなぁ…!
というわけで、OneNoteのページを移動させるのがめんどくさいのでなんかアプリケーション作ってコピーしようと思ったのですが、OneNoteのApplicationClassにはCopyPageContentみたいな気の利くメソッドがありません。どういうことだ。
ApplicationClassをオブジェクトブラウザで見てるとこんなメソッドがあることはわかります。
・GetPageContent(string pageId)
・UpdatePageContent(string pageXml)
・DeletePageContent(string pageId, string objectID)
…なんかDeletePageContentは違う気がする(DeleteHierarchyの気がしてきた)。
まあ確かに世の中取得と追加ができればコピーはできるでしょう。コピーして元のページを消せば移動もOK!
なんとありがたい!!今すぐ悔い改めてほしい!!!
とりあえずページのコピーができれば移動も達成できるのでコピーの仕方について書いていきます。
コピーしたXMLのdiffを確認しよう
Office界では同じ処理がすでにある場合、既存の処理によって得られる結果を確認するというのは定石ですね。どうかしてるぜ!
Visual Studio CodeのCompareで比較してみます。
まあ雰囲気だけ感じ取ってもらえばいいのですが、変化しているのは
・Page.@ID
・OE.@objectID
・CallbackID.@callbackID
の3つのIDです。
PageのIDは存在するページのIDを指定する必要があります。このIDを基にUpdatePageContentはページの書き換えを行います。そのためコピー先のページIDに書き換えます。ここはReplace(元のID, 新しいページのID)で終わるので楽です。
OEのobjectIDはOE要素を追加した際に自動で振られるIDです。競合してはいけないため、IDを残してしまうとUpdatePageContentに失敗します。しかし特にこちら側で割り当てる必要もないので(勝手に追加される)、すべてのobjectIDを消せばこちらはOKです。
CallbackIDも同じく勝手に振られるのですが、画像のデータについてはこちら側で指定する必要があります。最も面倒な作業。
まあ実際やっていきましょう。
VS拡張でAdornmentLayerを下に置きたいとき
ラボ畜から解放され社畜への一歩を着々とウワーッ。
先週あたりにVisual Studio 2017のRTMがリリースされましたね。しかもRC入れてたら更新でRTMになってて驚き。
なんだかいろいろ便利機能が増えてるらしいのでちまちまと試していきたいですね。
ところで今や痛VS拡張はこれという何かが分からないので、自分で作っていれるのが主流なのか、それとも私が知らないだけでどこかでまだ作られてるのか知りませんが、最近は自分で作ったのを使っています。
最近はVSIXもかなり楽に作れるので初めてのVSIXに痛背景をおすすめしまs
しかし痛背景を追加しようと思ったとき、AdornmentLayerが前面にくると結構面倒なので後ろ側に回しましょう。
追記(3/16):@kosmosebiさんにViewportAdornemntTextViewCreationListenerのOrderで変更できるという情報をもらったので過去の文を後ろに回して書き直しました。
PredefinedAdornmentLayersが持ってる定数を基にいじれるらしいです。感謝しかない…
PredefinedAdornmentLayers の説明が不十分 | ブチザッキ
ViewportAdornmentを追加したときにすでに書かれているサンプルをもとにImageを追加するようにしたのが次のコードです。
public ViewportAdornment1(IWpfTextView view) { this.view = view ?? throw new ArgumentNullException("view"); var opacity = 0.4; var file = @""; var imgSrc = new BitmapImage(new Uri(file)); this.image = new Image { Source = imgSrc, Stretch = Stretch.UniformToFill, }; this.adornmentLayer = view.GetAdornmentLayer("ViewportAdornment1"); image.Opacity = opacity; this.view.ViewportHeightChanged += this.OnSizeChanged; this.view.ViewportWidthChanged += this.OnSizeChanged; view.Closed += (o, e) => this.adornmentLayer.RemoveAdornmentsByTag("vsbg-adornmentTag"); }
OnSizeChangedは今回は関係ないので載せてません。大体サンプル通りです。
これでAdornmentLayerを追加して、AddAdornmentとかするとわかるのですが、メソッド内で使用しているAdornmentLayerはかなり上の方の要素になっています。
(2017限定? 2015でもちょくちょくそんな現象あった気がするけど)
こうなると正直見づらいですし(画像が文字の前に来てしまう)、"定義をここに表示"みたいなコントロールを出す系の機能については追加したAdornmentLayerのコントロールにフォーカスを奪われてしまいます。痛くするのにここまでのデメリットをつけるのはつらいです。そのため、追加するAdornmentLayerを後ろの方に回しましょう。
(以下追記部分。過去の文は水平線以下)
ここでAdornmentLayerのZ-Orderを無理やり変更してたのが過去の文なわけですが、実際はViewportAdornemntを追加すると作成される"ViewportAdornmentTextViewCreationListener"のほうで変更ができます。
こんな感じのコードがデフォルトで追加されています。
/// <summary> /// Defines the adornment layer for the scarlet adornment. This layer is ordered /// after the selection layer in the Z-order /// </summary> [Export(typeof(AdornmentLayerDefinition))] [Name("ViewportAdornment1")] [Order(After = PredefinedAdornmentLayers.Caret)] private AdornmentLayerDefinition editorAdornmentLayer;
(そういえばこんなの変更した記憶があるような…)
さて、このまま実行するとどこに追加されるかというと、通常のエディタ画面の場合、最前面に配置されます。
そもそも前面に置くときBeforeとAfterどっちだよという感じですが、「Before = Caretの場合、Caretのほうが前面に配置されること」を指定します。つまり背面に置きたい場合はBeforeを、前面に置きたい場合はAfterを指定します。
では今回はどうしましょう?というと最背面に置きたいので…どこだ?
ブチザッキ情報によるとDifferenceChangesを指定するとよいみたいですが、指定できるパラメータに若干知らない子がいるのでちょっと調べてみましょう。
これまたWpfTextView._baseLayer.Childrenに含まれるImplementation.AdormnemtLayer._nameを調べます。
するとこのような結果になります。
(最背面)
outlining
negativetextmarkerlayer
ViewportAdornment1 (After = DifferenceChanges)
BraceCompletion
CurrentLineHighlighter
ViewportAdornment1 (After = DifferenceSpace)
VsTextMarker
ViewportAdornment1 (After = DifferenceWordChanged)
TextMarker
SelectionAndProvisionHighlight
BlockStructure
Inter Line Adornment
RoslynLineSeparator
Squiggle
RoslynSuggestions
TextContentLayer
CaretElement
RoslynRenameDashboard
ViewportAdornment1 (After = Caret)
(最前面)
After = Caretを指定すると本当に前面に来ていることが分かります。
逆に最背面はoutliningです。どうもOutliningあたりから並びが謎い。
Difference~~を指定した場合、その時点で存在していなくても上のような位置に入ってきます。DifferenceChangesはかなり後ろに来ることが分かります。
RoslynRenameDashboardがViewportAdornmentに挟まれる理由としてはおそらくCaret, ViewportAdornment1がロードされた後にRoslynRenameDashboardがAfter Caretで読み込まれるためだと思われます。
そのためRoslynRenameDashboardを指定するのはつらそう(そもそも何のレイヤーかわからないけど)
ひとまず後ろの方に置きたい!って要望をかなえる場合これでいいかと。
/// <summary> /// Defines the adornment layer for the scarlet adornment. This layer is ordered /// after the selection layer in the Z-order /// </summary> [Export(typeof(AdornmentLayerDefinition))] [Name("ViewportAdornment1")] [Order(Before = PredefinedAdornmentLayers.DifferenceChanges)] private AdornmentLayerDefinition editorAdornmentLayer;
outliningは指定したくないし…。
ちなみに結局stringで_nameを指定しているだけなので、Order(Before = "negativetextmarkerlayer")と入れても通ります。
何のレイヤーかわかりませんけど。そしてなぜかこうするとoutliningよりも背面になります。謎だ。
(ここから過去の文)
後ろに回すにあたって、まずWpfTextViewのAdornmentLayerの管理方法を確認しておきます。
WpfTextView._baseLayerがViewStackという型の値をもっており、このViewStackのChildrenに複数のAdornmentLayerが含まれています。
そしてViewStackは(たしか)Canvasを継承?親要素?してた…ような気がします。
正直実装周りはどうでもいいのですが、WPFでCanvasなりの並び順を変更するプロパティとしては Panel.ZIndex 添付プロパティ があります。
今回はCanvasなりが絡んでいて、WPFなのでこれでいけそうですね。
ZIndexは添付プロパティなのでPanel.SetZIndexで指定しましょう。
指定するのはどこでもいいのですが、GetAdornmentLayerの後ろで指定してみるとこんな感じ。
this.adornmentLayer = view.GetAdornmentLayer("ViewportAdornment1"); Panel.SetZIndex((System.Windows.UIElement)adornmentLayer, -550);
SetZIndexの第1引数がUIElementである必要があります。IAdornmentLayerは当然UIElementと関係ありませんが、実際のImplementation.AdormnemtLayerはUIElementを継承しているのでこのキャストが通ります。
今回は後ろに回したいので第2引数は適当にマイナスの数値を入れています。こんくらいあれば大体後ろになるだろう計算。
数字はお好みで。
ひさびさにかいたわりによくわからない記事となってしまった…。
日本語が壊滅している…つらい…。
ようやく春の休みなのでちょこちょこプログラミングを思い出しつつブログも書きたい。
この辺で
Slack APIのスレッドの扱いについて
進捗…ガハッ…
Slackにスレッド機能が追加されたらしいですね。
見てたら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レスポンスではちゃんと拾えています。
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を始めてしまった感じです。
巫女アネモネかわいい。
イベントはそこそこ楽です。ガチャ?知らんなぁ
パーティはそこそこそろってきたのですがまだ足りない気はします。
とりあえず虹が欲しい……。
総合力の割には善戦してくれることが多いのですがさすがにパーティ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とかいうそれっぽい名前の関数にいきます。
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を変更したタイミングで値が書き換わらないことへの対処です。
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のコールバックとIRibbonExtensibility
リボンXMLとかいつも値で入れてたからコールバックとかわからん!っていう人がいるかと思います。私でs
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"; } }
これでリボンを読み込んでみると次の画像のようになります。
Labelがないのは初期値が""だからですね…
これでbbbbを押すと次の画像のようになります。
すごい!ViewModelの値を変えたらリボンの値も変わってる!!
というわけでクソ長い一発ネタ感のある記事でした。
頑張って作っているので実際に使えるようになるのはまだ先です、年内に終わらせたいなぁ…
でも頭を使う作業はもうないはずだしあとはコピペでがりがり進むはず…?
↑に今回の記事のコードと実行サンプルがあるのでどんな感じで動いているのかは見ていただければなぁとは思います。
この辺で