I am doing some work for someone that insists on using MS Access. I don't usually use it so I am a bit new to the whole control structure and best practices. What I am trying to achieve is to have a filter textbox on a form which, when a value is entered, it will filter the rows in the detail section. That seems like a straightforward use case. I initially tried the following behaviour as the event handler for the On Change event:
Private Sub FilterGrid()
Me.Text32.SetFocus
If Not IsNull(Me.Text32.Text) And Me.Text32.Text <> "" Then
Me.Filter = "JobNumber LIKE '*" & Me.Text32.Text & "*'"
Me.FilterOn = True
End
Else
Me.FilterOn = False
End If
End Sub
This worked perfectly until I typed something that didn't have any rows matching and the whole thing exploded with this error (and was unrecoverable without closing the form):
Run-time error '2185': You can't reference a property or method for a control unless the control has the focus.
I did some reading around and the general opinion was that .Text should not be used and .Value (or simply the Text32 without a property) should be used. That produced some very strange behaviour. The Text32.Value is ALWAYS null. I have a watch window and I can see that for the normal behaviour, Text32.Text has an actual value, but Text32.Value is NULL.
Obviously I am doing something wrong, but I don't have enough experience with Access to know what it is.
Just as an aside, another suggestion was to do Text32.SetFocus right before accessing the Text property. This doesn't resolve the error I mentioned. It still throws the exact same error.
Is anyone able to point me in the right direction here?
As you found out, the textbox's Value is only set after the control loses focus.
Conversely, the Text property is only accessible while the control has focus.
The Value property is defined as the default member for controls; that means Text32 will be implicitly the same as Text32.Value, however, depending on the context,Text32 can sometimes refer to the control itself, not just its value.
All these discrepancies can sometimes be infuriating.
To go back to the matter at hand: you have 2 ways to handle filtering.
if the list to filter is large, it's probably better that the user type their filter, then press ENTER to validate it.
if your list is not too large, you can implement filter as you type.
First case, wait for user input to be validated by ENTER.
Say your filtering textbox is called txtFilter and is located on a form whose subform is showing a datasheet (or continuous form) that you want to filter.
All you need to do is wire the textbox OnKeyDown events as such:
' We will only perform the filter if the user press the ENTER key
Private Sub txtFilter_KeyDown(KeyCode As Integer, Shift As Integer)
Select Case KeyCode
Case 13, 9
KeyCode = 0
QuickFilter
End Select
End Sub
' Perform the actual filtering on the subform
Private Sub QuickFilter()
Dim sql As String
Dim filter As String
If txtFilter.Text = vbNullString Then
' Reset the filter if the textbox is emtpy
SubForm.Form.FilterOn = False
Else
'Some common substitutions that users may have already inserted as wildchars
filter = Replace(txtFilter.Text, "%", "*")
filter = Replace("*" & filter & "*", "**", "*")
' We construct the filter SQL
sql = "([JobNumber ] LIKE """ & filter & """)"
sql = sql & " OR ([ProjectCode] LIKE """ & filter & """)"
sql = sql & " OR ([SupplierName] LIKE """ & filter & """)"
'... Add as many columns to filter on as you want
' Assign the filter to the subform
SubForm.Form.filter = sql
SubForm.Form.FilterOn = True
End If
End Sub
Second case, filter as you type
Well, it's fairly easy, we just need to add to the above solution a way to track changes as the user is typing.
This is best done through the OnChange event of the texbox.
Private Sub txtFilter_Change()
QuickFilter
End Sub
That's all you need to add.
dont use .text property
instead use .value property
text3.value=text1.value+text2.value
you dont need setfocus like .text property everytime.
it works perfectly....vba ms access
Related
I'm building a database that will be accommodating a large number of records, so I want a search function that is easiest on the server. I am using the following code but I know it isn't sustainable for a larger database. It's looking at the search box and running a query to narrow the search results:
Private Sub SearchFor_Change()
'Create a string (text) variable
Dim vSearchString As String
'Populate the string variable with the text entered in the Text Box SearchFor
vSearchString = SearchFor.Text
'Pass the value contained in the string variable to the hidden text box SrchText,
'that is used as the sear4ch criteria for the Query QRY_SearchAll
SrchText.Value = vSearchString
'Requery the List Box to show the latest results for the text entered in Text Box
'SearchFor
Me.SearchResults.Requery
'Tests for a trailing space and exits the sub routine at this point
'so as to preserve the trailing space, which would be lost if focus was shifted from
'Text Box SearchFor
If Len(Me.SrchText) <> 0 And InStr(Len(SrchText), SrchText, " ", vbTextCompare) Then
'Set the focus on the first item in the list box
Me.SearchResults = Me.SearchResults.ItemData(1)
Me.SearchResults.SetFocus
'Requery the form to refresh the content of any unbound text box that might be feeding
'off the record source of the List Box
DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor,
'and restores trailing space lost when focus is shifted to the list box
Me.SearchFor = vSearchString
Me.SearchFor.SetFocus
Me.SearchFor.SelStart = Me.SearchFor.SelLength
Exit Sub
End If
'Set the focus on the first item in the list box
Me.SearchResults = Me.SearchResults.ItemData(1)
Me.SearchResults.SetFocus
'Requery the form to refresh the content of any unbound text box that might be
'feeding off the record source of the List Box
DoCmd.Requery
'Returns the cursor to the the end of the text in Text Box SearchFor
Me.SearchFor.SetFocus
If Not IsNull(Len(Me.SearchFor)) Then
Me.SearchFor.SelStart = Len(Me.SearchFor)
End If
Ideally I want a form that has several search fields, and one 'find' button that runs the queries to return the results in a list box.
I'm also not sure how to set it up so that when the user double clicks on a selection from the search results, the selected record is opened in a form in edit mode.
Any help would be much appreciated, thanks!
First off, you've asked two questions in one post. I recommend you take out the second question regarding opening the selection in edit mode on double click.
As best as I can understand, you're concerned about the performance of your current code as well as the lack of features or flexibility it offers.
Regarding performance:
Don't use the change method to perform the filter. If you really do want to use the change method, use it only to set a timer interval to something like 500 (ms) and then perform the filter on the Timer event. This was the filter won't occur until after the user has stopped typing for a half second.
Avoid "fuzzy" searches (use of asterisk/percent in text fields). It doesn't look like you're using them now. While fuzzy searches usually make software more user friendly, they make it less user friendly when they cause a significant hit on the performance.
When working with large amounts of data, most performance gains come from carefully restructuring the way your application works, by upgrading to SQL Server, and by upgrading your server and network to better hardware. You can only improve about so much when using a JET/ACE backend database container. SQL Server with ADO and ODBC linked tables both offer some advantages over DAO with JET/ACE. ODBC linked tables offer lazy loading while ADO offers things like disconnected recordsets which can be filtered without an additional call back to the server (there are limitations to this).
As already mentioned above, you might need to carefully rethink how your application works and how it is designed. It's better to try to limit the amount of complicated queries that are needed and the amount of text-based searching that is allowed/required. Use more lookup/reference tables. Instead of storing thinks like categories as text, consider storing them as a Long Number CategoryID instead. Queries on indexed numeric fields usually perform better than queries on text-based fields, especially if you are using LIKE with asterisks in your query.
As far as the rest of your question (flexibility and features), consider creating a procedure that builds a criteria/where statement for you based on the values of multiple controls. In a situation such as yours my code would look something like this (below). Notice that I did use asterisk (fuzzy search) in my Description search/filter. If it performs poorly you'll need to consider taking that out and allowing the user to put their own asterisks in instead.
Private Sub cmdSearch_Click()
Call SetRowSource
End Sub
Private Sub txtSearch_AfterUpdate()
Call SetRowSource
End Sub
Private Sub cboCategoryID_AfterUpdate()
Call SetRowSource
End Sub
Private Sub txtBrand_AfterUpdate()
Call SetRowSource
End Sub
Private Sub SetRowSource()
Dim sSQL as String
sSQL = "SELECT ItemID, Description, Brand FROM tblItems "
sSQL = sSQL & GetWhere
Me.lstSearchResults.RowSource = sSQL
End Sub
Private Function GetWhere() as String
Dim sWhere as String
If Nz(Me.cboCategoryID, 0) <> 0 Then
sWhere = sWhere & "CategoryID = " & Me.cboCategoryID & " AND "
End If
If Nz(Me.txtSearch, "") <> "" Then
sWhere = sWhere & "Description LIKE '*" & Replace(Me.txtSearch, "'", "''") & "*' AND "
End If
If Nz(Me.txtBrand, "") <> "" Then
sWhere = sWhere & "Brand = '" & Replace(Me.txtBrand, "'", "''") & "' AND "
End If
If sWhere <> "" Then
sWhere = Left(sWhere, Len(sWhere)-5)
GetWhere = "WHERE " & sWhere
End If
End Function
I think I might be a little bit odd in the Access community but I generally do not allow my controls to reference other controls. In your case the RowSource in your listbox references the controls of the form it's located on. For a variety of reasons, I prefer to build my SQL statements in VBA code, particularly when they are subject to change/filtering. Another thing you might consider doing is using a Datasheet form instead of a listbox. You can set the form's RecordSource and just apply your WHERE statement to the form's Filter property then. Datasheet forms are more flexible for the user as they can resize columns and do sorting without any help from you the programmer. You can always lock the controls so they can't do any editing. When I use datasheets this way I think use the DoubleClick event to allow them to open the record, which is arguably less user friendly then using the single click on a listbox.
Can someone help me understand why this code isn't producing expected results?
I have a form with a text box called TxtVendorSearch.
When a user begins typing into this text box, I'd like for it to start filtering the results in the form's subdatasheet.
I am starting with a fully populated datasheet, and I'm not sure why the filter blanks out the whole thing as soon as I start typing a valid filter that should leave results.
Private Sub TxtVendorSearch_KeyPress(KeyAscii As Integer)
Dim str1 As String
str1 = "[VendorID] LIKE '*" & Me.TxtVendorSearch.Value & "*' "
Me!subOrderDS1.Form.Filter = str1
Me!subOrderDS1.Form.FilterOn = True
End Sub
I had a similar problem and i searched the internet for the keyword "find as you type" AND "ms access"
i found this great article. hopefully it will solve your problem.
N.B. this article also contains the source code to use.
Do not use the KeyPress event, you're not going to be able to easily reconstruct the actual input data from within the event handler itself (when the event is triggered, the key just pressed is not yet added to the text of the textbox, meaning you're always short a the last keystroke).
Instead, use the KeyDown event, and use the textbox' .Text property instead of .Value.
.Value is only set once the focus has moved away from the box.
So your code can be simply rewritten as (make sure your KeyDown event is set in the textbox's events on the form):
Private Sub TxtVendorSearch_KeyDown(KeyCode As Integer, Shift As Integer)
' If there is no filter criteria, disable filtering '
If Len(Trim(TxtVendorSearch.Text)) = 0 Then
subOrderDS1.Form.FilterOn = False
Exit Sub
End If
' Do nothing if user hits RETURN or TAB '
If KeyAscii < 32 Then
KeyCode = 0
Exit Sub
End If
Dim str1 As String
str1 = "[VendorID] LIKE '*" & Trim$(TxtVendorSearch.Text) & "*'"
subOrderDS1.Form.Filter = str1
subOrderDS1.Form.FilterOn = True
End Sub
I used Trim() to remove any leading and trailing white-space that the user may have typed.
Last thing: you don't need to use Me. or Me! from within the form's code itself.
Doesn't hurt if you do, but it makes things a bit less legible without adding anything to the code.
I have a textbox on an access form that I would like to enable for edditing if the textbox is blank. I am new to VBA, and am uncertain of the best way to go about it. Using a double-click event would be fine with me, but I would prefer to do it on load.
I have tried the following code, but can't seem to get it to work. Any help would be appreciated.
Private Sub EmpID_DblClick(Cancel As Integer)
If EmpID.Text = "" Then
Me.EmpID.Enabled = True
End If
End Sub
For things that should happen on a per record basis, you are best off using the current event. Do not use the .Text property for controls. If you must use a property, use .Value. The .Text property is only available when the control has focus and is generally only used in special cases such as the Change event.
Private Sub Form_Current()
If IsNull(EmpID) Then
Me.EmpID.Enabled = True
End If
End Sub
In a normal set-up, there is no chance of the control being equal to a zero-length string ("")
You can cover all bases by saying:
Trim(EmpID & "") = ""
You are running into the Null problem
Null is not the same as ""
Try this instead
If EmpID.Text Is Null Then
Me.EmpID.Enabled = True
End If
I have multiple fields on a form that have no values associated with them until data is entered into that field. At which point those controls have the #Name? value. All I'm trying to do is hide the fields using a loop as opposed to writing If statements. Here is the code working with a single If statement to hide said field/fields.
If Not Me.txtField.ControlSource = "#Name?" Then
Me.txtField.ColumnHidden = True
End If
I can't seem to figure out how to hide multiple controls that have the #Name? flag. Any help would be appreciated.
Dim ctl As Control
Dim ctlError As String
ctlError = "#Name?"
For Each ctl In Me.Controls
If Not ctl.ItemsSelected(txtField) = ctlError Then
ctl.ColumnHidden = True
End If
Next ctl
EDIT:
On the main form there is a subform with a cross tab query. But the problem is results on the query are populated from a combobox. So I've added fields that are not yet available in the query to the subform and end up with #Name? Attached is a screenshot to better illustrate the issue.
There is a 90% chance I'm going about this process wrong, so it's a learning process right now.
It would be easier to address this question if you described the specific errors that are raised by your code. This answer assumes that the control in question is a TextBox.
"#Name?" means that a control's ControlSource refers to a field that is not in the form's RecordSource. For example, if your query has two fields (ID and BirthDate, for example) and your form has three controls, with ControlSource of "ID", "BirthDate", and "BirthCountry", then the control whose RecordSource is "BirthCountry" will show "#Name?". However, its Value is not "#Name?". Rather, calling the Value property raises error 2424 (with the rather unhelpful message "The expression you entered has a field, control, or property name that Microsoft Office Access can't find").
The only way I know to check for the string "#Name?" is through the control's Text property, which is only available when the control has the focus, so that's not much help. If you retrieve the value of the ControlSource property, you'll get "BirthCountry" (of course). So you could check all ControlSources on your form against the fields of the form's RecordsetClone (add error handling, please):
For Each ctl In Controls
If Not RecordSourceContains(ctl.ControlSource) Then
ctl.Visible = False
End If
Next
Function RecordSourceContains(strFieldName as String) As Boolean
Dim fld As Field
For Each fld In RecordsetClone.Fields
If fld.Name = strFieldName Then
RecordSourceContains = True
Exit For
End If
Next
End Function
You'll probably want to do some other bookkeeping. For example, you'd want to check for expression control sources (which begin with "=") because they won't be in the RecordSource. You'll also either need to handle error 438 (Object doesn't support this property or method) or check each control's type to make sure it has a ControlSource property.
In design view for the form, what does the field say for Control Source? It should be blank, meaning the form is unbound. If it's not, then that explains #Name.
More information: http://www.databasedev.co.uk/unbound-forms-add-data.html
Here is code I use to take care of that:
Sub setControl
'' You can use this in your loop
Call FieldEnable(subFrm, "boxQty", booSellSet)
End Sub
''----------------
Private Sub FieldEnable(subFrm As Access.Form, strCtlName As String, _
booEnableMe As Boolean)
On Error GoTo errHandler
subFrm(strCtlName).Enabled = booEnableMe
subFrm(strCtlName).Visible = booEnableMe
errExit:
Exit Sub
errHandler:
If Err.Number = 2164 Or Err.Number = 2165 Then ''control has the focus
Resume Next
Else
DoCmd.Hourglass False
Screen.Application.Echo True
MsgBox "Problem with control named " & strCtlName
Resume errExit
End If
End Sub
I have to adjust many fields on the form, so this is very reusable. I might be turning them "on" or "off"; this takes care of either. Let me know if you have questions.
It all boils down to this method for hiding the field:
Control.Enabled = False
Control.Visible = False
Delete the field Black from table.
And then make make again the Black field in your table.
I have about 10 text boxes on a form that are actually used for display not entry. They are are named txt_001_Name, txt_002_Title, etc..what kind of loop is used for this.
What kind of VBA should I use to actually loop through the names of the text boxes? So if I was to debug.print it would look like:
txt_001_Title
txt_002_Title
txt_003_Title
This is probably pretty simple to do - all the more reason that I should learn how!
EDIT: Sorry, I should have been more descriptive about this.
Because of the above naming convention, I am looking to iterate through these text boxes so that I can perform something with each. What each of these 10 text boxes actually represent is numeric values, each having a SQL statement behind them in the form's onload event. I also have another set of ten that hold numeric values that are much more static, and finally another ten that use an expression to simply divide each of the first ten, against the relative "second" ten, and the value ends up in the relative 3. So basically it ends up looking like a dashboard table.
'first ten' 'second ten' 'resulting ten'
---------------------------------------------------
txt_001_value txt_001_calc txt_001_result
txt_002_value txt_002_calc txt_002_result
etc.
So I actually want to use this for the 'resulting' text boxes. I want to loop through the first ten and perform this easy calculation:
me.txt_001_result = me.txt_001_value / me.txt_001_calc
All the naming conventions "match up", so I can manually type out the 10 lines of the above for this, but I am sure there is a better way (loop through this), and I should probably learn it.
You can list the names of textbox controls with a simple procedure like this:
Public Sub TextBoxNames(ByRef pfrm As Form)
Dim ctl As Control
For Each ctl In pfrm.Controls
If ctl.ControlType = acTextBox Then
Debug.Print ctl.Name
End If
Next ctl
Set ctl = Nothing
End Sub
You could call it from the form's Load event:
Private Sub Form_Load()
TextBoxNames Me
End Sub
However, I don't understand what you're trying to accomplish. I realize you want to do something with ctl.Name other than Debug.Print, but I don't know what that is.
Rather than computing a result for me.txt_001_result and then assigning that value to the text box, consider setting the control source for txt_001_result to txt_001_value / txt_001_calc and let Access put the proper value into txt_001_result for you.
In response to your comments, I'll suggest this procedure as a starting point for you to build upon:
Public Sub MyTextBoxValues()
Const cintLastTextBoxNum As Integer = 10
Dim i As Integer
Dim strValueControl As String
Dim strCalcControl As String
Dim strResultControl As String
Dim strPrefix As String
For i = 1 To cintLastTextBoxNum
strPrefix = "txt_" & Format(i, "000")
'txt_001_value txt_001_calc txt_001_result '
strValueControl = strPrefix & "_value"
strCalcControl = strPrefix & "_calc"
strResultControl = strPrefix & "_result"
'me.txt_001_result = me.txt_001_value / me.txt_001_calc '
'Debug.Print strResultControl, strValueControl, strCalcControl '
Me.Controls(strResultControl) = Me.Controls(strValueControl) / _
Me.Controls(strCalcControl)
Next i
End Sub
I prefer to use a FOR EACH to iterate through the controls collection of whatever the textboxes are on (either the form itself or a panel control)
dim myBox as Textbox
For each myBox in myForm
myBox.Text = "hello"
Next
Also means you can make custom groups (by putting them all on the same container).
Note that if you have other controls, you might need a typecheck in there (IF TYPEOF(myBox) = "TextBox" THEN ...)
You could also do it like:
dim i as integer
For i = 1 to 10
myForm.Controls("txt_00" & i & "_Title").Text = "hello"
Next i
I definitely prefer the For Each, though.
I can't entirely understand why you need to do what you're doing, but I've had forms like that where I had an unbound form that I wanted to display an arbitrary number of fields, so I can see it. If you're walking the collection of controls only in the form's OnOpen event, that's fine. But if you're doing it in the OnCurrent of a bound form, or multiple times in an unbound form, you might consider a long post of mine on using custom collections to manage groups of controls.