Search This Blog

Monday, May 7, 2012

Developing a COM Class Collection using VB.NET COM Template

You can find the code here

In this post I will show you how to develop a COM Class Collection in VB.NET using the COM Template.
It is actually very easy, much easier that doing it manually. Here you can see the manual procedure.

The VB.NET ComClassAttribute, used by the COM Class template will generate for you automatically all the interface that you need to be exposed to COM.


If you define a Default indexed property, it will make it the default property for the COM Object, i.e. it will associate a DispId(0) to the Default indexed property.

In addition if you define a function

Function GetEnumerator() as System.Collection.IEnumerator
End Function

it will mark it as DispId(-4) to make it usable for the VB6/VBA For Each ... Next loop.

The COM Add-in will also create for you all the necessary GUID.

As you create a COM Class using the template, the template automatically will tick for you
1) Register for COM interop in Project Property/Compile/Register for COM Interop
2) It will make the assembly COM Visible. It will tick Project Property/Application/Assembly Infomatin/Make Assemby COM Visible.

The second part is usually a bad idea, this is because every type you include in the libray will be exported to COM. In case you do not provide some GUID for the type, each time you build the assembly the project will create some new ones for you, thus creating a dll hell.

The best thing you can do is to Untick Make Assembly COM Visible (and do it every time you use the COM template) and add a attribute ComVisible(true) on top of the class.
See an exampe here



Imports System.Collections
Imports System.Runtime.InteropServices



<ComClass(Employees.ClassId, Employees.InterfaceId, Employees.EventsId)> _
Public Class Employees
    Implements System.Collections.IEnumerable

#Region "COM GUIDs"
    ' These  GUIDs provide the COM identity for this class 
    ' and its COM interfaces. If you change them, existing 
    ' clients will no longer be able to access the class.
    Public Const ClassId As String = "4999e186-4ea8-4ce1-8da4-12db6f8600e8"
    Public Const InterfaceId As String = "43ecbe2f-714b-4dc9-a76c-85a84320b66d"
    Public Const EventsId As String = "069d7776-4953-44c2-bd17-0ff75cb5748b"#End Region

    ' A creatable COM class must have a Public Sub New() 
    ' with no parameters, otherwise, the class will not be 
    ' registered in the COM registry and cannot be created 
    ' via CreateObject.
    Dim _SortedList As SortedList
    Public Sub New()
        MyBase.New()
        _SortedList = New SortedList
    End Sub

    Default Public Property Item(ByVal key As Object)
        Get
            Return _SortedList(key)
        End Get
        Set(ByVal value)
            _SortedList(key) = value

        End Set
    End Property

    Public ReadOnly Property Count()
        Get
            Return _SortedList.Count
        End Get
    End Property


    Public Sub Remove(ByVal key As Object)
        _SortedList.Remove(key)
    End Sub


    Public Sub Add(ByVal key As Object, ByVal value As Object)
        _SortedList.Add(key, value)
    End Sub


    Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
        'Return _SortedList.GetEnumerator()
        Dim keys As ICollection = _SortedList.Keys
        Return CType(keys.GetEnumerator, IEnumerator)
    End FunctionEnd Class

Developing a COM Class Collection in VB.NET without using the COM Template

you can find the code here

This is an example of a COM Class Collection written in VB.NET.

You need to start a new project of type library, and set the project property Build -> Register for COM Interop
Do not check: Application, Assembly Information, Make Class COM Visible.
We are using the COMVisible attribute to decide which class is visible for us
The class will have both a default property  and an iterator. The iterator is exposed defining a public function.

Function GetEnumerator() as IEnumerator
End Function


It is also a Good Idea having the class to implement IEnumerable

Function GetEnumerator() as IEnumerator Implements IEnumerable.GetEnumerator

End Function

In order to get the new GUID use can either use the VB.NET COM template of the Tools- Create GUID tool.
In addtion you can also use my C# Com template to start with, and translate the code with a C# to VB.NET tool.
Other wise, you can just use the VB.NET COM tool. It is kind of easy to use and much faster


