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

空談録

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

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歳未満はあれなげな感じなので、そういうのに興味がないとか年齢的にあれとかあれとかだったら閉じましょう。ネタばれへの考慮もしません
あとそんなにいい感想を書かないのであれ

続きを読む

Office 2016 ver 16.0.7167.xとPowerPoint Zoomについて

PowerPoint 2016はアップデートでどんどん神がかっていく…

というわけでInsider向けにアップデートが来ていたらしいです。7/23に
まったく気づいていませんでした。ラボ畜は人を狂わせる

フォーラムの通知はこれ
http://answers.microsoft.com/en-us/office/forum/office_insider-registration/announcing-july-insider-update-1607167xxxx-for/a4c6159a-833e-43b8-9517-6ded4281592b
Office BlogでのJuly Updateの詳細はこれ
New to Office 365 in July—new intelligent services Researcher and Editor in Word and more - Office Blogs

変更点をフォーラムから引っ張ってくると
・自動ナビゲーション機能としてZoomを追加したよ!
・引用をWord上で行えるResearcher機能を追加したよ
Excel 2016からLocalのPowerBIにパブリッシュできるように
・Get&Transform周り?か何かで日付/時間変換を改良うんぬん
Outlookに@を付けたよ
Outlookで旅行の管理が楽になった
Outlookにいいアイデアがあったらフィードバックのサジェストから送れるよ
・Docs.comにOfficeから直接アップロードできるように

Outlook周りはよくわからないので誰か頼みました
Zoomは後で書きます

ResearcherはWordの新機能としてキーワードを検索してそれをドラッグアンドドロップで引用できるぜ!みたいな機能です。
便利そうではありますがあくまでペインで開くしページの詳細はブラウザに飛ぶので結局ブラウザでよくね?ってなりそう
なんとなくWordがTeXの立場を狙ってるんかなぁという感じではあります

PowerBI周りはサブスクリプションがないです。PowerBI.comじゃなくても大丈夫とかそんなんだと思います

Docs.comへのアップロードは使ってる人なら便利なのかなぁという気がします。私は結局PowerPoint OnlineならOneDrive埋め込みでよくねみたいな感じになりました

Wordが結構変わってる気がします。詳しい調査は今度します…(最近Wordを使う機会がない…)

PowerPointのZoomについて

で、まあZoomです。MorphもすごかったですけどZoomもなかなかやばい

Zoomは単純に言えば各セクションなりスライドのトップスライドへと遷移できる親スライドを作成する機能です
すごいめっちゃわかりづらい!

動画を適当に貼っておきます(14.9MB)

(↑なんかうまく動いてない?のでクリックしてOneDriveで見てください…)
とてもよさげでしょう?
では実際に作っていきましょう

適当にスライドを作成してInsert→Links→Zoomを選びます。
このとき3つありますが、Summary ZoomとSection Zoom, Slide Zoomの2つとの間には機能的に違いがあります。
今回はSummary Zoomを使います

f:id:fantasticswallow:20160801195848j:plain

するとこんな感じで使うスライドを選べるようになります。Summary Zoomでは選択したスライドAと選択したスライドCの間のスライドBはスライドAをトップスライドとするセクションに結合されます。
(画像でいう2枚目と4枚目のスライドはそれぞれセクションになり、3枚目のスライドは2枚目のセクションとなる。もちろん複数スライドがあればそのようになる)

今回は2,3,4枚目を選択して追加します。するとこのように新しいスライドが追加されます
f:id:fantasticswallow:20160801200211j:plain

まず注目してほしいのは左側です。選択スライドごとにセクションが分割されています。ここが割と大事だったりします
そもそもZoomの核となる機能は指定したスライドへの遷移です。その遷移の際にアニメーションをつけれるというのがZoomです
Zoomの優れている点は、通常同じことをすればZoomを含むスライドから遷移すると、そのあとにあるスライド、セクションに到達しますがZoomで遷移するセクションはスキップされて遷移する点です
例えば通常のセクションA、ZoomセクションB、Zoomで遷移するセクションC,D、通常セクションEという順で並んでいたとします。
(Return to ZoomをONという設定の場合)
上のスライドの遷移は A→B→C→B→D→B→E という順になります
(Return to Zoomを使わなければA→B→C→D→E)

