1

I would like to share with you my code...below is a short explanation for I want to develop: I have created a form with a connection to my Database (Access Db). through the form when I want to add data I shall press a button and new form will show with a set of control where my data shall be added. then I read those data and add a new datarow to my table which is bound to datagridview. until here everything is perfect.

but when I am trying to update my dataset to save the changes it gave me an error : "An INSERT INTO query cannot contain a multi-valued field".

I am trying to solve the issue but I couldn't succeed:

 Private Function AddDataFromControlArrayToDataRow(dataset As DataSet, tableName As String, Ctrl As Control())
        Dim row As DataRow = dataset.Tables(tableName).NewRow()
        Dim c As Integer = Ctrl.Length - 1
        For k As Integer = 0 To c - 1
            If (Ctrl(k).GetType.ToString = "System.Windows.Forms.CheckBox") Then
                Dim CheckBox As CheckBox = Ctrl(k)
                row(k) = CheckBox.Checked
            Else
                If Not Ctrl(k).Text = String.Empty Then
                    row(k) = Convert.ChangeType(Ctrl(k).Text, dataset.Tables(tableName).Columns(k).DataType)
                End If
            End If
        Next
        dataset.Tables(tableName).Rows.Add(row) '<----up ot here Code is running without error
        Form1.TableAdapterManager.UpdateAll(dataset)
        Return Nothing
    End Function
YowE3K
  • 23,852
  • 7
  • 26
  • 40
Yahya
  • 13
  • 1
  • 4
  • 1
    Does your DB table include an attachment field? Attachment fields are an example of a multi-valued field. Such fields require special handling. – TnTinMn Aug 04 '17 at 02:28
  • I suggest you show created query & table structure in Access DB. Multi-valued columns requires append query when doing INSERT (with `[column_name].Value`). Also try recreate the table without multi-valued columns if possible. – Tetsuya Yamamoto Aug 04 '17 at 03:40
  • As suggested, ADO.NET cannot really handle Access `Attachment` columns. It can retrieve them but you need to do a bit of extra work to get the data out and it cannot save them at all. If you really must use `Attachment` columns then you basically need to use DAO to deal with them. If you don't specifically need to use `Attachment` columns then don't. `OLE Object` columns can store binary data that you can save as `Byte` arrays using ADO.NET. – jmcilhinney Aug 04 '17 at 04:40
  • Is it a multi-value text field or an attachment type? – June7 Aug 04 '17 at 22:11
  • I have an attachment Field. – Yahya Aug 05 '17 at 10:00
  • If you want help with this, you will need to show us you Table schema definition. To void the error cause by the attachment field, you will need to write a specific insert command that excludes the attachment field. After the record is created, attachments can be added to the record, but you will need some workaround code that I can provide to prepare the attachment for insertion. To add attachments, your records must be uniquely identifiable (preferably via a primary key). – TnTinMn Aug 05 '17 at 14:22
  • @TnTinMn I have an example of Access Database with one table "MyTable" with 3 fields: ID (primary key), ClientName (String) and Attachment. I just want a code to read and write the attachment field...I tried to use DAO but it seems that is an old library and not supporting Access 2013...whenever i am trying to open a database is giving me an error _Unrecognized database format_... – Yahya Aug 06 '17 at 06:44

1 Answers1

3

You mentioned in your comments that you are having trouble using DAO. You need to add a reference to the Access Engine similar to this image (your version number may be different):

enter image description here

Then add this Imports statement to your code:

Imports DAO = Microsoft.Office.Interop.Access.Dao

An example of using DAO to manipulate the Attachment fields can be found in this SO question Storing an image into an Attachment field in an Access database.

If you don't mind using a hack, you can add/delete/extract attachments using OleDB. The following utility class is a result of hacking session I participated in on the MSDN forum about six years ago. I found that Byte array you retrieve from the AttachFieldName.FileData field is a header prefixed array. The first 4 bytes define an integer offset to actual file data. This allows you to recreate the attached file. I also discovered that if you prefix your file data with 8 bytes (the first four again being the offset to the file data and the remaining four left as zero) that Access will then accept it for storage. It will expand this header with miscellaneous values that are not important to you and rewrite the first 4 bytes with the new offset. I know this technique works for Access 2007 and 2010, but I have not been tested on newer versions. Also, as far as I know, you still need use the data offset in DAO to extract the original file; loading an attachment is easier due to the Field2.LoadFromFile command.

I apologize for this code dump, but getting the commands setup properly is a bit of a pain. Therefore I wrote this library to simplify things.

Imports System.Data.OleDb

