Click here to Skip to main content
Click here to Skip to main content

Tagged as

Go to top

Creating Dynamic User Friendly Web-Based Data Collection Forms

, , 27 May 2013
Rate this:
Please Sign up or sign in to vote.
An electronic data capture solution that allows the authorized clinical research staff to easily design, create, edit and fill CRFs online without the need for any programming experience. It is a VB.Net web application using EAV table on SQL or MS Access backend.

Introduction

Being members of a clinical research team, we recognized the inevitable need for accurate data collection. Standard forms with structured fields are usually described in the clinical research study protocol at the Case Report Forms (CRFs) section.  

CRFs completeness and accuracy are maintained through a set of drop-down menus and validation scripts coded into the software that is used to submit the collected data. Usually, clinical research electronic data capture software are either built "in-house" by the institution's IT department, purchased from a commercial vendor or adopted from opensource clinical research software consortia. The used software either implements the "classic" normalized database tables or the entity-attribute-value (EAV) model.

Although adoption of a ready-made open source electronic data capture software might seem tempting, many issues might arise due to the restriction of choice to a specific platform, database or web-server. The customization of the software to exactly fit your teams needs will also be limited and dependent on the community or consortium support. Moreover, some consortia puts conditions and legal restrictions that may not suit the clinical research study team. The same issues applies for purchased software, but, moreover, you will have to pay for it.

Our experience in clinical research pointed out the need for an electronic data capture software that allows the authorized clinical research staff to easily design, create, edit and fill CRFs online without the need for any programming experience. The resulting CRF's fields should contain (according to the preference of the designer) Regex validated text boxes and drop-down lists, but also shows a tool-tip with a custom message chosen by the designer. To increase accuracy, a "skip-logic" should exit for some fields. So, we decided to build our own electronic data capture web application fulfilling the needs previously described using VB.NET.

The database used in this sample is MS Access. However, using SQL server as back-end will only need slight modifications in the code and connection strings.

The database is composed of 3 tables: maintab, FollowUp and CRF.

The "maintab" containing the patient name, medical record number (or study number) as well as some "free text note" about the patient. This table serves two purposes: first, the enforced referential integrity will prevent the submission of data for a wrong or missing MRN. This table also act as "in-situ" verification, as it tells the user that he submitted the data for the correct patient name and MRN immediately after form submission.

 

The "FollowUp" table will contain the data of the submitted forms, where the field "FU" represents the Item of the case report form such as Name, Age, Gender, Diagnosis, etc.. and the field "FUData" will contain the value for that specific item such as "13 years" for Item "Age" or "female" for Item "Gender". 

 

The table "CRF" will contain the data used by the application to build the data entry form. The items required to be filled by the user will go in the field "Item".  

 

There are two modes in the application: the "design mode" and the "data submission" mode. The design mode allows the user to create and edit data collection forms through a web interface. This is performed through "add" and "update" queries writing on the "CRF" table.

The "type" of control is either "text" for textbox or "DDL" for drop-down lists. The application uses data from "Item" and "Type" to dynamically create textboxes and drop-downlists with the object name "Item". The field "options" will be filled with list items and respective correspondant values separated by a symbol for the code to identify where to separate list items from values, in our case we used a "$". 

In case of textboxes, the field "validation" is the source of the regex that is used by the programatically created client side validation control, with the property "ControlToValidate" consumed the data from the "Item" field. Other properties of the control such as tooltips or dependent skip logic will also be added programmatically to the object in question. 

  

Most clinical protocols will tell the designer exact specifications of the forms field names, type of control, if the field is required or not and even the validation conditions for each field. The designer will have to simply "translate" the protocols recommendations into values to be filled into the CRF design web interface, no programming or database design required.  

In the data submission mode the user will select the required CRF from a drop-down menu. The application will store the selected CRF name in a session and redirects the user to the data entry page. the session stored will be used in a select query to extract the data needed to build the form from the table "CRF".

 

The loaded form will show the properties assigned to each indiviual field as called from the "CRF" database table. The required field validator and the dynamically created regex validation controls will preserve data accuracy and completion while the tooltip pops up at the specified fields to guide the user how to submit correct data. 

Using the code

The solution is composed of two parts: the first part is concerned with data display, search, export to excel and audit trail. To save time we implemented a licensed commercially available ASP.Net code generator to create the aspx pages for the previously mentioned functions. The second part, that we will address in the following lines, is the logic behind the creation of the CRF and insertion of the submitted data into the database.

The Default page of the solution contains the following components: "Label1", "InsertData" button, "PlaceHolder1" and "CRFds" datasource. 

<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>

The "Page Load Event" will trigger the Sub "Initialize CRF"

Private Sub InitializeCRF()
    Dim CRFgrid As New GridView
    Session("itemcount") = CRFgrid.Rows.Count
    buildCRF(CRFgrid, CRFds, PlaceHolder1)
End Sub    

which then will pass the following variables to the function "buildCRF" to build the Form with the associated skip-logic and client side validation controls. 

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 

 The "InsertData" Button Click will pass the following variables and trigger the "insertCRFdata" function. Note that the "MRN" is the Medical Record Number and in the unique identifier for the patient in a clinical trial.

	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

Since the data stored in the EAV table isn't usable “as-is”, we needed to find a way to process the data into a meaningful table with the “classic” fields and records to be then processed by statistical software.

Using the “advanced” search page available in the application, the user choose the range of data to be extracted from the EAV table “Followup” using web enabled filters. The user then exports the EAV data table into an MS Excel file. Using the following line of VBA, the EAV table is “pivoted” into a “classic” table.

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

For large Case Report Forms, we enabled the off-line composition of an excel file containing the CRF data. The user then will simply upload the file, then some lines of code will perform the insert query. 

We also enabled the “un-pivoting” of classic tables to help the user importing his existing patient data into the solutions database. The user shall use the following VBA code in an excel sheet to convert the classic table into 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Authors

MohamedKamalPharm
Team Leader
Egypt Egypt
Clinical Pharmacy AND Programming were, and still are, my life's passion. Children suffering from cancer has been my cause since 1998. I chose to "focus" on Children with Brain Tumors, as it is -till this day- one of the most challenging pediatric malignancies. Throughout my professional career, i learnt a lot of stuff to help me help children with cancer. Working with those under privileged kids also required me to have a deeper knowledge of Pediatric Oncology, Patient Education and cancer supportive care. I got training in different fields -not only- Clinical Pharmacy, but also Fundraising, Clinical Informatics and Web Development !

MohamedSabryBakry
Team Leader
Egypt Egypt
Physician and web developer studying Master Degrees in Statistics and Clinical Research Administration.
 
My passion is in Clinical Data Management and Biostatistics, and I am curious about exploring different programming languages and statistical packages.

Comments and Discussions

 
GeneralMy vote of 5 PinmemberHumayun Kabir Mamun29-May-13 23:34 
GeneralRe: My vote of 5 PinprofessionalMohamedKamalPharm30-May-13 10:00 
GeneralMy vote of 5 PinmemberАslam Iqbal27-May-13 18:59 
GeneralRe: My vote of 5 PinprofessionalMohamedKamalPharm27-May-13 20:43 
Questionambreen11 PinmemberAmbreen Sadiq12-Dec-12 17:55 
AnswerRe: ambreen11 PinmemberMohamedKamalPharm12-Dec-12 20:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140926.1 | Last Updated 28 May 2013
Article Copyright 2012 by MohamedKamalPharm, MohamedSabryBakry
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid