创建用户友好的 Web 数据采集表单






4.89/5 (15投票s)
一个电子数据捕获解决方案,允许授权的临床研究人员轻松地在线设计、创建、编辑和填写 CRF,而无需任何编程经验。这是一个 VB.Net Web 应用程序,使用 SQL 或 MS Access 后端的 EAV 表。
- 下载 CP_Dynamic_CRF_18-08-2015-v3-RadioButtons.zip - 17.5 KB
- 下载 CP_Dynamic_CRF_29-11-2014-v2.zip - 17.5 KB
引言
作为临床研究团队的一员,我们认识到准确数据收集的必然需求。通常,带有结构化字段的标准表单在临床研究方案的病例报告表(CRF)部分进行描述。
CRF 的完整性和准确性通过一组下拉菜单和编码到用于提交收集数据的软件中的验证脚本来维护。通常,临床研究电子数据捕获软件要么由机构的 IT 部门“内部”构建,要么从商业供应商购买,要么从开源临床研究软件联盟采用。所使用的软件要么实现“经典”的规范化数据库表,要么实现实体-属性-值(EAV)模型。
虽然采用现成的开源电子数据捕获软件可能看起来很诱人,但由于选择受限于特定的平台、数据库或 Web 服务器,可能会出现许多问题。软件的定制以完全满足您团队的需求也将受到限制,并依赖于社区或联盟的支持。此外,一些联盟会设置可能不适合临床研究团队的条件和法律限制。购买的软件也存在同样的问题,而且您还需要为此付费。
我们在临床研究方面的经验表明,需要一个电子数据捕获软件,该软件允许授权的临床研究人员轻松地在线设计、创建、编辑和填写 CRF,而无需任何编程经验。结果 CRF 的字段应包含(根据设计者的偏好)经过 Regex 验证的文本框和下拉列表,但还应显示由设计者选择的自定义消息的工具提示。为了提高准确性,应为某些字段设置“跳过逻辑”。因此,我们决定使用 VB.NET 构建我们自己的电子数据捕获 Web 应用程序,以满足前面描述的需求。
本示例中使用的数据库是 MS Access。但是,使用 SQL Server 作为后端只需要对代码和连接字符串进行少量修改。
数据库由 3 个表组成:maintab、FollowUp 和 CRF。
“maintab”包含患者姓名、病历号(或研究号)以及关于患者的一些“自由文本注释”。此表有两个目的:首先,强制的引用完整性将防止提交错误或缺失 MRN 的数据。此表还充当“现场”验证,因为它在表单提交后立即告诉用户他提交的数据是否与正确的患者姓名和 MRN 匹配。
“FollowUp”表将包含已提交表单的数据,其中字段“FU”代表病例报告表单的项目,例如姓名、年龄、性别、诊断等。字段“FUData”将包含该特定项目的数值,例如“13 years”对应于项目“Age”,或“female”对应于项目“Gender”。
“CRF”表将包含应用程序用于构建数据录入表单的数据。用户需要填写的项目将进入“Item”字段。
应用程序有两种模式:“设计模式”和“数据提交”模式。设计模式允许用户通过 Web 界面创建和编辑数据收集表单。这通过“add”和“update”查询写入“CRF”表来完成。
控件的“type”是“text”(文本框)或“DDL”(下拉列表)。该应用程序使用“Item”和“Type”中的数据动态创建文本框和下拉列表,对象名为“Item”。“options”字段将用列表项和相应的对应值填充,并用一个符号分隔,以便代码识别列表项与值的分隔位置,在本例中我们使用了“$”。
对于文本框,字段“validation”是程序化创建的客户端验证控件使用的正则表达式的来源,其属性“ControlToValidate”从“Item”字段获取数据。 控件的其他属性,例如工具提示或依赖的跳过逻辑,也将通过编程方式添加到相关对象中。
大多数临床方案都会告诉设计者表单字段名称、控件类型、字段是否必需以及每个字段的验证条件的确切规范。设计者只需将方案的建议“翻译”成要填写到 CRF 设计 Web 界面的值,无需编程或数据库设计。
在数据提交模式下,用户将从下拉菜单中选择所需的 CRF。应用程序会将选定的 CRF 名称存储在会话中,并将用户重定向到数据录入页面。存储的会话将用于 select 查询,从“CRF”表中提取构建表单所需的数据。
加载的表单将显示从“CRF”数据库表调用的每个单独字段的属性。必需字段验证器和动态创建的正则表达式验证控件将保持数据的准确性和完整性,同时工具提示会在指定的字段弹出,以指导用户如何提交正确的数据。
使用代码
该解决方案由两部分组成:第一部分负责数据的显示、搜索、导出到 Excel 和审计跟踪。为了节省时间,我们实现了一个经过许可的商业 ASP.Net 代码生成器来为上述功能创建 aspx 页面。第二部分,我们将在接下来的几行中讨论,是 CRF 创建和将提交的数据插入数据库的逻辑。
该解决方案的 Default 页面包含以下组件:“Label1”、“InsertData”按钮、“PlaceHolder1”和“CRFds”数据源。
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
<div>
<asp:Label ID="Label1" runat="server" style="font-weight: 700; color: #516B92; font-size: large"></asp:Label>
<asp:Button ID="InsertData" runat="server" Text="InsertData" Width="85px"
style="height: 26px" />
</div>
<asp:PlaceHolder ID="PlaceHolder1" runat="server">
</asp:PlaceHolder>
<asp:AccessDataSource ID="CRFds" runat="server"
SelectCommand="SELECT [Item], [type], [options], [ID], [validation], [Tooltip], [required],[IndepControl],[IfValueEquals],[IsVisible] FROM [CRF] WHERE ([formname] = @formname)"
DataFile="~/App_Data/DataBaseName.mdb">
<SelectParameters>
<asp:ControlParameter ControlID="Label1" Name="formname" PropertyName="Text" Type="String" />
</SelectParameters>
</asp:AccessDataSource>
</asp:Content>
“Page Load Event”将触发 Sub “Initialize CRF”
Private Sub InitializeCRF()
Dim CRFgrid As New GridView
Session("itemcount") = CRFgrid.Rows.Count
buildCRF(CRFgrid, CRFds, PlaceHolder1)
End Sub
然后将以下变量传递给函数“buildCRF
”以构建带有相关跳过逻辑和客户端验证控件的表单。
Public Shared Sub buildCRF(ByVal CRFgrid As GridView, _
ByVal CRFds As DataSourceControl, ByVal placeholder1 As PlaceHolder)
CRFgrid.DataSource = CRFds
CRFgrid.DataBind()
Dim ItemCount As Integer = CRFgrid.Rows.Count
'build data table using asp table
Dim table1 As New Table
table1.ID = "table1"
For I = 0 To (ItemCount - 1)
'build table row
Dim myRow As New TableRow
Dim itemname As String = CRFgrid.Rows(0 + I).Cells(0).Text.ToString
Dim ItemNamelabel As New Label
ItemNamelabel.Text = "" & itemname
ItemNamelabel.Font.Bold = True
'build cells inside each row
Dim c1 As New TableCell()
Dim c2 As New TableCell()
c1.VerticalAlign = VerticalAlign.Top
c2.VerticalAlign = VerticalAlign.Bottom
Select Case (CRFgrid.Rows(0 + I).Cells(1).Text.ToString).ToLower
Case "text"
Dim ItemNameBox As New TextBox
ItemNameBox.ID = itemname
'This code is here to initialize skip logic
If CRFgrid.Rows(0 + I).Cells(7).Text <> " " Then
'These nested loop will search for the independentent variable and make its postback = true
For Each Rw As TableRow In table1.Rows
For Each CL As TableCell In Rw.Cells
Dim IndepControl As New Control
For Each c As Control In CL.Controls
'If the independent control is found, enable its autopostback, and add attributes to the cell
If LCase(c.ID) = LCase(CRFgrid.Rows(0 + I).Cells(7).Text.ToString) Then
'The previous IF condition is similiar to Findcontrol method
IndepControl = c
If TypeOf (IndepControl) Is TextBox Then
CType(IndepControl, TextBox).AutoPostBack = True
ElseIf TypeOf (IndepControl) Is DropDownList Then
CType(IndepControl, DropDownList).AutoPostBack = True
End If
'depvar = independant control (gender); depval = condition (is female ); depresult = true visible or not
c2.Attributes.Add("DepVar", CRFgrid.Rows(0 + I).Cells(7).Text.ToString)
c2.Attributes.Add("DepVal", CRFgrid.Rows(0 + I).Cells(8).Text.ToString)
c2.Attributes.Add("DepResult", CRFgrid.Rows(0 + I).Cells(9).Text.ToString)
End If
Next
Next
Next
End If
'End of skip logic initialization
c1.Controls.Add(ItemNamelabel)
c2.Controls.Add(ItemNameBox)
If itemname.ToString.Equals("MRN") Then
ItemNamelabel.ForeColor = Drawing.Color.Red
ItemNameBox.BackColor = Drawing.Color.Yellow
Dim myrequired As New RequiredFieldValidator
myrequired.ControlToValidate = ItemNameBox.ID
myrequired.ForeColor = Drawing.Color.Red
myrequired.ErrorMessage = "required !"
c2.Controls.Add(myrequired)
End If
If CRFgrid.Rows(0 + I).Cells(6).Text.ToString = "1" Then
ItemNamelabel.ForeColor = Drawing.Color.Red
ItemNameBox.BackColor = Drawing.Color.Yellow
Dim myrequired As New RequiredFieldValidator
myrequired.ControlToValidate = ItemNameBox.ID
myrequired.ForeColor = Drawing.Color.Red
myrequired.ErrorMessage = "required !"
c2.Controls.Add(myrequired)
End If
Dim tmpv = CRFgrid.Rows(0 + I).Cells(4).Text.ToString
Dim tooltipme = CRFgrid.Rows(0 + I).Cells(5).Text.ToString
If tooltipme <> "" And tooltipme <> " " Then
ItemNameBox.ToolTip = tooltipme.ToString
End If
If tmpv <> "" And tmpv <> " " Then
Dim myValidator As New RegularExpressionValidator
myValidator.ValidationExpression = tmpv
myValidator.ControlToValidate = ItemNameBox.ID
myValidator.ErrorMessage = "wrong data type !"
c2.Controls.Add(myValidator)
End If
myRow.Cells.Add(c1)
myRow.Cells.Add(c2)
table1.Rows.Add(myRow)
Case "ddl"
Dim ItemNameBox As New DropDownList
ItemNameBox.ID = itemname
Dim tooltipme = CRFgrid.Rows(0 + I).Cells(5).Text.ToString
If tooltipme <> "" And tooltipme <> " " Then
ItemNameBox.ToolTip = tooltipme.ToString
End If
Dim opts() As String = Split(CRFgrid.Rows(0 + I).Cells(2).Text.ToString, "$")
Dim optsLabels() As String = Split(opts(0), ",")
Dim optsVals() As String = Split(opts(1), ",")
For t = 0 To UBound(optsLabels)
ItemNameBox.Items.Add(optsLabels(t))
ItemNameBox.Items.Item(ItemNameBox.Items.Count - 1).Value = optsVals(t)
Next
'This code is here to initialize skip logic
If CRFgrid.Rows(0 + I).Cells(7).Text <> " " Then
'These nested loop will search for the independentent variable and make its postback = true
For Each Rw As TableRow In table1.Rows
For Each CL As TableCell In Rw.Cells
Dim IndepControl As New Control
For Each c As Control In CL.Controls
'If the independent control is found, enable its autopostback, and add attributes to the cell
If LCase(c.ID) = LCase(CRFgrid.Rows(0 + I).Cells(7).Text.ToString) Then
'The previous IF condition is similiar to Findcontrol method
IndepControl = c
If TypeOf (IndepControl) Is TextBox Then
CType(IndepControl, TextBox).AutoPostBack = True
ElseIf TypeOf (IndepControl) Is DropDownList Then
CType(IndepControl, DropDownList).AutoPostBack = True
End If
'depvar = independant control (e.g. gender); depval = condition (e.g. is female ); depresult = true visible or not
c2.Attributes.Add("DepVar", CRFgrid.Rows(0 + I).Cells(7).Text.ToString)
c2.Attributes.Add("DepVal", CRFgrid.Rows(0 + I).Cells(8).Text.ToString)
c2.Attributes.Add("DepResult", CRFgrid.Rows(0 + I).Cells(9).Text.ToString)
End If
Next
Next
Next
End If
'End of skip logic initialization
c1.Controls.Add(ItemNamelabel) : myRow.Cells.Add(c1)
c2.Controls.Add(ItemNameBox) : myRow.Cells.Add(c2)
table1.Rows.Add(myRow)
End Select
Next
placeholder1.Controls.Add(table1)
End Sub
“InsertData”按钮单击将传递以下变量并触发“insertCRFdata”函数。请注意,“MRN”是病历号,是临床试验中患者的唯一标识符。
Public Shared Sub insertCRFdata(ByVal mydb As OleDbConnection, _
ByVal myitemcount As Integer, ByVal placeholder1 As PlaceHolder, _
ByVal label1 As Label, ByVal activeuser As String)
mydb = New OleDbConnection("Provider=Microsoft.Jet.OLEDB.4.0;Data " & _
"Source= |datadirectory|DataBaseName.mdb;Persist Security Info=True")
mydb.Open()
Dim part2 As Integer = myitemcount
Dim myMRN As String = ""
Dim myTable As New Table
For Each T As Control In placeholder1.Controls
If T.ID = "table1" Then myTable = CType(T, Table)
Next
For Each Rw As TableRow In myTable.Rows
For Each CL As TableCell In Rw.Cells
For Each C As Control In CL.Controls
If C.ID = "MRN" Then myMRN = CType(C, TextBox).Text
Next
For Each C As Control In CL.Controls
If TypeOf (C) Is TextBox Or TypeOf (C) Is DropDownList Then
If C.ID <> "MRN" Then
Dim sqlstring = "insert into [followup] ([mrn], [fu], [fudata], " & _
"[formname], [enteredby]) values (@mrntxt, @mylabel, @mydata, @myformname, @myenteredby);"
Dim mydbcommand As New OleDbCommand(sqlstring, mydb)
mydbcommand.Parameters.Add("@mrntxt", OleDbType.VarChar).Value = myMRN
mydbcommand.Parameters.Add("@mylabel", OleDbType.VarChar).Value = C.ID
If TypeOf (C) Is TextBox Then
mydbcommand.Parameters.Add("@mydata", OleDbType.VarChar).Value = CType(C, TextBox).Text
ElseIf TypeOf (C) Is DropDownList Then
mydbcommand.Parameters.Add("@mydata", OleDbType.VarChar).Value = CType(C, DropDownList).SelectedItem.Text
End If
'-------------
mydbcommand.Parameters.Add("@myformname", OleDbType.VarChar).Value = label1.Text
mydbcommand.Parameters.Add("@myenteredby", OleDbType.VarChar).Value = activeuser
'------------
mydbcommand.ExecuteNonQuery()
End If
End If
Next
Next
Next
mydb.Close()
End Sub
由于 EAV 表中存储的数据“按原样”不可用,因此我们需要找到一种方法将数据处理成有意义的表,其中包含“经典”字段和记录,以便随后由统计软件处理。
使用应用程序中提供的“高级”搜索页面,用户通过 Web 启用的过滤器选择要从 EAV 表“Followup”中提取的数据范围。然后,用户将 EAV 数据表导出到 MS Excel 文件。使用以下 VBA 代码行,EAV 表被“透视”成一个“经典”表。
Sub PivotBtn_Click()
Dim srcworksheet, trgtworksheet As New Worksheet
Set srcworksheet = Sheets(1)
Set trgtworksheet = Sheets(2)
Dim CurrentTargetRow, CurrentTargetColumn As Integer
CurrentTargetRow = 1
CurrentTargetColumn = 1
For i = 2 To 1E+21
currMRN = srcworksheet.Cells(i, 2).Value
If currMRN = "" Then Exit For
currField = srcworksheet.Cells(i, 8).Value & "_" & srcworksheet.Cells(i, 3).Value
currFieldDat = srcworksheet.Cells(i, 4).Value
UniqueID = Str(srcworksheet.Cells(i, 5).Value) & " " & Str(srcworksheet.Cells(i, 6).Value) & " Entered by: " & srcworksheet.Cells(i, 7)
'Search for the current MRN in the target sheet. If found, record its row number.
'If not, then the row number should be set to the maxrownumber + 1 (i.e. new row)
For t = 2 To 1E+21
If Val(currMRN) = Val(trgtworksheet.Cells(t, 1).Value) Then
If trgtworksheet.Cells(t, 2).Value = UniqueID Then
CurrentTargetRow = t
Exit For
End If
End If
If trgtworksheet.Cells(t, 1).Value = "" Then CurrentTargetRow = t: Exit For
Next
'Search for the current field in the field labels of the target sheet.
'If found, just add the value, if not, then add this label and increase the LastTargColumn count
For r = 3 To 1E+21
If currField = trgtworksheet.Cells(1, r).Value Then
CurrentTargetColumn = r
Exit For
End If
If trgtworksheet.Cells(1, r).Value = "" Then CurrentTargetColumn = r: Exit For
Next
'Set the values
trgtworksheet.Cells(CurrentTargetRow, 1).Value = currMRN
trgtworksheet.Cells(CurrentTargetRow, 2).Value = UniqueID
trgtworksheet.Cells(1, CurrentTargetColumn).Value = currField
trgtworksheet.Cells(CurrentTargetRow, CurrentTargetColumn).Value = currFieldDat
Next
End Sub
对于大型病例报告表,我们启用了 Excel 文件的离线组合,其中包含 CRF 数据。然后,用户只需上传文件,然后一些代码行将执行插入查询。
我们也启用了“反透视”经典表,以帮助用户将现有的患者数据导入解决方案数据库。用户应在 Excel 工作表中使用以下 VBA 代码将经典表转换为 EAV。
Sub UnPivotBtn_Click()
Sheets("Sheet2").Cells(1, 1).Value = "MRN"
Sheets("Sheet2").Cells(1, 2).Value = "FU"
Sheets("Sheet2").Cells(1, 3).Value = "FU Data"
MaxRows = 999999
MaxColumns = 9999
x = 2
For i = 2 To MaxRows
d = Sheets("Sheet1").Cells(i, 1).Value
If d = "" Then Exit For
For t = 2 To MaxColumns
s = Sheets("Sheet1").Cells(1, t).Value
e = Sheets("Sheet1").Cells(i, t).Value
If s = "" Then Exit For
Sheets("Sheet2").Cells(x, 1).Value = d
Sheets("Sheet2").Cells(x, 2).Value = s
Sheets("Sheet2").Cells(x, 3).Value = e
x = x + 1
Next
Next
End Sub