Results 1 to 9 of 9
  1. #1
    Join Date
    Aug 2010
    Posts
    57

    Unanswered: Class Module to share functionality between forms

    In my application (Access 2007) I have about 10 forms all of which have some functionality in common (i.e. turn on/off navigation buttons depending on edit/new mode, process "Save" and "Cancel" buttons, etc). Coming from the C++/C# world, I figured it was best to have a single class module that covers all the common code, rather than proliferate it across 10 forms.

    Are there any best practices for this?

    My initial reaction is that I could either:
    1. Create an object variable in the form's module. The object, as defined in the class module has a form reference (WithEvents). In the Open event, I pass Me to my object variable, and now my class' object knows what to do with the form
    2. Alternatively, I could have my class have an "OpenForm" function that would open the form, and then manipulate it as needed.


    Does the concept sound about right? Any other alternatives?

    In both cases I know I can capture form events if I declare the object that captures the form with the "WithEvents" keyword. However, how do I capture button events? Do I need to also declare references to the shared buttons in my class?

    Thanks!
    Carlos

  2. #2
    Join Date
    Mar 2009
    Posts
    5,442
    Provided Answers: 14
    Here's what I use (here for dealing with subforms containing command buttons):
    1. The Class module:
    Code:
    Option Compare Database
    Option Explicit
    
    '  Version 2: Use Event for Parent Callback
    '  ---------
    '
    Public Event CallBack(ByVal Sender As String, ByVal Message As String, ByVal Extra As Variant)
    
    Private m_colButtons As Collection
    
    ' = [STDC]{Begin} =====================================================================
    ' =
    ' =         ---------------------------------------
    ' =         *            Cls_Std_  Class          *
    ' =         ---------------------------------------
    ' =
    ' =         This section is common  to all Cls_Std classes.
    '
    Private Const c_strStd_ClassName As String = "Cls_Std_E_ChildButtons"
    Private Const c_strStd_ClassGUID As String = "{B04C77E8-BED4-4712-BEC5-420BE425CBB6}"
    Private Const c_strStd_ClassBuild As String = "201111007-2.2.7"
    Private m_clsIdentity As Cls_Std_Identity
    '
    ' = [DEEP]{Begin} =====================================================================
    ' =
    ' =         ---------------------------------------
    ' =         *  Dynamic External Event Procedures  *
    ' =         ---------------------------------------
    '
    Private WithEvents Form As Form
    '
    ' = [DEEP]{End} =======================================================================
    '
    
    Public Property Get ClassBuild() As String
    
        ClassBuild = c_strStd_ClassBuild
        
    End Property
    
    Public Property Get ClassGUID() As String
    
        ClassGUID = c_strStd_ClassGUID
        
    End Property
    
    Public Property Get ClassName() As String
    
        ClassName = c_strStd_ClassName
        
    End Property
    
    Public Property Get Identity() As String
    
        Identity = m_clsIdentity.Identity
    
    End Property
    '
    ' = [STDC]{End} =======================================================================
    '
    
    Private Sub Class_Initialize()
    
        Set m_clsIdentity = New Cls_Std_Identity
        
    End Sub
    
    Private Sub Class_Terminate()
    
        Set m_clsIdentity = Nothing
        
    End Sub
    
    '
    ' = [DEEP]{Begin} =====================================================================
    '
    Public Property Get FormGUID() As String
    
        FormGUID = Nz(Form.Controls("Text_Dummy").Tag, "")
        
    End Property
    
    Public Property Get FormName() As String
    
        FormName = Form.Name
        
    End Property
    
    Public Function FormObject() As Form
    
        Set FormObject = Form
        
    End Function
    
    Public Function DEEP_Attach(ByRef frm As Form)
    
        Set Form = frm
        Cls_Specific    ' Perform class-specific initialization tasks.
        If HaveParent = True Then Form.Parent.Hook Me
    
    End Function
    
    Private Function HaveParent() As Boolean
    
        Dim frm As Form
        
        HaveParent = True
        For Each frm In Application.Forms
            If frm.Name = Form.Name Then
                HaveParent = False
                Exit For
            End If
        Next
    
    End Function
    
    Public Property Get Tag() As String
    
        Tag = Nz(Form.Tag, "")
        
    End Property
    '
    ' = [DEEP]{End} =======================================================================
    '
    
    Private Function Cls_Specific()
    
    '   Cls_Specific() is mandatory for D.E.E.P.
    '   ----------------------------------------
    '   Performs class-specific initialization.
    '
        GatherControls
        
    End Function
    
    Private Function GatherControls()
    
        Dim ctl As Control
        
        Set m_colButtons = New Collection
        For Each ctl In Form.Controls
            If ctl.ControlType = acCommandButton Then
                ctl.OnClick = "=CallBack()"
                m_colButtons.Add ctl, CStr(ctl.HelpConTextID)
            End If
        Next
    
    End Function
    
    Public Function ParentCallBack(Optional ByVal Value As Variant = Null, Optional ByVal Message As Variant = Null)
    
        RaiseEvent CallBack(Form.Name, Nz(Message, Form.ActiveControl.Tag), Value)
    
    End Function
    
    Public Property Get Settings() As Long
    
        Dim lngStatus As Long
        Dim strSettings As String
        Dim ctl As Control
        
        strSettings = String(8, "0")
        If m_colButtons Is Nothing Then GatherControls
        For Each ctl In m_colButtons
            lngStatus = 0
            If ctl.Enabled = True Then lngStatus = 3 Else lngStatus = 1
            If ctl.Visible = True Then lngStatus = lngStatus + 12 Else lngStatus = lngStatus + 4
            Mid(strSettings, ctl.HelpConTextID, 1) = Hex(lngStatus)
        Next
        Settings = Val("&h" & strSettings)
       
    End Property
    
    Public Property Let Settings(ByVal Setting As Long)
    
        Static lngFormerSettings As Long
        
        Dim lngStatus As Long
        Dim strSettings As String
        Dim cmd As CommandButton
        
        Form.Text_Dummy.SetFocus
        If Setting = 0 Then
            Setting = lngFormerSettings
        Else
            lngFormerSettings = Me.Settings
        End If
        strSettings = Hex(Setting)
        strSettings = String(8 - Len(strSettings), "0") & strSettings
        If m_colButtons Is Nothing Then GatherControls
        For Each cmd In m_colButtons
            lngStatus = Val("&H" & Mid(strSettings, cmd.HelpConTextID, 1))
            If (lngStatus And 1) <> 0 Then cmd.Enabled = (lngStatus And 2) <> 0
            If (lngStatus And 4) <> 0 Then cmd.Visible = (lngStatus And 8) <> 0
        Next
     
    End Property
    2. Each Form module:
    Code:
    Option Compare Database
    Option Explicit
    
    Private m_ClsStd_Buttons As Cls_Std_E_ChildButtons
    
    Private Function CallBack()
    
        If m_ClsStd_Buttons Is Nothing Then Initialize
        m_ClsStd_Buttons.ParentCallBack Me.ActiveControl.HelpConTextID
        
    End Function
    
    Private Sub Form_Open(Cancel As Integer)
      
        Initialize
        
    End Sub
    
    Private Sub Initialize()
    
        Set m_ClsStd_Buttons = New Cls_Std_E_ChildButtons
        m_ClsStd_Buttons.DEEP_Attach Form
    
    End Sub
    
    Public Function Settings(ByVal ButtonsMask As Long)
    
        m_ClsStd_Buttons.Settings = ButtonsMask
        
    End Function
    3. In the Parent form module:
    Code:
    Option Compare Database
    Option Explicit
    
    Private WithEvents m_clsButtons As Cls_Std_E_ChildButtons
    Private m_varArgument As Variant
    Private m_lngFilterID As Long
    Private m_booDirty As Boolean
    
    Public Sub m_clsButtons_CallBack(ByVal Sender As String, ByVal Message As String, ByVal Extra As Variant)
    
        Select Case Message
            Case "Command_Delete":      DeleteFilter
            Case "Command_Diskette":    SaveFilter
            Case "Command_Exit":        ExitForm
            Case "Command_Undo":        Reset
            Case Else:                  MsgBox "Unknwon message: " & Message & " received from: " & Sender, vbInformation, "CallBack"
        End Select
    
    End Sub
    
    Public Function Hook(ByVal Child As Object)
    
        Select Case Child.FormGUID
            Case "{05323997-9DEF-46C5-91F3-DD16FEE4989D}":   Set m_clsButtons = Child
                                                             m_clsButtons.Settings = &H411F
    
        End Select
    
    End Function
    '
    ' The code continues...
    '
    Have a nice day!

  3. #3
    Join Date
    Aug 2010
    Posts
    57
    Thanks Sinndho - this gives me some ideas. Although I'm still trying to figure out in your code the exact order of events after a user clicks a button. I'm having a hard time figuring out which callback/event gets executed when.

  4. #4
    Join Date
    Mar 2009
    Posts
    5,442
    Provided Answers: 14
    The problem I tried (successfully) to solve was that when you have a form with one or several subforms, You cannot determine the order in which those form/subforms will be loaded and ready. Some are very simple and load very quickly, while others (e.g. a subform in Datasheet view with thousands of rows) take a longer time. DEEP stands for "Dynamic External Event Processing" (see: http://smsconsulting.spb****/shamil_...s/deepcnpt.htm). Another advantage is that the same class can be common to several forms in an application. The code is then centralized and easy to maintain.

    My system works asynchonously:
    - When a subform executes the code in the Form_Open event handler, it instanciates it's related class 'Cls_Std_E_ChildButtons in my example) and calls the DEEP_Attach procedure of it, passing a reference (pointer) to itself as parameter.
    - The DEEP_Attach function checks whether the associated form is a subform (i.e. has a parent form) and,if yes, calls the Hook function of the Parent form.
    - The Hook function of the Parent form identifies the caller (there can be several objects registering ("hooking") themself to the parent, using the FormGUID property of the caller and initializes its own pointer to the calling class accordingly.
    - When a command button is clicked, it calls the CallBack function of its form which transmit the call to the ParentCallBack function of the instance of its associated class.
    - In turn, the ParentCallBack function raises the CallBack event that can be processed by the Parent Form if there is any.

    As I use several classes build on the same principle for various purposes, they all have several common ("standard") properties that reside at the beginning of the code for the class, betweem the "[STDC]{Begin}" and "[STDC]{End}" and between the "[DEEP]{Begin}" and "[DEEP]{End}" tags.

    From there, I have built several "assistants" (like the standard add-ins available with Access) that can create parts of or a whole application (if its simple) by assembling these classes with forms that are almost codeless.
    Have a nice day!

  5. #5
    Join Date
    Aug 2010
    Posts
    57
    @sinndho - the scheme you developed is pretty nifty, and I got it all figured out, I think.

    My requirements are a bit different, and perhaps you have some insight into how to implement your callback scheme. I've created a different thread to look for other potential options, but I'll summarize here with respect to the callback concept.

    I have a datasheet that uses an edit link to open up a form to edit the record. The form opens up as a tab (not modal) and therefore I need a mechanism to let the datasheet know when the form is closed so it can update/requery itself. I set up a similar hook/callback concept to the one you had and raise an event when the edit form is closing.

    Works like a charm as long as the datasheet is in its own form so I can find it and call its Hook routine. However, the datasheets I'm interested in are subforms (or even subforms to a subform in one case). I this scenario I cannot figure out how to hook to the datasheet.

    Any suggestions?
    Carlos

  6. #6
    Join Date
    Mar 2009
    Posts
    5,442
    Provided Answers: 14
    1. Usually I create a parent form that is used as a container for the various subforms: List (form in datasheet view), Edit (Form in normal view for editing the data), Buttons, Navigation Buttons and Filter. This is the basic schema for most of the applications I build on this model. This parent form has the Hook procedure (it then has a pointer to all the instances of the classes associated with its subforms) and acts as a dispatcher: it handles the different events raised by the classes and takes the related actions accordingly (it can be to raise an event for another class, use a method or a property of another class, etc.).

    2. If the Edit form is only open when needed (if I understand correctly, it is open as an independant form (i.e. not a subform), it can declare a "Closing" event and raise it on the Form_Close event handler. What could work for a form, independant or open as a subform would be to detect whether its data were changed or not (mimicking the Dirty property of a bound form) and raise a "FormDirty" or "DataChanged" event when needed. The Textboxes of this form could be intitialized in the Cls_Specific function of the associated class:
    Code:
    Public Event FormDirty(Byval FormName As String)
    
    Public Sub SetDataChanged()
    
        RaiseEvent FormDirty(Form.Name)
    
    End Sub
    
    Private Function Cls_Specific()
    
    '   Cls_Specific() is mandatory for D.E.E.P.
    '   ----------------------------------------
    '   Performs class-specific initialization.
    '
        Dim ctl As Control
    
        Form.OnClose = "[Event Procedure]" ' For raising the "Closing" event.
        For Each ctl In Form.Controls
            If ctl.ControlType = acTextBox Then
                ctl.AfterUpdate = "=DataChanged()"
            End If
        Next ctl
        
    End Function
    And In the Form module:
    Code:
    Private Function DataChanged()
    
        m_clsDEEP.SetDataChanged
        
    End Function
    Have a nice day!

  7. #7
    Join Date
    Aug 2010
    Posts
    57
    @Sinndho - Sorry to bug you more on this, but I think I'm so close to what I need to do.

    In your Hook function you pass the Cls_Std_E_ChildButtons by value. I'm still somewhat confused by VBA's handling of references/values/objects. In my mind if you are passing by value, the parent form now has a COPY of the object from the subform. If it is the subform's object that raises an event, how does the parent form know about it?

    In my case I've decided that rather than have the class call a Hook function in the form - it makes more sense for the form to call a Hook in the class. That is because as I explained the class object lives in a new form that was launched, and the parent form has knowledge of the form that was launched (whereas the launched form may not have knowledge of who launched it). Hopefully that is not too confusing.

    So from my parent, which has a class object, I try calling the Hook function in the class object of the launched form. Ideally I just want to do the same asignment you are carrying out, but from the other end. However, no matter how I pass the object (by reference or value) I can't seem to make it come back to the parent form.

  8. #8
    Join Date
    Mar 2009
    Posts
    5,442
    Provided Answers: 14
    Let's talk C++ a little bit, will you?

    When in VB/VBA you write:
    Code:
    Set frm = Forms("SomeForm")
    Forms("SomeForm") is a reference (i.e. a pointer in C/C++) to the instance of a Form object "SomeForm" (forms are classes in Access) that is a member of the Forms collection (i.e. it is loaded in memory, or Access speaking: "it is open"), and the Forms collection is a member of the Application object. So, the complete reference is:
    Code:
    Set frm = Application.Forms("SomeForm")
    frm is also a pointer that receives a copy of Application.Forms("SomeForm"), i.e. a copy of the memory address of the entry point of the object. You now have to references to the same object: one through the Forms collection (which actually is a collection of pointers to all loaded ("open") forms, and "frm" which received a copy of this reference (i.e. this address) with the instruction "Set frm = ..."

    Passing such an object ByRef (i.e. passing a pointer) would mean that you would pass a pointer to frm which is already a pointer, you would then pass a pointer to a pointer to the actual object.

    Note: In reality the situation is more complex than described here because all these classes (Form classes or independent classes) are built on the COM model, so the actual pointer is one to a COM interface.

    In cases where an independent form is open, here's what I do to keep the same logic in all parts of my application:

    1. In the subform "SF_Orders_Edit", an independent form ("SF_Customer_Lookup") is open and several data are passed through the OpenArgs (optional) parameter:
    Code:
    Private Function OpenLookup()
    
        Dim lngLeft As Long
        Dim lngTop As Long
        Dim strTopic As String
        Dim strArguments As String
        
        lngLeft = Me.WindowLeft + Me.ActiveControl.Left
        lngTop = Me.WindowTop + Me.ActiveControl.Top
        strArguments = Me.Parent.Name & ";" & lngLeft & ";" & lngTop & ";" & "Customers" & ";" & "Proc_SF_Customer_Lookup_Select"
                DoCmd.OpenForm "SF_Customer_Lookup", , , , , acDialog, strArguments
    
    End Function
    2. When the form "SF_Customer_Lookup" opens it binds itself to its associated class ("Cls_Std_E_ChildLookup"), then passes its OpenArgs received argument(s) to it:
    Code:
    Private Sub Form_Open(Cancel As Integer)
      
        Set m_clsDEEP = New Cls_Std_E_ChildLookup
        m_clsDEEP.DEEP_Attach Form
        m_clsDEEP.Initialize Split(Me.OpenArgs, ";")
        
    End Sub
    3. The Initialize method of the class Cls_Std_E_ChildLookup receives the name of the Parent form of the subform "SF_Customer_Lookup" and uses the Forms collection to call the Parent "Hook()" function and pass a reference (pointer) of itself ("Me") to it:
    Code:
    Public Sub Initialize(Arguments As Variant)
    
        Dim lngLeft As Long
        Dim lngTop As Long
        
        If IsArray(Arguments) Then
            '
            ' Arguments(0) contains the name of the Parent form 
            ' of the subform associated to this class.
            '
            Forms(Arguments(0)).Hook Me
    ' The code continues...
    End Sub
    4. The Parent form ("Frm_Orders") initializes its own pointer to the instance of the class Cls_Std_E_ChildLookup that called its Hook() function:
    Code:
    Private WithEvents m_clsLookup As Cls_Std_E_ChildLookup
    '
    ' More declarations here...
    '
    Public Function Hook(ByVal Child As Object)
    
        Select Case Child.FormGUID
            Case "{DB9318DE-2D9D-4FB6-A8C3-28808E56D548}":  Set m_clsButtons = Child
            Case "{3CB7D149-1186-4FF5-98A2-DDD0E4BEDAE7}":  Set m_clsFilter = Child
                                                            InitializeFilter
            Case "{EF5644FF-57DA-47D0-AAD0-DE601B95BCBB}":  Set m_clsList = Child
            Case "{2BF55604-DA21-40ED-86DF-85A3F9C3E552}":  Set m_clsOrdersEdit = Child
                                                            InitializeOrdersEdit
            Case "{85727236-543C-4CD8-A1B8-017E55CD5AB4}":  Set m_sfStatus = Child
                                                            InitializeStatus
            Case "{062E3A1F-4AC4-483C-8A39-9A7E290AB9D7}":  Set m_clsNavigationButtons = Child
            Case "{29CF998F-68EA-4B19-A3E3-CD1A72B0F820}":  Set m_clsFilterMask = Child
            Case "{24BF16C4-7D8D-421E-A683-18D1FBC1BA29}":  Set m_clsLookup = Child
        End Select
    
    End Function
    It also declares the Event Handler ("listener") that will process the events raised by the class "Cls_Std_E_ChildLookup":
    Code:
    Public Sub m_clsLookup_Callback(ByVal Sender As String, ByVal Message As String, ByVal Extra As Variant)
    
    '    Process the messages from the class Cls_Std_E_ChildLookup...
        
    End Sub
    What's interesting here is that, one at a time, 15 or so different independent forms can be open at a moment or another from the subform ("SF_Orders_Edit").

    In each case, an instance of the class "Cls_Std_E_ChildLookup" is created and registers itself to the parent form "Frm_Orders" by calling the "Hook()" function.

    So we have several forms with different shapes, different controls, etc. that are almost codeless and that all use an instance of the same class ("Cls_Std_E_ChildLookup"). Every event raised by this class is handled in a centralized way by an event handler ("m_clsLookup_Callback") in the parent form "Frm_Orders".
    Have a nice day!

  9. #9
    Join Date
    Aug 2010
    Posts
    57
    Thanks! I'll have all weekend to digest

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •