How to get available classes at runtime - forms

I would like to poll a class and have it return all available subclasses in a way I can then address them. It can return an array, a dictionary, or something else. As long as I can then loop through the set of them and read properties or call functions from each.
Scenario:
I want to create a form where the user inputs the details of a series of events. Then I read the form and output a report. Each kind of event has a ".Name", but a different set of inputs (FormOptions) and methods (FormatOutput) to create an output. Right now this is implemented with a complex form and a script that runs after the user submits the form.
The trouble is that every time I add an option to the form, I have to change code in several different places. In the interest of making my code easier to maintain, I would like to contain all the code for each event type in a Class, then build the form based on the available Classes.
So as an example, I'm building an itinerary from a collection of Meeting and Travel objects:
Class Itinerary
Class Event
Public Property Get Name()
Name = "Meeting"
End Property
Public Function FormOptions(id)
Form Options = "<div id="& id &">form code for Meeting options</div>"
End Function
Public Sub FormatOutput(Time, Title, Location)
'Make a fancy meeting entry
End Sub
End Class
Class Travel
Public Property Get Name()
Name = "Travel"
End Property
Public Function FormOptions(id)
Form Options = "<div id="& id &">form code for Travel options</div>"
End Function
Public Sub FormatOutput(StartTime, EndTime, Origin, Destination)
'Make a fancy travel entry
End Sub
End Class
End Class
When the script runs it creates a form where the user can add a series of events. Each time the user chooses between "Meeting" and "Travel" then fills out the options for that event type. At the end, they push a button and the script makes a pretty document listing all the user's inputs.
At some point in the future, I will want to add a new kind of event: lodging.
Class Lodging
Public Property Get Name()
Name = "Lodging"
End Property
Public Function FormOptions(id)
Form Options = "<div id="& id &">form code for Lodging options</div>"
End Function
Public Sub FormatOutput(Date, Location)
'Make a fancy Lodging entry
End Sub
End Class
How do I setup my Itinerary class so that it automatically recognizes the new class and can return it as an available event type? I can think of several ways of doing this, but they all involve keeping an index of the available classes separate from the actual classes, and I'm trying to minimize the number of places I have to change code when I add new event types.
I am very purposely not including any details on how I build the form since at this point I'm open to anything. Also, please be gentle with references to "inheritance", "extensibility", or "polymorphism". I'm a just a scripter and these OOP concepts are still a bit foreign to me.

I don't think this is possible, but one way to come close to this would be to have a unique list of class names -- the simplest would be to have a global dictionary.
You can get a list of classes by reading the Keys collection of the dictionary.
I don't think VBScript supports references to classes, so when the user chooses one of the types use Eval to create an instance of the appropriate class.
Dim ClassList
Set ClassList = CreateObject("Scripting.Dictionary")
'on type selection:
Function GetItineraryInstance(className)
Set GetItineraryInstance = Eval("New " & className)
End Function
ClassList("Travel") = 1
Class Travel
'methods
End Class
At least the class registration can be kept together with the class definition.

Related

Apex DateTime to String then pass the argument