Imports System.Runtime.InteropServices
Imports System.Collections

'Wee first define the interface of the Collection
<Guid("8beb176f-5357-4bb9-a5c1-38bdd0f7d3df"), _
InterfaceType(ComInterfaceType.InterfaceIsDual), _
ComVisible(True)> _
Public Interface INewEmployees
    Inherits System.Collections.IEnumerable


    <DispId(-4)> Shadows Function GetEnumerator() As IEnumerator 'Iterator
    <DispId(1)> Sub Add(ByVal key As Object, ByVal value As Object)
    <DispId(2)> ReadOnly Property Count()
    <DispId(3)> Sub Remove(ByVal key As Object)
    <DispId(0)> Default Property Item(ByVal key As Object)

End Interface

'We define the event interface
<Guid("e96bda2f-596f-419b-840c-4bd165930c4d"), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch), _
ComVisible(True)> _
Public Interface INewEmployeesEvents

End Interface



'<ComClass(NewEmployees.ClassId, NewEmployees.InterfaceId, NewEmployees.EventsId)> _
<Guid("67d85fea-43d6-457e-8db1-cc9601bdd9ec"), _
ClassInterface(ClassInterfaceType.None), _
ComSourceInterfaces(GetType(INewEmployeesEvents)), _
ComDefaultInterface(GetType(INewEmployees)), _
ComVisible(True)> _
Public Class NewEmployees
    Implements INewEmployees


#Region "COM GUIDs"
    ' These  GUIDs provide the COM identity for this class 
    ' and its COM interfaces. If you change them, existing 
    ' clients will no longer be able to access the class.
    Public Const ClassId As String = "67d85fea-43d6-457e-8db1-cc9601bdd9ec"
    Public Const InterfaceId As String = "8beb176f-5357-4bb9-a5c1-38bdd0f7d3df"
    Public Const EventsId As String = "e96bda2f-596f-419b-840c-4bd165930c4d"
#End Region

    ' A creatable COM class must have a Public Sub New() 
    ' with no parameters, otherwise, the class will not be 
    ' registered in the COM registry and cannot be created 
    ' via CreateObject.
    Dim _SortedList As SortedList
    Public Sub New()
        MyBase.New()
        _SortedList = New SortedList
    End Sub



    Default Public Property Item(ByVal key As Object) Implements INewEmployees.Item
        Get
            Return _SortedList(key)
        End Get
        Set(ByVal value)
            _SortedList(key) = value

        End Set
    End Property

    Public ReadOnly Property Count() Implements INewEmployees.Count
        Get
            Return _SortedList.Count
        End Get
    End Property


    Public Sub Remove(ByVal key As Object) Implements INewEmployees.Remove
        _SortedList.Remove(key)
    End Sub


    Public Sub Add(ByVal key As Object, ByVal value As Object) Implements INewEmployees.Add
        _SortedList.Add(key, value)
    End Sub


    Public Function GetEnumerator() As System.Collections.IEnumerator Implements INewEmployees.GetEnumerator, System.Collections.IEnumerable.GetEnumerator
        'Return _SortedList.GetEnumerator()
        Dim keys As ICollection = _SortedList.Keys
        Return CType(keys.GetEnumerator, IEnumerator)
    End Function


End Class


COM interop an interesting link


Here you can find an interesting link about COM interop

http://limbioliong.wordpress.com/2011/10/28/exposing-an-enumerator-from-managed-code-to-com/

This is one of his most important tips

The interface type should be either :

ComInterfaceType.InterfaceIsDual
or ComInterfaceType.InterfaceIsIDispatch
otherwise the GetEnumerator() method
will not be marked with dispid -4.
Without this dispid, this method
will not be recognized by COM as
returning an enumerator. It will not thus
not be usable in a VB6.0 For Each Next
statement.

Example of a COM Class Collection Written in C#

