使用脚本字典在 MS Access 中存储对象以避免循环引用并允许表单知道其所有者对象

Posted

技术标签:

【中文标题】使用脚本字典在 MS Access 中存储对象以避免循环引用并允许表单知道其所有者对象【英文标题】:Using Scripting Dictionary to store Objects in MS Access to avoid circular references and allow forms to know their owner objects 【发布时间】:2020-02-14 09:24:59 【问题描述】:

我正在构建一个带有类的 Access 数据库,例如 clsOrder、clsCustomer 等,这些类管理带有表的接口。这些类在显示其数据时创建表单实例。我发现一旦代码的执行在其中一种形式中,我就无法引用创建它的父对象(那么有没有更好的方法来做到这一点?将是我的问题的一部分)。

为了解决这个问题,我使用脚本字典来存储类的实例,其中的键使用类的 ID 和类的唯一标识符(例如 Order-3265)。然后,我将对所有者对象的引用存储在表单本身中。

因此,当一个对象被创建并且它的 ID 已知时,它会在字典中放置一个指向自身的指针,并将该指针指向它的形式(希望这足够清楚)。

这允许表单与其所有者类进行交互。

我正在使用另一个类 clsManager 来将项目添加到字典或检索或删除(带有销毁)。

类的例子 - 严重削减..

clsManager:

Public WorkingObjects As New Scripting.Dictionary

Public Function AddWorkingObject(key As String, ObjectType As Object) As Boolean
    If Me.WorkingObjects.Exists(key) Then
        Me.WorkingObjects.Remove key
        Me.WorkingObjects.Add key, ObjectType
    Else
        Me.WorkingObjects.Add key, ObjectType
    End If
End Function

Public Function GetWorkingObject(key As String) As Object
    If Me.WorkingObjects.Exists(key) Then
        Set GetWorkingObject = Me.WorkingObjects(key)
    Else
        Set GetWorkingObject = Nothing
    End If
End Function

Public Function DestroyObject(obj As Object) As Boolean
Dim key As String
    If Not obj Is Nothing Then
        key = obj.DictionaryKey
        If Me.WorkingObjects.Exists(key) Then
            Me.WorkingObjects.Remove (key)
            Set obj = Nothing
            If obj Is Nothing Then
                Debug.Print key & " destroyed"
            Else
                Debug.Print obj.DictionaryKey & " NOT destroyed"
            End If
        End If
        Set obj = Nothing
    End If
End Function

cls引用:

Option Compare Database
    Option Explicit    

'use a form using an instance of this class to control manipulation of Quote records
'Loading and saving set default values if a null value is detected    

Private Const scTABLE As String = "tblQuote"    

Private intID As Long 'unique identifier
Private intCustomerID As Long
Private intSiteID As Long
Private rsQuoteTotalValues As DAO.Recordset
Private oCustomer As clsCustomer
Const ObjectType = "Quote-"
Private oEditForm As Form_frmQuote    

Property Get EditForm() As Form_frmQuote
    Set EditForm = oEditForm
End Property    

Property Get ID() As Long
    ID = intID
End Property
Property Let ID(QuoteID As Long)
    intID = QuoteID
    Me.EditForm.ID = QuoteID
End Property    

Property Get Customer() As clsCustomer
    Set Customer = oCustomer
End Property    

Property Let CustomerID(ID As Long)
    intCustomerID = ID
    oCustomer.Load (ID)
    EditForm.SiteID.RowSource = oCustomer.AddressSQL
    EditForm.SiteID.Requery
    EditForm.ContactID.RowSource = oCustomer.ContactsSQL
    EditForm.ContactID.Requery
    EditForm.CustomerID = ID
End Property    

Property Get DictionaryKey() As String
    DictionaryKey = ObjectType & CStr(Me.ID)
End Property
'END PROPERTIES//////////////////////////////////    

Public Sub DisplayForm(Visibility As Boolean)
    With Me.EditForm
        .Visible = False
        .subFrmQuoteSectionsSummary.SourceObject = ""
        If Visibility = True Then
            ...some stuff...
            .Visible = True
        End If
    End With
End Sub    

