Search code examples
vb.nettimerstopwatchelapsedtime

Tracking elapsed time for a set of Tasks


I am trying to write a small VB.NET application to conduct a cross team time in motion study in work. I need the ability to have multipul timers running for single tasks, and when a user has finished their tasks they can end it and it will be stored in a CSV file. I can do it with single tasks at once, and this has been running for a while within the business. What is the best method to have multipul tasks running at once. I had the idea of adding the tasks to a listview and having double click event as the end task function. However I can't seem to get the timers to run independantly. Can anyone help? Does this make sense?

This is the main bulk of the workload in this class. If this doesnt make sense I appologise in advance. I'm not a programmer by trade, it's just a side hobby which I am practising in work.

Another thing would be to have the elapsed seconds counting live, I can do this with a label but I would need an unlitmited amount of task in theory...

Public Class runningTask
    Dim timenow As DateTime = Now
    Dim elapsed
    Dim localTask = GlobalVars.task

    Public Sub AddItem()
        CreateTimer()

        Dim str(3) As String
        Dim itm As ListViewItem

        str(0) = localTask
        str(1) = timenow
        str(2) = elapsed

        itm = New ListViewItem(str)

        frmTime.lvTimers.Items.Add(itm)
    End Sub

    Private Sub CreateTimer()
        Dim tmr As New Timer
        tmr.Interval = 1000
        tmr.Enabled = True
        AddHandler tmr.Tick, AddressOf GlobalTimerTick
    End Sub

    Private Sub GlobalTimerTick(ByVal sender As Object, ByVal e As EventArgs)
        Dim t As TimeSpan = Now - timenow
        elapsed = String.Format("{0:00}:{1:00}:{2:00}:{3:00}", Math.Floor(t.TotalHours), t.Minutes, t.Seconds, t.Milliseconds)
    End Sub

End Class

