Arquitetura do WPF

Este tópico fornece um tour guiado da hierarquia de classes do Windows Presentation Foundation (WPF). Ele cobre a maioria dos principais subsistemas do WPF e descreve como eles interagem. Ele também detalha algumas das escolhas feitas pelos arquitetos do WPF.

System.Object

O modelo de programação WPF primário é exposto por meio de código gerenciado. No início da fase de projeto do WPF, houve uma série de debates sobre onde a linha deveria ser traçada entre os componentes gerenciados do sistema e os não gerenciados. O CLR fornece uma série de recursos que tornam o desenvolvimento mais produtivo e robusto (incluindo gerenciamento de memória, tratamento de erros, sistema de tipo comum, etc.), mas eles têm um custo.

Os principais componentes do WPF são ilustrados na figura abaixo. As seções vermelhas do diagrama (PresentationFramework, PresentationCore e milcore) são as principais partes de código do WPF. Dessas, somente uma é um componente não gerenciado – milcore. O Milcore é escrito em código não gerenciado para permitir uma integração total com o DirectX. Toda a exibição no WPF é feita através do mecanismo DirectX, permitindo uma renderização eficiente de hardware e software. WPF também exigia controle fino sobre a memória e execução. O motor de composição em milcore é extremamente sensível ao desempenho, e necessário abrir mão de muitas vantagens do CLR para ganhar desempenho.

The position of WPF within the .NET Framework.

A comunicação entre as partes gerenciadas e não gerenciadas do WPF é discutida posteriormente neste tópico. O restante do modelo de programação gerenciado é descrito abaixo.

System.Threading.DispatcherObject

A maioria dos objetos no WPF deriva de DispatcherObject, que fornece as construções básicas para lidar com simultaneidade e threading. WPF é baseado em um sistema de mensagens implementado pelo despachante. Isso funciona muito como a bomba de mensagem Win32 familiar; na verdade, o dispatcher WPF usa mensagens User32 para executar chamadas de thread cruzado.

Há realmente dois conceitos centrais para entender ao discutir a simultaneidade no WPF - o dispatcher e a afinidade de thread.

Durante a fase de design do WPF, o objetivo era passar para um único thread de execução, mas um modelo "afinizado" sem thread. A afinidade de thread ocorre quando um componente usa a identidade do thread em execução para armazenar algum tipo de estado. A forma mais comum disso é usar o armazenamento local de thread (TLS) para armazenar o estado. Afinidade de threads requer que cada thread lógica de execução pertença a apenas um thread físico no sistema operacional, que pode passar a fazer uso intensivo de memória. No final, o modelo de threading do WPF foi mantido em sincronização com o modelo de threading User32 existente, de execução em thread único com afinidade de thread. A principal razão para isso foi a interoperabilidade – sistemas como o OLE 2.0, a área de transferência e o Internet Explorer exigem execução STA (Single Thread Affinity).

Considerando que você tem objetos com threading STA, você precisa de uma maneira para se comunicar entre threads e validar que você está no thread correto. Eis aí a função do dispatcher. O dispatcher é um sistema básico de despacho de mensagens, com várias filas priorizadas. Exemplos de mensagens incluem notificações de entrada bruta (o mouse foi movido), funções de estrutura (layout) ou comandos do usuário (executar este método). Derivando de , você cria um objeto CLR que tem comportamento STA e receberá um ponteiro para um dispatcher no momento da DispatcherObjectcriação.

System.Windows.DependencyObject

Uma das principais filosofias arquitetônicas usadas na construção do WPF foi a preferência por propriedades em vez de métodos ou eventos. Propriedades são declarativas e permitem que você especifique mais facilmente a intenção em vez de ação. Isso também deu suporte para um sistema controlado por modelo ou por dados para exibição do conteúdo de interface do usuário. Essa filosofia tinha o efeito desejado de criar mais propriedades do que o número ao qual você era capaz de se associar, de modo a controlar melhor o comportamento de um aplicativo.

