PDA

Vollständige Version anzeigen : ItemChange löst sich selbst aus


halweg
05.01.2018, 18:31
Ja, es ist ein Programmier-Klassiker:
Ich gebe Aufgaben per ItemAdd - Ereignis abhängig vom Betreff automatisch die richtige Kategorie (TaskItem.categories = "Privat" o.ä.) . Dieser Ereignisprozedur funktioniert so weit.

Nun will ich auch wenn sich die Aufgaben nur ändern (ItemChange-Ereignis) die Kategorie automatisch anpassen.
Dabei tritt das Problem auf, dass, sobald eine neue Kategorie in die Aufgabe geschrieben wird, dieses Ereignis ja erneut ausgelöst wird, es kommt also zu einer Endlosschleife.

Kennt ihr eine elegante Methode, dies zu verhindern. Idealerweise, ohne dass ich irgendein Zusatzfeld in der Aufgabe belegen muss.
Das Setzen einer Statusvariable scheint mir nicht optimal, da ja evtl. eine neue Instanz (mit eigenen Variablen) der Ereignisprozedur aufgerufen wird. Oder liege ich da falsch?
Aus verständlichen Gründen (Endlosschleife=Absturz) kann ich hier schlecht alle Varianten ausprobieren.

H_E_K
05.01.2018, 19:02
Ja, es ist ein Programmier-Klassiker:
Ich gebe Aufgaben per ItemAdd - Ereignis abhängig vom Betreff automatisch die richtige Kategorie (TaskItem.categories = "Privat" o.ä.) . Dieser Ereignisprozedur funktioniert so weit.

Nun will ich auch wenn sich die Aufgaben nur ändern (ItemChange-Ereignis) die Kategorie automatisch anpassen.
Dabei tritt das Problem auf, dass, sobald eine neue Kategorie in die Aufgabe geschrieben wird, dieses Ereignis ja erneut ausgelöst wird, es kommt also zu einer Endlosschleife.

Kennt ihr eine elegante Methode, dies zu verhindern. Idealerweise, ohne dass ich irgendein Zusatzfeld in der Aufgabe belegen muss.
Das Setzen einer Statusvariable scheint mir nicht optimal, da ja evtl. eine neue Instanz (mit eigenen Variablen) der Ereignisprozedur aufgerufen wird. Oder liege ich da falsch?
Aus verständlichen Gründen (Endlosschleife=Absturz) kann ich hier schlecht alle Varianten ausprobieren.
Ohne zumindest Teile des VBA-Codes wird das nichts werden. Also?

halweg
05.01.2018, 19:32
Danke für die schnelle Rückmeldung.
Na ja, ich suche eben eine generelle Lösung durch jemanden der öfter Ereignisprozeduren schreibt. Von daher gehe ich davon aus, dass Die Startroutinen für Ereignisprozeduren bekannt sind.
Aber bitte, es hilft vielleicht der Vorstellung, wenn ich eine (stark vereinfachte Version) reinstelle::
Private WithEvents Aufgaben As Outlook.Items

Sub Application_Startup()
Set Aufgaben = Session.GetDefaultFolder(olFolderTasks).Items
End Sub

Sub Aufgaben_ItemChange(ByVal Item As Object)
On Error Resume Next '
Aufgabe_Kategorie_nach_Betreff_zuordnen Item
End Sub

Sub Aufgabe_Kategorie_nach_Betreff_zuordnen(aufgabe As TaskItem)
aufgabe.Categories = "Privat"
aufgabe.save
End Sub
Klar, dass das finale .Save die Endlosschleife bringt, aber wie verhindere ich das am elegantesten?

H_E_K
05.01.2018, 21:24
Danke für die schnelle Rückmeldung.
Na ja, ich suche eben eine generelle Lösung durch jemanden der öfter Ereignisprozeduren schreibt. Von daher gehe ich davon aus, dass Die Startroutinen für Ereignisprozeduren bekannt sind.
Aber bitte, es hilft vielleicht der Vorstellung, wenn ich eine (stark vereinfachte Version) reinstelle::
Private WithEvents Aufgaben As Outlook.Items

Sub Application_Startup()
Set Aufgaben = Session.GetDefaultFolder(olFolderTasks).Items
End Sub

Sub Aufgaben_ItemChange(ByVal Item As Object)
On Error Resume Next '
Aufgabe_Kategorie_nach_Betreff_zuordnen Item
End Sub

Sub Aufgabe_Kategorie_nach_Betreff_zuordnen(aufgabe As TaskItem)
aufgabe.Categories = "Privat"
aufgabe.save
End Sub
Klar, dass das finale .Save die Endlosschleife bringt, aber wie verhindere ich das am elegantesten?
Also, mit Aufgaben habe ich mich noch nie beschäftigt. Aber folgende Auffälligkeit/Frage ergibt sich:
Mit dem
Aufgaben_ItemChange
schaffst du dir möglicherweise die Falle, weil
aufgabe.Categories = "Privat"
ja wohl das Change-Ereignis auslöst und dann erneut die zuletzt genannte Prozedur aufruft. Das sieht nach einer klassischen Endlosschleife aus.
Wenn das der Grund ist, dann würde ich (vielleicht nicht elegant, geht aber) mit einer boolschen Variablen arbeiten, die das ItemChange-Event nicht ausführt, wenn es von
Aufgabe_Kategorie_nach_Betreff_zuordnen
(ungewollt) ausgelöst wird. Nach der Änderung der Kategorie setzt du diese Variable wieder um, so dass das Change-Event wieder möglich ist.

halweg
06.01.2018, 11:52
Danke Hans, du hast meinen ersten Beitrag hier gut wiedergegeben, also das Problem verstanden.
Ich hatte allerdings eher auf konkrete Tipps zu der auch von mir angesprochene Variante mit den Variablen gehofft, um rund um das "Outlook hängt sich auf" Problem nicht zu viel probieren zu müssen...

H_E_K
06.01.2018, 12:01
Danke Hans, du hast meinen ersten Beitrag hier gut wiedergegeben, also das Problem verstanden.
Ich hatte allerdings eher auf konkrete Tipps zu der auch von mir angesprochene Variante mit den Variablen gehofft, um rund um das "Outlook hängt sich auf" Problem nicht zu viel probieren zu müssen...

