Esiste una sorta di strategia sistematica per la progettazione e l'implementazione di GUI?


18

Sto usando Visual Studio per creare un'applicazione GUI in C #. La Casella degli strumenti funge da elegante tavolozza di componenti che mi consente di trascinare e rilasciare facilmente i pulsanti e altri elementi (per chiarezza dirò pulsante ogni volta che intendo "controllo") sul mio modulo, il che rende le forme statiche abbastanza facili da fare. Tuttavia, ho riscontrato due problemi:

  • La creazione dei pulsanti in primo luogo richiede molto lavoro. Quando ho un modulo che non è statico (es. Pulsanti o altri controlli vengono creati in fase di esecuzione in base a ciò che l'utente fa) non riesco affatto a usare la palette. Devo invece creare manualmente ciascun pulsante, chiamando il costruttore in qualsiasi metodo che sto utilizzando, quindi inizializzare manualmente specificando altezza, larghezza, posizione, etichetta, gestore di eventi e così via. Questo è estremamente noioso perché devo indovinare tutti questi parametri cosmetici senza essere in grado di vedere come apparirà il modulo e genera anche molte righe di codice ripetitivo per ogni pulsante.
  • Far fare qualcosa ai pulsanti è anche molto lavoro. Gestire gli eventi in un'applicazione completa è un dolore enorme. L'unico modo in cui so come fare è selezionare un pulsante, andare alla scheda eventi nelle sue proprietà, fare clic OnClicksull'evento in modo che generi l'evento nel Formcodice, quindi riempire il corpo dell'evento. Poiché desidero separare la logica e la presentazione, tutti i gestori di eventi finiscono per essere chiamate a linea singola verso la funzione di logica aziendale appropriata. Ma usarlo per molti pulsanti (ad esempio, immagina il numero di pulsanti presenti in un'applicazione come MS Word) inquina il mio codice Formcon dozzine di metodi di gestione degli eventi boilerplate ed è difficile mantenerlo.

Per questo motivo, qualsiasi programma di interfaccia grafica più complicato di Hello World è davvero poco pratico da realizzare per me. Per essere chiari, non ho alcun problema ad affrontare la complessità dei programmi che scrivo che hanno un'interfaccia utente minima: mi sento in grado di usare OOP con un discreto grado di competenza per strutturare in modo ordinato il mio codice di logica aziendale. Ma quando sviluppo la GUI, sono bloccato. Sembra così noioso che mi sento come se stessi reinventando la ruota, e c'è un libro da qualche parte là fuori che spiega come fare correttamente la GUI che non ho letto.

Mi sto perdendo qualcosa? O tutti gli sviluppatori C # accettano solo gli elenchi infiniti di gestori di eventi ripetitivi e codice di creazione di pulsanti?


Come suggerimento (si spera utile), mi aspetto che una buona risposta parlerà di:

  • Utilizzo di tecniche OOP (come il modello di fabbrica) per semplificare la creazione ripetuta di pulsanti
  • Combinando molti gestori di eventi in un unico metodo che controlla Senderper capire quale pulsante lo ha chiamato e si comporta di conseguenza
  • XAML e utilizzo di WPF invece di Windows Form

Non devi parlare di uno di questi, naturalmente. È solo la mia migliore ipotesi su quale tipo di risposta sto cercando.


Per come la vedo sì, Factory sarebbe un buon modello, ma vedo molto di più un modello Model-View-Controller con comandi collegati ai controlli invece che agli eventi direttamente. WPF è solitamente MVVM ma MVC è ben possibile. Al momento disponiamo di un enorme software in WPF che utilizza MVC al 90%. Tutti i comandi vengono inoltrati al controller e gestisce tutto
Franck,

Non puoi creare il modulo dinamico con tutti i pulsanti possibili con un editor GUI e rendere invisibili dinamicamente cose che non ti servono? Sembra molto meno lavoro.
Hans-Peter Störr,

@hstoerr Ma sarebbe difficile far funzionare il layout. Molti pulsanti si sovrappongono.
Superbo

Risposte:


22

