Editing a record immediately after creating it - vba

For my paste function, in certain cases, I need to be able to create a record and immediately edit it afterwards. This is because I need to know the value that gets generated for the AutoNumber field.
The error I get is "Update or CancelUpdate without AddNew or Edit." I marked where this error pops up in my code sample.
I pasted my entire paste function in case that it might help. Though the code that I am unsure of how to properly execute is in the bottom 4th (everything below the ***).
If you want to know exactly what I am trying to do feel free to read the rest of the post though it should be sufficient without.
Essentially what I am trying to do is for each record in my clipboard, I want to make a duplicate of it - copying values of all the fields. There are exceptions however. The ones of interest are the AutoNumber field, "ESDNodeID", and "ParentID" which is the ESDNodeID of the record that the record inherits from.
The clipboard (which contains the ESDNodeID and the ParentID) of the nodes that already exist which are being copied are sorted so that if a child record has a parent record, its parent is the next one in the list. So my idea is that I can use the AutoNumber value that gets generated for the record's id to find out what it will be for its parent id (which should just be its id + 1 since it is next in the for loop).
Public Function Paste(nodeID As Long)
Dim currScenarioID As Long
Dim i As Long
Dim saveParentIDs As Collection
Set saveParentIDs = New Collection
currScenarioID = Forms("Main")!Scenarios!ScenarioID
Dim rstSource As DAO.Recordset
Dim rstInsert As DAO.Recordset
Dim fld As DAO.Field
'We want to insert records into the ESDNodes table
Set rstInsert = CurrentDb.OpenRecordset("ESDNodes")
'We want to insert a record for each element in the clipboard
For i = 0 To UBound(clipboard)
'rstSource represents the record that we want to copy. Should only be 1 as ESDNodeID is unique.
Set rstSource = CurrentDb.OpenRecordset("SELECT * FROM ESDNodes WHERE ESDNodeID = " & clipboard(i)(0))
rstSource.MoveFirst
With rstInsert
'create a new record
.AddNew
'Want to copy all the fields
For Each fld In rstSource.Fields
With fld
If .Name = "ESDNodeID" Then
'Skip Autonumber field
'If the field is the ParentID
ElseIf .Name = "ParentID" Then
'If the clipboard has a NULL value that means the node selected is the Parent
If IsNull(clipboard(i)(1)) Then
rstInsert.Fields(.Name).value = nodeID
'If the parent ID has already been created for another node, we want to grab that ID
ElseIf Contains(saveParentIDs, CStr(clipboard(i)(1))) Then
rstInsert.Fields(.Name).value = saveParentIDs(CStr(clipboard(i)(1)))
'If neither of these conditions pass, the parentID is handled after the for loop
End If
'We want the active scenario id
ElseIf .Name = "ScenarioID" Then
rstInsert.Fields(.Name).value = currScenarioID
'Copy all other fields direcly from record
Else
rstInsert.Fields(.Name).value = .value
End If
End With
Next
'If the parent ID was not set above, that means we have not yet created the record corresponding to its parentID
'But because of how our clipboard is sorted, it will be the next one in the loop. Meaning that we can create this new record
'with an empty parentID, and then predict the id of its parent by simply adding 1 to its id
'*****************
.Update
.MoveLast
If Not IsNull(clipboard(i)(1)) Then
If Not Contains(saveParentIDs, CStr(clipboard(i)(1))) Then
!parentID = !ESDNodeID + 1 'ERROR HERE
saveParentIDs.Add !parentID, CStr(clipboard(i)(1))
.Update
End If
End If
.Close
End With
Next i
loadESDTreeView
End Function

You should use method .Edit before changing existing recordset fields.
Also don't close rstInsert inside For, it will fail on next row.
Few more things.
May be I didn't catch the whole idea, but if you moving to last row before editing it in order to just read ESDNodeID, generated after .AddNew, you don't need to use .Update with .MoveLast, you can read the new Id right after .AddNew, it is available.
Predicting of autonumber field value is bad idea, especially in multiuser environment.
Database integrity is very important, so ParentID should have foreign key constrain on ESDNodeID, in this case database won't allow you to insert not existing yet ESDNodeID. Try to review the logic of this procedure.

Related

Runtime error 3164 for Access when copying [duplicate]

