Splitting large code into Modules to Call - vba

My end goal was to generate text into a TextBox (let's call it TextBox06) based on the values inserted into a series of Textboxes -- let's name them TextBox01, TextBox02, TextBox03, TextBox04, TextBox05. These TextBoxes are in a Userform (let's call it Userform01).
These TextBoxes can have a lot of different values and combinations (over 50 values each, and depending on which order the values are placed in the textboxes there will be a different output). Moral of the story: code probably works, but gives "procedure too large" error with no real way around it (pretty sure I have shrunk the code as much as possible, but there just aren't any more generic statements that repeat, and most of it must be a bruteforce-style method of inserting most of the combinations individually).
So I went looking on the internet for a way to split the code. I tried using the "call procedure" feature. What I did was:
I created two new Modules (Module01 and Module02) in the same "project".
I split the big code half in Module01, and half in Module02. The Modules' content is something like this:
Sub Module01()
[half the code]
End Sub
I wrote the following lines of code in each sub of the textboxes involved (this is in Userform01).
Private Sub TextBox01_Change()
Application.Run ("Module01")
Application.Run ("Module02")
End Sub
[Then five more copies for Private Sub TextBox02_Change () until Private Sub TextBox06_Change () included]
Assuming this is even the correct way of doing it, I then hit a roadblock. Apparently, copy-pasting the big code in Module01 and Module02 wasn't enough. From what I understood, the program doesn't seem to understand what the TextBox01.Text (and the like) values in the code are. So I tried looking for a solution to THAT, and found several possible codes, none of which seems to have worked so far. The one that I currently have is this:
Sub Module01()
Dim Text01 As String
Set Text01 = TextBox01.Text
[repeat for Text02 to Text06 included]
[half the code]
End Sub
[repeat for Module02 with the other half of the code]
Assuming that I'm even on the right track when it comes to properly using the "call" function, what is the proper way to make this work (i.e. how do I correctly reference the TextBox01.Text content in the Module, so I can then call the Modules in the Userform01) ?

Related

Microsoft Project VBA to update Custom field on task change

I have been wracking my brain trying to work out how to write a small piece of code that will activate only when particular fields at a task level have been modified.
I tried to make this code work at the project change level with a for each loop and select cases but that lags the whole program and still doesn't give me the result I need. I also tried to make it work when run manually with a for each loop and select cases or a bunch of If statements, but again, it can't tell me which field changed, but it can highlight a discrepancy between two fields.
The goal is to have a change log field (Text10) that auto updates based on the field that is modified and the date of the change. I only care about 4 fields changing (Date1, Date2, Date3, Date4).
e.g. If [Date1] is modified, Text10 = "Date1 modified 10/11/21"
Note: If 2 fields are modified, I would be happy enough with just listing the last one.
I was hoping there was some sort of "On Change, If Target = xxx" but I have not been able to find anything like that.
I also tried implementing the code as defined here >> Microsoft Documents: Project.Change Event but I am unclear what this is supposed to do and couldn't actually see it doing anything / I never got the message box I believe was supposed to appear.
I am using Microsoft Project Standard 2019.
After much research and trial and error, I ended up solving this.
To get it working, I added a Class Module and ran a piece of code on open to initialize it. This essentially tells Project to start watching for events. I then use the "Field" variant to fill the field name amongst the text string and "NewVal" variant to fill the result. This was an easy solution in the end. The code I found that worked is below:
In Class Module "cm_Events"
Public WithEvents MyMSPApp As MSProject.Application
Private Sub Class_Initialize()
Set MyMSPApp = Application
End Sub
Private Sub MyMSPApp_ProjectBeforeTaskChange(ByVal tsk As Task, ByVal Field As PjField, ByVal NewVal As Variant, Cancel As Boolean)
'What you want the code to do
End Sub
In Module "m_Events"
Public oMSPEvents As New cm_Events
Sub StartEvents()
Set oMSPEvents.MyMSPApp = MSProject.Application
End Sub
In ThisProject code
Private Sub Project_Open(ByVal pj As Project)
Call m_Events.StartEvents
End Sub

What role do modules play in writing code that is less memory-intensive?

At work, I'm venturing into the world of VBA to try to make a spreadsheet template, which will run a report when a command button is clicked.
On several occasions I have encountered an "Out of Memory" run-time error, which has been easily fixed by jigging with the code using combinations of suggestions by other users on this site!
However, I am now curious about how to make codes less memory-intensive. In particular, do modules help make code less memory-intensive and, if so, how should I use them effectively in this regard? For example, should I assign a Sub to each module or would that be overkill?
I'm new to VBA so any/all help and criticism is very welcome!
Short answer: None whatsoever.
An "out of memory" error has many reasons to occur, none of which relate to how many modules your code is organized in. Because that's all modules are: an organizational tool at your disposal.
It is completely impossible to diagnose an "out of memory" error without knowing anything about what the code is doing: <meta> you would have to narrow down the problem to a particular specific piece of code before you can ask about it on this site (dumping a whole module and asking "what's wrong with this code?" isn't going to fly) </meta>
Look for very, very large arrays, clipboard operations perhaps. Maybe you're running a loop that's attempting to create and store a bajillion New large objects - could be pretty much anything... just not the number of modules.
should I assign a Sub to each module or would that be overkill?
As with many things, the answer is "it depends". Once you've familiarized yourself with the language, and start learning about Object-Oriented Programming (OOP), you'll come across design guidelines and principles that strongly advocate towards keeping public interfaces as simple as possible: adhering to the Interface Segregation Principle (the "I" of "SOLID") means that this is a perfectly complete and acceptable class module to have:
'#Interface IComparable
'#ModuleDescription("Defines a generalized type-specific comparison method that a class implements to order or sort its instances.")
Option Explicit
'#Description("Compares the current instance with another object of the same type and returns an integer that indicates whether the current instance precedes (-1), follows (1), or occurs in the same position in the sort order (0) as the other object.")
Public Function CompareTo(ByVal other As Object) As Integer
End Function
But that's well beyond beginner-level procedural programming concepts.
[...] which will run a report when a command button is clicked.
That command button could be an ActiveX button, or a Shape object that's attached to a macro. Assuming it's ActiveX, the worksheet's code-behind might look like this:
Option Explicit
Private Sub RunSalesReportButton_Click()
SalesReportMacro.CreateWeeklyReport
End Sub
And then you can have a SalesReportMacro standard module that might start out like this:
'#ModuleDescription("A macro that generates the weekly sales report.")
Option Explicit
Option Private Module
'#Descrition("Generates the weekly sales report.")
Public Sub CreateWeeklyReport()
If Not RefreshSalesData Then Exit Sub
CreatePivotReport
End Sub
The Public procedure at the top has a very high abstraction level: it's easy to tell at a glance what the procedure is going to be doing, because the lower-level procedures have meaningful names that tell us exactly what's going on. These lower-level procedures would be right underneath:
Private Function RefreshSalesData() As Boolean
Dim reportWeek As String
reportWeek = PromptForReportWeek
If Not IsNumeric(reportWeek) Then Exit Function
On Error GoTo CleanFail
AdjustDataConnectionCommand reportWeek
RefreshSalesData = True
CleanExit:
Exit Function
CleanFail:
MsgBox "Could not refresh the data for week " & reportWeek & ".", vbExclamation
Resume CleanExit
End Function
And the deeper you go, the lower the abstraction level becomes:
Private Function PromptForReportWeek() As String
Dim currentWeek As Integer
currentWeek = GetCurrentWeekNumber
PromptForReportWeek = InputBox("Please specify week#", "Generate Report", currentWeek)
End Function
Private Function GetCurrentWeekNumber()
GetCurrentWeekNumber = 42 'todo
End Function
Private Sub AdjustDataConnectionCommand(ByVal weekNumber As String)
With ThisWorkbook.Connections(1).OLEDBConnection
.CommandText = "dbo.WeeklySalesReport " & weekNumber
.Refresh
End With
End Sub
Private Sub CreatePivotReport()
'todo
End Sub
Procedures (Sub, Function, and others you'll discover in due time) should have one purpose, and be kept as short and focused on that one task as possible. By doing that, you make it easier to later refactor your code - say in 2 months you need to add another ActiveX button for a new CreateWeeklyInventoryReport macro: there's a good chance that this new report will also need to PromptForReportWeek - if that functionality is already well-abstracted into its own scope, you can more easily just remove/cut it from that module, add a new CalendarParameterPrompt module (which... might end up only having that procedure for a while.. until you need a good place to put a similar PromptForReportMonth function), add/paste it in there, make it Public, and invoke it from any other macro/module.
Doing this instead of massive omnipotent procedures that know everything and do everything (and eventually stop compiling because they got so large VBA refuses to compile them), will spare you lots of headaches in the near future.

How to read specific lines of codes in another sub?

I was wondering if there is a way to run specified lines of code in another sub , say execute only line 1 through line 10 of Sub_2 before executing the code in say, Sub_1.
I have done some background research and found up a code suggested by #Andrew below.
Private Sub Sub_1(sender As System.Object, e As System.EventArgs) Handles Button1.Click
'Call the Save Sub
Call Sub_2(sender, e)
'Proceed to execute the rest of the this Sub_1
What the above code does at the moment is execute the entire sub I have called, which is not what I want to achieve.
I have also read about the using a shared routine common to both Subs as suggested by #Henry , but the problem for me is that variables are declared in the shared routine which are required at various stages in Sub_2, but which cannot be used as the program says they are not declared (in Sub_1).
Again, what I want to achieve is execute only a few lines of code from Sub_2 in Sub_1.
I am using Visual Basic 2010 Express.
What you are trying to do is technically possible (in various really hackish ways), but would easily become a code structure nightmare. Instead, consider an alteration to the way you are building your routines.
Subs and Functions are intended to be independent 'islands' of code, which can execute (generally) without knowing anything about the rest of the program. You can pass variables into them, and get variables back, but they should operate the same time after time.
Consider this theoretical code from the scenario you are proposing:
Dim variable_1
Private Sub Sub_1()
Dim variable_2
Call Sub_2()
End Sub
Private Sub Sub_2()
'lines 1-10
'lines 11-20
End Sub
Here we have the setup for what you're looking at. Both Subs can 'see' variable_1, but Sub_2 can't see variable_2. This is called scope. Finally, there is the issue of only wanting to execute lines 1-10 inside Sub_2 when being called from Sub_1.
The first thing to do is to separate our code into logical groups. According to what you're describing, lines 1-10 of Sub_2 make sense when looked at on their own, so this automatically means that they should be grouped together. Lines 11-20 may be a different story, but since they are sometimes relevant and others not, we should separate them as well.
Private Sub Sub_2()
'lines 1-10
End Sub
Private Sub Sub_3()
Call Sub_2()
'lines 11-20
End Sub
The change looks simple, but is actually quite powerful. When we only want to execute lines 1-10, we call Sub_2. If we want to execute lines 1-20, we call Sub_3. Sub_3 will automatically call Sub_2 (lines 1-10) to execute first, and then execute lines 11-20. Easy.
Now we just have to figure out what to do about the variables. There are quite a few ways you can handle this situation, but the easiest thing to do is to expand their scope. If all the variables you need to access are declared outside of these Subs (one "level up"), then they can all access them. For example:
Dim variable_1
Dim variable_2
Private Sub Sub_1()
Dim variable_3
'can access 1, 2, 3 but NOT 4, 5
Call Sub_2()
End Sub
Private Sub Sub_2()
Dim variable_4
'can access 1, 2, 4 but NOT 3, 5
End Sub
Private Sub Sub_3()
Dim variable_5
'can access 1, 2, 5 but NOT 3, 4
End Sub
This should get you started. Once you've got this down, you can read about the various ways to pass variables into Subs and Functions (which also gives some hints about how to get them out, as well).
I very much doubt you will be able to do that. A function is a unit of code which can be called via Reflection if required, however statements within a function are not exposed via Reflection and when optimised often may not even execute in the order that they are written.
Such a process would break a lot of bad practises - what if the code changes in the future for example? An implementation can change, although one wouldn't expect the function's definition to.
I would question why you are wanting to do this and look for an alternative means of achieving your goals.
If you were hell bent on such a implementation, then you could potentially break open the CLR code and decompile and then select certain aspects to compile and run - however it would be a lot of hard work, unsustainable and very bad practise.

Execute a user-defined function into another cell VBA Excel

I need to automatize a process which execute various (a lot) user-defined function with different input parameters.
I am using the solution of timer API found in I don't want my Excel Add-In to return an array (instead I need a UDF to change other cells) .
My question is the following: "Does anybody can explain to me HOW IT IS WORKING?" If I debug this code in order to understand and change what I need, I simply go crazy.
1) Let say that I am passing to the public function AddTwoNumbers 14 and 45. While inside AddTwoNumber, the Application.Caller and the Application.Caller.Address are chached into a collection (ok, easier than vectors in order not to bother with type). Application.Caller is kind of a structured object where I can find the function called as a string (in this case "my_function"), for example in Application.Caller.Formula.
!!! Nowhere in the collection mCalculatedCells I can find the result 59 stored.
2)Ok, fair enough. Now I pass through the two UDF routines, set the timers, kill the timers.
As soon as I am inside the AfterUDFRoutine2 sub, the mCalculatedCell(1) (the first -- and sole -- item of my collection) has MAGICALLY (??!?!?!!!??) obtained in its Text field exactly the result "59" and apparently the command Set Cell = mCalculatedCells(1) (where on the left I have a Range and on the right I have ... I don't know) is able to put this result "59" into the variable Cell that afterward I can write with the .Offset(0,1) Range property on the cell to the right.
I would like to understand this point because I would like to give MORE task to to inside a single collection or able to wait for the current task to be finished before asking for a new one (otherwise I am over-writing the 59 with the other result). Indeed I read somewhere that all the tasks scheduled with the API setTimer will wait for all the callback to be execute before execute itself (or something like this).
As you can see I am at a loss. Any help would be really really welcomed.
In the following I try to be more specific on what (as a whole)
I am planning to achieved.
To be more specific, I have the function
public function my_function(input1 as string, Field2 as string) as double
/*some code */
end function
I have (let's say) 10 different strings to be given as Field2.
My strategy is as follow:
1)I defined (with a xlw wrapper from a C++ code) the grid of all my input values
2)define as string all the functions "my_function" with the various inputs
3)use the nested API timer as in the link to write my functions IN THE RIGHT CELLS as FORMULAS (not string anymore)
3)use a macro to build the entire worksheet and then retrieve the results.
4)use my xlw wrapper xll to process further my data.
You may wonder WHY should I pass through Excel instead of doing everything in C++. (Sometime I ask myself the same thing...) The prototype my_function that I gave above has inside some Excel Add-In that I need to use and they work only inside Excel.
It is working pretty well IN THE CASE I HAVE ONLY 1 "instance" of my_function to write for the give grid of input. I can even put inside the same worksheet more grids, then use the API trick to write various different my_functions for the different grids and then make a full calculation rebuild of the entire worksheet to obtain the result. It works.
However, as soon as I want to give more tasks inside the same API trick (because for the same grid of input I need more calls to my_function) I am not able to proceed any further.
After Axel Richter's comment I would like to ad some other information
#Axel Richter
Thank you very much for your reply.
Sorry for that, almost surely I wasn't clear with my purposes.
Here I try to sketch an example, I use integer for simplicity and let's say that my_function works pretty much as the SUM function of Excel (even if being an Excel native function I could call SUM directly into VBA but it is for the sake of an example).
If I have these inputs:
input1 = "14.5"
a vector of different values for Field2, for instance (11;0.52;45139)
and then I want to write somewhere my_function (which makes the sum of the two values given as input).
I have to write down in a cell =my_function(14.5;11), in the other =my_function(14.5;0.52) and in a third one =my_function(14.5;45139).
These input changes any time I need to refresh my data, then I cannot use directly a sub (I think) and, in any case, as far as I understand, in writing directly without the trick I linked, I will always obtain strings : something like '=my_function(14.5;0.52). Once evaluated (for example by a full rebuild or going over the written cell and make F2 + return) will give me only the string "=my_function(14.5;0.52)" and not its result.
I tried at the beginning to use an Evaluate method which works well as soon as I write something like 14.5+0.52, but it doesn't work as soon as a function (nor a user-defined function) is used instead.
This is "as far as I can understand". In the case you can enlighten me (and maybe show an easier track to follow), it would be simply GREAT.
So far the comments are correct in that they repeat the simple point that a User-Defined Function called a worksheet can only return a value, and all other actions that might inject values elsewhere into the worksheet calculation tree are forbidden.
That's not the end of the story. You'll notice that there are add-ins, like the Reuters Eikon market data service and Bloomberg for Excel, that provide functions which exist in a single cell but which write blocks of data onto the sheet outside the calling cell.
These functions use the RTD (Real Time Data) API, which is documented on MSDN:
How to create a RTD server for Excel
How to set up and use the RTD function in Excel
You may find this link useful, too:
Excel RTD Servers: Minimal C# Implementation
However, RTD is all about COM servers outside Excel.exe, you have to write them in another language (usually C# or C++), and that isn't the question you asked: you want to do this in VBA.
But I have, at least, made a token effort to give the 'right' answer.
Now for the 'wrong' answer, and actually doing something Microsoft would rather you didn't do. You can't just call a function, call a subroutine or method from the function, and write to the secondary target using the subroutine: Excel will follow the chain and detect that you're injecting values into the sheet calculation, and the write will fail.
You have to insert a break into that chain; and this means using events, or a timer call, or (as in RTD) an external process.
I can suggest two methods that will actually work:
1: Monitor the cell in the Worksheet_Change event:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim strFunc As String strFunc = "NukeThePrimaryTargets" If Left(Target.Formula, Len(strFunc) + 1) = strFunc Then Call NukeTheSecondaryTargets End If End Sub
Alternatively...
2: Use the Timer callback API:
However, I'm not posting code for that: it's complex, clunky, and it takes a lot of testing (so I'd end up posting untested code on StackOverflow). But it does actually work.
I can give you an example of a tested Timer Callback in VBA:
Using the VBA InputBox for passwords and hiding the user's keyboard input with asterisks.
But this is for an unrelated task. Feel free to adapt it if you wish.
Edited with following requirements: It is necessary to run a user defined worksheet function, because there are addins called in this function and those work only within a Excel sheet. The function has to run multiple times with different parameters and its results have to be gotten from the sheet.
So this is my solution now:
Public Function my_function(input1 As Double, input2 As Double) As Double
my_function = input1 + input2
End Function
Private Function getMy_Function_Results(input1 As Double, input2() As Double) As Variant
Dim results() As Double
'set the Formulas
With Worksheets(1)
For i = LBound(input2) To UBound(input2)
strFormula = "=my_function(" & Str(input1) & ", " & Str(input2(i)) & ")"
.Cells(1, i + 1).Formula = strFormula
Next
'get the Results
.Calculate
For i = LBound(input2) To UBound(input2)
ReDim Preserve results(i)
results(i) = .Cells(1, i + 1).Value
Next
End With
getMy_Function_Results = results
End Function
Sub test()
Dim dFieldInput2() As Double
Dim dInput1 As Double
dInput1 = Val(InputBox("Value for input1"))
dInput = 0
iIter = 0
Do
dInput = InputBox("Values for fieldInput2; 0=END")
If Val(dInput) <> 0 Then
ReDim Preserve dFieldInput2(iIter)
dFieldInput2(iIter) = Val(dInput)
iIter = iIter + 1
End If
Loop While dInput <> 0
On Error GoTo noFieldInput2
i = UBound(dFieldInput2)
On Error GoTo 0
vResults = getMy_Function_Results(dInput1, dFieldInput2)
For i = LBound(vResults) To UBound(vResults)
MsgBox vResults(i)
Next
noFieldInput2:
End Sub
The user can input first a value input1 and then input multiple fieldInput2 until he inputs the value 0. Then the results will be calculated and presented.
Greetings
Axel

Profiling VBA code for microsoft word

I have some legacy code that uses VBA to parse a word document and build some XML output;
Needless to say it runs like a dog but I was interested in profiling it to see where it's breaking down and maybe if there are some options to make it faster.
I don't want to try anything until I can start measuring my results so profiling is a must - I've done a little searching around but can't find anything that would do this job easily. There was one tool by brentwood? that requires modifying your code but it didn't work and I ran outa time.
Anyone know anything simple that works?
Update: The code base is about 20 or so files, each with at least 100 methods - manually adding in start/end calls for each method just isn't appropriate - especially removing them all afterwards - I was actually thinking about doing some form of REGEX to solve this issue and another to remove them all after but its just a little too intrusive but may be the only solution. I've found some nice timing code on here earlier so the timing part of it isn't an issue.
Using a class and #if would make that "adding code to each method" a little easier...
Profiler Class Module::
#If PROFILE = 1 Then
Private m_locationName As String
Private Sub Class_Initialize()
m_locationName = "unknown"
End Sub
Public Sub Start(locationName As String)
m_locationName = locationName
MsgBox m_locationName
End Sub
Private Sub Class_Terminate()
MsgBox m_locationName & " end"
End Sub
#Else
Public Sub Start(locationName As String)
'no op
End Sub
#End If
some other code module:
' helper "factory" since VBA classes don't have ctor params (or do they?)
Private Function start_profile(location As String) As Profiler
Set start_profile = New Profiler
start_profile.Start location
End Function
Private Sub test()
Set p = start_profile("test")
MsgBox "do work"
subroutine
End Sub
Private Sub subroutine()
Set p = start_profile("subroutine")
End Sub
In Project Properties set Conditional Compilation Arguments to:
PROFILE = 1
Remove the line for normal, non-profiled versions.
Adding the lines is a pain, I don't know of any way to automatically get the current method name which would make adding the profiling line to each function easy. You could use the VBE object model to inject the code for you - but I wonder is doing this manually would be ultimately faster.
It may be possible to use a template to add a line to each procedure:
http://msdn.microsoft.com/en-us/library/aa191135(office.10).aspx
Error handler templates usually include an ExitHere label of some description.. The first line after the label could be the timer print.
It is also possible to modify code through code: "Example: Add some lines required for DAO" is an Access example, but something similar could be done with Word.
This would, hopefully, narrow down the area to search for problems. The line could then be commented out, or you could revert to back-ups.
Insert a bunch of
Debug.Print "before/after foo", Now
before and after snippets that you think might run for long terms, then just compare them and voila there you are.
My suggestion would be to divide and conquer, by inserting some timing lines in a few key places to try to isolate the problem, and then drill down on that area.
If the problem is more diffused and not obvious, I'd suggest simplifying by progressively disabling whole chunks of code one at a time, as far as is possible without breaking the process. This is the analogy of finding speed bumps in an Excel workbook by progressively hard coding sheets or parts of sheets until the speed problem disappears.
About that "Now" function (above, svinto) ...
I've used the "Timer" function (in Excel VBA), which returns a Single.
It seems to work just fine. Larry