I have an access application that has a form that allows the user to enter case notes. The main field of this form is tied to a SQL Server varchar(MAX) field in the source table. Since the users switched to Access 2007, their program keeps crashing when they are on the case notes form. As a possible solution to this problem, I would like to try unbinding this form and re-building it as an unbound form.
This form needs to be able to add and update records into my SQL Server database. It also needs to be able to browse between records. I guess I am at a loss as to where to start. Any suggestions/code snippets is appreciated.
As a starting point, try Google on "unbound form in Access". Don't be distracted by PacMan! ;)
Anyway, the basic idea of an unbound form is to load the data into the unbound controls from a recordset, then to save it back when edits are done. This means you need these things:
controls to select the needed record, some kind of find functionality.
code to open the recordset and write the data from the fields into the corresponding controls on the form.
controls to save the record back to the database, which will use a SQL update to write the values in the unbound controls back to the database. I prefer not to update fields that have not changed (because I do lots of replicated Jet apps, and multiple updates can lead to unnecessary replication conflicts). You can compare the data in the undound controls to the data in the original recordset (if you open it as a snapshot type recordset, it won't reflect any updates since it was opened), and write your SQL UPDATE for only the fields where the values don't match. You'll have to account for Nulls.
The common practice is to name the controls exactly the same as the fields they correspond with so you can loop the recordset's fields collection and load the data into the controls:
For Each fld In rs.Fields
Me.Controls(fld.Name) = fld.Value
Next fld
You can do likewise for saving the data and checking the control values against the original recordset values.
I don't know if this works with SQL Server VarChar() fields or not, but you could also try what I call a "semi-bound" form, where you load the recordset with the form's RecordSource property but don't bind the fields to controls. Thus, the form is bound, but the controls are not. I very often do this with a fully bound form where I make the memo fields unbound (to avoid the danger of memo field pointer corruption in Jet/ACE back ends). In that case, with a bound form recordset and an unbound textbox for editing, you'd do this:
in the form's OnCurrent event, load the unbound field(s) data into the corresponding unbound textbox(es).
in the AfterUpdate event(s) of the unbound control(s), write the data in the unbound textbox(es) back to the recordsource.
Those two steps would basically look something like this:
Private Sub Form_Current()
Me!txtMemo = Me!Memo
End Sub
Private Sub txtMemo_AfterUpdate()
Me!Memo = Me!txtMemo
Me.Dirty = False
End Sub
With a Jet/ACE back end, you'd want to save the record immediately after you write the memo field value because otherwise you haven't avoided the danger of corrupting the memo field pointer. With a SQL Server back end, you may or may not need to do that, since the issues are completely different. Saving will release the write lock, but you might not need to avoid that.
Also, I'm assuming that the VarChar() data can be read from the form's underlying Recordsource and written into the textbox. You'll have to see if that works.
Related
Summary
In a Microsoft Access 2010 database (accdb), I have a form that dynamically loads other forms into a subform object on the main form. The forms used in the subform object are bound to ODBC pass-through queries that execute stored procedures to return recordsets. I can't figure out why I can use one sproc and it works perfectly fine, but if I bind the form to another sproc, it fails to load the subform.
Technical Walkthrough
I have two pass-through ODBC queries. qryGood and qryBad. They use identical ODBC connection strings (ODBC;DRIVER=SQL Server;SERVER=MyServer;UID=MyUser;Trusted_Connection=Yes;DATABASE=MyDatabase), and the SQL behind them is identical, but pointing to two different SQL stored procedures on the SQL 2012 database server.
qryGood source: exec spGoodProc 123456
qryBad source: exec spBadProc 123456
The SQL behind the sproc is very simple. Return records from a single table, filtering by the ID passed as a parameter. (Some will do more complex things, but I am just focusing on a simplified example here that demonstrates the problem.)
The RecordSource property of frmMySubform is set to qryBad.
The subform SourceObject is set via VBA code: sfrmMain.SourceObject = "frmMySubform" No errors are thrown at this point. While the SourceObject property now returns frmMySubform, the .Form object does not seem to be set.
I then try to reference a property on the subform: Debug.Print sfrmMain.Form.Name This fails with error 2467: The expression you entered refers to an object that is closed or doesn't exist.
I can then open frmMySubform in design view, change the RecordSource property to qryGood and it works just fine. This seems to point to a problem with spBadProc that only manifests itself when used as the RecordSource on a subform.
What I Have Tried
In an effort to troubleshoot this problem, I have used the process of elimination to narrow this down as far as I can, but I am still not understanding why the one sproc works and the other doesn't. Both return records just fine in SQL and when running the pass-through query directly. Both work fine when opening the form directly. It only becomes a problem when the form is set as a SourceObject in a subform control.
I have used sp_procedure_params_rowset to compare the parameters in the sprocs, and they are identical. I have compared the data types of the columns in SQL and there is nothing new or different in tblBad that isn't in tblGood. I have also tried profiling the SQL server while setting the form, and it seems to call the sproc just fine. I didn't see any clues when comparing the trace between the bad and the good calls.
Setting the RecordSet directly to an ODBC link to tblBad works just fine (and I presume a view would be fine as well) but having the simple stored procedure wrapper somehow triggers the error.
I have also compared the security, properties and extended properties for spGoodProc and spBadProc and they are identical.
My Question
What can I do on the troubleshooting side to reduce this down further? Has anyone out there encountered similar issues with bound sprocs on subforms? I am working on a very complex database with hundreds of forms, tables and queries, so I would really like to understand why this is occurring before I go too far down this path.
Thanks in advance for any insight you are able to share on this perplexing problem. :-)
Found it!
After tracing it back to something with the specific table, I removed all constraints, keys, and then columns from a copy of the table, systematically testing to see if I could pinpoint the problem. Sure enough, it was a specific column name in the stored procedure!
Simply aliasing this column to a different name solved the problem. (See below for expanded details)
Update after Further Testing
After additional testing to further pinpoint the issue, I think I now understand why this was occurring. When you link an ODBC table and specify a unique (key) column, Access will automatically attempt to set the LinkMasterFields and LinkChildFields to the key column name when a subform is loaded and the subform has a column with the same name. While this works fine with linked tables or views, it does not work when the RecordSource of the subform is set to a stored procedure.
If you attempt to do this by manually adding the subform, you will see the following notification:
However, if you set the subform target through VBA code, you don't get any warning or error message. It simply doesn't (fully) load the subform. #Albert D. Kallal, you were right on about this being related to a master/child fields issue!
I was able to consistently reproduce the issue in a test database file in both Access 2010 and Access 2016. If you would like to see this for yourself, you can use the following steps to reproduce it:
Create a SQL table with a PrimaryID column.
CREATE TABLE [dbo].[tblBugTest](
[PrimaryID] [int] NOT NULL,
[TestColumn] [nchar](10) NULL
) ON [PRIMARY]
Add a couple test records to the table you just created.
Create a Stored Procedure to return the records.
CREATE PROCEDURE [dbo].[spBugExample]
AS SELECT * FROM tblBugTest
Create a blank Microsoft Access database (accdb).
Using ODBC, create a linked table to tblBugTest.
Important: Select PrimaryID as the unique column.
Create a pass-through query named qryPassThrough using ODBC to the same database, and set the SQL to exec spBugExample.
Open the query to verify that it returns records.
Create three blank forms in the database. frmMain, frmSubForm, and frmBlank.
Add frmBlank as a subform to frmMain. Name the subform sfrmSubform.
Set the RecordSource of frmMain to the linked table.
Add a button to frmMain to switch the subform from frmBlank to frmSubForm.
Private Sub cmdShowBug_Click()
With Me.sfrmSubform
.SourceObject = "frmSubForm"
Debug.Print .LinkMasterFields
Debug.Print .LinkChildFields
Debug.Print .Form.Name
.SourceObject = "frmBlank"
End With
End Sub
Set the RecordSource of frmSubForm to qryPassThrough.
Drop a couple bound controls onto frmSubForm.
Test frmSubForm by itself. It should load a record from tblBugTest.
Open frmMain and click the button. It should throw an error.
If you step through the code, you will see that before setting the SourceObject, the LinkMasterFields property is blank. After setting SourceObject, you can hover over LinkMasterFields and see that it is now set to the PrimaryID column.
Workarounds
Changing any of the following will work around the error by avoiding the problematic auto-linking of the master/child fields.
Delete and relink the linked table, this time not specifying a unique column.
Alias the column in the Stored Procedure to a different name than the unique column.
Clear the RecordSource property of the parent form.
Clear the subform RecordSource property and set the RecordSet after loading the subform.
Use a view or linked table instead of a stored procedure in the subform.
Keep in mind that the subform data source will be attempted to be loaded BEFORE the main form loads. What this suggests is that on the main form's load event, you will
First setup the PT query.
Then set the OBJECT source of the sub form.
In other words, the source object of the sub form control should be blank.
You code then to set up the PT query will be:
With currentdb.queryDefs("qryGood")
.SQL = "EXEC spGoodProc " & 123456
end with
Of course you can replace the 123456 with a varible, or even a value from a text box (from the main form).
Now that the PT query is setup, you THEN are to set the form that the sub-form is to load.
So, after above code, we then have:
me.mySubForm.SourceObject = "name of subform goes here"
So, it should be about a total of 4 lines of code. And as above shows, you don't even need any connection string stuff in your VBA code.
So, just keep in mind:
Setup the PT query as per above. You can then launch a report, or even a form, or in this case set the form that the sub-form control is to load. This also suggests/hints that you need to remove the source object of the sub form control (leave it blank).
You can have the sub-from source object set, but then this would suggest that you setup the PT query source as per above BEFORE you launch the main form with the sub form based on the PT query. As noted, this set of steps is required since the sub form actually load and resolves it data source BEFORE the main form displays and renders. So, by leaving the source-object blank for the sub form, then you the developer re-gains complete control over the order of loading.
I have an Access 2010 database that's linked to a SQL Azure backend (yes, I know this isn't ideal, and it's being slowly phased out). On the backend, I have a stored procedure that I want to use to populate a read-only subform each time a new record is loaded. I'm attempting to do this by generating a recordset in VBA, then setting the subform's RecordSet property. It actually works, but with a nasty side-effect.
When I set the RecordSet property, it also seems to be setting the RecordSource property of the subform. That RecordSource is something Access can't parse, because it's meant to be a call to the backend. If I try using a DAO passthrough query to generate the recordset, the RecordSource looks like:
EXEC dbo.GetDuplicateAddressesByManufacturer N'...', N'...', N'...'
If I try using an ADO command to generate the recordset, it looks like:
{ call dbo.GetDuplicateAddressesByManufacturer ?, ?, ? }
As soon as I try to move to the next record, Access throws an error because it tries to load a new record for the subform first, and it can't open what it sees as the subform's RecordSource. If I'm trying the DAO route, it tells me "Invalid SQL Statement", and if I'm using ADO, it tells me "Data provider could not be initialized."
Anyone have an idea how I can get around this? Isn't there a way to set the RecordSet property without also setting the RecordSource? I could swear I've done that before, but maybe I've just never noticed that the RecordSource gets set too.
Failing that, is there a way I can interject some code before the Form_Current event to remove the subform's RecordSource? The code I'm using to set the RecordSet each time works great -- the problem is the error that's raised before my code works. Once I dismiss the error message, everything works fine, but obviously I don't want users to get an error message each time they change records.
If all else fails, I guess I could always use the query to populate a local temp table, but it seems like a lot of overhead to do every single time someone moves to a new record.
Why bother with a stored procedure? Just link the sub form to the table, and setup the link master/child settings. You only pull down the required records.
If the sub form is a complex query with multiple tables, then you certainly want the data joins etc. to occur server side and AGAIN simply create a view and again set the sub form source to that view (and again the link master/child settings will do all the dirty work for you).
And there no reason why you cannot create a pass-through query and SIMPLE assign that to the forms recordSource.
It does not matter what “junk” you place inside of the query, and that includes RAW T-SQL.
And while you can load up the DAO reocrdset with this pass-thought, you really don’t need to. I suppose for some happy reason you are doing this, and at least if you must, then recordSoruce becomes the name of the pass-though and NOT your raw T-SQL anyway.
However, really, just dump all that recordset junk, and just go:
Me.MySubForm.Form.RecordSource = "my pass though query".
Thus above is only one line.
You doing all these hand stands to increase performance then at the end of the day why does your main form allow navigation? You should build a search screen, display the results, let user pick a row and THEN launch the main form to edit/display the ONE record along with the sub form data.
When they close, they are back to searching for the next customer etc. This approach also thus solves your messy navigation issues. It also why the web and most software works this way (it reduces bandwidth issues).
However, if you must have navigation, and for some reason CANNOT use a view and cannot let Access use the link master/child settings to do your dirty work?
Then in the forms on-current event you can modify the pass-though and simply re-assign it to the sub form.
Eg:
With CurrentDb.QueryDefs("qPass")
.SQL = "select * from FaxBook3 where id = 3"
End With
'
Me.RecordSource = "qpass"
Now how in the above I am using RAW T-SQL in the pass-though query, and then simply assign the pass-though to the forms recordsource (in your case you assign to the sub form.
Me.MySubForm.Form.RecordSource = above
And there NO reason why the above .SQL cannot be your stored procedure
.SQL = "Exec your-storedProcedure " & strVbaStringParmater
And again assign the form (or sub form) recordSource.
So you REALLY do not need to create some reocrdset in code and it not yield you any performance increase, but will cause you to write more code and have problems as you outlined in your post.
BACK END - SQL Server
FRONT END - Access 2010 (2000 format)
The system stores and retrieves data about technical documents. Broadly, there are three tables A, B and C, each of which maintains data about a different type of document.
The ACCESS front end provides a Search Form and Data Entry/Edit form (bound to the underlying table) for each document type. In all three document types, when adding a new record, the user will open the Search form and press a button called "Add". This opens the Data Entry/Edit form and in the Form_Load event is the line
DoCmd.GoToRecord , , acNewRec
When the data entry is complete, the user presses a "Close" button which simply runs the code
DoCmd.Close
As I said, the design and code of the objects relating to the three document types is, for all intents and purposes, identical. However, while for tables A and B the process of adding a new record is seamless and extremely quick, for table C it has proved impossible to add a new record via the ACCESS UI. The edit form will open correctly to add the data, but when the user presses the "Close" button the form hangs, and eventually returns to the Search form without the new record having been added.
It is possible to bypass the UI by opening ACCESS while holding down the SHIFT key, opening the linked table, and adding new records directly. While this is acceptable as an interim measure, it is unacceptable in the long term. It should be noted that the system is about ten years old, and has been working entirely correctly for about nine of those years (apart from minor glitches moving between different versions of ACCESS).
Unfortunately this system is owned and operated by a major global corporation and it is very difficult for me, a subcontracted supplier, to get access to the SQL Server box to run diagnosis (SQL Profiler would be a good starting point). My gut feeling is that there is a subtle difference in the permissions model for that particular table but I don't know.
The situation is further complicated by the fact that I have a copy of the system at my work and I cannot reproduce the problem. Of course, there are bound to be subtle differences between the two architectures (for example, I don't know for certain what version of SQL Server it's running on, but I believe it's 2000, nor do I know how completely it is patched or updated) but the facts are that for one particular table bound to one particular form, it is not possible to add records, whereas for other tables there is no such problem.
I would be grateful if anyone has any ideas about how to go about diagnosing this or even solving it (if anyone has come across the same problem before).
Many thanks
Edward
As a general rule when you encounter problems to update a table, then this tends to suggests that the table does not have a PK or the form the query is based on does not have a PK exposed.
The next thing I would ensure is the table has a time stamp column as Access uses this to test for record changes behind the scenes.
Next up I would check the default locking for the form (while these settings generally don't effect odbc, they should be checked).
Next up is to check if the table has any "bit" column (true/false) and ensure that the defaults for such controls are set SQL SERVER side (they should default to 0). This null bits issue will cause updates to fail if not addressed.
I would also check if the form in question is based on a query or if the data source is set directly to the table. As noted the PK auto number ID of that table in query should be INTEGER value sql side – big int is NOT supported.
So check default values (both in sql table and on the form (those controls) to ensure nothing be set that would prevent the update.
I've been working with a Legacy application which interacts with a database through ADODB, and most of the changes to records follow a fairly straightforward pattern of:
Create a Recordset from a query
Make various change to the recordset
call .Update on the recordset.
What I'm wondering is, with ADODB recordsets, is there anyway to extract the 'changes'. The logic which changes the recordset is scattered about, and all I need is the changes, not how it was changed...
Any suggestions for tracking changes in a recordset (in code, a trigger on the DB or similar is no use here)
I personally have never used this functionality, but the documentation states you can set rs.Filter property to adFilterPendingRecords to show records that have been changed but not sent to the server yet (only applies to batch update mode).
Or, you could iterate through all records in a recordset, and if the .Status property has the adRecModified flag set, then you can compare .Value and .UnderlyingValue of each of the fields to see whether they are different.
We have a Access-db with linked (odbc) tables on an sql-server. Some times there is a rare problem with one column of a particular row: no more changes in the field of type memo are possible.
Changes in other columns of this particular rows are saved as normal.
The error-msg goes something like: annother application has changed the row and therefor the update has been canceled.
what could be the reason, what can be done to prevent this behaviour?
Peace
Ice
Update:
The mdb is definitively not corrupt. There are only the odbc-connections inside, we use it in read-only mode. The issue must be between the jet-engine and the odbc-driver respective the sql-server, that is what i think.
Peace
Ice
If your data were stored in a Jet/ACE back end, I'd say you likely have a corrupted memo pointer. Since your data is in SQL Server and accessed via ODBC, that can't be the answer. But since others might come to this discussion who are encountering the problem with a Jet/ACE back end, the below might be helpful:
In Jet/ACE tables, memo data is not stored inline with the the other fields. Instead, the data is stored in separate data pages elsewhere and all that is stored inline is a pointer to the first of the external data pages. That pointer is susceptible to corruption and is a frequent cause of lost data.
Some links from Tony Toews (the best source for this):
General Reference Source on Jet/ACE Corruption
Corruption Symptoms
In that second one, search for 3197, which is likely the error number of the problem you're encountering. There's a link there that explains how to troubleshoot it.
After you've fixed it, you should consider restructuring your data tables to minimize the risk of memo field corruption.
I know you are not using Access, but for Access forms, one solution is to avoid bound memo fields, and instead edit them in unbound textboxes. In the Access form's OnCurrent event, you'd copy the memo data from the form's fields collection into the unbound textbox for it, and in the textbox's AfterUpdate event, save it back to the form's underlying recordset.
for all applications, Access or not, putting the memos in a separate table segregates the memo field pointer from the rest of the data. If you have one memo, it can be a 1:1 table, and if you have multiple memos, you would have a 1:N and the memo table would have to have a field to indicate the memo type. With that structure, the main record does not have to be deleted and recreated to fix a corrupted pointer -- all you have to do is delete the corrupted record in the memo table.