I am having a difficult time how to properly copy specific field data from previous records on my user form. I don't have a code sample to show but my request is very simplistic.
Currently, out of 12 fields, I have 6 that I often repeat data. I can click on and press Ctrl+' ("Insert the value from the same field in the previous record") and it performs the task I want. However, it adds a lot of time to the task. I simply want to write VBA code to perform that command to those specific fields.
I haven't been able to get SendKeys to work. DLast appears to provide random data at times. I feel like this should be a very simple request but for some reason I am not finding a functional solution for it.
Don't fiddle with arrays or queries - use the power of DAO:
Private Sub CopyButton_Click()
CopyRecord
End Sub
If a record is selected, copy this.
If a new record is selected, copy the last (previous) record.
Private Sub CopyRecord()
Dim Source As DAO.Recordset
Dim Insert As DAO.Recordset
Dim Field As DAO.Field
' Live recordset.
Set Insert = Me.RecordsetClone
' Source recordset.
Set Source = Insert.Clone
If Me.NewRecord Then
' Copy the last record.
Source.MoveLast
Else
' Copy the current record.
Source.Bookmark = Me.Bookmark
End If
Insert.AddNew
For Each Field In Source.Fields
With Field
If .Attributes And dbAutoIncrField Then
' Skip Autonumber or GUID field.
Else
Select Case .Name
' List names of fields to copy.
Case "FirstField", "AnotherField", "YetAField" ' etc.
' Copy field content.
Insert.Fields(.Name).Value = Source.Fields(.Name).Value
End Select
End If
End With
Next
Insert.Update
Insert.Close
Source.Close
End Sub
This also, by the way, is an excellent example of the difference between the RecordsetClone and the Clone of a recordset - the first being "the records of the form", while the second is an independant copy.
This also means, that the form will update automatically and immediately.
Provided that it's a simple form to edit a simple table, and that the bound data field names match the control names, you may get away with
If Me.Recordset.AbsolutePosition > 0 Then
With Me.Recordset.Clone()
.AbsolutePosition = Me.Recordset.AbsolutePosition - 1
Dim control_name As Variant
For Each control_name In Array("field1", "field2", "field3", "field4", "field5", "field6")
Me.Controls(control_name).Value = .Fields(control_name).Value
Next
End With
End If
which you assign to a separate button on the same form.
You have a good idea post here already.
You could also say place a function in the before insert event. This event ONLY fires when you start typing into a NEW reocrd, and it becomes dirty.
So, maybe this:
Private Sub Form_BeforeInsert(Cancel As Integer)
Dim rstPrevious As DAO.Recordset
Dim strSQL As String
strSQL = "SELECT TOP 1 * FROM tblPeople ORDER BY ID DESC"
Set rstPrevious = CurrentDb.OpenRecordset(strSQL)
' auto file out some previous values
If rstPrevious.RecordCount > 0 Then
Me.Firstname = rstPrevious!Firstname
Me.LastName = rstPrevious!LastName
End If
End Sub
And some good ideas in say having a "list" or "array" of controls/fields to setup, so you don't have to write a lot of code. (as suggested in the other post/answer here)

MS Access VBA equivalent to Ctrl+'?

I am having a difficult time how to properly copy specific field data from previous records on my user form. I don't have a code sample to show but my request is very simplistic.
Currently, out of 12 fields, I have 6 that I often repeat data. I can click on and press Ctrl+' ("Insert the value from the same field in the previous record") and it performs the task I want. However, it adds a lot of time to the task. I simply want to write VBA code to perform that command to those specific fields.
I haven't been able to get SendKeys to work. DLast appears to provide random data at times. I feel like this should be a very simple request but for some reason I am not finding a functional solution for it.
Don't fiddle with arrays or queries - use the power of DAO:
Private Sub CopyButton_Click()
CopyRecord
End Sub
If a record is selected, copy this.
If a new record is selected, copy the last (previous) record.
Private Sub CopyRecord()
Dim Source As DAO.Recordset
Dim Insert As DAO.Recordset
Dim Field As DAO.Field
' Live recordset.
Set Insert = Me.RecordsetClone
' Source recordset.
Set Source = Insert.Clone
If Me.NewRecord Then
' Copy the last record.
Source.MoveLast
Else
' Copy the current record.
Source.Bookmark = Me.Bookmark
End If
Insert.AddNew
For Each Field In Source.Fields
With Field
If .Attributes And dbAutoIncrField Then
' Skip Autonumber or GUID field.
Else
Select Case .Name
' List names of fields to copy.
Case "FirstField", "AnotherField", "YetAField" ' etc.
' Copy field content.
Insert.Fields(.Name).Value = Source.Fields(.Name).Value
End Select
End If
End With
Next
Insert.Update
Insert.Close
Source.Close
End Sub
This also, by the way, is an excellent example of the difference between the RecordsetClone and the Clone of a recordset - the first being "the records of the form", while the second is an independant copy.
This also means, that the form will update automatically and immediately.
Provided that it's a simple form to edit a simple table, and that the bound data field names match the control names, you may get away with
If Me.Recordset.AbsolutePosition > 0 Then
With Me.Recordset.Clone()
.AbsolutePosition = Me.Recordset.AbsolutePosition - 1
Dim control_name As Variant
For Each control_name In Array("field1", "field2", "field3", "field4", "field5", "field6")
Me.Controls(control_name).Value = .Fields(control_name).Value
Next
End With
End If
which you assign to a separate button on the same form.
You have a good idea post here already.
You could also say place a function in the before insert event. This event ONLY fires when you start typing into a NEW reocrd, and it becomes dirty.
So, maybe this:
Private Sub Form_BeforeInsert(Cancel As Integer)
Dim rstPrevious As DAO.Recordset
Dim strSQL As String
strSQL = "SELECT TOP 1 * FROM tblPeople ORDER BY ID DESC"
Set rstPrevious = CurrentDb.OpenRecordset(strSQL)
' auto file out some previous values
If rstPrevious.RecordCount > 0 Then
Me.Firstname = rstPrevious!Firstname
Me.LastName = rstPrevious!LastName
End If
End Sub
And some good ideas in say having a "list" or "array" of controls/fields to setup, so you don't have to write a lot of code. (as suggested in the other post/answer here)