This is an example of a COM Class Collection written in C#.
You can use as starting point my template or use the Tools - Create Guid Tool on VS 2008.
You need to start a new project of type library, and set the project property Build -> Register for COM Interop
Do not check: Application, Assembly Information, Make Class COM Visible.
We are using the COMVisible attribute to decide which class is visible for us
The class will have both a defaul property (the indexers) and an iterator




using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections;

namespace TestEmployeesCol
{
    //Wee first define the interface of the Collection
    [Guid("21C027E8-CF8C-4166-A63B-25D8E790F040"), InterfaceType(ComInterfaceType.InterfaceIsDual), ComVisible(true)]
    public interface INewEmployees : System.Collections.IEnumerable
    {


        [DispId(-4)]
        new IEnumerator GetEnumerator();
        //Iterator
        [DispId(1)]
        void Add(object key, object value);
        [DispId(2)]
        object Count { get; }
        [DispId(3)]
        void Remove(object key);
        [DispId(0)]
        object this[object key] { get; set; }
    }

    //We define the event interface
    [Guid("5C6B8153-D2D6-4e98-80EF-D13A53CC9CDD"), InterfaceType(ComInterfaceType.InterfaceIsIDispatch), ComVisible(true)]
    public interface INewEmployeesEvents
    {

    }



    //<ComClass(NewEmployees.ClassId, NewEmployees.InterfaceId, NewEmployees.EventsId)> _
    [Guid("1692DD4D-6F3E-4e77-AB50-5401F04306DC"), 
    ClassInterface(ClassInterfaceType.None),
    ComSourceInterfaces(typeof(INewEmployeesEvents)), 
    ComDefaultInterface(typeof(INewEmployees)), 
    ComVisible(true)]
    public class NewEmployees : INewEmployees
    {


        #region "COM GUIDs"
        // These  GUIDs provide the COM identity for this class 
        // and its COM interfaces. If you change them, existing 
        // clients will no longer be able to access the class.
        public const string ClassId = "1692DD4D-6F3E-4e77-AB50-5401F04306DC";
        public const string InterfaceId = "21C027E8-CF8C-4166-A63B-25D8E790F040";
        #endregion
        public const string EventsId = "5C6B8153-D2D6-4e98-80EF-D13A53CC9CDD";

        // A creatable COM class must have a Public Sub New() 
        // with no parameters, otherwise, the class will not be 
        // registered in the COM registry and cannot be created 
        // via CreateObject.
        SortedList _SortedList;
        public NewEmployees()
            : base()
        {
            _SortedList = new SortedList();
        }



        public object this[object key]
        {
            get { return _SortedList[key]; }

            set { _SortedList[key] = value; }
        }

        public object Count
        {
            get { return _SortedList.Count; }
        }


        public void Remove(object key)
        {
            _SortedList.Remove(key);
        }


        public void Add(object key, object value)
        {
            _SortedList.Add(key, value);
        }


        public System.Collections.IEnumerator GetEnumerator()
        {
            ICollection keys = _SortedList.Keys;
            return (IEnumerator)keys.GetEnumerator();
        }


    }



}

Saturday, May 5, 2012

Visual Studio 2008 Image library



The image library should be at "C:\Program Files\Microsoft Visual Studio 9.0\Common7\VS2008ImageLibrary\1033\VS2008ImageLibrary.zip" and in your installation process you must ensure that you check the option "Tools for redistributing Applications\Graphics Library" (See the attached image)



Friday, April 27, 2012

Interface Implementation in VBA and VB6

WARNING THIS CODE HAS A MEMORY LEAK: check UPDATE HERE

I have realized that setting Set mInstruments = Me in the Class inizialize method create a circular reference and so a memory leak. The new code is much better.

