Condividi tramite


Architettura WPF

In questo argomento viene fornita una presentazione guidata della gerarchia di classi Windows Presentation Foundation (WPF). Illustra la maggior parte dei sottosistemi principali di WPF e descrive come interagiscono. Descrive anche alcune delle scelte effettuate dagli architetti di WPF.

System.Object

Il modello di programmazione WPF primario viene esposto tramite codice gestito. All'inizio della fase di progettazione di WPF ci sono stati diversi dibattiti su dove la linea dovrebbe essere tracciata tra i componenti gestiti del sistema e quelli non gestiti. CLR offre una serie di funzionalità che rendono lo sviluppo più produttivo e affidabile (tra cui gestione della memoria, gestione degli errori, sistema di tipi comuni e così via), ma sono a un costo.

I componenti principali di WPF sono illustrati nella figura seguente. Le sezioni rosse del diagramma (PresentationFramework, PresentationCore e milcore) sono le parti di codice principali di WPF. Di questi, solo uno è un componente non gestito– milcore. Milcore viene scritto in codice non gestito per consentire una stretta integrazione con DirectX. Tutta la visualizzazione in WPF è fatta tramite il motore DirectX, consentendo un rendering efficiente sia di hardware che di software. WPF ha anche richiesto un controllo fine sulla memoria e l'esecuzione. Il motore di composizione in milcore è estremamente sensibile alle prestazioni ed è necessario rinunciare a molti vantaggi del CLR per migliorare le prestazioni.

Posizione di WPF all'interno di .NET Framework.

La comunicazione tra le parti gestite e non gestite di WPF viene descritta più avanti in questo argomento. Il resto del modello di programmazione gestito è descritto di seguito.

System.Threading.DispatcherObject

La maggior parte degli oggetti in WPF deriva da DispatcherObject, che fornisce i costrutti di base per gestire la concorrenza e il threading. WPF si basa su un sistema di messaggistica implementato dal dispatcher. Questo funziona molto come la nota pompa di messaggi Win32; Infatti, il dispatcher WPF usa messaggi User32 per l'esecuzione di chiamate tra thread.

Esistono due concetti fondamentali da comprendere quando si discute di concorrenza in WPF, ovvero il dispatcher e l'affinità di thread.

Durante la fase di progettazione di WPF, l'obiettivo era passare a un modello con un singolo thread di esecuzione ma non vincolato a un thread specifico. L'affinità thread si verifica quando un componente usa l'identità del thread in esecuzione per archiviare un tipo di stato. Il formato più comune di questo consiste nell'usare l'archivio locale del thread (TLS) per archiviare lo stato. L'affinità di thread richiede che ogni thread logico di esecuzione sia di proprietà di un solo thread fisico nel sistema operativo, il che può richiedere molta memoria. Alla fine, il modello di threading WPF è stato mantenuto sincronizzato con il modello di threading User32 esistente di esecuzione a thread singolo con affinità dei thread. Il motivo principale era l'interoperabilità: sistemi come OLE 2.0, gli Appunti e Internet Explorer richiedono l'esecuzione con affinità a thread singolo (STA).

Dato che si dispone di oggetti con threading STA, è necessario un modo per comunicare tra thread e verificare che si sia nel thread corretto. Qui risiede il ruolo del dispatcher. Il dispatcher è un sistema di base per la distribuzione dei messaggi, con più code con priorità. Esempi di messaggi includono notifiche di input grezze (spostamento del mouse), funzioni del framework (layout) o comandi utente (eseguire questo metodo). Quando si deriva da DispatcherObject, si crea un oggetto CLR che ha comportamento STA e verrà assegnato un puntatore a un dispatcher al momento della creazione.

System.Windows.DependencyObject

Una delle filosofie principali dell'architettura usata per la compilazione di WPF era una preferenza per le proprietà rispetto a metodi o eventi. Le proprietà sono dichiarative e consentono di specificare più facilmente la finalità anziché l'azione. In questo modo è supportato anche un sistema basato su modello o basato sui dati per la visualizzazione del contenuto dell'interfaccia utente. Questa filosofia ha avuto l'effetto previsto di creare più proprietà a cui è possibile associare, al fine di controllare meglio il comportamento di un'applicazione.