Da ich nur teile deines Codes habe ist das schwierig.
Du deklarierst in einem Modul (Nicht im Workbook oder Sheet) eine Variable wie etwa
Public JetztNicht as Boolean
Dann setzt du in Aufgaben_ItemChange als erstes ein:
If JetztNicht = True then exit Sub
Und diese Prozedur änderst du wie folgt ab:

Sub Aufgabe_Kategorie_nach_Betreff_zuordnen(aufgabe As TaskItem)
JetztNicht = True 'ItemChange-Prozedur wird nicht ausgeführt
aufgabe.Categories = "Privat"
aufgabe.save
JetztNicht = False 'ItemChange-Prozedur ist wieder aktiv
End Sub
Probiere es mal so!

halweg
06.01.2018, 12:04
PS: Habe gerade etwas gefunden: Bei Excel gibt es ein
Application.EnableEvents = False
womit man temporär die Ereignisbehandlung ausschalten könnte.
Leider nicht bei Outlook.:(

H_E_K
06.01.2018, 12:07
PS: Habe gerade etwas gefunden: Bei Excel gibt es ein
Application.EnableEvents = False
womit man temporär die Ereignisbehandlung ausschalten könnte.
Leider nicht bei Outlook.:(

Korrekt, gibt es in OL nicht, war auch meine erste Idee. Hast du meinen Vorschlag mal ausprobiert? Der macht nämlich genau das.

halweg
06.01.2018, 12:47
Hab's jetzt mit der globalen Variable hinbekommen, es war allerdings nicht einfach, da meine
Aufgabe_Kategorie_nach_Betreff_zuordnen
- Prozedur beendet war, ehe der erneute Aufruf die globale Variable auswerten konnte. Ein Sleep von 300 ms löste dann das Problem...
Danke also noch mal für die Idee mit der globalen Variable.

H_E_K
06.01.2018, 12:48
Fein, freut mich.

EarlFred
08.01.2018, 05:56
Hallo halweg,

ändere die Kategorie nur dann, wenn sie nicht der gewünschten entspricht (simple Prüfung voranstellen).

Grüße
EarlFred

mumpel
08.01.2018, 06:06
Hallo!

BTW:
Es genügt If JetztNicht oder If Not JetztNicht. Das "=True" ist eine doppelte Prüfung, kann man also weglassen.

Gruß, René

H_E_K
08.01.2018, 08:48
Hallo!

BTW:
Es genügt If JetztNicht oder If Not JetztNicht. Das "=True" ist eine doppelte Prüfung, kann man also weglassen.

Gruß, René

Ja, René, das ist richtig. Allerdings erhöht es die Lesbarkeit, gerade für Neulinge, die hier auch etwas lernen können, und Platznot ist bei heutigen Rechnern und Festplatten ja auch weniger verbreitet. :)

mumpel
08.01.2018, 08:53
Es kann Prozeduren aber langsamer machen, was sich bei längeren Prozeduren mit mehreren Boolean-Variablem bemerkbar machen könnte.

H_E_K
08.01.2018, 08:55
Es kann Prozeduren aber langsamer machen, was sich bei längeren Prozeduren mit mehreren Boolean-Variablem bemerkbar machen könnte.

Werde ich bei Gelegenheit in einer Schleife mal testen, danke für den Hinweis!

H_E_K
08.01.2018, 09:34
Es kann Prozeduren aber langsamer machen, was sich bei längeren Prozeduren mit mehreren Boolean-Variablem bemerkbar machen könnte.

So, René, ich wollte es wissen, und es ist tatsächlich so. Hier der "Testaufbau":
Public Anfang, Ende As Date
Public laufzeit As Single
Sub Test1()
Dim IstWahr As Boolean, i, n As Long
Anfang = Time
n = 1000000000
For i = 1 To n
If IstWahr = True Then
IstWahr = False
Else
IstWahr = True
End If
Next i
Ende = Time
laufzeit = (Ende - Anfang) * 100000
Debug.Print "Test1 "; n; " Durchläufe dauern "; laufzeit; " Sec."
End Sub

Sub Test2()
Dim IstWahr As Boolean, i, n As Long
Anfang = Time
n = 1000000000
For i = 1 To n
If IstWahr Then
IstWahr = False
Else
IstWahr = True
End If
Next i
Ende = Time
laufzeit = (Ende - Anfang) * 100000
Debug.Print "Test2 "; n; " Durchläufe dauern "; laufzeit; " Sec."
End Sub

Und hier das Ergebnis:
Test1 1000000000 Durchläufe dauern 11,57407 Sec.
Test2 1000000000 Durchläufe dauern 10,41667 Sec.
Nun ist eine Sekunde Differenz bei einer Milliarde Durchläufen nicht gerade viel, aber es macht tatsächlich einen Unterschied.

mumpel
08.01.2018, 09:48
Es gibt Anwender die sich ins Hemd machen wenn der Rechner 12 Sekunden zum Hochfahren benötigt, und nicht 7 Sekunden wie beim Kollegen. Für solche Anwender ist eine Sekunde fünf Sekunden zuviel. ;)

markusxy
08.01.2018, 10:24
@H_E_K
Performance ergibt sich hauptsächlich aus der Anzahl der notwendigen Schritte. Daher sollte diese Variante viel schneller gehen:


For i = 1 To n
IstWahr = not IstWahr
Next i


Aber bezüglich des Themas halten ich deinen Vorschlag der Prüfung, für den einzig sinnvollen.
LG M

Edit:
Abgesehen davon sind Zeitunterschiede von 10% in der Praxis nicht relevant.
Bei mehreren Durchläufen, stellt man dann oft fest, dass sich das Ergebnis auch umkehren kann.

EarlFred
08.01.2018, 11:07
Hallo Hans,

ich beziehe mich mal auf Deinen Code aus Beitrag #16 und spare mir das Fullquote:

Bei meinem altersschwachen Rechner beträgt die Differenz deiner Testmakros immerhin ca. 6 Sekunden, was eine Steigerung der Dauer von immerhin fast 30% ausmacht.
Die Prüfung auf = True/False würde ich mir jedoch auch dann ersparen, wenn sie nur einmal vorkommt, weil es einfach unnötig ist. Ich finde den Code dann übrigens keineswegs besser lesbar, eher im Gegenteil. Aber das mögen Lesegewohnheiten sein.