The Keyworkd Implements in VBA/VB6 allows you to Interface Implementation in VB6, which in turns allows for Polymorfism. This is indeed a great capabilites on VB6 language that is often overlooked.
While the VB6 version of interface implementation is not so elegant as the one from .NET environment, it still allows you to implement some nice pattern like the strategy or the abstract facotory one.
I will show you a very easy example on how to use it in VBA."
The Idea is to create an interface calle IInstrumets with just one property "Id", and have a Security class that implements its interface. You could also have a Fund, Porftolio or a Security Class that implements its interface. This wil allow for polymorfism.

Dim inst as IInstrument
Dim sec as Security
Dim fn as Fund

Set sec = new Security
Set fn = new Fund

Set inst = sec
Set inst = fn

As you can see, bot a security and a fund can be assigned to an Instruments object!
We first define a Class called IInstrument. The code is here

'This is an Interface for the Generic Financial Instrument

Public Property Get Id() As String
'Only Signature
End Property

Public Property Let Id(strId As String)
'Only Signature
End Property


We now create a new Class called Security that Implements the IInstrument one. This is a bit more tricky.
Once we implement an Interface, the Class that implements it in VB6 will declare those method as Private like that.

Private Property Get IInstrument_Id() As String
   IInstrument_Id = mId
End Property

This is a kind of unsual behaviour, because if I know that the Security class implements the IInstrument interface, I expect to have access to the same methods and property made available by the interface obejcts. For this reason, I usually code the Class using this rules
1) In the Class that implements the interface I declare at Class level a private object of the interface type

      Private mInstrument As IInstrument



2) In the constructor I assing the Obj to this instance. This will allow me to call the interface methods of my class

  Private Sub Class_Initialize()
    'I need to Access the IInstruments Methods
    Set mInstrument = Me
End Sub

3) I implement the interface methods as I normally do.

Private Property Get IInstrument_Id() As String

    IInstrument_Id = mId  
End Property

Private Property Let IInstrument_Id(strId As String)
   mId = strId
End Property

4) I crate public properties / methods mirroring the interface delegating their implementation to the
    mInstrument object
Public Property Get Id() As String

   Id = mInstrument.Id
End Property

Public Property Let Id(strId As String)
   mInstrument.Id = strId
End Property

Here you can find the Security Class Code


Implements IInstrument

Private mId As String
Private mInstrument As IInstrument

Public Ticker As String

Private Sub Class_Initialize()
  'I need to Access the IInstruments Methods
  Set mInstrument = Me
End Sub

Private Property Get IInstrument_Id() As String
       IInstrument_Id = mId
End Property


Private Property Let IInstrument_Id(strId As String)
  mId = strId
End Property


'Public Interface

Public Property Get Id() As String
     Id = mInstrument.Id
End Property

Public Property Let Id(strId As String)
  mInstrument.Id = strId
End Property

We can now test the code

Sub TestSecurity()
 Dim Sec1 As Security
 Dim Inst As IInstrument
 Dim Sec2 As Security
 Set Sec1 = New Security
 
 Sec1.Id = 10
 Sec1.Ticker = "MXEU"
 
 Set Inst = Sec1 'Upcast: A Security in an Instruments
 Debug.Print Inst.Id
 
 'DownCast, this should have been done explicit, but VBA does not support CType.
 'VB6 does. So instead of CType(Inst, "Security") we can do
 If TypeName(Inst) = "Security" Then
    Set Sec2 = Inst
    End If
 
 
 Set Sec2 = Inst

 Debug.Print Sec2.Id
 Debug.Print Sec2.Ticker
End Sub

VB6 and VBA Enumerations

An Enumeration in VBA/VB6 is a special type of long Variable.
A great resource can be found here on cpearson.com

Enum Position
  [_First] = -1
  RelativeDynamic = 0
  RelativeStatic = 1
  Absolute = 2
  [_Last] = 3
End Enum


Enum FactorEngine
  [_First] = -1
  AA = 0
  [_Last] = 1
