Modify an existing VBA class - excel

I would like to know if there is some way to add your own methods/properties to an existing VBA class (such Range, Charts, etc).
An example:
I would like the currently VBA class Worksheet have a specific method done by myself, something like:
'Worksheet methods
Public Sub LookFor (ByVal Value as String)
'My code
End Sub
Then I can call from any declared Worksheet class this function.
In class MyClass:
'MyClass members
Private pWS1 as Worksheet
Private pWS2 as Worksheet
Private pWS3 as Worksheet
'MyClass methods
Private Sub Class_Initialization()
Set pWS1 = Worksheets("WS1")
Set pWS2 = Worksheets("WS2")
Set pWS3 = Worksheets("WS3")
End Sub
Public Sub Example()
pWS1.LookFor("abc")
pWS2.LookFor("123")
pWS3.LookFor("def")
End Sub
Thanks!

There is no direct way to do this in VBA.
Best you can do is create a "wrapper" class which has a private Worksheet member, and expose that via a Sheet property. Add your "extension" methods to the class and have them operate on m_sheet.
Initialize your class by creating an instance of it and assigning a worksheet object to its Sheet property.
You can call your "extension" methods directly on the object, and any existing methods you'd access via the Sheet property.
Class MySheet:
Private m_sht As Worksheet
Public Property Set Sheet(ws As Worksheet)
Set m_sht = ws
End Property
Public Property Get Sheet() As Worksheet
Set Sheet = m_sht
End Property
Public Property Get CountHellos() As Long
CountHellos = Application.CountIf(m_sht.Cells, "Hello")
End Property
Test sub:
Sub Tester()
Dim sht As MySheet
Set sht = New MySheet
Set sht.Sheet = ActiveSheet
MsgBox sht.CountHellos '<< "extension" method
MsgBox sht.Sheet.Rows.Count '<< built-in object property
End Sub
Edit: you might be able to make the Sheet property the default for your class by following the steps outlined by Chip here: http://www.cpearson.com/excel/DefaultMember.aspx
May work to allow you to skip the Sheet property when working with instances of your class (but I've not tested this)

What you are looking for is called "Extension" methods in tradition Object Oriented Programming Languages.
See MSDN: Extension Methods (Visual Basic)
AFAIK, what you are looking for is not supported / available in traditional Visual Basic for Applications (VBA).
Here's an example of doing extension methods in Visual Basic .Net (VB.Net) from the MSDN source.
Step 1. Declare Extension method like so...
Step 2. Call extension method like so...

Related

automatically declare object class variable when creating new sheet in EXCEL

How do I declare a - private - class variable, when a new sheet is created using the tab. It has to be done automatically. I presume it is a good idea to declare it by using the
Private Sub Workbook_NewSheet(ByVal Sh As Object) -event from the Wookbook object
Sub Workbook_NewSheet(ByVal Sh As Object)
Dim sh.privateVariableOfSheet As Integer
Declare New sh.privateVariableOfSheet2 As Integer
End Sub
Both above 'declarations' fails of course!
And for the completeness, how to refer to this variable from an ordinary module.
Another method is to use the CustomProperties collection of the Worksheet. For example:
Option Explicit
Private Sub Workbook_NewSheet(ByVal Sh As Object)
Sh.CustomProperties.Add Name:=Sh.Name, Value:=99
End Sub
You can then use it later in a sub-routine:
Option Explicit
Sub Test()
Dim var As Variant
var = ThisWorkbook.Worksheets("Sheet13").CustomProperties(1)
MsgBox var
End Sub
Let take some class - clsFoo - which has a single property of type Range with a getter and setter:
Private m_rngSomewhere As Range
Public Property Get SomeRange() As Range
Set SomeRange = m_rngSomewhere
End Property
Public Property Set SomeRange(rng As Range)
Set m_rngSomewhere = rng
End Property
Now, in the Workbook code module you have:
a Public variable which we will set as a Dictionary
a Sub to instantiate the Dictionary - could be called from Workbook_Open or something
an event handler for Workbook_NewSheet
The event handler creates a new instance of clsFoo and sets its property as a Range from the new Worksheet, and then adds that to the dictionary (and checks if it was already there for some new reason).
Code in Workbook module:
Option Explicit
Public SheetFooDic As Object
Public Sub InitialiseSheetFooDic()
Set SheetFooDic = CreateObject("Scripting.Dictionary")
End Sub
Private Sub Workbook_NewSheet(ByVal Sh As Object)
Dim rng As Range
Dim cls As clsFoo
If Not SheetFooDic.Exists(Sh) Then
Set rng = Sh.Range("A1")
Set cls = New clsFoo
Set cls.SomeRange = rng
SheetFooDic.Add Sh, cls
End If
End Sub
This leaves you needing to simply use some Worksheet object as a key into the Dictionary in order to retrieve the Range you stored when the Worksheet was created. You can refer to the public Dictionary like this:
ThisWorkbook.SheetFooDic(ThisWorkbook.Worksheets("Sheet2")).SomeRange.Address
And get:
$A$1
You can store the address of the last selected cell as a string. So, asking for an object variable might be misleading. Either way, the easiest way is to declare a public variable (for example PrevCell As String, or As Range if you prefer or need the value) in each of your worksheets' code and set that variable in each sheet's Selection_Change event procedure.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
PrevCell = Target.Cells(1) ' or .Address
End Sub
Since you may find it useful to avoid recording selections of multiple cells my above procedure just records the address of the first cell which avoid errors that often crop up when a copy/paste action was performed on a sheet.
Now, when you insert a new sheet, don't use the Add method. Instead copy an existing sheet and clean it up the way you want. In this way the new sheet will already have the variable and the code that sets it.
I can't imagine your having need of the previous cell in any circumstance other than when the sheet is activated, but if my imagination is insufficient in this case, you might declare a global array with an element for each sheet, using the sheets' CodeName property for identification. This array would be set by the Selection_Change event procedure as demonstrated above, but when a sheet which isn't known to the array tries to register its latest selection it must be a new sheet and the array is extended to include it. The code to do so is inherited from other sheets by the same method described above.

Instantiate a class that sits in a workbook from XLA add-in

I have a class that I have defined in a workbook and I have an add-in installed already. I want to create an instance of that class from inside a module that is in the add-in without wiring the two from Tools/References. I want to do that purely with VBA, is there anyway to do that?
Create a function in your WB which returns an instance of the class, and use Application.Run from the add-in to call it.
Simple example:
ObjectSource.xlsm
Class module clsTest:
Public MyNumber As Long
Private Sub Class_Initialize()
MyNumber = 999
End Sub
Regular module:
Public Function GetObject() As Object
Set GetObject = New clsTest
End Function
Other workbook
Sub Tester()
Dim obj
Set obj = Application.Run("ObjectSource.xlsm!GetObject")
MsgBox obj.MyNumber ''>> 999
End Sub

How do you store a worksheet reference in a VBA object?

This is going to seem trivial to those of you steeped in Excel object programming but it's beat me.
In the past, I've done the following in Excel's vba to restore the activesheet before exiting a subroutine..
sub foo()
dim cursheet
cursheet = ActiveSheet
someOtherSheet.activate
....
cursheet.activate
end sub
That works fine. I attempted to do something similar using objects and after several different approaches, wrote the following in a new Problem class...
''''''''''''''''''''''
' sheet property
''''''''''''''''''''''
Public Property Get sheet() As Worksheet
Set sheet = psheet
End Property
Public Property Let sheet(Value As Worksheet)
Set psheet = Value
End Property
Public Sub saveCursheet()
Me.sheet = ActiveSheet
End Sub
Public Sub activateSheet()
Me.sheet.Activate
End Sub
In my code, I invoke the methods this way...
Sub TallyQuizScore()
Dim curStudent As Problem
Set curStudent = New Problem
curStudent.saveCursheet
Worksheets("QuizTallies").Activate
...
curStudent.activateSheet
End Sub
When I attempt to execute curStudent.activateSheet, I get an error saying I need an object. So I reran the calling code and stepped through the saveCursheet method. I see the activesheet get stored but notice that the sheet object disappears as soon as I hit the setter's end property line. I don't know if that's an artifact of the debugger or if the sheet really does get tossed when I hit the end property line but whatever it is, the object is gone when I attempt to reactivate it when I'm done.
The frustrating thing is what I really wanted to write in my caller was
curStudent.sheet = Activesheet
and
curStudent.sheet.Activate
by somehow inheriting the builtin worksheet methods but that led to a rabbit's warren of code as I tried to make it work.
So three questions:
Why did the sheet I stored in saveCursheet disappear?
What do I need to change to make the code work?
What do I need to do differently from the above approach to make the curStudent.sheet = Activesheet and it's partner, curStudent.sheet.Activate approach work?
You need a module-level variable to store the value while your code is doing other things. Note that it's private.
Also, as caught by ja72, in the case of objects it's Set, not Let:
UNTESTED:
Private m_Sheet as Worksheet
Public Property Get Sheet() As Worksheet
Set sheet = m_Sheet
End Property
Public Property Set Sheet(Value As Worksheet)
Set m_Sheet = Value
End Property
Public Sub saveCursheet()
Me.Sheet = ActiveSheet
End Sub
Public Sub activateSheet()
Me.m_Sheet.Activate
End Sub

I have a range in an excel document which I need to extract into a listbox in Visual basic

I have a range in an excel document which I need to extract into a listbox. I can do this through .txt file, but really would prefer to source the information from excel. How?
I am using Visual Studio 2010 and so far my code is:
Public class1 Public Class Form1
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim objXLApp As Excel.Application
Dim intLoopCounter As Integer
objXLApp = New Excel.Application
With objXLApp
.Workbooks.Open("C:\report.xls")
.Workbooks(1).Worksheets(1).Select()
For intLoopCounter = 1 To CInt(.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row)
List1.AddItem.Range("A" & intLoopCounter)
Next intLoopCounter
.Workbooks(1).Close(False)
.Quit()
End With
objXLApp = Nothing
End Sub
When I build this program I get two errors;
statement is not valid in namespace
end of statement expected (I did put end class but it adds more couple of errors")
I don't really do VB, but I think you should break the binding out of the loop with something like:
var data = .ActiveSheet.Range("A1:A"&.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row).Value2
List1.DataSource = data
List1.DataBind()
You could try (assuming you want the whole Range object to be bound to the list and not just the displayed value.
For intLoopCounter = 1 To CInt(.ActiveSheet.Cells.SpecialCells(xlCellTypeLastCell).Row)
List1.AddItem .ActiveSheet.Range("A" & intLoopCounter)
Next intLoopCounter
Edit:
Also you should use Set objXLApp = New Excel.Application. You're missing the set keyword.
This is also not how you declare classes in VBA. You make a new class like you make a new module. The class is named in the properties pane.
Also this is not how events are handled in VBA. If you want to handle the button click event of Button1 then the event handler must look like
Private Sub Button1_Click()
End Sub
It must have the same name as the object and the name of the event being handled. It also must be on the code page for the user form and has no parameters. VBA is really lacking in this regard.
Hope this helps!

How to assign an event to multiple objects with excel vba?

I have ten drop down menus on a worksheet each of which should respond the same to the GotFocus() event.
I have written the following code but I get a run time error (459) - "Object or class does not support the set if events"
In a class called clsPDRinput I have the following:
Public WithEvents inputObj As OLEObject
Public Property Set myInput(obj As OLEObject)
Set inputObj = obj
End Property
Public Sub tbPDRInput_GotFocus()
//Do some stuff...
End Sub
I am then running the following code which is producing the error:
Dim tbCollection As Collection
Public Sub InitializePDRInput()
Dim myObj As OLEObject
Dim obj As clsPDRInput
Set tbCollection = New Collection
For Each myObj In Worksheets("1. PDR Documentation").OLEObjects
If TypeName(myObj.Object) = "ComboBox" Then
Set obj = New clsPDRInput
Set obj.myInput = myObj <-- **THIS LINE THROWS ERROR**
tbCollection.Add obj
End If
Next myObj
Set obj = Nothing
End Sub
I am not sure what is causing this error. One thought I had is that OLEObject is too generic and not every OLEObject supports the GotFocus() event and that is why the code is giving the error message?
I have tried replacing OLEObject with MSForms.ComboBox but that doesn't resolve issue.
Any ideas - have googled for two hours now and come up blank...
EDIT - Update on what I think the issue is...
I did more investigating and here is what the issue is as far as I can tell.
If you declare a variable as OLEObject (as in ...inputObj as OLEObject) then the only events exposed are GotFocus() and LostFocus().
If you declare a variable as MSForms.ComboBox (as in ...inputObj as MSForms.ComboBox) then a variety of events are exposed (e.g. Change(), Click(), DblClick()) but the events GotFocus() and LostFocus() are not exposed
Points 1 and 2 are consistent with the object model in excel. As a result, when I try to assign a ComboBox to my class I get an error (see original post) as the ComboBox does not support the GotFocus() and LostFocus events.
Now for the puzzle. If I add a ComboBox onto a worksheet (using Control ToolBox) and I double click that ComboBox to get to the code behind then all events are exposed, including GotFocus() and LostFocus()!
The below works for me. There were a couple of problem with your code, and comboboxes don't have a GotFocus event, so you'll have to use a different one.
The collection needs to be a global in the module, not part of the class.
I couldn't get this to work using the generic "OLEobject" approach (same error you got).
' ### in the class
Public WithEvents inputObj As MSForms.ComboBox
Private Sub inputObj_Change()
MsgBox "Change!"
End Sub
' ### in a module
Dim tbCollection As Collection
Public Sub InitializePDRInput()
Dim myObj As OLEObject
Dim obj As clsPDRInput
Set tbCollection = New Collection
For Each myObj In Worksheets("Sheet1").OLEObjects
If TypeName(myObj.Object) = "ComboBox" Then
Set obj = New clsPDRInput
Set obj.inputObj = myObj.Object
tbCollection.Add obj
End If
Next myObj
End Sub
Update
I was too focused in making the code compile and someone was nice enough to point out that the answer below is bad juju. So do not use. It does compile, but not a good answer.
I reproduced your error and fixed by changing the following declaration:
Public WithEvents inputObj As OLEObject
to this:
Public inputObj As New OLEObject
Of course, this is a different type of declaration so I'm not sure if it will work for you. It does remove the exception.
I'd also like to note that if you don't have Option Explicit set, you should. There are some variables in your code that are not declared. My guess is that you perhaps modified the code before posting your question.
Just making sure.

Resources