|
Eine Klasse, die selbsttätig im Hintergrund arbeitet, ihr nach und nach zugeschobene Aufgaben der Reihe nach abwickelt, und die erledigten Jobs auf Abruf zur Verfügung stellt? Die sich die Aufgaben gegebenenfalls sogar selbst abholt und genau auch selbst wieder abliefern kann? Zugegeben, das mag sich vielleicht etwas abstrakt und auch kompliziert anhören. Aber ich bin sicher, dass Ihnen der eine oder andere Verwendungszweck aufgehen wird, wenn Sie das zu diesem Artikel herunterladbare Beispiel ausprobiert haben. Und wenn Sie gesehen, haben, wie einfach die Handhabung ist...
Das Grundprinzip ist sogar wirklich sehr simpel. Man nehme zwei Collections ("ToDo" und "Ready") und einen Timer. Dazu eine Klasse ("Job"), deren Instanzen mit Daten gefüttert und in der ersteren Collection abgelegt werden. Im Timer-Ereignis wird nun jeweils eine Instanz aus dieser ToDo-Collection geholt und eine bestimmte Methode der Klasse zur Bearbeitung der Daten, mit der die jeweilige Instanz gefüttert worden war, aufgerufen. Dann wird die Instanz in der Ready-Collection abgelegt und steht damit zur Abholung bereit.
Die Job-Klasse (clsJob) könnte beispielsweise so aussehen - ihre Aufgabe wäre hier, einen Datenwert zu verdoppeln:
Private pData As Long
Private pResult As Long
Public Sub SetData(ByVal Data As Long)
pData = Data
End Sub
Public Sub Execute()
pResult = pData * 2
End Sub
Public Property Get Result() As Long
Result = pResult
End Sub
Der Code zur Verwendung in einem Form, auf der besagte Timer platziert ist, könnte nun (ein wenig vereinfacht dargestellt) wie folgt aussehen - die Ausgangsdaten befinden sich hier in einer ListBox, die Resultate sollen in einer anderen ListBox abgelegt werden:
Private mToDo As Collection
Private mReady As Collection
Private Sub Form_Load()
' ListBox1 mit 500 Zahlen füllen...
Set mToDo = New Collection
Set mReady = New Collection
End Sub
Private Sub cmdAdd_Click()
Dim i As Integer
Dim nJob As clsJob
With ListBox1
For i = 1 To 25
If .ListCount Then
Set nJob = New clsJob
nJob.SetData .List(i)
mToDo.Add nJob
Else
Exit For
End If
Next 'i
End With
End Sub
Private Sub Timer1_Timer()
Dim nJob As clsJob
With mToDo
If .Count Then
Set nJob = .Item(1)
.Remove 1
nJob.Execute
mReady.Add nJob
End
End With
End Sub
Private Sub cmdResults_Click()
With mReady
If .Count Then
ListBox2.AddItem .Item(1).Result
.Remove 1
End If
End With
End Sub
Über die cmAdd-Schaltfläche fügen Sie neue Jobs dem wartenden Stapel hinzu - entweder gleichen einen ganzen Pulk von neuen Jobs, oder auch einzelne Jobs. Diese Jobs brauchen natürlich nicht so trivial wie in diesem Beispiel zu sein. Es können durchaus auch anspruchsvollere Aufgaben von einer solchen Job-Klasse ausgeführt werden, so etwa auch länger andauernde Bearbeitungen von Dateien und dergleichen. Die Detailstruktur einer Job-Klasse kann individuell angelegt werden, mit beliebig vielen weiteren Eigenschaften und Methoden.
Es braucht noch nicht einmal eine spezielle Job-Klasse zu sein. Wenn Sie wissen, was in Visual Basic mit Schnittstellen und deren Implementierung machbar ist (das Thema ist zu umfangreich, um im Rahmen dieses Artikels darauf einzugehen), werden Sie sich vorstellen können, dass Sie jede beliebige Klasse in diesem Sinne "job-fähig" machen können. Genau so gut können Sie auch Instanzen völlig verschiedener Klassen nacheinander auf den ToDo-Stapel legen, solange diese Klassen die job-fähige Schnittstelle implementieren - etwa verschiedene Dateibearbeiter für verschiedene Dateiformate. Natürlich sollten solche Langzeitbearbeiter mit DoEvents-Aufrufen dafür sorgen, dass sie nicht die ganze Anwendung blockieren, oder diese Bearbeiter sind in einer ActiveX-EXE angelegt und können so in einem eigenen Thread im Hintergrund operieren.
So schlicht der oben stehende Form-Modul-Code auch ist - er ist immer noch etwas zu umständlich. Die ganze Abwicklung können Sie auch in eine eigenständige Klasse packen, gemäß der Überschrift dieses Artikels in eine "Arbeiter-Klasse" (clsWorker):
Private mToDo As Collection
Private mReady As Collection
Private Sub Class_Initialize()
Set mToDo = New Collection
Set mReady = New Collection
End Sub
Public Sub Add(Job As clsJob)
mReady.Add Job
End Sub
Public Sub Trigger()
Dim nJob As clsJob
With mToDo
If .Count Then
Set nJob = .Item(1)
.Remove 1
nJob.Execute
mReady.Add nJob
End If
End With
End Sub
Public Sub Fetch(Job As clsJob)
With mReady
If .Count Then
Set Job = .Item(1)
.Remove 1
End If
End With
End Sub
Die Trigger-Methode wird direkt vom Timer-Ereignis aufgerufen. Und beim Aufruf der Fetch-Methode brauch Sie lediglich noch darauf zu achten, ob tatsächlich eine Job-Instanz geliefert wird, oder Nothing.
Eingangs hatte ich Ihnen allerdings noch etwas mehr Komfort versprochen, etwa die Selbstabholung oder die automatische Ablieferung. Beide werden über weitere Eigenschaften, AutoFeed und AutoDeliver, angelegt. Setzen Sie eine der Eigenschaften auf 0, so ist das jeweilige Feature ausgeschaltet. Setzen Sie einen Wert größer als 0, legen Sie damit fest, wie viele Jobs auf einmal abgeholt bzw. zugestellt werden sollen.
Die Abholungen erfolgen in der Trigger-Methode vor der nächsten anstehenden Bearbeitung, die Zustellungen erfolgen im Anschluss daran. In beiden Fällen werden Ereignisse (Feed und Deliver) der Klasse ausgelöst, entsprechend den jeweils in den beiden zugehörigen Eigenschaften eingestellten Werten. Das Feed-Ereignis erwartet, dass im Parameter Job eine neue Instanz der Job-Klasse zurückgegeben wird. Das Deliver-Ereignis nimmt eine Instanz (sofern vorhanden) vom Ready-Stapel und übergibt sie in Ihrem Job-Parameter nach draußen.
Public Event Deliver(Job As clsJob)
Public Event Feed(Job As clsJob)
Private pAutoDeliver As Long
Private pAutoFeed As Long
Public Property Get AutoDeliver() As Long
AutoDeliver = pAutoDeliver
End Property
Public Property Let AutoDeliver(New_AutoDeliver As Long)
pAutoDeliver = Abs(New_AutoDeliver)
End Property
Public Property Get AutoFeed() As Long
AutoFeed = pAutoFeed
End Property
Public Property Let AutoFeed(New_AutoFeed As Long)
pAutoFeed = Abs(New_AutoFeed)
End Property
Die Deklaration der Trigger-Methode ist gegenüber der bereits gezeigten Grundform ein wenig verändert. Sie ist jetzt als Funktion ausgelegt, der der Parameter StopTimer übergeben werden. Der Rückgabewert ist so lange True, wie noch zur Bearbeitung anstehende Jobs im ToDo-Stapel oder bereits erledigte, aber noch nicht abgeholte bzw. zugestellte Jobs im Ready-Stapel vorhanden sind. Somit können Sie anhand des Rückgabewerts entscheiden, ob der Timer abgeschaltet werden kann, so lange die Arbeiter-Klasse nichts mehr zu tun hat. Über den Parameter StopTimer können sie mit dem Wert True die Information übermitteln, dass kein weiterer Nachschub mehr zu erwarten ist. Wenn dann noch erledigte Jobs im Ready-Stapel sind, und AutoDeliver gesetzt ist, werden diese ausgeliefert, auch wenn die Anzahl niedriger ist, als der in AutoDeliver eingestellte Schwellwert.
Public Function Trigger(Optional ByVal StopTimer As Boolean) _
As Boolean
Dim nJob As clsJob
Dim l As Long
With mToDo
If pAutoFeed Then
If .Count = 0 Then
For l = 1 To pAutoFeed
Set nJob = Nothing
RaiseEvent Feed(nJob)
If nJob Is Nothing Then
Exit For
Else
.Add nJob
End If
Next 'l
End If
End If
If .Count Then
Set nJob = .Item(1)
.Remove 1
nJob.Execute
mReady.Add nJob
End If
If pAutoDeliver Then
With mReady
If (.Count >= pAutoDeliver) Or _
(StopTimer And (mToDo.Count = 0)) Then
Do While .Count
RaiseEvent Deliver(.Item(1))
.Remove 1
Loop
End If
End With
End If
Trigger = (.Count Or mReady.Count)
End With
End Function
Eine kleine Erweiterung stellt noch die Eigenschaft JobsPerTrigger dar. Sie legt fest, wie viele Jobs während eines Trigger-Aufrufs direkt hintereinander weggearbeitet werden sollen, falls etwa die Bearbeitungszeit jedes einzelnen Jobs ansehbar kürzer als die minimale Timer-Auflösung ist.
Public Property Get JobsPerTrigger() As Long
JobsPerTrigger = pJobsPerTrigger
End Property
Public Property Let JobsPerTrigger(New_JobsPerTrigger As Long)
Select Case JobsPerTrigger
Case Is >= 1
pJobsPerTrigger = New_JobsPerTrigger
Case Else
pJobsPerTrigger = 1
End Select
End Property
In der Trigger-Methode ändert sich dazu im Mittelteil zur Job-Bearbeitung nur wenig:
Dim nJobsPerTrigger As Long
' ...
nJobsPerTrigger = pJobsPerTrigger
Do While .Count
Set nJob = .Item(1)
.Remove 1
nJob.Execute
mReady.Add nJob
nJobsPerTrigger = nJobsPerTrigger - 1
Debug.Print nJobsPerTrigger
If nJobsPerTrigger = 0 Then
Exit Do
End If
Loop
Die letzte Verbesserung betrifft den "Motor" des Ganzen, den Timer. Die Bearbeitung des Timer-Ereignisses kann nämlich auch komplett in die Arbeiter-Klasse verlegt werden. Dazu wird in einer zusätzlichen Init-Methode der Timer an die Klasse übergeben und einer in der Klasse deklarierten Ereignisempfänger-Variablen ("WithEvents...") zugewiesen (nebenbei kann dieser Init-Methode noch zugleich ein Wert für JobsPerTrigger optional übergeben werden).
Private WithEvents eTimer As Timer
Public Sub Init(Timer As Timer, _
Optional ByVal JobsPerTrigger As Variant)
Set eTimer = Timer
If Not IsMissing(JobsPerTrigger) Then
pJobsPerTrigger = JobsPerTrigger
End If
End Sub
Private Sub Class_Terminate()
If Not (eTimer Is Nothing) Then
eTimer.Enabled = False
Set eTimer = Nothing
End If
End Sub
Für das Timer-Ereignis gibt es nun eine eigene Ereignisprozedur innerhalb der Klasse. Damit außerhalb der Klasse gegebenenfalls eine Zähler- oder Fortschrittsanzeige aktualisiert werden kann, wird hier nach dem internen Aufruf der Trigger-Methode das weitere Klassen-Ereignis AfterTrigger ausgelöst. Der Einfachheit halber werden von ihm die aktuellen "Füllhöhen" der Stapel mToDo und mReady als Parameter nach draußen gereicht. Da hier der Aufruf der Trigger-Methode intern erfolgt, entfällt hier auch die direkte Rückmeldemöglichkeit zur Timer-Abschaltung. Daher verfügt die AfterTrigger-Ereignis über den weiteren Parameter StopTimer. Er erfüllt den gleichen Zweck wie beim externen Aufruf der Trigger-Methode. Da das Ereignis jedoch erst nach dem Trigger-Aufruf ausgelöst wird, kommt der Wert gewissermaßen "zu spät", um noch die ansonsten noch innerhalb der Trigger-Methode mögliche letzte Auslieferungsaktion anzustoßen, wenn in StopTimer True zurückgegeben wurde. Die Auslieferung ist dazu in eine private Prozedur verlagert worden, die nun sowohl aus der Trigger-Methode als auch von hier aus aufgerufen werden kann. Anschließend wird erneut das AfterTrigger-Ereignis ausgelöst - in erster Linie, damit der nun letzte Stand außerhalb der Klasse bekannt wird. Wird nun hier StopTimer mit True zurückgegeben, und sind beide Stapel leer, wird der Timer abgeschaltet. Da die Trigger-Methode weiterhin öffentlich bleibt, können Sie auch grundsätzlich auf einen Timer verzichten und den Aufruf von beliebigen anderen Ereignissen abhängig machen und manuell aus Ihrem Code heraus vornehmen.
Private Sub eTimer_Timer()
Dim nStopTimer As Boolean
Me.Trigger
With mToDo
RaiseEvent AfterTrigger(.Count, mReady.Count, nStopTimer)
If nStopTimer Then
zAutoDeliver True
RaiseEvent AfterTrigger(.Count, mReady.Count, nStopTimer)
End If
eTimer.Enabled = (Not nStopTimer Or .Count Or mReady.Count)
End With
End Sub
Private Sub zAutoDeliver(ByVal StopTimer As Boolean)
If pAutoDeliver Then
With mReady
If (.Count >= pAutoDeliver) Or _
(StopTimer And (mToDo.Count = 0)) Then
Do While .Count
RaiseEvent Deliver(.Item(1))
.Remove 1
Loop
End If
End With
End If
End Sub
Als letzte Kleinigkeit bleibt noch die Eigenschaft AutoTrigger anzuführen, über die der interne Timer, sofern zugewiesen, ein- oder ausgeschaltet werden kann.
Public Property Get AutoTrigger() As Boolean
If Not (eTimer Is Nothing) Then
AutoTrigger = eTimer.Enabled
End If
End Property
Public Property Let AutoTrigger(New_AutoTrigger As Boolean)
If Not (eTimer Is Nothing) Then
eTimer.Enabled = New_AutoTrigger
End If
End Property
|