またZoomの各スライドをクリックするとそのセクション、スライドに遷移します。
そのため特定のスライドだけ見たい!と言われてもすぐに飛べたりします。

ところでSummary ZoomとSection Zoom/Slide Zoomの違いですが、Summary Zoomはスライドをセクション分割して遷移するZoomを作成しますが、Section ZoomとSlide Zoomはスライドに対して何かをするわけではなく、Zoomのみを作成します。
ただしSlide Zoomは特に複雑なのですが、スライドの位置関係によってはZoomを使うとうまく遷移できません
(例えばZoomスライドA、普通のスライドB、Zoomで遷移するスライドC,D,Eがある場合、Aから通常遷移すると→B→C→D→Eと行きますが、Zoomで遷移するとA→C→D→Eとなってしまう。Return to Zoomを有効にしても無視されるので注意(E→Bとなるがそのままスライドショーが終了する))
Zoomは対象スライドを連続で並べて使用するのが吉です。適当にやると本当におかしくなる…


個人的にはZoomは使ってスライド作ってみたいなぁという感じがします
とはいえMorphより派手なので結構使いどころが難しそう
アジェンダに使うと面白いかなぁという気もします
どこかでまたLTかなぁ
とりあえず最近あまりOfficeを触れていないのでまた触っていきたいなぁというところ

この辺で

Windows向けSlackクライアント、FluffyFromageの開発版を公開します

ようやく日の目を見ることになった…長かった…(触ってない時期が)

というわけで2月だか3月から作っていたWPFなSlackクライアントであるFluffyFromageの"開発版"を公開します。
正式版?そんなものはないというのはさておき、個人的にまだ正式に出すつもりはないというだけのやつです。
単純に言えば機能が明らかに足りていないという状態
じゃあなんでそんなもんだすんだっていうと、必要な機能がよくわからないというのが感想だからです。

そもそもElectron版なSlackのアプリの完全な代替となることを目的としていません。
あくまでメッセージをパッと確認したりそれにレスポンスを行うことができる程度のものがあればいいなぁという形で作っています
とはいえそれしかできないのも大変微妙なので適当に触ってもらって「この機能がほしい!」みたいなのが分かったらいいなぁという

まあはい、モチベーションが不足気味ということです。
主に2か月まったく触らないでいたら何一つ思い出せなくなってしまったのが問題
若干そのあたりを変えたいなぁということで公開することにしました

で、肝心のアプリケーションですがこんな感じです。

f:id:fantasticswallow:20160722200308p:plain

f:id:fantasticswallow:20160722200330p:plain

ザ・開発途中…

Modern UI for WPFが結構いい感じだったのでこれで攻めてみました。若干戸惑いましたがかなりよいかなぁと
機能的には
・起動時メッセージ取得
・Real Time Messagingによるリアルタイム取得
・普通にポストとか
・絵文字入力支援(バグある)
とかそんな感じです

ダウンロードは https://dl.dropboxusercontent.com/u/12260653/FluffyFromage-0-3b.zip からどうぞ(Dropbox...)

追記(7/30):Windows 7だと動かないという情報をいただいて調べた結果、なぜかWinRTのアセンブリを参照していて、なおかつ使ってもいないのに型を読み込んでいたので該当部分を削除しました。たぶん動くはず(Win7な環境がない…)
直し忘れて#e7のままですがダウンロードしたファイルが0-3bなら問題ないはずなので…


ちょっと長くなりましたが、上のもろもろを見てよさそう!って思ったらぜひ使ってほしいです。ついでに感想をください…
あっ「使い物にならない」だけだと私も知ってるので書かなくて大丈夫です
どうなったら使える、そもそも根本的に合わないその他あると思いますが、対応できる内容であればこちらも頑張りますので…

ちなみにバージョンですが、そもそも普通の人間はバージョンを見てもよくわからないのでは?って思ったのでリリース回数でつけることにしました。
現在は#e7です(devcussionで過去に何回か出したのを継続)
うまく正式リリースまで行ければいいなぁというあれ

この辺で

UWPのMessageWebSocketでSlackのReal Time Messagingにつなぐとき

おーーーっとふぁぼツバメ選手助走をつけてからの渾身の腹パンをMessageWebSocketに打ち込んだぁぁぁ!
はい、どうでもいいですね