Public Function Load(ID As Long) As Boolean
'On Error GoTo HandleError
Dim RS As DAO.Recordset
Dim sQry As String
    Load = False
    If Nz(ID, 0) <> 0 Then
        sQry = "SELECT * FROM " & scTABLE & " WHERE ([ID]=" & ID & ");"
        Set RS = Manager.DB().OpenRecordset(sQry, dbOpenForwardOnly)
            With RS
                If .RecordCount = 0 Then
                    MsgBox "Cannot find Quote with ID = " & ID, vbCritical
                    GoTo Done
                End If
                Me.ID = Nz(!ID, 0)
                Me.CustomerID = Nz(!CustomerID, 0)
                Manager.AddWorkingObject Me.DictionaryKey, Me
                Me.EditForm.SetOwnerObject (Me.DictionaryKey)
                .Close
            End With
        Set RS = Nothing
        Load = True
    End If
Done:
    Exit Function
HandleError:
    MsgBox "Error in Customer Load: " & vbCrLf & Err.Description, vbCritical
    Resume Done
End Function    

Private Sub Class_Initialize()
    Debug.Print "Quote class initialized"
    Set oCustomer = New clsCustomer
    Set oEditForm = New Form_frmQuote
    Me.ID = 0
    Set oQuoteTidier = New clsClassTidier
    Me.DisplayForm (False)
End Sub    

Private Sub Class_Terminate()
    Set oCustomer = Nothing
    Set oEditForm = Nothing
    Debug.Print "Quote class terminated"
End Sub    

来自 EditForm:

Option Compare Database
Option Explicit    

'necessary for the object to have a reference to its owner in this manner to prevent circular reference
Private OwnerObject As clsQuote    

Public Function SetOwnerObject(OwnerKey As String) As Boolean
    SetOwnerObject = False
    Set OwnerObject = Manager.GetWorkingObject(OwnerKey)
    SetOwnerObject = True
End Function    

Private Sub cmdClose_Click()
    OwnerObject.EditForm.Visible = False
    Manager.DestroyObject OwnerObject
    DoCmd.Close acForm, Me.Name
End Sub    

每个业务对象类(如 ClsOrder)都有一个 editForm 实例,该实例在需要时加载和隐藏,并有一个保持打开状态的最多 3 个 DAO 记录集。

我认为所有对相互关联的业务对象的引用都是指向字典中对象的指针。

我的问题是错误 3035 超出系统资源。我检查过对象在不使用时被破坏,但反复打开和关闭对象会导致错误 3035。

所以问题是 - 我只是要求 Access 做一些它不能做的事情还是更好的编程来解决它?

【问题讨论】:

请分享minimal reproducible example。请注意,将对象添加到字典会创建强引用,并不能真正解决引用循环。有关弱引用,请参阅 This blog on RubberDuckVBA。在 Access 中使用对表单的引用时创建引用循环和内存泄漏非常容易,但如果没有完整的代码,就很难知道你在做什么。 谢谢埃里克。添加了一些代码,以便您可以看到我想要实现的目标。非常感谢指向 RubberDuck 的指针。真的,我在回答“我想这样做是不是疯了?” an editForm instance which is loaded and hidden until required 这是必需的吗?如果您需要编辑/显示数据而不是隐藏某些内容,为什么不制作new 编辑表单? 好吧,也许有点误导,因为 IMO 使用全局状态字典来避免引用循环更有可能导致而不是解决问题。就个人而言,我有时会在表单和类之间使用循环引用,并通过取消引用表单的 Form_Unload 事件中的对象来解决这个问题,该事件在用户关闭表单时触发,但在表单对象实际销毁之前。请牢记 KISS 原则,我不知道您的项目的确切内容,但您可能过于复杂了。 顺便说一下,Erik,RubberDuckVBA 链接非常好 - 谢谢。 【参考方案1】:

我认为编写所有代码的理由为零。为什么不让表单处理所有这些?请记住,每个表单实际上都是一个“类”实例。您甚至可以启动单个表单的多个副本,每个副本都有自己的代码、自己的数据,并且 SAME 表单的每个实例都可以 100% 独立于同一表单的其他工作副本运行。

如果您尝试查看此问题并希望为表单创建一个类对象,那么只需使用表单对象 - 这就是它为您做的事情!

我认为编写所有代码的好处为零。虽然 .net 具有数据集管理器和系统(现在是非常相似的实体框架,但由于 .net 没有数据绑定表单,所以这已经完成了很多工作。

在 Access 中,每个表单实际上都是一个类对象。这包括该形式的任何公共子或函数(因此函数成为该形式的方法,公共变量成为该形式的属性)。除了具有卡车装载事件的绑定表单之外,这些事件还用作针对任何数据编辑的操作。因此,与大多数系统不同,您在更新事件之前、更新事件之后都有“更改”事件。因此,通过简单地采用绑定形式,您将得到: 为您自动创建一个类对象。 您可以拥有该类的多个实例,因此同一表单的多个实例同时打开。 您将获得所有可用于验证数据输入的数据事件(或者让用户在满足您的条件之前不保存记录。 您可以充分利用所有数据列,即使控件未放置在绑定到这些列的表单上。因此,您甚至可以获得所有数据列的智能感知——即您的地图。

我不知道这里有一些巨大的循环引用问题。这就像在脚趾上扎伤,然后去看医生进行一些巨大的心脏直视搭桥手术。因此,继续进行一些大规模的编码狂潮,并为一些“罕见”问题或某种罕见且未见的循环参考问题消耗大量开发人员资金,本质上是一场巨大的疯狂追逐,只会让你咀嚼大量的开发人员代码和时间,而根本不需要 NONE。

我的意思是,如果您说打开了 SAME 表单的 3 个实例?那么代码如何知道并引用该形式的实例?好吧,在这里可以并且应该使用与典型 OO 编程中使用的完全相同的方法。这种方法意味着您不会对表单进行硬编码!代码中的名称或引用永远。你永远不想这样做。

那么,如果你在子窗体中,并且需要在父窗体中引用数据或控件?

你可以这样做:

strLastName = forms!frmCustomer!LastName

在上面,我们对表单名称进行了硬编码。你不想那样做。

在该子表单中,编写此代码的正确方法是:

strLastName = me.Parent.form!LastName

现在,请注意上面是如何引用父表单的。因此,即使我们同时有 3 个 frmCustomer 副本处于活动状态,该代码也可以工作。您可以通过对象“实例”完全引用表单中的任何内容。因此,在 JavaScrip 或 c# 中,您经常将“this.SomProperty”视为对该对象的引用。

在访问中,您可以做同样的事情,并使用“我”。或者 me.Parent.From 来引用父表单。因此,作为这里的一般方法,您永远不必对表单引用进行硬编码。如果您采用这种方法,那么循环引用的所有问题不仅将被消除,而且您正在使用经典和传统的方法进行对象编程和对象引用。虽然 Access 不是完整的 OO,但它肯定遵循许多 OO 设计概念,并且 Access 中的表单如何工作最肯定是对象的实例。

当表单对象模型已经作为该表单的“单个”类对象实例存在时,尝试编写大量代码是没有意义的,也不是必需的,而且你走的路可能会阻碍并降低你的能力处理你已经拥有的那个表单的奇妙实例。

如前所述,表单已经附加了字典和列,Access EVEN 会为您自动生成成员。结果是您可以引用表单绑定到的表的任何列

我.姓氏 我!姓

虽然上述两种格式是允许的,但第一种(me + 点 + 列名)实际上是表单类的成员。你会发现如果你使用代码来设置表单数据源,那么这些成员通常不会为你生成,因此你必须使用! (bang) 为该表单引用表中的列。

因此,当表单具有您在类对象中要求的所有功能时,我无法理解当您尝试所有额外的代码时。

【讨论】:

哇....那里有很多信息。谢谢你。事实上,我基本上都知道这些东西,而你告诉我的只是——别傻了,让你的班级属于这种形式,而不是反过来我想?我能看出这其中的意义。我的目标是所有“业务逻辑”都被排除在表格之外的情况,我的错误是我从你所说的内容中思考它的方式。不需要该类是表单的所有者... 如果我将 clsQuote 设置为表单中的变量,我可以通过 Me.Parent 从 clsQuote 对象中引用表单吗?如果没有,那么我回到 1 号方格...... 是的,你可以。但是表单已经是一个类,因此几乎不需要在该表单的代码模块中添加一个类——它已经是一个充分利用强类型数据和 Intel-sense 数据的类。你不会使用“me.Parent”,但实际上只是在 c# 中像“this”一样使用“me”,或者说 javascript。我经常在表单打开事件中这样做:frmPrevious = Screen.ActiveForm。结果是当前表单中的任何代码都可以引用打开该表单的调用表单。作为结果,我可以去 frmPrevious.Requery 或 frmPrevioius!LastName 或该表单中的任何位置。

以上是关于使用脚本字典在 MS Access 中存储对象以避免循环引用并允许表单知道其所有者对象的主要内容,如果未能解决你的问题,请参考以下文章

MS Access 客户端:在 Oracle 表中存储文档

使用拼写检查时使用 VBA 切换字典 (MS Access)

如何在 SQL Server 中存储图片(MS Access 界面)

MS SQL生成数据库字典脚本

带有 ADODB 记录集的 MS Access ListBox 列属性创建错误 424 需要对象

使用 Java Ucanaccess 在 ms-Access 中恢复数据和存储的查询