Assigning an entity attribute to a variable - vba

I'm new to programming in VBA or object-oriented programming. I'm trying to get this code to work in the VBA compiler of Arena by Rockwell Automation.
I sometimes get Error 424: 'Object required', and after debugging I'm referred to the XL.Quit at the end. Sometimes I've got an error related to the use of ActiveEntity. I don't know if I'm wrong getting the Excel file to work or if it's something do to with accessing the entity attributes of Arena anymore.
Can you spot the error? Or do you have a suggestion on how to find it myself?
Public Sub ModelLogic_RunBeginSimulation()
Dim m As Model
Set m = ThisDocument.Model
Dim s As SIMAN
Set s = m.SIMAN
Dim FileToOpen As String
Dim ArenaDir As String
Set XL = GetObject("", "Excel.Application")
ArenaDir = Mid(m.FullName, 1, Len(m.FullName) - Len(m.Name))
FileToOpen = ArenaDir & "data.xlsx"
XL.Workbooks.Open FileToOpen
End Sub
Public Sub VBA_Block_1_Fire()
Dim RNEG_deltaw As Double, Z_deltaw As Double, RPOS_deltaw As Double
Dim R_SI As Double, Z_SI As Double, S_SI As Double
Dim SI As Double, NumContenedor As Integer, Wo As Double, W_1 As Double, deltaW As Double
NumContenedor = s.EntityAttribute(s.ActiveEntity,s.SymbolNumber("NumContenedor"))
SI = XL.Workbooks(1).Worksheets(1).Cells(NumContenedor, 3).value
s.EntityAttribute(s.ActiveEntity, s.SymbolNumber("deltaW")) = SI
End Sub
Public Sub ModelLogic_RunEnd()
XL.Quit
End Sub

The XL Object is declared as procedure-level and therefore doesn't exist in the scope of another procedure.
If you want a variable or object to be visible to all procedures, declare it at the beginning of the module, perhaps like:
Public XL as Object
That being said, you didn't declare XL it in the other procedure either. Add this line to the very top of this module (and every module, ever):
Option Explicit
This will force you to properly declare and handle your variables, objects, properties, etc, and help prevent issues like this.
One more thing: I bet you've run this code at least a few times, right?
Save your work and then REBOOT, right away. Just trust me.
More information:
MSDN : Understanding Scope and Visibility
When Are You Required To Set Objects To Nothing?
Jon Skeet : Writing the Perfect Question

Assuming XL is declared at module level, I believe the error relates to this line:
Set XL = GetObject("", "Excel.Application")
This requires Excel to be running. It appears when ModelLogic_RunEnd() is fired, tries to call the .Quit() method of Excel, but XL is nothing.
Try changing it to CreateObject() and see if that helps:
Set XL = CreateObject("Excel.Application")

Related

Get Excel Object when Option Strict On

