0
votes

This question relates directly to MS Access: Link to Excel file with header on row X > 1?

  • I want to target a randomly placed range of data in an MS Excel spreadsheet from MS Access and use it in a linked table.
  • The range must be able to both change location and grow downward.

Without modifying the target excel spreadsheet and creating a named range, can I somehow specify a range on the spreadsheet for a linked table in access that starts at a target cell/row and ends at the bottom right-most cell/row? A VBA Macro would probably fit this task well (similar to one in the linked example).

Alternatively, is there some way to have MS Access analyze the target spreadsheet, find the top row, leftmost cell and then determine where the bottom row, rightmost cell is? Almost like Application.ActiveSheet.UsedRange. This range would then be used as the target for the linked table.

The spreadsheet is published by another organization and is periodically replaced. Therefore any named ranges made within it would be removed when a new version of the spreadsheet is published.

I plan on adding this functionality into a database that will be given to a user who may not understand how to modify linked files. I.E. the top two or 3 rows are not needed, but the spreadsheet has additional rows added to it all the time. I want to give them a macro that simply re-links the updated spreadsheet without needing to change the format of the spreadsheet itself.

2
See msdn.microsoft.com/en-us/library/office/gg264813.aspx, but Access is quite intelligent about rows / columns as long as the rows / columns you do not need are null.Fionnuala
Seems like eliminating the unneeded rows above the headers is the problem. At least one of the cells has text in it which throws off the import functions... I'm finding some sample macros and hopefully I can create something that does this, was hopeful that there was an easy, quick way! Thanks for the link!Shrout1

2 Answers

0
votes

Ok, self answering.

  • This subroutine checks if the linked table already exists and then updates the table if it does.
  • The data on the Excel spreadsheet can move around. So long as the target header column always exists then this macro will find the first row that contains the header.
  • It utilizes excel's "Used Range" functionality, which isn't always 100% accurate, but it seems to be working well in my situation.

If adapting this code:

  • Be sure to modify the target table names and the target header text in this code to match your Excel file.
  • Be sure that the target header text isn't duplicated in the excel file and that it is on the same row as the other headers.
  • The row of the target header text is used as the starting row for the target range
  • Be sure that your target worksheet is the first worksheet in the workbook.

Thanks to this tek-tips post for the basis of this code. I am no expert, but this accomplishes what I set out to do. I'm sure this code could be streamlined further.

Public Sub ImportCLINDataSub()
Dim strCurrProjPath As String
Dim objExcel As Object 'Excel.Application
Dim objWorkbook As Object 'Excel.Workbook
Dim objWorksheet As Object 'Worksheet
Dim strXlFileName As String 'Excel Workbook name
Dim strWorksheetName As String 'Excel Worksheet name
Dim objCell As Object 'Last used cell in column
Dim strTargetRow As String 'Cell containing target text
Dim strUsedRange As String 'Used range
Dim strUsedRange1 As String 'This will store the first half of the used range, adjusted for the appropriate row
Dim strUsedRange1Column As String 'This will store the column value of the first half of the used range
Dim strUsedRange2 As String 'This will store the second half of the used range
Dim FileName As String
Dim objDialog, boolResult
Dim iPosition As Integer 'For finding first numeric character

Set objDialog = CreateObject("UserAccounts.CommonDialog")

objDialog.Filter = "Excel Files|*.xlsx|All Files|*.*"
objDialog.FilterIndex = 1

boolResult = objDialog.ShowOpen

If boolResult = 0 Then
    Exit Sub
