Il miglior design per moduli Windows che condividerà funzionalità comuni


20

In passato ho usato l'ereditarietà per consentire l'estensione dei moduli di Windows nella mia applicazione. Se tutti i miei moduli avessero controlli, grafica e funzionalità comuni, creerei un modulo di base che implementa i controlli e le funzionalità comuni e quindi consenti agli altri controlli di ereditare da quel modulo di base. Tuttavia, ho riscontrato alcuni problemi con quel design.

  1. I controlli possono essere solo in un contenitore alla volta, quindi qualsiasi controllo statico che hai sarà complicato. Ad esempio: supponiamo di avere un modulo di base chiamato BaseForm che conteneva un TreeView che rendi protetto e statico in modo che tutte le altre istanze (derivate) di questa classe possano modificare e visualizzare lo stesso TreeView. Ciò non funzionerebbe per più classi che ereditano da BaseForm, perché TreeView può trovarsi in un solo contenitore alla volta. Probabilmente sarebbe sull'ultimo modulo inizializzato. Sebbene ogni istanza possa modificare il controllo, verrà visualizzato solo in uno alla volta. Certo, ci sono soluzioni alternative, ma sono tutti brutti. (Questo sembra essere un progetto davvero brutto per me. Perché più contenitori non possono memorizzare puntatori sullo stesso oggetto? Comunque, è quello che è.)

  2. Stato tra i moduli, ovvero gli stati dei pulsanti, il testo dell'etichetta, ecc., Devo utilizzare le variabili globali per e ripristinare gli stati su Caricamento.

  3. Questo non è molto ben supportato dal designer di Visual Studio.

Esiste un design migliore, ma ancora facilmente gestibile da usare? O l'ereditarietà delle forme è ancora l'approccio migliore?

Aggiornamento Sono passato dall'osservare MVC a MVP al modello Observer al modello eventi. Ecco cosa sto pensando per il momento, per favore, critica:

La mia classe BaseForm conterrà solo i controlli e gli eventi collegati a tali controlli. Tutti gli eventi che necessitano di qualsiasi tipo di logica per gestirli passeranno immediatamente alla classe BaseFormPresenter. Questa classe gestirà i dati dall'interfaccia utente, eseguirà qualsiasi operazione logica e quindi aggiornerà BaseFormModel. Il Modello esporrà gli eventi, che si attiveranno al cambio di stato, alla classe Presenter, alla quale si iscriverà (o osserverà). Quando il Presenter riceve la notifica dell'evento, eseguirà qualsiasi logica e quindi il Presenter modificherà la vista di conseguenza.

Ci sarà solo una di ciascuna classe Model in memoria, ma potrebbero esserci potenzialmente molte istanze di BaseForm e quindi BaseFormPresenter. Ciò risolverebbe il mio problema di sincronizzazione di ogni istanza di BaseForm con lo stesso modello di dati.

Domande:

Quale livello dovrebbe memorizzare cose come, l'ultimo pulsante premuto, in modo da poterlo evidenziare per l'utente (come in un menu CSS) tra i moduli?

Per favore, critica questo disegno. Grazie per l'aiuto!


Non vedo perché sei costretto a usare le variabili globali, ma in tal caso sì, sicuramente ci deve essere un approccio migliore. Forse le fabbriche per creare componenti comuni combinate con la composizione anziché l'eredità?
stijn

L'intero design è difettoso. Se hai già capito cosa vuoi fare non è supportato da Visual Studio che dovrebbe dirti qualcosa.
Ramhound,

1
@Ramhound È supportato da Visual Studio, ma non bene. Ecco come Microsoft ti dice di farlo. Trovo che sia un dolore in A. msdn.microsoft.com/en-us/library/aa983613(v=vs.71).aspx Comunque, se hai un'idea migliore, sono tutti gli occhi.
Jonathan Henson,

@stijn Suppongo di poter avere un metodo in ogni modulo di base che caricherà gli stati di controllo in un'altra istanza. vale a dire LoadStatesToNewInstance (istanza BaseForm). Potrei chiamarlo ogni volta che voglio mostrare una nuova forma di quel tipo di base. form_I_am_about_to_hide.LoadStatesToNewInstance (questo);
Jonathan Henson,

Non sono uno sviluppatore .net, puoi espandere il punto numero 1? Potrei avere una soluzione
Imran Omar Bukhsh il

Risposte:


6
  1. Non so perché hai bisogno di controlli statici. Forse sai qualcosa che io non so. Ho usato molta eredità visiva ma non ho mai visto i controlli statici necessari. Se si dispone di un controllo treeview comune, lasciare che ogni istanza del modulo abbia la propria istanza del controllo e condividere una singola istanza dei dati associati alle treeview.

  2. Anche la condivisione dello stato di controllo (al contrario dei dati) tra i moduli è un requisito insolito. Sei sicuro che FormB debba davvero conoscere lo stato dei pulsanti su FormA? Prendi in considerazione i progetti MVP o MVC. Pensa a ogni modulo come a una "vista" stupida che non sa nulla delle altre viste o persino dell'applicazione stessa. Supervisiona ogni visualizzazione con un presentatore / controller intelligente. Se ha senso, un singolo relatore può supervisionare diverse visualizzazioni. Associa un oggetto stato a ciascuna vista. Se hai qualche stato che deve essere condiviso tra le viste, lascia che il / i presentatore / i mediano questo (e considera la creazione di database - vedi sotto).

  3. D'accordo, Visual Studio ti farà venire il mal di testa. Quando si considera l'eredità della forma o del controllo dell'utente, è necessario valutare attentamente i vantaggi rispetto al potenziale (e probabile) costo della lotta con le stranezze e le limitazioni frustranti del progettista della forma. Suggerisco di ridurre al minimo l'eredità della forma: usala solo quando il profitto è alto. Tieni presente che, in alternativa alla sottoclasse, puoi creare un modulo "base" comune e semplicemente istanziarlo una volta per ogni aspirante "figlio" e quindi personalizzarlo al volo. Ciò ha senso quando le differenze tra ciascuna versione del modulo sono minori rispetto agli aspetti condivisi. (IOW: forma base complessa, forme figlio solo leggermente più complesse)

Usa i controlli utente quando ti aiuta a prevenire una duplicazione significativa dello sviluppo dell'interfaccia utente. Considerare l'ereditarietà del controllo utente, ma applicare le stesse considerazioni relative all'ereditarietà dei moduli.

Penso che il consiglio più importante che posso offrire sia, se al momento non usi una qualche forma del modello di visualizzazione / controller, ti incoraggio vivamente a iniziare a farlo. Ti costringe ad apprendere e ad apprezzare i vantaggi dell'accoppiamento libero e della separazione degli strati.

risposta al tuo aggiornamento

Quale livello dovrebbe memorizzare cose come, l'ultimo pulsante premuto, in modo da poterlo evidenziare per l'utente ...

È possibile condividere lo stato tra le visualizzazioni proprio come se si condividesse lo stato tra un relatore e la sua vista. Creare una classe speciale SharedViewState. Per semplicità puoi renderlo un singleton o puoi istanziarlo nel presentatore principale e passarlo a tutte le visualizzazioni (tramite i loro presentatori) da lì. Quando lo stato è associato ai controlli, utilizzare l'associazione dei dati ove possibile. Molte Controlproprietà possono essere associate a dati. Ad esempio, la proprietà BackColor di un pulsante potrebbe essere associata a una proprietà della classe SharedViewState. Se si esegue questa associazione su tutti i moduli con pulsanti identici, è possibile evidenziare Button1 su tutti i moduli semplicemente impostando SharedViewState.Button1BackColor = someColor.

Se non si ha familiarità con il database di WinForms, premere MSDN e leggere. Non è difficile. Scopri di più INotifyPropertyChangede sei a metà strada.

Ecco un'implementazione tipica di una classe viewstate con la proprietà Button1BackColor come esempio:

public class SharedViewState : INotifyPropertyChanged
{
    // boilerplate INotifyPropertyChanged stuff
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    // example of a property for data-binding
    private Color button1BackColor;
    public Color Button1BackColor
    {
        get { return button1BackColor; }
        set
        {
            if (value != button1BackColor)
            {
                button1BackColor = value;
                NotifyPropertyChanged("Button1BackColor");
            }
        }
    }
}

Grazie per la tua risposta premurosa. Conosci un buon riferimento al modello di vista / controller?
Jonathan Henson,

1
Ricordo di aver pensato che fosse una buona introduzione: codebetter.com/jeremymiller/2007/05/22/…
Igby Largeman,

per favore vedi il mio aggiornamento.
Jonathan Henson,

@JonathanHenson: risposta aggiornata.
Igby Largeman,

5

Mantengo una nuova applicazione WinForms / WPF basata sul modello MVVM e sui controlli di aggiornamento . È iniziato come WinForms, quindi ho creato una versione WPF a causa dell'importanza del marketing di un'interfaccia utente che sembra buona. Ho pensato che sarebbe stato un vincolo di progettazione interessante mantenere un'applicazione che supportasse due tecnologie UI completamente diverse con lo stesso codice di backend (modelli e modelli) e devo dire che sono abbastanza soddisfatto di questo approccio.

Nella mia app, ogni volta che devo condividere funzionalità tra parti dell'interfaccia utente, utilizzo controlli personalizzati o controlli utente (classi derivate da WPF UserControl o WinForms UserControl). Nella versione WinForms c'è un UserControl all'interno di un altro UserControl all'interno di una classe derivata di TabPage, che è finalmente all'interno di un controllo a schede nel modulo principale. La versione di WPF ha effettivamente UserControls nidificato di un livello più in profondità. Utilizzando i controlli personalizzati, è possibile comporre facilmente una nuova interfaccia utente dai controlli utente creati in precedenza.