Problem with RecordCount in MS-access 2010 VBA

I am trying to understand what I have done wrong in the code snippet below. I am reading some session records with a specific Client ID and putting the records returned into LineGrid for further processing. The problem is with the line NoL = Rs.RecordCount The query returns multiple lines (I can see this in the development environment). So in one example NoL is assigned the value 1 even although Rs.Recordcount is larger (3). I can check this directly in the debug environment.
At risk of stating the obvious even is record count is 3 only the one row is inserted into LineGrid
Dim Db As DAO.Database
Dim RsCL As DAO.Recordset 'ClientSession
Dim NoL As Integer
Dim LineGrid As Variant
Set RsCL = Db.OpenRecordset("Select * From ClientSession WHERE ClientID = " & CDID)
If RsCL.EOF Then
MsgBox ("Nothing to Invoice")
Exit Sub
End If
'RsCL.MoveFirst
NoL = RsCL.RecordCount
LineGrid = RsCL.GetRows(NoL)
RsCL.Close
I have tried
Changing the data type of NoL to Long - No effect
Putting Rs.Count in the place of (NoL) in GetRows property - No effect
If I replace NoL with a value e.g. 100 then the code works fine then all records are loaded into LineGrid but I don't have the true record count.
I have tried moving the record pointer with .movefirst property ( you will note that line is currently commented out) - No effect
Ultimately I need LineGrid to contain the data from the table and NoL to have the true record count
When you open a recordset, not all of the records are loaded immediately. In order to force all of the records to load, you need to move to the last record before moving back to the first one:
RsCL.MoveLast
RsCL.MoveFirst
Regards,

Can an individual record in a recordset be moved?

I have a table subform containing jobs to be completed. I'm creating an algorithm in VBA to organize the jobs in the most efficient order to be completed.
Is there a way to move an individual record in a recordset or am I stuck with OrderBy?
Edit:
To add some clarity, I want to be able to move a record to any other index in the same table. I intend to run my algorithm which will move the records into the order they are to be completed. Then each records' "Process Date" field is set to keep track of the order.
The short answer is "No", the index of a record in a recordset cannot be directly updated. The order of rows in a recordset can only be changed by either setting a different ORDER BY clause and requerying the database, or by setting the Recordset.Sort property or the Form.OrderBy property (when bound to a form).
Let's assume that there is a updatable recordset field called [JobOrder]. The SQL source query can include a sort order like ... ORDER BY [JobOrder] ASC which first sorts the data when it is retrieved from the database. As a matter of fundamental database concept, it should be assumed that if no ORDER BY clause is specified that the database can return data in a random order. (In practice that is not usually the case. It will be sorted by some indexed primary key by default, but that should not be assumed if the order is important.)
The form's (or subform's) sort order can be set and changed without requerying the data from the database again. That is done by setting the OrderBy property and ensuring that OrderByOn = True. (FYI: Unless you take measures to hide default tool ribbons (i.e. toolbars) and shortcut menus, this sort order can be altered by the user.)
Now your VBA code can use various techniques to set the JobOrder values. You could perhaps use the Me.RecordsetClone method to enumerate and update the values using the recordset object. Using RecordsetClone will avoid certain side effects of updating the bound primary recordset. Lastly, the following assumes that all records already have valid, unique JobOrder values, but it assumes that JobOrder is not required to be unique (since the swap technique temporarily sets two rows to the same value). It's up to you to write your own clever implementation to guarantee that JobOrder values remain valid and unique.
Private Sub MoveCurrentUp()
Dim rs As Recordset2
Dim thisID As Long
Dim thisSort As Long
Dim previousID As Long
Dim previousSort As Long
On Error Resume Next
'* Error handling to avoid cases where recordset is empty
'* and/or the current record is not valid (i.e. new record)
If Not IsNull(Me.ID.Value) Then
thisID = Me.ID.Value
If Err.Number = 0 Then
On Error GoTo Catch
'* Any errors from this point should be
'* handled specifically rather than ignored
Set rs = Me.RecordsetClone
rs.FindFirst "ID=" & thisID
If Not rs.NoMatch Then
thisSort = rs!JobOrder
rs.MovePrevious
If Not rs.BOF Then
previousID = rs!ID
previousSort = rs!JobOrder
rs.Edit
rs!JobOrder = thisSort
rs.Update
rs.MoveNext
rs.Edit
rs!JobOrder = previousSort
rs.Update
Set rs = Nothing
RefreshSort
End If
End If
Set rs = Nothing
Debug.Print Me.Sort
End If
End If
Exit Sub
Catch:
MsgBox "Error updating order." & vbNewLine & vbNewLine & _
" " & Err.Number & ": " & Err.Description, vbOKOnly Or vbExclamation, "Error"
End Sub
Aferward, you would refresh the form's sort order with something like:
Private Sub RefreshSort(Optional restoreCurrentRecord As Boolean = True)
Dim rs As Recordset2
Dim saveID As Long
saveID = Me.ID.Value
Me.OrderBy = "[JobOrder] ASC"
Me.OrderByOn = True
If restoreCurrentRecord Then
Set rs = Me.RecordsetClone
rs.FindFirst "ID=" & saveID
If Not rs.NoMatch Then
Me.Bookmark = rs.Bookmark
End If
Set rs = Nothing
End If
End Sub
Or you could update rows using SQL queries, then call Me.OrderByOn = False then Me.Requery to force the entire recordset to be reloaded in the proper order (assuming the record source has a proper ORDER BY clause). This technique has the benefit of wrapping all the changes in a transaction which can be committed or rolled back altogether, something you can't do with the bound form's recordset objects.