Public NotInheritable Class Attachments
    Private Const prependLength As Int32 = 8

    ''' <summary>Expands the attachment field name to the 2 fields that need to be retrieved</summary>
    ''' <param name="attachmentFieldName"></param>
    ''' <returns>"attachmentFieldName.FileData, attachmentFieldName.FileName"</returns>
    Public Shared Function ExpandAttachmentFieldNames(attachmentFieldName As String, tableName As String) As String
        Return String.Format("[{0}].[{1}].FileData, [{0}].[{1}].FileName", tableName, attachmentFieldName)
    End Function

    ''' <summary>Prepends the 8 byte header block required by Access to store the bytes that comprise the fiel</summary>
    ''' <param name="source"></param>
    ''' <returns>8 bytes + source</returns>
    Public Shared Function PrependFileDataHeader(source As Byte()) As Byte()
        ' Through reverse engineering, it has been determined that while length of the 
        ' actual data header varies based on file type.

        ' This header always begins with the first 4 bytes representing the offset to the actual data bytes.
        ' The bytes from index 4 (zero based) to the beginning of the data appear to represent the file type 
        ' and other undetermined information.

        ' If an 8 byte field in prepended to the data bytes, with the first 4 bytes representing the
        ' length of 8, it has been found that that Access will accept it.  The second 4 bytes will be expanded
        ' by Access and filled by it.  The initial four bytes is modified to the new data offset.

        Dim ret As Byte() = New Byte(0 To (prependLength + source.Length) - 1) {}
        Dim lengthBytes As Byte() = BitConverter.GetBytes(prependLength)
        Array.ConstrainedCopy(lengthBytes, 0, ret, 0, lengthBytes.Length)
        Array.ConstrainedCopy(source, 0, ret, prependLength, source.Length)
        Return ret
    End Function

    ''' <summary>Extracts the bytes that define the attached file</summary>
    ''' <param name="fileData">the header prepended bytes array returned by Access</param>
    ''' <returns>the bytes defining the attached file</returns>
    Public Shared Function ExtractFileBytes(fileData As Byte()) As Byte()
        Dim dataOffset As Int32 = BitConverter.ToInt32(fileData, 0)
        Dim databytes(0 To (fileData.Length - dataOffset) - 1) As Byte
        Array.ConstrainedCopy(fileData, dataOffset, databytes, 0, fileData.Length - dataOffset)
        Return databytes
    End Function

    ''' <summary>
    ''' Takes an Access FileData byte array and returns a stream of it contained file.
    ''' </summary>
    ''' <param name="fileData">the attachment data as received from Access</param>
    ''' <returns>MemoryStream constrained to the contained file data</returns>
    Public Shared Function FileDataAsStream(fileData As Byte()) As IO.MemoryStream
        Dim dataOffset As Int32 = BitConverter.ToInt32(fileData, 0)
        Dim datalength As Int32 = fileData.Length - dataOffset
        Return New IO.MemoryStream(fileData, dataOffset, datalength)
    End Function

    ''' <summary>
    ''' Takes FileData Byte() from Access and writes its contained file to the passed stream.
    ''' </summary>
    ''' <param name="fileData">the attachment data as received from Access</param>
    ''' <param name="strm">stream to copy file contents to</param>
    ''' <remarks></remarks>
    Public Shared Sub WriteFileDataToStream(fileData As Byte(), strm As IO.Stream)
        Dim dataOffset As Int32 = BitConverter.ToInt32(fileData, 0)
        Dim datalength As Int32 = fileData.Length - dataOffset
        strm.Write(fileData, dataOffset, datalength)
    End Sub

    ''' <summary>
    ''' Copies entire stream to an Access FileData Byte()
    ''' </summary>
    ''' <param name="strm">source stream to wrap in Access FileData Byte()</param>
    ''' <returns>An Access FileData Byte()</returns>
    ''' <remarks></remarks>
    Public Shared Function FileDataFromStream(strm As IO.Stream) As Byte()
        Dim ret As Byte() = Nothing
        If strm.CanSeek Then
            Dim dataLength As Int32 = CInt(strm.Length)
            strm.Position = 0
            ret = New Byte(0 To (prependLength + dataLength) - 1) {}
            Dim lengthBytes As Byte() = BitConverter.GetBytes(prependLength)
            Array.ConstrainedCopy(lengthBytes, 0, ret, 0, lengthBytes.Length)
            strm.Read(ret, prependLength, dataLength)
        Else
            Throw New IO.IOException("Stream must be seekable.")
        End If
        Return ret
    End Function

    ''' <summary>
    ''' Copies data from read file to an Access FileData Byte()
    ''' </summary>
    ''' <param name="filePath">Full path to source file</param>
    ''' <returns>An Access FileData Byte()</returns>
    Public Shared Function FileDataLoadFile(filePath As String) As Byte()
        Dim ret As Byte() = Nothing
        Dim fi As New IO.FileInfo(filePath)
        If fi.Exists Then
            Dim strm As IO.Stream = IO.File.OpenRead(filePath)
            ret = FileDataFromStream(strm)
        Else
            Throw New IO.FileNotFoundException(filePath)
        End If
        Return ret
    End Function

    ''' <summary>
    ''' Prepares a OleDBCommand with parameters to add an attachment to record
    ''' </summary>
    ''' <param name="connection">OleDbConnection to use</param>
    ''' <param name="attachmentFieldname">Name of attachment field</param>
    ''' <param name="tableName">DB Table name</param>
    ''' <param name="attachmentFileData">the raw bytes that comprise the file to attach</param>
    ''' <param name="attachmentFileName">a name for this attachment</param>
    ''' <param name="pkName">the primary key field name</param>
    ''' <param name="pkType">the type of the primary key, typically OleDbType.Integer</param>
    ''' <param name="pkValue">primary key value of the racord to add attachment to</param>
    ''' <returns>prepared OleDbCommand</returns>
    Public Shared Function AddAttachmentCommand(connection As OleDbConnection,
                                                                attachmentFieldname As String,
                                                                tableName As String,
                                                                attachmentFileData As Byte(),
                                                                attachmentFileName As String,
                                                                pkName As String,
                                                                pkType As OleDbType,
                                                                pkValue As Object) As OleDbCommand
        Dim insertCommand As New OleDb.OleDbCommand()

        insertCommand.CommandText = String.Format("INSERT INTO [{0}] ([{0}].[{1}].FileData, [{0}].[{1}].FileName) VALUES (?, ?) WHERE ([{0}].[{2}]=?)", tableName, attachmentFieldname, pkName)
        insertCommand.Connection = connection

        ' Parameter Order: FileData, FileName, pk

        Dim paramData As New OleDbParameter("FileData", OleDbType.Binary)
        paramData.Value = PrependFileDataHeader(attachmentFileData)
        insertCommand.Parameters.Add(paramData)

        Dim paramName As New OleDbParameter("FileName", OleDbType.WChar)
        paramName.Value = attachmentFileName
        insertCommand.Parameters.Add(paramName)

        insertCommand.Parameters.Add(pkName, pkType).Value = pkValue

        Return insertCommand
    End Function

    ''' <summary>
    ''' Prepares an OleDBCommand that removes the specified attachment from a record.
    ''' </summary>
    ''' <param name="connection">OleDbConnection to use</param>
    ''' <param name="attachmentFieldname">Name of attachment field</param>
    ''' <param name="tableName">DB Table name</param>
    ''' <param name="attachmentFileName">the attachment name as received from Access</param>
    ''' <param name="pkName">the primary key field name</param>
    ''' <param name="pkType">the type of the primary key, typically OleDbType.Integer</param>
    ''' <param name="pkValue">primary key value of the racord to delete attachment to</param>
    ''' <returns>prepared OleDbCommand</returns>
    Public Shared Function DeleteAttachmentCommand(connection As OleDbConnection,
                                                                attachmentFieldname As String,
                                                                tableName As String,
                                                                attachmentFileName As String,
                                                                attachmentFileData As Byte(),
                                                                pkName As String,
                                                                pkType As OleDbType,
                                                                pkValue As Object) As OleDbCommand

        ' Note:  Eventhough Access acts like FileName is the pk for attachments, 
        ' we need to include filedata in contraints, orelse all attachments for record are deleted.
        Dim deleteCommand As New OleDbCommand()
        deleteCommand.CommandText =
            String.Format("DELETE [{0}].[{1}].FileData From [{0}] WHERE ( ([{0}].[{2}] = ?) AND ([{0}].[{1}].FileName = ?) AND ([{0}].[{1}].FileData = ?) )",
                                tableName, attachmentFieldname, pkName)
        deleteCommand.Connection = connection

        ' Parameter Order: pk, FileName
        Dim paramPK As OleDbParameter = deleteCommand.Parameters.Add(pkName, pkType)
        paramPK.Value = pkValue

        Dim paramName As New OleDbParameter("FileName", OleDbType.WChar)
        paramName.Value = attachmentFileName
        deleteCommand.Parameters.Add(paramName)

        Dim paramData As New OleDbParameter("FileData", OleDbType.Binary)
        paramData.Value = attachmentFileData
        deleteCommand.Parameters.Add(paramData)
        Return deleteCommand
    End Function