Para ter mais do sistema orientado por propriedades, era necessário um sistema de propriedades mais rico do que o fornecido pelo CLR. Um exemplo simples desta sofisticação é encontrado nas notificações de alteração. Para habilitar a associação bidirecional, você precisa que ambos os lados da associação deem suporte à notificação de alteração. Para ter comportamento ligado aos valores de propriedade, você precisa ser notificado quando o valor da propriedade é alterado. O Microsoft .NET Framework tem uma interface, INotifyPropertyChange, que permite que um objeto publique notificações de alteração, no entanto, é opcional.

WPF fornece um sistema de propriedade mais rico, derivado do DependencyObject tipo. O sistema de propriedades é realmente um sistema de propriedade de "dependência", que controla as dependências entre expressões de propriedade e revalida automaticamente valores de propriedade quando as dependências são alteradas. Por exemplo, se você tiver uma propriedade que herda (como FontSize), o sistema será atualizado automaticamente se a propriedade for alterada em um pai de um elemento que herda o valor.

A base do sistema de propriedades WPF é o conceito de uma expressão de propriedade. Nesta primeira versão do WPF, o sistema de expressão de propriedade é fechado e as expressões são todas fornecidas como parte da estrutura. As expressões são o motivo pelo qual o sistema de propriedades não tem associação de dados, definição de estilo ou herança fixados no código, mas sim fornecidos por camadas posteriores dentro do framework.

O sistema de propriedades também fornece armazenamento esparso de valores de propriedade. Já que os objetos podem ter dezenas (se não centenas) de propriedades e a maioria dos valores estão em seu estado padrão (herdado, definido por estilos, etc.), nem toda instância de um objeto precisa ter o peso total de todas as propriedades definidas nele.

O último novo recurso do sistema de propriedades é o conceito de propriedades anexadas. Os elementos do WPF são construídos com base no princípio de composição e reutilização de componentes. Muitas vezes, algum elemento que contém (como um Grid elemento de layout) precisa de dados adicionais em elementos filho para controlar seu comportamento (como as informações de Linha/Coluna). Em vez de associar todas essas propriedades com cada elemento, qualquer objeto tem permissão para fornecer definições de propriedade para qualquer outro objeto. Isso é semelhante aos recursos "expando" do JavaScript.

System.Windows.Media.Visual

Com um sistema definido, a próxima etapa é fazer com que pixels sejam desenhados na tela. A Visual classe fornece a construção de uma árvore de objetos visuais, cada um opcionalmente contendo instruções de desenho e metadados sobre como renderizar essas instruções (recorte, transformação, etc.). Visual foi projetado para ser extremamente leve e flexível, portanto, a maioria dos recursos não tem exposição pública à API e depende muito de funções de retorno de chamada protegidas.

Visual é realmente o ponto de entrada para o sistema de composição WPF. Visual é o ponto de conexão entre esses dois subsistemas, a API gerenciada e o milcore não gerenciado.

O WPF exibe dados atravessando as estruturas de dados não gerenciadas gerenciadas pelo milcore. Essas estruturas, chamadas nós de composição, representam uma árvore de exibição hierárquica com instruções de renderização em cada nó. Essa árvore, ilustrada no lado direito da figura abaixo, só é acessível por meio de um protocolo de mensagens.

Ao programar o WPF, você cria Visual elementos e tipos derivados, que se comunicam internamente com a árvore de composição por meio desse protocolo de mensagens. Cada Visual um no WPF pode criar um, nenhum ou vários nós de composição.

The Windows Presentation Foundation Visual Tree.

Há um detalhe arquitetural muito importante a observar aqui – toda a árvore de elementos visuais e instruções de desenho é armazenada em cache. Em termos gráficos, o WPF usa um sistema de renderização retido. Isso permite ao sistema redesenhar com altas taxas de atualização sem o bloqueio do sistema de composição em retornos de chamada para o código do usuário. Isso ajuda a impedir o surgimento de um aplicativo que não responde.