I have two Visualforce pages, and for several reasons I've decided it is best to pass a datetime value between them as a string. However, after my implementation my date always appear to be null even though my code seems to compile.
I have researched quite a few formatting topics but unfortunately each variation of the format() on date time seems to not produce different results.
The controller on page 1 declares two public variables
public datetime qcdate;
public String fdt;
qcdate is generated from a SOQL query.
wo = [SELECT id, WorkOrderNumber, Quality_Control_Timestamp__c FROM WorkOrder WHERE id=:woid];
qcdate = wo.Quality_Control_Timestamp__c;
fdt is then generated from a method
fdt = getMyFormattedDate(qcdate);
which looks like this
public String getMyFormattedDate(datetime dt){
return dt.format(); }
fdt is then passed in the URL to my next VF page.
String url = '/apex/workordermaintenanceinvoice?tenlan='+tenlan +'&woid=' + woid + '&invnum=' + invnum + '&addr1=' + addr1 + 'fdt=' + fdt;
PageReference pr = new PageReference(url);
I expected when calling {!fdt} on my next page to get a proper string. But I do not.
UPDATE:
Sorry I guess I should not have assumed that it was taken for granted that the passed variable was called correctly. Once the variable is passed the following happens:
The new page controller creates the variable:
public String fdt
The variable is captured with a getparameters().
fdt = apexpages.currentPage().getparameters().get('fdt');
The getfdt() method is created
public String getfdt(){
return fdt;
}
Which is then called on the VF page
{!fdt}
This all of course still yields a 'blank' date which is the mystery I'm still trying to solve.
You passed it via URL, cool. But... so what? Page params don't get magically parsed into class variables on the target page. Like if you have a record-specific page and you know in the url there's /apex/SomePage?id=001.... - that doesn't automatically mean your VF page will have anything in {!id} or that your apex controller (if there's any) will have id class variable. The only "magic" thing you get in apex is the standard controller and hopefully it'll have something in sc.getId() but that's it.
In fact it'd be very stupid and dangerous. Imagine code like Integer integer = 5, that'd be fun to debug, in next line you type "integer" and what would that be, the type or variable? Having a variable named "id" would be bad idea too.
If you want to access the fdt URL param in target page in pure Visualforce, something like {!$CurrentPage.parameters.fdt} should work OK. If you need it in Apex - you'll have to parse it out of the URL. Similar thing, ApexPages.currentPage() and then call getParameters() on it.
(Similarly it's cleaner to set parameters that way too, not hand-crafting the URL and "&" signs manually. If you do it manual you theoretically should escape special characters... Let apex do it for you.

Lookup Edit binding

I have a model written using Entity Framework Code First called Project.
In my UI, there is a page called ProjectEdit as shown in the image below. As seen in the image below, Customer and BOMs are Lookup Edit.
I'm trying to load Customer and BOMs to Lookup Edit but it's not working. Below is my code.
//New
if (entity == null)
{
Entity = new Project();
}
//Edit
else
{
ProjectCodeTextEdit.DataBindings.Add("EditValue", entity, "ProjectCode");
DescriptionTextEdit.DataBindings.Add("EditValue", entity, "Description");
CustomerLookUpEdit.DataBindings.Add("EditValue", entity, "CustomerId");
BOMsLookUpEdit.DataBindings.Add("EditValue", entity, "BOMs");
}
Below is my LookUpEdit Properties.
Generally LookUpEdit object's data binding is not implemented the same way as a TextEdit object's. While in TextEdits's case you just need to assign the variable value to EditValue property (I suppose your TextEdits binding work fine, isn't it?), with LookUp Edit you should assign variables to ValueMember and a DisplayMember properties of the object. That is why we usually display data rows with LookUpEdit objects, where ValueMember is the identification field of the row and DisplayMember is the field of the row whose value you wish to be displayed.
In your case you should be more clear about what you wish to display in your lookupedits. Each Project instance has one Customer property and many BOMs, right? So CustomerLookUpEdit will show one record and BOMsLookUpEdit a list of values according to the Project object that was chosen for edit, correct? I suppose that both your Customer and BOM classes have some kind of ID property and description property of their own. In this case you should bind these values to the LookUpEdits. eg. in your initialization function code add these lines
CustomerLookUpEdit.Properties.DataSource = entity.Customer;
CustomerLookUpEdit.Properties.ValueMember = "someCustomerIDpropertyName" ;
CustomerLookUpEdit.Properties.DisplayMember = "someCustomerDescriptionpropertyName";
BOMsLookUpEdit.Properties.DataSource = entity.BOMs;
BOMsLookUpEdit.Properties.ValueMember = "someBOMIDpropertyName" ;
BOMsLookUpEdit.Properties.DisplayMember = "someBOMDescriptionpropertyName" ;
You can read more in this topic https://documentation.devexpress.com/#WindowsForms/clsDevExpressXtraEditorsLookUpEdittopic
When we are adding entities to a List, we have to take care of our DataSource if is a DBContext or a DBSet, each one has implications in the compiler, that was your case, in this case you had to especify your DataSource like a DBSet and get the Entities
Add<TEntity>(TEntity entity)
The type parameter omitted is posible because the compiler will infer it.

