I recently posted this answer as a way to get user input when creating a MS Access query or report. The general behaviour is that if an undeclared variable is placed in SQL query code or a report (e.g. [UndeclaredVariable]), a small input/dialog box appears for the user to input the variables value.
I have been unable to find any mention of this functionality in documentation or elsewhere. All discussion is about using InputBox() in the standard way.
This functionality is unexpected/unusual for several reasons:
(In my limited knowledge) Using undeclared variables in MS Access generally causes an error
The input/dialog box is different than the one created when InputBox() is used
The functionality seems to transcend standard behavior (e.g. when an two undeclared variable are used in this way as the "ifTrue" and "ifFalse" components of an IIf() statement, BOTH dialog boxes are created sequentially!)
Does anyone know what this functionality is called or why it works in these ways?
To summarize the above comments:
the behavior is called a "parameter query" and is like normal parameterized queries (see here)
The behavior with IIf() is because Access requires a parameter to be given whether or not the value is used (in this case for both [ifTrue] and [ifFalse])
There seems to be no way to conditionally parameterize a query or report
Ok, as to why they work the way they do? Well a lot of people use Access for just working with data tables. They don't necessary build forms and reports. So, if you place a [parm] box in a query, it will prompt you, and you don't need code. So, a really handy feature.
And of course if you do build a report, and need a quick and dirty way to prompt for some criteria then once again, super handy, and again no code or forms need be written.
And if you do create a form, then you can place criteria in a query like this:
select * from tblCustomers where City = Forms!MyPromptForm!City
once again, no code. And using a forms! expression like above means no prompt when you run the query (or a report based on that query).
The only real downside of using these parameters is they are not without effort to make them optional.
As you are finding out, putting forms! expression in queries can get really messy real fast. And such queries can prompt you when you don't want to.
Even worse, is now that the query is now "married" and attached to that ONE form (if you use forms! expressions for parameters). Often, I have a nice query that I could use MANY times for different reports, and often even that same query could be used for reports...but then someone comes along and puts in a expression or paramter(s) that means the query is ONLY good when that form is opened, or you get nasty prompts for some parameter that you now don't want. Or you want DIFFERENT criteria on a different column.
So hard coding the number of parameters in such a query is REALLY painful, not flexible, and really starts to fall down.
So, if you have 5 combo boxes on a form, but the user only selects restrictions in 3 of the combo boxes...and wants the other 2 to be ignored?
I could probably write another 10 or pages as to why putting forms expressions in queries or shoving in parameters is bad (besides...it makes the queries real ugly, and hard to read. and, the sql then is not standard anymore (it will not work with server based systems either).
So, the solution use now is simply to take the values from the form, and build your own where clause in code. That way, you simply design the reports (or forms), and attached them to the query, BUT NO FORMS! conditions are placed in the query. And the SQL is clean without garbage in it.
To "send" the conditions to the report (or form), you simply use the "where" clause. This is exactly why ms-access has this feature…and it solves a zillion problems…and will reduce your development costs by a substantial amount.
Take a look at the following screen shots to see what I mean:
So in above we have a start date, and end date. And the user can select 1, or several tours and THEN click on the report view button. Trying to deal with a easy and clean "prompt" form for user and THEN attempt to use a query with parmaters is sheer folly.
The code to make those above screens work and launch the report with the selected restrictions when you hit the "print" button is easy:
Or take this screen:
So in the above we have a combo box to select the sales rep.
But, you can leave it blank.
So, code to deal with above will work like this:
dim strWhere as string
'select sales rep combo
if isnull(cboSalesRep) = false then
strWhere = "SalesRep = " & cboSalesRep & ""
end if
' select "status" for the report
if isnull(lstStatus) = false then
if strWhere <> "" then
strWhere = strWhere " and "
end if
strWhere = strWhere & "Status = '" & litStatus & "'"
end if
Note how the 2nd list box is checked for a value. You can add as "many" more conditions you want. Lets say we have a check box to only include Special Customers. We can add to our very nice prompt screen a check box to
[x] Show Only Special customers
The code we add would be:
if chkSpeicalOnly = True then
if strWhere <> "" then
strWhere = strWhere " and "
endif
strWhere = strWhere & "SpecialCust = true"
end if
For sure, each combo and control we add to the nice report screen takes a bit of code, but no more messy then the query builder..and this way, each query is nice and clean, and free of a bunch of HIGHLY un-maintainable forms! expressions or "optional" paramters.
Further, it means you can re-use the same query for different reports, and have no worries about some form that is supposed to be open or some nasty out of the blue prompt for a parameter. So, a tiny bit more code eliminates the messy query problem.. For me, this is very worth while trade.
So parameters are quick and dirty. But if REALLY going to start building a decent user interface and some nice report prompt forms? Dump the use of parameters - you be glad you did. And you only have to go to the ONE code spot, add another code block for a new and optional critera. You don't have to change the report, and and you don't have to go and modify the SQL either. So, a bit of code goes a LONG way to attacking this problem.
At the end of your code, we now have a valid SQL where clause. So, now just launch the report with with this:
Docmd.OpenReport "ReportName",acViewPreview,,strWhere
And the above also works for searching and launching a form. So the form can be based on such queries, and again no prompts or paramters means the form can be used to edit that data.
So the concept of filter data and launching forms (or reports) can and should be detached from the concept of some query that pulls data from a table. Over time, software will change, and then the boss will ask how about a filter or prompt for invoice number, but that again has to be optional. Over time your business rules will change. And you can often apply that filter to "many" reports. So in this form, the critea is built up, but the list box allows you to select one of "many" reports, and not all are based on the same SQL, but they all accept the same "filter" or better term used is the "where" clause of the open report or form command.
In above, the user can hold down the ctrl-key to select 1 or 2 or hit the "select all" button. In that form, the user is actually selecting the bus tour ID for a given tour. And then they can choose the report in the lower left. Again, attempting to acheive the above multiple critera simply can't work with sql paramets anyway.
I am using a TourID = some value and Busid in (LIST OF buses selected).
once again, you can simply pass that complex "where" clause to the report - and that where clause can even have a sub-query, or a Field in (LIST OF ID'S) criteria (which can't be done with parameters anyway).
Related
I am working on a very complex legacy ADP project in Microsoft Access that connects to a 2008 SQL Server back end. A common usage scenario is a form in datasheet view that contains an editable recordset.
The approach used through most of the database is to load the form, then build the SQL dynamically in VBA using form parameters, and apply the SQL as the RecordSource for the form. Many of the queries are quite complex, so this makes for really ugly code and a maintenance nightmare.
For the read-only recordsets, I have converted the dynamic sql to parameterized stored proceedures, which works great. Nice and clean, and easy to maintain going forward.
But for the editable recordsets, I am trying to determine the best approach:
1. Dynamic SQL - As mentioned above, I would really like to avoid this approach.
2. SQL View - The challenge here is that some of the tables are very large, so if I try to load the view and then filter it on the form, it has to pull the entire recordset from the SQL server, even though I only need a small number of rows. (Negative performance and IO impact.)
3. Use context_info - This sounds intriguing, but does not sound like a recommended approach based on discussion here: Create parameterized VIEW in SQL Server 2008 If I was developing against SQL Server 2016 I might look more into SESSION_CONTEXT.
4. Parameter Table with View - This is an idea that I am leaning towards. I would create a Parameters table in SQL, and set the parameter value (as a key/value pair) with the session ID. The view would then filter based on the current value in the parameter table. This would allow me to use a view as my RecordSource to support the edits, but the filtering would take place on the SQL Server side.
Is the parameter table indeed the best approach to take with this project, or is there another way that I could access a parameterized read-write recordset that is filtered on the server side?
I assume you talking about a non adp project now.
Even if you don’t use a view, and say bind a form directly to a linked table of 1 million rows, then access will ONLY pull down the records you requests. You simply just use the forms “where” clause of the open Form command.
So you don’t even have to use dynamic sql here.
However, you don’t want to launch a form bound to large table UNLESS you set the where clause.
You can certainly open a form without a reordsource, have the user enter some parameter values into a text box, and then go:
Dim strSQL as string
strSQL = "select * from tblCustomers where InvoiceNum = " & me.MyInvoiceTextBox
Me.RecordSoruce = strSQL
However, in most cases you better to create some type of search form. Let the user enter some values, display the results like this:
So in above, the person type in "smi". You display the results
(and in above I did use the above approach of stuffing the sql directly into the forms reocrdsource
Now on the edit buttons along the side to launch + edit one record,
I simply go:
Docmd.OpenForm "frmCustomer",,,id = & me!id
Once again, EVEN if the form is bound directly to the linked SQL server table, only the ONE record will be pulled from SQL server. So no messing with sql, no messing with parameters etc. is required.
So a regular non ADP access application with linked tables DOES NOT pull the whole table.
You can also after opening a form set the forms filter – and again access will ONLY pull the reocrds in question from the linked sql table. It is a “common” myth that access pulls all records – it does not if you provide a filter, and I recommend you open a form to one record, let the user work then close the form and return back to some search screen in which you ready to do battle with the next customer etc.
so provide a search form - don't recommend having the form to edit data be all cluttered up with the ability to search records. Let the user edit, and then close the form - this also promotes the record being saved after the user done working.
edit:
For a form that has any kind of complex joins etc., then create a view, and bind the form to that view. You use the forms "where" clause, then once again access will only pull down the one record. So for complex joins etc., yes access can often mess that query up and it runs slow. So if the form is bound to one table (that is MOST cases), then bind the form directly to the linked table. If the sql is complex, then bind the form to the linked view, and as noted in either case ALWAYS provide a "where clause" to the openform command - it will in these cases ONLY pull the one record into the form. And once again, no messy parameters, no messy sql is required on the access side - you will save MASSIVE amounts of coding if you adopt this approach, and you also get stellar performance since you limiting the reocrds pulled into that form from sql server.
I maintain an Access DB at work that we use to send out hourly updates on employee productivity. Currently I have the form arranged into columns: The first column contains a ComboBox that we can select an employee's name from. Once a name is selected, the next column fills in automatically with the agent's employee ID (TID), through this code:
AgentName = rs.Fields("AgentName")
sqlString2 = "SELECT * FROM " & "AllAgents WHERE FullName ='" & AgentName & "'"
rs2.Open sqlString2, CurrentProject.Connection, adOpenKeyset, adLockOptimistic
AgentTID = rs2.Fields("TID").Value
rs2.Close
Everything works fine when working in the office on the corporate network, but I've just discovered that working over the VPN causes horrendous slowdown when using this form. Slowness is an issue I've fought with over the VPN forever, but I had a thought in this case that may potentially aleve the slowness, I judt want to know if I'm correct before I go to the trouble of re-coding everything.
My idea is that I could create an array in VBA that would be populated with the agents' name & ID's when the form first loads, or even when the DB is opened on each inidividual laptop. Then the DB would only need to read from the 'AllAgents' table once, and could simply use the array as a source instead. What I doin't know is if this would have an effect or not. Basically, if an Access DB is saved onto a network drive and accessed over a VPN, would the array be stored in the RAM of the laptop? If it is, I would assume this would alleviate the speed issues and would be worthwhile taking the time to re-code.
Thanks,
The thing about form-level or global variables in Access is that you better have good error handling in the application. If an uncaptured error occurs it can result in those variables being for lack of better word discarded. The result would be the next time you try to access the data in the array you get another exception.
Here are a few things you could try before going the array route:
Your combo box probably doesn't need to be bound to your recordset rs directly. Set the source of the combo box at design time to the underlying query or table.
This makes it possible to simply refer to the combo box's bound field using something like this: AgentName = cboAgentName.Value
(If you can eliminate an unnecessary recordset object from the form the better off you will be in the long run)
Your lookup code shouldn't need to use SELECT *. This just isn't a good practice to use in production code. Instead, use SELECT TID. Basically, only return in your query the fields you actually need.
You don't need to use the adOpenKeySet option, which is unnecessary overhead. You should be able to use adOpenForwardOnly.
I would also suggest looking at the AllAgents table to make sure that there is an index on the field you are using for the lookup. If there isn't, think about adding one.
You still might need to go the array route, but these are relatively simple things that you can use to try to troubleshoot performance without introducing massive code changes to the application.
So I've been researching for days now on how to filter a rowsource result on a control in a way that is comfortable, hopefully you understand what I mean by that as I explain. I have found solutions, a bunch of solutions. I'm more concerned with evaluating their benefits and negatives.
I have a specific example, but my concern is really more generic. This, to me, seems like the backbone of my application and so I want to make sure it's being done correctly, the best way, not just in a way that "works".
Basically, I have progressive combo box filters. The first box filters the second box, which then selects a record in a Single Form view. The two combo boxes are in the header of the form.
Lets say I have a table CanadianCities. The two combo boxes might be, cboProvinceFilter "Filter by Province", and cboCitySelect "Select City"
When I load the form the province filter is off, so the cities list is populated with a rowsource that selects ALL the cities (SELECT ID, CityName FROM CanadianCities). But that's a big list, so I have the second combo box to narrow that list down by province (SELECT ID, ProvinceName FROM CanadianProvinces).
So the goal is that on cboProvinceFilter.AfterUpdate to requery cboCitySelect with an altered where clause ("[...] WHERE ProvinceID = [cboProvinceFilter]").
The problem is in how to alter the where clause. Ideally, the above would work right in the designer, but SQL designs seem to be out of the form's scope so cboProvinceFilter doesn't exist there. I agree with the opinion that direct referencing forms is bad. I don't want to marry my sql to the form like that. Plus, I want to use a navigation form, but also have a mobile option, so running the forms individually AND in navigation would be ideal, absolute referencing can't do this.
Having my repetitious SQL statements buried in code feels like poor design, and repeating the same queries with slightly different filtering is terrible when Parameters are exactly for that reason.
And some will scoff at this, but it also feels bad to rewrite the functionality of the Access designer in VBA. If I build my own SQL, execute my own queries, and populate my own lists, why did Microsoft put all the effort into building this productivity assisting tool for? Filtering is not exactly an obscure feature of database management... I feel like there must be a reasonable way to do this sort of thing.
Also, popup forms are obnoxious, so I won't be making specific forms just to have reliable absolute references. That definitely feels like a cop-out.
Solutions that do feel good but I haven't made work...
SQL Parameters
The most sensible way of doing this I feel should be with SQL Parameters, as that's what they're intended for right? The QueryDef would store values for it's parameters that could I could change as needed. However, I would let the queries execute naturally on requery.
So instead of writing the handful of lines to execute the Query and populate the control, I'd just set the parameter values and call requery on my control, which has all that functionality built into it.
So defined some parameters in the SQL statement, then tried to set the values of those parameters in VBA before the Query was executed, but JET always seems to pop-up for the parameters if it doesn't reference an actual object, it wasn't checking my code-set querydefs.
For that to work, it seems that I'd have to execute the SQL manually, and parse my own recordset, and populate the control. Which feels like an excessive amount of repetition for every filter option I'd want to offer.
Relative Referencing
I don't mind referencing forms as long as it's a relative path. Unfortunately [Screen].[ActiveForm] refers to the navigation form, not the actual, active form... So that seems to be out.
Right now I'm thinking my only option is to set rowsource manually then call the control's requery. that's the less offensive feeling option. Might be best to take the current query and string replace the where portion, so i don't have to update every event if the query structure changes.
Final Thought
Anyway, this is getting ranty, so let me know your thoughts. I'm not really looking for code solutions, which is why I offered few to no hard examples. I'm looking for a paradigm for managing this kind of filtering that isn't too restrictive (absolute referencing) or too repetitive/wheel-re-inventing (hard coded sql, executing, control populating).
If your Access version is >= 2007, you can use the TempVars Collection. Here is an Immediate window session.
' add a TempVar with value
TempVars.which_id = 12
' or do it explicitly with Add method
TempVars.Add "which_id", 12
? TempVars!which_id
12
' asking for the value of non-existent TempVar returns Null
? TempVars!bogus
Null
A query can reference the TempVar to filter the result set.
SELECT f.*
FROM tblFoo AS f
WHERE f.id=[TempVars]![which_id] OR [TempVars]![which_id] Is Null;
So you could use that approach in the row source query for the cboCitySelect combo box. Then assign the TempVar value in the After Update event of cboProvinceFilter and next Requery cboCitySelect.
For Access versions < 2007, the TempVars Collection is not available. In that situation you could use a custom VBA function to hold a static value which can be referenced in a query.
SELECT f.*
FROM tblFoo AS f
WHERE f.id=TargetId() OR TargetId() Is Null;
Public Function TargetId(Optional ByVal pValue As Variant) As Variant
Static varReturn As Variant
If IsMissing(pValue) Then
If VarType(varReturn) = vbEmpty Then
varReturn = Null
End If
Else
varReturn = pValue
End If
TargetId = varReturn
End Function
I have come across the problem of generating complex access reports (by complex I mean with data processing, variable number of fields, among others).
Let me explain in deeper detail some of the things I need to implement:
Some fields should not show according to some values in a query
If a certain record does not exist, a nice colored (very noticeable) message should appear instead of the values that would be there (Suppose, for example, that a record with 03/04/2009 in the date field exists, a record with 03/06/2009 in the date field also exists but no record with 03/05/2009 exists. Before showing the data related to the last record, I should print something like "Didn't show up on 03/05/2009")
A bar chart that takes as data not the values in the records, but instead something else that is calculated over a set of records (like an average of all grades for a certain date). The number of series in this chart also varies according to values in the records, this chart would not be in the detail section, but instead in the page heading or some kind of group heading.
It should also be mentioned that the query is a TRANSFORM query (more precisely, an INNER JOIN of many TRANSFORM queries), and thus the number of columns returned by the query varies. While in the past I've been unable to bind this query as the recordsource for the report, somehow Access stopped complaining for now (can someone please clarify this? Is this normal, should I not worry about it and use it as a recordsource or should I avoid it?)
There are two options to achieve what I want (that I can see for now):
Create a report with no record source and lots of unbound fields, and through several events (Report_Open, Section_Format, etc.) and with the help of DAO, manually set the values of these fields. Changing the Data Series of the chart is also possible through VBA.
Set the record source to the query, and create some crazy and confusing VBA code to deal with the data and implement everything I need.
It seems to me that option 2 is going to be a huge headache and waste of time, and I recognize option 1 is pretty much like writing to an Excel file (since all the data is obtained with DAO), which would be much easier since I have much more control over almost everything there (but for many other reasons, we want everything in an access report)
While I'm biased and intend to go with option 1, I have found several problems with this option, for example:
I can't find a way to create new pages in the report with VBA, and thus I'm limited only to the first page.
Lack of some kind of free, online, decent and complete documentation on VBA and Access Reports
Also, if option 2 is more viable, I'm certainly willing to go with it, but I would also need some advice, and perhaps some tips to solving the problems I mentioned in this question.
So, the questions are:
Where can I find some decent and complete documentation on Access Reports and VBA?
How can I create pages in an access report, and choose which page I want to write to?
With the problem I have in my hands, will I reach any bottlenecks I should know about? Should I already be thinking of alternatives to Access Reports (writing to a spreadsheet, for example?)
Sounds like you want to dynamically create the report and avoid all the dummy text boxes.
In regard to:
I can't find a way to create new pages
in the report with VBA, and thus I'm
limited only to the first page.
Your solution #1 seems to assume an unbound report.
I think what I'd do is have the form the crosstab as the rowsource, so you'd have records to generate the pages, and then define your report's controls with no ControlSource (except for the controls that are bound to fields that are always present in the CrossTab). Then you could assign the ControlSources at runtime based on the particular columns. Here's the SQL for a crosstab grabbed from an app I'm working on now:
TRANSFORM First(impNoMatch.PersonID) AS FirstOfPersonID
SELECT impNoMatch.LastName, impNoMatch.FirstBame
FROM impNoMatch
GROUP BY impNoMatch.LastName, impNoMatch.FirstName
PIVOT impNoMatch.Status;
Now, you know that the fields in the SELECT clause will always be present, so if you opened a recordset on the SQL string you are using and count the number of fields in the recordset's Fields collection (you can't use the report's Recordset unless it's an ADO recordset, i.e., not bound to the Recordsource):
Dim strSQL As String
Dim rsFields As DAO.Recordset
Dim lngFieldCount As Long
strSQL = Me.Recordsource
Set rsFields = CurrentDB.OpenRecordset(strSQL)
lngFieldCount = rsFields.Fields.Count
From that, since you know the number of fields in the SELECT statement (i.e., the row headings), you can calculate the number of dynamic controls you want to assign, and you can use this recordset's fields collection to assign the ControlSources and unhide the controls.
You'd start out with all your controls that will display the dynamic fields set so their Visible property is FALSE. You'd also use a naming convention for those controls. In the code below, I've used txtNN, where NN is the numeric index in the Fields collection formatted as 2 digits. Here's the code (which adds lines to what's listed above, and is run in the OnOpen event):
Dim strSQL As String
Dim rsFields As DAO.Recordset
Dim lngFieldCount As Long
Dim l As Long
Dim strControlName As String
strSQL = Me.RecordSource
Set rsFields = CurrentDb.OpenRecordset(strSQL)
lngFieldCount = rsFields.Fields.Count
For l = 2 To lngFieldCount - 1
strControlName = "txt" & Format(l, "00")
Me(strControlName).ControlSource = rsFields.Fields(l).Name
Me(strControlName).Visible = True
Next l
rsFields.Close
Set rsFields = Nothing
Now, if you want to get fancy, you can reformat the controls, changing widths and horizontal/vertical position. If you do that, you have to do it in a different event, and it's a bit tricky to choose that. The only good place to put it is in a report group's header's OnFormat event. If you don't have any grouping, you can add one that doesn't do anything. In the case of my crosstab, a two-level sort on Lastname and Firstname and a header on the Firstname group with nothing in it is a good place to use the OnFormat event to change the appearance/layout of the controls on your report.
As to your question about how to learn how to do this, I recommend picking up an intermediate/advance Access programming book. The Access Developers Handbook is the gold standard on this, and includes tons of examples of programmatic control of reports.
I've got an existing Access MDB. I'm adding a command button to an existing Form that runs an existing report. The change being made is that this button needs to pass in a parameter containing the ID of the record being reported on - currently the report runs on every record in the MDB.
I've altered the Query that the report runs on to use a parameter for the ID value, so that now when the button is clicked Access prompts for the record ID to report on, and the report displays like it should.
However, I can't for the life of me figure out how to pass a parameter into the report for the query to use. How can I do this?
The DoCmd.OpenReport method has various arguments, one of which is a Where statement:
DoCmd.OpenReport"rptReport", acViewPreview,,"ID=" & Me.ID
That is
expression.OpenReport(ReportName, View, FilterName, WhereCondition, WindowMode, OpenArgs)
My general approach to this type of problem is to save the criteria in the database, typically a control table that has one row. Then to reference your criteria you put a query in paranthesis that returns one value, of the criteria you want. In your case, it would be something like:
(select reportID from control)
The advantage of this techinque is that the control table remembers the settings for the next time you run the report. Of course, ReportID would be tied to a field in a form. I also like the fact that your queries are isolated from forms; they can be run independently of forms.
The Where clause of the docmd.openreport is a string that uses the same format as the where clause in a SQL statement.
The reason to put parameterize you query at the docmd instead of the RecordSource of the report is flexibility. You may have a need to open the report without any paremeter/return all the records or have the ability to filter on different fields.
Why everyone wants to make this so complicated, I don't know.
save your report's recordsource without parameters.
as suggested by Remou, pass the criteria in the appropriate argument of DoCmd.OpenReport.
Trying to do it any other way is going to be a matter of resisting the natural methods for accomplishing tasks in Access.
I know this is an old post but this took me a bit. Error was "Invalid use of parren" however the issue was the space in the field name. I was creating a report from a db that someone did the common mistake, spaces.
To pass a param to a query through the where clause when the database field has a space use this example:
DoCmd.OpenReport "rptByRegionalOffice", acViewPreview, , "[" & "Regional Office" & "]" & "=" & "'" & cmboOffices.Value & "'"
If you think about this you can see that this will produce where [Regional Office]='string value' just as you would expect in access sql.