Per avere un sistema più orientato alle proprietà, era necessario un sistema di proprietà più completo rispetto a quello fornito dal CLR. Un semplice esempio di questa ricchezza è costituito da notifiche di modifica. Per abilitare l'associazione bidirezionale, entrambe le parti dell'associazione devono supportare la notifica delle modifiche. Per avere un comportamento associato ai valori delle proprietà, è necessario ricevere una notifica quando il valore della proprietà cambia. Microsoft .NET Framework dispone di un'interfaccia, INotifyPropertyChange, che consente a un oggetto di pubblicare notifiche di modifica, ma è facoltativo.

WPF offre un sistema di proprietà più avanzato, derivato dal tipo DependencyObject. Il sistema di proprietà è veramente un sistema di proprietà "dipendenza" in quanto tiene traccia delle dipendenze tra espressioni di proprietà e riconvalida automaticamente i valori delle proprietà quando le dipendenze cambiano. Ad esempio, se si dispone di una proprietà che eredita (come FontSize), il sistema viene aggiornato automaticamente se la proprietà cambia su un elemento padre di un elemento che eredita il valore.

La base del sistema di proprietà WPF è il concetto di espressione di proprietà. In questa prima versione di WPF, il sistema di espressioni di proprietà viene chiuso e tutte le espressioni vengono fornite come parte del framework. Le espressioni sono il motivo per cui il sistema di proprietà non dispone di data binding, stili o ereditarietà incorporati direttamente, ma sono invece forniti da livelli successivi all'interno del framework.

Il sistema di proprietà prevede anche un'archiviazione sparsa dei valori delle proprietà. Poiché gli oggetti possono avere decine (se non centinaia) di proprietà e la maggior parte dei valori è nello stato predefinito (ereditato, impostato da stili e così via), non tutte le istanze di un oggetto devono avere il peso completo di ogni proprietà definita.

L'ultima nuova funzionalità del sistema di proprietà è il concetto di proprietà associate. Gli elementi WPF sono basati sul principio della composizione e del riutilizzo dei componenti. Spesso è necessario che alcuni elementi contenenti (ad esempio un elemento di layout Grid) richiedano dati aggiuntivi sugli elementi figlio per controllarne il comportamento,ad esempio le informazioni Row/Column. Anziché associare tutte queste proprietà a ogni elemento, qualsiasi oggetto può fornire definizioni di proprietà per qualsiasi altro oggetto. Questo comportamento è simile alle funzionalità "expando" di JavaScript.

System.Windows.Media.Visual

Con un sistema definito, il passaggio successivo è ottenere i pixel visualizzati sullo schermo. La classe Visual fornisce la creazione di un albero di oggetti visivi, ognuno dei quali contiene facoltativamente istruzioni di disegno e metadati su come eseguire il rendering di tali istruzioni (ritaglio, trasformazione e così via). Visual è progettato per essere estremamente leggero e flessibile, quindi la maggior parte delle funzionalità non ha esposizione all'API pubblica e si basa principalmente su funzioni di callback protette.

Visual è davvero il punto di ingresso al sistema di composizione WPF. Visual è il punto di connessione tra questi due sottosistemi, l'API gestita e il milcore non gestito.

WPF visualizza i dati attraversando le strutture di dati non gestite dal milcore. Queste strutture, denominate nodi di composizione, rappresentano un albero di visualizzazione gerarchico con istruzioni di rendering in ogni nodo. Questo albero, illustrato sul lato destro della figura seguente, è accessibile solo tramite un protocollo di messaggistica.

Durante la programmazione di WPF, si creano gli elementi Visual e i tipi derivati, che comunicano internamente con l'albero di composizione tramite questo protocollo di messaggistica. Ogni Visual in WPF può creare uno, nessuno o più nodi di composizione.

struttura ad albero visuale di Windows Presentation Foundation.

C'è un dettaglio architetturale molto importante da notare qui: l'intero albero di elementi visivi e istruzioni di disegno viene memorizzato nella cache. In termini grafici, WPF usa un sistema di rendering persistente. In questo modo il sistema può eseguire il ripristino a frequenze di aggiornamento elevate senza il blocco del sistema di composizione sui callback al codice utente. Ciò aiuta a prevenire l'apparenza di un'applicazione che non risponde.