Ein paar Anmerkungen zu Deinem Code:
- Typangaben werden nach jeder Variable benötigt, sonst ist der Typ Variant (solange diese Grundeinstellung nicht geändert wurde)
- Zeitmessungen bitte nicht mit Time, sondern mindestens mit Timer (Typ Single) - das reicht i. d. R. aus für solche einfachen Test. Wenn es genauer sein soll: API GetTickCount und getFrequency; Codes findest Du im Netz
- Willst Du die Differenz in Sekunden zwischen 2 Date-Variablen bzw. Rückgabewerten von Time messen, musst Du mit 86400 multiplizieren und nicht mit 100000.

Aber all das ist weit weg von exakter Wissenschaft - denn es gibt viel zu viele Störfaktoren (andere Programme, Dienste, Speicherverwaltung....) Das müsstest Du alles eliminieren.

Option Explicit

Sub Test1()
Dim IstWahr As Boolean

Dim A1 As Date, E1 As Date
Dim A2 As Single, E2 As Single
Dim A3 As Single, E3 As Single

Dim laufzeit1 As Single
Dim laufzeit2 As Single
Dim laufzeit3 As Single


Const n As Long = 1000000000
Dim i As Long

A1 = Time
A2 = Time
A3 = Timer


For i = 1 To n
If IstWahr = True Then
IstWahr = False
Else
IstWahr = True
End If
Next i


E1 = Time
E2 = Time
E3 = Timer


laufzeit1 = (E1 - A1) * 86400
laufzeit2 = (E2 - A2) * 86400
laufzeit3 = (E3 - A3)

Debug.Print "Time als Date: ", laufzeit1
Debug.Print "Time als Single:", laufzeit2
Debug.Print "Timer: ", , laufzeit3
End Sub

Grüße
EarlFred

H_E_K
08.01.2018, 11:08
@H_E_K
Performance ergibt sich hauptsächlich aus der Anzahl der notwendigen Schritte. Daher sollte diese Variante viel schneller gehen:


For i = 1 To n
IstWahr = not IstWahr
Next i


Schon klar, aber hier ging es jetzt ja gerade darum, Zeit zu verbrauchen, um die Unterschiede zu messen. 1 Milliarde Durchläufe!!! Wer braucht sowas schon??? :boah:

Aber gut, ich bin ja neugierig, also habe ich deine Variante auch getestet:
Sub Test3()
Dim IstWahr As Boolean, i, n As Long
Anfang = Time
n = 1000000000
For i = 1 To n
IstWahr = Not IstWahr
Next i
Ende = Time
laufzeit = (Ende - Anfang) * 100000
Debug.Print "Test3 "; n; " Durchläufe dauern "; laufzeit; " Sec."
End Sub

Messergebnis:
Test3 1000000000 Durchläufe dauern 6,944445 Sec.
War klar, weil ja die Zeitraubende If-Then-Abfrage entfiel.
Jetzt aber das überraschende mit dieser If-Then-Abfrage, die sicher nicht zeitraubender ist als in der ersten Variante:
Sub Test4()
Dim IstWahr As Boolean, i, n As Long
Anfang = Time
n = 1000000000
For i = 1 To n
If i Then
IstWahr = Not IstWahr
End If
Next i
Ende = Time
laufzeit = (Ende - Anfang) * 100000
Debug.Print "Test4 "; n; " Durchläufe dauern "; laufzeit; " Sec."
End Sub

Ergebnis:
Test4 1000000000 Durchläufe dauern 12,73148 Sec.

Also 1 Sekunde mehr als in Variante 1 und langsamer als alle anderen Varianten. Interessant, nicht?

EarlFred
08.01.2018, 11:11
@M
Aber bezüglich des Themas halten ich deinen Vorschlag der Prüfung, für den einzig sinnvollen.
welche Prüfung meinst Du?

Grüße
EarlFred

EarlFred
08.01.2018, 11:13
@Hans,

Interessant, nicht?
nicht wirklich.
Beim ersten Mal prüfst Du eine Boolsche Variable auf Wahr oder Falsch - dieser Typ enthält bereits die Wahrheitsinformation.

Das andere Mal prüfst Du eine Variable vom Typ Variant. Da diese "alles" enthalten kann, muss aus dieser Information erst einmal ein Wahrheitswert generiert werden... Bei Zahlen wird alles <> 0 zu "Wahr" - aber das muss ja zuerst berechnet werden.

Grüße
EarlFred

H_E_K
08.01.2018, 11:17
@Hans,


nicht wirklich.
Beim ersten Mal prüfst Du eine Boolsche Variable auf Wahr oder falsch, das andere mal eine vom Typ Variant - da muss erstmal ein Wahrheitswert generiert werden...

Grüße
EarlFred

Ich wollte mir die Antwort eigentlich ersparen, aber das kann ich nun doch nicht stehen lassen.
Alle Makros stehen im selben Modul untereinander. Die über den Makros deklarierten Variablen haben also in allen Makros Gültigkeit und behalten ihren Datentyp.

H_E_K
08.01.2018, 11:20
@M

welche Prüfung meinst Du?

Grüße
EarlFred
Beitrag #6 - guckst du ...

EarlFred
08.01.2018, 11:23
Hallo Hans,

Die über den Makros deklarierten Variablen haben also in allen Makros Gültigkeit und behalten ihren Datentyp.
gut, dass Du nachfragst, denn das meinte ich überhaupt nicht.

Option Explicit

Public Anfang, Ende As Date
Public laufzeit As Single

Sub sagmirwasichbin()
Debug.Print "Anfang", TypeName(Anfang)
Debug.Print "Ende", TypeName(Ende)
End Sub

Oder wirf mal einen Blick ins Lokalfenster.

Grüße
EarlFred

H_E_K
08.01.2018, 11:28
Hallo Hans,
gut, dass Du nachfragst, denn das meinte ich überhaupt nicht.


Ups, wieder was gelernt. Ich ging davon aus, dass die Deklaration
Public Anfang, Ende As Date
beide Variablen einschließt. Das ist aber nicht so:
Anfang Empty
Ende Date

Erst bei separater Deklaration
Public Anfang As Date
Public Ende As Date

werden beide als Date-Variable angelegt. Danke.

markusxy
08.01.2018, 11:31
Public Anfang As Date, Ende As Date

so passt es auch. Du musste nur bei jeder Variable den Typ anführen.