VBA Adding images to a MS Database as per its corrsponding data in the database entry

This is part of another question which has been resolved, pasting the link here for the convenient of those coming across in the future.
Credit to Erik_von_Asmuth for his help previously.
Using VBA to import a large number of attachment into Microsoft Access
The concept of the code I think that might work:
Sub MacroInsertImageToDatabase()
Dim I As Integer 'number of row in file_paths.txt
Dim J As Integer 'number of entries in the database
For J = 1 To 100
For I = 1 To 100
'Lets say there are 100 lines in file_paths.txt. Something like:
'C:\image_folder/image1.jpg
'C:\image_folder/image2.jpg
'all the way to
'C:\image_folder/image100.jpg
If (string of file_name in column 2 in the database) = (current row in file_paths.txt we are looking at)
Then 'That means there is a match!
[Attach the image from as given from file_paths.txt(we ar looking at) into the 3rd row of the database(we are looking at)]
[also escape this loop through file_paths.txt so we can move onto the next entry in the database to repeat this If statement]
Else 'current row in file_paths.txt we are looking at is NOT what we
[move in the the next "I" iteration to look at the next row in file_paths.txt]
Next I 'successfull attached the image to the correponse entry in the database as per string in the 2nd column (file_name)
Next J 'now move on to the next row (entry) in the database, the "J" loop
End Sub
Or should I exploit the features of MS Access, I am reading the documentation about the "database table relationships". Have 1 table with just the attachments. Have another table with the corresponding file names (and other data). Then use the relationship features of MS Access to link them together.
You can use the following code to add attachments to your table based on the location set in the file_name column
Public Sub MacroInsertImageToDatabase()
Dim db As DAO.Database
Dim rsEmployees As DAO.Recordset, rsPictures As DAO.Recordset, ws As DAO.Workspace
'Initialize database, workspace and recordset
Set db = CurrentDb()
set ws = DBEngine.Workspaces(0)
Set rsEmployees = db.OpenRecordset("Table1", dbOpenDynaset)
Do While Not rsEmployees.EOF
'Open the attachment recordset for the current record
Set rsPictures = rsEmployees.Fields("attachment_column").Value
'If there are no attachments yet
If rsPictures.BOF Then
'Edit the record
rsEmployees.Edit
'Begin a separate transaction for inserting the picture
ws.BeginTrans
'Load the picture
rsPictures.AddNew
rsPictures.Fields("FileData").LoadFromFile rsEmployees.Fields("file_name")
'Update the pictures recordset and commit the transaction to avoid troubles with nested transactions
rsPictures.Update
ws.CommitTrans
rsPictures.Close
'Then update the record (if anything has changed inside it, which it actually hasn't)
rsEmployees.Update
End If
rsEmployees.MoveNext
Loop
rsEmployees.Close
End Sub