End Class ' Attachments

Public NotInheritable Class Connect 
    Public Shared Function ConnectionString(dbFilePath As String) As String
        Return String.Format("Provider=Microsoft.ACE.OLEDB.12.0;Data Source='{0}';Persist Security Info=True", dbFilePath)
    End Function
End Class

For the purpose of the demo usage code shown below based on your table schema, the above classes are in the namespace AccessUtilities. The Button handler adds a new record and an attachment. The Button2 handler demonstrates how to delete an attachment. I have never figured out how to modify (Update) an attachment. If you need to do that, just delete the existing one and add a new one with the revised info.

    ' Table Schema
    ' Field                 Type            
    ' --------------        --------------------
    ' ID                        AutoNumber
    ' ClientName            Text
    ' Attachments           Attachment

Public Class Form1
    Const pkName As String = "ID"
    Const pkDataType As OleDb.OleDbType = OleDb.OleDbType.Integer
    Const attachmentFieldName As String = "Attachments"
    Const tableName As String = "MyTable"
    Const dbPath As String = "C:\Users\UserName\Documents\DBs\Access\AttachmentDemoTest.accdb"

    ' adds a empty record and returns its PK (ID)
    Private Function AddNewRecord(clientName As String) As Int32
        Dim ret As Int32
        Using conn As New OleDb.OleDbConnection(AccessUtilities.Connect.ConnectionString(dbPath))
            Dim insertCommand As New OleDb.OleDbCommand()
            insertCommand.CommandText = String.Format("INSERT INTO {0} (ClientName) VALUES (?)", tableName)
            Dim paramDesc As OleDb.OleDbParameter = insertCommand.Parameters.Add("Description", OleDb.OleDbType.VarWChar)
            paramDesc.IsNullable = True
            paramDesc.Value = DBNull.Value
            insertCommand.Connection = conn

            conn.Open()
            ret = insertCommand.ExecuteNonQuery

            If ret = 1 Then ' one record inserted, retrieve pk
                insertCommand.CommandText = "Select @@IDENTITY"
                insertCommand.Parameters.Clear()
                ret = CInt(insertCommand.ExecuteScalar)
            End If

            conn.Close()
        End Using
        Return ret
    End Function

    Private Sub AddRecordAndAttachment(clientName As String, fileBytes As Byte(), fileName As String)
        Dim id As Int32 = AddNewRecord(clientName)
        ' adding an attachment only needs a way to refer to the record to attach to
        ' this is done using the pk (ID)
        Using conn As New OleDb.OleDbConnection(AccessUtilities.Connect.ConnectionString(dbPath))
            Dim attachCommand As OleDb.OleDbCommand = AccessUtilities.Attachments.AddAttachmentCommand(conn, attachmentFieldName, tableName, fileBytes, fileName, pkName, pkDataType, id)
            conn.Open()
            Dim result As Int32 = attachCommand.ExecuteNonQuery()
            conn.Close()
        End Using
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ' demo adding new record and attachment
        Dim fileBytes As Byte() = IO.File.ReadAllBytes("C:\Users\UserName\Pictures\BottleTop.png")
        AddRecordAndAttachment("Fred Inc.", fileBytes, "FredsPic.png")
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click

        Dim id As Int32 = 1 ' assumed - change as needed

        ' need to retrieve FileData and FileName from an attachment
        Using conn As New OleDb.OleDbConnection(AccessUtilities.Connect.ConnectionString(dbPath))
            ' retrieve all attachments for id
            Dim selectCommand As New OleDb.OleDbCommand
            selectCommand.CommandText = String.Format("Select {0} From [{1}] Where [{1}].[{2}]=?", AccessUtilities.Attachments.ExpandAttachmentFieldNames(attachmentFieldName, tableName), tableName, pkName)
            selectCommand.Parameters.Add("ID", OleDb.OleDbType.Integer).Value = id
            selectCommand.Connection = conn
            Dim dt As New DataTable
            Dim da As New OleDb.OleDbDataAdapter(selectCommand)
            da.Fill(dt)

            If dt.Rows.Count > 0 Then ' we have attachments
                ' delete the 1st attachment retrieved
                Dim accessFileData As Byte() = CType(dt.Rows(0).Item(String.Format("{0}.{1}.FileData", tableName, attachmentFieldName)), Byte())
                Dim accessFileName As String = CStr(dt.Rows(0).Item(String.Format("{0}.{1}.FileName", tableName, attachmentFieldName)))

                Dim deleteCommand As OleDb.OleDbCommand = AccessUtilities.Attachments.DeleteAttachmentCommand(conn, attachmentFieldName, tableName, accessFileName, accessFileData, pkName, pkDataType, id)
                conn.Open()
                Dim result As Int32 = deleteCommand.ExecuteNonQuery()   ' if = 1 then attachment deleted
                conn.Close()
            End If
        End Using

    End Sub

End Class

You may find the following article of interest as well.

Using multivalued fields in queries

TnTinMn
  • 11,522
  • 3
  • 18
  • 39