Un altro dettaglio importante che non è davvero evidente nel diagramma è il modo in cui il sistema esegue effettivamente la composizione.

In User32 e GDI il sistema funziona su un sistema di ritaglio in modalità immediata. Quando è necessario eseguire il rendering di un componente, il sistema stabilisce un limite di ritaglio all'esterno del quale il componente non è autorizzato a toccare i pixel e quindi viene richiesto al componente di disegnare i pixel in tale casella. Questo sistema funziona molto bene nei sistemi con vincoli di memoria perché quando qualcosa cambia è necessario toccare solo il componente interessato: non due componenti contribuiscono mai al colore di un singolo pixel.

WPF usa un modello di disegno chiamato "l'algoritmo del pittore". Ciò significa che invece di ritagliare ogni componente, a ogni componente viene richiesto di eseguire il rendering dalla parte posteriore alla parte anteriore dello schermo. Ciò consente a ogni componente di disegnare sullo schermo del componente precedente. Il vantaggio di questo modello è che è possibile avere forme complesse parzialmente trasparenti. Con l'hardware grafico moderno di oggi, questo modello è relativamente veloce (che non era il caso in cui è stato creato User32/ GDI).

Come accennato in precedenza, una filosofia di base di WPF consiste nel passare a un modello più dichiarativo incentrato sulle proprietà di programmazione. Nel sistema visivo, questo viene visualizzato in un paio di luoghi interessanti.

In primo luogo, se si considera il sistema grafico a modalità mantenuta, questo si sta realmente allontanando da un modello imperativo di tipo DrawLine/DrawLine verso un modello orientato ai dati – new Line()/new Line(). Questo passaggio al rendering basato sui dati consente di esprimere operazioni complesse sulle istruzioni di disegno usando le proprietà. I tipi derivati da Drawing sono effettivamente il modello a oggetti per il rendering.

In secondo luogo, se si valuta il sistema di animazione, si noterà che è quasi completamente dichiarativo. Invece di richiedere a uno sviluppatore di calcolare la posizione successiva o il colore successivo, puoi esprimere le animazioni come set di proprietà in un oggetto di animazione. Queste animazioni possono quindi esprimere la finalità dello sviluppatore o del progettista (spostare questo pulsante da qui a lì in 5 secondi) e il sistema può determinare il modo più efficiente per farlo.

System.Windows.UIElement

UIElement definisce sottosistemi principali, tra cui Layout, Input ed Eventi.

Il layout è un concetto di base in WPF. In molti sistemi è presente un set fisso di modelli di layout (HTML supporta tre modelli per il layout; flusso, assoluto e tabelle) o nessun modello per il layout (User32 supporta in realtà solo il posizionamento assoluto). WPF ha iniziato con il presupposto che gli sviluppatori e i progettisti volevano un modello di layout flessibile ed estendibile, che potrebbe essere guidato dai valori delle proprietà anziché dalla logica imperativa. A livello di UIElement, viene introdotto il contratto di base per il layout: un modello a due fasi con fasi Measure e Arrange.

Measure consente a un componente di determinare quanto spazio desidera occupare. Si tratta di una fase separata da Arrange perché esistono molte situazioni in cui un elemento padre chiederà a un elemento figlio di misurare più volte per determinare la posizione e le dimensioni ottimali. Il fatto che gli elementi padre chiedono agli elementi figlio di misurare dimostra un'altra filosofia chiave di WPF, ovvero le dimensioni del contenuto. Tutti i controlli in WPF supportano la capacità di adattarsi alle dimensioni naturali del loro contenuto. In questo modo la localizzazione risulta molto più semplice e consente il layout dinamico degli elementi man mano che le cose vengono ridimensionate. La fase Arrange consente a un padre di posizionare e determinare le dimensioni finali di ogni figlio.

Spesso si passa molto tempo a parlare del lato di output di WPF, Visual e oggetti correlati. Tuttavia, c'è una grande quantità di innovazione anche sul lato input. Probabilmente la modifica più fondamentale nel modello di input per WPF è il modello coerente in base al quale gli eventi di input vengono instradati attraverso il sistema.

