When a new row was added programmatically using an auto generated method on my strongly typed DataTable, How can I fire my custom validation which validate the maxleng of my field?
My client (C#)
DAL.ImportMarcDataSet.PublicationsRow newRow = importMarcDataSet.Publications.NewPublicationsRow();
newRow.CallNumber ="QA76.76.A65";
newRow.Title = "Programming WCF services";
newRow.ISBN = "0596526997";
importMarcDataSet.Publications.AddPublicationsRow(newRow);
My Data Access Layer (VB)
Partial Class ImportMarcDataSet
Partial Class PublicationsDataTable
Private Sub CallNumberMaxLength(ByVal pRow As PublicationsRow)
If pRow.CallNumber.Length > 25 Then
pRow.SetColumnError("CallNumber", "The value entered is over the maximum length")
Else
pRow.SetColumnError("CallNumber", "")
End If
End Sub
'this event is ok when user made changes to the CallNumber column of the current row
Private Sub PublicationsDataTable_ColumnChanged(ByVal sender As Object, ByVal e As System.Data.DataColumnChangeEventArgs) Handles Me.ColumnChanged
If e.Column Is Me.CallNumberColumn Then
CallNumberMaxLength(e.Row)
End If
End Sub
End Class
End Class
You can handle the table's RowChanging event. When the DataRowChangeEventArgs.Action is equal to Add or one of the change... actions do your validation.
It has been a long time since I did this, but I believe you can even cancel the edit if needed by calling CancelEdit on the DataRowChangeEventArgs.Row. Check the documentation. See Handling DataTable Events (ADO.NET) at http://msdn.microsoft.com/en-us/library/w9y9a401.aspx.
The TableNewRow will not help because it is only raised when NewRow is called.
Related
I have text boxes on several forms in my application that have a "zoom" capability -- double-click on them and a pop-up form appears which you can resize and edit to your hearts content. This is coded via a class module "appZoomText" which acts as an 'event sink' for the text box events.
I'm trying to create a content menu and ribbon item which replicates the double click behaviour (for those of my users who don't read documentation but might wonder what a zoom icon does if they see it.)
On entry to a zoomable text box, this code is executed:
dim mclsZoomtext as appZoomtext
set mclsZoomtext = new appZoomtext
Set mclsZoomText.pTextBox = ActiveControl
In the class module, the following code is executed in Set pTextBox
Private WithEvents myTextBox As Access.TextBox
Set myTextBox = pTextBox
myTextBox.OnDblClick = "[Event Procedure]"
The double-click code for myTextBox is:
Private Sub myTextBox_DblClick(intCancel As Integer)
OpenZoomForm 'Opens the relevant form with the right contents -- works fine
End Sub
The relevant menu item is enabled in Set ptextBox and disabled when pTextBox is exited (this is working fine).
The code invoked when the context menu item is clicked is:
Public Function OnActionZoom() As Boolean
Dim ctl As Control
Set ctl = GetCurrentControl
'GetCurrentControl returns the current Control object on a form or subform and works fine
CallByName ctl.Parent, ctl.Name & "_DblClick", VbMethod
OnActionZoom = True
End Function
I get an error 2465 ("can't find the field referred to") on the CallByName line. I'm assuming that this is because the DblClick code is in the event sink not the form. ctl.parent and ctl.name are set correctly.
How can I code this to invoke the event sink code?
Update: I've tried creating an empty (Public) Field_DblClick sub in the form; doesn't help - this empty sub runs but the event in the class module doesn't fire. Neither does making myTextBox_DblClick Public instead of Private.
In this instance I could just invoke OpenZoomForm directly, or maybe I could have a single public instance of appZoomText that gets associated with different textboxes as required (I haven't tried this).
However, I need to use a similar method to create a menu item to drill-down in a number of comboboxes and textboxes -- double-clicking in the combo-box/textbox opens an edit form for the item in the ox -- but the edit form isn't always the same, and I can't have a single public instance of each event sink.
Similar code:
Combobox_Enter or TextBox_Enter:
dim mclsClass as ClassX
set mclsClass as new ClassX
set mclsClass.ComboBox = ActiveControl 'Or set mclsClass.textbox = activecontrol
in ClassX:
Private Withevents myComboBox as ComboBox
Public Property Set ComboBox (pctlComboBox as ComboBox)
set myComboBox = pctlComboBox
myComboBox.OnDblClick = "[Event Procedure]"
End Property
Private WithEvents myTextBox as TextBox
Public Property Set TextBox(pctltextBox as TextBox)
set myTextBox= pctltextBox
myTextBox.OnDblClick = "[Event Procedure]"
End Property
Public Sub myComboBox_DblClick(intCancel As Integer)
If Not IsNull(KeyID(myComboBox.Text)) Then 'Check that there is a record to edit
EditFormX(myComboBox) 'EditFormX depends on the Class
'Some more code in here depending on the user's edit
myComboBox.Requery
End If
End Sub
Public Sub mytextBox_DblClick(intCancel As Integer)
If Not IsNull(KeyID(myTextBox.Text)) Then 'Check that there is a record to edit
EditFormX(myTextBox) 'EditFormX depends on the Class
'Some more code in here depending on the user's edit
myTextBox.Requery
End If
End Sub
For CallByName to work, you need to ensure the following:
1) The method being called is public not private.
2) All required parameters are passed.
As such, you need to make the event handlers public (as you are now doing), and pass an additional argument to CallByName for the DblClick handers' Cancel parameter. Since you aren't doing anything with that parameter inside the methods themselves, passing just 0 will do:
CallByName Ctl.Parent, Ctl.Name + "_DblClick", 0
Update 1 - example of this working [note this proves too simple in the OP's case - see Update 2 below]
a) Create a new Access project.
b) Add a new blank form to the project, and a TextBox to the form.
c) Double click the text box's On DblClick event in the Properties window, choosing the Code Builder option if prompted.
d) Add the following code for the handler:
Private Sub Text1_DblClick(Cancel As Integer)
MsgBox "Hello World!"
End Sub
e) Amend the method's header so that it reads Public Sub not Private Sub
f) View (open) the form, and focus the text box by clicking inside it.
g) Go back into the VBA editor, and add a new standard module.
h) Add the following sub-routine to the module:
Sub Test()
Dim Ctl As Access.Control
Set Ctl = Screen.ActiveControl
CallByName Ctl.Parent, Ctl.Name + "_DblClick", VbMethod, 0
End Sub
i) With the caret inside the Test routine, press F5 or click the Run button
On this, the 'Hello World' message appears for me.
Update 2
With the use of WithEvents now explicit, a demo of something that may work in your situation:
1) In Access, create a new database and add a blank form to it.
2) Add a text box and a combo box to the form, and call then txtTest and cboTest; next, add three command buttons, calling them cmdCreateControllers, cmdDestroyControllers and cmdExecuteController respectively, and setting their captions to 'Create controllers', 'Destroy controllers' and 'Execute controller'. Also set cmdDestroyControllers and cmdExecuteController's Enabled properties to False.
3) In the VBA editor, add a class module, rename it IController, and add the following code:
Option Explicit
Sub Execute()
End Sub
This is our interface type (i.e., abstract class definition).
4) Via Tools|References..., add a reference to 'Microsoft Scripting Runtime', of whose Dictionary class we will shortly be using.
5) Add a standard module, and the following code to it:
Option Explicit
Private mControllers As New Scripting.Dictionary
Sub RegisterController(Obj As Object, Controller As IController)
mControllers.Add Obj, Controller
End Sub
Sub UnregisterController(Obj As Object)
mControllers.Remove Obj
End Sub
Function GetController(Obj As Object) As IController
Set GetController = mControllers(Obj)
End Function
Function IController_Initialize(Controller As IController, _
OldObj As Object, NewObj As Object) As Object
If Not (NewObj Is OldObj) Then
If Not (OldObj Is Nothing) Then UnregisterController OldObj
If Not (NewObj Is Nothing) Then RegisterController NewObj, Controller
End If
Set IController_Initialize = NewObj
End Function
The last function here is a helper one for IController implementations. Let's now create a couple -
6) Add another class module, rename it MyTextBoxController, and add the following code:
Option Explicit
Implements IController
Private WithEvents mTextBox As Access.TextBox
Property Set TextBox(NewValue As Access.TextBox)
Set mTextBox = IController_Initialize(Me, mTextBox, NewValue)
If Not (mTextBox Is Nothing) Then mTextBox.OnDblClick = "[Event Procedure]"
End Property
Private Sub mTextBox_DblClick(Cancel As Integer)
IController_Execute
End Sub
Private Sub IController_Execute()
MsgBox "Hello from the example text box controller!"
End Sub
7) Add another class module, rename it MyComboBoxController, and add the following code:
Option Explicit
Implements IController
Private WithEvents mComboBox As Access.ComboBox
Property Set ComboBox(NewValue As Access.ComboBox)
Set mComboBox = IController_Initialize(Me, mComboBox, NewValue)
If Not (mComboBox Is Nothing) Then mComboBox.OnDblClick = "[Event Procedure]"
End Property
Private Sub mComboBox_DblClick(Cancel As Integer)
IController_Execute
End Sub
Private Sub IController_Execute()
MsgBox "Hello from the example combo box controller!"
End Sub
8) Go back the form and handle cmdCreateControllers' Click event as thus:
Option Explicit
Private mTextBoxController As MyTextBoxController, mComboBoxController As MyComboBoxController
Private Sub cmdCreateControllers_Click()
If mTextBoxController Is Nothing Then Set mTextBoxController = New MyTextBoxController
Set mTextBoxController.TextBox = txtTest
If mComboBoxController Is Nothing Then Set mComboBoxController = New MyComboBoxController
Set mComboBoxController.ComboBox = cboTest
cmdDestroyControllers.Enabled = True
cmdExecuteController.Enabled = True
End Sub
9) Handle the other two buttons' Click events like this:
Private Sub cmdDestroyControllers_Click()
cmdCreateControllers.SetFocus
cmdDestroyControllers.Enabled = False
cmdExecuteController.Enabled = False
Set mTextBoxController.TextBox = Nothing
Set mComboBoxController.ComboBox = Nothing
End Sub
Private Sub cmdExecuteController_Click()
Dim Name As String
Name = InputBox("Enter the name of the control whose controller you want to execute:")
If Name = "" Then Exit Sub
GetController(Me.Controls(Name)).Execute ' add error handling as desired!
End Sub
10) Open the form, and double click either the text box or the combo box - nothing should happen.
11) Click Create Controllers, and double click again: a message box should show.
12) Click Execute Controller, and enter txtTest: a message box should again show.
13) Unhook the custom event sinks and unregister them as object controllers by clicking Destroy Controller; having done that, double clicking either of the subject controls should once more do nothing.
I've found the following works for the "zoomable" boxes, where the name of the relevant class and the textbox control within it are always the same.
Within the form:
Public mclsZoomText As appZoomText
and on entry to the control:
set mclsZoomtext = new appZoomtext
Set mclsZoomText.pTextBox = ActiveControl
Within the class:
Public WithEvents myTextBox As Access.TextBox
Public Sub myTextBox_DblClick(intCancel As Integer)
OpenZoomForm
End Sub
and when the zoom option is invoked from the menu:
Public Function OnActionZoom() As Boolean
Dim ctl As Control
dim intX as Integer
Set ctl = GetCurrentControl
'GetCurrentControl returns the current Control object on a form or subform
Call ctl.Parent.mclsZoomText.myTextBox_DblClick(intX)
OnActionZoom = True
End Function
For the more complex case this answer is excellent.
I am writing my first classes.
One is cCRElist which is essentially a collection of cCRE instances (some specialized events).
I want there to be a sub or function inside cCRElist that will load all the CRE's from the worksheet into one big collection I can work with. I created the function and it worked OK when I called it from a normal code module, but then I tried to move the code into the class. Now I am having trouble calling the function LoadFromWorksheet(myWS as Worksheet).
The error is "object does not support this property or method". I have tried making it a sub, a function, making it public, not public, I have tried turning into a Property Let instead of a sub. Obviously I have a flimsy grasp on what that does. I have tried
Call CREList.LoadFromWorksheet(myWS)
and
CREList.LoadfromWorksheet myWS
Same error every time.
Here is the test code that uses the class and calls the function:
Sub TestClassObj()
Dim CRElist As cCRElist
Set CRElist = New cCRElist
Dim myWS As Worksheet
Set myWS = ThisWorkbook.ActiveSheet
CRElist.LoadFromWorksheet (myWS)
End Sub
Here is a snippet of the class cCRElist:
' **** CLASS cCRElist
Option Explicit
' This is a collection of CRE objects
Private pCRElist As Collection
Private Sub Class_Initialize()
Set pCRElist = New Collection
End Sub
Public Property Get CREs() As Collection
Set CREs = pCRElist
End Property
Public Property Set Add_CRE(aCRE As cCRE)
pCRElist.Add aCRE
End Property
Function LoadFromWorksheet(myWS As Worksheet)
Dim CRE As cCRE
Dim iFirst As Long
Dim iLast As Long
Dim i As Long
Set CRE = New cCRE
iFirst = gHeader_Row + 1
iLast = FindNewRow(myWS) - 1
' update data in CRE then add
For i = iFirst To iLast
If myWS.Cells(i, gCRE_Col) <> "" Then ' This is a CRE row
Set CRE = New cCRE
With CRE
.CRE_ID = myWS.Cells(i, gCRE_Col)
If Not IsDate(myWS.Cells(i, gCRE_ETA_Col)) Then
.ETA = "1/1/1900"
Else
.ETA = Trim(myWS.Cells(i, gCRE_ETA_Col))
End If
<... snipped ...>
End With
pCRElist.Add_CRE CRE
End If
Next
End Sub
' **** END OF CLASS cCRElist
Thanks for your expertise.
Here is what worked based on help I got in the comments. First, I did the "break in class module". In the test code, I changed the function call from:
CRElist.LoadFromWorksheet(myWS)
to
CRElist.LoadFromWorksheet myWS
Inside the class, i had to change
Set pCRElist.Add_CRE CRE
to
pCRElist.Add CRE
Then I was able to get rid of extraneous CLASS functions Add_CRE and Count.
Thanks for everyone's input. I couldn't figure out how to mark comments as accepted answers so I did this. Let me know if I need to do something differently.
Now it works!
I'm trying to create a class that manages the events of a combo box in Access 2010. Here you have the code:
Class TestEvents
Public WithEvents cbMyCombo As Access.ComboBox
Public Property Get AssociatedCombo() As Access.ComboBox
Set AssociatedCombo = cbMyCombo
End Property
Public Sub cbMyCombo_Change()
MsgBox "Combo has changed!"
End Sub
Private Sub Class_Initialize()
Set cbMyCombo = Form_Form1.Combo1
End Sub
Form1 Code (Contains a combobox named Combo1)
Option Compare Database
Option Explicit
Private MyTestEvents As TestEvents
Private Sub Form_Load()
Set MyTestEvents = New TestEvents
MsgBox MyTestEvents.AssociatedCombo.Name
End Sub
When running the code, I get (as expected) a message with the combobox name (Combo1), so the TestEvents.AssociatedCombo property is pointing to the right object but nothing happens when I change the combo value. I would expect to get the message "Combo has changed".
Is there anything I'm doing wrong?
Thanks in advance for your help :)
By default events sunk for a form control are not raised in VBA at all; to enable them (and subsequently enable your WithEvents) you need to wire up each event you want to handle;
Set cbMyCombo = Form_Form1.Combo1 '//please put this in a property!
cbMyCombo.OnKeyDown = "[Event Procedure]"
cbMyCombo.OnBlaDeBla = "[Event Procedure]"
(Note that's the actual string value you need to set, not a placeholder/example)
You can also do this on the Events tab of the control properties.
I have problem with a setter in grails. I have two properties beforeTax and afterTax. I only want to store one property in the db beforeTax. In the ui I want the user to enter either before or after tax. So I made the afterTax a transient property like this:
double getAfterTax(){
return beforeTax * tax
}
void setAfterTax(double value){
beforeTax = value / tax
}
When I now enter the after tax value and want to save the object the validation fails (before tax can not be an empty value)
What am I doing wrong?
If I understand your question correctly, you want to compute beforeTax based on the value of afterTax?
You could use the event handler methods beforeXXX() where XXX is Validate, Insert, and Update to compute beforeTax. Then beforeTax's constraint can be nullable:false.
def beforeValidate() {
computeBeforeTax()
}
You have to flag one property as transient, in order to prevent GORM from trying to save this variable into DB. Try to add this line into your domain class, which contains afterTax.
static transients = ['afterTax']
I have a very simple mapping function called "BuildEntity" that does the usual boring "left/right" coding required to dump my reader data into my domain object. (shown below) My question is this - If I don't bring back every column in this mapping as is, I get the "System.IndexOutOfRangeException" exception and wanted to know if ado.net had anything to correct this so I don't need to bring back every column with each call into SQL ...
What I'm really looking for is something like "IsValidColumn" so I can keep this 1 mapping function throughout my DataAccess class with all the left/right mappings defined - and have it work even when a sproc doesn't return every column listed ...
Using reader As SqlDataReader = cmd.ExecuteReader()
Dim product As Product
While reader.Read()
product = New Product()
product.ID = Convert.ToInt32(reader("ProductID"))
product.SupplierID = Convert.ToInt32(reader("SupplierID"))
product.CategoryID = Convert.ToInt32(reader("CategoryID"))
product.ProductName = Convert.ToString(reader("ProductName"))
product.QuantityPerUnit = Convert.ToString(reader("QuantityPerUnit"))
product.UnitPrice = Convert.ToDouble(reader("UnitPrice"))
product.UnitsInStock = Convert.ToInt32(reader("UnitsInStock"))
product.UnitsOnOrder = Convert.ToInt32(reader("UnitsOnOrder"))
product.ReorderLevel = Convert.ToInt32(reader("ReorderLevel"))
productList.Add(product)
End While
Also check out this extension method I wrote for use on data commands:
public static void Fill<T>(this IDbCommand cmd,
IList<T> list, Func<IDataReader, T> rowConverter)
{
using (var rdr = cmd.ExecuteReader())
{
while (rdr.Read())
{
list.Add(rowConverter(rdr));
}
}
}
You can use it like this:
cmd.Fill(products, r => r.GetProduct());
Where "products" is the IList<Product> you want to populate, and "GetProduct" contains the logic to create a Product instance from a data reader. It won't help with this specific problem of not having all the fields present, but if you're doing a lot of old-fashioned ADO.NET like this it can be quite handy.
Although connection.GetSchema("Tables") does return meta data about the tables in your database, it won't return everything in your sproc if you define any custom columns.
For example, if you throw in some random ad-hoc column like *SELECT ProductName,'Testing' As ProductTestName FROM dbo.Products" you won't see 'ProductTestName' as a column because it's not in the Schema of the Products table. To solve this, and ask for every column available in the returned data, leverage a method on the SqlDataReader object "GetSchemaTable()"
If I add this to the existing code sample you listed in your original question, you will notice just after the reader is declared I add a data table to capture the meta data from the reader itself. Next I loop through this meta data and add each column to another table that I use in the left-right code to check if each column exists.
Updated Source Code
Using reader As SqlDataReader = cmd.ExecuteReader()
Dim table As DataTable = reader.GetSchemaTable()
Dim colNames As New DataTable()
For Each row As DataRow In table.Rows
colNames.Columns.Add(row.ItemArray(0))
Next
Dim product As Product While reader.Read()
product = New Product()
If Not colNames.Columns("ProductID") Is Nothing Then
product.ID = Convert.ToInt32(reader("ProductID"))
End If
product.SupplierID = Convert.ToInt32(reader("SupplierID"))
product.CategoryID = Convert.ToInt32(reader("CategoryID"))
product.ProductName = Convert.ToString(reader("ProductName"))
product.QuantityPerUnit = Convert.ToString(reader("QuantityPerUnit"))
product.UnitPrice = Convert.ToDouble(reader("UnitPrice"))
product.UnitsInStock = Convert.ToInt32(reader("UnitsInStock"))
product.UnitsOnOrder = Convert.ToInt32(reader("UnitsOnOrder"))
product.ReorderLevel = Convert.ToInt32(reader("ReorderLevel"))
productList.Add(product)
End While
This is a hack to be honest, as you should return every column to hydrate your object correctly. But I thought to include this reader method as it would actually grab all the columns, even if they are not defined in your table schema.
This approach to mapping your relational data into your domain model might cause some issues when you get into a lazy loading scenario.
Why not just have each sproc return complete column set, using null, -1, or acceptable values where you don't have the data. Avoids having to catch IndexOutOfRangeException or re-writing everything in LinqToSql.
Use the GetSchemaTable() method to retrieve the metadata of the DataReader. The DataTable that is returned can be used to check if a specific column is present or not.
Why don't you use LinqToSql - everything you need is done automatically. For the sake of being general you can use any other ORM tool for .NET
If you don't want to use an ORM you can also use reflection for things like this (though in this case because ProductID is not named the same on both sides, you couldn't do it in the simplistic fashion demonstrated here):
List Provider in C#
I would call reader.GetOrdinal for each field name before starting the while loop. Unfortunately GetOrdinal throws an IndexOutOfRangeException if the field doesn't exist, so it won't be very performant.
You could probably store the results in a Dictionary<string, int> and use its ContainsKey method to determine if the field was supplied.
I ended up writing my own, but this mapper is pretty good (and simple): https://code.google.com/p/dapper-dot-net/