Einfrieren der GUI bei Zugriff auf Outlooks "öffentliche Ordner" verhindern

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 3 Antworten in diesem Thema. Der letzte Beitrag () ist von Schievel.

    Einfrieren der GUI bei Zugriff auf Outlooks "öffentliche Ordner" verhindern

    Hallo,

    ich Programmiere gerade ein Outlook VSOT Addin, das Mails anhand ihres Betreffs in verschiedene öffentliche Ordner in Outlook einordnet.
    Da die öffentlichen Ordner nicht besonders strukturiert sind, mache ich eine rekursive Suche nach dem passenden Ordner:

    VB.NET-Quellcode

    1. Private Function searchForFolderByName(ByVal startFolder As Object, searchedFolderName As String, recursive As Boolean) As Folder
    2. ' sucht in einem uebergebenen Startfolder nach einem Unterordner mit dem uebergebenen Namen. Bei recursive = true werden auch Unterordner bis in alle Ebenen mitdurchsucht
    3. ' sucht NICHT case sensitive
    4. Dim Folder As Folder
    5. For Each Folder In startFolder.Folders
    6. If InStr(LCase(Folder.Name), LCase(searchedFolderName)) Then
    7. searchForFolderByName = Folder
    8. Exit Function
    9. Exit For
    10. End If
    11. If recursive Then
    12. searchForFolderByName = searchForFolderByName(Folder, searchedFolderName, True)
    13. If Not searchForFolderByName Is Nothing Then
    14. If InStr(LCase(searchForFolderByName.Name), LCase(searchedFolderName)) Then
    15. Exit Function
    16. Exit For
    17. End If
    18. End If
    19. End If
    20. Next
    21. Return Nothing
    22. End Function


    Nun friert dadurch natürlich erstmal die Outlook-GUI ein. Die öffentlichen Ordner sind je nach Netzwerkverbindung ziemlich lahm. Auch, wenn ich im Outlook auf einen klicke, dauert es ca. 3s, bis dieser Ordner aufgeht. Die gesamte Suche dauert bei einem Ordner der ziemlich tief in der Struktur liegt und entsprechend spät gefunden wird, etwa 90s. Das ist halt inakzeptabel die GUI für 90s einzufrieren.
    Ich habe nun versucht den ganzen Archiviervorgang in einem eigenen Thread auszufuehren, indem ich da, wo der Vorgang gestartet wird (beim Klick auf einen Button im Ribbon) das mache:

    VB.NET-Quellcode

    1. Private Sub ButtonArchive_Click(sender As Object, e As RibbonControlEventArgs) Handles ButtonArchive.Click
    2. Dim thread As New Thread(AddressOf AutoArchiverObj.ArchiveSelectedMails)
    3. thread.Start()
    4. End Sub

    ArchiveSelectedMails() startet dann spaeter auch die Methode searchForFolderByName(). Das funktioniert auch insofern, dass die GUI wieder reagiert, allerdings so ruckelig, dass man eigentlich auch nicht arbeiten kann.

    Als nächsten habe ich gelesen, dass es DoEvents() gibt. Das hat ja anscheinen auch seinen Ruf, aber Versuch macht kluch. Hab ich so ausprobiert:

    VB.NET-Quellcode

    1. Private Function searchForFolderByName(ByVal startFolder As Object, searchedFolderName As String, recursive As Boolean) As Folder
    2. ' sucht in einem uebergebenen Startfolder nach einem Unterordner mit dem uebergebenen Namen. Bei recursive = true werden auch Unterordner bis in alle Ebenen mitdurchsucht
    3. ' sucht NICHT case sensitive
    4. Dim Folder As Folder
    5. For Each Folder In startFolder.Folders
    6. If InStr(LCase(Folder.Name), LCase(searchedFolderName)) Then
    7. searchForFolderByName = Folder
    8. Exit Function
    9. Exit For
    10. End If
    11. If recursive Then
    12. searchForFolderByName = searchForFolderByName(Folder, searchedFolderName, True)
    13. If Not searchForFolderByName Is Nothing Then
    14. If InStr(LCase(searchForFolderByName.Name), LCase(searchedFolderName)) Then
    15. Exit Function
    16. Exit For
    17. End If
    18. End If
    19. End If
    20. If GetInputState() <> 0 Then
    21. System.Windows.Forms.Application.DoEvents()
    22. End If
    23. Next
    24. Return Nothing
    25. End Function


    Nun ist es so, dass ich keine Schleife habe, dessen häufige Ausführung vieler kleiner schneller Befehle den Thread blockieren wuerden, sondern die Anweisung " InStr(LCase(Folder.Name), LCase(searchedFolderName))" ist einfach sehr langsam (300-500ms laut Visual Studio, manchmal sind aber auch Zugriffe dabei, die mehrere Sekunden dauern.) was an dem Netzwerkzugriff liegt. Das heist der macht jetzt immer eine Anweisung die eine halbe Sekunde dauert und dann ein DoEvents(). Entsprechend ruckelig ist die GUI auch.
    Bei dem Thread vermute ich eine ganz ähnliche Problematik. Der Thread wird anscheinend nicht wirklich Parallel mit dem Main (GUI)-Thread ausgeführt, sondern es wird immer mal ein bisschen was in einem Thread gemacht und dann in dem Anderen. Wenn der Ordnersuche-Thread nun eine Anweisung hat, die lange dauert, ist der GUI-Thread für diese Zeit auch blockiert.
    Ich denke, wenn ich es hinkriegen würde, dass der Suche-Thread auf einem separaten Prozessor ausgeführt wird, also wirklich parallel zu dem Main-Thread, dann würde es auch gehen. Nur wie geht das?

    Darum den Suche-Thread zu blockieren, damit der User das nicht zweimal drücken kann, kümmer ich mich, wenn es funktioniert.

    Gruesse
    Schievel
    Willkommen im Forum.

    Stichwort Async/Await

    Probier es mal testweise mit:

    VB.NET-Quellcode

    1. Private Async Sub DieProzedurDieDieSuchfunktionAufruft()
    2. Dim Folder = Await Threading.Tasks.Task.Run(Function() searchForFolderByName(…))
    3. 'hier die Verarbeitung des Ergebnisses
    4. End Sub
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.
    Danke für deine Antwort.
    Über Await/ Async habe ich auch schon gelesen. Verstanden habe ich allerdings viel weniger, als ich gelesen habe. Komisch. :D

    Wenn ich nur in DieProzedurDieDieSuchfunktionAufruft() Async mache und die Prozedur searchForFolderByName() dann mit Await aufrufe, bekomme ich eine Execpion in der Methode, die Wiederrum DieProzedurDieDieSuchfunktionAufruft() aufruft.
    Und zwar dass der Typ Task nicht in Folder umgewandelt werden kann. Die uebergeordnete Methode ist Void, daher habe ich die jetzt einfach mal Async gemacht und die Methode DieProzedurDieDieSuchfunktionAufruft() auch mit Await aufgerufen. So geht es.
    Das ist auch besser so, denn die Methode DieProzedurDieDieSuchfunktionAufruft() ruft searchForFolderByName() mehrmals auf, das mache ich schon um zeit zu sparen, weil ich in den oberen Ebenen weis wie die Ordner aussehen und sie sich auch nicht aendern werden, muss ich da nicht Rekursiv suchen sondern picke mir einfach den Ordner aus der Ebene raus.

    Lange Rede, kurzer Sinn: Es ruckelt genauso, wie wenn ich einen eigenen Thread beim Ereignishandler von dem Button starte.
    Vielleicht hat das bei VSOT Addins auch was damit zutun, dass die Addinthreads immer zusammen mit dem Outlook-Prozess auf einem einzigen Prozessor ausgefuehrt werden. Danach sieht es zumindest im Taskmanager von Windows aus.
    Eine Möglichkeit in Visual Basic auf sowas Grundlegendes wie dem Scheduler vom Betriebssystem zuzugreifen und dem Mitzuteilen, dass man diesen einen speziellen Task jetzt auf einem anderen Kern haben möchte wenns geht, gibt es nicht, oder?

    / edit: Mit dem Backgroundworker hab ichs auch schon versucht, mit dem selben Ergebnis. :D

    // hier schreibt MS auch was darueber: docs.microsoft.com/de-de/visua…rt-in-office?view=vs-2019
    Das Office-Objektmodell ist nicht threadsicher, aber es ist möglich, mit mehreren Threads in einer Office-Projektmappe zu arbeiten. Office-Anwendungen sind Component Object Model (COM)-Server. COM ermöglicht Clients das Aufrufen von COM-Servern in beliebigen Threads. Für COM-Server, die nicht threadsicher sind, bietet COM einen Mechanismus zum Serialisieren gleichzeitiger Aufrufe, sodass immer nur ein logischer Thread auf dem Server ausgeführt wird.


    Versteh ich jetzt so, dass es zwar mehrere Threads gibts, die aber immer seriell ausgeführt werden. Also so wie frueher das Threading bei Singlecore-Prozessoren immer war. Also dass dann einfach staendig zwischen den Threads hin und her gesprungen wird.

    Dieser Beitrag wurde bereits 4 mal editiert, zuletzt von „Schievel“ ()

    Zumindest glaube ich kapiert zu haben, warum die Expetion kommt bei

    VB.NET-Quellcode

    1. Private Async Sub DieProzedurDieDieSuchfunktionAufruft()
    2. Dim Folder = Await Threading.Tasks.Task.Run(Function() searchForFolderByName(…))
    3. 'hier die Verarbeitung des Ergebnisses
    4. End Sub


    Ich poste mal meinen ganzen Code, damit klar wird wovon ich eigentlich rede:

    VB.NET-Quellcode

    1. Private Sub ArchiveMailToFolder(ByVal Item As Outlook.MailItem, subjectNr As String)
    2. ' Such nach dem Projektfolder mit "subjectNr" im Namen und kopiert die uebergebene Mail in den gefundenen Folder.
    3. ' Kategorisiert die uebergebene Mail und ihre Kopie falls gewuenscht
    4. Dim CopiedMail As Outlook.MailItem
    5. Dim ProjectFolder As MAPIFolder
    6. ProjectFolder = searchPublicFoldersTree(subjectNr)
    7. If Not ProjectFolder Is Nothing Then
    8. CopiedMail = Item.Copy
    9. If Settings1.Default.CatMyMail Then
    10. Item.Categories = Item.Categories + ";" + Settings1.Default.CatMyMailName
    11. Item.Save()
    12. End If
    13. If Settings1.Default.CatArchMail Then
    14. CopiedMail.Categories = CopiedMail.Categories + ";" + Settings1.Default.CatArchMailName
    15. End If
    16. CopiedMail.Move(ProjectFolder)
    17. Settings1.Default.BodyCount += 1
    18. Else
    19. MsgBox("Timout bei der Suche." _
    20. & vbCrLf & "Eventuell ist Ihre Internetverbindung zu langsam.",, "Projekt: " + subjectNr)
    21. End If
    22. End Sub ' uebergebene Mail archivieren
    23. Private Async Function searchPublicFoldersTree(searchString As String) As Task(Of MAPIFolder)
    24. 'das habe ich etwas eingekuerzt, die Funktion hangelt sich mit mehren nicht rekursiven Suchen bis zu einem Ordner, in dem alle relevanten Ordner liegen.
    25. Dim MyNameSpace As Object
    26. Dim MyFolder As MAPIFolder
    27. Dim choice As Long
    28. MyNameSpace = myOlApp.GetNamespace("MAPI")
    29. MyFolder = searchForFolderByName(MyNameSpace, "öffentlich", False)
    30. MyFolder = Await Threading.Tasks.Task.Run(Function() searchForFolderByName(MyFolder, searchString, True))
    31. Return MyFolder
    32. End Function 'sucht in den oeffentlichen ordnern nach einem Ordner dessen Name den uebergebenen String enthaelt
    33. Public Declare Function GetInputState Lib "user32" () As Long
    34. Private Function searchForFolderByName(ByVal startFolder As Object,
    35. searchedFolderName As String, recursive As Boolean) As Folder
    36. ' sucht in einem uebergebenen Startfolder nach einem Unterordner mit dem uebergebenen Namen. Bei recursive = true werden auch Unterordner bisin alle Ebenen mitdurchsucht
    37. ' sucht NICHT case sensitive
    38. Dim Folder As Folder
    39. For Each Folder In startFolder.Folders
    40. If InStr(LCase(Folder.Name), LCase(searchedFolderName)) Then
    41. searchForFolderByName = Folder
    42. Exit Function
    43. Exit For
    44. End If
    45. If recursive Then
    46. searchForFolderByName = searchForFolderByName(Folder, searchedFolderName, True)
    47. If Not searchForFolderByName Is Nothing Then
    48. If InStr(LCase(searchForFolderByName.Name), LCase(searchedFolderName)) Then
    49. Exit Function
    50. Exit For
    51. End If
    52. End If
    53. End If
    54. 'If GetInputState() <> 0 Then
    55. ' System.Windows.Forms.Application.DoEvents()
    56. 'End If
    57. Next
    58. Return Nothing
    59. End Function
    60. #End Region
    61. End Class


    Wenn ich es so mache geht es nicht, die da wirft er bei Zeile 6 (ProjectFolder = searchPublicFoldersTree(subjectNr)) die Execption
    "System.InvalidCastException: "Das Objekt des Typs "System.Threading.Tasks.Task`1[Microsoft.Office.Interop.Outlook.MAPIFolder]" kann nicht in Typ "Microsoft.Office.Interop.Outlook.MAPIFolder" umgewandelt werden."
    hoch.
    Der wartet anscheinend auch garnicht bei dem Await auf das Ergebnis, stattdessen bekommt er ein Ergebnis von der Run()-Methode, naemlich ein Aufgabenobjekt vom Typ System.Threading.Tasks.Task`1[Microsoft.Office.Interop.Outlook.MAPIFolder]. Ach sehe ich beim Debuggen, dass er die Exception wirf, dass unten in der Methode searchForFolderByName() gerade noch eine Ausführung stattfindet.

    Wenn ich nun aber die Funktion searchPublicFoldersTree() wieder normal mache (also ohne Async-Modifizierer) und stattdessen die sie Aufrufende Prozedur mit Async modifiziere, dann geht es und der wartet mit der Ausfuehrung von Zeile 7 (If Not ProjectFolder Is Nothing Then) bis ProjectFolder in Zeile 6 einen Wert hat.

    VB.NET-Quellcode

    1. Private Async Sub ArchiveMailToFolder(ByVal Item As Outlook.MailItem, subjectNr As String)
    2. ' Such nach dem Projektfolder mit "subjectNr" im Namen und kopiert die uebergebene Mail in den gefundenen Folder.
    3. ' Kategorisiert die uebergebene Mail und ihre Kopie falls gewuenscht
    4. Dim CopiedMail As Outlook.MailItem
    5. Dim ProjectFolder As MAPIFolder
    6. ProjectFolder = Await Threading.Tasks.Task.Run(Function() searchPublicFoldersTree(subjectNr))
    7. If Not ProjectFolder Is Nothing Then
    8. CopiedMail = Item.Copy
    9. If Settings1.Default.CatMyMail Then
    10. Item.Categories = Item.Categories + ";" + Settings1.Default.CatMyMailName
    11. Item.Save()
    12. End If
    13. If Settings1.Default.CatArchMail Then
    14. CopiedMail.Categories = CopiedMail.Categories + ";" + Settings1.Default.CatArchMailName
    15. End If
    16. CopiedMail.Move(ProjectFolder)
    17. Settings1.Default.BodyCount += 1
    18. Else
    19. MsgBox("Timout bei der Suche." _
    20. & vbCrLf & "Eventuell ist Ihre Internetverbindung zu langsam.",, "Projekt: " + subjectNr)
    21. End If
    22. End Sub ' uebergebene Mail archivieren


    Ich kapier jetzt nicht, warum er mal wartet und mal nicht. Hat es was damit zutun, dass der eine Await-Aufruf in einem Sub stattfindet und der Andere in einer Function? Sonst kann ich keinen Unterschied erkennen.

    Jetzt nur interessehalber weil ich ja noch was lernen moechte. Die Aufrufe der Foldernamen in der Function searchForFolderByName() sind ja alles Aufrufe vom COM-Server, wenn ich das richtig verstehe. Und die sind ja nach Mikrosofts Link weiter oben nicht so Multithreadingfaehig, wie ich das brauche.