none
Aufgaben an VB.Net abgeben oder Parallel ausführen RRS feed

  • Frage

  • Ich habe es immer mal wieder das ich in Access Aufgaben habe die nicht Ausführungs - Zeitrelevant sind aber meine Access Anwendung abbremsen.

    zb mal eine Abfrage die  LogTabellen bis auf X Datensätze löscht

    oder Dateien die angelegt werden oder Kopiert werden…

    alles Dinge von denen ich keine Rückgabe erwarte/benötige.

    Nun möchte ich diese Dinge „abgeben“ oder „Parallel“ laufen lassen und suche dafür einen Ansatz

    Kann ich mit VB.net eine Programm schreiben und dieses in Access mit einbinden?

    Ähnlich einer DLL

    Oder kann ich Access dazu bringen Parallel arbeite auszuführen?

    Mir geht es darum an einigen Punkten in der Anwendung „zeit“ zu sparen die zwar nicht lang ist aber die Anwendung sich dadurch nicht flüssig anfühlen lässt

    Dienstag, 31. Juli 2018 11:38

Antworten

  • Hi,
    eigentlich ist die Realisierung recht einfach.

    1. Du schreibst in einer .NET-Sprache (z.B. VB.NET) ein Klassenbibliotheks-Projekt.

    2. Die in der COM-Welt sichtbare Klasse versiehst Du mindestens mit einer GUID und dem ComVisible-Attribut.

    3. Das Bibliotheks-Projekt kennzeichnest Du mit "Make assembly COM-Visible".

    4. Zielplattform ist als X86 zu setzen.

    5. Die erzeugte dll ist dann im System zu registrieren (automatisch oder mit RegAsm.exe -tlb -codebase mydll.dll)

    Viel wichtiger ist die Programmlogik. Die aufgerufene Methode der .NET-dll wird synchron abgearbeitet. Wenn Du dort eine asynchrone Arbeit starten willst, dann ist wichtig festzulegen, wie das Ende der asynchronen Arbeit auf das Hauptprogramm wirken soll.

    Da die Übergabe von Objekten aus Access zur Nutzung in der .NET-dll nur Probleme bringen kann, sollte eine Art von Aufgaben in eine "Warteschlange" in der Access-Anwendung eingereiht werden. Die Warteschlange könnte z.B. eine temporäre Access-Tabelle sein. Mit einem Aufruf einer Methode in der .NET-dll wird dann ein asynchroner Prozess zur Verarbeitung eines Warteschlangeneintrages gestartet. In der .NET-dll wird ohne Rücksicht auf die Access-Anwendung ein eigener Zugriff auf die Access-Datenbank organisiert. Das bedeutet, dass zwei konkurrierende Anwendungen auf die Datenbank zugreifen, was in der Logik zu berücksichtigen ist. Dieser Ablauf entspricht logisch in etwa dem Beitrag von Markus.


    Hier mal ein Gerüst für eine Lösung. Es wird in VB.NET eine Start-Methode aufgerufen, der im Beispiel ein Parameter übergeben wird, dann ein Thread gestartet und über ein Ereignis wird das Ende gemeldet.

    Imports System.Runtime.InteropServices
    
    <ComVisible(False)>
    Public Delegate Sub SomeEventHandler(response As String)
    
    <Guid("032F0C1E-3AD1-4BF7-A4BC-37769EC4560D"),
    InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
    Public Interface ComClass1Interface
      <DispId(1)> Sub Start(msg As String)
    End Interface
    
    
    <Guid("55139E66-AD7D-4387-9E39-9117FBB34009"),
      ComVisible(True),
      InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
    Public Interface IComClass1Events
      <DispId(2)> Sub SomeEvent(response As String)
    End Interface
    
    <Guid("53CC5ACE-712F-4BDA-9B72-503C42F99F99"),
      ComVisible(True), ClassInterface(ClassInterfaceType.None),
      ComSourceInterfaces(GetType(IComClass1Events))>
    Public Class ComClass1
      Implements ComClass1Interface
    
      Public Event SomeEvent As SomeEventHandler
    
      Public Sub Start(msg As String) Implements ComClass1Interface.Start
        Dim c As New ThreadClass With {.Message = msg}
        AddHandler c.ThreadEnde, AddressOf StartEnde
        Dim th As New Threading.Thread(New Threading.ThreadStart(AddressOf c.Execute))
        th.Start()
      End Sub
    
      Private Sub StartEnde(response As String)
        RaiseEvent SomeEvent(response)
      End Sub
    
    End Class
    
    Friend Class ThreadClass
      Friend Event ThreadEnde As SomeEventHandler
    
      Friend Property Message As String
    
      Friend Sub Execute()
        Threading.Thread.Sleep(5000) ' Verzögerung der Arbeit simulieren
        RaiseEvent ThreadEnde($"{Date.Now.ToLongTimeString} {Message}")
      End Sub
    
    End Class

    Der Prinzipaufruf in VB6 (z.B. im Access-VBA) sieht dann so aus:

    Public WithEvents obj As ComClass1
    
    Sub Execute()
      Set obj = New ComClass1
      obj.Start "Los geht's " & Now
    End Sub
    
    Private Sub obj_SomeEvent(ByVal response As String)
      MsgBox response
    End Sub

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks





    • Bearbeitet Peter Fleischer Samstag, 4. August 2018 08:43 Code hinzugefügt, korrigiert
    • Als Antwort vorgeschlagen Peter Fleischer Samstag, 4. August 2018 08:44
    • Als Antwort markiert MCDPone Donnerstag, 9. August 2018 05:39
    Freitag, 3. August 2018 12:03

Alle Antworten

  • Hallo!

    Das klingt eh nach einer langfristigen Sache. Falls du hier keine zielführenden Antworten/Beispiele erhalten solltest und noch 2,5 Monate Zeit hast: Auf der AEK im Oktober wird es genau zu diesem Thema einen Vortrag geben (Access/VBA –> Interop –> .NET).

    Also entweder anmelden, um dich richtig auszukennen und den Referenten persönlich zu löchern, oder nach dem zweiten Termin schauen, ob die freien Downloads ausreichen, um die Techniken nachzuvollziehen.

    (Ende der kleinen Werbeeinschaltung. ;-)


    Karl
    http://www.AccessDevCon.com
    http://www.donkarl.com

    Dienstag, 31. Juli 2018 14:16
  • Ich hab mich vor kurzem mit diesem Thema gespielt.

    Im Ergebnis verwende ich eine zweite unsichtbar Access Anwendung, die Aufgaben parallel abarbeitet, oder ein kleines Script Programm das echtes Multithreading (QuickMacros) ermöglicht.

    Im meinem Fall ging es zuerst darum, dass ein User einen länger dauernden Prozess unterbrechen kann.

    Ich erreiche so, dass das Formular sofort auf seine Eingaben reagiert - ansonsten müsste er warten, bis ein DoEvents aus dem Hauptprozess veranlasst wird. So bekommt er sofort ein Feedback.

    Vom Konzept her:

    Es wird per Run ein Sub in der zweiten Anwendung gestartet.

    Die zweite Anwendung startet einen Timer, damit die Steuerung wieder sofort an die rufende Anwendung zurückkehrt.

    Unmittelbar danach beginnt die parallele Abarbeitung in der zweiten Anwendung.

    Gruss


    Markus



    • Bearbeitet markus888 Dienstag, 31. Juli 2018 14:51
    Dienstag, 31. Juli 2018 14:46
  • Danke erst einmal für deine Antwort

    Zweite Access Anwendung mache ich auch - finde es aber nicht so wirklich schön - weil so wirklich unsichtbar bekomme ich die nicht

    was meinst du mit script? was nutzt du dafür visual studio?


    • Bearbeitet MCDPone Donnerstag, 2. August 2018 10:02 s
    Donnerstag, 2. August 2018 10:02

  • Zweite Access Anwendung mache ich auch - finde es aber nicht so wirklich schön - weil so wirklich unsichtbar bekomme ich die nicht


    Du musst die Anwendung entweder unsichtbar starten (ich start mit der Shell Funktion), oder nach dem Start das Hauptfenster per API unsichtbar setzen.

    Scriptprogramm: Quick Macros. Mit Visual Studio hab ich bis jetzt nicht viel Erfahrung gemacht - da fehlt leider etwas die Zeit.


    Markus

    Freitag, 3. August 2018 07:45
  • Hi,
    eigentlich ist die Realisierung recht einfach.

    1. Du schreibst in einer .NET-Sprache (z.B. VB.NET) ein Klassenbibliotheks-Projekt.

    2. Die in der COM-Welt sichtbare Klasse versiehst Du mindestens mit einer GUID und dem ComVisible-Attribut.

    3. Das Bibliotheks-Projekt kennzeichnest Du mit "Make assembly COM-Visible".

    4. Zielplattform ist als X86 zu setzen.

    5. Die erzeugte dll ist dann im System zu registrieren (automatisch oder mit RegAsm.exe -tlb -codebase mydll.dll)

    Viel wichtiger ist die Programmlogik. Die aufgerufene Methode der .NET-dll wird synchron abgearbeitet. Wenn Du dort eine asynchrone Arbeit starten willst, dann ist wichtig festzulegen, wie das Ende der asynchronen Arbeit auf das Hauptprogramm wirken soll.

    Da die Übergabe von Objekten aus Access zur Nutzung in der .NET-dll nur Probleme bringen kann, sollte eine Art von Aufgaben in eine "Warteschlange" in der Access-Anwendung eingereiht werden. Die Warteschlange könnte z.B. eine temporäre Access-Tabelle sein. Mit einem Aufruf einer Methode in der .NET-dll wird dann ein asynchroner Prozess zur Verarbeitung eines Warteschlangeneintrages gestartet. In der .NET-dll wird ohne Rücksicht auf die Access-Anwendung ein eigener Zugriff auf die Access-Datenbank organisiert. Das bedeutet, dass zwei konkurrierende Anwendungen auf die Datenbank zugreifen, was in der Logik zu berücksichtigen ist. Dieser Ablauf entspricht logisch in etwa dem Beitrag von Markus.


    Hier mal ein Gerüst für eine Lösung. Es wird in VB.NET eine Start-Methode aufgerufen, der im Beispiel ein Parameter übergeben wird, dann ein Thread gestartet und über ein Ereignis wird das Ende gemeldet.

    Imports System.Runtime.InteropServices
    
    <ComVisible(False)>
    Public Delegate Sub SomeEventHandler(response As String)
    
    <Guid("032F0C1E-3AD1-4BF7-A4BC-37769EC4560D"),
    InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
    Public Interface ComClass1Interface
      <DispId(1)> Sub Start(msg As String)
    End Interface
    
    
    <Guid("55139E66-AD7D-4387-9E39-9117FBB34009"),
      ComVisible(True),
      InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
    Public Interface IComClass1Events
      <DispId(2)> Sub SomeEvent(response As String)
    End Interface
    
    <Guid("53CC5ACE-712F-4BDA-9B72-503C42F99F99"),
      ComVisible(True), ClassInterface(ClassInterfaceType.None),
      ComSourceInterfaces(GetType(IComClass1Events))>
    Public Class ComClass1
      Implements ComClass1Interface
    
      Public Event SomeEvent As SomeEventHandler
    
      Public Sub Start(msg As String) Implements ComClass1Interface.Start
        Dim c As New ThreadClass With {.Message = msg}
        AddHandler c.ThreadEnde, AddressOf StartEnde
        Dim th As New Threading.Thread(New Threading.ThreadStart(AddressOf c.Execute))
        th.Start()
      End Sub
    
      Private Sub StartEnde(response As String)
        RaiseEvent SomeEvent(response)
      End Sub
    
    End Class
    
    Friend Class ThreadClass
      Friend Event ThreadEnde As SomeEventHandler
    
      Friend Property Message As String
    
      Friend Sub Execute()
        Threading.Thread.Sleep(5000) ' Verzögerung der Arbeit simulieren
        RaiseEvent ThreadEnde($"{Date.Now.ToLongTimeString} {Message}")
      End Sub
    
    End Class

    Der Prinzipaufruf in VB6 (z.B. im Access-VBA) sieht dann so aus:

    Public WithEvents obj As ComClass1
    
    Sub Execute()
      Set obj = New ComClass1
      obj.Start "Los geht's " & Now
    End Sub
    
    Private Sub obj_SomeEvent(ByVal response As String)
      MsgBox response
    End Sub

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks





    • Bearbeitet Peter Fleischer Samstag, 4. August 2018 08:43 Code hinzugefügt, korrigiert
    • Als Antwort vorgeschlagen Peter Fleischer Samstag, 4. August 2018 08:44
    • Als Antwort markiert MCDPone Donnerstag, 9. August 2018 05:39
    Freitag, 3. August 2018 12:03
  • Danke genau so etwas habe ich gesucht für den einstig
    Donnerstag, 9. August 2018 05:40
  • Also noch einmal vielen dank für deine Antwort

    ich bin weiter aber leider auch an meiner grenze

    ich habe mir eine dll erstellt die ich auch aus Access heraus ansprechen kann

    ich hänge das mal an

    Imports System.Data.OleDb
    Imports System.Data.SqlClient
    
    
    <ComClass(CSR_Access.ClassId, CSR_Access.InterfaceId, CSR_Access.EventsId)>
    Public Class CSR_Access
    
    #Region "COM-GUIDs"
        Public Const ClassId As String = "88c9c1d9-da81-4f5f-919f-5b26b9594ca5"
        Public Const InterfaceId As String = "b5edffe4-ae93-4e5a-9eb9-50181a2a278c"
        Public Const EventsId As String = "8dae24ed-85ae-48f0-b454-fc9445b23b35"
    #End Region
    
        Public Function SQLAusfuehrenInMSSQL(ByVal WelcherConnectionString As String, ByVal WelcherSqlString As String, ByVal AbfrageKennung As String) As String
            Dim conn As SqlConnection = Nothing
    
            Try
                conn = New SqlConnection(WelcherConnectionString)
                Dim sqlCMD As New SqlCommand(WelcherSqlString, conn)
                conn.Open()
                Return sqlCMD.ExecuteNonQuery()
            Catch ex As Exception
    
                Return (ex.Message)
            Finally
                If Not IsNothing(conn) Then conn.Close()
            End Try
        End Function
    
        Public Function SQLAusfuehrenInACCDB(ByVal WelcherConnectionString As String, ByVal WelcherSqlString As String, ByVal AbfrageKennung As String) As String
            Dim conn As OleDbConnection = Nothing
    
            Try
                conn = New OleDbConnection(WelcherConnectionString)
                Dim sqlCMD As New OleDbCommand(WelcherSqlString, conn)
                conn.Open()
                Return sqlCMD.ExecuteNonQuery()
                'Return Nothing
            Catch ex As Exception
    
                Return ex.Message
            Finally
                If Not IsNothing(conn) Then conn.Close()
            End Try
        End Function
    
    End Class

    das funktioniert auch sehr gut ich kann schön aus Access die Functionen aufrufen und diese werden abgearbeitet

    (leider muss ich hier eben so lange warten bis ich den Rückgabewert erhalte)

    dann habe ich DEINE Variante mit den Threads erstellt Funktioniert Super nur verstehe ich nicht wie ich in der Version Übergaben von Access hin bekomme (wie bei mir mit ConnectionString oder wie ich Verschiedene Functionen dort anlegen kann


     

    Dienstag, 21. August 2018 11:20
  • Hi,
    kannst Du mal eine konkrete Aufgabenstellung für eine Demo formulieren. Dann könnte ich versuchen, eine Demo zu erstellen.

    Wenn Du von Access aus Methoden der .NET-dll aufrufst, kannst Du Parameter übergeben.

    Das Ende der asynchronen Arbeit kann ein Ereignis auslösen, welches Ergebnisdaten zurückliefert. Die Ereignisroutine in Access kann dann die Daten zur weiteren Nutzung in Access aufbereiten, z.B. zur Anzeige.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks

    Dienstag, 21. August 2018 11:50
  • Hi

    die Aufgabenstellung ist in "meinem" beispiel schon Abgebildet

    in dem möchte ich zb. eine Funktion aufrufen in der ich ein SQL String an eine bestimmte Datenbank abschicke

    ich habe in meier Demo 2 Funktionen 

    einmal

    sql String an eine MSSQL datenbank und 

    SQL String an ACCDB über eine OLEDB Connection

    den Connection String und den SQL String übergebe ich aus der Access Anwendung zusätzlich möchte ich eine Kennung übergeben aus der ich ersehen Kann aus welcher Access Funktion ich den Aufruf gemacht habe(damit ich den Rückgabe Wert oder Fehlermeldung in Access in einer Tabelle Schreiben kann)

    Die DLL soll später wachsen weil ich einige nicht Zeitlich relevanten Dinge aus der Anwendung auslagern möchte damit diese sich an einigen stellen "flüssiger" anfühlt


    zb temp Tabellen löschen bis auf X Datensätze
    Dateien via ftp hoch/runter laden

    ...

    Eben Dinge die erledigt werden sollen aber ich in Acccess nicht warten möchte/brauch

    Dienstag, 21. August 2018 13:13
  • Hi,
    ich dachte, dass mein Beispiel eigentlich aussagekräftig ist. Hier mal eine Demo etwas erweitert:

    Im Access-Formular gibt es eine Befehlsschaltfläche "DataLoad" und den folgenden Hintergrundcode:

    Option Compare Database
    
    Private WithEvents c As ComClass1
    Private id As Integer
    
    Private Sub DataLoad_Click()
      If c Is Nothing Then Set c = New ComClass1
      id = id + 1
      c.ExecuteScalar id, CurrentDb.Name, "SELECT count(*) FROM Tab1"
    End Sub
    
    Private Sub c_SomeEvent(ByVal id As Long, ByVal response As String)
      MsgBox "Prozess: " & id & vbNewLine & response
    End Sub

    Und die VB-NET-Klasse sieht dazu so aus:

    ' Das Bibliotheks-Projekt mit "Make assembly COM-Visible" kennzeichnen.
    ' Zielplattform als X86 setzen.
    
    Imports System.Data.OleDb
    Imports System.Runtime.InteropServices
    
    <ComVisible(False)>
    Public Delegate Sub SomeEventHandler(id As Integer, response As String)
    
    <Guid("5EBDEBEB-D4BD-4B16-B668-83E8E8C09D35"),
    InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
    Public Interface ComClass1Interface
      <DispId(1)> Sub ExecuteScalar(id As Integer, dbName As String, sql As String)
    End Interface
    
    
    <Guid("0D9F42A5-7540-4FF3-9010-821126D88B47"),
      ComVisible(True),
      InterfaceType(ComInterfaceType.InterfaceIsIDispatch)>
    Public Interface IComClass1Events
      <DispId(2)> Sub SomeEvent(id As Integer, response As String)
    End Interface
    
    <Guid("03E0273C-5473-4B29-BE99-F90903334436"),
      ComVisible(True), ClassInterface(ClassInterfaceType.None),
      ComSourceInterfaces(GetType(IComClass1Events))>
    Public Class ComClass1
      Implements ComClass1Interface
    
      Public Event SomeEvent As SomeEventHandler
    
      Public Sub ExecuteScalar(id As Integer, dbName As String, sql As String) Implements ComClass1Interface.ExecuteScalar
        Dim c As New ThreadClass With {.ID = id, .DbName = dbName, .Sql = sql}
        AddHandler c.ThreadEnde, AddressOf StartEnde
        Dim th As New Threading.Thread(New Threading.ThreadStart(AddressOf c.ExecuteScalar))
        th.Start()
      End Sub
    
      Private Sub StartEnde(id As Integer, response As String)
        RaiseEvent SomeEvent(id, response)
      End Sub
    
    End Class
    
    Friend Class ThreadClass
      Friend Event ThreadEnde As SomeEventHandler
    
      Friend Property ID As Integer
      Friend Property DbName As String
      Friend Property Sql As String
    
      Friend Sub ExecuteScalar()
        Try
          Dim cnstring = $"Provider=Microsoft.ACE.OLEDB.12.0;Data Source={DbName}"
          Using cn As New OleDbConnection(cnstring)
            Using cmd As New OleDbCommand(Sql, cn)
              cn.Open()
              Dim res = cmd.ExecuteScalar
              Threading.Thread.Sleep(1000) ' Verzögerung der Arbeit simulieren
              RaiseEvent ThreadEnde(ID, $"{Date.Now.ToLongTimeString} Result: {res}")
            End Using
          End Using
        Catch ex As Exception
          RaiseEvent ThreadEnde(ID, $"{Date.Now.ToLongTimeString} Error: {ex.Message}")
        End Try
      End Sub
    
    End Class

    Im Beispiel wird über ExecuteScalar der erste gelesene Wert über ein Ereignis an Access zurückgeliefert. Im Beispiel wird aus Access eine SQL Anfrage übergeben, die im Beispiel nur die Zahl der Datensätze in Tab1 per SQL ermittelt. Es wird eine Verzögerung für mögliche länger andauernde Arbeiten simuliert. Jede neue Anfrage bekommt eine neue ID. Für eine produktive Anwendung sollte jedoch berücksichtigt werden, dass die Jet bei intensiver Parallelarbeit mit ständigen Öffnen und Schließen der Verbindung den Dienst versagt ("aufhängt"). Deshalb sollte in diesem Fall die Com-Klasse mit Start und Ende Verbindungsaufbau separat zu Beginn und zum Ende der Arbeit aufgebaut werden. Alle Zugriffe laufen dann über die gleiche geöffnete Verbindung, wie das von Microsoft bei der Nutzung von Access empfohlen wird. Das muss dann in der Nutzung der COM-dll in Access berücksichtigt werden, um auch den belegten Speicherplatz wieder freizugeben.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP)
    Meine Homepage mit Tipps und Tricks

    Dienstag, 21. August 2018 16:26