Miglior design per moduli Windows che condivideranno funzionalità comuni

19

In passato, ho utilizzato l'ereditarietà per consentire l'estensione dei moduli di Windows nella mia applicazione. Se tutte le mie forme avessero controlli, elementi grafici e funzionalità comuni, creerei un modulo di base che implementa i controlli e le funzionalità comuni e quindi consentirò ad altri controlli di ereditare da tale modulo di base. Tuttavia, ho riscontrato alcuni problemi con questo design.

  1. I controlli possono essere solo in un contenitore alla volta, quindi qualsiasi controllo statico che hai sarà difficile. Ad esempio: Supponiamo di avere un modulo 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ò essere solo in un contenitore alla volta. Sarebbe probabilmente sull'ultimo modulo inizializzato. Sebbene ogni istanza possa modificare il controllo, verrà visualizzata solo in una volta. Certo, ci sono dei work-around, ma sono tutti brutti. (Questo mi sembra un progetto davvero pessimo, perché non è possibile che più contenitori memorizzino puntatori allo stesso oggetto? Comunque, è quello che è.)

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

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

C'è un design migliore, ma ancora facilmente gestibile da usare? O l'ereditarietà della forma è ancora l'approccio migliore?

Aggiorna Sono passato dall'osservazione di MVC a MVP a modello di osservatore per il pattern di evento. Ecco cosa sto pensando per il momento, per favore critica:

La mia classe BaseForm conterrà solo i controlli e gli eventi connessi a quei controlli. Tutti gli eventi che richiedono una sorta di logica per gestirli passeranno immediatamente alla classe BaseFormPresenter. Questa classe gestirà i dati dall'interfaccia utente, eseguirà tutte le operazioni logiche e quindi aggiornerà BaseFormModel. Il Modello esporrà eventi, che spareranno su cambiamenti di stato, alla classe Presenter, alla quale si iscriverà (o osserverà). Quando il relatore riceve la notifica dell'evento, eseguirà qualsiasi logica, quindi il relatore modificherà la vista di conseguenza.

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

Domande:

Quale layer dovrebbe memorizzare cose come, l'ultimo pulsante premuto, in modo che possa tenerlo evidenziato per l'utente (come in un menu CSS) tra le forme?

Si prega di criticare questo design. Grazie per il tuo aiuto!

    
posta Jonathan Henson 12.12.2011 - 06:32
fonte

3 risposte

6
  1. Non so perché hai bisogno di controlli statici. Forse sai qualcosa che 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. La condivisione dello stato di controllo (al contrario dei dati) tra le forme è anche un requisito inusuale. Sei sicuro che FormB abbia davvero bisogno di conoscere lo stato dei pulsanti su FormA? Considera i progetti MVP o MVC. Pensa a ogni modulo come a una "vista" stupida che non conosce nulla delle altre visualizzazioni o persino dell'applicazione stessa. Supervisiona ogni vista con un presentatore / controller intelligente. Se ha senso, un singolo presentatore può supervisionare diverse visualizzazioni. Associare un oggetto stato a ciascuna vista. Se hai qualche stato che deve essere condiviso tra le viste, lascia che il presentatore (i) lo faccia mediare (e considera l'associazione - vedi sotto).

  3. D'accordo, Visual Studio ti darà grattacapi. Quando si considera l'ereditarietà di forma o di usercontrollo, è necessario valutare attentamente i benefici rispetto al potenziale (e probabile) costo del wrestling con le stranezze e le limitazioni frustranti del progettista di moduli. Suggerisco di mantenere l'ereditarietà della forma al minimo - usarla solo quando il guadagno è alto. Tieni presente che, in alternativa alla sottoclasse, puoi creare una "base" comune e semplicemente istanziarla una volta per ogni aspirante "bambino" e quindi personalizzarla al volo. Ciò ha senso quando le differenze tra ciascuna versione della forma sono minori rispetto agli aspetti condivisi. (IOW: forma base complessa, forme figlio solo-leggermente-più-complesse)

Fai uso di usercontrols quando ti aiuta a prevenire una significativa duplicazione dello sviluppo dell'interfaccia utente. Considera l'ereditarietà di usercontrol ma applica le stesse considerazioni dell'ereditarietà dei moduli.

Penso che il consiglio più importante che posso offrire sia che, se al momento non utilizzi alcuna forma del modello di visualizzazione / controller, ti incoraggio vivamente a iniziare a farlo. Ti costringe ad apprendere e ad apprezzare i vantaggi del loose-couping e della separazione dei livelli.

risposta al tuo aggiornamento

Quale layer dovrebbe memorizzare cose come, l'ultimo pulsante premuto, in modo che possa tenerlo evidenziato per l'utente ...

È possibile condividere lo stato tra le visualizzazioni in modo molto simile a quanto si condividerebbe lo stato tra un relatore e la sua vista. Crea una classe speciale SharedViewState. Per semplicità puoi renderlo un singleton, o puoi istanziarlo nel presentatore principale e passarlo a tutti i punti di vista (tramite i loro presentatori) da lì. Quando lo stato è associato ai controlli, utilizzare il binding dei dati laddove possibile. La maggior parte delle proprietà Control può essere associata a dati. Ad esempio, la proprietà BackColor di un pulsante potrebbe essere associata a una proprietà della classe SharedViewState. Se esegui questa associazione su tutti i moduli che hanno pulsanti identici, puoi evidenziare Button1 su tutti i moduli solo impostando SharedViewState.Button1BackColor = someColor .

