29 julho, 2011

Busca por controles na árvore visual de aplicações Silverlight e WPF

Ontem eu vi uma pergunta no fórum de Silverlight do MSDN sobre como achar todos os campos TextBox que existem dentro de uma ChildWindow. Já havia algumas respostas para a pergunta mas elas eram bem pontuais para aquele problema específico e necessitavam de várias suposições sobre a estrutura da aplicação para funcionar sem problemas (por exemplo, saber quais tipos de Panel estão sendo usados). Alguns anos atrás, quando eu comecei a fazer uma das minhas primeiras behaviors para publicar na galeria do Expression Blend, eu descobri uma classe do Silverlight (também existe no WPF) que serve justamente para permitir navergarmos na árvore visual de uma aplicação, tanto procurando controles filhos quanto pais de um determinado controle.

A behavior em questão é a que permitia que se fizesse scroll com a wheel do mouse (a rodinha) em controles que apresentassem scrollbar para aplicações feitas em Silverlight 3. Hoje essa behavior praticamente não é mais necessárias pois o Silverlight 4 já implementa esse comportamento nativamente, mas não era esse o caso na época. Para poder implementar essa função eu precisei criar um código que fosse capaz de ler toda a hierarquia visual do controle (vasculhando todos os componentes do qual o template do controle era composto), procurando por algum ScrollViewer. Se eu o encontrasse, a behavior assinava os eventos necessários do controle para que o scroll funcionasse como esperado.

Para navegar pela árvore visual, a classe que utilizei foi a VisualTreeHelper. Voltando à dúvida do fórum, eu decidi criar um método genérico que fosse capaz de encontrar todos os controles de um determinado tipo em uma hierárquia utilizando essa classe, assim estaria garantindo que não precisaria ficar colocando “if”s para cada tipo de painel diferente que aparecesse na minha frente. Sem mais delongas, segue abaixo o método que eu fiz.

public static T[] SearchUIElements<T>(UIElement root, int maxlevel = int.MaxValue, int level = 0) 
   
where T : UIElement
{

   
var result = new List
<T>();

   
if (root != null
) {
       
if (root is
T) {
            result.Add(root
as
T);
        }

       
if
(level < maxlevel) {
           
var childrencount = VisualTreeHelper
.GetChildrenCount(root);
           
DependencyObject
child;
           
for (var
i = 0; i < childrencount; i++) {
                child =
VisualTreeHelper
.GetChild(root, i);
               
if (child is UIElement
) {
                    result.AddRange(SearchUIElements<T>(child
as UIElement
, maxlevel, level + 1));
                }
            }
        }
    }

   
return result.ToArray();
}

Como vocês podem ver o método não é grande e é bem simples. Ele aceita 3 parâmetros:



  • root: controle raiz a partir de onde será iniciada a busca. Por exemplo: LayoutRoot.
  • maxlevel: número máximo de níveis que a busca irá “descer” nos descendentes. Este parâmetro é opcional e o seu valor padrão é int.MaxValue, garantindo que será lida a hierarquia inteira a partir do ponto inicial.
  • level: nível atual da busca. Esse parâmetro é utilizado apenas pela própria função para controlar quando a busca atingirá o nível máximo solicitado pelo usuário.

A função é genérica. O parâmetro T serve para indicar qual tipo de controle será procurado, assim como permitir que o retorno sejá tipado corretamente. A é executada de forma recursiva, chamando a si mesma para cada novo ítem na hierarquia.


O resultado da função é sempre um array do tipo de controle solicitado. Esta função sempre retorna uma array, mesmo que seja vazio (não será retornado null).


Abaixo temos um xaml de exemplo e algumas chamada à função com a descrição do que será encontrado em cada caso.

<Grid x:Name="LayoutRoot">
    <TextBox />
    <TextBox />
    <Grid>
        <TextBox />
        <TextBox />
        <Grid>
            <TextBox />
        </Grid>
    </Grid>
</Grid>
//acha TODOS os 5 campos TextBox 
var textboxes = SearchUIElements<TextBox
>(LayoutRoot);

//acha apenas os 2 campos TextBox de LayoutRoot
textboxes = SearchUIElements<TextBox
>(LayoutRoot, 1);

//acha 4. Os 2 acima e os 2 que estão no primeiro Grid filho
textboxes = SearchUIElements<TextBox
>(LayoutRoot, 2);

//acha TODOS os Grids a partir de LayoutRoot, inclusive ele mesmo
var grids = SearchUIElements<Grid>(LayoutRoot);        

Agora que eu já mostrei como faz, você acha que consegue fazer uma função semelhante que navegue ao contrário na hierárquia? (procurando nos pais de um controle até chegar na raíz da aplicação…). Fica o desafio. Winking smile

Nenhum comentário: