I am wondering whether it is possible to create a script that runs a calculation for a selected task's field after a change event. I currently have a script that runs calculations for every task in my ms project file. I want to move away from this method by implementing code that isolates a task and computes a respective value.
Can someone please let me know if this is possible?
I do not have a bock of code at the moment because I have been struggling on where to start. I have referenced this thread (Microsoft Project VBA to update Custom field on task change) but have not been able to implement it with success.
My cm_Events class module:
Public WithEvents MyMSPApp As MSProject.Application
Private Sub Class_Initialize()
Set MyMSPApp = Application
End Sub
Private Sub MyMSPApp_ProjectBeforeAssignmentChange(ByVal Assgn As Assignment, ByVal Field As PjField, ByVal NewVal As Variant, Cancel As Boolean)
If EnableEvents Then
EnableEvents = False
Dim X As Integer
X = 5
Debug.Print X
EnableEvents = True
End If
End Sub
My m_Events Module:
Public oMSPEvents As New cm_Events
Public EnableEvents As Boolean
Sub StartEvents()
Set oMSPEvents.MyMSPApp = Application ' MSProject.Application
EnableEvents = True
End Sub
My Project Code Block:
Private Sub Project_Open(ByVal pj As Project)
Call m_Events.StartEvents
End Sub
What I Would Place Into Before Assignment Change Script:
tsk.Cost5 = 0
Dim Assgn As Assignment
For Each Assgn In tsk.Assignments
If Assgn.Resource.Text1 = "Labor" Then
tsk.Cost5 = tsk.Cost5 + Assgn.Cost
Assgn.Cost5 = Assgn.Cost5 + Assgn.Cost
End If
Next Assgn
This code will track costs for Labor resources using the Cost5 field and will update automatically when either the task or the assignment is updated, using the respective event handlers.
For the cm_Events class module:
Option Explicit
Public WithEvents MyMSPApp As MSProject.Application
Private Sub Class_Initialize()
Set MyMSPApp = Application
End Sub
Private Sub MyMSPApp_ProjectBeforeAssignmentChange(ByVal asg As Assignment, ByVal Field As PjAssignmentField, ByVal NewVal As Variant, Cancel As Boolean)
If EnableEvents Then
EnableEvents = False
Dim tsk As Task
Set tsk = asg.Task
tsk.Cost5 = 0
UpdateCost5 tsk
EnableEvents = True
End If
End Sub
Private Sub MyMSPApp_ProjectBeforeTaskChange(ByVal tsk As Task, ByVal Field As PjField, ByVal NewVal As Variant, Cancel As Boolean)
If EnableEvents Then
EnableEvents = False
tsk.Cost5 = 0
UpdateCost5 tsk
EnableEvents = True
End If
End Sub
Private Sub UpdateCost5(tsk As Task)
tsk.Cost5 = 0
Dim Assgn As Assignment
For Each Assgn In tsk.Assignments
If Assgn.Resource.Text1 = "Labor" Then
tsk.Cost5 = tsk.Cost5 + Assgn.Cost
Assgn.Cost5 = Assgn.Cost
Else
Assgn.Cost5 = 0
End If
Next Assgn
End Sub
For the m_Events module:
Public oMSPEvents As New cm_Events
Public EnableEvents As Boolean
Sub StartEvents()
Set oMSPEvents.MyMSPApp = Application ' MSProject.Application
EnableEvents = True
End Sub
For the ThisProject module:
Private Sub Project_Open(ByVal pj As Project)
Call m_Events.StartEvents
End Sub
Tips:
make sure cm_Events is a class module and not a regular module.
any time you make a change to the code the events will stop running--always run the StartEvents procedure after making any changes.
Start with simple code in the ProjectBeforeTaskChange event like a debug statement to verify the event is getting triggered.
the TaskChange event is just that--triggered by changes to the task, but not to a resource or assignment.
Related
I'm trying to display the progress of various routines on a modeless form, by having those routines raise custom events detailing their progress. The form should handle those events to display appropriate information.
The problem is that although RaiseEvent is called, the event handlers don't then do anything.
The intended result of the following code is the two debug.prints would be called whenever an event is raised by triggerTest.
The only success I've had is with raising the error within the userform, by CommandButton1_Click in the following code. The form's event handler then kicks in (rather redundantly, but perhaps that means I'm on the right path).
Thanks
Event class clsChangeProgressTrigger
Option Explicit
Public Enum geProgressStatus
geProgressStatusComplete = -1
geProgressStatusRestart = -2
End Enum
Public Event ChangeProgress(dProgress As Double, sProcedure As String)
'
Public Sub Update(dProgress As Double, sProcedure As String)
RaiseEvent ChangeProgress(dProgress, sProcedure)
End Sub
Public Sub Complete(sProcedure As String)
RaiseEvent ChangeProgress(geProgressStatusComplete, sProcedure)
End Sub
Public Sub Restart(sProcedure As String)
RaiseEvent ChangeProgress(geProgressStatusRestart, sProcedure)
End Sub
User form frmOutput
Option Explicit
Private WithEvents mProgressTrigger As clsChangeProgressTrigger
'
Private Sub CommandButton1_Click()
Call mProgressTrigger.Update(12.34, "SomeValue")
End Sub
Private Sub CommandButton2_Click()
Call modZTest.triggerTest
End Sub
Private Sub UserForm_Initialize()
Set mProgressTrigger = New clsChangeProgressTrigger
End Sub
Private Sub mProgressTrigger_ChangeProgress(dProgress As Double, sProcedure As String)
Debug.Print "Form Event Handled"
End Sub
Event test class clsEventTest
Option Explicit
Private WithEvents mProgressTrigger As clsChangeProgressTrigger
'
Private Sub mProgressTrigger_ChangeProgress(dProgress As Double, sProcedure As String)
Debug.Print "Class Event Handled"
End Sub
Private Sub Class_Initialize()
Set mProgressTrigger = New clsChangeProgressTrigger
End Sub
Test wrapper in public module modZTest
Public Sub triggerTest()
Application.EnableEvents = True
' Instantiate Trigger class for this routine
' Dim cChangeProgressTrigger As clsChangeProgressTrigger
Set gChangeProgressTrigger = New clsChangeProgressTrigger
' Instantiate Event Test class, which should handle raised event
Dim cEventTest As clsEventTest
Set cEventTest = New clsEventTest
' Instantiate user form, which should handle raised event
Set gfrmOutput = New frmOutput ' Modeless form, gfrmOutput has global scope
gfrmOutput.Show
Stop
' Raise an event
Call gChangeProgressTrigger.Complete("SomeValue")
' Tidy Up
Set gfrmOutput = Nothing
Set gChangeProgressTrigger = Nothing
Set cEventTest = Nothing
End Sub
Thanks Dee, that helped me along to the solution.
With this declared as global scope:
Public gChangeProgressTrigger As clsChangeProgressTrigger
I had to change the class / form level initialisations as follows:
Private Sub UserForm_Initialize()
' Set mProgressTrigger = New clsChangeProgressTrigger ' Old
Set mProgressTrigger = gChangeProgressTrigger ' New
End Sub
and
Private Sub Class_Initialize()
' Set mProgressTrigger = New clsChangeProgressTrigger ' Old
Set mProgressTrigger = gChangeProgressTrigger ' New
End Sub
Then the event handlers fired as desired.
I am new to vb.net multithreading, and I am facing a issue.
For threading I am using system.threading.thread function.
I know I can stop a thread using thread.abort call. But that's not a good choice (I have searched a bit and came to know that). This also will not work because my running threads needs some cleanup before cancellation.
So, I'm trying to do some kind of action that set a variable to UI thread like
dim bolStop as Boolean
So, if my UI thread set that Boolean value as true then all running thread can access/notice that change and can start cleanup process before stop/cancel the thread itself.
hope that makes sense...
best regards
Edit:1 (Source Is Pasted Here as Suggested By #DrunkenCodeMonkey
This is the code for Main Form (Form1.vb) (I think i need something here and also on the thread module (class) too) so, below i will paste module code too
Public Class Form1
Private objThreadList As ArrayList
Dim bolStop As Boolean
Private Sub cmd1_Click(sender As System.Object, e As System.EventArgs) Handles cmd1.Click
Dim intTLoop As Integer
sPrepareThreading()
For intTLoop = 1 To 5
sSendThread("", "Zakir", "Biplob")
Next
'sFinishThreading()
MessageBox.Show("Done!")
End Sub
Sub sPrepareThreading()
Dim intTLoop As Integer
objThreadList = New ArrayList
For intTLoop = 1 To 3
objThreadList.Add(Nothing)
Next intTLoop
End Sub
Sub sSendThread(strDraft As String, ByVal strTitle As String, ByVal strSubHeader As String)
Dim bolFalied As Boolean
Dim intTLoop As Integer
Dim objItem As System.Windows.Forms.ListViewItem
Dim objTData As objThreadCrossData
Dim objThreadClass As clsThread
Dim objNewThread As System.Threading.Thread
Zakir_RecheckThread:
My.Application.DoEvents()
If bolStop = True Then
Exit Sub
End If
bolFalied = True
For intTLoop = 1 To 3
bolFalied = True
If objThreadList(intTLoop - 1) Is Nothing Then
bolFalied = False
Else
If CType(objThreadList(intTLoop - 1), System.Threading.Thread).IsAlive = False Then
bolFalied = False
Else
bolFalied = True
End If
End If
If bolFalied = False Then
objTData = New objThreadCrossData
objTData.ID = ListView1.Items.Count
objTData.bolStop = False
objTData.strTime = DateTime.Now.ToString
objItem = ListView1.Items.Add(strTitle)
objItem.SubItems.Add(strSubHeader)
objItem = Nothing
objThreadClass = New clsThread(objTData, "", Me)
objNewThread = New System.Threading.Thread(AddressOf objThreadClass.StartThread)
objNewThread.IsBackground = True
objNewThread.Start()
objThreadList(intTLoop - 1) = objNewThread
Exit For
End If
Next
If bolFalied = True Then
GoTo Zakir_RecheckThread
Exit Sub
End If
End Sub
Public Sub ReceiveThreadMessage(ByVal objCrosData As Object)
Dim objTDataV1 As objThreadCrossData
objTDataV1 = CType(objCrosData, objThreadCrossData)
ListView1.Items(objTDataV1.ID).SubItems(1).Text = objTDataV1.strProgress
End Sub
Private Sub cmd2_Click(sender As System.Object, e As System.EventArgs) Handles cmd2.Click
REM This is where i like to stop the running thred either by setting bolStop = True and all running thead check that variable.
REM or by chate the thread (Thread Data Object Property naemd "bolStop" value"
REM please help me out...
End Sub
End Class
Code For Thread Module (clsThread.vb):
Public Class clsThread
Private m_Counter As Integer = 0
Private m_MainWindow As Form
Dim m_ThreadCrorssData As objThreadCrossData
Private Delegate Sub NotifyMainWindow(ByVal objCrosData As objThreadCrossData)
'We need an object of this deletegate
Private m_NotifyMainWindow As NotifyMainWindow
Public Sub New(ByVal objCrossData As Object, ByVal strXML As String, ByRef MainWindow As Form1)
m_MainWindow = MainWindow
m_ThreadCrorssData = CType(objCrossData, objThreadCrossData)
'We need to point our delegate to the Method, which we want to call from this thread
m_NotifyMainWindow = AddressOf MainWindow.ReceiveThreadMessage
End Sub
Public Sub StartThread()
While m_Counter < 100
m_Counter = m_Counter + 1
rem for example here i like to check for the value that whether user asked to stop
m_ThreadCrorssData.strProgress = CStr(m_Counter)
m_MainWindow.Invoke(m_NotifyMainWindow, m_ThreadCrorssData)
rem for example also here i like to check for the value that whether user asked to stop
'wait for some time before continuing loop
System.Threading.Thread.Sleep(1000)
End While
End Sub
End Class
Here is the Code For Module (Thread Object Data)
Module mdlCMM
Public Class objThreadCrossData
Property strProgress As String
Property bolStop As Boolean = False
Property strTime As String
Property ID As Integer
Property intStatusNum As Integer
Property strStatusValue As String
End Class
End Module
I'm trying struggling to understand how to create a custom event using class modules in VBA.
I've put together the simple following example. You put a value in A1 and B1 and then re activate the sheet to calculate the sum of the two and then I hoped an event would fire to warn of calculation, but nothing happens.
I'd be very grateful for any help solving this example.
Class module cCalc:
Dim m_wks As Worksheet
Public Event BeforeCalc()
Property Set Worksheet(wks As Worksheet)
Set m_wks = wks
End Property
Public Sub Calc()
Dim dVal1 As Double
Dim dVal2 As Double
With m_wks
dVal1 = .Range("A1").Value
dVal2 = .Range("B1").Value
RaiseEvent BeforeCalc
.Range("C1").Value = dVal1 + dVal2
End With
End Sub
In a module mGlobal:
Public gCalc As cCalc
In the code behind Sheet1:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set gCalc = New cCalc
Set gCalc.Worksheet = ActiveSheet
gCalc.Calc
End Sub
You can't declare event-driven classes in modules. You'll need to set the cCalc reference in gModule equal to the object you declared WithEvents in Sheet1. Change your code in Sheet1 to what i wrote below and it will work:
Private WithEvents calcEvent As cCalc
Private Sub calcEvent_BeforeCalc()
MsgBox "About to Calc!", vbInformation
End Sub
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc 'Instantiate the WithEvents object above
Set mGlobal.gCalc = calcEvent 'Set the object declared in gModule
Set mGlobal.gCalc.Worksheet = ActiveSheet
mGlobal.gCalc.Calc
End Sub
Note that this is using the variable you put in gModule... The event that is actually called is still calcEvent_BeforeCalc(), which is good as this way you can have n number of objects defined in gModule that would all fire off the same event code when the event is triggered.
To simplify the code, you could always just write:
Private Sub Worksheet_Activate()
Set calcEvent = New cCalc
Set calcEvent.Worksheet = ActiveSheet
calcEvent.Calc
End Sub
I'm trying to use the .OnTime method in a class module, but can't figure out how to call a procedure in the class. All of the .OnTime examples I've seen refer to using the method from a standard code module rather than a custom class. Is there any way of calling a procedure in the class module rather than a standard code module?
#Alex P: Updated to include code. Here is the Class Module:
Option Explicit
Public Sub Test()
MsgBox "Success"
End Sub
Private Sub Class_Initialize()
Application.OnTime EarliestTime:=Now + TimeValue("00:00:03"), _
Procedure:="Test"
End Sub
And the Standard Module:
Option Explicit
Public Sub TestOnTime()
Dim OnTime As CCOnTime
Set OnTime = New CCOnTime
End Sub
I've also tried Procedure:="CClass.Test"
You can do it, but the call-back needs to be bounced back into the object from a Standard Module or a Worksheet Module or Thisworkbook.
Here is an example that pulses a value in a worksheet cell.
The timer is (almost) encapsulated in the cOnTime Class.
A cOnTime Object is instantiated in the host worksheet, whose code module can have a property to set the pulse time as well as the call-back routine.
If you protect the sheet, it will start pulsing and you can stop it by un-protecting the sheet.
If you navigate away from the host sheet, the timer is killed and if you navigate back it re-starts (as long as the sheet is protected).
Class cOnTime
Option Explicit
Const DEFPulseTime = "PulseTime"
Const DEFearliestTime As Long = 5
Const DEFlatestTime As Long = 15
Public WithEvents wb As Workbook
Public ws As Worksheet
Private DoWhen As String
Public mPulseTime As Long
Public mNextTime As Double
Property Let callBackDoWhen(cb As String)
DoWhen = "'" & wb.Name & "'!" & ws.CodeName & "." & cb 'e.g. 'wb Name.xlsm'!Sheet1.kickdog
End Property
Private Function PulseTime() As Long
On Error Resume Next
PulseTime = CallByName(ws, DEFPulseTime, VbGet)
If Err.number <> 0 Then
PulseTime = DEFearliestTime
End If
On Error GoTo 0
End Function
Property Get designMode() As Boolean
designMode = Not ws.ProtectContents
End Property
Public Sub kickDog()
Const myName As String = "kickDog"
Dim psMessage As String
If ws Is ActiveSheet And Not designMode Then
mNextTime = Now + TimeSerial(0, 0, mPulseTime)
On Error Resume Next
Application.OnTime mNextTime, DoWhen
On Error GoTo 0
End If
Exit Sub
End Sub
Public Sub killDog()
If ws Is Nothing Or mNextTime = 0 Then Exit Sub
On Error Resume Next
Application.OnTime mNextTime, DoWhen, , False
On Error GoTo 0
End Sub
Private Sub Class_Initialize()
Dim errorContext As String
On Error GoTo enableAndExit
Set wb = ActiveWorkbook
Set ws = ActiveSheet
On Error GoTo 0
callBackDoWhen = DEFDoWhen
callBackPulseTime = DEFPulseTime
mPulseTime = PulseTime
kickDog
Exit Sub
enableAndExit:
If Err <> 0 Then
If ws Is Nothing Then
errorContext = "ws"
ElseIf wb Is Nothing Then
errorContext = "wb"
End If
End If
End Sub
Private Sub Class_Terminate()
Const myName As String = "Class_Terminate"
On Error Resume Next
killDog
Set ws = Nothing
Set wb = Nothing
Exit Sub
End Sub
Private Sub wb_WindowActivate(ByVal Wn As Window)
wb_Open
End Sub
Private Sub wb_WindowDeactivate(ByVal Wn As Window)
killDog
End Sub
Private Sub wb_BeforeClose(Cancel As Boolean)
killDog
End Sub
Private Sub wb_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean)
If SaveAsUI Then killDog
End Sub
In Worksheet Module
Option Explicit
Const cPulseTime As Long = 1
Dim mOnTime As cOnTime
Property Get PulseTime() As Long
PulseTime = cPulseTime
End Property
'****************************************
'Timer call-back for cOnTime
Public Sub kickDog()
' Code to execute on timer event
'******************************************
On Error Resume Next
Me.Cells(1, 1) = Not Me.Cells(1, 1)
On Error GoTo 0
'******************************************
Debug.Print "woof!!"
mOnTime.kickDog
End Sub
Private Sub Worksheet_Activate()
Me.Cells(1,1) = False
Set mOnTime = New cOnTime
End Sub
Private Sub Worksheet_Deactivate()
On Error Resume Next
Set mOnTime = Nothing
End Sub
You are asking for magic - VBA is a comprehensive tool but it is not magic.
The reason is that every Class module is simply a template which can be instantiated any number of times in the application code. Excel could not hope to correctly guess which particular instantiation of the Class module is the correct one on which to invoke the method. You are responsible for making this decision and managing the references to the appropriate Class instance.
Ah you say - But there is no private data/references being used by the method I want called. It is a static method. Well the answer to that is that VBA does not support static methods on Class modules, only on Standard modules. Any method that you wish to declare to the environment as being static is declared as being static by being included in a standard module.
So, place your call-back method in a Standard module, and declare a private member that holds a reference to the particular instance of the Class that you wish to handle the event.
One alternative route which isn't specified by others here but, I think, would theoretically work is:
Register VBA object as an active object with a specified GUID
Launch a PowerShell (or binary) daemon to connect to the object with specified GUID and call a specified method on that object every n seconds.
Pros:
Totally encapsulated
Cons:
Launches external process
Method likely has to be public (unless connection points can be abused)
Potentially crash-prone
I haven't implemented such a solution yet but intend to on stdCOM of stdVBA library if everything works out.
In excel vba I have added a commandbutton to userform... like below
Set ctrl = Me.Controls.Add( _
bstrProgID:="Forms.CommandButton.1", _
Name:="CommandButton1", Visible:=True)
Now I wanted to know how would I tell it what to do when it is clicked?
This is one of those techniques that vba will let you do, but you probably shouldn't. For all the same reasons you shouldn't use code that alters your code.
That said, here is how to do what you want. First insert a class module and name it DynBtn, then paste this code into it:
Private WithEvents mobjBtn As MSForms.CommandButton
Private msOnAction As String
''// This has to be generic or call by name won't be able to find the methods
''// in your form.
Private mobjParent As Object
Public Property Get Object() As MSForms.CommandButton
Set Object = mobjBtn
End Property
Public Function Load(ByVal parentFormName As Object, ByVal btn As MSForms.CommandButton, ByVal procedure As String) As DynBtn
Set mobjParent = parentFormName
Set mobjBtn = btn
msOnAction = procedure
Set Load = Me
End Function
Private Sub Class_Terminate()
Set mobjParent = Nothing
Set mobjBtn = Nothing
End Sub
Private Sub mobjBtn_Click()
CallByName mobjParent, msOnAction, VbMethod
End Sub
Now to use this in your form, create a blank user form and paste this code into it:
Private Const mcsCmdBtn As String = "Forms.CommandButton.1"
Private mBtn() As DynBtn
Private Sub UserForm_Initialize()
Dim i As Long
ReDim mBtn(1) As DynBtn
For i = 0 To UBound(mBtn)
Set mBtn(i) = New DynBtn
Next
''// One Liner
mBtn(0).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn1", True), "DoSomething").Object.Caption = "Test 1"
''// Or using with block.
With mBtn(1).Load(Me, Me.Controls.Add(mcsCmdBtn, "Btn2", True), "DoSomethingElse").Object
.Caption = "Test 2"
.Top = .Height + 10
End With
End Sub
Public Sub DoSomething()
MsgBox "It Worked!"
End Sub
Public Sub DoSomethingElse()
MsgBox "Yay!"
End Sub
Private Sub UserForm_Terminate()
Erase mBtn
End Sub