Se non hai familiarità con il databinding di WinForms, premi MSDN e fai qualche lettura. Non è difficile. Scopri INotifyPropertyChanged e 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");
            }
        }
    }
}
    
risposta data 15.12.2011 - 21:34
fonte
5

Gestisco una nuova applicazione WinForms / WPF basata sul pattern MVVM e Controlli di aggiornamento . È iniziato come WinForms, quindi ho creato una versione WPF a causa dell'importanza di marketing di un'interfaccia utente che sembra buona. Ho pensato che sarebbe stato un interessante vincolo progettuale mantenere un'applicazione che supportava due tecnologie dell'interfaccia utente completamente differenti con lo stesso codice di backend (viewmodels e modelli), e devo dire che sono abbastanza soddisfatto di questo approccio.

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

Poiché utilizzo il pattern MVVM, inserisco il maggior numero possibile di logica di programma nel ViewModel (incluso il Presentazione / Modello di navigazione ) o il Modello (a seconda che il codice sia correlato o meno all'interfaccia utente), ma poiché lo stesso ViewModel è utilizzato sia da una vista WinForms sia 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 visualizzazione.

Anche nel modello MVVM, gli oggetti dell'interfaccia utente dovrebbero evitare di interagire tra loro! Invece interagiscono con i modelli viewmodels. Ad esempio un TextBox non chiederebbe a un ListBox vicino quale elemento è selezionato; invece la listbox salverà un riferimento all'elemento attualmente selezionato da qualche parte nel livello viewmodel, e il TextBox interrogherà il livello viewmodel per scoprire cosa è selezionato in questo momento. Questo risolve il tuo problema di scoprire quale pulsante è stato inserito in una forma da una forma diversa. Devi solo 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 che è stato premuto.

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

Secondo me, questo approccio alla progettazione del programma funziona molto bene e ho intenzione di usarlo su progetti futuri. I motivi per cui funziona così bene sono (1) che Update Controls gestisce automaticamente le dipendenze e (2) che di solito è molto chiaro come si dovrebbe strutturare il codice: tutto il codice che interagisce con gli oggetti UI appartiene alla View, tutto il codice che è L'interfaccia utente, ma non deve interagire con gli oggetti UI, appartiene a ViewModel. Molto spesso dividi il tuo codice in due parti, una parte per la vista e un'altra per il ViewModel. Ho avuto qualche difficoltà nella progettazione di menu contestuali nel mio sistema, ma alla fine ho trovato anche un design per questo.

Ho anche delirante sui controlli di aggiornamento sul mio blog . Questo approccio richiede molto tempo per abituarsi, tuttavia, e nelle app su larga scala (ad esempio se le tue caselle di riepilogo contengono migliaia di elementi) potresti riscontrare problemi di prestazioni dovuti alle attuali limitazioni della gestione automatica delle dipendenze.

    
risposta data 17.12.2011 - 01:05
fonte
3

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

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

Comunque, ho avuto lo stesso problema di te: nella mia applicazione WinForms ho diverse forme che condividono alcune funzionalità e alcuni controlli. Inoltre, tutte le mie forme derivano già da un modulo di base (chiamiamolo "MyForm") che aggiunge funzionalità a livello di framework (indipendentemente dall'applicazione.) Il Designer di moduli di Visual Studio supporterà supporti di modifica di moduli che ereditano da altre forme, ma in pratica funziona solo fino a quando le tue forme non fanno altro che "Ciao, mondo! - OK - Annulla".

Quello che ho finito è questo: mantengo la mia classe base comune "MyForm", che è piuttosto complessa, e continuo a ricavarne tutti i moduli della mia applicazione. Tuttavia, non faccio assolutamente nulla in queste forme, quindi VS Designer di forme non ha problemi a modificarle. Queste forme sono costituite esclusivamente dal codice generato da Forms Designer. Quindi, ho una gerarchia parallela separata di oggetti che chiamo "Surrogates" che contengono tutte le funzionalità specifiche dell'applicazione, come l'inizializzazione dei controlli, la gestione degli eventi generati dal modulo e dai controlli, ecc. Esiste un one-to-one corrispondenza tra le classi surrogate e le 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", e quindi viene derivata un'intera serie di altre classi surrogate, che corrispondono a ogni forma diversa visualizzata dalla mia applicazione.

Ogni surrogato accetta come parametro del tempo 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" della forma, così che quando la forma viene distrutta, la surrogata invoca su se stessa una sovrascrittura in modo tale che essa, e tutti i suoi discendenti, possano eseguire la pulizia. (Deregistrare dagli eventi, ecc.)

Fino ad ora ha funzionato bene.

    
risposta data 21.12.2011 - 15:11
fonte

Leggi altre domande sui tag