H_E_K
08.01.2018, 11:32
Ein paar Anmerkungen zu Deinem Code:
...
- Willst Du die Differenz in Sekunden zwischen 2 Date-Variablen bzw. Rückgabewerten von Time messen, musst Du mit 86400 multiplizieren und nicht mit 100000.
Das hätte ich auch gedacht. Überraschenderweise stimmt das aber nicht.
Bitte sehr:
Public Anfang As Date
Public Ende As Date
Public laufzeit As Single
Sub Test1()
Dim IstWahr As Boolean, i, n As Long
Anfang = Time
n = 1000000000
For i = 1 To n
If IstWahr = True Then
IstWahr = False
Else
IstWahr = True
End If
Next i
Ende = Time
laufzeit = (Ende - Anfang) * 100000
Debug.Print "Test1 "; n; " Durchläufe dauern "; laufzeit; " Sec."
End Sub
Sub Test1a()
Dim IstWahr As Boolean, i, n As Long
Anfang = Time
n = 1000000000
For i = 1 To n
If IstWahr = True Then
IstWahr = False
Else
IstWahr = True
End If
Next i
Ende = Time
laufzeit = Ende - Anfang
Debug.Print "Test1a "; n; " Durchläufe dauern "; laufzeit; " Sec."
End Sub

Ergebnis im Direktfenster:
Test1 1000000000 Durchläufe dauern 11,57407 Sec.
Test1a 1000000000 Durchläufe dauern 1,157407E-04 Sec.

markusxy
08.01.2018, 12:34
@H_E_K,
Laufzeit = (Ende - Anfang) * 86400

Ich hoffe du weißt was E-04 bedeutet.
Die angezeigten Werte haben mit der Realität also nur wenig gemeinsam.
Ich muss gestehen, dass ich mir den Code gar nicht so genau angesehen habe, da ich davon ausgegangen bin, dass du weißt was du tust.
Schau dir also nochmal genau an was EarlFred dir mitteilt.

Ein Beispiel was nach deiner Methode aus 33 Sekunden wird:
?cdate("00:00:33")*100000
38,1944444444445

richtig:
?cdate("00:00:33")*86400
33
LG Markus

halweg
08.01.2018, 12:37
Btt: Hier mal meine bisherige Endfassung des ItemChange - Programms:

Public Aufgabenaenderung_nicht_verarbeiten As Boolean

Sub Aufgaben_ItemChange(ByVal geaenderte_Aufgabe As Object)
Dim alte_kategorie As String
On Error Resume Next '
If Not Aufgabenaenderung_nicht_verarbeiten Then
Aufgabenaenderung_nicht_verarbeiten = True
alte_kategorie = geaenderte_Aufgabe.Categories
Aufgabe_Kategorie_nach_Betreff_zuordnen geaenderte_Aufgabe
If geaenderte_Aufgabe.Categories <> alte_kategorie Then geaenderte_Aufgabe.Save: Sound "autoaktiv": Sleep 0.3
Aufgabenaenderung_nicht_verarbeiten = False
End If
End Sub

Sub Aufgabe_Kategorie_nach_Betreff_zuordnen(Aufgabe_oder_Termin As Object)
Aufgabe_oder_Termin.Categories = Switch( _
Aufgabe_oder_Termin.Subject Like "[HGMF] *", "Privat", _
Aufgabe_oder_Termin.Subject Like "*M *", "Einkauf", _
...)
End Sub

Funktioniert gut!
Das Ganze ist nur ein kleiner Teil meiner Ereignismakros, mit der Behandlung des Change-Problems bin ich da aber ein gutes Stück weitergekommen.:)

H_E_K
08.01.2018, 12:59
Jetzt ist mir auch klar, wofür du die Sleep-Anweisung brauchst. Das wird mit deiner Unterprozedur 'Sound' zusammenhängen. Die würde mich unabhängig von deinem Problem mal interessieren, weil ich da für ein größeres Excel-VBA-Projekt dran zu knacken hatte, es aber schließlich sehr gut gelöst habe - ohne Timing-Probleme.

EarlFred
08.01.2018, 13:37
@halweg,
mit der Variante aus Beitrag #11 sparst Du Dir eine Menge Sorgen, vor allem auch die globale Variable...

Grüße
EarlFred

markusxy
08.01.2018, 14:32
@M

welche Prüfung meinst Du?

Grüße
EarlFred

Sorry, das war ursprünglich dein Vorschlag, aus #11 den ich meinte.

EarlFred
08.01.2018, 14:37
Hallo Markus,

kein Anlass für ein "Sorry". Danke für die Aufklärung!

Ich dachte, ich hätte eine Lösung übersehen.

Grüße
EarlFred

halweg
08.01.2018, 14:38
Jetzt ist mir auch klar, wofür du die Sleep-Anweisung brauchst. Das wird mit deiner Unterprozedur 'Sound' zusammenhängen. Die würde mich unabhängig von deinem Problem mal interessieren, weil ich da für ein größeres Excel-VBA-Projekt dran zu knacken hatte, es aber schließlich sehr gut gelöst habe - ohne Timing-Probleme.

Sound macht nichts anders, als über den Shell Befehl ein selbstgebautes AHK-Skript zu starten, welches einen Tooltip und einen vordefinierten Beep-Sound ausgibt. Ich war eigentlich der Meinung, dass ich mir wegen dieses Aufrufs das Sleep sparen kann. Das .save sollte also das Change-Ereignis triggern, solange die Variable Aufgabenaenderung_nicht_verarbeiten noch nicht zurück gesetzt ist. Das war nicht der Fall, deswegen das Sleep von 300ms nach dem .save.

@halweg,
mit der Variante aus Beitrag #11 sparst Du Dir eine Menge Sorgen, vor allem auch die globale Variable...
Danke EarlFred. Das "Speichern nur bei Änderung" wird auch jetzt schon gemacht, wie du an meinem letzten Makro sehen kannst (war immer so geplant).
Dein Vorschlag liefe dann darauf hinaus, dass zulasse, dass auch die geänderte und gespeicherte Aufgabe ein zweites Mal durch die ItemChange Prozedur gejagt wird und dann aber, da sich kein neuer Änderungsbedarf mehr ergibt, zu keinem dritten Change Ereignis führt.

Muss ich kurz durchdenken, ob das "beim zweiten Mal nicht ändern" bei allen derartigen Routinen, die ich habe und plane, gewährleistet sein wir. es könnte ja auch iterative Routinen geben.
Im Moment würde ich eine eindeutige "bitte jetzt nicht auf Änderungen reagieren"-Lösung bevorzugen.

