自製簡易排程器(2)

這一次來講比較進階型的排程器作法,主要想達到以下幾個目的:
1.能夠以類別的型式存在,方便改裝成類別庫的型式提供其它的應用程式引用。
2.不需要每秒﹝或每分鐘﹞不斷地去檢查是否已經抵達指定的時間,而是在指定時間到達時刻去執行必要程序即可。
3.可以將要執行的程序以CallBack方式傳入執行個體中。
4.當系統時間被改變的時候,依然能夠準確的在指定抵達時間執行程序。

        這一次來講比較進階型的排程器作法,主要想達到以下幾個目的:

1.能夠以類別的型式存在,方便改裝成類別庫的型式提供其它的應用程式引用。 
2.不需要每秒﹝或每分鐘﹞不斷地去檢查是否已經抵達指定的時間,而是在指定時間到達時刻去執行必要程序即可。 
3.可以將要執行的程序以CallBack方式傳入執行個體中。 
4.當系統時間被改變的時候,依然能夠準確的在指定抵達時間執行程序。

       關於第一點的要求其實就是要把整個排程器封裝成一個類別,在這個情形下就要思考這個類別會需要哪些個屬性、方法等等。先讓排程器的功能單純化,只有四種週期可以選擇:每小時、每天、每週與每月,所以先設定一個列舉來用。

Enum Period
       Hourly = 0
       Daily = 1
       Weekly = 2
       Monthly = 3
End Enum

       設計一個小小的RunningTime類別來定義指定時間﹝詳情請看原始碼﹞:這類別說穿了就是幾點幾分幾秒。

Public Class RunningTime

…(略)

End Class

     幾個屬性列示如下:

     1.DoPeriod :指定程序的執行週期
     2.WorkTime :指定程序的執行時間﹝時分秒﹞
     3.NextTime :指定程序下次的執行時間﹝年月日時分秒﹞
     4.IsStart  :計時器是否已被啟動
     5.iWeekDay :當週期設定為Weekly時,每個星期的星期幾要執行
     6.iMonthDay:當週期設定為Monthly時,每月幾號要執行

       第二點的要求用如何達成呢?一個簡單的想法是當啟動類別中的計時器時,先計算啟動到下一次要執行的時間將其設為System.Threading.Timer.Change方法的duetime參數,並且將period參數設為0,而於每次執行完程序後重新計算一次duetime參數,然後再執行一次System.Threading.Timer.Change方法。有人可能會認為這樣很麻煩,因為如果固定是每小時的話不就只要System.Threading.Timer.Change(第一次計算的dutime, 每小時的毫秒數)就可以了嗎?這個方法一樣可以應用在每日和每週;這樣就不需要每次還要重新去計算一次。可是因為每個月的天數是不一定的,所以為了要能夠讓這個排程器也能適用於每個月,只好採取這個比較麻煩的做法。在這個類別中有一個TimeChanged與其它相關連方法就是在作這個計算的工作:

  Private Sub TimeChanged()
       If m_IsStart = True Then
           SchTimer.Change(CalcNext(), 0)
       End If
   End Sub

       一旦需要重新計算時,就要呼叫這個方法來呼叫System.Threading.Timer.Change方法,藉以重新定義到下一次的執行時間,這邊傳入的dutime是從CalcNext函式而來,以下是CalcNex函式:

Private Function CalcNext() As Long
        Dim dTimeNow, dNextTime As Date
        dTimeNow = Now()
        Select Case m_DoPeriod
            Case Period.Hourly
                If CalTotalSeconds(0, 0, dTimeNow.Minute, dTimeNow.Second) >= CalTotalSeconds(0, 0, m_WorkTime.Minute, m_WorkTime.Second) Then
                    dNextTime = DateAdd(DateInterval.Hour, 1, dTimeNow).ToString("yyyy/MM/dd HH") & ":" & m_WorkTime.Minute & ":" & m_WorkTime.Second
                Else
                    dNextTime = dTimeNow.ToString("yyyy/MM/dd HH") & ":" & m_WorkTime.Minute & ":" & m_WorkTime.Second
                End If

            Case Period.Daily
                If CalTotalSeconds(0, dTimeNow.Hour, dTimeNow.Minute, dTimeNow.Second) >= CalTotalSeconds(0, m_WorkTime.Hour, m_WorkTime.Minute, m_WorkTime.Second) Then
                    dNextTime = DateAdd(DateInterval.Day, 1, dTimeNow).ToString("yyyy/MM/dd") & " " & m_WorkTime.Hour & ":" & m_WorkTime.Minute & ":" & m_WorkTime.Second
                Else
                    dNextTime = dTimeNow.ToString("yyyy/MM/dd") & " " & m_WorkTime.Hour & ":" & m_WorkTime.Minute & ":" & m_WorkTime.Second
                End If

            Case Period.Weekly
                If CalTotalSeconds(dTimeNow.DayOfWeek, dTimeNow.Hour, dTimeNow.Minute, dTimeNow.Second) >= CalTotalSeconds(m_WeekDay, m_WorkTime.Hour, m_WorkTime.Minute, m_WorkTime.Second) Then
                    dNextTime = DateAdd(DateInterval.Day, (m_WeekDay - dTimeNow.DayOfWeek + 7), dTimeNow).ToString("yyyy/MM/dd") & " " & m_WorkTime.Hour & ":" & m_WorkTime.Minute & ":" & m_WorkTime.Second
                Else
                    dNextTime = DateAdd(DateInterval.Day, (m_WeekDay - dTimeNow.DayOfWeek), dTimeNow).ToString("yyyy/MM/dd") & " " & m_WorkTime.Hour & ":" & m_WorkTime.Minute & ":" & m_WorkTime.Second
                End If

            Case Period.Monthly
                Dim dChkTime As Date
                dChkTime = (dTimeNow.ToString("yyyy/MM") & "/01 00:00:00")
                If CalTotalSeconds(dTimeNow.Day - 1, dTimeNow.Hour, dTimeNow.Minute, dTimeNow.Second) >= CalTotalSeconds(m_MonthDay - 1, m_WorkTime.Hour, m_WorkTime.Minute, m_WorkTime.Second) Then
                    dNextTime = FindNextDayofMonth(DateAdd(DateInterval.Month, 1, dChkTime))
                Else
                    dNextTime = FindNextDayofMonth(dChkTime)
                End If

        End Select
        m_NextTime = dNextTime
        CalcNext = CalcToMS(dTimeNow, dNextTime)
    End Function

       這個CalcNex函式主要是在執行各種不同週期的計算,找出下一次要執行指定程序的確切時間存入dNextTime變數,並經由指派給m_NextTime變數,使得呼叫這個類別執行個體的程式可經由NextTime屬性得知下次執行時間。計算出下次執行時間後則使用CalcToMS函式去算出從現在到下一次指定執行時間的毫秒數。所以TimmeChanged()方法中就以這個數值作為傳入System.Threading.Timer.Change方法的duetime參數。

        CalTotalSeconds函式是用於確認下次的執行時間是否必須增加一個完整的週期。

        FindNextDayofMonth則是一個遞迴的函式,用於處理週期為月的時候,尋找下一個符合條件的日期,這個函式就是因為每個月的日數不一定才需要的,通常日期小於或等於28一次就會結束;這個函式也可用迴圈方式寫,不一定非用遞迴不可。
 

       第三個要求則是要在類別中先宣告一個委派,Public Delegate Sub SchCallBack(ByVal state As Object),這個委派再傳入建構函式後會被轉換為一個State Object當成是System.Threading.Timer建構函式的State Object;之所以不讓這個委派直接成為System.Threading.Timer的TimerCallBack參數,是因為當執行完呼叫這個類別的委派程序後,類別本身還需要處理其它事情,在範例中就是呼叫TimeChaned方法。至於Timer是如何執行了自己的TimerCallBack又可以順便執行已經變成了State Object的SchCallBack,可以看一下以下的程序

[在建構函式中建立System.Threading.Timer的執行個體]
SchTimer = New System.Threading.Timer(AddressOf myCallBack, objState, -1, 0)

[使用TimeChnge方法設定好duetime]
Private Sub TimeChanged()
        If m_IsStart = True Then
            SchTimer.Change(CalcNext(), 0)
        End If
End Sub

[在執行TimerCallback的程序中,將狀態物件還原為SchCallBack並且呼叫Invoke方法]
Private Sub myCallBack(ByVal state As Object)
        CType(state, SchCallBack).Invoke(Nothing)
        TimeChanged()
End Sub

       第四個要求是當使用者直接去更動系統時間的情形必需能夠加以反應,並重新呼叫TimeChaned方法重新設定時間,本來我想說這是不是要去呼叫Win32 API,不過後來發現 .Net Framework中其實就有提供事件可以使用了,這個有趣的事件是[SystemEvents.TimeChanged 事件] ,讓我感嘆 .Net Framework真是博大精深兼無奇不有。因此我們在類別的建構函式中要增加這個一事件的委派:

AddHandler Microsoft.Win32.SystemEvents.TimeChanged, AddressOf SysTimeChanged

並且在此事件程序中呼叫TimeChaned方法。

Private Sub SysTimeChanged(ByVal sender As Object, ByVal e As EventArgs)
     '當系統時間被改變,呼叫Timechanged
     TimeChanged()
End Sub

       整個排程器的概念大約這樣就完成了,不過其實還缺挺多東西的,例如可以給這個類別增加一些事件供傳遞訊息到呼叫者、或是利用IsStart屬性來停止排程器、或是可以改成每四小時 or 每三天為週期之類的,小弟的文章僅是拋磚引玉,各位網友就可以自由發揮了。程式是以VB2005撰寫,可在後面的連結下載,因為沒有測試的很徹底,如果有任何問題或謬誤之處也煩請各位不吝指教。SchedulerTest3.rar