空談録

世界で5人くらいに役立ちたい

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

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

この辺で