EarlFred
08.01.2018, 14:43
Hallo halweg,

Das "Speichern nur bei Änderung" wird auch jetzt schon gemacht, wie du an meinem letzten Makro sehen kannst.
ja, das hat aber noch keine Konsequenzen. Du verlässt Dich weiterhin auf Deine globale Variable, die schlicht ein Kropf ist und Fehlerpotential birgt (was Du zumindest dadurch verringerst, dass die Variable beim Fehlerrückfall auf FALSE gesetzt wird und Ereignisse damit wieder bearbeitet werden).

Auch bei Deiner Variante läuft das Change-Ereignis zweifach - führt beim zweiten Durchlauf aber zu keiner weiteren Bearbeitung. Das tut mein Ansatz genauso.

Grüße
EarlFred

halweg
08.01.2018, 17:20
Wie gesagt, ich suchte eine generelle Lösung für das Handling der Change-Ereignisse. Da hängen ja zig Routinen dran und ich kann noch nicht ausschließen, dass da auch mal eine Mehrfachänderung passiert.

EarlFred
08.01.2018, 17:36
Hallo halweg,

generelle Lösungen sind ein hohes Ziel. Bei der ein oder anderen Situation mag die Verwendung einer globalen Variablen auch sinnvoll oder gar die einzig brauchbare Variante sein.

Hier in diesem konkreten Beispiel (so wie Du die Codes gepostet hast), würde ich sie aber eben nicht anwenden.

Grüße
EarlFred

Andre.Heisig
09.01.2018, 12:56
Darf ich mich hier mal mit einhängen, weil ich ein ähnliches Problem habe, und bisher keine Lösung wirklich gute gefunden hab: Ich syncronisiere Termine zwischen Access und Outlook:

In Outlook reagiere ich ebenfalls auf ItemChange, wenn bestimmte Termine geändert werden (und auch da hab ich das "Nebenproblem", dass ItemChange grundsätzlich 2x auslöst), und übergebe geänderte Daten an Access; aus Access heraus suche ich die zum Datensatz gehörenden Outlook-Termine und update in diese Richtung. Bis dahin alles soweit fehlerfrei.

Jetzt löst aber jedes Termin-Update aus Access zwangsläufig auch das ItemChange in Outlook aus, und ich generiere die eingangs erwähnte Schleife ebenfalls. Habt ihr hierzu eine Idee?

halweg
09.01.2018, 14:28
Du solltest, wen du dich hier reinhängst, vielleicht erst mal die hier besprochenen Lösungsansätze lesen und erläutern, warum die nicht funktionieren.
Und, auch das wurde mir am Anfang klar gemacht, hilft ein Stück Code allen Beteiligten, sich dein Problem besser vorzustellen.

Andre.Heisig
09.01.2018, 15:02
Aber selbstredend, auch wenn ich glaub(t)e, dass das nicht hilft:


Kurzfassung Outlook:
Option Explicit

Public WithEvents myOlItems As Outlook.items
Public WithEvents olTermine As Outlook.items

Public Sub Application_Startup()
Set olTermine = Application.Session.GetDefaultFolder(olFolderCalendar).items
End Sub


Private Sub olTermine_ItemChange(ByVal Item As Object)

If Item.Class = olAppointment Then


' Termin in Access aktualisieren ...
Dim strSQL As String
Dim accApp As Object ' Access.Application
Dim accDB As DAO.Database

strSQL = "UPDATE [tbl_AUFTRAGSDETAILS] SET " & _
"[AUFDET_Datum]='" & Format(olItem.Start, "dd.mm.yyyy") & "', " & _
"[AUFDET_UhrzeitBeginn]='" & Format(olItem.Start, "hh:mm") & "', " & _
"[AUFDET_Einheiten]='" & olItem.Duration / 60 & "', " & _
"[AUFDET_SatzUpdate]=#" & fct_DatumZeitSQL(Now()) & "# " & _
"WHERE " & _
"[AUFDET_AuftragsdetailID_PK]=" & olItem.BillingInformation & " AND " & _
"[AUFDET_Abgerechnet]=False AND " & _
"[AUFDET_SatzUpdate]<#" & fct_DatumZeitSQL(olItem.LastModificationTime) & "#;"

Set accDB = DAO.OpenDatabase(const_strAuftragsVerwaltung_DB)
accDB.Execute strSQL, dbFailOnError

DoEvents
strSQL = ""
accDB.Close
Set accDB = Nothing


End If
' =========>|
End Sub

Der Rückweg, Access zu Outlook, ist der Standard-Weg Outlook-Automatiserung, zuviel Code zum Posten, und m.E. tatsächlich irrelevant, weil beide Wege an sich funktionieren.

Ich hatte lediglich auf Ideen gehofft, wie aus der ItemChange-Schleife auszubrechen ist, wenn ich per Access-Änderung einen Termin aktualisiere, der dann seinerseits ein in dem Moment ungewolltes ItemChange antriggert, das dann wiederum den Termin zurück an Access übergibt ...

halweg
09.01.2018, 15:29
Auch in deinem Fall müsstest du irgendwie den Ereignisstatus festhalten.

Also könnte Access evtl. einer Variablen oder einem Item oder einem Ordnernamen in Outlook einen Signalwert zukommen lassen, der sagt "Ich schreibe gerade, bitte im Moment nicht auf "ItemChange" reagieren.
Alternativ, und hier fehlt die Beschreibung des Access-Teils, könnte es sein, dass, wenn Access schreibt, das daran erkennbar ist, dass eben kein Outlook-Fenster im Vordergrund ist, das ließe sich im Outlook-Code abfragen.

Andre.Heisig
10.01.2018, 09:38
Moin!

Also könnte Access evtl. einer Variablen oder einem Item in Outlook einen Signalwert zukommen lassen

Das klingt brauchbar. Hast du einen Codeschnipsel, den ich weiterverfolgen könnte? Sonst wühl ich mich durchs Web.

Danke in jedem Fall.

H_E_K
10.01.2018, 09:41
Moin!
Das klingt brauchbar. Hast du einen Codeschnipsel, den ich weiterverfolgen könnte? Sonst wühl ich mich durchs Web.


Wühl dich doch mal durch diesen Thread! :)
Zum Beispiel Beitrag #6.