Error 1004 - Vlookup in vba - Unable to get the Vlookup property of the WorksheetFunction class

I've browsed the various questions already asked with this issue other users have faced and none of the solutions seem to fix the error code coming up.
I have a form which prompts the user for a reference number - they input this into the text field and then press OK
'OK button from form1
Public Sub CommandButton1_Click()
refInput = refTextBox.Value
InputRef.Hide
ExportShipForm.Show
End Sub
Once this has been pressed, the next form appears which I would like to be populated with data based on the reference number input on the first form. I have an update button which will update the "labels" on the form to show the data - this is where I am getting an error.
The first label to update is through a Vlookup:
Below the users clicks the update button the 2nd form:
Public Sub btnUpdate_Click()
Call ICS_Caption
lbl_ICS.Caption = Label_ICS
End Sub
This calls a function below:
Public Sub ICS_Caption()
Dim ws1 As Worksheet
refInput = InputRef.refTextBox.Value
Set ws1 = Worksheets("MACRO")
dataRef = Worksheets("Shipping Data").Range("A:K")
Label_ICS = WorksheetFunction.VLookup(refInput, dataRef, 7, False)
End Sub
The error continues to come up each time - I have ran the vlookup manually in a cell outside of VBA and it works fine.
I have typed the range in the Vlookup whilst also using named ranges but each variation shows the same error.
Eventually, I would want the label on form 2 to update with the result of the Vlookup.
Any ideas?
You need to Dim dataRef as Range and then Set it.
See code Below:
Dim DataRef as Range
Set dataRef = Worksheets("Shipping Data").Range("A:K")
Just like a Workbook or Worksheet you need to Set the Range
Just as Grade 'Eh' Bacon suggest in comments its always best to Dim every reference.
The best way to do so is to put Option Explicit all the way at the top of your code. This forces you to define everything which helps it preventing mistakes/typo's etc.
Update edit:
The problem was you are looking for a Reference number in your Sheet (thus an Integer) but refInput is set as a String this conflicts and .VLookup throws an error because it can't find a match.
I have reworked your code:
Your sub is now a function which returns the .Caption String
Function ICS_Caption(refInput As Integer)
Dim dataRef As Range
Set dataRef = Worksheets("Shipping Data").Range("A:K")
ICS_Caption = WorksheetFunction.VLookup(refInput, dataRef, 7, False)
End Function
The update Button calls your Function and provides the data:
Public Sub btnUpdate_Click()
lbl_ICS.Caption = ICS_Caption(InputRef.refTextBox.Value)
End Sub
By using a Function you can provide the Integer value and get a return value back without the need of having Global Strings or Integers.
Which would have been your next obstacle as you can only transfer Variables between Modules/Userforms by using a Global Variable.
I would even advice to directly use the function in the Initialize Event of your 2nd Userform to load the data before the Userform shows this is more user friendly then needing to provide data and then still needing to push an update button.
Verify that you have no missing libraries in VBA IDE > Tools > References
Try using a worksheet cell as the place to store and retrieve refTextBox.Value, rather than refInput (which I assume is a global variable):
Public Sub CommandButton1_Click()
...
Worksheets("Shipping Data").Range($M$1).Value=refTextBox.Value
End Sub
Public Sub ICS_Caption()
Dim refInput as Long'My assumption
...
refInput=Worksheets("Shipping Data").Range($M$1).Value
...
End Sub
Make sure you have Option Explicit at the top of all of your code windows.

Passing arguments to Access Forms created with 'New'