というわけでタイトル通りです。UWPでReal Time Messagingにつなぎたいんだけど!っていう話ですが、MessageWebSocketでつなぐのがたいへんクソだったという話です
マジでわけわからなすぎる。そもそも情報がなさすぎる。助けてUWPのプロ

Real Time Messagingにつなぐのには

rtm.start method | Slack
Slackのrtm.startメソッドたたいて、戻ってきたデータのurlを用いてWebSocketで接続するだけです。

ちなみにWPFのときはwebsocket-sharpを使っています。
github.com
めっちゃ素直でいい子です。残念ながらUWPには対応していませんが…

で、UWPのときはMessageWebSocketとかいうのを使います
MessageWebSocket Class (Windows)
つなぎ方は最高に不可解です。もう嫌だ

MessageWebSocket.ConnectAsyncのやり方について

いつも通りダメパターンから書いていきましょうね

MessageWebSocketに指定したWebSocket Uriで接続して、MessageReceivedの結果をIObservable<string>で返すメソッドConnectAsyncを考えます。
とりあえず次のような感じで書いてみましょう

using System.Reactive.Linq;
using Windows.Networking.Sockets;
using System.IO;
using Windows.Storage.Streams;
using System.Threading.Tasks;
using System.Reactive.Subjects;

// ~~~~~~~

private MessageWebSocket webSockets { get; set; }
private DataWriter dataWriter { get; set; }

public async Task<IObservable<string>> ConnectAsync(string url)
{
    webSockets = new MessageWebSocket();
    webSockets.Control.MessageType = SocketMessageType.Utf8;

    webSockets.Closed += WebSockets_Closed;

    var observable = Observable.FromEventPattern(webSockets, "MessageReceived").Select(x => {
        var e = ((MessageWebSocketMessageReceivedEventArgs)x.EventArgs);
        using (var reader = new StreamReader(e.GetDataStream().AsStreamForRead()))
        {
            return reader.ReadToEnd();
        }
    });

    await webSockets.ConnectAsync(new Uri(url));
    dataWriter = new DataWriter(webSockets.OutputStream);

    return observable;
}

まあ普通のFromEventPatternです。FromEventは使い方がわからず…
とりあえず上のコードは返したobservableをSubscribeしてもイベント登録が行えていないらしく何も飛んできません。これはUWPの仕様?
FromEventなら動くぜ!って方はご連絡をください…

というわけでSubject<T>を使って実現してみましょう
このときも次のように書くことはできません。(以下のコードはConnectAsyncのみ記述)

public async Task<IObservable<string>> ConnectAsync(string url)
{
    webSockets = new MessageWebSocket();
    webSockets.Control.MessageType = SocketMessageType.Utf8;

    webSockets.Closed += WebSockets_Closed;
    
    var messageSubject = new Subject<string>();
    
    await webSockets.ConnectAsync(new Uri(url));
    dataWriter = new DataWriter(webSockets.OutputStream);

    webSockets.MessageReceived += (sender, e) =>
    {
        using (var reader = new StreamReader(e.GetDataStream().AsStreamForRead()))
        {
            messageSubject.OnNext(reader.ReadToEnd());
        }
    };

    return messageSubject;
}

このコードは、「MessageReceivedのAddHandlerの部分でInvalidOperationException」を起こします
何を言ってるのかよくわからないって? 私も知らんし…むしろどういう記述してたら+=のところで落ちんの…
推測だけだとConnectAsyncの時点でMessageReceivedのイベントハンドラを囲い込んで実行する、とかそんな感じでしょうか。どっちにしてもどこに書いてあるんだ

というわけで最終的には以下のコードが動きます。

public async Task<IObservable<string>> ConnectAsync(string url)
{
    webSockets = new MessageWebSocket();
    webSockets.Control.MessageType = SocketMessageType.Utf8;

    webSockets.Closed += WebSockets_Closed;

    var messageSubject = new Subject<string>();
    webSockets.MessageReceived += (sender, e) =>
    {
        using (var reader = new StreamReader(e.GetDataStream().AsStreamForRead()))
        {
            messageSubject.OnNext(reader.ReadToEnd());
        }
    };
    
    await webSockets.ConnectAsync(new Uri(url));
    dataWriter = new DataWriter(webSockets.OutputStream);

    return messageSubject;
}

