.NET-Auflistung mit Enumerator für VB6 (COM) - was fehlt nur?

  • VB6
  • .NET (FX) 4.0

Es gibt 5 Antworten in diesem Thema. Der letzte Beitrag () ist von Arby.

    .NET-Auflistung mit Enumerator für VB6 (COM) - was fehlt nur?

    Eigentlich dachte ich, ich hätte COM im Griff, aber irgendwie steh ich grad wie der Ochs vorm Berg.

    Ich möchte in .NET eine Bibliothek erstellen, die auch über COM z.B. in Visual Basic 6 nutzbar ist. Eine der darin enthaltenen Klassen soll eine Auflistung darstellen, die zum einen die Eigenschaften Count und Item(index) haben und in VB6 (auch) mit For Each durchlaufen werden können soll:

    C#-Quellcode

    1. public interface IPartCollection : IEnumerable, IEnumerator
    2. {
    3. //[DispId(-4)]
    4. //IEnumerator _newEnum();
    5. [DispId(1)]
    6. int Count { get; }
    7. [DispId(2)]
    8. Part this[int index] { get; }
    9. }
    10. [ClassInterface(ClassInterfaceType.None)]
    11. public class PartCollection : IPartCollection
    12. {
    13. private List<Part> _partList = new List<Part>();
    14. public PartCollection()
    15. {
    16. // ...
    17. }
    18. public int Count
    19. {
    20. get { return _partList.Count; }
    21. }
    22. public Part this[int index]
    23. {
    24. get { return _partList[index]; }
    25. }
    26. public IEnumerator GetEnumerator()
    27. {
    28. return _partList.GetEnumerator();
    29. }
    30. private IEnumerator _enum;
    31. public object Current
    32. {
    33. get { return _enum.Current; }
    34. }
    35. public bool MoveNext()
    36. {
    37. return _enum.MoveNext();
    38. }
    39. public void Reset()
    40. {
    41. _enum = GetEnumerator();
    42. }
    43. }


    Ein For-Each in .NET ist dabei kein Problem, in VB sähe die Verwendung (eigentlich) so aus:

    Visual Basic-Quellcode

    1. Dim Liste As PartCollection
    2. Set Liste = New PartCollection
    3. Dim obj As Object
    4. For Each obj in Liste
    5. '...
    6. Next

    Leider gibt es dann in der For-Each-Zeile den Laufzeitfehler 438 "Objekt unterstützt diese Eigenschaft oder Methode nicht".

    Wie man am auskommentierten Teil im Interface erkennen kann, habe ich es auch schon damit probiert, die Interface-Methode _newEnum() zu implementieren, das änderte aber nichts.
    Ich steh grad tierisch auf dem Schlauch, weil ich das Gefühl habe, dass die Lösung sau-einfach ist, ich nur einfach grad nicht drauf komme und wohl auch die falschen Suchbegriffe verwende um die Lösung im Netz zu finden.

    Langer Rede kurze Frage: Was fehlt, um auch in VB6 ein For-Each mit dieser Klasse zu ermöglichen?
    Weltherrschaft erlangen: 1%
    Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
    Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
    Danke.
    Damit VB6 For Each auf einer Auflistung unterstützt, muss diese natürlich entsprechend "vorbereitet" sein (teilweise an meinem obigen Ansatz mit _NewEnum und der besonderen Dispatch-ID "-4" zu erkennen), was aber - wie man ebenfalls an diesem Thread erkennen kann - offenbar nicht ganz so trivial ist wie ich bisher dachte.

    Zumindest in VB6 habe ich damals problemlos For-Each-fähige Auflistungs-Klassen erstellen können. Aber schon in C++ erstellte COM-Objekte hatten zum Teil diese Unterstützung nicht, weil den meisten C++-Entwicklern die VB-ler (und VBA-ler) am A... vorbei gingen.
    Weltherrschaft erlangen: 1%
    Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
    Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
    Danke.
    Während ihr wohl noch rätselt, was ich eigentlich will oder wie die Lösung lautet, habe ich in der Zwischenzeit selbst ein paar Sachen herausgefunden. Und gleichzeitig neue Fragen aufgeworfen.

    Es ist mir nach wie vor ein Rätsel, warum VB6 die Methode namens _NewEnum() (auch nach korrigierter Groß/Kleinschreibung) mit der DispId -4 (DISPID_NEWENUM) nicht schlucken will. Unter VB6 selbst oder auch C++ war das immer die korrekte Vorgehensweise um OLE-Collections zu implementieren. Unter .NET funkt offenbar das Framework oder der Compiler dazwischen, der scheinbar meint, schlauer zu sein und einfach mal ein paar Dinge anders angeht.

    Meine erste "lauffähige" Version obigen Codes kommt ohne die Ableitung des IEnumerator-Interfaces aus und als entscheidende Änderung habe ich die Methode _NewEnum() im IPartCollection-Interface nach GetEnumerator() umbenannt.

    C#-Quellcode

    1. public interface IPartCollection : IEnumerable
    2. {
    3. [DispId(-4)]
    4. IEnumerator GetEnumerator(); // <-- besser mit "new" davor
    5. [DispId(1)]
    6. int Count { get; }
    7. [DispId(0)]
    8. Part this[int index] { get; }
    9. }
    10. [ClassInterface(ClassInterfaceType.None)]
    11. public class PartCollection : IPartCollection
    12. {
    13. private List<Part> _partList = new List<Part>();
    14. public PartCollection()
    15. {
    16. // ...
    17. }
    18. public int Count
    19. {
    20. get { return _partList.Count; }
    21. }
    22. public Part this[int index]
    23. {
    24. get { return _partList[index]; }
    25. }
    26. public IEnumerator GetEnumerator()
    27. {
    28. return _partList.GetEnumerator();
    29. }
    30. }


    Damit funktioniert das For-Each sowohl in VB6 als auch in .NET und ich hab meine gewünschten Eigenschaften Item und Count.
    Kleiner Schönheitsfehler: VS2010 gibt nun eine Warnung aus: "IPartCollection.GetEnumerator()" blendet den vererbten Member "System.Collections.IEnumerable.GetEnumerator()" aus. Verwenden Sie das new-Schlüsselwort, wenn das Ausblenden vorgesehen war.

    Okay, dann halt ein "new" davor. Klappt wunderbarerweise genauso, ist ja letztendlich wohl auch dasselbe Ergebnis, nur dass der Compiler anhand des "new" nun weiß, dass ich mit voller Absicht das GetEnumerator() aus IEnumerable mit meiner eigenen Schnittstelle überschreiben will.

    Aber will ich das? Es ist doch das gleiche, also kann ich doch auch die Definition aus dem Original-IEnumerable nehmen. Oder?
    Lass ich also die (zusätzliche) GetEnumerator() Methode aus dem Interface raus, durch die implizite Implementierung von GetEnumerator() muss ich auch sonst nix am Code ändern, er implementiert damit quasi automatisch das "Original" aus IEnumerable. Macht er auch, nur nicht in COM. Denn in COM hat das Objekt nur das IPartCollection-Interface. Vererbung der Definitionen aus IEnumerable findet nicht statt. Also falsch abgebogen, wieder einen Schritt zurück.

    Dann dachte ich mir: Wenn ich die Methode GetEnumerator() aus IEnumerable eh mit meiner eigenen Signatur (die aber identisch ist) übertünche, wozu dann überhaupt IEnumerable implementieren? Also entfernte ich die Ableitung von IEnumerable testweise.

    Um es kurz zu machen: Auch dann ging noch alles:

    C#-Quellcode

    1. public interface IPartCollection
    2. {
    3. [DispId(-4)]
    4. IEnumerator GetEnumerator();
    5. [DispId(1)]
    6. int Count { get; }
    7. [DispId(0)]
    8. Part this[int index] { get; }
    9. }

    Selbst .NET scheint es nicht zu stören, dass die PartCollection-Klasse ja nun gar kein IEnumerable mehr implementiert. Sobald ich aber den Namen von GetEnumerator() auf was anderes, z.B. _NewEnum() ändere, dann meckert .NET. Und VB6 will auch kein For-Each mehr ausführen - obwohl der Rückgabewert identisch ist.

    Das kommt mir ehrlich gesagt sehr mysteriös vor. So als würde Visual Studio allein anhand des Namens einer Funktion plötzlich Klassen-fremde Interfaces automatisch mit implementieren, nur weil "zufällig" Signatur und Funktionsname übereinstimmen. Weiß nicht, ob mir das gefällt...

    Wie auch immer, für mich ist das Thema vorerst abgehakt. Ein Schönheitsfehler jedoch bleibt: Während bei COM-Klassen Funktionen, die mit "_" beginnen in VB6 (und auch in VB.NET), automatisch im Objectbrowser ausgeblendet/versteckt werden, habe ich mit der jetzigen Lösung eine fette "unsaubere" GetEnumerator()-Funktion bei meiner PartCollection-Klasse im Objektkatalog, die sich auch von der Kennzeichnung mit Dispatch-ID "-4" nicht beeindrucken lässt.

    Übrigens:

    Es hat übrigens auch funktioniert, als ich das Interface komplett weggelassen und nur die PartCollection-Klasse mit den drei o.g. Funktionen/Eigenschaften implementiert habe. Natürlich musste dazu das ClassInterface-Attribut auf "AutoDual" gestellt werden, was aber weitere/andere Nachteile mit sich bringt: zum einen habe ich dann zusätzlich noch die ganzen von Object geerbten Methoden (ToString, GetHashCode, Equals) im COM-Interface, zum anderen handelt man sich damit massive Kompatibilitätsprobleme ein, wenn man mal Änderungen an der Klasse vornimmt, durch die sich die (dann automatisch erzeugten) Dispatch-IDs verschieben bzw. ändern.
    Weltherrschaft erlangen: 1%
    Ist dein Problem erledigt? -> Dann markiere das Thema bitte entsprechend.
    Waren Beiträge dieser Diskussion dabei hilfreich? -> Dann klick dort jeweils auf den Hilfreich-Button.
    Danke.