Ci sono diverse metodologie che si sono evolute nel corso degli anni per affrontare questi problemi che hai citato, che sono, sono d'accordo, le due principali questioni che i framework UI hanno dovuto affrontare negli ultimi anni. Provenienti da un background WPF, questi vengono affrontati come segue:

Design dichiarativo, piuttosto che imperativo

Quando descrivi scrupolosamente la scrittura di codice per istanziare i controlli e impostarne le proprietà, stai descrivendo il modello imperativo della progettazione dell'interfaccia utente. Anche con il designer WinForms, stai solo usando un wrapper su quel modello: apri il file Form1.Designer.cs e vedi tutto quel codice seduto lì.

Con WPF e XAML - e modelli simili in altri framework, ovviamente, dall'HTML in poi - dovresti descrivere il tuo layout e lasciare che il framework faccia il duro lavoro per implementarlo. Utilizzi controlli più intelligenti come Pannelli (Griglia o WrapPanel, ecc.) Per descrivere la relazione tra gli elementi dell'interfaccia utente, ma l'aspettativa è che non li posizioni manualmente. Concetti flessibili come controlli ripetibili, come Repeater di ASP.NET o ItemsControl di WPF, aiutano a creare un'interfaccia utente con ridimensionamento dinamico senza scrivere codice ripetuto, consentendo alle entità di dati a crescita dinamica di essere rappresentate dinamicamente come controlli.

I DataTemplates di WPF ti consentono di definire - ancora una volta, dichiaratamente - piccole pepite di bontà dell'interfaccia utente e di abbinarle ai tuoi dati; ad esempio, un elenco di oggetti dati del cliente potrebbe essere associato a ItemsControl e un modello di dati diverso invocato a seconda che si tratti di un normale dipendente (utilizzare un modello di riga della griglia standard con nome e indirizzo) o un cliente principale, mentre un modello diverso , con una foto e pulsanti di facile accesso sono visualizzati. Ancora una volta, senza scrivere il codice nella finestra specifica , solo controlli più intelligenti che sono consapevoli del loro contesto di dati, e quindi ti permettono semplicemente di legarli ai tuoi dati e consentire loro di eseguire operazioni pertinenti.

Binding dei dati e separazione dei comandi

Toccando l'associazione dei dati, la banca dati di WPF (e, di nuovo, anche in altri framework, come AngularJS) ti consente di dichiarare il tuo intento collegando un controllo (diciamo, una casella di testo) con un'entità dati (diciamo, il nome del cliente) e lascia che la struttura gestisce l'impianto idraulico. La logica è gestita in modo simile. Invece di collegare manualmente i gestori di eventi code-behind alla logica aziendale basata sul controller, si utilizza il meccanismo di associazione dei dati per collegare il comportamento di un controller (ad esempio, la proprietà Command di un pulsante) a un oggetto Command che rappresenta il nugget di attività.

Consente a questo comando di essere condiviso tra le finestre senza riscrivere ogni volta i gestori di eventi.

Livello di astrazione più elevato

Entrambe queste soluzioni ai tuoi due problemi rappresentano un passaggio a un livello di astrazione più elevato rispetto al paradigma di Windows Form guidato dagli eventi che ritieni giustamente noioso.

L'idea è che non si desidera definire, nel codice, ogni singola proprietà che ha il controllo e ogni singolo comportamento a partire dal pulsante e in poi. Volete che i vostri controlli e il framework sottostante facciano più lavoro per voi e vi permettano di pensare a concetti più astratti di Data Binding (che esiste in WinForms, ma non è affatto utile come in WPF) e il modello Command per definire i collegamenti tra l'interfaccia utente e comportamenti che non richiedono di arrivare al metal.

Il modello MVVM è l'approccio di Microsoft a questo paradigma. Suggerisco di leggerlo.

Questo è un esempio approssimativo di come apparirebbe questo modello e di come risparmierebbe tempo e righe di codice. Questo non verrà compilato, ma è pseudo-WPF. :)

Da qualche parte nelle risorse dell'applicazione, si definiscono i modelli di dati:

<DataTemplate x:DataType="Customer">
   <TextBox Text="{Binding Name}"/> 
</DataTemplate>