Else

    'Assign Path and filename of XL file to variable
    strXlFileName = objDialog.FileName

    'Assign Excel application to a variable
    Set objExcel = CreateObject("Excel.Application")
    objExcel.Visible = False 'Can be visible or not visible
    objExcel.UserControl = True

    'Open the Excel Workbook
    Set objWorkbook = objExcel.Workbooks.Open(strXlFileName)

    'Assign required worksheet to a variable
    With objWorkbook
        Set objWorksheet = .Worksheets(1)
    End With

    With objWorksheet
    'Assign worksheet name to a string variable
    strWorksheetName = .Name
    End With

    'Assign used range to a string variable.
    strUsedRange = objWorksheet.Usedrange.Address(0, 0)
    'Turn off/Close in reverse order to setting/opening.

    'Check for target cell that indicates presence of CLIN data
    On Error Resume Next
    'This find command searches the used range for your header text
    'Replace "One Time Price" with target header text
    strTargetRow = objWorksheet.Range(strUsedRange).Find("One Time Price").Cells.Row

    'This error appears if the target header text is not found
    If Err.Number = 91 Then
        MsgBox "CLIN Data was not found in " & strXlFileName & vbCr & _
        "Check that CLIN listing is the first worksheet and that data format has not changed.", vbOKOnly, "Missing Data"
        'If data is not found, close all open Excel workbooks and instances
        objWorkbook.Close SaveChanges:=False
        Set objWorkbook = Nothing
        objExcel.Quit
        Set objExcel = Nothing
            Exit Sub
    End If
    'If no error, clear any errors and resume trapping
    Err.Clear
    On Error GoTo 0
    strUsedRange1 = Mid(strUsedRange, 1, InStr(1, strUsedRange, ":", vbBinaryCompare) - 1)
    strUsedRange2 = Mid(strUsedRange, InStr(1, strUsedRange, ":", vbBinaryCompare) + 1, Len(strUsedRange) - InStr(1, strUsedRange, ":"))
    iPosition = GetPositionOfFirstNumericCharacter(strUsedRange1)
    strUsedRange1Column = Mid(strUsedRange1, 1, iPosition - 1)
    strUsedRange = strUsedRange1Column & strTargetRow & ":" & strUsedRange2
    Set objCell = Nothing
    Set objWorksheet = Nothing

    'SaveChanges = False suppresses save message
    objWorkbook.Close SaveChanges:=False
    Set objWorkbook = Nothing
    objExcel.Quit
    Set objExcel = Nothing

     'If the table already exists, linking again will create a duplicate.
     'This prevents that from occurring.
     'THIS LINE IDENTIFIES TARGET TABLE NAME
     If ifTableExists("CLINs") = True Then
       'MsgBox "Clins Exists!"
        UpdateExcelLinkedTable (strWorksheetName & "$" & strUsedRange)
    Else
       'Import the worksheet - Change target table name ("CLINs" below) 
       'to match the table listed in the "ifTableExists" function call. 
       'If that is not changed then duplicates will be created each 
       'time this subroutine is run.
         DoCmd.TransferSpreadsheet acLink, 8, "CLINs", _
         strXlFileName, True, strWorksheetName & "!" & strUsedRange
    End If

 End If
  MsgBox "CLIN data imported successfully!"
End Sub

This function allows an Access Macro to call the main sub. Only for user convenience

Public Function ImportClinData()
    'Call Subroutine from here
    ImportCLINDataSub
End Function

Thanks to Rob for a function that gets the position of the first numerical value in the string that is used to establish the range for the source data. This allows the macro to reset the target row down to the first row where the headers are detected.

Public Function GetPositionOfFirstNumericCharacter(ByVal s As String) As Integer
    For i = 1 To Len(s)
        Dim currentCharacter As String
        currentCharacter = Mid(s, i, 1)
        If IsNumeric(currentCharacter) = True Then
            GetPositionOfFirstNumericCharacter = i
            Exit Function
        End If
    Next i
End Function

Another borrowed function (thanks Karthik) that checks to see if my target table exists

Public Function ifTableExists(tblName As String) As Boolean

ifTableExists = False
If DCount("[Name]", "MSysObjects", "[Name] = '" & tblName & "'") = 1 Then
ifTableExists = True
End If

End Function

Big thank you to Gord Thompson for this one. This function updates the "SourceTableName" component of the connection string. Because the "SourceTableName" appears to be a read-only property, the target object must be cloned and then deleted. I don't believe that this will interfere with pre-existing references to the linked data...

Sub UpdateExcelLinkedTable(TargetSourceTableName As String)
Dim cdb As DAO.Database
Dim tbd As DAO.TableDef, tbdNew As DAO.TableDef
Dim n As Long
Const LinkedTableName = "CLINs"
Set cdb = CurrentDb

Set tbd = cdb.TableDefs(LinkedTableName)
Debug.Print "Current .SourceTableName is: " & tbd.SourceTableName
On Error Resume Next
n = DCount("*", LinkedTableName)
Debug.Print "The linked table is " & IIf(Err.Number = 0, "", "NOT ") & "working."
On Error GoTo 0

Set tbdNew = New DAO.TableDef
tbdNew.Name = tbd.Name
tbdNew.Connect = tbd.Connect
tbdNew.SourceTableName = TargetSourceTableName 'Replace this with new string
Set tbd = Nothing
cdb.TableDefs.Delete LinkedTableName
cdb.TableDefs.Append tbdNew
Set tbdNew = Nothing

Set tbd = cdb.TableDefs(LinkedTableName)
Debug.Print "Updated .SourceTableName is: " & tbd.SourceTableName
On Error Resume Next
n = DCount("*", LinkedTableName)
Debug.Print "The linked table is " & IIf(Err.Number = 0, "", "NOT ") & "working."
On Error GoTo 0

Set tbd = Nothing
Set cdb = Nothing

End Sub
0
votes

I use dynamic names ranges all the time by setting the named range with a formula like this using the header row and first column of the range as the anchor point like this: =OFFSET(A1,1,0,COUNTA(A:A)-1,8) You can also use COUNTA to set the width of the columns. The limitations are that there must be nothing else but the table data in the column you are using, unless you can adjust for it on the formula e.g. the formula shown above calculated the number of text rows, less the header row. You could increase that for other values in the column as long as it is constant. You can also use COUNT instead of COUNTA if the column values are numeric (and the header isn’t). As long as the column is clean, you then just link to the names range which will automatically adjust to the number or rows in the table (and columns if that is set using a COUNTA function).