2007 Office system におけるカスタム作業ウィンドウ、リボン UI、および VBA コードの再利用

概要: Microsoft の情報技術 (IT) が、十分に動作確認されたレガシ VBA コードを再利用しながら、2007 Office system のプログラミング拡張機能を利用している方法について学習します。記事では、Microsoft が社内で使用している意思決定支援システム Rhythm of the Business (英語) (「Microsoft gives powerful support to executive decision making process with enhanced functionality in Microsoft Office Excel 2007 」) を、Microsoft Office 2003 から 2007 Office system に変換した際に使用したいくつかの方法論についても説明します (26 印刷ページ)。

Sergei Gundorov、Microsoft Corporation

2006 年 12 月

適用対象: 2007 Microsoft Office スイート

目次

  • シナリオの概要

  • オン デマンドで Office アプリケーションを起動する

  • アプリケーション読み込み時にカスタム リボン タブを "アクティブ化" する

  • リボン UI でカスタム イメージを読み込む

  • 実行時にリボン UI プロパティをコマンド バーのように変更する

  • 2007 Office スイートでもレガシ コードを再利用する

  • まとめ

  • 追加情報

シナリオの概要

1991 年以来この仕事を続けている私のような Office 開発者であれば既にご存知かもしれませんが、2007 Microsoft Office system には、多くの拡張機能と新しい "遊び道具" が組み込まれています。それだけでなく、新しい条件付き書式が導入され、"書式が多すぎます" というエラー メッセージはもう表示されなくなり (従来は、現実的かつ適切なサイズのスプレッドシートであるにもかかわらず、このエラーが付いて回りました)、ビジネス ユーザー向けに訴求力のあるデータ ビジュアライゼーションが使えるようになりました。Microsoft Office Excel 2003 および Microsoft Visual Basic for Applications (VBA) から、この機能を Office の最新バージョンおよびマネージ コードに移行することを提案したとき、弊社アプリケーションのユーザーがまったく難色を示さなかった理由は、容易に想像できます。私たちは今回のプロセスでいくつかの教訓を得ました。リボン ユーザー インターフェイス (UI) は、コマンド バーのはるかに上を行くものです (確かに多少覚えなければならないことはありますが、リボン UI の操作は本当に快適です)。ドキュメントを中心としないカスタム作業ウィンドウは、旧来の VBA フォームと比べて格段に優れており、しかも必要に応じて旧来の VBA またはマネージ コードを存分に再利用することができます (UI に "若返り手術" を施し、アプリケーションの移行コストを低く抑えることができます)。

この記事では、次の方法について具体的に説明します。

  • アプリケーションをオン デマンドで起動し、[ホーム] タブではなくカスタム タブを最初に表示する。

  • リボン UI にカスタム アイコンを読み込む。

  • コマンド バーと同様の方法で必要なリボン UI 項目を表示する抽象レイヤを記述し、リボン UI の使いやすさと柔軟性を高める。

  • VBA や Office の従来のオートメーション コードを再利用できる抽象レイヤを作成する。

この記事で紹介するコンセプトは、マネージ Office アドインを対象としています。私たちがなぜ、新製品 Microsoft Visual Studio 2005 Tools for the 2007 Microsoft Office system (別名 "Visual Studio 2005 Tools for Office Second Edition") を使わなかったかと言うと、2006 年 3 月に変換を開始した時点では、この製品がまだ出現していなかったからです。しかし、マネージ アドインの方向性を採用することで、私たちは新しいオブジェクト モデルを探索する必要に迫られ、新しいプログラミング機能の内部的なしくみを学習しました。そこで得た経験を、この記事で公表することにしました。紹介するコンセプトの多くは、Visual Studio 2005 Tools for Office Second Edition にそのまま当てはまります。なお、この記事は、読者が Office 2007 リリースのオートメーションについての基礎的な知識を持っていることを前提としています。

オン デマンドで Office アプリケーションを起動する

大部分の Excel アドインは、ユーザーが新しい Excel インスタンスを開くたびに読み込まれます。たとえば、Microsoft 財務部門の標準的な従業員が使用しているデスクトップを見ると、5 つか 6 つのカスタム Excel アプリケーションがあることに気付くでしょう。このユーザーが単に Excel を使用したいだけで、5 つのアドイン全部を読み込む必要がない場合は、どうでしょうか。Excel と Excel 用に書かれたカスタム アプリケーションとを別々にするために、私たちは常にユーザーがオン デマンドでアプリケーションを起動できるようにしてきました。Microsoft Office 2003 では、これは簡単にできました。入り口としての役割を果たすブックの Open イベントを含む VBA アドインを使用し、ブックを開くためのショートカットをユーザーのデスクトップに配置しました。2007 Office system の新しいプログラミング機能 (リボン UI コールバック、カスタム作業ウィンドウなど) は、マネージ コード内でコールバックを使用することによって非常に効率的に利用されます。この場合、Visual Studio 2005 Tools for Office Second Edition、マネージ COM アドインという 2 つの選択肢があります。ほとんどのマネージ アドインは、アプリケーションの起動時に読み込まれるように設計されています。Visual Studio 2005 Tools for Office Second Edition を使用してマネージ COM Office アドインを作成する場合、これは既定の動作です。一方、マネージ コードを使用する場合、ユーザーが要求したときにショートカットを通じてカスタム Office アプリケーションを起動できるようにするには、次の作業が必要です。

  1. アドインを作成するとき、Setup プロジェクトでアドインの LoadBehavior レジストリ値を 1 に設定します。既定値の 3 を使用すると、Excel セッションのたびにアドインが読み込まれます。

    レジストリ エントリの例:

    HKCU\Software\Microsoft\Office\Excel\Addins\(your add-in name)\LoadBehavior=1

    その他の LoadBehavior レジストリ キー値についての詳細は、Andrew Whitechapel 著『Microsoft .NET Development for Microsoft Office (英語)』を参照してください。

  2. 起動プログラムを作成します。この起動プログラムが EXE または VBA アドイン ファイルのどちらであるかは、重要ではありません。どちらの種類のファイルにも、ショートカットを設定できます。ただし、ユーザーがこの起動プログラムを開くと、Office アドインの Connect プロパティが True に設定されるようにします。

私たちが使用した VBA 起動プログラムの例を示します。

Sub Workbook_Open()
   
   Dim comAddIn As Office.comAddIn
   ...
   'Set reference to COM add-in by using its ProgId property value.
   Set comAddIn = Application.COMAddIns("RhythmOfTheBusiness.Connect")
   comAddIn.Connect = True
   ...
End Sub

Office アプリケーション セッションのたびにアドインを読み込むという標準的な動作の代わりに、アプリケーション起動プログラムを使用するメリットは、他にもあります。たとえば、Connect プロパティを True に設定して、アプリケーションの読み込みを開始する前に、コードの更新がないかどうかを調べ、ユーザーのコンピュータにインストールされているコンポーネントや変更をスキャンすることができます。また、Excel がクラッシュしたためにアドインが無効になっていないかどうかをチェックし、再び有効にするため、ユーザーに適切な警告メッセージを表示することも可能です。その結果、サポート コールを大幅に減少させることができます。弊社の Excel ユーザーは、アプリケーションを使用する際、動作確認されていない VBA のオートメーションによって 0.5 GB というピボット テーブルの制限を踏み超えてしまうということがよくあります。

アプリケーション読み込み時にカスタム リボン タブを "アクティブ化" する

このテクニックは、上記のアプリケーション起動プログラムと同様のアプローチによるものです。コマンド バーの時代には、アプリケーションを読み込むとき、単にカスタム メニュー項目とコマンド バーを作成し、それらの表示可能プロパティが True に設定されるようにしていました。これはリボン UI には当てはまりません。リボン UI では、アクティブ化するタブをプログラムによって制御することはできません。2007 Office system で、レガシ コードによるカスタム コマンド バーまたはオートメーション用コードの追加メニューの使用を試みると、これらのカスタム コントロールは、他のカスタム アプリケーションと共に [アドイン] タブに表示されることに気付きます。弊社のビジネス ユーザーは、アプリケーションを起動したとき最初に表示されるのはアプリケーション タブにしてほしいと即座に主張しました (何しろ、マニュアルをほとんど読まず、他の 5 つのカスタム アプリケーションにも手こずっているユーザーが約 3,500 人いますから)。これまでのところ、カスタム アプリケーションのタブを最初に表示するための唯一の簡単な方法は、既定の [ホーム] タブより前にそのタブを挿入することでした。そのためにカスタム XML を定義する方法は、次のとおりです。

<?xml version="1.0" encoding="utf-8" ?>
<customUI xmlns="https://schemas.microsoft.com/office/2006/01/customui" 
         onLoad="GetRibbonXControl">
   <ribbon startFromScratch="false">
   <tabs>
      <tab id="ROB_Tab" label="Rhythm of the Business" 
   getVisible ="GetItemVisible" 
   insertBeforeMso="TabHome">
   . . .