I have a form called 'detail' which shows a detailed view of a selected record. The record is selected from a different form called 'search'. Because I want to be able to open multiple instances of 'detail', each showing details of a different record, I used the following code:
Public detailCollection As New Collection
Function openDetail(patID As Integer, pName As String)
'Purpose: Open an independent instance of form
Dim frm As Form
Debug.Print "ID: " & patID
'Open a new instance, show it, and set a caption.
Set frm = New Form_detail
frm.Visible = True
frm.Caption = pName
detailCollection.Add Item:=frm, Key:=CStr(frm.Hwnd)
Set frm = Nothing
End Function
PatID is the Primary Key of the record I wish to show in this new instance of 'detail.' The debug print line prints out the correct PatID, so i have it available. How do I pass it to this new instance of the form?
I tried to set the OpenArgs of the new form, but I get an error stating that OpenArgs is read only. After researching, OpenArgs can only be set by DoCmd (which won't work, because then I don't get independent instances of the form). I can find no documentation on the allowable parameters when creating a Form object. Apparently, Microsoft doesn't consider a Constructor to be a Method, at least according to the docs. How should I handle this? (plz don't tell me to set it to an invisible text box or something) Thanks guys, you guys are the best on the net at answering these questions for me. I love you all!
Source Code for the multi-instance form taken from: http://allenbrowne.com/ser-35.html
Inside your Form_detail, create a custom property.
Private mItemId As Long
Property Let ItemID(value as Long)
mItemId = value
' some code to re query Me
End Property
Property Get ItemId() As Long
ItemId = mItemId
End Property
Then, in the code that creates the form, you can do this.
Set frm = New Form_detail
frm.ItemId = patId
frm.Visible = True
frm.Caption = pName
This will allow you to pass an ID to the new form instance, and ensure it gets requeried before making it visible. No need to load all of the results every time if you're always opening the form by Newing it. You let the property load the data instead of the traditional Form_Load event.
This works because Access Form modules are nothing more than glorified classes. Hope this helps.
You could try applying a filter:
frm.Filter = "[ID] = " & patID
frm.FilterOn = True
The Record Source of the Detail form will need to be set to the table to which the ID belongs.
UPDATE
As you requested, here is the code to set the RecordSource:
frm.RecordSource = "select * from TableName where [ID] = " & patID
This is probably cleaner than using a filter given that a user can remove the filter (depending on the type of form).

VBA: Using WithEvents on UserForms

I have a Word userform with 60+ controls of varying types. I would like to evaluate the form every time a control_change event is triggered and change the enabled state of the form's submit button. However, I really don't want to write and maintain 60 on change event handlers.
You can create an event-sink class that will contain the event-handling code for all of your controls of a particular type.
For example, create the a class called TextBoxEventHandler as follows:
Private WithEvents m_oTextBox as MSForms.TextBox
Public Property Set TextBox(ByVal oTextBox as MSForms.TextBox)
Set m_oTextBox = oTextBox
End Property
Private Sub m_oTextBox_Change()
' Do something
End Sub
Now you need to create & hook up an instance of that class for each control of the appropriate type on your form:
Private m_oCollectionOfEventHandlers As Collection
Private Sub UserForm_Initialise()
Set m_oCollectionOfEventHandlers = New Collection
Dim oControl As Control
For Each oControl In Me.Controls
If TypeName(oControl) = "TextBox" Then
Dim oEventHandler As TextBoxEventHandler
Set oEventHandler = New TextBoxEventHandler
Set oEventHandler.TextBox = oControl
m_oCollectionOfEventHandlers.Add oEventHandler
End If
Next oControl
End Sub
Note that the reason you need to add the event handler instances to a collection is simply to ensure that they remain referenced and thus don't get discarded by the garbage collector before you're finished with them.
Clearly this technique can be extended to deal with other types of control. You could either have separate event handler classes for each type, or you could use a single class that has a member variable (and associated property & event handler) for each of the control types you need to handle.
In that case you have few options, because event handlers cannot be shared in VBA/VB6
Option 1: Use a central handling function which is called from every event handler.
Sub Control1_ChangeEvent()
CommonChangeEvent // Just call the common handler, parameters as needed
End Sub
Sub Control2_ChangeEvent()
CommonChangeEvent
End Sub
...
Sub CommonChangeEvent(/* Add necessary parameters */)
//Do the heavy lifting here
End Sub
Option 2: Organize your controls in control arrays.
Sub TextBox_ChangeEvent(Index As Integer)
CommonChangeEvent
End Sub
Sub OtherControlType_ChangeEvent(Index As Integer)
CommonChangeEvent
End Sub
Combining both options your total event handler count will shrink considerably and the remaining handlers are just brainless stubs for the one true event handler.