halweg
10.01.2018, 09:58
Das klingt brauchbar. Hast du einen Codeschnipsel, den ich weiterverfolgen könnte? Sonst wühl ich mich durchs Web.
Leider habe ich keine Erfahrung, wie man Outlook aus Access heraus bedient, insbesondere, wie persistente Outlook-Variablen geschrieben werden. Aber das Umbenennen von Ordnern oder das Schreiben eines Notiz-Items sollte dir ja geläufig sein, da du ja schon Outlook "fernbedienst".

Andre.Heisig
10.01.2018, 10:07
Moin auch dir,

#6 hab ich gesehen und die Logik auch verstanden, aber ich müsste die Outlook-Variable ja aus Access heraus zumindest wertmäßig umsetzen. #6 handelt das ja Outlook-Intern ab, oder hab ich was übersehen?

H_E_K
10.01.2018, 11:10
Moin auch dir,

#6 hab ich gesehen und die Logik auch verstanden, aber ich müsste die Outlook-Variable ja aus Access heraus zumindest wertmäßig umsetzen. #6 handelt das ja Outlook-Intern ab, oder hab ich was übersehen?

Jein. Das ist schwer zu beantworten, ohne zu wissen
wie der Access-VBA-Code aussieht
welche anderen Ereignisse auf das Item_Change-Event einwirken.

Aber du musst doch in Access eine Outlook-Instanz geöffnet haben, um überhaupt auf OL zugreifen zu können. Kannst du da nicht (wie gesagt, wir kennen den Code nicht) eine ungenutzte Eigenschaft des Appointment-Items so besetzen, dass OL erkennt, dass der Aufruf von Access kommt?
Es bietet sich zum Beispiel die mehr oder weniger unbekannte und wohl kaum genutzte .milage-Eigenschaft (https://msdn.microsoft.com/de-de/vba/outlook-vba/articles/appointmentitem-mileage-property-outlook) an, die du mit einem beliebigen Text besetzen kannst.
Zum Beispiel so, wenn (in Access) das Appointment-Item objItem heißen würde:
objItem.Mileage = "Access"
Diese Eigenschaft kannst du dann in OL auslesen

If objItem.Mileage = "Access" Then
JetztNicht = True
objItem.Mileage = ""
Exit Sub
End If
und somit die boolsche Variable in Abhängigkeit von der Herkunft setzen. Danach löscht du die Milage-Eigenschaft gleich wieder. Die Item_Change Routine wird wieder verlassen, denn das Change-Ereignis wird ja ohnehin noch einmal ausgelöst, da das Appointment-Item noch nicht erstellt bzw. geändert wurde.
Die Item_Change Routine muss nun am Anfang die Abfrage beinhalten:
If JetztNicht Then Exit Sub
Und am Ende nach Durchlaufen von Item_Change:
JetztNicht = False
Ob du jetzt beim Erkennen der Access-Herkunft die Change-Routine wie im Beispiel verlässt, oder eine bestimmte andere Prozedur aufrufst, das ist dir überlassen.

Ich kann das alles nur begrenzt testen, da ich nicht mit Access arbeite. Aber teste es mal, es könnte gehen.

Andre.Heisig
10.01.2018, 12:36
Das klingt alles nach nem soliden Plan, und die .mileage-Eigenschaft nutz ich tatsächlich bis dato nicht. Ich setz das so um und melde hier zurück!

Danke an alle fürs Mitdenken!:)

EarlFred
10.01.2018, 15:04
Hallo Andre,

mir ist allerdings nicht klar, warum das ItemChange-Ereignis beim gezeigten Code doppelt anspringt. Ich kann das auch bei mir nicht nachstellen - eine Änderung führt zum einmaligen Durchlauf des Ereignisses - und Änderungen an dem ereignisauslösenden Item nimmst Du ja gar nicht vor.

Ich vermute die Ursache irgendwo anders. Vielleicht ist die Lösung da ganz leicht?

Grüße
EarlFred

H_E_K
10.01.2018, 15:12
Hallo Andre,

mir ist allerdings nicht klar, warum das ItemChange-Ereignis beim gezeigten Code doppelt anspringt. Ich kann das auch bei mir nicht nachstellen - eine Änderung führt zum einmaligen Durchlauf des Ereignisses - und Änderungen an dem ereignisauslösenden Item nimmst Du ja gar nicht vor.

Ich vermute die Ursache irgendwo anders. Vielleicht ist die Lösung da ganz leicht?

Grüße
EarlFred
Ja, doch, das Change-Ereignis könnte sogar noch öfter ausgelöst werden. Wenn ich nicht irgendwo einen Knoten im Gedankenstrang habe, dann ist das so:
In Access(!) eröffnet er eine Outlook-Instanz. Dann erschafft er ein Appointment-Item (oder öffnet ein vorhandenes). Nun erst kann er Eigenschaften des Items setzen, und mit jeder zugefügten Eigenschaft löst er in OL das ItemChange-Ereignis aus.
Da Access selbst ja keine Appointment-Items kennt, kann er die auch nicht in Access "vorbereiten" und dann senden.

Andre.Heisig
10.01.2018, 15:27
Hallo Andre,

mir ist allerdings nicht klar, warum das ItemChange-Ereignis beim gezeigten Code doppelt anspringt. Ich kann das auch bei mir nicht nachstellen - eine Änderung führt zum einmaligen Durchlauf des Ereignisses

Das fast immer (ich bin mir bewusst, dass das merkwürdig klingt, die Beobachtung ist aber so) doppelte ItemChange merke ich daran, dass ich in der kompletten Codestruktur noch eine "Wollen Sie den Termin aktualisieren"-Abfrage vorgeschaltet habe. Alternativ eine debug.print Zeile. Wenn ich einen meiner "relevanten" Termine im OL-Kalender verschiebe oder durch Eingaben Daten ändere, triggert das ItemChange bzw. die daran hängende Rückfrage i.d.R. doppelt, die zweite Rückfrage um ein paar Sekunden zeitversetzt - ich kann mir auch nicht erklären warum, bin aber laut Netzrecherche auch nicht ganz alleine mit dem Thema.

Insofern ist das ein reines Outlook Ding, das hat mit der Access-Anbindung nichts zu tun!

Edit: Eben nochmal getestet: Lasst das Access Thema hier mal komplett außen vor: Ich nehme einen Termin und verschiebe den in Outlook um eine Stunde o.ä., oder öffne ihn und ändere irgendwas. Meine ItemChange-Reaktion ploppt zweimal auf. An der Stelle ist die Interaktion mit Access noch gar nicht erfolgt.

H_E_K
10.01.2018, 15:32
Insofern ist das ein reines Outlook Ding, das hat mit der Access-Anbindung nichts zu tun!
Hängt an (in) Outlook vielleicht noch eine Synchronisierungssoftware? Oder eine Regel?
Aber das alles kann dir egal sein, wenn es mit dem geschilderten Trick klappen sollte ...

halweg
10.01.2018, 15:43
Doppeltes Change ohne die Codezeilen zur Access-Anbindung? Ohne dass bei ItemChange etwas geändert wird?
Da würde ich glatt ein AddOn oder ähnliches vermuten. War bei mir jedenfalls einmal so. Vielleicht gibt es auch wie Hans vermutet eine Sync-Software, z. B. einen Sync auf Exchange Ebene. Oder der Alarm wird manuell nachsynchronisiert, so macht es z. B. meine Smartphone-Software.

Interessant wäre ggf. der zeitliche Abstand zwischen den zwei Änderungsereignissen. Oder sogar ein per Code vorgenommener Vergleich des AppointmentItems nach dem ersten und dem zweiten Ereignis.

Andre.Heisig
10.01.2018, 16:00
Hängt an (in) Outlook vielleicht noch eine Synchronisierungssoftware? Oder eine Regel?
Aber das alles kann dir egal sein, wenn es mit dem geschilderten Trick klappen sollte ...

Addons kann ich ausschließen, das Phänomen ist auf 3 Rechnern zu beobachten, allesamt Office 365, aktuelle Version 2016, ohne Drittsoftware. Regeln nur den Maileingang betreffend.

"Egal"? hm ... es ist nicht dramatisch. Ich muss meine Bestätigungs-Rückfrage halt immer einmal wegklicken, allerdings ist die Anzahl meiner Verarbeitungen auf dem Weg tatsächlich überschaubar. Das ist mehr ein Fall von "wüsste gern warum".


Doppeltes Change ohne die Codezeilen zur Access-Anbindung? Ohne dass bei ItemChange etwas geändert wird?
Da würde ich glatt ein AddOn oder ähnliches vermuten. War bei mir jedenfalls einmal so. Vielleicht gibt es auch wie Hans vermutet eine Sync-Software, z. B. einen Sync auf Exchange Ebene. Oder der Alarm wird manuell nachsynchronisiert, so macht es z. B. meine Smartphone-Software.

Interessant wäre ggf. der zeitliche Abstand zwischen den zwei Änderungsereignissen. Oder sogar ein per Code vorgenommener Vergleich des AppointmentItems nach dem ersten und dem zweiten Ereignis.

Wie gesagt, das ist Outlook "ohne". Das Stichwort Exchange könnte aber zielführend sein, es ist ein Exchange Postfach. Du meinst, das Speichern im lokalen Outlook und das Syncen zum Exchange triggert jeweils eine Änderung an? Das könnte es erklären.

Den Zeitabstand zwischen den beiden Reaktionen müsste ich messen. Da ich standardmäßig eine Rückfrage ausführe, verzieht das Zeitfenster ja alleine durch das "bestätigen müssen" etwas, ich könnte aber auch ein debug.print mit Zeitstempel testen.

halweg
10.01.2018, 16:05
Evtl. könntest du testen, ob ohne Netzwerkanbindung auch ein doppeltes Change kommt.

H_E_K
10.01.2018, 16:11
Das Stichwort Exchange könnte aber zielführend sein, es ist ein Exchange Postfach. Du meinst, das Speichern im lokalen Outlook und das Syncen zum Exchange triggert jeweils eine Änderung an? Das könnte es erklären.

Wir reden ja hier vom Item_Change-Ereignis beim Kalender. Eigentlich ist da Exchange erst einmal nicht beteiligt, das Anlegen (oder Verändern) am Client, deinem Rechner löst das Event erstmalig aus. Wenn aber nun Exchange daraufhin so etwas wie eine "Gebucht"-Eigenschaft zurück liefert, dann wäre das die Erklärung für den zweiten Aufruf.
Etwas aufwändig, aber zielführend: Hänge in den Item_Change-Code einen Sub-Aufruf, der per debug.print nacheinander sämtliche Eigenschaftem des Items auflistet. Da müsste es ja dann einen Unterschied zwischem dem ersten und dem zweiten Durchlauf geben. :cool:

Andre.Heisig
10.01.2018, 16:11
Mach ich bei Gelegenheit. Dabei fällt mir Folgendes auf: Wenn ich an meinem Laptop Terminänderungen ausführe, und mit später am PC an das gleiche Outlook-Postfach setze, kommt immer mal wieder direkt nach dem Start von Outlook ein ganzer Schwung meiner Änderungs-Rückfragen (Ergänzung: ", die ich sehr sicher schon mal bestätigt habe")

Das spricht ja für die Exchange-Sync-These.

Kann man programmatorisch feststellen, ob eine Änderung vom Benutzer oder von Exchange kommt, oder ist das zuviel des Guten? :grins:

H_E_K
10.01.2018, 16:12
Evtl. könntest du testen, ob ohne Netzwerkanbindung auch ein doppeltes Change kommt.

Hatte ich mir auch überlegt, aber wenn ich mich recht erinnere, läuft dann OL gar nicht.

H_E_K
10.01.2018, 16:13
Kann man programmatorisch feststellen, ob eine Änderung vom Benutzer oder von Exchange kommt, oder ist das zuviel des Guten? :grins:

Geht mein zeitgleicher Beitrag #56 in die Richtung?

Andre.Heisig
10.01.2018, 16:35
Ich teste das mal aus, evtl findet sich eine Eigenschaft.

EarlFred
10.01.2018, 18:51
Hallo Hans,

mit jeder zugefügten Eigenschaft löst er in OL das ItemChange-Ereignis aus
nein, das kann ich nicht bestätigen. Bei mir feuert das Ereignis genau einmal beim Save.

Grüße
EarlFred

P.S.: Bis Du so nett und zitierst mich nicht immer voll, sondern nur die Teile, auf die Du Dich auch tatsächlich beziehst? Neben der Störung des Leseflusses irritiert mich das, wenn ich meine Grüße an Dich an mich als Echo lese. Danke!

halweg
17.01.2018, 17:58
Hallo noch mal,
ich melde mich, weil ich eine "saubere" Lösung für das eingangs genannte Problem gefunden habe. Ohne extra Variable oder sonst was.

Die Sache funktioniert, indem ich einfach die Event-Variable zwischenzeitlich deaktiviere, alsoPrivate WithEvents Aufgaben_beobachtet As Outlook.Items

Sub Application_Startup()
Set Aufgaben_beobachtet = Outlook.Application.GetNamespace("MAPI").GetDefaultFolder(olFolderTasks).Items
Sound "autostart"
End Sub

Sub Aufgaben_beobachtet_ItemChange(ByVal geaenderte_Aufgabe As Object)
On Error Resume Next
Set Aufgaben_beobachtet = Nothing
...
gaenderte_Aufgabe.Save: Sound "autoaktiv"
Set Aufgaben_beobachtet = Outlook.Application.GetNamespace("MAPI").GetDefaultFolder(olFolderTasks).Items
End SubDas Ganze funktioniert, ich konnte sogar auf das Sleep verzichten. :cool:
Dane nochmal allen fürs Mitdenken. Zum Glück standen wir alle gemeinsam auf dem Schlauch, sonst würde ich jetzt an meiner Kreativität zweifeln. ;)

@Andre: Da ich gerade hier bin: Du weißt nicht zufällig, ob und wie man aus einer Fremdapplikation heraus (z. B. Access) eine VBA-Outlook-Routine, welche sich in Outlook befindet, aufrufen kann? Ich muss öfter Outlook über Parameter steuern (z. B. Aufruf <objekttyp> <Ansicht> <Filter>) und will dazu der entsprechenden Outlook Prozedur Parameter übergeben. Momentan läuft das über VBScript aber der Start einer Outlook-internen Routine würde hier viel vereinfachen.

Andre.Heisig
17.01.2018, 18:22
@Andre: Da ich gerade hier bin: Du weißt nicht zufällig, ob und wie man aus einer Fremdapplikation heraus (z. B. Access) eine VBA-Outlook-Routine, welche sich in Outlook befindet, aufrufen kann? Ich muss öfter Outlook über Parameter steuern (z. B. Aufruf <objekttyp> <Ansicht> <Filter>) und will dazu der entsprechenden Outlook Prozedur Parameter übergeben. Momentan läuft das über VBScript aber der Start einer Outlook-internen Routine würde hier viel vereinfachen.

Kann ich leider nicht weiter helfen, weil noch nie versucht, sorry!

halweg
17.01.2018, 18:33
Kann ich leider nicht weiter helfen, weil noch nie versucht, sorry!Ok, danke trotzdem.

Ich bin inzwischen soweit, dass ich eine Ereignisroutine geschrieben habe, die auf _FolderAdd im Entwurfsordner reagiert. Sie liest dessen Namen aus und löscht ihn sofort wieder. Der Name wird nach einem Split als Parameter an die jeweils zuständige Prozedur übergeben.

Es ist ein Workaround, aber bisher funktioniert es, denn Ordner kann man aus der Ferne gut hinzufügen.

H_E_K
17.01.2018, 18:46
Es ist ein Workaround, aber bisher funktioniert es, denn Ordner kann man aus der Ferne gut hinzufügen.
Du kannst "aus der Ferne" alles hinzufügen, auch Tasks, Appointment-Items, oder Emails. Du musst nur (aus einer Office-Anwendung wie Excel, Access, Word) eine Outlook-Instanz eröffnen.
Mal ein Beispiel, mit dem ich aus Excel einen Newsletter versendet hatte (Excel 2003):
Sub test_mail()
'*********************** KLAPPT !!! ********************************
'*******************************************************************

Dim Text As String
Dim Betreff As String
Dim Entwurf As String
Dim Nummer As String
Dim myOLApp As New Outlook.Application
Dim Neuemail As Outlook.MailItem

Text = ""
' Nummer = InputBox("Nummer des Newsletters ?")
' Entwurf = "Neuigkeiten von der Reitpferd-Boerse -" & Nummer & "-"


Set myOLApp = CreateObject("Outlook.Application")
Set myNamespace = myOLApp.GetNamespace("MAPI")
Set myfolder = myNamespace.GetDefaultFolder(olFolderOutbox)
Set mynewfolder = myfolder.Parent
Set mynewfolder = mynewfolder.Folders("Newsletterentwürfe")
Set myItem = mynewfolder.Items(1)
Text = myItem.Body
Betreff = myItem.Subject

Debug.Print (Betreff & Chr$(13) & Text)

Set Adressbuch = Application.Session.AddressLists("Testkontakte")
Set Adresseintraege = Adressbuch.AddressEntries
For j = 1 To Adresseintraege.Count
Set Neuemail = myOLApp.CreateItem(olMailItem)


Set Mailempfaenger = Neuemail.Recipients.Add(Adressbuch.AddressEntries(j).Address)
Mailempfaenger.Resolve

With Neuemail
.Subject = Betreff
.Body = Text
.Send
End With

Next j
End Sub

halweg
17.01.2018, 18:53
Nein, klar Hans. Man kann Outlook gut fernsteuern und das hatten wir hier ja reichlich diskutiert. Mit "gut hinzufügen" meinte ich, dass ich hier einen Weg sehe, Outlook etwas mitzuteilen, ohne dass ein Outlook Element (also Primärdaten) davon betroffen werden.

Aber ok, dein Beispiel macht vielleicht anderen Mut, das applikationsübergreifende Programmieren auch mal in Angriff zu nehmen.
Letztlich sehe ich in dem ganzen VBA Zeugs den entscheidenden Vorteil von MS Office.

EarlFred
18.01.2018, 10:12
Hallo halweg,

der Charme deiner "Brutalo"-Methode aus Beitrag #62 ist ganz klar, dass erst gar keine Events anspringen, da der Pfad gekappt ist, aber am Ende der Prozedur wiederhergestellt wird. Du bist also unabhängig "von außen".

Dennoch ein weiterer Weg ohne globale Variable:
Private Sub oTemp_ItemChange(ByVal Item As Object)
Static blnDontDoAnything As Boolean

If blnDontDoAnything Then
blnDontDoAnything = False
Else
'Code, um Item zu ändern

blnDontDoAnything = True
End If

End Sub

Taugt bei mir aber auch nichts, da Exchange beim Synchronisieren wieder das Ereignis auslöst.

Grüße
EarlFred