L'input ha origine come segnale su un driver di dispositivo in modalità kernel e viene instradato al processo e al thread corretti tramite un processo complesso che coinvolge il kernel di Windows e User32. Dopo che il messaggio User32 corrispondente all'input viene indirizzato a WPF, viene convertito in un messaggio di input non elaborato WPF e inviato al dispatcher. WPF consente la conversione di eventi di input non elaborati in più eventi effettivi, consentendo l'implementazione di funzionalità come "MouseEnter" a un basso livello del sistema con recapito garantito.

Ogni evento di input viene convertito in almeno due eventi, ovvero un evento di "anteprima" e l'evento effettivo. Tutti gli eventi in WPF seguono il concetto di routing attraverso l'albero degli elementi. Gli eventi vengono detti "bolle" se attraversano da una destinazione verso l'alto l'albero alla radice e vengono detti "tunnel" se iniziano alla radice e attraversano verso il basso fino a una destinazione. Tunnel degli eventi di anteprima dell'input, offrendo a qualsiasi elemento dell'albero l'opportunità di filtrare o agire sull'evento. Gli eventi regolari (non di anteprima) risalgono dalla destinazione fino alla radice.

Questa suddivisione tra il tunnel e la fase bolla rende l'implementazione di funzionalità come gli acceleratori di tastiera funzionano in modo coerente in un mondo composito. In User32 si implementano i tasti di scelta rapida con una singola tabella globale contenente tutti gli acceleratori che si desidera supportare (ctrl+N mapping a "Nuovo"). Nel dispatcher della tua applicazione si chiamerebbe TranslateAccelerator, che analizzerebbe i messaggi di input in User32 e determinerebbe se un acceleratore registrato corrispondesse. In WPF questo non funzionerebbe perché il sistema è completamente "componibile" - qualsiasi elemento può gestire e usare qualsiasi acceleratore di tastiera. La presenza di questo modello in due fasi per l'input consente ai componenti di implementare il proprio "TranslateAccelerator".

Per compiere questo passo avanti, UIElement introduce anche la nozione di CommandBindings. Il sistema di comandi WPF consente agli sviluppatori di definire funzionalità in termini di endpoint del comando, un elemento che implementa ICommand. Le associazioni di comandi consentono a un elemento di definire un mapping tra un movimento di input (CTRL+N) e un comando (Nuovo). Entrambi i movimenti di input e le definizioni dei comandi sono estendibili e possono essere collegati insieme in fase di utilizzo. Ciò rende semplice, ad esempio, consentire a un utente finale di personalizzare le associazioni di tasti che vogliono usare all'interno di un'applicazione.