I have got following codes from this microsoft page.
https://msdn.microsoft.com/en-us/library/e9waz863(v=vs.90).aspx
' Add Option Strict Off to the top of your program.
Option Strict Off
.
Private Sub getExcel()
Dim fileName As String = "c:\vb\test.xls"
If Not My.Computer.FileSystem.FileExists(fileName) Then
MsgBox(fileName & " does not exist")
Exit Sub
End If
' Set the object variable to refer to the file you want to use.
Dim excelObj As Object = GetObject(fileName)
' Show Excel through its Application property.
excelObj.Application.Visible = True
' Show the window containing the file.
Dim winCount As Integer = excelObj.Parent.Windows.Count()
excelObj.Parent.Windows(winCount).Visible = True
' Insert additional code to manipulate the test.xls file here.
' ...
excelObj = Nothing
End Sub
Everything is okey when Option Strict Off
Everything is not okey when Option Strict On
So, how to solve that errors when Option Strict On?
Be careful! I want to get specific excel file from the same excel instance.
+1 for striving to use Option Strict On. :)
However, the code is using what is known as Late Binding and that requires you have Option Strict Off. You can however, minimize the scope of Option Strict Off by creating a new code file and using a Partial Class definition to contain the code that needs Late Binding.
You might be able to get by using the VB CallByName function with Option Strict On, but that would get ugly real quick and probably be very slow.
There also is an advanced technique using native API's that is called COM reflection that should work with Option Strict On. This technique is described in the article: [Basic Instincts - Inspecting COM Objects with Reflection].(https://msdn.microsoft.com/en-us/magazine/dd347981.aspx).
The typical method for early binding (Option Strict On) is to add a reference to the Excel primary interop assembly. This technique has some detractors as well, but is by far the easiest method.
Edit: The following demonstrates how to use the Excel PIA's and early binding to open a workbook directly similar to the OP's original code.
Dim wbPath As String = "*** replace with path to your workbook ***"
Dim wb As Excel.Workbook = CType(GetObject(wbPath), Excel.Workbook)
'or
'Dim wb As Excel.Workbook = CType(Marshal.BindToMoniker(wbPath), Excel.Workbook)
Dim app As Excel.Application = wb.Application
app.Visible = True
wb.Windows(1).Visible = True
As a side note, there is no need to depend on the Interop.Excel early binding, if you know that it works. You can change the Error Notifications to Warning and still compile and run.
' Imports Microsoft.Office.Interop.Excel
Dim fileName = "c:\vb\test.xls"
If Not IO.File.Exists(fileName) Then MsgBox(fileName & " does not exist") : Exit Sub
Dim obj = GetObject(fileName, "Excel.Application")
Dim wb = TryCast(obj, Workbook)
wb.Application.Visible = True
wb.Windows(1).Visible = True

Range.Formula error with user defined functions

Hello guy I have a user defined function inside VBA
Function clean(word As String, ParamArray characters() As Variant) As String
For i = 0 To UBound(characters)
word = Replace(word, characters(i), "")
Next i
clean = word
End Function
whenever I try to use it in another subroutine like that
Sub prova()
Dim wb As Workbook
Dim wsB As Worksheet
Set wb = ThisWorkbook
Set wsB = wb.Sheets("Bond Holdings")
wsB.Range("R3").Formula = "=clean(""dfsduuu"",""u"")"
End Sub
I get runtime error 1004. Could you guys help me figure out why? this is driving me crazy.
Thank you
Excel has a built-in function called CLEAN. You have a name-clash. If you call your function e.g. cleaner, it will work as expected.

VBA - Global Variable dropping out of scope

I'm having an issue with a global variable dropping out of scope. I have defined a public variable in "ThisWorkbook":
Public dict As Scripting.Dictionary
This gets initalized in "Workbook_Open()"
Set dict = New Scripting.Dictionary
After initialization I run a Sub (code located in "ThisWorkbook" still) that fills this dictionary with IDs and instances of a custom class.
I'm having trouble working with this dict variable in other modules though. The goal is to build a number of public functions that the worksheet will be able to call. These functions manipulate/retrieve/etc data in the custom classes in the dictionary.
For example, this test sub (code in ModuleXYZ) throws "Object variable or With block variable not set"
Private Sub TestSub()
Dim x As Integer
x = ThisWorkbook.dict.Count
End Sub
This is the same error I would get when I first started this coding project, when the dict fell out of scope in the "ThisWorkbook" module, and I'd have to redo the "Set dict = New Scripting.Dictionary"
My hope was that by setting the dict as a Public variable in "ThisWorkbook", that it would stay in scope the entire time this workbook was open.
Thanks - KC
Try declaring the public variable in a module rather than the workbook. Also check that you haven't declared it twice. Convention is to declare all globals in one module which is often called globals.bas.
In cases like these, I sometimes like to use a Singleton pattern of sorts (at least, this is my understanding of the singleton pattern). I create a publicly available function that either returns the object if it exists, or creates then returns the object. This way, you don't have to worry too much about it falling out of scope.
Here's an example:
Public oDict As Object
Public Function GetDictionary() As Object
If oDict Is Nothing Then
Set oDict = CreateObject("Scripting.Dictionary")
For Each cel In Range("A1:A10")
oDict.Add cel.Value, cel.Offset(, 1).Value
Next cel
End If
Set GetDictionary = oDict
End Function
To reference it, it's like:
Sub GetDictCount()
MsgBox GetDictionary().Count
End Sub
Sub OtherDictTest()
MsgBox GetDictionary()(1)
End Sub