Solution

  • I took a more OOP approach which may be more economical as far as Timers, but has a very simple interface. The core is a TaskItem which tracks the job and time, then a collection class to expose them, start them and stop them. By burying as much functionality at as low of level as possible things get simpler.

    I only used one timer and cant see why you'd need multiple timers for a single task. If a TaskItem records the DateTime when it starts (this happens automatically when it is created in mine), then you don't need any Timers at all. Elapsed is always calculated using StartedTime and either DateTime.Now (for a running task) or CompletedTime for completed tasks. I cant see where a Timer adds anything.

    You'll have to make many mods for your purposes. This does not save completed items, nor remove them from the list, but may give you a place to start. Its also a first-pass thing; there are surely issues here and there.

    Imports System.Collections.ObjectModel
    
    Public Class Tasks
    
        ' slight overkill
        Private Enum LVGroups
            Running
            Completed
        End Enum
    
        Public Enum TaskJobs
            SweepFloor
            BuildRobot
            CleanOven
            RepairRobot
            EmptyTrash
            TeachRobotToTalk
            MakeCoffee
            TeachRobotToWalk
            WashDishes
            TeachRobotVisualBasic
        End Enum
    
        ' class for a single task
        Public Class TaskItem
            ' these ought to be readonly; so no cheating
            Public Property ID As String
            Public Property EmpName As String
    
            Public Property StartedTime As DateTime
            Public Property CompletedTime As DateTime
    
            ' or job code...
            Public Property Job As TaskJobs
    
            ' put in a ClassLib or Namespace
            Friend Sub New(sName As String, tj As TaskJobs)
                EmpName = sName
                Job = tj
    
                StartedTime = DateTime.Now
                CompletedTime = DateTime.MaxValue
                ID = System.Guid.NewGuid.ToString
            End Sub
    
            Public Function IsActive() As Boolean
                Return (CompletedTime = DateTime.MaxValue)
            End Function
    
            Public Sub EndTask()
                CompletedTime = DateTime.Now
            End Sub
    
            Public Function GetElapsed() As String
                Dim t As TimeSpan
                If IsActive() Then
                    t = DateTime.Now - StartedTime
                Else
                    t = CompletedTime - StartedTime
                End If
                Return String.Format("{0:00}:{1:00}:{2:00}", Math.Floor(t.TotalHours),
                                                     t.Minutes, t.Seconds)
            End Function
    
            Public Overrides Function ToString() As String
                Return String.Format("{0} {1} {2}", EmpName, Job.ToString, GetElapsed)
            End Function
    
        End Class
    
    
        Private col As Collection(Of TaskItem)
    
        Private myLV As ListView
    
        ' assign the LV when tasks are created
        Public Sub New(lvTask As ListView)
            myLV = lvTask
            col = New Collection(Of TaskItem)
        End Sub
    
        ' add new task with EmpName and JobCode
        Public Sub NewTask(sName As String, tj As TaskJobs)
    
            Dim ti As New TaskItem(sName, tj)
            col.Add(ti)
    
            ' LV layout: empname, job, elapsed, ID
            ' but only 3 columns so that ID is "hidden"
    
            Dim lvi As New ListViewItem(ti.EmpName)
            lvi.SubItems.Add(ti.Job.ToString)
            lvi.SubItems.Add(ti.GetElapsed)
            lvi.SubItems.Add(ti.ID)
    
            lvi.Group = myLV.Groups(LVGroups.Running.ToString)
            myLV.Items.Add(lvi)
    
        End Sub
    
        ' called from dbl click on a running task item
        Public Sub StopTask(lvi As ListViewItem)
    
            Dim ti As TaskItem = (From t In col Where t.ID = lvi.SubItems(3).Text).First
    
            ' the form doesnt check the status, so we must
            If ti.IsActive Then
                ti.EndTask()
                lvi.Group = myLV.Groups(LVGroups.Completed.ToString)
            End If
    
        End Sub
    
        ' called from the form timer tick event
        Public Sub UpdateDisplay()
            Dim ti As TaskItem
    
            ' iterate items to get ID of runners
            For Each lvi As ListViewItem In myLV.Groups(LVGroups.Running.ToString).Items
                ' ToDo add error checking
    
                ti = (From t In col Where t.ID = lvi.SubItems(3).Text).First
                lvi.SubItems(2).Text = ti.GetElapsed
    
            Next
    
        End Sub
    
        Public Function RunnersCount() As Integer
            Return GetActiveList.Count
        End Function
    
        Public Function GetCompletedList() As List(Of TaskItem)
            ' long form
            Dim tList As New List(Of TaskItem)
    
            For Each ti As TaskItem In col
                If ti.IsActive = False Then
                    tList.Add(ti)
                End If
            Next
    
            Return tList
    
            ' short form
            'Dim list = (From t In col Where t.IsActive = False).ToList
        End Function
    
    
        Public Function GetActiveList() As List(Of TaskItem)
            ' long form
            'Dim tList As New List(Of TaskItem)
    
            'For Each ti As TaskItem In col
            '    If ti.IsActive Then
            '        tList.Add(ti)
            '    End If
            'Next   
            'Return tList
    
            ' short form
            Dim list = (From t In col Where t.IsActive).ToList
            Return list
    
        End Function
    End Class
    

    Usage:

    Public Class Form1
    
        Private myTasks As Tasks
    
        Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
            myTasks = New Tasks(Me.lvTask)
            cboTask.Items.AddRange([Enum].GetNames(GetType(Tasks.TaskJobs)))
        End Sub
    

    Add a task:

    myTasks.NewTask(cboEmp.Text, CType(cboTask.SelectedIndex, Tasks.TaskJobs))
    

    Stop a task:

    ' called from LV mousedoubleclick, so determine the item
    Dim lvi As ListViewItem = lvTask.GetItemAt(e.X, e.Y)
    myTasks.StopTask(lvi)
    

    Obviously, stopping a task might automatically append the data to the export file. That functionality would be in the Task Collection Class, where it would be done just before the task is removed from the collection (optional - they arent hurting any thing there as long as you can tell active from completed ones).

    Timer Tick event:

        myTasks.UpdateDisplay()
    

    enter image description here

    Finally, turn on Option Strict especially since people will be making decisions based on the project's output.