次に、このしくみを説明します。起動プログラムがアドインの Connect プロパティを True に設定するとき、Excel は既に開いていて、他のすべてのカスタム アドインが読み込まれています。そのため、アプリケーションのカスタム UI の定義は、読み込みシーケンスの最後に読み込まれることになります。[ホーム] タブより前に他のアドインのタブが挿入されていないという事実を当てにしているわけです。これは企業以外の環境では制御が難しいことかもしれません。しかし、Microsoft の IT グループでは、非常に簡単なルールを実施しています。必要に応じてショートカットを使用してカスタム アプリケーションを読み込むとき、そのアプリケーションのカスタム タブは [ホーム] タブより前に挿入しなければならないというルールです。Excel セッションのたびにアドインを読み込む場合は、そのアドインのタブをリボンの後ろへ移動するか、既存のタブを用途変更する必要があります。既存のタブにカスタム ボタンを追加し、組み込みのアイコンを再利用することは、簡単にできます。カスタム イメージを追加することも難しくありません。

リボン UI でカスタム イメージを読み込む

リボン UI のカスタム イメージは、Office アプリケーションに明確な個性を与えます。また、現在使用しているアプリケーションが単なる Excel ではないことをユーザーに伝える役割も果たします。リボン UI では、独自のカスタム アイコンおよびイメージを簡単に読み込むことができます。次に、私たちのプロジェクトで最もうまくいった例を示します。ここでは、リボン UI コントロールにカスタムのビットマップおよびアイコンを使用しています。

リボン コントロール用のカスタム イメージを取得するようにコールバック関数を定義する方法としては、グローバルまたはコントロール単位の 2 とおりがあります。次のカスタム UI の XML 定義では、グローバルなイメージ コールバック関数を使用しています。

<?xml version="1.0" encoding="utf-8" ?>
<customUI xmlns="https://schemas.microsoft.com/office/2006/01/customui" 
         onLoad="GetRibbonXControl" 
         loadImage="GetItemIcon">
   <ribbon startFromScratch="false">
   <tabs>
      <tab id="ROB_Tab" label="Rhythm of the Business" 
   getVisible ="GetItemVisible" 
   insertBeforeMso="TabHome">
      <group id="ROBGroup1" label="ROB Tools" >
         <button id="ROB_Button" 
         size="large" 
         image="ROB_Button.bmp" 
         supertip="Launches ROB Team intranet site"/>

この例で、ROB_Button.bmp はアセンブリ内の埋め込みリソースです。このリソースの名前が loadImage コールバック関数に渡されています。カスタム UI タブにこのアイコンを読み込むすべてのコントロール関数のグローバルなコールバックは、次のとおりです。

internal ResourceManager Resources = new ResourceManager();
. . .
public stdole.IPictureDisp GetItemIcon(string image_id)
{
return Resources.GetIcon(image_id);
}

注意

マネージ COM アドインの場合、この関数は Connect クラスで定義する必要があります。

次に、アセンブリからイメージを取り出し、イメージの背景を透明にし、リボン UI で必要とされる形式 IPictureDisp にイメージを変換するコードの例を示します。

