使用MVC设计模式在Access中实现数据的“查改增删”后记

一、MV未彻底解耦

上篇文章探讨了MVC设计模式在Access VBA中的一种实现,MVC设计模式的核心思想,是切断M(模型层)与V(视图层)之间的直接联系。

然而细心的读者会发现,上篇在V(视图层)类中的数据显示(Display)函数设计中,直接访问了作为参数传入的M(模型层)类的对象,代码如下:

'View类模块
'...
'以上代码省略

'数据显示
Public Function Display(ByRef data As Model) As Boolean
    Display = False
    If Not mfrm Is Nothing Then
        With mfrm
            .Controls("txtEmployeeNo") = data.EmployeeNo
            .Controls("txtEmployeeName") = data.EmployeeName
            .Controls("txtGender") = data.Gender
            .Controls("txtDOB") = data.DOB
            .Controls("txtJobTitle") = data.JobTitle
            .Controls("txtSalary") = data.Salary
        End With
        Display = True
    End If
End Function

'以下代码省略
'...

这种在V中显式访问M的属性,违反了MVC的核心原则,M和V之间依然没有解耦。可以设想,当tblData中的人事数据结构有了变化,比如,新增了一列,需要将员工的“电话号码”保存到数据库中,我们不仅需要修改M的设计,对V中的代码也需要相应变更。这不是我们所期望的。

本篇将与大家探讨彻底将M与V解耦的一个方案。


二、彻底解耦的方案选择

V中的Display函数之所以要逐个访问M中的每个字段属性,目的就是将每个字段的属性值,显示到目标窗体上。

为了实现这个目的,而不显式调用M的属性,我目前能想到的有2中方案:

方案一:为M添加一个记录集(Recordset)类型的属性,V直接将该记录集绑定给目标窗体。

方案二:还是允许Display函数访问M中的字段属性,但是使用隐式的方式。比如给M添加一个字符数组属性,在M实例化的时候,将M的所有字段属性名保存在该数组中。这样,我们就可以在Display函数中遍历该数组,隐式的调用M中的属性值。

方案一将目标窗体绑定到指定的记录集,如果目标窗体已经被绑定到其他的记录集的话,这种方案就会产生问题。另外,该指定的记录集中只有一条记录,我个人觉得不太适合将只有一条记录的记录集绑定给窗体。有的时候,我们也许确实需要为M添加一个记录集属性,比如需要M返回多条记录的记录集,然后将其绑定给一个子窗体。

方案二没有改变目标窗体的绑定属性。我个人倾向使用方案二。


三、解耦方案的实施

首先我们要给M添加一个保存全部字段名的数组属性。

在Model类中,定义一个字符数组变量,在类的初始化事件中将字段名保存其中:

'Model类
'...
'以上代码省略

Private marrFields() As String

Private Sub Class_Initialize()
    Dim db As DAO.Database
    Set db = CurrentDb()
    Set mrst = db.OpenRecordset("tblData")
    
    Dim fld As DAO.Field2
    Dim i As Integer
    ReDim marrFields(mrst.Fields.Count)
    For i = 0 To mrst.Fields.Count - 1
        marrFields(i) = mrst.Fields(i).Name
    Next i
End Sub

'...
'以下代码省略

然后为Model添加一个该字符串数组的只读属性:

'Model类
'...
'以上代码省略

Public Property Get Fields() As Variant
    Fields = marrFields
End Property

'...
'以下代码省略

这样,我们就有了一个保存所有字段名,而且可以随时访问,不用担心被人串改的属性。

现在,我们去到View类模块中,修改其中的Display函数:

'View类
'...
'以上代码省略

'数据显示
Public Function Display(ByRef data As Model) As Boolean
    Display = False
    If Not mfrm Is Nothing Then
        Dim i As Integer
        For i = 0 To UBound(data.Fields) - 1
            mfrm.Controls("txt" & data.Fields(i)) = CallByName(data, data.Fields(i), VbGet)
        Next i
        Display = True
    End If
End Function

'...
'以下代码省略

遍历Fields数组属性,通过CallByName函数,实现对象的字符串访问。

现在,我们可以看到,Display中已经没有任何Model中的属性名了。哦,有一个除外:Fields。如果我们在每一个Model类中,都使用同样的逻辑,部署一个Fields属性,那么,V和M就彻底解耦了!!!

修改完Display函数,我们可以用同样的方法,修改GetDisplayedData函数:

'View类
'...
'以上代码省略

'获取显示数据
Public Function GetDisplayedData() As Model
    Dim data As Model
    If Not mfrm Is Nothing Then
        Set data = New Model
        Dim i As Integer
        For i = 0 To UBound(data.Fields) - 1
            Select Case TypeName(CallByName(data, data.Fields(i), VbGet))
                Case "String"
                    CallByName data, data.Fields(i), VbLet, Nz(mfrm.Controls("txt" & data.Fields(i)), "")
                Case Else
                    CallByName data, data.Fields(i), VbLet, Nz(mfrm.Controls("txt" & data.Fields(i)), 0)
            End Select
        Next i
        Set GetDisplayedData = data
        Set data = Nothing
    End If
End Function

GetDisplayedData函数涉及到对目标窗体上的空值的转换,所以需要稍微绕一下。通过TypeName函数获知Model中字段的类型后,再决定Nz函数的第二个参数。

接下来,我们看看Clear函数。该函数虽然只是简单的将目标窗体上的控件全部清空,不涉及到对Model属性的任何访问。但是,我们既然有了Fields属性,完全可以用遍历的方式来做:

'View类
'...
'以上代码省略

'清理窗体
Public Function Clear() As Boolean
    Dim data As New Model
    Dim i As Integer
    Clear = False
    If Not mfrm Is Nothing Then
        For i = 0 To UBound(data.Fields) - 1
            mfrm.Controls("txt" & data.Fields(i)) = Null
        Next i
        Clear = True
        Set data = Nothing
    End If
End Function

这里我们为了能访问到Model的Fields属性,创建了一个临时的Model对象,尽管用完就将其释放掉,但心里多少有一点“奢侈浪费”的感觉。^_^

现在,我们再来重新审视一下类模块View,再也看不到任何与字段名相关的代码了!


四、Model中完美的代码

我们进到类模块Model中,发现还是有很多字段名相关的代码。虽然没有什么不对,但总感觉我们可以做得更完美——尽量减少对字段名的明文引用。

我们修改Model中的“查改增”函数如下:

'Model类
'...
'以上代码省略

'查
Public Function GetData(ByVal EmployeeNo As Long) As Boolean
    Dim i As Integer
    GetData = False
    With mrst
        .Index = "PrimaryKey"
        .Seek "=", EmployeeNo
        If Not .NoMatch Then
            For i = 0 To UBound(Me.Fields) - 1
                CallByName Me, Me.Fields(i), VbLet, .Fields(Me.Fields(i))
            Next i
            GetData = True
        End If
    End With
End Function

'改
Public Function Update(ByRef data As Model) As Boolean
    Dim i As Integer
    Update = False
    With mrst
        .Index = "PrimaryKey"
        .Seek "=", data.EmployeeNo
        If Not .NoMatch Then
            .Edit
                For i = 0 To UBound(data.Fields) - 1
                    .Fields(data.Fields(i)) = CallByName(data, data.Fields(i), VbGet)
                Next i
            .Update
            Update = True
        End If
    End With
End Function

'增
Public Function AddNew(ByVal data As Model) As Boolean
    Dim i As Integer
    AddNew = False
    With mrst
        If Not Me.GetData(data.EmployeeNo) Then
            .AddNew
                For i = 0 To UBound(data.Fields) - 1
                    .Fields(data.Fields(i)) = CallByName(data, data.Fields(i), VbGet)
                Next i
            .Update
            AddNew = True
        End If
    End With
End Function

这回世界终于清静了。


五、示例文件

知乎不能上传Access文件,读者可以到我的个人博客上下载示例文件。

http://www.jasoftiger.com/?ddownload=843www.jasoftiger.com

编辑于 2018-11-09

文章被以下专栏收录