End Enum

  
The code above specify two kind of Enumerations, Position and FactorEngine.
The [_First] and [_Last] are not necessary but they can be used to validate the Enumerated variables.
The _ makes the Enumrated variable hidden to the intellisense, while the [ makes it a valid character for the VB6 interpreter.
This is an example on how to Validate Enumerations


Sub TestEnum()
Dim Fac As FactorEngine
Dim i As Long
Dim IsValid As Boolean

Fac = AA
IsValid = False

For i = FactorEngine.[_First] To FactorEngine.[_Last]

  If Fac = i Then
      IsValid = True
      Exit For
  End If
Next i

If IsValid = True Then
   Debug.Print Fac & " Is a valid Engine"
Else
   Debug.Print Fac & " Is NOT a valid Engine"
End If


End Sub

Thursday, April 26, 2012

UML Association, dependency, composition and aggregation


These are a couple of forum post I found that goes straight to the point (I think)

An association almost always implies that one object has the other object as a field/property/attribute (terminology differs). A dependency typically (but not always) implies that an object accepts another object as a method parameter, instantiates, or uses another object. A dependency is very much implied by an association.

A dependency is very general and lowering complexity is about diminishing dependencies as much as possible. An association is a strong (static) dependency. Aggregation and Composition are even stronger


Dependency is like when you define a method that takes a String(in Java, C#, as string is a object in them) as a parameter, then your class is dependent on String class.

Association is like when you declare a string as an attribute in your class. then your code is associated with the string class.
String name = null //: is a association.


Aggregation is a kind of association that specifies a whole/part relationship between the aggregate (whole) and component part. This relationship between the aggregate and component is a weak “has a” relationship as the component may survive the aggregate object. The component object may be accessed through other objects without going through the aggregate object. The aggregate object does not take part in the lifecycle of the component object, meaning the component object may outlive the aggregate object. The state of the component object still forms part of the aggregate object.




An example of aggregation is a History-Class object contains zero or more of Student objects. The state of each Student object has an influence on the state of the History-Class object. If the History-Class object is destroyed, the Student objects may continue to exist.

Composition is a kind of association very similar to aggregation except where the composite object has sole responsibility for the disposition of the component parts. The relationship between the composite and the component is a strong “has a” relationship, as the composite object takes ownership of the component. This means the composite is responsible for the creation and destruction of the component parts. An object may only be part of one composite. If the composite object is destroyed, all the component parts must be destroyed, or the reference and responsibility of the component part must be handed over to another object. Composition enforces encapsulation as the component parts usually are members of the composite object.




An example of composition is a House object contains zero or more Room objects. The state of each Room object has an influence on the House object. If the House object is destroyed, the Room objects will also be destroyed.



To to me is kind of like

Compostion -> Aggegation -> Association -> Dependency




How to decouple the business layer and the DAL suing the provider model pattern

A way to make the DAL layer indipendent of the BLL is as follow
The Layer between the Business and Data Layer will be made of Abstact Classes (instead of interfaces)
The Idea is that each abstract class has a public static get property "Instance" that use reflection to create a concrete class. The concrete class to be created is defined in a configuration file
The code call will look like this

IRSBodyProvider irsBodyProvider = IRSBodyProvider.Instance

Or if we create a helper class DataProvider we will have

IRSBodyProvider irsBodyProvider = DataProvider.IRSBody

---------------------------
Code
---------------------------

public Abstract IRSBodyProvider
{

   static private ArticlesProvider _instance = null;
   ///

   /// Returns an instance of the provider type specified in the config file
   ///


    static public ArticlesProvider Instance
   {
        get
        {
             if (_instance == null)
            _instance = (ArticlesProvider)Activator.CreateInstance(
                     Type.GetType(Globals.Settings.Articles.ProviderType));
            return _instance;
         }
   }
}

static public class DataProvider
{
    public static IRSBodyProvider IRSBody
   {
       get { return IRSBodyProvider.Instance; }
   }
}


Another way to make the DAL Layer indipendent of the BLL is
1) Create a Layer between the DAL and BLL with Provider Interfaces
     IIRSBodyProvider (with the CRUD Method signature)
     ITRSBodyProvider (with the CRUD Method signature)

2) Then we add in the same Layer an abstract factory "ProviderFactory" that creates/gets the provider inferfaces

