Arquitetura 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. Destes, apenas um é 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. O WPF também exigia um bom controle sobre a memória e a execução. O motor de composição em milcore é extremamente sensível ao desempenho, e precisou abrir mão de muitas vantagens do CLR para ganhar desempenho.
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 gerenciada é descrito abaixo.
System.Threading.DispatcherObject
A maioria dos objetos do WPF deriva de DispatcherObject, que fornece as construções básicas para lidar com simultaneidade e encadeamento. O WPF é baseado em um sistema de mensagens implementado pelo dispatcher. Isso funciona muito como a conhecida bomba de mensagens Win32; na verdade, o dispatcher do WPF usa mensagens User32 para executar chamadas cross thread.
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 modelo de execução em um único encadeamento, mas não um modelo dependente de encadeamento. A afinidade de thread acontece quando um componente usa a identidade do thread de execução para armazenar algum tipo de estado. A forma mais comum disso é usar o armazenamento local de thread (TLS) para armazenar o estado. A afinidade de thread requer que cada thread lógico de execução seja de propriedade de apenas um thread físico no sistema operacional, que pode se tornar intensivo em memória. No final, o modelo de threading do WPF foi mantido em sincronia com o modelo de threading User32 existente de execução mono-thread 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 com afinidade a um único encadeamento (STA).
Dado que você tem objetos com threading STA, você precisa de uma maneira de se comunicar entre threads e validar que você está no thread correto. Aqui reside o papel do despachante. O dispatcher é um sistema básico de envio de mensagens, com várias filas priorizadas. Exemplos de mensagens incluem notificações de entrada em bruto (movimento do rato), funções de estrutura de software (layout) ou comandos de usuário (executar este método). Derivando de DispatcherObject, você cria um objeto CLR que tem comportamento STA e receberá um ponteiro para um dispatcher no momento da criaçã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. As propriedades são declarativas e permitem que você especifique mais facilmente a intenção em vez da ação. Isso também suportava um sistema orientado por modelo, ou orientado por dados, para exibir o conteúdo da interface do usuário. Essa filosofia tinha o efeito pretendido de criar mais propriedades às quais você poderia se ligar, a fim de 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 que o CLR fornece. Um exemplo simples dessa riqueza são as notificações de alteração. Para habilitar a vinculação bidirecional, você precisa de ambos os lados da associação para dar suporte à notificação de alteração. Para ter um comportamento vinculado aos valores da propriedade, você precisa ser notificado quando o valor da propriedade mudar. 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 tipo DependencyObject. O sistema de propriedades é realmente um sistema de propriedades de "dependência", pois rastreia dependências entre expressões de propriedade e revalida automaticamente os valores de propriedade quando as dependências mudam. Por exemplo, se tiveres uma propriedade que herda (como FontSize), o sistema será automaticamente atualizado se a propriedade for alterada num elemento 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 vinculação de dados, estilo ou herança codificados, mas sim fornecidos por camadas posteriores dentro da estrutura.
O sistema de propriedades também prevê armazenamento esparso de valores de propriedade. Como os objetos podem ter dezenas (se não centenas) de propriedades, e a maioria dos valores está em seu estado padrão (herdado, definido por estilos, etc.), nem todas as instâncias de um objeto precisam ter o peso total de cada propriedade definida nele.
A última novidade do sistema de propriedade é a noção 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 (tal como um elemento de layout Grid) precisa de dados adicionais sobre elementos-filho para controlo do seu comportamento (tal como as informações de linha/coluna). Em vez de associar todas essas propriedades a 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, o próximo passo é fazer com que os pixéis sejam desenhados no ecrã. A classe Visual 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 à API pública e depende fortemente de funções de retorno de chamada protegidas.
Visual é realmente o ponto de entrada para o sistema de composição do WPF. Visual é o ponto de conexão entre esses dois subsistemas, a API gerenciada e o milcore não gerenciado.
O WPF exibe dados percorrendo 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ó. Esta árvore, ilustrada no lado direito da figura abaixo, só é acessível através de um protocolo de mensagens.
Ao programar o WPF, você cria elementos Visual e tipos derivados que se comunicam internamente com a árvore de composição por meio desse protocolo de mensagens. Cada Visual no WPF pode criar um, nenhum ou vários nós de composição.
Há um detalhe arquitetônico muito importante a ser notado aqui – toda a árvore de visuais e instruções de desenho é armazenada em cache. Em termos gráficos, o WPF usa um sistema de renderização retido. Isso permite que o sistema faça a repintura em altas taxas de atualização sem que o sistema de composição interfira nos callbacks para o código do utilizador. Isso ajuda a evitar o aparecimento de um aplicativo que não responde.
Outro detalhe importante que não é realmente percetível no diagrama é como o sistema realmente executa a composição.
Em User32 e GDI, o sistema funciona em um sistema de clipping de modo imediato. Quando um componente precisa ser renderizado, o sistema estabelece limites de corte fora dos quais o componente não pode tocar nos pixels, e então o componente é instruído a pintar os pixels dentro dessa caixa. Este sistema funciona muito bem em sistemas com restrição de memória porque quando algo muda você só precisa tocar no componente afetado – nenhum componente contribui para a cor de um único pixel.
WPF usa um modelo de pintura "algoritmo do pintor". Isso significa que, em vez de recortar cada componente, cada componente é solicitado a renderizar de trás para a frente da tela. Isso permite que cada componente pinte sobre a exibição do componente anterior. A vantagem deste modelo é que você pode ter formas complexas e 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 é mudar para um modelo de programação mais declarativo e "centrado na propriedade". No sistema visual, isso aparece em alguns lugares interessantes.
Primeiro, se você pensar sobre o sistema gráfico de modo retido, isso está realmente se afastando de um modelo imperativo do tipo DrawLine/DrawLine para um modelo orientado a dados – new Line()/new Line(). Essa mudança para a renderização orientada por dados permite que operações complexas nas instruções de desenho sejam expressas usando propriedades. Os tipos derivados de Drawing são efetivamente o modelo de 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 então expressar a intenção do desenvolvedor ou designer (mova esse botão daqui para lá em 5 segundos), e o sistema pode determinar a maneira mais eficiente de realizar isso.
System.Windows.UIElement
UIElement define os subsistemas principais, incluindo Layout, Entrada e Eventos.
Layout é um conceito central no WPF. Em muitos sistemas, há um conjunto fixo de modelos de layout (HTML suporta três modelos para layout; fluxo, absoluto e tabelas) ou nenhum modelo para layout (User32 realmente só suporta 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 impulsionado por valores de propriedade em vez de lógica imperativa. No nível UIElement, é introduzido o contrato básico de layout – um modelo bifásico com passagens Measure e Arrange.
Measure permite que um componente determine quanto tamanho ele gostaria de ter. Esta é uma fase separada da Arrange porque há muitas situações em que um elemento pai pedirá a um elemento filho para medir várias vezes, a fim de determinar a sua posição e tamanho ideais. O fato de os elementos pai pedirem aos elementos filho para medir demonstra outra filosofia chave do WPF – tamanho para conteúdo. Todos os controles no WPF suportam a capacidade de dimensionar para o tamanho natural de seu conteúdo. Isso torna a localização muito mais fácil e permite o layout dinâmico dos elementos à medida que as coisas são redimensionadas. A fase Arrange permite que os pais posicionem e determinem 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, há uma enorme quantidade de inovação também do lado dos insumos. 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 origina-se a partir de um sinal num driver de dispositivo no modo kernel e é encaminhada para o processo e o thread corretos através de um processo intrincado envolvendo o núcleo do Windows e o User32. Uma vez que a mensagem User32 correspondente à entrada é roteada para WPF, ela é convertida em uma mensagem de entrada bruta WPF e enviada para o dispatcher. O WPF permite que eventos de entrada brutos 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 encaminhamento através da hierarquia de elementos. Diz-se que os eventos "borbulham" se se deslocam de um alvo até à raiz, e diz-se que "tunnelam" se começam na raiz e descem até um alvo. Canalização de eventos de pré-visualização de entrada, permitindo que qualquer elemento na árvore tenha a oportunidade de filtrar ou executar ações no evento. Os eventos regulares (sem visualização) propagam-se então do destino até à raiz.
Essa divisão entre a fase de túnel e bolha faz com que a implementação de recursos como aceleradores de teclado funcione de forma consistente em um mundo composto. Em User32 você implementaria aceleradores de teclado tendo uma única tabela global contendo todos os aceleradores que você queria suportar (Ctrl+N mapeando para "Novo"). No dispatcher do seu aplicativo, você chamaria TranslateAccelerator que detetaria as mensagens de entrada no User32 e determinaria se alguma correspondia a um acelerador registrado. No WPF, isso não funcionaria porque o sistema é totalmente "composable" - qualquer elemento pode manipular e usar qualquer acelerador de teclado. Ter este modelo de duas fases para 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 ICommand. As associações de comandos permitem que um elemento defina um mapeamento entre um gesto de entrada (Ctrl+N) e um comando (Novo). Os gestos de entrada e as definições de comando são extensíveis e podem ser conectados juntos no momento do uso. Isso torna trivial, por exemplo, permitir que um usuário final personalize as associações de chave que deseja usar em um aplicativo.
Até este ponto do tópico, os recursos "principais" do WPF, recursos implementados na biblioteca PresentationCore, foram o foco. Ao construir o WPF, uma separação clara entre peças fundacionais (como o contrato de layout com Measure e Arrange) e peças de estrutura (como a implementação de um layout específico como Grid) foi o resultado desejado. O objetivo era fornecer um ponto de extensibilidade baixo na pilha que permitisse que desenvolvedores externos criassem suas próprias estruturas, se necessário.
System.Windows.FrameworkElement
FrameworkElement pode ser encarado de duas maneiras diferentes. Ele introduz um conjunto de políticas e personalizações nos subsistemas introduzidos nas camadas inferiores do WPF. Introduz igualmente um conjunto de novos subsistemas.
A principal política introduzida pelo 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 facilita para os autores de layout terem um conjunto consistente de semânticas de layout orientadas por propriedades. Propriedades como HorizontalAlignment, VerticalAlignment, MinWidthe Margin (para citar alguns) fornecem todos os componentes derivados de FrameworkElement comportamento consistente dentro de contêineres de layout.
FrameworkElement também fornece uma exposição mais fácil da API a muitos recursos encontrados nas camadas principais do WPF. Por exemplo, FrameworkElement fornece acesso direto à animação através do método BeginStoryboard. Um Storyboard fornece uma maneira de programar várias animações para 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 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 que você deseja que uma ou mais propriedades de um determinado elemento sejam vinculadas a uma parte de dados. O WPF tem suporte total para vinculação de propriedade, transformação e vinculação de lista.
Um dos recursos mais interessantes da vinculação de dados no WPF é a introdução de modelos de dados. Os modelos de dados permitem especificar declarativamente como uma parte dos dados deve ser visualizada. Em vez de criar uma interface de usuário personalizada que pode ser vinculada aos dados, você pode reverter o problema e deixar que os dados determinem a exibição que será criada.
O estilo é realmente uma forma leve de vinculação de dados. Usando estilo, você pode vincular 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 propriedade Style) ou implicitamente associando um estilo ao tipo CLR do elemento.
System.Windows.Controls.Control
A funcionalidade mais significativa do controlo é a modelagem. Se você pensar no sistema de composição do WPF como um sistema de renderização de modo retido, a modelagem permite que um controle descreva sua renderização de maneira parametrizada e declarativa. Um ControlTemplate nada mais é do que um script para criar um conjunto de elementos filho, com ligações às propriedades oferecidas pelo controle.
Control fornece um conjunto de propriedades de estoque, Foreground, Background, Padding, para citar algumas, que os autores do modelo podem usar para personalizar a exibição de um controle. A implementação de um controle fornece um modelo de dados e um modelo de interação. O modelo de interação define um conjunto de comandos (como Fechar para uma janela) e ligações a 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 personalizar a exibição (determinada 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 personalização completa da aparência e do comportamento de um controle.
Um aspeto comum do modelo de dados de 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 é projetada para criar objetos por meio de conjuntos de propriedades que impulsionam o comportamento. A vinculação de dados é uma parte fundamental do sistema e está integrada em todas as camadas.
Os aplicativos tradicionais criam uma exibição e, em seguida, vinculam-se a alguns dados. No WPF, tudo sobre o controle, todos os aspetos da exibição, são gerados por algum tipo de ligação de dados. O texto encontrado dentro de um botão é exibido criando um controlo composto dentro do botão e associando a sua exibição à propriedade conteúdo do botão.
Quando tu começas a desenvolver aplicações baseadas em WPF, o processo deve parecer muito familiar. Você pode definir propriedades, usar objetos e vincular dados da mesma forma 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 fundamentalmente tratam os dados como o driver principal do aplicativo.
Ver também
- Visual
- UIElement
- ICommand
- FrameworkElement
- DispatcherObject
- CommandBinding
- Control
- Visão geral da vinculação de dados
- Layout
- Visão geral da animação
.NET Desktop feedback