none
Chiamare un Winform da DLL RRS feed

  • Domanda

  • Ciao a tutti,

    ho sviluppato un .exe winform in vb.net, e ora vorrei condividere alcune funzioni al suo interno (che non sono separabili dal resto del codice) in una sorta di API chiamate da una DLL a parte, che le espone in COM (ad esempio per essere utlizzate dal VBA di Excel).

    Ho creato quindi questa DLL che è chiamata "api.dll" e l'ho registrata con regasm generando il .tlb - fin qui tutto bene.

    La DLL al suo interno chiama il form di avvio nel seguente modo:

    Public Class api Dim nf As Form1 Public Sub New() nf = New Form1 ' partenza nf.Show() nf.Visible = False End Sub

        Public Sub showWindow()
            If nf IsNot Nothing Then
                nf.Visible = True
            End If
        End Sub ...

    Come vedete ho dichiarato come globale l'istanza del form nf che mi permette di chiamare i suoi metodi nel resto del codice della DLL. Se non uso poi:

    nf.Show()
    nf.Visible = False

    il form non risulta accessibile... non sono riuscito a spiegarmi il perchè, penso di perdermi qualcosa per strada...

    Ad ogni modo, il form così avviato da api.dll non funziona correttamente: tutte le variabili globali del form non sono correttamente inizializzate, e il codice non viene eseguito come se fosse partito dall'exe. Se lo provo ad esempio dal VBA di excel 2013 con queste righe:

    Sub avvia()
        Dim chp As New mydll.api
        chp.ShowWindow
    End Sub

    il programma non funziona correttamente: alcune (ma non tutte) variabili globali (soprattutto gli oggetti più complessi che mi servono) non sono inizializzate, anche se debuggando durante la chiamata al New sembra comportarsi normalmente. Premetto che non posso dichiarare tali oggetti globali come Shared - tutto il programma necessita che le loro istanze siano inizializzate per poter funzionare: le loro dichiarazioni si trovano nella classe Form1.
    Nell'esempio precedente lo stack di chiamate è: excel -> api.dll -> form1.exe

    Leggo in rete che è possibile avviare form contenuti in una dll semplicemente con Application.Run() o con rundll32. Io sto cercando una via per fare il contrario.
    Posso fare in modo di avere avviato il programma da DLL in modo corretto? Che differenza c'è fra la mia chiamata al form da DLL e l'avvio da .exe?

    EDIT: alcune doverose precisazioni dopo diverse altre prove: sto cercando ancora un metodo per l'avvio dell'applicazione che sia "completo", incluso Application.Run(nf), che però dà lo stesso problema di nf.Show() o ShowDialog().

    L'applicazione ha form multipli: le variabili globali non inizializzate sono tutte del Form1; vengono viste come Nothing da altri form, ai quali Form1 permette di accedere.
    Infine, avrei la necessità ulteriore di far leggere il file .config dell'exe in fase di avvio - sono abbastanza sicuro che ora non venga né letto né applicato.




    lunedì 20 gennaio 2020 14:52