IIRSBodyProvider IProviderFactory.GetIRSBodyProvider()
ITRSBodyProvider IProviderFactory.GetTRSBodyProvider()


3) Then we create in the DAL some concrete implementation of the IProviderFactory.

class SQLProvideFactory : IProviderFactory {

   IIRSBodyProvider IProviderFactory.GetIRSBodyProvider()
   ITRSBodyProvider IProviderFactory.GetTRSBodyProvider()
}

calss XMLProviderFactory : IProviderFactory {

   IIRSBodyProvider IProviderFactory.GetIRSBodyProvider()
   ITRSBodyProvider IProviderFactory.GetTRSBodyProvider()
}
Note how we are implementing the Provider Model Patter + Abstract Factory Patter to create decoupling from the Layers.
In Addition note how the two methods are the equivalent of the Instance method created in the first solution of the abstact class. They are the ones creating the concrete class. In this solution is the factory that is creating the class non the static method of the abstract class)

4) We then create a static class client of the factory called DataProvider that instanziate a concrete IProviderFactory using Reflection.

The call to the code will look like this

IIRSBodyProvider irsBodyProvider = DataProvider.IRSBody

We will use this kind of call in the retrive method of the IRSBody object.
The decoupling of the BOL and the DAL is complete

This is a Static Class. It works more or less like this

DataProvider.IRSBody.Retrieve(irsId, trsId)
DataProvider.TRSBody.Retrieve(trid, fundId)



-------------------------------------------------------------------------------------------------

PSEUDO CODE FOR THE CLASS

--------------------------------------------------------------------------------------------------

static Class DataProvider {
    //We provide a private member to hold and instance of the class
    static private IProviderFactory _factory = null;
    static DataProvider()
   {
     //NomeAssembly and SomeProviderFactory can be read from some configuration file
     string nome = "NomeAssembly";
     Assembly assembly = Assembly.Load(nome);
     _factory = (IDataProviderFactory)assembly.CreateInstance(nome+".SomeProviderFactory");
    }

  public static IIRSBody IRSBody()
  {
    // Here we assume that the factory does not create a new instance of the provide each time we call the
    // method get.
      return factory.GetIRSBodyProvider();
   }
}
 
 

Wednesday, April 25, 2012

How to Create a Strongly typed Collection in Vba

An Excel File with the sample code can be found here

In this blog post I will show you how to create a Strongly Type Collection Class wich has both an Item as default property and for wich we can use the For Each Loop.
First we create a very simple Class named Person with just three properties Name, Surname and Date of Birth

Option Explicit

Dim mName As String
Dim mSurname As String
Dim mDateofBirth As Date

Public Property Get Name() As String
  Name = mName
End Property
Public Property Let Name(strName As String)
   mName = strName
End Property
Public Property Get Surname() As String
   Surname = mSurname
End Property
Public Property Let Surname(strSurname As String)
   mSurname = strSurname
End Property
Public Property Get DateOfBirth() As Date
  DateOfBirth = mDateofBirth
End Property
Public Property Let DateOfBirth(dteDateofBirth As Date)
   mDateofBirth = dteDateofBirth
End Property 
 
  

Then we can create the Collection Class called People

Option Explicit

'This is going to be a stroingly type Collection

Private mCol As Collection

Private Sub Class_Initialize()
  Set mCol = New Collection
End Sub
Private Sub Class_Terminate()
  Set mCol = Nothing
End Sub
Property Get Item(Index As Variant) As Person
   'Attribute Item.VB_UserMemId = 0
   'This Attribute makes Item the default property
      
   Set Item = mCol.Item(Index)