Outro detalhe importante que não é realmente perceptível no diagrama é o modo como o sistema realmente executa a composição.

No User32 e GDI, o sistema funciona em um sistema de clipping de modo imediato. Quando um componente precisa ser renderizado, o sistema estabelece os limites de recorte fora dos quais o componente não tem permissão para tocar nos pixels e, em seguida, é solicitado ao componente que ele pinte os pixels dentro dessa caixa. Este sistema funciona muito bem em sistemas com restrição de memória porque quando algo é alterado, você só tem que tocar o componente afetado – não há jamais dois componentes que contribuam para a cor de um único pixel.

WPF usa um modelo de pintura "algoritmo do pintor". Isso significa que, em vez de cortar cada componente, é solicitado a cada componente que renderize da parte de trás da exibição para a parte da frente. Isso permite que cada componente pinte sobre a exibição do componente anterior. A vantagem desse modelo é que você pode ter formas complexas, parcialmente transparentes. Com o hardware gráfico moderno de hoje, este modelo é relativamente rápido (o que não era o caso quando User32 / GDI foram criados).

Como mencionado anteriormente, uma filosofia central do WPF é passar para um modelo de programação mais declarativo, "centrado na propriedade". No sistema visual, isso aparece em alguns locais interessantes.

Primeiro, se você pensar sobre o sistema gráfico de modo retido, isso estará realmente se movendo de um modelo de tipo DrawLine/DrawLine imperativo para um modelo orientado a dados – new Line()/new Line(). A mudança para a renderização controlada por dados permite que operações complexas nas instruções de desenho sejam expressas usando propriedades. Os tipos derivados de são efetivamente o modelo de Drawing objeto para renderização.

Em segundo lugar, se você avaliar o sistema de animação, verá que ele é quase completamente declarativo. Em vez de exigir que um desenvolvedor calcule o próximo local, ou a próxima cor, você pode expressar animações como um conjunto de propriedades em um objeto de animação. Essas animações podem expressar a intenção do desenvolvedor ou designer (mover esse botão daqui para lá em 5 segundos), e o sistema pode determinar a maneira mais eficiente de fazer isso.

System.Windows.UIElement

UIElement define os principais subsistemas, incluindo Layout, Input e Events.

Layout é um conceito central no WPF. Em muitos sistemas, há um conjunto fixo de modelos de layout (o HTML dá suporte a três modelos de layout; fluxo, absoluto e tabelas) ou nenhum modelo de layout (o User32 dá suporte somente a posicionamento absoluto). O WPF começou com a suposição de que os desenvolvedores e designers queriam um modelo de layout flexível e extensível, que pudesse ser orientado por valores de propriedade em vez de lógica imperativa. UIElement No nível, o contrato básico para layout é introduzido – um modelo de duas fases com Measure e Arrange passa.

Measure permite que um componente determine quanto tamanho ele gostaria de tomar. Esta é uma fase separada porque Arrange há muitas situações em que um elemento pai pedirá a uma criança para medir várias vezes para determinar sua posição e tamanho ideais. O fato de os elementos pai pedirem aos elementos filho para medir demonstra outra filosofia fundamental do WPF – tamanho para conteúdo. Todos os controles no WPF oferecem suporte à capacidade de dimensionar para o tamanho natural de seu conteúdo. Isso torna a localização muito mais fácil e permite layout dinâmico dos elementos conforme as coisas são redimensionadas. A Arrange fase permite que um pai posicione e determine o tamanho final de cada criança.

Muito tempo é frequentemente gasto falando sobre o lado de saída do WPF - Visual e objetos relacionados. No entanto, também há um tremendo montante de inovação no lado de entrada. Provavelmente, a mudança mais fundamental no modelo de entrada para WPF é o modelo consistente pelo qual os eventos de entrada são roteados através do sistema.