<DataTemplate x:DataType="PrimeCustomer">
   <Image Source="GoldStar.png"/>
   <TextBox Text="{Binding Name}"/> 
</DataTemplate>

Ora collega la schermata principale a un ViewModel, una classe che espone i tuoi dati. Ad esempio, una raccolta di clienti (semplicemente a List<Customer>) e un oggetto Command (di nuovo, una semplice proprietà pubblica di tipo ICommand). Questo collegamento consente l'associazione:

public class CustomersnViewModel
{
     public List<Customer> Customers {get;}
     public ICommand RefreshCustomerListCommand {get;}  
}

e l'interfaccia utente:

<ListBox ItemsSource="{Binding Customers}"/>
<Button Command="{Binding RefreshCustomerListCommand}">Refresh</Button>

E questo è tutto. La sintassi di ListBox estrarrà l'elenco dei clienti da ViewModel e tenterà di renderli nell'interfaccia utente. A causa dei due modelli di dati che abbiamo definito in precedenza, importerà il modello pertinente (basato sul tipo di dati, presupponendo che PrimeCustomer erediti dal cliente) e lo inserisca come contenuto di ListBox. Nessun loop, nessuna generazione dinamica di controlli tramite codice.

Allo stesso modo, Button ha una sintassi preesistente per collegare il suo comportamento a un'implementazione di ICommand, che presumibilmente sa aggiornare la Customersproprietà, spingendo il framework di associazione dei dati ad aggiornare nuovamente l'interfaccia utente, automaticamente.

Ho preso alcune scorciatoie qui, ovviamente, ma questa è la sostanza.


5

Innanzitutto, raccomando questa serie di articoli: http://codebetter.com/jeremymiller/2007/07/26/the-build-your-own-cab-series-table-of-contents/ - non risolve i problemi di dettaglio con il codice del pulsante, ma offre una strategia sistematica per la progettazione e l'implementazione del proprio framework applicativo, in particolare per le applicazioni GUI, che mostra come applicare MVC, MVP, MVVM alla tipica applicazione Forms.

Affrontare la tua domanda sul "gestire gli eventi": se hai molti moduli con pulsanti che funzionano in modo simile, probabilmente pagherà per implementare da solo il compito del gestore di eventi nel tuo "framework applicativo". Invece di assegnare gli eventi clic utilizzando la GUI Designer, creare una convenzione di denominazione su come un gestore "clic" deve essere nominato per un pulsante con un nome specifico. Ad esempio - per il pulsante MyNiftyButton_ClickHandlerper il pulsante MyNiftyButton. Quindi non è davvero difficile scrivere una routine riutilizzabile (usando la riflessione) che scorre su tutti i pulsanti di un modulo, controlla se il modulo contiene un gestore di clic relativo e assegna automaticamente il gestore all'evento. Vedi qui per un esempio.

E se vuoi usare solo un gestore di eventi per un gruppo di pulsanti, sarà anche possibile. Poiché ogni gestore di eventi ottiene il mittente passato e il mittente è sempre il pulsante che è stato premuto, è possibile inviare il codice commerciale utilizzando la proprietà "Nome" di ciascun pulsante.

Per la creazione dinamica di pulsanti (o altri controlli): è possibile creare pulsanti o altri controlli con il designer in un modulo non utilizzato solo allo scopo di creare un modello . Quindi, si utilizza la riflessione (vedere qui un esempio ) per clonare il controllo del modello e modificare solo le proprietà come posizione o dimensione. Sarà probabilmente molto più semplice che inizializzare manualmente due o tre dozzine di proprietà nel codice.

L'ho scritto sopra pensando a Winforms (proprio come tu, immagino), ma i principi generali dovrebbero applicarsi ad altri ambienti GUI come WPF con funzionalità simili.


In effetti è possibile farlo in WPF ed è persino molto più semplice. Basta compilare un elenco di comandi (funzioni specifiche precodificate) e impostare l'origine sulla finestra. Crei un bel modello visivo semplice e tutto ciò di cui hai bisogno è occuparti di riempire una raccolta con i comandi che desideri e l'eccezionale associazione WPF si prende cura di tutto il resto per te visivamente.
Franck,
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.