Im not really used on Kofax technologies and I have a Kofax Transformation form with fields on 2 different tabs. Here is an abstract of this form on which I have to interact on validation process.
Among those fields, I try to update the content of some of them with a validation rule on validation stage. I simply created a multi field validation rule and mapped correctly the fields.
It was proposed a basic script to check if the fields are valid or not. Based on this script I tried some logic. The Objective is to set the content of a field (which is empty and required) based on a basic condition on the second field.
My objective (later) will be to fill / update the fields based on the “Siret” field value with a database call.
My validation rule is the following : I check the “Siret” string length (it should be a 14 chars string). If this is true, I set the Validation to true and set the other field a value.
Private Sub Validation_Validate(ByVal ValItems As CASCADELib.CscXDocValidationItems, ByVal pXDoc As CASCADELib.CscXDocument, ByRef ErrDescription As String, ByRef ValidField As Boolean)
Dim strNAF As String
Dim strSiret As String
strNAF = ValItems.Item("NAF").Text
strSiret = ValItems.Item("Siret").Text
' enter your own validation rule here
If Len(strSiret) <= 14 Then
pXDoc.Fields.ItemByName("NAF").Text = "GOOD JOB"
ValidField = True
Else
ValidField = False
ErrDescription = "Describe the error here"
End If
End Sub
This validation should occurred when I press key enter on the “Siret” input field. It doesn’t seem to work actually. I wonder what is going wrong at this stage.
The best place to have a field's value changed depends on your use case. Kofax Transformation Modules follows a distinct pattern, and you should always try to follow it. For example, when you find yourself putting code in the AfterExtract method, you should reconsider -- there is almost always a better way. For a tl;dr just jump to Where to set values?
Natural Order
When you observe a field in Validation, here's what happens behind the scenes:
A locator detects information (i.e. one or more alternatives).
The locator is assigned to a field. The field will always hold the topmost alternative. By default, your field will be valid if there is only a single confident alternative, or if the second best alternative has a 20% lower confidence.
Your field may or or may not have a formatter associated with it. The formatter can make the field invalid (or even valid, if desired)
You field can be part of one or more validation rules. Validation rules can make fields valid or invalid, but they only fire if formatting was successful.
Example
Here's an example. Imagine you want to detect dates, and you want them in database format (yyyyMMdd). The dates on your documents are in US format (MM/dd/yyyy), and there are two, but the first one has "invoice date" as keyword besides it.
You configure a format locator to pick up dates, along with a keyword. The locator yields two alternatives: 01/20/2020 at 100% and 01/10/2020 at 0%.
Since the second alternative is not considered (below 10% threshold), the field will hold the first one.
Since the first alternative of the field in confident (80%), formatting is applied. The formatter changes the field's value to 20200120, and the field is still valid.
You configure a date validation method, and use said method for the field. You check whether the date is in the future or not, and let's say today's January 19. The field is now invalid since 20200120 is one day in the future.
This brings us back to the original question - where you you change a field's value? Changing it in a validation script is possible, but bear in mind that this breaks the natural order of things. Even worse, if formatting fails in the first place your code never executes.
Where to set values?
Depending on the use case, here are my recommendations:
You need to set a dynamic value, get values from an external system, or anything else that does not depend on another field/value in KTM: create a script locator and link it to your field. In script, create a new alternative and set it to the desired value. You can even make this dependent on other locators (since locators execute in sequence), but not on other fields (since fields are populated AFTER all locators have fired).
You want to normalize values. For example, days of the week should be represented by letters (SUN, MON, TUE, et cetera). You still want your users to enter numeric values (1 = SUN, 2 = MON, 3 = TUE). Create a script formatter and link it to your field. The benefit here is that you don't have to repeat yourself (for example in a script locator and later in validation).
You want to change values based on a user's interaction. You can use any event in validation (such as ButtonClicked, AfterFieldChanged or AfterFieldConfirmed), just keep in mind that some are not supported by Thin Client Validation (such as AfterFieldChanged). Keep in mind that you want to change the pointer and not the field itself since validation methods can be used by any number of fields. In your example, pXDoc.Fields.ItemByName("NAF").Text = "GOOD JOB" would violate this principle (i.e. making validation methods reusable -- your rule is now tied to the NAF field). Change the validation object instead; e.g. ValItems.ItemByName("NAF").Text = "GOOD JOB". Also keep in mind that changing a field at this point will NOT call the formatter associated with your field automatically, so make sure to provide a value that's already formatted. You can however call formatting in script manually.
Your requirement
Now, back to your original requirement:
The Objective is to set the content of a field (which is empty and required) based on a basic condition on the second field.
My objective (later) will be to fill /
update the fields based on the “Siret” field value with a database
call. My validation rule is the following : I check the “Siret” string
length (it should be a 14 chars string). If this is true, I set the
Validation to true and set the other field a value.
This depends on whether you want your users to be able to change the second field during validation. If not, go for a script locator. Note that script locators can have two subfields, and each subfield can be assigned to a different field.
If your users should be able to change it, go for multi-field script validation. Both fields should be part of the validation, and a length check should be the first thing you do. Then, if Siret has more than 14 characters, issue the database call.
A word about DRY and General Design
Not knowing your exact requirements, here are some thoughts about reusability. Let's say that Siret isn't always keyed in manually by users - in fact, a locator might pick up said text. This is where you want to create a specific method for calling the database and returning a result. Note that KTM has native support for relational databases, and you can even access this model in script.
Another alternative is to use local or remote fuzzy databases along with a database locator (again, if Siret is present on your document).
Thanks to Wolfgang and my team, I finally solved my issue.
Here is the code used to manage this :
Private Sub ValidationForm_AfterFieldConfirmed(ByVal pXDoc As CASCADELib.CscXDocument, ByVal pField As CASCADELib.CscXDocField)
Select Case pField.Name
Case "FieldNameInForm"
' simple check if field empty
If(pXDoc.Fields.ItemByName("FieldNameInForm").Text <> "") Then
completeForm(pXDoc, pXDoc.Fields.ItemByName("FieldNameInForm").Text)
End If
End Select
End Sub
Private Sub completeForm(ByVal pXDoc As CASCADELib.CscXDocument, ByVal myString As String)
'define required properties
Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Dim sqlRequest As String
Dim dbHostServer As String
Dim dbUsername As String
Dim dbPassword As String
Dim dbName As String
Dim dbConnString As String
'Retrieve information for DB Connection (in ScriptVariables.xml)
dbHostServer = "localhost"
dbUsername = "root"
dbPassword = "root"
dbName = "mydatabase"
'build the connection string and open connection to database
dbConnString = "Provider=MSDASQL;Driver={MySQL ODBC 5.3 Unicode Driver};
dbConnString = dbConnString & "Server=" & dbHostServer & ";"
dbConnString = dbConnString & "UID=" & dbUsername & ";"
dbConnString = dbConnString & "PWD=" & dbPassword & ";"
dbConnString = dbConnString & "database=" & dbName
'Create recordset and set connection
'Prapare the db connection
Set rs = New ADODB.Recordset : : Set cn=New ADODB.Connection
cn.ConnectionString = dbConnString : cn.Open
'build query with concatenation
sqlRequest = "SELECT field1, field2, field3 FROM table"
sqlRequest = sqlRequest & " where fieldN= '" & myString & "'
'Execute the SQL request
Set rs = cn.Execute(sqlRequest)
' if the recordset returns a result
If (rs.EOF Or rs.BOF) Then
rs.MoveFirst
pXDoc.Fields.ItemByName("formField1").Text = CStr(rs.Fields("field1"))
Call ValidStrField("formField1", pXDoc.Fields.ItemByName("field1").Text, pXDoc)
pXDoc.Fields.ItemByName("formField2").Text = CStr(rs.Fields("field2"))
Call ValidStrField("formField2", pXDoc.Fields.ItemByName("field2").Text, pXDoc)
pXDoc.Fields.ItemByName("formField3").Text = CStr(rs.Fields("field3"))
Call ValidStrField("Commune", pXDoc.Fields.ItemByName("field3").Text, pXDoc)
'ifthe recordset do not return a value, we set to Undefined
Else
pXDoc.Fields.ItemByName("formField1").Text = "Undefined"
pXDoc.Fields.ItemByName("formField2").Text = "Undefined"
pXDoc.Fields.ItemByName("formField3").Text = "Undefined"
MsgBox("No result found in database")
End If
' Close connection & recordset
rs.Close : Set rs = Nothing
cn.Close : Set cn=Nothing
End Sub
' methods To validate Fields
Private Sub ValidStrField(ByVal StrItem As String,ByVal StrVal As String,ByVal pXDoc As CASCADELib.CscXDocument)
pXDoc.Fields.ItemByName(StrItem).Text = StrVal
pXDoc.Fields.ItemByName(StrItem).ExtractionConfident = True
pXDoc.Fields.ItemByName(StrItem).Confidence = 100.00
pXDoc.Fields.ItemByName(StrItem).ForcedValid = False
pXDoc.Fields.ItemByName(StrItem).Valid = True
End Sub
Private Sub UnValidStrField(ByVal StrItem As String,ByVal StrVal As String,ByVal pXDoc As CASCADELib.CscXDocument)
pXDoc.Fields.ItemByName(StrItem).Text = StrVal
pXDoc.Fields.ItemByName(StrItem).ExtractionConfident = False
pXDoc.Fields.ItemByName(StrItem).Confidence = 0.00
pXDoc.Fields.ItemByName(StrItem).ForcedValid = True
pXDoc.Fields.ItemByName(StrItem).Valid = False
End Sub
Related
I have the following code:
Try
Dim queryString As String
queryString = "Insert into ServiceRecords([Personnel]) Values(#Personnels)"
command1 = New OleDbCommand(queryString, connection)
For i As Integer = 0 To Me.ListBox1.Items.Count + 1
command1.Parameters.AddWithValue("Personnels", ListBox1.Items(i))
command1.Parameters.Clear()
command1.ExecuteNonQuery()
Next
Catch ex As Exception
End Try
But I get the error below, and I don't know how to fix it. I think it happens because of my code.
And this is what I get:
Let's review.
First, as muffi suggested, use the Add method instead of .AddWithValue but instead of DBType use OLEDBType. There is not .String type in OleDBType. You will have to check your Access db to get the correct datatype. Probably VarChar. In addition, the parameter name should match the parameter name in your query string. With Access the position is the important thing but with other databases the name matters.
Second, as Charles suggested, change the plus one to minus 1. Most people start counting at one but computers usually start at zero so the upper index of the ListBox is one less than the Count (remember you are starting at zero not one).
Third , as Charles also pointed out it is wrong to clear the parameters before you execute. Then you would have nothing in your parameter. It is not necessary to clear them at all because you are overwriting the Value property with each iteration of your loop and I have set the name and datatype outside the loop because they stay the same. We don't want to reset properties for each iteration when they don't change.
command1.Parameters.Add("#Personnels", OleDbType.VarChar)
For i As Integer = 0 To ListBox1.Count -1
command1.Parameters("#Personnels").Value = ListBox1.Items(i)
command1.ExecuteNonQuery
Next
While I have my app running, I question the methodology, and wondering if there’s a “better way”…
Overall design is to allow editing 200-300 records from a gridview (phase1) using VB.Net. The database itself is on SQL Server. There are a number of columns a user will enter into an “application”, and there are several columns that will be edited/maintained by “office users”, if you will. There are several dates involved in this ongoing maintenance, and that’s where the first of my questions revolves.
I have found “solutions” on the internet that got the code working, but am questioning them…
Problem #1 I ran into – dates are NULL in the database, and in trying to read them in using a SqlDataReader led to errors (cannot assign NULL to a Date object). Ok, that led into using a ternary operator to use “IsDBNull”, and either assign the value read from the DB, or to assign DateTime.MinValue. Problem “solved”…
Problem #2 – using the above method now shows dates that are the minimum VB date value – showing actual dates in the fields the user is to edit – definitely not “user friendly”, nor what I want. The only solution to this issue was:
Convert dates from Date or DateTime objects into String objects. This would then allow me to be able to assign an empty string to the gridview in the case where the date was originally NULL in the DB, which had to be transformed into DateTime.MinValue (which could be tested), and then another ternary operator to assign either “ToString” conversion, or an empty string to the gridview field.
Ok – editing is now accomplished. I added some “ScriptManager.RegisterStartupScript” commands to allow testing the validity of the dates the user enters – all is well.
Problem #3 (or 4) – I now need to update the database with the data the user entered – PRESERVING THE EMPTY DATE STRINGS – and update the database (using parameters…) with NULLs back in those date columns. However, again – the date is a string, and is empty, so I had to assign to a “MinValue”, first, then another ternary operator to test each date against “MinValue”, and either assign the date, or a DBNull.Value…
Yes, I guess I could have come up with a number of different update strings (including dates in some, excluding in others), depending on whether or not a string/date was empty or not... But that will only lead to future bugs, so, I guess I’ll be keeping a series of ternary operators.
So, the code for beginning the edit process looks something like:
While sdr.Read
Dim _date1 As Date = If(IsDBNull(sdr("date1")), DateTime.MinValue, sdr("date1"))
.
.
.
‘ Now add them to a List of my Class:
appsList.Add(New AppClass(… _
If(_date1 = DateTime.MinValue, " ", _date1.ToString("MM/dd/yyyy")), _
… )
Now to get the data back from the gridview to update the database:
Dim _date1 As Date
' see if we can convert the various dates...
Try
' see if empty…
If ((CType((row.Cells(19).Controls(0)), TextBox)).Text).Length < 2 Then
_date1 = DateTime.MinValue
Else
_date1 = DateTime.Parse((CType((row.Cells(19).Controls(0)), TextBox)).Text)
End If
Catch ex As Exception
ErrFlag = True
ScriptManager.RegisterStartupScript(Me, Page.GetType, "Script", "alert(‘Date1 Date is not valid - enter as MM/DD/YYYY');", True)
End Try
.
.
.
Dim sql As String = "UPDATE [foo_bar].[dbo].[bar_foo] set date1=#Date1, …….)
cmd.Parameters.AddWithValue("#Date1", If(_date1 = DateTime.MinValue, DBNull.Value, _date1))
Honestly, all this conversion back and forth seems like it’s going to lead to bugs or errors at some point.
So – is this the “best” method for handling this? There isn’t a cleaner way?
If your using winforms then you can handle the Format and parse of the databindings. I haven't tried it on a Gridviewtextbox but worst case you can use a custom cell template and a textbox with format and parse handlers. The code would be something like this:
mybinding = New Binding("text", DataSet1, "table1.datefield")
Me.DataGridTextBoxColumn1.TextBox.DataBindings.Add(mybinding)
AddHandler mybinding.Parse, AddressOf StringToDateTime
AddHandler mybinding.Format, AddressOf formatdate
Private Sub StringToDateTime(ByVal sender As Object, ByVal cevent As ConvertEventArgs)
If cevent.Value.GetType().Equals(GetType(String)) And _
cevent.DesiredType Is GetType(DateTime) Then
If cevent.Value <> "" Then
' Make sure matches format in format funtion
cevent.Value = DateTime.Parse(String.Format(cevent.Value, "MMM d, yy"))
Else
cevent.Value = DBNull.Value
End If
'cevent.Value = DateTime.Parse(String.Format(cevent.Value, "MMM d yyyy")
End If
End Sub
Public Sub formatdate(ByVal sender As Object, ByVal e As ConvertEventArgs)
If e.Value.GetType().Equals(GetType(DateTime)) And e.DesiredType Is GetType(String) Then
' Hard-coded or user-specified
' Make sure matches format in parse funtion
e.Value = Format(e.Value, "d")
End If
End Sub
I don't know if this question will make sense to begin with...
Example Given: the following value is given for a single cell (we'll call it A1): Sub-value #1|Here's another sub-value #2|Yet again, last but not least, sub-value #3. I already know someone will tell me that this is where a database should be used (trust me, my major is DB Management, I know, but I need my data in this fashion). My delimiter is the |. Now say I want to create a function that will take the LEN() of each sub-value and return the AVERAGE() of all the sub-values. If I wanted to create a single function to do this, I could use an split(), take each value, do an LEN() and return the AVERAGE().
For the example given, let's utilize cell B1. I have created similar functions in the past that would work by the following method (although not this exact one), but it requires splitting and joining the array/cell value(s) each time: =ARRAY_AVERAGE(ARRAY_LEN(A1,"|","|"),"|","|").
ARRAY_LEN(cell,delimiter[,Optional new_delimiter])
ARRAY_AVERAGE(cell,delimiter[,Optional new_delimiter])
However, I'm wondering if there might be a more dynamic approach to this. Basically, I want to split() an array with some custom VBA function, pass it to parent cell functions, and I wrap up the array by a function that will merge the array back together.
Here's how the cell function will run:
=ARRAY_AVERAGE(ARRAY_LEN(ARRAY_SPLIT(A1,"|"))).
ARRAY_SPLIT(cell,delimiter) will split the array.
ARRAY_LEN(array) will return the length of each sub-value of the array.
ARRAY_AVERAGE(array) will return the average of each sub-value of the array. Since this function returns a single value of multiple values, this will take the form of an imaginary ARRAY_JOIN(array,delimiter) that would merge the array back again.
This requires one or two additional functions in the cells, but it also lowers the number of iterations that the cell would be converting to and from a single cell value and VBA array.
What do you think? Possible? Feasible? More or less code efficient?
Now, this is a very crude example but it should give you an idea of how to get started and how you can customize this method to suit your needs. Assume you have the following data in a text file called example.txt :
Name|Age|DoB|Data1|Data2|Data3
David|25|1987-04-08|100|200|300
John|42|1960-06-21|400|500|600
Sarah|15|1997-02-01|700|800|900
This file resides in the folder C:\Downloads. To query this in VBA using ADO you'll need to reference the Microsoft ActiveX Data Objects 2.X Library where X is the latest version you have installed. I also reference the Microsoft Scripting Library to create my Schema.ini files at run-time to ensure that my data is read properly. Without the Schema.ini file you run the risk of your data not being read as you expect it to be by the driver. Numbers as text can ocassionally be read as null for no reason and dates often get returned null as well. The Schema.ini file gives the text driver an exact definition of your data and how to handle it. You don't HAVE to define every column explicitly like I have done but at the very least you should set your Format, ColNameHeader, and DateTimeFormat values.
Example Schema.ini file used:
[example.txt]
Format=Delimited(|)
ColNameHeader=True
DateTimeFormat=yyyy-mm-dd
Col1=Name Char
Col2=Age Integer
Col3=DoB Date
Col4=Data1 Integer
Col5=Data2 Integer
Col6=Data3 Integer
You'll notice that the file name is enclosed in brackets on the first line. This is NOT optional and it also allows you to define different schemas for different files. As mentioned earlier I create my Schema.ini file in VBA at run-time with something like the following:
Sub CreateSchema()
Dim fso As New FileSystemObject
Dim ts As TextStream
Set ts = fso.CreateTextFile(FILE_DIR & "Schema.ini", True)
ts.WriteLine "[example.txt]"
ts.WriteLine "Format=Delimited(|)"
ts.WriteLine "ColNameHeader=True"
ts.WriteLine "DateTimeFormat=yyyy-mm-dd"
ts.WriteLine "Col1=Name Char"
ts.WriteLine "Col2=Age Integer"
ts.WriteLine "Col3=DoB Date"
ts.WriteLine "Col4=Data1 Integer"
ts.WriteLine "Col5=Data2 Integer"
ts.WriteLine "Col6=Data3 Integer"
Set fso = Nothing
Set ts = Nothing
End Sub
You'll notice that I use the variable FILE_DIR which is a constant I define at the top of my module. Your Schema.ini file -MUST- reside in the same location as your data file. The connection string for your query also uses this directory so I define the constant to make sure they reference the same place. Here's the top of my module with the FILE_DIR constant along with the connection string and SQL query:
Option Explicit
Const FILE_DIR = "C:\Downloads\"
Const TXT_CONN = "Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq=" & FILE_DIR & ";Extensions=asc,csv,tab,txt;"
Const SQL = "SELECT Name, DoB, ((Data1 + Data2 + Data3)/3) AS [Avg_of_Data]" & _
"FROM example.txt "
Notice the portion in TXT_CONN called Dbq. This is the directory where your data file(s) are stored. You'll actually define the specific file you use in the WHERE clause of your SQL string. The SQL constant contains your query string. In this case we're just selecting Name, DoB, and Averaging the three data values. With all of that out of the way you're ready to actually execute your query:
Sub QueryText()
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Dim i As Integer
'Define/open connection
With cn
.ConnectionString = TXT_CONN
.Open
'Query text file
With rs
.Open SQL, cn
.MoveFirst
'Loop through/print column names to Immediate Window
For i = 0 To .Fields.Count - 1
Debug.Print .Fields(i).Name
Next i
'Loop through recordset
While Not (.EOF Or .BOF)
'Loop through/print each column value to Immediate Window
For i = 0 To .Fields.Count - 1
Debug.Print .Fields(i)
Next i
.MoveNext
Wend
.Close 'Close recordset
End With
.Close 'Close connection to file
End With
Set rs = Nothing
Set cn = Nothing
End Sub
I know that I said doing this is extremely simple in my comments above and that this looks like a lot of work but I assure you it's not. You could use ONLY the QueryText() method and end up with similar results. However, I've included everything else to try and give you some ideas of where you can take this for your project as well as to show you how to solve problems you might run into if you're not getting the results back that you expected.
This is the guide I originally learned from: http://msdn.microsoft.com/en-us/library/ms974559.aspx
Here is a guide for doing the same thing to actual Excel files: http://support.microsoft.com/kb/257819
Lastly, here's more info on Schema.ini files: http://msdn.microsoft.com/en-us/library/windows/desktop/ms709353(v=vs.85).aspx
Hopefully you're able to find a way to make use of all this information in your line of work! A side benefit to learning all of this is that you can use ADO to query actual databases like Access, SQL Server, and Oracle. The code is nearly identical to what is printed here. Just swap out the connection string, sql string, and ignore the whole bit about a Schema.ini file.
Here are 2 example VBA UDFs that work on a single cell: enter the formula as
=AVERAGE(len_text(SPLIT_TEXT(A1,"|")))
Note that in this particular case you don't actually need the len_text function, you could use Excel's LEN() instead, but then you would have to enter the AVERAGE(..) as an array formula.
Option Explicit
Public Function Split_Text(theText As Variant, Delimiter As Variant) As Variant
Dim var As Variant
var = Split(theText, Delimiter)
Split_Text = Application.WorksheetFunction.Transpose(var)
End Function
Public Function Len_Text(something As Variant) As Variant
Dim j As Long
Dim k As Long
Dim var() As Variant
If IsObject(something) Then
something = something.Value2
End If
ReDim var(LBound(something) To UBound(something), LBound(something, 2) To UBound(something, 2))
For j = LBound(something) To UBound(something)
For k = LBound(something, 2) To UBound(something, 2)
var(j, k) = Len(something(j, k))
Next k
Next j
Len_Text = var
End Function
I need to open a query or recordset or something (datasheet view) with some sql i build in my vba based on form control values. I have a "many to many" relationship in my data (kind of). Pretend we have a Person table, a Pet table and an ID Pet_Person table. I want to have a list of my People with a concatenated string of all their pets in one row of returned data. So....
Row Person Pets
1 Kim Lucius, Frodo, Cricket, Nemo
2 Bob Taco
And I googled and I found you can write functions in the VBA to be called from the SQL. So. Here's the problem. I have a lot of records and once opened, I cannot move around the datasheet view/query/whatever without that concatenation function being called everytime I click on a row. I only need it called once when the sql is initially ran... like a snapshot or something.
I'm not too well versed with Access and the possibilities. I've tried some things I found online that all had the same result... that concatenation function being called when I touched that resulting dataset at all.
Last thing I tried looks something like:
With db
Set qdf = .CreateQueryDef("Search Results", q)
DoCmd.OpenQuery "Search Results", , acReadOnly
.QueryDefs.Delete "Search Results"
End With
StackOverflow really never formats my stuff correctly. Probably user error.... oh, well.
Edit:
Oh Bart S. Thank you but you went away too soon for me to understand the answer if it is there. Thank you.
Oh Remou. Yes, I saw your post. I used your post. I've used many of your posts while working on this project. Why access doesn't support all SQL functions I am so used to with MySQL I have no idea. You're a great addition to this site. I should have linked to it with my question but the coffee hadn't kicked in yet.
I have my concatenation function and I am calling it within the sql. I was opening it with the docmd to open that recorset or query or whatever. But here is my issue (and I may be creating this myself by trying too many solutions at once or I might be overlooking something)... it keeps calling that function each time I touch the resulting dataset/query/thing and there's too much data for that to be happening; I am seeing the hourglass simply too much. I am positive this is because of the way I am opening it. This is intended to be the result of a search form screen thing. I'm thinking I need to just literally make another form in access and populate it with my resulting recordset. I think that is what you guys are telling me. I'm not sure. This is a weird example. But... you know with Excel, when you write an inline function of some kind to get some value for each row... and then you do a copy and paste special for just values (so not the function)... I need that. Because this function (not in Excel, obviously) must query and that takes to long to reapply each time a row is clicked on (I think it's actually requerying each row if a single row is clicked on, almost like it's rerunning the sql or something). Like the NIN/Depeche Mode song Dead Souls... It keeps calling me/it.
Here are a few thoughts and strategies for coping with the issue of constant data re-loading:
Make sure your query is set to snapshot. Same for the form.
This of course makes the data read-only, but it may help a bit.
Cache the result of your query into a local table, then show/bind that table instead of the query itself.
This will make the user wait a bit longer initially while the query is executed and saved into the local table, but it makes the interface much smoother afterwards since all data is local and doesn't need to be re-calculated.
Create a local table localPetResult (on the client side) that has all the fields matching those of the query.
Instead of binding the query itself to the datasheet form, bind the localPetResult to it, then in the form's VBA module handle the OnLoad event:
Private Sub Form_Load()
' Remove all previous data from the local cache table '
CurrentDb().Execute "DELETE FROM localPetResult"
' Define the original query '
Dim q as String
q = q & "SELECT Row, "
q = q & " Person, "
q = q & " Coalesce(""SELECT PetName FROM Pets WHERE Person='"" & [Person] & ""',"","") AS PetNames "
q = q & "FROM MyData"
' Wrap the query to insert its results into the local table '
q = "INSERT INTO localPetResult " & q
' Execute the query to cache the data '
CurrentDb().Execute q
End Sub
One you have it working, you can improve on this in many ways to make it nicer (freeze the screen and display the hourglass, dynamically bind the ersult table to the form after the data has been calculated, etc)
Cache the result of each call to the coalescing function.
I've used that to calculate the concatenation once for each record, then store the result in a Dictionary whose key is the ID of the record. Subsequent calculations for the same ID are just pulled from the Dictionary instead of re-calculated.
For instance, add the following to a VBA module. I'll assume that you use Remou's Coalesce function as well.
Option Compare Database
Option Explicit
' A Scripting.Dictionary object we'll use for caching '
Private dicCache As Object
' Call to initialise/reset the cache before/after using it '
Public Sub ResetCoalesceCache()
If Not (dicCache Is Nothing) Then
dicCache.RemoveAll
End If
End Sub
' Does the Same as Coalesce() from Remou, but attempts to '
' cache the result for faster retrieval later '
Public Function Coalesce2(key As Variant, _
sql As String, _
sep As String, _
ParamArray NameList() As Variant) As Variant
' Create the cache if it hasn't been initialised before '
If dicCache Is Nothing Then
Set dicCache = CreateObject("Scripting.Dictionary")
End If
If IsNull(key) Then
' The key is invalid, just run the normal coalesce '
Coalesce2 = Coalesce(sql, sep, NameList)
ElseIf dicCache.Exists(key) Then
' Hurray, the key exists in the cache! '
Coalesce2 = dicCache(key)
Else
' We need to calculate and then cache the data '
Coalesce2 = Coalesce(sql, sep, NameList)
dicCache.Add(key, Coalesce2)
End If
End Function
Then, to use it in your query:
' first clear the cache to make sure it doesn't contain old '
' data that could be returned by mistake '
ResetCoalesceCache
' Define the original query '
Dim q as String
q = q & "SELECT Row, "
q = q & " Person, "
q = q & " Coalesce2([Row], ""SELECT PetName FROM Pets WHERE Person='"" & [Person] & ""',"","") AS PetNames "
q = q & "FROM MyData"
' Bind to your form or whatever '
...
I always do it like this:
Dim strSql As String
strSql = "SELECT * FROM table WHERE field=something;"
Set rs = CurrentDb.OpenRecordSet(strSql)
Then use RS to perform actions. There may be better ways. You can, for example, create a query directly in Access and call it from VBA.
While looping the recordset, you can concatenate the string:
Dim strResult As String
While (Not rs.EOF)
strResult = strResult & rs!yourfield
WEnd
We are building a client program where parameters for storage in a web server with Oracle backend are set in the .Net client program and uploaded as a dataset via webservice.
In the webservice code, data is read from the dataset and added to UPDATE statements on the web server (Oracle backend).
Because the server will run on the customer's LAN behind a firewall and because of the dynamic nature of the parameters involved, no sprocs are being used - SQL strings are built in the logic.
Here is an example string:
UPDATE WorkOrders
SET TravelTimeHours = :TravelTimeHours,
TravelTimeMinutes = :TravelTimeMinutes,
WorkTimeHours = :WorkTimeHours,
WorkTimeMinutes = :WorkTimeMinutes,
CompletedPersonID = :CompletedPersonID,
CompletedPersonName = :CompletedPersonName,
CompleteDate = :CompleteDate
WHERE WorkOrderNumber = :WorkOrderNumber
When debugging code in VS 2010 and stepping into the server code, we receive the following error:
ORA-01036: illegal variable name/number
when executing the SQL command on destination oracle machine, we were prompted to enter the bind
variables for the above statement, and as long as we used the correct date format, the UPDATE statement
worked correctly.
QUESTIONS:
1) is it possible that oracle threw the ORA-01036 error when the month format was wrong?
2) why don't we have to convert the date format from the ASP.net website running on the Oracle machine?
does Oracle have a default conversion routine that excludes the bind variable entry screen?
3) if the date format was not the problem, what precisely does ORA-1036 mean and how do I discover
WHICH variable had an illegal name/number?
This is a snippet of a function that takes the type of the dataset (WOName) and returns the appropriate SQL string.
Many Cases exist but have been removed for readability.
Private Function GetMainSQLString(ByVal WOName As String) As String
Dim Result As String = ""
Select Case WOName
Case "Monthly Site Inspection"
Dim sb As New StringBuilder
sb.Append("UPDATE WorkOrders SET ")
sb.Append("CompletedPersonID = :CompletedPersonID, CompletedPersonName = :CompletedPersonName, CompleteDate = :CompleteDate, ")
sb.Append("SupervisorID = :SupervisorID, SupervisorName = :SupervisorName ")
sb.Append("WHERE WorkOrderNumber = :WorkOrderNumber")
Result = sb.ToString
End Select
Return Result
End Function
This is a snippet of a function that takes the Oracle command object byRef and adds the required parameters to it,
depending upon which of the possible 15 types of dataset(WOName) is received from the client program.
Many Cases exist but have been removed for readability.
The updated Cmd object is then returned to the main program logic, where ExecuteNonQuery() is called.
The test values of params below are as follows:
dr.Item("CompletedPersonID") 21
dr.Item("CompletedPersonName") Pers Name
dr.Item("CompleteDate") #8/16/2010#
dr.Item("SupervisorID") 24
dr.Item("SupervisorName") Sup Name
dr.Item("WorkOrderNumber") 100816101830
Private Function addMainCmdParams(ByVal WOName As String, ByRef cmd As OracleCommand, ByVal dr As DataRow) As OracleCommand
Select Case WOName
Case "Monthly Site Inspection"
cmd.Parameters.Add(":CompletedPersonID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("CompletedPersonID")
cmd.Parameters.Add(":CompletedPersonName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("CompletedPersonName")
cmd.Parameters.Add(":CompleteDate", Oracle.DataAccess.Client.OracleDbType.Date).Value = dr.Item("CompleteDate")
cmd.Parameters.Add(":SupervisorID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("SupervisorID")
cmd.Parameters.Add(":SupervisorName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("SupervisorName")
cmd.Parameters.Add(":WorkOrderNumber", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("WorkOrderNumber")
End Select
Return cmd
End Function
While running this today, this precise code WAS successful; but another similar case was not. I still distrust any implicit typecasting performed by Oracle (if any) - and I'm especially suspicious of how Oracle handles any of these parameters that are passed with a dbNull.value - and I know it's going to happen. so if that's the problem I'll have to work around it. There are too many optional parameters and columns that don't always get values passed in for this system to break on nulls.
One Oracle "gotcha" that can cause this error is the fact that, by default, Oracle maps parameters to parameter symbols in the query by sequence, not by name. If the number/type of parameters does not match, you get an error like this one.
The solution is to tell Oracle to bind by name:
cmd.BindByName = true
Without diving into the details of your code, this may or may not be the answer to your specific problem, but this setting should be the default, and should be part of any command setup that uses parameters. It's rather amazing to watch this one statement fix some obscure problems.
EDIT: This assumes that you're using Oracle's data access provider. In .NET, you should be using this, not Microsoft's Oracle provider.
The error has nothing to do with date formats, it means that a variable in the statement was not bound.
Could be as simple as a spelling mistake (would be nice if Oracle included the variable name in the error message).
Can you update your question with the surrounding code that creates, binds, and executes the statement?
This is a snippet of a function that takes the type of the dataset (WOName) and returns the appropriate SQL string.
Many Cases exist but have been removed for readability.
Private Function GetMainSQLString(ByVal WOName As String) As String
Dim Result As String = ""
Select Case WOName
Case "Monthly Site Inspection"
Dim sb As New StringBuilder
sb.Append("UPDATE WorkOrders SET ")
sb.Append("CompletedPersonID = :CompletedPersonID, CompletedPersonName = :CompletedPersonName, CompleteDate = :CompleteDate, ")
sb.Append("SupervisorID = :SupervisorID, SupervisorName = :SupervisorName ")
sb.Append("WHERE WorkOrderNumber = :WorkOrderNumber")
Result = sb.ToString
End Select
Return Result
End Function
This is a snippet of a function that takes the Oracle command object byRef and adds the required parameters to it,
depending upon which of the possible 15 types of dataset(WOName) is received from the client program.
Many Cases exist but have been removed for readability.
The updated Cmd object is then returned to the main program logic, where ExecuteNonQuery() is called.
The test values of params below are as follows:
dr.Item("CompletedPersonID") 21
dr.Item("CompletedPersonName") Pers Name
dr.Item("CompleteDate") #8/16/2010#
dr.Item("SupervisorID") 24
dr.Item("SupervisorName") Sup Name
dr.Item("WorkOrderNumber") 100816101830
Private Function addMainCmdParams(ByVal WOName As String, ByRef cmd As OracleCommand, ByVal dr As DataRow) As OracleCommand
Select Case WOName
Case "Monthly Site Inspection"
cmd.Parameters.Add(":CompletedPersonID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("CompletedPersonID")
cmd.Parameters.Add(":CompletedPersonName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("CompletedPersonName")
cmd.Parameters.Add(":CompleteDate", Oracle.DataAccess.Client.OracleDbType.Date).Value = dr.Item("CompleteDate")
cmd.Parameters.Add(":SupervisorID", Oracle.DataAccess.Client.OracleDbType.Int32).Value = dr.Item("SupervisorID")
cmd.Parameters.Add(":SupervisorName", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("SupervisorName")
cmd.Parameters.Add(":WorkOrderNumber", Oracle.DataAccess.Client.OracleDbType.Varchar2).Value = dr.Item("WorkOrderNumber")
End Select
Return cmd
End Function
While running this today, this precise code WAS successful; but another similar case was not. I still distrust any implicit typecasting performed by Oracle (if any) - and I'm especially suspicious of how Oracle handles any of these parameters that are passed with a dbNull.value - and I know it's going to happen. so if that's the problem I'll have to work around it. There are too many optional parameters and columns that don't always get values passed in for this system to break on nulls.