End Property
Property Get NewEnum() As IUnknown
  'Attribute NewEnum.VB_UserMemId = -4
  'Attribute NewEnum.VB_MemberFlags = "40"
  
  'The first Attribute makes it the Default Enumerator Property
  'The second Attribute makes the Enumerator a hidden property. This does not work with the VBA intellisense
 
  
  'This Routine  Get the Enumerator for the Collection.
  'To get this to work you must add two attributes
  
  
  
  Set NewEnum = mCol.[_NewEnum]
End Property
Public Sub Add(Item As Person, Optional key As Variant)
  Call mCol.Add(Item, key)
End Sub
Public Function Count() As Long
   Count = mCol.Count
End Function
Public Sub Remove(Index As Variant)
   mCol.Remove (Index)
End Sub
There are few Attributes that you cannot see in the VBA IDE. If you export a file from one of your VB6 procedures and view it, you'll notice that some Attributes, not visible while editing your code, are added to the top of the routine(s). The two properties in question will look something like this:

Property Get Item(Index As Variant) As Parameter
       Attribute Item.VB_UserMemId = 0   

       Set Item = m_Collection.Item(Index)
End Property

Property Get NewEnum() As IUnknown

   Attribute NewEnum.VB_UserMemId = -4  
   Attribute NewEnum.VB_MemberFlags = "40"

  Set NewEnum = Me.mCollection.[_NewEnum]

End Property

Note that the Attribute directive must be just below the functions signatures, otherwise the code will not work.
Now the above all looks "normal" except for the addition of the three "Attribute" Lines.
In the Item Property the line "Attribute Item.VB_UserMemId = 0" makes it the default property.
In the NewEnum, the "Attribute NewEnum.VB_UserMemId = -4" makes it the Default Enumeration Property (I'm sure you recognize the "-4" part.)
The Attribute NewEnum.VB_MemberFlags = "40" is to make the Enumerator a Hidden property, but, technically, this is not recognized in VBA, so it will be visible in IntelliSense, but I don't find that a big deal.

The solution is to
(1) Make your Class
(2) SAVE
(3) Export the Class
(4) Remove the Class (steps 3 and 4 can be combined into one, as it asks you if you wish to "Export" when you right-click and choose "Remove")
 (5) Manually add the Attribute Lines as shown above
 (6) Re-Import the edited Class

 An easier way, as one of my reader pointed out is
1) To write the Attributes directives directly on the VBA Ide. You will get a syntax error. Ignore it
2)  Export the Class
3) Remove the Class
4) Reinport it again.
 The Attribute will be in the .cls file, but they will not be visible (you can add the Attribute NewEnum.VB_MemberFlags = "40" line if you wish -- it won't hurt anything -- but it won't be recognized in VBA, it will just be quietly ignored. So there's no reason to bother doing this, really.)
As you know, editing the code thereafter has some propensity to lose these properties (even in VB6) and so this may have to be repeated occassionally. (A bit of a pain.) The alternative is to create your class 100% within VB6 and then import it into your VBA Project. Or, even better, make it in VB6, debugg it, get it running 100%, compile to DLL and then add this DLL to your references. This last concept is probably the most solid, but there could be deployment issues as your DLL now has to be correctly Registered on the Client machine. Not that this is a big problem, but it's not as easy as distributing a VBA Project...

This is the Code you can use to test the Class
Option Explicit

Sub prova()

Dim Employee As Person
Dim Director As Person
Dim Team As People
Dim p As Person
Dim i As LongSet Employee = New Person
Employee.DateOfBirth = "10 Jan 1974"
Employee.Name = "Mario"
Employee.Surname = "Rossi"Set Director = New Person
Director.DateOfBirth = "10 Mar 1970"
Director.Name = "Giulia"
Director.Surname = "Verdi"Set Team = New People
Call Team.Add(Employee)
Call Team.Add(Director)

For i = 1 To Team.Count
 Debug.Print Team(i).Name
  
Next i

For Each p In Team
  Debug.Print p.Name
NextEnd Sub