A entrada se origina como um sinal em um driver de dispositivo de modo kernel e é roteada para o processo e thread corretos por um processo intrincado, que envolve o kernel do Windows e o User32. Depois que a mensagem User32 correspondente à entrada é roteada para o WPF, ela é convertida em uma mensagem de entrada bruta do WPF e enviada ao despachante. O WPF permite que eventos de entrada bruta sejam convertidos em vários eventos reais, permitindo que recursos como "MouseEnter" sejam implementados em um nível baixo do sistema com entrega garantida.

Cada evento de entrada é convertido em pelo menos dois eventos – um evento de "visualização" e o evento real. Todos os eventos no WPF têm uma noção de roteamento através da árvore de elementos. Diz-se que os eventos "borbulham" se atravessam de um alvo até a árvore até a raiz, e dizem que "túnel" se começam na raiz e atravessam até um alvo. Eventos de visualização de entrada são roteados por túnel, permitindo que qualquer elemento na árvore tenha uma oportunidade de filtrar ou executar uma ação no evento. Os eventos regulares (não visualização) são então roteados por propagação do destino até a raiz.

Essa divisão entre as fases de túnel e de propagação torna a implementação de recursos como aceleradores de teclado funcionar de forma consistente em um mundo composto. No User32, você poderia implementar aceleradores de teclado tendo uma única tabela global contendo todos os aceleradores aos quais que você quisesse dar suporte (Ctrl + N mapeando para "New"). No dispatcher para seu aplicativo, você chamaria TranslateAccelerator, que rastrearia as mensagens de entrada no User32 e determinaria se havia correspondência entre alguma delas e um acelerador registrado. No WPF isso não funcionaria porque o sistema é totalmente "componível" – qualquer elemento pode lidar e usar qualquer acelerador de teclado. Esse modelo de duas fases para a entrada permite que os componentes implementem seu próprio "TranslateAccelerator".

Para levar isso um passo adiante, UIElement também introduz a noção de CommandBindings. O sistema de comando WPF permite que os desenvolvedores definam a funcionalidade em termos de um ponto final de comando – algo que implementa ICommando . Associações de comando permitem a um elemento definir um mapeamento entre um gesto de entrada (Ctrl + N) e um comando (New). Tanto os gestos de entrada quanto as definições de comando são extensíveis e podem ser ligados juntos durante o tempo de uso. Isso torna trivial, por exemplo, permitir que um usuário final personalize as associações de teclas que deseja usar em um aplicativo.

Até este ponto do tópico, os recursos "principais" do WPF – recursos implementados no assembly PresentationCore, têm sido o foco. Ao construir o WPF, uma separação clara entre peças fundamentais (como o contrato de layout com Measure and Arrange) e peças de estrutura (como a implementação de um layout específico como Grid) era o resultado desejado. A meta era fornecer um ponto de extensibilidade baixo na pilha que permitisse aos desenvolvedores externos criar suas próprias estruturas, se necessário.

System.Windows.FrameworkElement

FrameworkElement pode ser visto de duas maneiras diferentes. Ele introduz um conjunto de políticas e personalizações nos subsistemas introduzidos nas camadas inferiores do WPF. Ele também introduz um conjunto de novos subsistemas.

A principal política introduzida por FrameworkElement é em torno do layout do aplicativo. FrameworkElement baseia-se no contrato de layout básico introduzido por UIElement e adiciona a noção de um "slot" de layout que torna mais fácil para os autores de layout ter um conjunto consistente de semântica de layout orientada por propriedade. Propriedades como HorizontalAlignment, , MinWidthVerticalAlignmente Margin (para citar algumas) fornecem todos os componentes derivados do FrameworkElement comportamento consistente dentro de contêineres de layout.

FrameworkElement também fornece exposição de API mais fácil a muitos recursos encontrados nas camadas principais do WPF. Por exemplo, FrameworkElement fornece acesso direto à animação por meio do BeginStoryboard método. A Storyboard fornece uma maneira de criar scripts de várias animações em um conjunto de propriedades.