class ResourceManager
   {
      Assembly thisAssembly = Assembly.GetExecutingAssembly();
      string[] resources=
         Assembly.GetExecutingAssembly().GetManifestResourceNames();
      //Load custom icon for Ribbon control from embedded assembly resources
      public stdole.IPictureDisp GetIcon(string resourceName)
      {
         foreach (string resource in resources)
         {
            if (resource.EndsWith(resourceName))
            {
               try
               {
                  System.IO.BinaryReader customIconReader =
                     new System.IO.BinaryReader(
                     thisAssembly.GetManifestResourceStream(resource));
                  if (customIconReader != null)
                  {
                     if (resourceName.ToUpper().EndsWith(".ICO"))
                     {
                        System.Drawing.Icon customIcon =
                           new System.Drawing.Icon(
                             customIconReader.BaseStream);
                        return 
            ConvertImage.Convert(customIcon.ToBitmap());
                     }
                     if (resourceName.ToUpper().EndsWith(".BMP"))
                     {
                        System.Drawing.Bitmap customBitmap =
                           new System.Drawing.Bitmap(
                           customIconReader.BaseStream);   
                        customBitmap.MakeTransparent(
                           customBitmap.GetPixel(0,0));
                        return ConvertImage.Convert(customBitmap);
                      }
                  }
               }
               catch (Exception e)
               {
                  Connect.RobApp.appFuncCaller.MakeLogEntry("Error 
         Loading " + resourceName + "-" + e.Message);
               }
            }
         }
         return null;
      }
   }

   //Helper class to convert image to IPictureDisp -
   //OLE type is the only type recognized by callback function
   //SOURCE: https://msdn.microsoft.com/ja-jp/library/ms268747.aspx
   sealed internal class ConvertImage : System.Windows.Forms.AxHost
   {
      private ConvertImage(): base(null)
      {
      }
      public static stdole.IPictureDisp Convert(System.Drawing.Image image)
      {
         return (stdole.IPictureDisp)System.Windows.Forms.AxHost
            .GetIPictureDispFromPicture(image);
      }
   }

アセンブリ イメージ内のカスタムな埋め込みリソースを取得するには、別の方法もあります。IPictureDisp を返すヘルパ クラスに注目します。これは、リボン UI で組み込みの Office アイコン以外に受け入れられるカスタム イメージの種類としては、唯一のものです。上記の例で、その他に役立つコード行は、ビットマップ イメージの透明な背景を設定する行です。ビットマップ イメージの背景を透明にしないと、おそらく、イメージの周囲に目障りな黒い四角形が表示されることになります。

実行時にリボン UI プロパティをコマンド バーのように変更する

リボン UI を初めて見た Office 開発者が真っ先に探すのは、CustomRibbonUI.Items のような要素でしょう。この考え方はもう通用しません。IRibbonUI を返す onLoad コールバックを見つけると、だれもが大喜びします。しかし、IRibbonUI タイプが Invalidate() および InvalidateControl(string) の 2 つのメソッドしか持たず、しかも、リボン項目のコレクションへのアクセスをまったく提供しないことに気付くと、その喜びは終わってしまいます。リボン UI では、コールバックとユーザーの "プル" にすべてが依存しています。しかし、簡単な抽象型によって、一般的なリボン UI コントロールの大部分のプロパティを、なじみのある方法で表示できます。それは、MyRibbonButton1.Visible=true というテクニックです。これは、昔ながらの方法にあくまでも固執する開発者や、高度な言語でのコールバックを難しいと感じている開発者を Office 開発に引き戻すのにも役立ちます。

リボン UI のコールバック関数はいずれも、Connect クラスに置く必要があります。固有のコールバックを使用するコントロールが多ければ、Connect クラスは大きくなる一方です。しかし、Connect クラスにあらゆるものを詰め込むことは、優れたオブジェクト指向型プログラミングの原則に反しています。私たちが使用しているテクニックなら、必然とされる密結合を回避し、リボン コールバックを Connect クラス以外の固有のクラスで実際に実装することができます。また、私たちはコールバックとフラグの代わりにプロパティを使用することにより、実行時にアクセス可能なリボン UI 項目のコレクションを取得しています。

カスタム UI の XML で汎用コールバックを定義する

私たちはアドインの読み込み時に、カスタム リボン UI の XML をプログラムによって作成しています。具体例として、私たちのコードが生成する XML フラグメントの一例を示します。

<?xml version="1.0" encoding="utf-8" ?> 
   <customUI xmlns="https://schemas.microsoft.com/office/2006/01/customui" 
   onLoad="GetRibbonXControl" 
   loadImage="GetItemIcon">
   <ribbon startFromScratch="false">
   <tabs>
   <tab id="ROB_Tab" 
      label="Rhythm of the Business" 
      getVisible="GetItemVisible" 
      insertBeforeMso="TabHome">
      <group id="ROBGroup1" label="ROB Tools">
      <button id="ROB_Button" 
      size="large" 
      image="ROB_Button.ico"      
      getLabel="GetItemLabel" 
      onAction="ExecuteItemOnAction" 
      getVisible="GetItemVisible" 
      getEnabled="GetItemEnabled" /> 
      <button id="ROB_Tree_Button" 
      image="DeckTree.bmp" 
      size="large" 
      getLabel="GetItemLabel" 
      onAction="ExecuteItemOnAction" 
      getVisible="GetItemVisible" 
      getEnabled="GetItemEnabled" /> 
. . .
     </group>
   </tab>
   </tabs>
   </ribbon>
   </customUI>

この例で特に注目すべき点は、一連のリボン UI 項目のコールバックが同じであるという事実です。そのため、可能な限り汎用的にしておきたい Connect クラス内で、コールバック関数の集合を非常に小さくすることができます。

汎用カスタム UI のコールバックをサポートする

上記の例で示した汎用カスタム UI のコールバック関数の定義をサポートするために、Connect クラスがどのようになっているかを示します。リボン UI に関連するすべてを 1 つの ConnectRibbonX.cs ファイルに収めるため、私たちは Microsoft .NET Framework 2.0 の部分クラスのサポートをフルに活用しています。

   public partial class Connect
   {
      //RibbonX reference for callbacks
      internal IRibbonUI ribbonX;
      //Application reference to enable all event handlers access to application state  
      internal static Connect RobApp;
      //ROB Ribbon Items collection
      internal Dictionary<string, IRibbonBasicItem> AllRibbonItems = new 
      Dictionary<string, IRibbonBasicItem>();

      region Ribbon CustomUI callbacks
      //Callback to set RibbonX control reference 
      public void GetRibbonXControl(IRibbonUI ribbon)
      {
         //Reference to enable access to RibbonX
         //So we can call Invalidate and InvalidateControl methods inside this code
         ribbonX = ribbon;
      }

      //Generic calls that expose Ribbon items in a command bar-like way
      public string GetItemLabel(IRibbonControl control)
      {
         return AllRibbonItems[control.Id].Label;
      }

      public bool GetItemVisible(IRibbonControl control)
      {
         return AllRibbonItems[control.Id].Visible;
      }

      public bool GetItemEnabled(IRibbonControl control)
      {
         return AllRibbonItems[control.Id].Enabled;
      }
      
      //More complex Ribbon items with type-specific test
      public void ExecuteItemOnAction(IRibbonControl control)
      {
         if (AllRibbonItems[control.Id] is IRibbonItemWithAction)
         {
            ((IRibbonItemWithAction)AllRibbonItems[control.Id]).ExecuteOnAction();
         }
      }
. . .

AllRibbonItems は、カスタム タイプ IRibbonBasicItem の汎用的な Dictionary コレクションです。このコレクションは、このアセンブリ内の全クラスに対して可視であり、すべてのリボン項目およびそのプロパティへのコントロール名によるアクセスを提供します。ここでは、リボン UI のネイティブな control.id プロパティが利用されています。

IRibbonBasicItem およびその他の拡張されたタイプを設定する

このタイプには、実行時に容易に管理したい基本的なプロパティの定義を含める必要があります。前に示したカスタム UI の XML の例では、GetItemLabel、GetItemVisible、GetItemEnabled、および ExcuteItemOnAction を定義していました。次に、このインターフェイスがコードでどのように設定されているかを示します。

interface IRibbonBasicItem
   {
      string ControlID { get; }
      bool Enabled { get; set; }
      string Label { get; set; }
      bool Visible { get; set; }
   }

   interface IRibbonItemWithAction
   {
      void ExecuteOnAction();
   }

ここでは、コントロール ID と使用頻度の高い 3 つのプロパティを追跡しています。次の例では、アクションの割り当てられたボタンなどのリボン コントロールを示します。

ボタンのコールバックを作成する

すべての基本的なリボン項目のタイプは、次のとおりです。

internal class RibbonBasicItem : IRibbonBasicItem
   {
      string controlID = "";
      string label = "Label not set!";
      bool visible = false;
      bool enabled = false;
      internal RobRibbonItemType ItemType;

      internal RibbonBasicItem(string ControlID)
      {
         this.controlID = ControlID;
         Connect.RobApp.AllRibbonItems.Add(controlID, this);
      }

      region RibbonItem properties

      //Read-only control ID
      public string ControlID
      {
         get
         {
            return controlID;
         }
      }

      public string Label
      {
         get
         {
            return label;
         }

         set
         {
            //Restricting the label length
            if (value.Length <= 50)
            {
               label = value;
            }
            else
            {
               label = value.Substring(0, 50);
            }

            if (Connect.RobApp.ribbonX != null)
            {
               Connect.RobApp.ribbonX.InvalidateControl(controlID);
            }
         }
      }

      public bool Visible
      {
         get
         {
            return visible;
         }

         set
         {
            this.visible = value;
            if (Connect.RobApp.ribbonX != null)
            {
               Connect.RobApp.ribbonX.InvalidateControl(controlID);
            }
         }
      }

      public bool Enabled
      {
         get
         {
            return enabled;
         }
         set
         {
            this.enabled = value;
            if (Connect.RobApp.ribbonX != null)
            {
               Connect.RobApp.ribbonX.InvalidateControl(controlID);
            }
         }
      }

      endregion
      }

アプリケーションのボタンの状態を追跡するためにクラス内に複数のフラグを設定する必要性がなくなりました。また、特定のコントロールの状態を追跡するフラグが変化したとき、希望する効果をトリガするために、直ちに InvalidateControl コールバックを実行する必要性もなくなっています。代わりに、このリボン項目プロパティの定義を使用すれば、次の 1 行を指定するだけで済みます。AllRibbonItems["ROB_Button"].Visible=true。

ボタン コントロールの一般的な抽象型

IRibbonBasicItem を拡張する、使用頻度の最も高い "ボタン" タイプの UI コントロールの抽象型は、次のとおりです。

   //Ribbon item dynamic onAction method signature
   internal delegate void RibbonAction(RibbonBasicItem ribbonItem);

   internal class RibbonButtonItem : RibbonBasicItem, IRibbonItemWithAction
   {
      //Control-specific
      internal RibbonAction OnControlAction;

      internal RibbonButtonItem(string ControlID)
         : base(ControlID)
      {
      }

      //Using a dedicated method to perform test and ensure delegate is not null
      //instead of a try-catch block
      public void ExecuteOnAction()
      {
         if (OnControlAction != null)
         {
            try
            {
               OnControlAction(this);
            }

            catch (Exception e)
            {
               MessageBox.Show("Error executing action for: "+this.ControlID+"\n"+
                  e.Message+"\nCheck application error log. "+
                  "You might need to restart the application.","Rhythm of the 
                   Business");
            }
         }
      }
   }

   //All other specific implementations of Ribbon items 
   //must derive from RibbonBasicItem
}

RibbonAction は、特定のコントロールのアクションを実行するとき、そのコントロールの状態にアクセスできるようにするために、RibbonBasicItem タイプをパラメータとして使用するデリゲートです。

コマンド バーのようなリボン UI 項目を作成する

次に、コマンド バーのようなリボン UI 項目を作成します (今回のケースでは、大部分がボタンです)。

   internal class RibbonItemsCreator
   {
      OnControlAction ribbonActions = new OnControlAction();
      
      internal void CreateRibbonItems()
      {
         //Temporary type-specific variables used to construct
         //application-level collection
         RibbonBasicItem itemBasic;
         RibbonButtonItem itemButton;
         . . .
         //ROB_Tab
         itemBasic = new RibbonBasicItem("ROB_Tab");
         itemBasic.Label = "Rhythm of the Business";
         itemBasic.Enabled = true;
         itemBasic.Visible = true;

         //ROB_Button
         itemButton = new RibbonButtonItem("ROB_Button");
         itemButton.Label = "Link to ROB Homepage";
         itemButton.Enabled = true;
         itemButton.Visible = true;
         itemButton.OnControlAction = new 
            RibbonAction(ribbonActions.ROB_Button_OnAction);
         
         //ROB_Tree_Button
         itemButton = new RibbonButtonItem("ROB_Tree_Button");
         itemButton.Label = "Show GEO Tree View";
         itemButton.Enabled = true;
         itemButton.Visible = true;
         itemButton.OnControlAction = new 
            RibbonAction(ribbonActions.ROB_Tree_Button_OnAction);
            . . . 
}
   }

注意

RibbonBasicItem のコンストラクタは、AllRibbonItems に自動的に新しいコントロールを追加します。つまり、カスタム UI の XML をオン デマンドで生成します。UI の定義を 2 か所で保守しては、カスタム UI の XML 定義と、その情報および状態を格納するタイプとの間に競合を引き起こす可能性があるのではないかと思われるかもしれません。しかし、検証のための各種ルールとコントロールの XML を作成するメソッドにより、このような競合は起こらないことが保証されています。この例では、わかりやすさの観点から、カスタム XML をオン デマンドで作成するコードは意図的に除外しました。

OnControlAction を使用する

上記の例でまだ説明していない唯一のタイプが、OnControlAction です。OnAction メソッドの割り当てではマルチキャスト デリゲートを使用しているため、処理メソッドの再割り当て、追加、または削除は、実行時に簡単に行うことができます。これは、この抽象型によるメリットの 1 つです。

   class OnControlAction
   {
      Connect thisApp = Connect.RobApp;
      . . .
      public void ROB_Button_OnAction(RibbonBasicItem ribbonItem)
      {
         thisApp.appFuncCaller.OpenHomepage();
      }

      public void ROB_Tree_Button_OnAction(RibbonBasicItem ribbonItem)
      {
         thisApp.ShowTreeViewTaskPane();
      }
   . . .
   }

appFuncCaller は、レガシ コードの呼び出しを格納する特殊なタイプです。次のセクションでは、このタイプについて詳しく説明します。

2007 Office スイートでもレガシ コードを再利用する

私たちの経験では、すべてのカスタム ソリューションが 2007 Office system でも引き続き使用できます。この点で、Office プロダクト チームは実にすばらしい仕事をしています。2005 年 8 月に初めて発表された Excel のプレ ベータ 1 バージョンでさえも、アプリケーションを読み込んで実行することができました。[アドイン] タブにメニューやコマンド バーが表示されることは特に求めていませんでしたが、これがレガシ コードに対する既定の動作になっています。私たちは、この 3 年間で非常に機能豊富な VBA アプリケーションを開発しており、2007 Office スイートでリボン UI およびカスタム作業ウィンドウを使用したいと強く望んでいました。レガシ VBA コードには何も問題はありませんでした。徹底的に動作確認されており、安定性にも優れています。課題は、マネージ コードを完全に書き換えることなく、カスタム作業ウィンドウでレガシ VBA の関数をリボン UI および Microsoft .NET ベースのカスタム コントロールに結合させることでした。そこで、VBA コードの先頭に、非常に単純な抽象型を書き加えることにしました。その結果、2007 Office スイートへの移行コストを最小限に抑えながら、新しいツール機能の開発に専念することができました。

私たちは、VBA コード ファイルを読み込む、次のようなタイプを作成しました。

   internal class XlaAddinLoader
   {
      internal Excel._Workbook xla;
      object mv = Missing.Value;

      /// <summary>
      /// Opens the specified VBA file
      /// </summary>
      /// <param name="xl">Excel application instance in which to open the file 
      /// specified</param>
      /// <param name="addInName">Add-in file name</param>
      /// <returns></returns>
      internal bool OpenAddin(Excel._Application xl, string addInName)
      {
         if (!addInExists(addInName)) return false;

         try
         {
            xla = xl.Workbooks.Open(addInName, mv, mv, mv, mv, mv, mv, mv, mv, mv, 
              mv, mv, mv, mv, mv);
            return true;
         }
         //Add specific error trapping
         catch
         {
            throw;
         }
      }

      private bool addInExists(string addInName)
      {
         try
         {
            FileInfo addinFile = new FileInfo(addInName);
            return addinFile.Exists;
         }
         //Add specific error trapping
         catch
         {
            throw;
         }
      }
   }

続いて、VBA から実行するサブタイプおよび関数を規定し、いくつかのオーバーロードを追加しました。

   /// <summary>
   /// Base class for executing VBA code call-throughs
   /// </summary>
   public class XlaFunctionsWrapper
   {
      protected Excel.Application xl;
      protected Excel._Workbook xla;
      protected object mv = Missing.Value;
      protected string addInName = "";
      XlaAddinLoader AddIn;
      string addInPath = "";

      public XlaFunctionsWrapper(Excel.Application xlInstance, string addInName, 
         string addInPath)
      {
         xl = xlInstance;
         AddIn = new XlaAddinLoader();
         if (AddIn.OpenAddin(xl, addInPath))
         {
            xla = AddIn.xla;
            this.addInName = addInName;
            this.addInPath = addInPath;
         }
         else
         {
            MessageBox.Show("Unable to open: \n" + addInPath);
         }
      }

      //Parameterless void VBA call-through overload
      public void ExecuteCall(string funcName)
      {
         if (xla != null)
         {
            xl.Run(addInName + "!" + funcName, mv, mv, mv, mv, mv, mv, mv, mv, mv, 
               mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, mv, 
               mv, mv, mv, mv);
         }
      }
      
      //Parameterized VBA call-through overload
      public void ExecuteCall(string funcName, params object[] parameters)
      {
         //Initializing optional parameters
         //Application.Run takes 30 parameters
         object[] par = GetVBAParameters(parameters);

         if (xla != null)
         {
            xl.Run(addInName + "!" + funcName, par[0], par[1], par[2], par[3], 
               par[4], par[5], par[6], par[7], par[8], par[9], par[10], par[11],
               par[12], par[13], par[14], par[15], par[16], par[17], par[18],
               par[19], par[20], par[21], par[22], par[23], par[24], par[25], 
               par[26], par[27], par[28], par[29]);
         }
      }

      //Parameterized VBA call-through overload with object return type
      //NOTE: Returned object needs to be cast to its proper type in the calling 
      //function
      public object ExecuteGetValue(string funcName, params object[] parameters)
      {
         //Initializing optional parameters
         //Application.Run takes 30 parameters
         object[] par = GetVBAParameters(parameters);

         if (xla != null)
         {
            return xl.Run(addInName + "!" + funcName, par[0], par[1], par[2], par[3],
               par[4], par[5], par[6], par[7], par[8], par[9], par[10], par[11], 
               par[12], par[13], par[14], par[15], par[16], par[17], par[18],
               par[19], par[20], par[21], par[22], par[23], par[24], par[25], 
               par[26], par[27], par[28], par[29]);
         }

         return null;
      }

      //Parameters builder
      internal object[] GetVBAParameters(params object[] parameters)
      {
         //Initializing optional parameters
         //Application.Run takes 30 parameters
         object[] par = new object[30];
         for (int i = 0; i <= 29; i++)
         {
            if (parameters!=null && parameters.Length - 1 >= i)
            {
               par[i] = parameters[i];
            }
            else
            {
               par[i] = mv;
            }
         }
         return par;
      }
   }

Application.Run は、30 個のパラメータを取ります。Microsoft Visual C# では省略可能なパラメータがサポートされませんが、それによって生じる不都合を GetVBAParameters を使用することで解決しました。これにより、任意の VBA サブタイプまたは関数を呼び出せるようになり、C# で 30 個の Missing.Values を入力することを心配する必要はなくなりました。

私たちはさらに、最終的にはすべてのレガシ VBA コードをマネージ コードに変換することを目標に、アプリケーションと VBA の間に本格的な抽象レイヤを含めることにしました。この抽象レイヤにより、アプリケーションに影響を及ぼすことなく、メソッドの実装を VBA からマネージ コードに変更できます。RobFunctionsWrapper は、この目的だけのために、メイン アプリケーション DLL とは別の場所に配置されています。次に例を示します。

   /// <summary>
   /// ROB-specific extended VBA call-through methods
   /// </summary>
   public class RobFunctionsWrapper : XlaFunctionsWrapper
   {
      public RobFunctionsWrapper(Excel.Application xlInstance, string robAddInName, 
         string robAddInPath):base(xlInstance, robAddInName, robAddInPath)
      {
      }

      //Using facade design pattern
      //Legacy code calls to be replaced by managed code without impact
      //on core application
      . . .
      public void ShowAboutForm()
      {
         ExecuteCall("ShowAboutForm");
      }

      public void OpenHomepage()
      {
         ExecuteCall("OpenHomepage");
      }

      public void MakeLogEntry(string logEntry)
      {
         ExecuteCall("MakeLogEntry", "[Managed Code]: " + logEntry);
      }

      //Global variables in VBA handlers
      public object GetGlobalVarValue(string varName)
      {
         return ExecuteGetValue("GetGlobalVarValue", varName);
      }

      public void SetGlobalVarValue(string varName, object varValue)
      {
         ExecuteCall("SetGlobalVarValue", varName, varValue);
      }

      . . .
      //Explicit unmanaged resources clean up to prevent password 
      //prompt on Excel session shutdown
      ~RobFunctionsWrapper()
      {
         if (xla != null)
         {
            try
            {
               xla.Close(false, mv, mv);
            }
            catch
            {
               throw; //TODO: add proper error handler here
            }

            finally
            {
               System.Runtime.InteropServices.Marshal.ReleaseComObject(xla);
               xla = null;
            }
         }
      }
   }

このコード例では、デストラクタが意図的に示されています。マネージ COM アドインまたは Visual Studio 2005 Tools for Office Second Edition アプリケーションで VBA を使用する場合は、すべての COM リソースを確実に解放する必要があります。

アドインを読み込むときに、VBA コール スルー オブジェクトがどのようにインスタンス化されるかを示します。

   public partial class Connect : Object, Extensibility.IDTExtensibility2,
      ICustomTaskPaneConsumer, IRibbonExtensibility
   {
      internal Excel.Application excelApp = null;
      //application-specific VBA call wrapper/facade
      internal RobFunctionsWrapper appFuncCaller = null;
      
      Public void OnConnection(object application,
         Extensibility.ext_ConnectMode connectMode, 
         object addInInst, ref System.Array custom)
      {
         . . .
         excelApp = (Excel.Application)application;
         appFuncCaller=
          new RobFunctionsWrapper(excelApp, "ROB.XLA", userAppsPath + "\\ROB.XLA");
         . . .
      }

      . . .
      public void OnBeginShutdown(ref System.Array custom)
      {
         //releasing COM resources
         //IMPACT: If COM objects are not properly released, get 
         //password prompts
         //implemented by using destructor in the respective class. 
         //Unavoidable coupling
         //to ensure proper resources clean up
         appFuncCaller = null;
         
         GC.Collect();
         GC.WaitForPendingFinalizers();
         GC.Collect();
         GC.WaitForPendingFinalizers();
      }
         }

appFuncCaller により、マネージ コードが RobFunctionsWrapper にラップされた VBA コードを実行できるようになります。

まとめ

この記事は、私たちが 2007 Microsoft Office system のコーディングに関する究極のベスト プラクティスを発見したと主張するために書かれたものではなく、私たちのアイデアを、世界中の Office 開発者コミュニティに紹介することを目的としています。開発者コミュニティのおかげで、私たちの仕事がエキサイティングになるだけでなく、一般的な問題に対する新しい創造的なソリューションを求めて努力を続けることができます。この記事では、最も興味深いコンセプトのうちのほんのいくつかを紹介しているにすぎません。カスタム作業ウィンドウのいくつかのインスタンスを追跡するための汎用ディクショナリの使用、ワークシートに埋め込まれた Microsoft ActiveX コントロールにイベント ハンドラを動的に追加/削除するためのジェネリックの使用など、その他の可能性については言及しませんでした。これらの方法についての情報と、その他の所見およびコード例については、「追加情報」を参照してください。

追加情報

詳細については、以下のリソースを参照してください。