A questo punto dell'argomento, le funzionalità principali di WPF, ossia quelle implementate nell'assembly PresentationCore, sono state oggetto di attenzione. Durante lo sviluppo di WPF, una separazione pulita tra elementi fondamentali (ad esempio il contratto per il layout con Measure e Arrange) e le parti del framework (ad esempio l'implementazione di un layout specifico come Grid) rappresentava il risultato desiderato. L'obiettivo era quello di fornire un punto di estendibilità basso nello stack che consentiva agli sviluppatori esterni di creare i propri framework, se necessario.

System.Windows.FrameworkElement

FrameworkElement può essere esaminato in due modi diversi. Introduce un set di criteri e personalizzazioni nei sottosistemi introdotti in livelli inferiori di WPF. Introduce anche un set di nuovi sottosistemi.

Il criterio principale introdotto da FrameworkElement riguarda il layout dell'applicazione. FrameworkElement si basa sul contratto di layout di base introdotto da UIElement e aggiunge la nozione di uno "slot" di layout che permette agli autori di layout di avere un insieme coerente di semantiche di layout guidate dalle proprietà. Le proprietà come HorizontalAlignment, VerticalAlignment, MinWidthe Margin (per citarne alcuni) forniscono a tutti i componenti derivati da FrameworkElement comportamento coerente all'interno dei contenitori di layout.

FrameworkElement offre anche un'esposizione api più semplice a molte funzionalità disponibili nei livelli principali di WPF. Ad esempio, FrameworkElement fornisce accesso diretto all'animazione tramite il metodo BeginStoryboard. Un Storyboard consente di realizzare script per più animazioni su un set di proprietà.

Le due cose più importanti che FrameworkElement introduce sono data binding e stili.

Il sottosistema di data binding in WPF deve essere relativamente familiare a chiunque abbia usato Windows Form o ASP.NET per la creazione di un'interfaccia utente dell'applicazione. In ognuno di questi sistemi esiste un modo semplice per esprimere che si desidera associare una o più proprietà da un determinato elemento a una parte di dati. WPF offre il supporto completo per l'associazione di proprietà, la trasformazione e l'associazione di elenchi.

Una delle funzionalità più interessanti del data binding in WPF è l'introduzione di modelli di dati. I modelli di dati consentono di specificare in modo dichiarativo come visualizzare una parte di dati. Anziché creare un'interfaccia utente personalizzata che può essere associata ai dati, è invece possibile risolvere il problema e consentire ai dati di determinare la visualizzazione che verrà creata.

Lo stile è davvero una forma leggera di associazione di dati. Usando lo stile è possibile associare un set di proprietà da una definizione condivisa a una o più istanze di un elemento. Gli stili vengono applicati a un elemento tramite riferimento esplicito (impostando la proprietà Style) o in modo implicito associando uno stile al tipo CLR dell'elemento.

System.Windows.Controls.Control

La caratteristica più importante di Control è la creazione di modelli. Se si considera il sistema di composizione di WPF come un sistema di rendering in modalità trattenuta, la creazione di modelli consente a un controllo di descrivere il proprio rendering in maniera dichiarativa e parametrizzata. Un ControlTemplate non è in realtà altro che uno script per creare un set di elementi figlio, con associazioni alle proprietà offerte dal controllo.

Control fornisce un insieme di proprietà standard, come Foreground, Background, Padding, tra le altre, che gli autori dei modelli possono utilizzare per personalizzare la visualizzazione di un controllo. L'implementazione di un controllo fornisce un modello di dati e un modello di interazione. Il modello di interazione definisce un set di comandi (ad esempio Chiudi per una finestra) e le associazioni ai movimenti di input ( ad esempio facendo clic sulla X rossa nell'angolo superiore della finestra). Il modello di dati fornisce un set di proprietà per personalizzare il modello di interazione o personalizzare la visualizzazione (determinata dal modello).

Questa suddivisione tra il modello di dati (proprietà), il modello di interazione (comandi ed eventi) e il modello di visualizzazione (modelli) consente la personalizzazione completa dell'aspetto e del comportamento di un controllo.

Un aspetto comune del modello di dati dei controlli è il modello di contenuto. Se si esamina un controllo come Button, si noterà che ha una proprietà denominata "Content" di tipo Object. In Windows Form e ASP.NET questa proprietà è in genere una stringa, ma limita il tipo di contenuto che è possibile inserire in un pulsante. Il contenuto di un pulsante può essere una stringa semplice, un oggetto dati complesso o un intero albero di elementi. Nel caso di un oggetto dati, il modello di dati viene usato per costruire una visualizzazione.

Sommario

WPF è progettato per consentire di creare sistemi di presentazione dinamici basati sui dati. Ogni parte del sistema è progettata per creare oggetti tramite set di proprietà che determinano il comportamento dell'unità. Il data binding è una parte fondamentale del sistema ed è integrato a ogni livello.

Le applicazioni tradizionali creano una visualizzazione e quindi si associano ad alcuni dati. In WPF, tutto il controllo, ogni aspetto della visualizzazione, viene generato da un tipo di data binding. Il testo trovato all'interno di un pulsante viene visualizzato creando un controllo composto all'interno del pulsante e associandone la visualizzazione alla proprietà del contenuto del pulsante.

Quando si inizia a sviluppare applicazioni basate su WPF, dovrebbe risultare molto familiare. È possibile impostare proprietà, utilizzare oggetti e data bind nello stesso modo in cui è possibile usare Windows Form o ASP.NET. Con un'analisi più approfondita dell'architettura di WPF, si scopre che esiste la possibilità di creare applicazioni molto più avanzate che considerano fondamentalmente i dati come il driver principale dell'applicazione.

Vedere anche