Understanding VBA Object Variables

I'm learning Visual Basic for Applications, and I'm a little unsure of when to use procedures. For example, is a procedure needed every time an object variable is created? Or can an object variable be created without being executed as a procedure?
Could I have just
Dim wkbInventory As Workbook
Set wkbInventory = Application.Workbooks("name.xlsm")
Thank you!
A object variable can be declared outside a procedure. So the
Dim wkbInventory As Workbook
can be in the Declarations (see https://msdn.microsoft.com/en-us/library/dd897495%28v=office.12%29.aspx#odc_ac2007_bk_BeginningAccess2007VBA_Chapt2_CreatingModules) .
The scope of the variable is dependent on where the declaration happens. See http://www.cpearson.com/Excel/Scope.aspx.
But a variable can't be instantiated outside a procedure or function. So the
Set wkbInventory = Application.Workbooks("name.xlsm")
must be inside the Procedures within a sub procedure or a function.
Almost all of the work in VBA happens inside of macros. You can declare variables outside of macros (although this changes the variable's scope compared to declaring it inside a macro) and specify some compiler options but that's about it.
This is OK:
Option Explicit
' Declaration
Dim c As Collection
Sub foo()
Set c = New Collection
c.Add "hello", "world"
MsgBox c.Count
End Sub
whereas this will generate a compile error - "Invalid outside procedure":
Option Explicit
' Declaration
Dim c As Collection
' This causes an error
Set c = New Collection
Sub foo()
c.Add "hello", "world"
MsgBox c.Count
End Sub
If you use implicit creation with your variable declaration (Dim x As New y) then that works outside of a macro. Not all objects can be created that way though. This code works:
Option Explicit
' Declaration with implicit creation
Dim c As New Collection
Sub foo()
c.Add "hello", "world"
MsgBox c.Count
End Sub
whereas this code doesn't work (you'll get error 429 - ActiveX component can't create object):
Option Explicit
' Declaration with implicit creation
Dim w As New Workbook
Sub foo()
MsgBox w.FullName
End Sub
First recommendation I would make is to put Option Explicit at the top of your code(above all subs and functions). That will help you know when things are not correct.
Dim wkbInventory As Workbook Set wkbInventory = Application.Workbooks("name.xlsm")
Will not work. You will have to do
Dim wkbInventory As Workbook
Set wkbInventory = Application.Workbooks("name.xlsm")
FYI, there is late binding versus early binding that might help you (see also this thread for a an overview) . This is something to learn unrelated to procedures so I'm just providing as an if you already din't know...
You can early bind
' Set reference to 'Microsoft Excel 8.0 Object Library' in
' the Project|References dialog (or Tools|References for VB4 or VBA).
' Declare the object as an early-bound object
Dim oExcel As Excel.Application
Set oExcel = CreateObject("Excel.Application")
' The Visible property is called via the v-table
oExcel.Visible = True
' Good practice to explicitly set object variables to "Nothing" when
' done but not mandatory (especially not for local references).
Set oExcel = Nothing
Or late bind
' No reference to a type library is needed to use late binding.
' As long as the object supports IDispatch, the method can
' be dynamically located and invoked at run-time.
' Declare the object as a late-bound object
Dim oExcel As Object
Set oExcel = CreateObject("Excel.Application")
' The Visible property is called via IDispatch
oExcel.Visible = True
' Good practice to explicitly set object variables to "Nothing" when
' done but not mandatory (especially not for local references).
Set oExcel = Nothing

Declare a Workbook as a Global variable

I am starting to write a code that will become applicable to multiple workbooks, but always uses the same reference workbook. The code will have many subs, and as I am trying to avoid to dim a variable to the reference workbook in every sub I would like to declare them Global.
First I had:
Global Locations As Excel.Workbook
Set Locations = Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx")
Which gave me:
"Compile error: Invalid outside procedure"
After some googling I found the following bit of code somewhere:
Public Const Locations As Excel.Workbook = "Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx")"
Which gave me:
"Compile error: Expected: type name"
Edit:
Using:
Public Const Locations As Excel.Workbook = "Workbooks.Open('M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx')"
(Single quotation marks within the Workbooks.Open statement) results as the same error as when using double quotation marks.
Who knows what I am doing wrong?
Edit2:
I also tried to declare the variables in the "ThisWorkbook", following this answer using:
Private Sub Workbook_Open()
Dim Locations As Excel.Workbook
Dim MergeBook As Excel.Workbook
Dim TotalRowsMerged As String
Locations = Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx")
MergeBook = Workbooks.Open("M:\My Documents\MSC Thesis\Italy\Merged\DURUM IT yields merged.xlsm")
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
End Sub
But then it returns an
"Object Required"
within my module.
Edit3:
I now have this which works, but has the downside of having to copy the SET lines into every Sub, there has to be a better way to do this?
Global Locations As Workbook
Global MergeBook As Workbook
Global TotalRowsMerged As String
Sub Fill_CZ_Array()
Set Locations = Application.Workbooks("locXws.xlsx")
Set MergeBook = Application.Workbooks("DURUM IT yields merged.xlsm")
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
I think the most universal way for workbook global variable would be creating a module with a Public Property Get procedure. You can refer to it without calling any code first, and you don't have to worry if the file is open or not.
Here is the sample module code for one of the variables:
Private wLocations As Workbook
Public Property Get Locations() As Workbook
Const sPath As String = "M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx"
Dim sFile As String
If wLocations Is Nothing Then
'extract file name from full path
sFile = Dir(sPath)
On Error Resume Next
'check if the file is already open
Set wLocations = Workbooks(sFile)
If wLocations Is Nothing Then
Set wLocations = Workbooks.Open(sPath)
End If
On Error GoTo 0
End If
Set Locations = wLocations
End Property
You can use it anywhere in the code as a global variable:
Sub Test()
Debug.Print Locations.Worksheets.Count
End Sub
Your question implies that you want a global workbook constant, not a variable. Because VBA doesn't allow objects to be initialised outside of a procedure, you can't have an object constant. The best you can do is have a public workbook variable that's initialised in an event.
You can declare a global variable, but you can't execute code to assign a value outside of a procedure:
Public myBook As Excel.Workbook
Sub AssignWorkbook()
Set myBook = Workbooks.Open("C:\SomeBook.xlsx") '// <~~ valid, inside sub
End Sub
Sub TestItWorked()
MsgBox myBook.Name
End Sub
So in a normal module you could have:
Public myBook As Excel.Workbook
And in your Workbook_Open() event:
Private Sub Workbook_Open()
Set myBook = Workbooks.Open("C:\SomeOtherBook.xlsx")
End Sub
Then you can use myBook elsewhere in your code without having to re-assign it.
It might be worth having a look at Chip Pearson's article about variable scope in VBA here
what you want is some sort of Factory with static properties, for example in a separate module
mFactoryWkbs
Private m_WkbLocations As Workbook
Private m_WkbMergeBook As Workbook
Public Property Get LOCATIONS() As Workbook
If m_WkbLocations Is Nothing Then
Set m_WkbLocations= Workbooks.Open("wherever")
End If
Set LOCATIONS = m_WkbLocations
End Property
Public Property Get MERGEBOOK () As Workbook
If m_WkbMergeBook Is Nothing Then
Set m_WkbMergeBook = Workbooks.Open("wherever")
End If
Set MERGEBOOK = m_WkbMergeBook
End Property
To use, just call the property where & when you need it, no extra variables (or Sets for them) required.
TotalRowsMerged = MERGEBOOK.Worksheets("Sheet1").UsedRange.Rows.Count
This is the best I can come up with until now. The result is that there is now only one place to change the name of the file, however I still need to copy the SET function within every subroutine. Not completely ideal yet, but better then nothing.
Public Const DESTBOOK = "DURUM IT yields merged.xlsm"
Global Locations As Workbook
Global MergeBook As Workbook
Global TotalRowsMerged As String
Sub Fill_CZ_Array()
Set Locations = Application.Workbooks("locXws.xlsx")
Set MergeBook = Application.Workbooks(DESTBOOK)
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
Whenever I run into this, I declare wb as a public constant string:
public wb as string = "c:\location"
Then, throughout the code in the project, you can refer to
workbooks(wb).anything
This is the sort of thing I usually do when I have global variables that need to be properly initialized:
In a general code module put the following code:
Public Initialized As Boolean
Public Locations As Workbook
Sub Initialize()
If Initialized Then Exit Sub
Const fname As String = "M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx"
On Error Resume Next
Set Locations = Workbooks(Dir(fname))
On Error GoTo 0
If Locations Is Nothing Then
Set Locations = Workbooks.Open(fname)
End If
Initialized = True
End Sub
Then in the workbook's code module put:
Private Sub Workbook_Open()
Initialize
End Sub
Furthermore, in any "gateway" sub or function (e.g. event-handlers, UDFs, etc.) which might launch your code, put Initialize (or maybe: If Not Initialized Then Initialize) as the first line. Typically most subs won't be directly launched and can rely on Locations being properly set by the caller. If you need to test something which won't run properly if the variable isn't set then you can just type initialize directly in the Immediate Window.
You could also do it with a class module and rely on the class initialiser to do the work for you when it gets used in the module:
Class module called cLocations:
Public Workbook As Workbook
Private Sub Class_Initialize()
Set Workbook = Workbooks.Open("C:\Temp\temp.xlsx")
End Sub
And where you like in your module, or anywhere for that matter:
Dim Locations As New cLocations
Sub dosomething()
Locations.Workbook.Sheets(1).Cells(1, 1).Value = "Hello World"
End Sub
And then, you can just use Locations.Workbook to refer to the locations workbook, and ThisWorkbook to refer to the workbook the code is running in and ActiveWorkbook to refer to the workbook that has focus. This way you could run your code from one workbook (ThisWorkbook), using the locations workbook (Locations.Workbook) as a reference and iterate over other workbooks (ActiveWorkbook) to add another level of automation.
If you step through the code, you will see that the class is only initialised when you hit a line of code that requires it, not when the workbook is loaded.
I must add though, in this case I think if you give us a slightly bigger picture of what you are trying to achieve we might be able to give you a solution to a better problem than the one you have hit while coding.
You could also take this a step further, and abstract to the application level, keep the locations workbook hidden, and even provide intellisense for named sheets if you know their position or their name explicitly:
Class module:
Private App As Application
Public Workbook As Workbook
Public NamedSheet As Worksheet
Private Sub Class_Initialize()
Set App = New Application
App.Visible = False
App.DisplayAlerts = False
Set Workbook = App.Workbooks.Open("C:\Temp\temp.xlsx") 'maybe open read only too?
Set NamedSheet = Workbook.Sheets("SomethingIKnowTheNameOfExplicitly")
End Sub
Public Sub DoSomeWork()
'ThisWorkbook refers to the one the code is running in, not the one we opened in the initialise
ThisWorkbook.Sheets(1).Cells(1, 1).Value = Wb.Sheets(1).Cells(1, 1).Value
End Sub
Public Function GetSomeInfo() As String
GetSomeInfo = NamedSheet.Range("RangeIKnowTheNameOfExplicitly")
End Function
And then in your module, the first time you use the variable it will be initialised in one line of code:
Dim Locations As New cLocations
Dim SomeInfo
Sub DoSomething()
SomeInfo = Locations.GetSomeInfo 'Initialised here, other subs wont re-initialise
Locations.Workbook.Sheets(1).Cells(1, 1).Value = _
ThisWorkbook.Sheets(1).Cells(1, 1).Value
Locations.NamedSheet.Cells(1,1).Value = "Hello World!"
Locations.Workbook.Save
End Sub
This solution will work only if you know the numbers and names of all the worksheets that you will use from referenced workbook.
In your module, declare worksheet public variable for all your worksheets as follows:
Public sht1 As Worksheet
Public sht2 As Worksheet
Public sht3 As Worksheet
...
Instantiate these public variables in the application load event.
Sub Workbook_Open()
Workbooks.Open ("your referenced workbook")
'Instantiate the public variables
Set sht1 = Workbooks("Test.xlsm").Sheets("Sheet1")
Set sht2 = Workbooks("Test.xlsm").Sheets("Sheet2")
Set sht3 = Workbooks("Test.xlsm").Sheets("Sheet3")
End Sub
Now you can refer these global worksheets in your sub.
For example:
Sub test()
MsgBox sht1.Range("A1").Value
MsgBox sht2.Range("A1").Value
MsgBox sht3.Range("A1").Value
End Sub
If you create a Module say ExcelMod and within that Module you have a public function or subroutine Initialize() and another one called Terminate() you can initialize and terminate Module level variables using those routines. For example I have used this before: (Note that module variables are the first thing declared at the top of the module.)
Dim excelApp As Object, wb As Workbook, ws As Worksheet
Sub Initialize()
Set excelApp = CreateObject("Excel.Application")
Set wb = Workbooks.Open("C:\SomeOtherBook.xlsx")
End Sub
Sub Terminate()
Set excelApp = Nothing
Set wb = Nothing
End Sub
The variables are part of the entire module and only get initialized and terminated with these subroutines. You can pass the variables in and out of the module as you wish and use them in ALL of this modules subroutines without having to set again. If you need to use in another module you will need to pass it to that module as you normally would.
Also as others have mentioned you can use the workbook_Open event to call the initialization sub to create the objects and set them only once if needed.
Is this what you are after?
If I understand your question correctly, you are creating a code that should work on the application level and not on workbook level. In this case why don't you create an add-in.
All the code inside the add-in will have access to all the open workbooks at application level.
You might want to create an Add-In, or use a Class module to work with properties, ...
But I'm not sure it'll be that cleaner than a simple declaration in a regular module and a call to that procedure at workbook's open will do the trick just fine too .
(I have been using this method for quite some times and haven't been bothered)
So you can use this in a (dedicated or not) regular module :
'Set the path to your files
Public Const DESTBOOK = "M:\My Documents\MSC Thesis\Italy\Merged\DURUM IT yields merged.xlsm"
Public Const LOCBOOK = "M:\My Documents\MSC Thesis\Italy\Merged\locXws.xlsx"
'Declare all global and public variables
Global Locations As Workbook
Global MergeBook As Workbook
Global TotalRowsMerged As String
'Set all variable (Procedure call from Workbook_Open)
Sub Set_All_Global_Variables()
Set Locations = Set_Wbk(LOCBOOK)
Set MergeBook = Set_Wbk(DESTBOOK)
TotalRowsMerged = MergeBook.Worksheets("Sheet1").UsedRange.Rows.Count
'...
End Sub
'Function to check if the workbook is already open or not
Function Set_Wbk(ByVal Wbk_Path As String) As Workbook
On Error Resume Next
Set Set_Wbk = Workbooks(Dir(Wbk_Path))
On Error GoTo 0
If Set_Wbk Is Nothing Then
Set Set_Wbk = Workbooks.Open(Wbk_Path)
End If
End Function
And call the procedure setting all the variables in the ThisWorkbook module :
Private Sub Workbook_Open()
Set_All_Global_Variables
End Sub