というわけでコードの順序を間違えると落ちることを覚えておきましょう

Pingを送ろう

さて、Real Time Messagingでは一定時間通信できていなかったらpingを送って通信が持続しているか確認しろと書かれています。
MessageWebSocketではちゃんと送信もできますので(当然だ)そちらも実装していきましょう
こちらも落ちるコードの実例がいくつもあります。見ていきましょう
…いうほどなかった

送るときのメソッドはstringを受け取ったらWebSocketで送るSendAsync(string)を実装します
usingは上のusingで足りるはず(System抜く人はいないだろう…)

public async Task SendAsync(string text)
{
    using (var dataWriter = new DataWriter(webSockets.OutputStream))
    {
        dataWriter.WriteString(text);
        await dataWriter.StoreAsync();
    }
}

まずは落ちるパターン、usingで囲むです
これをするとOutputStreamが閉じます。2回目以降は送れません。閉じてるって言われて落ちます
とはいえこの記事のコードだと接続時にDataWriterを作成して退避しているのでこれをする人はいないでしょう…

というわけでこうなります

public async Task SendAsync(string text)
{
    dataWriter.WriteString(text);
    await dataWriter.StoreAsync();
}

このときStoreAsyncではなくFlushAsyncだと動きません。
なんだすんなりいくじゃん?ってなるでしょう?
このとき「不正なデータを転送するとその時点でWebSocketが切断」されます
おそらくなんですが通信エラーを引くと何も言わずに死んでるのでは…

まじめにデータが正しいかどうかは確認した方がいいです。できるならWPFのほうで同じping文字列送信して動くことを確認しましょう。
私はそれでようやく気付きました。変な動きのときはそちらに気を付けましょう。

ちなみに私はこんなんを毎回送りつけています

private const string pingText = "{ \"id\": 1234, \"type\": \"ping\"}";

確認用にでもどうぞ


というわけでまあはい、わけがわからないよ…という感想です。
もうかかわりたくない。というかMessageWebSocketで調べても簡単なサンプルみたいなのしか出てこないのが本当に困った

ラボ畜ワークは今週はさほど多くないので自分のプログラミングを進めたいなぁというところ

この辺で

ライブラリを.NET StandardにしたらDataContractがないって言われたとき

プログラミングしな過ぎて久々にプロジェクトを開いたら理解ができなかった

Portable Class Libraryでライブラリを作っていたのですが、最近は.NET Standard Libraryとかいうよさげなバージョンがあるそうです
というのをC#ググるサイトのスライドを見て知りました
ufcpp.net

上の一番最初のスライドですね。他も面白いのでついでに見ましょう

さて、私の手元にも無駄にPortable Class Libraryで作られているものがあるので.NET Standardにするか~と思ったのですが(どこで使うのか不明)、.NET Standard 1.1に変えてみたところエラーだらけに
こんな感じのコードだったのですがどの辺が悪いのかすぐに理解できなかった

[DataContract]
public class Team
{
    [DataMember(Name = "id")]
    public string Id { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }

    [DataMember(Name = "domain")]
    public string Domain { get; set; }

    [DataMember(Name = "email_domain")]
    public string EmailDomain{ get; set; }

    [DataMember(Name = "icon")]
    public Icons Icon { get; set; }
}

まあ普通のDataContractなクラスですね
これのDataContractAttributeとDataMemberAttributeがないと怒られるわけですよ。なぜPCLで書けるコードが.NET Standardで書けないんだ、という感じです

と思って調べてたらこんなNuGetパッケージがありました。
NuGet Gallery | System.Runtime.Serialization.Primitives 4.1.1
ここに.NET Standard 1.0以上に対応すると書いてあるので、まあつまるところ足りなかったらこれを入れろということなのでしょう。むしろどこでそれを察知すればよかったんだ…

System.Runtime.Serialization.Jsonもないのでおそらく使用率が低い標準ライブラリについて使う場合はNuGetから落とす必要があるようです。一歩学習したぞ


ってだけの記事です。誰向けなんだ

ようやくプログラミング欲が復活してきたのでいろいろ進めたいところ
そろそろ埃をかぶってきたアプリケーションとか世に送り出したい…

この辺で