2009-11-13 3 views

Répondre

3

Si vous avez vraiment besoin d'un accès à une instance de la classe, vous pouvez effectuer les opérations suivantes:

  • Générer une bibliothèque de type pour une interface COM que vous souhaitez exposer votre classe de VBA (par exemple IMyComInterface)

  • Ajouter un référe e à cette bibliothèque de type dans votre projet VBA

  • Implémentez l'interface dans votre module de classe VBA - par ex. MyVbaClass (utilisez le mot-clé Met en œuvre):

    Option Explicit 
    Implements IMyComInterface 
    
    Private Sub IMyComInterface_SomeMethod(...) 
        ... 
    End Sub 
    ... 
    
  • référence à la même bibliothèque de type dans votre projet C#

  • Créer un ComVisible C# classe avec une méthode qui accepte une référence à l'instance d'interface VBA.Quelque chose comme:

    public class MyVbaLoader 
    { 
        public IMyComInterface MyComInterface 
        { 
         get { return myComInterface; } 
         set { myComInterface = value; } 
        } 
    } 
    
  • écrire une méthode « usine » dans un module standard VBA, qui prend un objet en tant que paramètre ByRef. Cet objet doit supposer que l'objet passé en argument a une propriété "MyComInterface" et doit définir cette propriété sur une nouvelle instance de la classe VBA MyClass.

    Public Sub MyFactoryMethod(MyVbaLoader As Object) 
        Dim objClass As MyVbaClass 
        Set objClass = New MyVbaClass 
        ... any initialization of objClass here ... 
    
        ' Pass a reference to the VBA class to the C# object MyVbaLoader 
        MyVbaLoader.MyComInterface = objClass 
    End Sub 
    
  • Appelez la méthode usine à partir de votre code C#. En supposant que vous avez ouvert le classeur et un « classeur » refence dans votre code VBA, le code ressemblera à quelque chose comme:

    MyVbaLoader loader = new MyVbaLoader(); 
    workbook.Application.Run("MyModule.MyFactoryMethod", loader, Type.Missing, ... , Type.Missing); 
    // we now have a reference to the VBA class module in loader.MyComInterface 
    // ... 
    

Comme vous pouvez le voir, il est assez complexe. Sans plus de détails sur le problème que vous essayez de résoudre, il est difficile de dire si cette complexité est justifiée ou s'il existe une solution plus simple.

Si ce qui précède n'est pas clair, faites le moi savoir et je vais essayer de clarifier. Fondamentalement, vous ne pouvez pas retourner une valeur d'une macro VBA appelée à partir de votre code C# en utilisant Application.Run, vous devez donc passer à un objet par valeur qui a une méthode ou une propriété qui peut être appelée de VBA à définir l'instance.

+0

Bonne réponse, mais en fait vous pouvez retourner une valeur de la macro VBA - voir ma réponse pour plus de détails. Cela permettrait d'éviter le seul petit truc de votre solution autrement agréable. –

+0

Remarque: La classe VBA doit être marquée comme 'PublicNotCreatable' ou vous obtiendrez une exception liée à HRESULT 0x800A0062. – robodude666

2

Les modules de classe VBA n'ont que deux modes d'instanciation: privé et public-non-créable. Donc, vous ne pouvez même pas les instancier dans un autre projet VB (A), et encore moins depuis C#. Cependant, rien ne vous empêche d'avoir un module standard qui fait office d'usine de classe. Donc, si votre module de classe est Foo alors vous pouvez avoir une méthode dans un module standard appelé NewFoo qui instancie un nouveau Foo pour vous et le renvoie à l'appelant. L'objet Foo devrait évidemment être public-non-créable.

[Votre méthode NewFoo peut prendre des paramètres, vous pouvez simuler les constructeurs paramétrés, qui ne sont pas disponibles dans VBA.]

EDIT: détails sur la façon d'appeler la fonction VBA (dans un module standard) de C# et obtenez la valeur de retour en utilisant Application.Run.

private static object RunMacro(Excel.Application excelApp, string macroName, object[] parameters) 
{ 
    Type applicationType = excelApp.GetType(); 

    ArrayList arguments = new ArrayList(); 

    arguments.Add(macroName); 

    if (parameters != null) 
     arguments.AddRange(parameters); 

    try 
    { 
     return applicationType.InvokeMember("Run", BindingFlags.Default | BindingFlags.InvokeMethod, null, excelApp, arguments.ToArray()); 
    } 
    catch (TargetInvocationException ex) 
    { 
     COMException comException = ex.InnerException as COMException; 

     if (comException != null) 
     { 
      // These errors are raised by Excel if the macro does not exist 

      if ( (comException.ErrorCode == -2146827284) 
       || (comException.ErrorCode == 1004)) 
       throw new ApplicationException(string.Format("The macro '{0}' does not exist.", macroName), ex); 
     } 

     throw ex; 
    } 
} 

Notez que vous pouvez omettre tout ce que ... essayer des trucs fourre - tout qu'il fait manipule l'erreur spécifique lorsque la macro n'existe pas et élever une exception plus significative dans ce cas.

Le object renvoyé par la fonction peut être converti en le type dont vous avez besoin. Par exemple:

object o = RunMacro(excelApp, "MyModule.MyFunc", new object[] { "param1", 2 }); 
if (o is string) 
{ 
    string s = (string) o; 
    Console.WriteLine(s); 
} 

En supposant que la fonction retourne en fait une instance de votre objet de classe VBA défini, vous pouvez appeler les méthodes de cet objet de la même manière, en utilisant à nouveau InvokeMember:

object o = RunMacro(excelApp, "MyModule.MyFunc", new object[] { "param1", 2 }); 

// assume o is an instance of your class, and that it has a method called Test that takes no arguments 
o.GetType().InvokeMember("Run", BindingFlags.Default | BindingFlags.InvokeMethod, null, o, new string[] {"Test"}); 

Si vous faites beaucoup de ces appels, alors évidemment vous pouvez cacher les détails laids en créant des méthodes wrapper pour faire l'appel pour vous.

S'il vous plaît noter que je l'ai tapé la plupart de cela de mémoire, donc je me attends qu'il y ait la syntaxe, les erreurs logiques et peut-être même fait :-)

+0

Mais vous n'avez toujours pas de bibliothèque de types pour générer le wrapper callable runtime (RCW) – Joe

+0

True, mais vous pouvez appeler les méthodes de la classe directement à l'aide de InvokeMember. Pas joli ou agréable, mais toujours possibe. S'il n'y a qu'un seul membre que vous voulez appeler, alors c'est plus facile que de faire tout le branchement requis dans votre réponse (franchement excellente). –

+0

+1, je n'étais pas au courant que vous pouviez appeler une fonction VBA depuis Application.Run et retourner un résultat. Je vais retravailler ma réponse pour en profiter. Cela laisse l'OP avec deux solutions - l'implémentation d'une interface d'une bibliothèque de type dans le code VBA de ma réponse, ou l'accès aux membres de classe VBA en utilisant la réflexion comme dans votre réponse. – Joe

Questions connexes