Tutte le risposte

  • Un'opzione è creare un ServicedComponent.
    martedì 21 gennaio 2020 12:34
  • Grazie della risposta, ma mi servirebbe qualche indizio in più: a quanto leggo dalla documentazione, un ServicedComponent è la classe di base per tutte quelle classi che useranno il COM.

    In che componente andrebbe implementato? nel codice dell'exe che chiamo o nella dll?

    ps.

    non so perchè il mio post è stato spostato nel forum di VB6 e VBA, la mia richiesta è relativa al codice .NET, il COM è solo una feature che per ora non sembra essere il problema.



    martedì 21 gennaio 2020 19:51
  • Il fatto è che nella tua classe non c'è il minimo riferimento a COM, quindi probabilmente ti affidi ad un'esportazione automatica e allora vai a capire cosa succede dietro le quinte. Se invece fai un'esportazione consapevole magari riesci a capire meglio come controllare i vari aspetti che ti stanno dando problemi.

    In ogni caso il tuo oggetto form è privato nella classe api, probabilmente è per questo che non lo vedi, non te lo esporta.

    martedì 21 gennaio 2020 22:19
  • Il form è pubblico, e riesco a istanziarlo e avviarlo - forse questo non era esplicito.

    Una volta avviato non funziona bene: gli altri form dell'applicazione non vedono le variabili globali Public definite in Form1. Anche se Form1 è privato in api, le funzioni di api hanno sono un wrapper, cioè fanno da chiamante per le funzioni in form1 (che, come hai correttamente scritto, non sono esposte in COM), ma questo meccanismo è voluto.

    Credo dipenda dall'application context: la mia app si comporta con gli stessi errori di cui sopra se deseleziono "Application Framework" dalle opzioni del progetto vb.net.


    Giovanni Rinaldin

    mercoledì 22 gennaio 2020 08:10
  • Il form è pubblico, e riesco a istanziarlo e avviarlo - forse questo non era esplicito.


    Quando hai detto che il form risulta inaccessibile, credevo che volessi accedervi direttamente da VBA.

    Comunque se non esegui il metodo Run di Application, la gui non riceve i messaggi e se i vari eventi di inizializzazione non vengono chiamati, eventuali variabili non verranno impostate con i valori che ti aspetti. (Mentre non so cosa intendi con "gli altri form dell'applicazione non vedono le variabili globali": se non riescono ad accedervi, il problema dovrebbe essere un problema già al momento della compilazione).

    Quindi se non vuoi usare Application.Run perchè ti blocca tutto, dovrai far girare la gui della dll in un altro thread ed usare comunque Application.Run e gestire la sincronizzazione tra i thread. Pensavo che magari usando un ServicedComponent con la tua "api" come un'applicazione a se stante, magari molti dei problemi di marshalling verrebbero gestiti da un proxy dell'infrastuttura.



    • Modificato BlueLed mercoledì 22 gennaio 2020 09:21
    mercoledì 22 gennaio 2020 09:17
  • (Mentre non so cosa intendi con "gli altri form dell'applicazione non vedono le variabili globali": se non riescono ad accedervi, il problema dovrebbe essere un problema già al momento della compilazione)

    Penso sia questo il punto: la mia applicazione Winforms in vb.net ha form multipli e funziona correttamente solo se imposto come "Form di avvio" (o "Oggetto di avvio" se viene disabilitata l'opzione "Attiva framework applicazione") il Form1.

    Forzando ad esempio un oggetto di avvio come la Sub Main() qui sotto:

        Public Sub Main()
            Application.EnableVisualStyles()
            Application.Run(New Form1())
        End Sub

    allora accade quanto ho riportato, e cioè che una variabile globale (ad es. Public istanza as New MyClass) viene vista come Nothing da qualsiasi altro form (quanto chiamata come Form1.istanza).




    mercoledì 22 gennaio 2020 09:26
  • Sono le variabili del Form1 ad essere Nothing oppure è il valore di Form1 stesso ad essere Nothing?
    (Quando vi accedi da altro form)

    • Modificato BlueLed mercoledì 22 gennaio 2020 09:56
    mercoledì 22 gennaio 2020 09:55
  • Form1 non è Nothing, lo sono le sue variabili - è come se non fosse inizializzato.

    è come se fosse un problema di istanza che l'Application Framework risolve automaticamente.





    mercoledì 22 gennaio 2020 10:48
  • Quando l'Application Framework è abilitato il Main è generato automaticamente. Il file che lo contiene lo dovresti trovare tra i vari file nel progetto. Se lo apri puoi vedere quali sono le differenze con il tuo Main.

    mercoledì 22 gennaio 2020 11:05
  • No, non c'è nessun file con il Main all'interno con l'Application Framework attivo. Leggo in rete che per progetti VB.net non c'è mai.

    è la classe Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase che si occupa di questo automatismo, ma non riesco a trovare come lo fa. Anche il seguente codice mi provoca gli stessi problemi:

        Class MyApplicationFramework
            Inherits ApplicationServices.WindowsFormsApplicationBase
            Public Sub New()
                MyBase.New(ApplicationServices.AuthenticationMode.Windows)
                Me.EnableVisualStyles = True
                Me.IsSingleInstance = False
                Me.SaveMySettingsOnExit = False
                Me.ShutdownStyle = ApplicationServices.ShutdownMode.AfterMainFormCloses
            End Sub
            Protected Overrides Sub OnCreateMainForm()
                Me.MainForm = New Form1
            End Sub
        End Class

    Addirittura nei casi in cui si usi il C# taluni consigliano di usare Microsoft.VisualBasic.dll per approfittare di questo automatismo.

    La cosa è parecchio nebulosa: c'è qualcosa nell'automatismo dell'Application Framework che NON è equivalente al Program.cs di C# e nemmeno al suo porting in VB. Credo sia legato all'ApplicationContext, ma non so specificatamente a cosa - altro segnale è la mancata lettura del file .config.




    mercoledì 22 gennaio 2020 11:20
  • Potresti creare una classe con membro pubblico e shared di tipo Form1 e copiarci il riferimento all'istanza del form1 e poi vedere se attraverso questa gli altri form vedono i valori inizializzati.

    mercoledì 22 gennaio 2020 11:54
  • Tra l'altro trattandosi di una dll non credo che il metodo main venga chiamato automaticamente. Al più prova a chiamarlo esplicitamente. In ogni caso il metodo Run bloccherà l'applicazione se app e dll stanno sullo stesso thread.

    mercoledì 22 gennaio 2020 12:07
  • Ho provato a creare una nuova classe così:

        Public Class startForm
            Inherits System.Windows.Forms.Form
            Public Shared Form1 As New Form1
            Public Sub New()
                Form1.Show()
                Form1.Visible = False
            End Sub
        End Class

    Come nei tentativi precedenti, si avvia ma stesso errore: gli altri form vedono come Nothing tutte gli oggetti globali di Form1.

    Riguardo il secondo consiglio, ho spostato il Main() dentro form1, usando "Me" al posto di New Form1.

        Public Shared Sub Main()
            Application.EnableVisualStyles()
            Application.DoEvents()
            Application.Run(Me)
        End Sub
    Il programma non si avvia per "Errore di automazione", perchè chiamando Main() non ho accesso al form istanziato, anche dichiarandone un altro come Shared nella dll api.


    JohnnyBeGood__

    mercoledì 22 gennaio 2020 14:12
  • Intendevo una cosa semplice come questa:

       Public Class MyGlobals
            Public Shared f1 As Form1
       End Class

    poi il valore ad f1 lo imposti quando crei il form nella classe api.

    mercoledì 22 gennaio 2020 14:34
  • Intendevo una cosa semplice come questa:

       Public Class MyGlobals
            Public Shared f1 As Form1
       End Class

    poi il valore ad f1 lo imposti quando crei il form nella classe api.

    Così facendo non va, questa è la prima prova che ho fatto. Chiamando il Main con questo, o usando un Application.Run in un thread separato (perché è blocking), il f1 resta per sempre separato (e non inizializzato) dall'istanza di Form1 in esecuzione.



    JohnnyBeGood__

    mercoledì 22 gennaio 2020 14:50
  • A)

    L'hai inizializzato in questo modo?

    Public Sub New()

    nf = New Form1 ' partenza nf.Show() nf.Visible = False

    MyGlobals.f1 = nf

    End Sub

    B)

    A parte questo, se dai dei valori di default a quei campi che trovi Nothing, quando vai a leggerli da altri form che valore trovi?

    mercoledì 22 gennaio 2020 15:01
  • A) per scrupolo ho provato esattamente come riporti, ma non cambia nulla. Spostiamo solo il riferimento di nf a MyGlobals.f1 . A mio avviso è equivalente ad avere in api:

    Shared nf As Form1

    che tra l'altro ha il vantaggio di essere Private.

    B) Se inizializzo in api alcuni dei campi Public dell'istanza nf di form1, trovo sempre nothing da altri form (questo è strano…).


    JohnnyBeGood__

    mercoledì 22 gennaio 2020 15:12
  • A) per scrupolo ho provato esattamente come riporti, ma non cambia nulla. Spostiamo solo il riferimento di nf a MyGlobals.f1 . A mio avviso è equivalente ad avere in api:

    Shared nf As Form1

    che tra l'altro ha il vantaggio di essere Private.

    Negli altri form, poi, stai usando MyGlobals.f1 per accedere ai campi che ti interessano?

    B) Se inizializzo in api alcuni dei campi Public dell'istanza nf di form1, trovo sempre nothing da altri form (questo è strano…).


    JohnnyBeGood__

    Dicevo di inizializzarli nel Form1 stesso, proprio quando li dichiari.

    Tra l'altro perchè non usi delle properties invece di esporre i campi? Puo darsi che con i campi ci sia qualche problema con il meccanismo che esporta tramite COM.

    mercoledì 22 gennaio 2020 15:32
  • A) MyGlobals.f1 l'ho messo nelle api. non posso quindi usarlo in Form1. nelle altre funzioni di api ho chiamato proprio MyGlobals.f1.

    B) sono già inizializzati in Form1, è proprio quell'inizializzazione che viene saltata!

    Avrei potuto col senno di poi usare delle properties, ma il mio obiettivo attuale è proprio quello di non modificare il codice di Form1 - altrimenti, accettando una riprogettazione completa, sarei riuscito a modificare il fatto che tutti gli altri form e altri moduli facciano riferimento a campi Public di form1.

    ps.

    faccio notare che Form1 non può essere tramutato in una DLL - infatti ci sarebbero degli errori di compilazione proprio perché tutti gli altri form e altri moduli fanno riferimento a campi Public di form1.

    ps2

    mi ostino a pensare che non sia un problema di COM ma di avvio del main form, che deve essere caricato prima degli altri per fornire oggetti esistenti e inizializzati a tutto il resto. Il codice per simulare l'Application Framework è il seguente:

        Class MyApplicationFramework
            Inherits ApplicationServices.WindowsFormsApplicationBase
            Public Sub New()
                MyBase.New(ApplicationServices.AuthenticationMode.Windows)
                Me.EnableVisualStyles = False
                Me.IsSingleInstance = False
                Me.SaveMySettingsOnExit = False
                Me.ShutdownStyle = ApplicationServices.ShutdownMode.AfterMainFormCloses
            End Sub
            Protected Overrides Sub OnCreateMainForm()
                Me.MainForm = nf
            End Sub
        End Class

    in cui nf = New Form1.
    Notare che proprio nella riga "Me.MainForm = nf", il designer di visual studio (se si usa l'opzione citata prima nelle proprietà del progetto) scrive:

    Me.MainForm = Global.spazionomiExe.Form1

    senza inizializzare un bel niente. Sembra che questo sia permesso solo al compilatore vb e nello spazio dei nomi "My".

    A dimostrazione di questa differenza, se uso la classe MyApplicationFramework come riportata e lancio:

    Dim app As New MyApplicationFramework
    app.Run(args())

    il Run dà errore scrivendo che "è impossibile avviare un secondo ciclo di messaggi per l'applicazione". Significa che il New Form1 avvia già il ciclo di messaggi win32 propri della sola istanza nf, impedendo però il funzionamento come applicazione formata da form multipli (o almeno credo).


    JohnnyBeGood__

    mercoledì 22 gennaio 2020 15:46
  • Immaginavo che avessi un progetto form1.exe e lo avessi convertito nel progetto api.dll. Ma se il form non è nella dll allora cosa hai fatto? Hai creato una dll .Net che supporta COM e la usi in form1.exe? Quindi lanci form1.exe e l'applicazione vba? Oppure non lanci form1.exe, ma soltanto l'applicazione vba che ti carica la dll? Immagino che nel primo caso hai due istanze diverse della dll, mentre nel secondo l'assembly form1.exe se pur dovesse esser caricato, non venga eseguito.

    mercoledì 22 gennaio 2020 16:49
  • Nel primo post avevo chiarito che avevo sia exe che dll, quest'ultima chiamata da VBA.

    L'obiettivo è quello di lanciare solo l'app VBA (Excel in questo caso) che abbia una reference alla DLL. La DLL deve caricare form1.exe ed eseguire dei metodi al suo interno con i dati passati dal VBA. Solo la DLL è esposta in COM, e form1 è una reference per la DLL. Una sorta di API in pratica.

    Volevo evitare le soluzioni che coinvolgono tcp services interni come questa.

    La necessità di avviare correttamente Form1 è nata quando mi sono accorto che senza l'application framework il mio exe non si caricava correttamente (e infatti come detto, mettendo l'exe in una dll non compila).


    JohnnyBeGood__

    mercoledì 22 gennaio 2020 16:56
  • Sì, avevo letto che hai un exe ed una dll, ma avevo escluso la possibilità che li facessi lavorare insieme perchè non mi pare sia questa la pratica, poi non so, magari hai trovato questa tecnica su qualche tutorial. In ogni caso come dicevo, credo che la dll pur caricando l'exe perchè ne usa il tipo Form1, non faccia eseguire l'assembly come un exe e probabilmente è per questo ti trovi l'applicazione non inizializzata correttamente.

    mercoledì 22 gennaio 2020 17:16
  • Eppure viene eseguita l'inizializzazione completa di form1, il problema è appunto negli altri form.

    Oltre a ServicedComponent e WCF, ci sono altre vie per tua conoscenza? Pur nello stesso thread, l'applicazione avviata (non con New, ma con un sub Main apposito) sembra dentro una sorta di sandbox che rende inaccessibile tutto al suo interno.


    JohnnyBeGood__

    mercoledì 22 gennaio 2020 17:23
  • Quello che vorresti fare sembra un compito per un Out Of Process COM Server. Il fatto è che volendolo realizzare in .Net, non si trova molto in giro. Questo sembra promettente:

    https://github.com/avarghesein/ActiveX.NET

    mercoledì 22 gennaio 2020 17:57
  • Ti ringrazio, lo proverò e posterò qui se troverò qualche soluzione

    JohnnyBeGood__

    mercoledì 22 gennaio 2020 18:21
  • faccio notare che Form1 non può essere tramutato in una DLL - infatti ci sarebbero degli errori di compilazione proprio perché tutti gli altri form e altri moduli fanno riferimento a campi Public di form1.
    Per curiosità ho creato ed esportato per COM una dll con un paio di form ed il secondo vede non nullo un campo shared del form principale. Per quel poco di test che ho fatto, sembra funzionare comunque  bene.


    • Modificato BlueLed giovedì 23 gennaio 2020 13:47
    giovedì 23 gennaio 2020 13:45
  • Sì, perché è shared. Anche per me è così, ma come dicevo ho campi che non possono essere shared.

    JohnnyBeGood__

    giovedì 23 gennaio 2020 13:49
  • Sì, perché è shared. Anche per me è così, ma come dicevo ho campi che non possono essere shared.

    JohnnyBeGood__

    Perchè se te ne servono solo alcuni shared devi comunque renderli tutti shared?

    giovedì 23 gennaio 2020 14:04
  • No, no, solo alcuni campi non possono essere shared perché ho usato operazioni su di loro con più thread all'interno della stessa applicazione.

    Indipendentemente dal motivo, mi sono convinto che non è possibile fare una libreria da un'applicazione abbastanza complessa (o scritta male, dipende dai punti di vista…) perché gli automatismi dell'application sono molto diversi.

    Mi sono anche reso conto che sto prima a riscrivere alcuni pezzi dell'exe nella dll api al posto di usare un Out Of Process COM Server.

    Ad ogni modo ho imparato molte cose - ti ringrazio ancora dell'aiuto.


    JohnnyBeGood__

    giovedì 23 gennaio 2020 14:09
  • No, no, solo alcuni campi non possono essere shared perché ho usato operazioni su di loro con più thread all'interno della stessa applicazione.

    JohnnyBeGood__

    Quello che mi chiedevo è "perchè con la gui sulla dll sei costretto a renderli tutti shared?" Hai detto che non puoi usare solo la dll perchè hai dei campi devono essere non shared.

    giovedì 23 gennaio 2020 14:17
  • Ho detto che, limitatamente al solo exe, ho dei campi al suo interno che non possono essere shared. 

    Probabilmente, per convertire l'exe in una dll che compili, dovrei renderli tutti shared per questo semplice motivo: un altro form (chiamamolo f2) o un modulo che chiami Form1.campoNonShared dà errore di compilazione perché in una DLL può esistere l'istanza da sola di f2 senza che sia istanziato Form1.


    JohnnyBeGood__

    giovedì 23 gennaio 2020 14:23
  • Probabilmente, il problema di fondo è che ti affidi all'istanza di default dei Form che è una caratteristica del VB.Net:

    http://jmcilhinney.blogspot.com/2009/07/vbnet-default-form-instances.html

    Se invece di accedere al form1 tramite Form1, vi accedi tramite la proprietà OpenForms dell'oggetto Application, forse hai risolto.

    giovedì 23 gennaio 2020 16:14
  • Già provato, se, dalla DLL, dopo l'avvio dell'applicazione uso:

    nf = app.OpenForms.Item("Form1")
    ottengo nf che è Nothing.


    JohnnyBeGood__

    giovedì 23 gennaio 2020 16:18
  • Già provato, se, dalla DLL, dopo l'avvio dell'applicazione uso:

    nf = app.OpenForms.Item("Form1")
    ottengo nf che è Nothing.


    JohnnyBeGood__

    Sì, ma app non è Application.

    In ogni caso, immagino che i form secondari li lanci dal form1 (o almeno uno di questi). Allora a ciascun form puoi passare il 'Me'.

    giovedì 23 gennaio 2020 16:35
  • In ogni caso, immagino che i form secondari li lanci dal form1 (o almeno uno di questi). Allora a ciascun form puoi passare il 'Me'.

    Ok, il Me vale dall'interno di Form1. Se nf è nothing non riesco ad accedere a nulla dalla DLL.

    JohnnyBeGood__

    giovedì 23 gennaio 2020 16:39
  • Intendo questo:

    Class Form1
    
    ...
    
    Sub OpenForm2()
        Dim f2 As Form2 = New Form2(Me)
        f2.Show()
    End Sub
    
    ...
    Chiaramente in Form2 definirai un costruttore con parametro Form1.


    • Modificato BlueLed giovedì 23 gennaio 2020 16:56
    giovedì 23 gennaio 2020 16:55
  • Non capisco cosa dovrebbe cambiare questo: proprio per il meccanismo dell'istanza di default nessun form viene istanziato con New all'interno di una applicazione.

    Inoltre, OpenForms è ReadOnly, quindi (anche se, come invece succede, non rappresenta dall'esterno l'istanza del form all'interno dell'applicazione) non sarebbe accessibile in scrittura per i campi non shared di cui si parlava.


    JohnnyBeGood__

    giovedì 23 gennaio 2020 20:08
  • Cambia il fatto che non usi più le variabili condivise. Ti faccio un parallelo con una telefonata: tu vorresti che form2 chiami form1 facendogli cercare il numero di telefono sull'elenco che, non appartenendo a nessuno dei due, è globale. Qualcuno scrive il telefono di form1 sull'elenco e form2 consulta l'elenco. Se invece form1 comunica esso stesso il suo numero di telefono a form2 e form2 se lo appunta internamente, quando questo avrà bisogno di chiamarlo non dovrà consultare l'elenco.

    venerdì 24 gennaio 2020 08:42
  • è una stategia interessante da provare - a valle di tutti i miei tentativi però dubito che possa funzionare, almeno non senza cambiare il codice di Form1 e negli altri form.

    Infatti ribadisco che normalmente non è prevista nessuna dichiarazione dei form aggiunti ad una applicazione nel form chiamante. Inoltre con l'esempio di codice che riporti, la visibilità di Form2 dalla DLL non c'è, dovrebbe essere dichiarato public a livello di classe in form1.

    In pratica cerchiamo di sostituire l'oggetto Application con l'istanza stessa di Form1, che dovrà gestire tutta il messaging al posto di Application. Questo può sicuramente funzionare perché la classe form lo fa.


    JohnnyBeGood__

    venerdì 24 gennaio 2020 16:00