Dato che utilizzo il modello MVVM, ho inserito quanta più logica di programma possibile nel ViewModel (incluso il modello di presentazione / navigazione ) o nel modello (a seconda che il codice sia correlato o meno all'interfaccia utente), ma dal momento che lo stesso ViewModel viene utilizzato sia da una vista WinForms che da una vista WPF, ViewModel non può contenere codice progettato per WinForms o WPF o che interagisce direttamente con l'interfaccia utente; tale codice DEVE andare nella vista.

Anche nel modello MVVM, gli oggetti UI dovrebbero evitare di interagire tra loro! Invece interagiscono con i modelli di visualizzazione. Ad esempio un TextBox non chiederebbe a un ListBox vicino quale elemento è selezionato; invece la casella di riepilogo salverebbe un riferimento all'elemento attualmente selezionato da qualche parte nel layer viewmodel e TextBox interrogherà il layer viewmodel per scoprire cosa è selezionato in questo momento. Questo risolve il tuo problema di scoprire quale pulsante è stato premuto in una forma all'interno di una forma diversa. È sufficiente condividere un oggetto Modello di navigazione (che fa parte del livello viewmodel dell'applicazione) tra i due moduli e inserire una proprietà in quell'oggetto che rappresenta il pulsante premuto.

WinForms stesso non supporta molto bene il modello MVVM, ma Update Controls fornisce il proprio approccio unico MVVM come libreria che si trova in cima a WinForms.

A mio avviso, questo approccio alla progettazione del programma funziona molto bene e ho intenzione di utilizzarlo su progetti futuri. Le ragioni per cui funziona così bene sono (1) che Update Controls gestisce automaticamente le dipendenze e (2) che di solito è molto chiaro come strutturare il codice: tutto il codice che interagisce con gli oggetti dell'interfaccia utente appartiene alla vista, tutto il codice che è Relativo all'interfaccia utente ma NON DEVE interagire con gli oggetti dell'interfaccia utente appartiene a ViewModel. Abbastanza spesso dividerai il tuo codice in due parti, una parte per View e un'altra parte per ViewModel. Ho avuto qualche difficoltà a progettare menu contestuali nel mio sistema, ma alla fine ho trovato anche un design per quello.

Ho delirio per i controlli di aggiornamento anche sul mio blog . Tuttavia, questo approccio richiede molto tempo per abituarsi e nelle app di grandi dimensioni (ad es. Se le caselle di riepilogo contengono migliaia di elementi) potrebbero verificarsi problemi di prestazioni a causa delle attuali limitazioni della gestione automatica delle dipendenze.


3

Risponderò a questa domanda anche se hai già accettato una risposta.

Come altri hanno sottolineato, non capisco perché tu abbia dovuto usare qualcosa di statico; sembra che tu abbia fatto qualcosa di molto sbagliato.

Comunque, ho avuto lo stesso problema con te: nella mia applicazione WinForms ho diversi moduli che condividono alcune funzionalità e alcuni controlli. Inoltre, tutti i miei moduli derivano già da un modulo di base (chiamiamolo "MyForm") che aggiunge funzionalità a livello di framework (indipendentemente dall'applicazione). Il Forms Designer di Visual Studio suppone presumibilmente la modifica di moduli ereditati da altri moduli, ma in pratica funziona solo fintanto che i tuoi moduli non fanno altro che "Ciao, mondo! - OK - Annulla".

Quello che ho finito per fare è questo: mantengo la mia classe base comune "MyForm", che è piuttosto complessa, e continuo a ricavarne tutti i moduli della mia domanda. Tuttavia, non faccio assolutamente nulla in queste forme, quindi VS Forms Designer non ha problemi a modificarle. Questi moduli sono costituiti esclusivamente dal codice che il Designer moduli genera per loro. Quindi, ho una gerarchia parallela separata di oggetti che chiamo "Surrogati" che contengono tutte le funzionalità specifiche dell'applicazione, come l'inizializzazione dei controlli, la gestione degli eventi generati dal modulo e i controlli, ecc. C'è uno a uno corrispondenza tra classi surrogate e finestre di dialogo nella mia applicazione: esiste una classe surrogata di base che corrisponde a "MyForm", quindi viene derivata un'altra classe surrogata che corrisponde a "MyApplicationForm",

Ogni surrogato accetta come parametro dei tempi di costruzione un tipo specifico di modulo e si attacca ad esso registrandosi per i suoi eventi. Delega anche alla base, fino a "MySurrogate" che accetta un "MyForm". Quel surrogato si registra con l'evento "Disposed" del modulo, in modo che quando il modulo viene distrutto, il surrogato invoca un overridable su se stesso in modo che esso, e tutti i suoi discendenti, possano eseguire la pulizia. (Annulla la registrazione dagli eventi, ecc.)

Finora ha funzionato bene.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.