As duas coisas mais críticas que FrameworkElement introduz são a vinculação de dados e os estilos.

O subsistema de vinculação de dados no WPF deve ser relativamente familiar para qualquer pessoa que tenha usado o Windows Forms ou ASP.NET para criar uma interface do usuário (UI) do aplicativo. Em cada um desses sistemas, há uma maneira simples de expressar se você deseja que uma ou mais propriedades de um determinado elemento seja associada a uma parte dos dados. O WPF tem suporte completo para vinculação de propriedade, transformação e associação de lista.

Um dos recursos mais interessantes da vinculação de dados no WPF é a introdução de modelos de dados. Modelos de dados permitem que você especifique declarativamente como uma parte dos dados deve ser visualizada. Em vez de criar uma interface do usuário personalizada que pode ser associada a dados, você pode inverter o problema e permitir que os dados determinem a exibição que será criada.

Definição de estilo é na verdade uma forma leve de associação de dados. Usando estilização, você pode associar um conjunto de propriedades de uma definição compartilhada a uma ou mais instâncias de um elemento. Os estilos são aplicados a um elemento por referência explícita (definindo a Style propriedade) ou implicitamente associando um estilo ao tipo CLR do elemento.

System.Windows.Controls.Control

O recurso mais significativo do controle é a modelagem. Se você pensar sobre o sistema de composição do WPF como um sistema de renderização de modo retido, a modelagem permitirá que um controle descreva sua renderização de maneira declarativa e parametrizada. A ControlTemplate nada mais é do que um script para criar um conjunto de elementos filho, com ligações às propriedades oferecidas pelo controle.

ControlFornece um conjunto de propriedades de estoque, , , , para citar algumas, ForegroundBackgroundPaddingque os autores de modelo podem usar para personalizar a exibição de um controle. A implementação de um controle oferece um modelo de dados e um modelo de interação. O modelo de interação define um conjunto de comandos (como Close para uma janela) e associações para gestos de entrada (como clicar no X vermelho no canto superior da janela). O modelo de dados fornece um conjunto de propriedades para personalizar o modelo de interação ou então personalizar a exibição (determinado pelo modelo).

Essa divisão entre o modelo de dados (propriedades), o modelo de interação (comandos e eventos) e o modelo de exibição (modelos) permite a completa personalização da aparência e comportamento de um controle.

Um aspecto comum do modelo de dados dos controles é o modelo de conteúdo. Se você olhar para um controle como Button, verá que ele tem uma propriedade chamada "Content" do tipo Object. No Windows Forms e ASP.NET, essa propriedade normalmente seria uma cadeia de caracteres – no entanto, isso limita o tipo de conteúdo que você pode colocar em um botão. O conteúdo de um botão pode ser uma cadeia de caracteres simples, um objeto de dados complexo ou uma árvore de elementos inteira. No caso de um objeto de dados, o modelo de dados é usado para construir uma exibição.

Resumo

O WPF foi projetado para permitir que você crie sistemas de apresentação dinâmicos e orientados por dados. Cada parte do sistema foi projetada para criar objetos através de conjuntos de propriedades que controlam o comportamento. A associação de dados é uma parte fundamental do sistema e está integrada em todas as camadas.

Aplicativos tradicionais criam uma exibição e, em seguida, associam a alguns dados. No WPF, tudo sobre o controle, cada aspecto da exibição, é gerado por algum tipo de associação de dados. O texto encontrado em um botão é exibido por meio da criação de um controle composto dentro do botão e da associação de sua exibição à propriedade de conteúdo do botão.

Quando você começa a desenvolver aplicativos baseados em WPF, deve parecer muito familiar. Você pode definir propriedades, usar objetos e vincular dados da mesma maneira que pode usar o Windows Forms ou ASP.NET. Com uma investigação mais profunda sobre a arquitetura do WPF, você descobrirá que existe a possibilidade de criar aplicativos muito mais ricos que tratam fundamentalmente os dados como o driver principal do aplicativo.

Confira também