Clique aqui para ler este artigo em pdf ![]()
Clique aqui para ler todos os artigos desta edição
Data Binding de Radio Buttons em uma lista com Windows Forms
por Duncan Mackenzie
Recentemente, ouvi alguém fazer declarações céticas sobre um aplicativo Windows® Forms e se declarar surpreso porque "ele até usava data binding!" Isso me fez pensar na história do data binding nas últimas gerações de ferramentas de desenvolvimento. Lembrei-me de problemas relacionados às várias versões do Visual Basic® e do Microsoft® Access, que deram má reputação ao data binding. Os desenvolvedores, especialmente os mais experientes, geralmente vêem o data binding como um meio inflexível e não confiável de conectar uma IU aos dados, como uma tecnologia apropriada a demos, mas não indicada para uso no mundo real. Além dessas preocupações, um dos maiores problemas enfrentados pelos usuários do data binding é que ele implica uma conexão rígida entre os dados e as camadas de apresentação do aplicativo — um processo não apropriado em muitas situações.
Apesar de alguns desses problemas terem realmente ocorrido no passado, o data binding no Windows Forms foi projetado para evitar ou reduzir a maioria deles. A tecnologia data-binding no Microsoft .NET Framework é personalizável, flexível e (como ela permite criar vínculos com objetos, e não apenas com sistemas de bancos de dados) não requer que você associe sua interface diretamente ao banco de dados back-end. Graças a essas alterações, eu uso data binding sempre que possível, seja para vincular a um DataView ou a uma coleção strongly typed, em praticamente todos os meus projetos.
Com tantos problemas solucionados, posso me concentrar na minha lista de recursos desejados. O primeiro a ser examinado é a ausência de um controle data-bound que permitiria aos usuários selecionar a partir de uma lista de radio buttons. Neste artigo, pretendo descrever como você pode criar seu próprio controle para contornar essa omissão por parte das classes Windows Forms.
Por que não usar Radio Buttons em um painel?
O processo de Binding funciona por meio de uma associação a um único valor de propriedade em um controle, e não existe nenhum tipo de propriedade "selected value" em um grupo de radio buttons; desse modo, se quiser usar esse tipo de interface em seu aplicativo, você precisará tratá-la por conta própria. Ter um conjunto de controles que não seja vinculado a dados não é uma situação horrível, mas leva a casos especiais em seu código da camada de apresentação, onde você faz um loop uma vez para todos seus controles de vínculo e, em seguida, adiciona um pequeno fragmento de código especial para um ou mais grupos de radio buttons em sua interface. Em minha experiência, o código no qual você trata cada caso de forma diferente é difícil de manter e é a origem da maioria dos bugs.

Figura 1 RadioButtonList do ASP.NET
Uma solução melhor seria criar um tipo de controle completamente novo, o RadioButtonList, que seria o equivalente Windows Forms do controle RadioButtonList ASP.NET (consulte a Figura 1), contendo uma propriedade SelectedValue à qual você possa se vincular. Esse controle seria vinculado de duas maneiras: através de uma propriedade data source de sua lista de opções (o texto e os valores de todos os radio buttons) e por meio de uma propriedade SelectedValue para o valor que foi selecionado. Esse tipo de data binding é usado para todos os controles da lista data-bound no Windows Forms, inclusive o controle ListBox e ComboBox, por isso esse novo controle será consistente com o que está atualmente disponível.
Não reinvente a roda
Com o Visual Basic 6.0, você só tinha uma escolha ao desenvolver seu próprio controle; iniciar um User Control em branco e adicionar toda a funcionalidade de que precisava. Essa opção ainda existe no .NET, mas em geral ela não é a melhor escolha. Você deve tentar basear seu novo controle nos controles existentes e herdar de outro controle ou classe (em vez de Control ou UserControl). Evidentemente, nem sempre isso é possível, mas neste caso existe uma classe ListControl que fornece a implementação básica tanto para ComboBox como para ListBox, e ela dará ao meu novo controle RadioButtonList toda a funcionalidade data-binding de que necessito:
Public Class RadioButtonList
Inherits ListControl
•••
End Class
Embora ListControl ofereça enorme funcionalidade (incluindo o gerenciamento da fonte dos dados vinculados e a disponibilização de uma instância CurrencyManager com a qual interagir), eu ainda preciso escrever o código para lidar com algumas questões-chave. Em primeiro lugar, preciso criar o controle, o que significa criar uma série de radio buttons. Além disso, também preciso autorizar a interação do mouse e do teclado com o controle, incluindo o rastreamento de qual item na lista tem o foco (o que é diferente de qual item está selecionado). Por fim, preciso lidar com o redimensionamento e fornecer uma scrollbar (barra de rolagem) se o controle for pequeno para o número de itens na lista.
Criando os Radio Buttons
A primeira tarefa que irei abordar consiste no trabalho gráfico, porque enquanto isso não for feito eu não poderei criar nada na tela, e é aí que está toda a graça, não é? Em minha nova classe control, vamos substituir a rotina OnPaint e adicionar um loop para criar a lista de itens como fiz na Listagem 1.
Listagem 1 Desenhando Items
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
Dim myList As IList
Dim gr As Graphics = e.Graphics
gr.Clear(Me.BackColor)
ControlPaint.DrawBorder3D(gr, Me.DisplayRectangle, m_borderStyle)
gr.TranslateTransform(0, -1 * Me.m_currentTopPosition)
If Me.DataManager Is Nothing Then
myList = Nothing
Else
myList = Me.DataManager.List
Dim itemCount As Integer
itemCount = myList.Count
Dim i As Integer 'loop indexes
For i = 0 To itemCount - 1
DrawOneItem(i, gr)
Next
End If
End Sub
Percorrendo esse código de cima a baixo, você verá que a primeira etapa consiste em limpar o controle todo para remover quaisquer pixels decorrentes da criação de um conjunto anterior de botões ou de um controle de tamanho diferente. Feito isso, criei a borda, usando uma variável interna m_borderStyle (também adicionei uma propriedade ao controle para permitir que o usuário definisse o estilo de borda, mas não tratarei dessa ação neste artigo). Em seguida, é aplicada uma transformação ao objeto Graphics (usando o método TranslateTransform) para alterá-lo de acordo com a posição atual da scrollbar (darei mais informações sobre essa ação em seguida). Por fim, com todas as coisas configuradas, os itens reais (radio buttons) são criados dentro do controle por meio de um loop na fonte de dados (representada pela variável myList) e da extensão de cada item (criando cada item) individualmente.
A rotina DrawOneItem, como sugere seu nome, é chamada pra criar cada RadioButton individual no controle:
Private Sub DrawOneItem(ByVal index As Integer, ByVal gr As Graphics)
Para começar, o código captura os valores Font e ForeColor das propriedades internas do controle e os utiliza em todas as suas criações, o que garante que o usuário tenha amplo controle sobre a aparência de cada item. O uso de propriedades internas tem várias vantagens: você não precisa escrever nenhuma parte de seu código, o controle não fica saturado de propriedades adicionais e você aproveita o comportamento interno das propriedades básicas. No caso das propriedades Font e ForeColor, o seu controle permanecerá automaticamente sincronizado com o container; as alterações feitas nas propriedades correspondentes do form serão refletidas em seu controle (presumindo que o usuário ainda não as tenha substituído no controle):
Dim textFont As Font = Me.Font
Dim textBrush As New SolidBrush(Me.ForeColor)
Após capturar os valores Font e ForeColor, uma variável StringFormat será criada e configurada para controlar como será desenvolvida a legenda do Radio Button. Nesse caso, a StringFormat é configurada para alinhar o texto Near (que corresponde ao alinhamento esquerdo se você estiver usando uma máquina US English), mas essa configuração de alinhamento não terá realmente o efeito desejado porque, quando DrawString for chamado posteriormente, só será fornecido um ponto de início, e não uma área de texto completa. Tente alterá-la para Far se quiser ver o que acontece. O texto será criado de modo a terminar naquele ponto de início, o que significa que a maior parte dele não estará visível no controle. FormatFlags controla como o texto deverá ser ajustado se ele tiver mais de uma linha; LineLimit indica que somente as linhas completamente visíveis deverão ser incluídas:
Dim myStringFormat As New StringFormat
myStringFormat.Alignment = StringAlignment.Near
myStringFormat.FormatFlags = StringFormatFlags.LineLimit
Essa rotina usa diversas variáveis private para configurar o layout do botão e o texto (m_HorizontalSpacing e outros), o que é feito para fornecer um pouco mais de controle ao usuário (um desenvolvedor que utilize esse controle) e permitir a ele que use as propriedades de controle correspondentes para ajustar a aparência do controle da maneira como ele preferir. Outra variável interna, o array rowStarts, contém a posição vertical de cada item na lista e, como ela só é calculada uma vez quando o controle é redimensionado, ela é usada para economizar tempo, no lugar de recalcular uma posição superior durante a criação de cada item. Caso queira ver todo o código que expõe esses valores como propriedades ou que efetue o cálculo da posição de início, consulte o código-fonte completo referente a essa classe na seção de download disponível no link no final deste artigo.
Dim leftPos As Integer = m_HorizontalSpacing
Dim topPos As Integer = Me.rowStarts(index)
O texto de exibição do item atual é o valor da propriedade DisplayMember do objeto atual, e acessar esse valor (que envolve descritores de propriedade e algumas linhas de código) é simples graças à existência de uma função GetItemText na classe básica do controle (ListControl).
'grab the display member text from the data source
Dim itemText As String = Me.GetItemText(Me.DataManager.List.Item(index))
Agora que eu tenho o texto do item e posso determinar a quantidade de espaço que ele ocupará no controle, posso criar um retângulo de foco para indicar se o item atual está selecionado. Usando um método da classe ControlPaint (que é um conjunto avançado de funções de utilitários projetadas para uso durante a criação de seus próprios controles), crio um retângulo de linha pontilhada em volta do texto se o item atual possui o foco, conforme mostrado aqui:
'draw focus rectangle (dotted line)
If Me.focusedItem = index Then
Dim rowSize As SizeF
rowSize = gr.MeasureString(itemText, textFont, _
New PointF(leftPos, topPos), myStringFormat)
Dim focusRect As New Rectangle(leftPos, topPos, _
rowSize.Width + m_buttonSize, rowSize.Height)
focusRect.Inflate(m_focusRectInflation, m_focusRectInflation)
ControlPaint.DrawFocusRectangle(gr, focusRect)
End If
Em seguida, após a configuração de uma variável ButtonState, será criado o RadioButton propriamente dito. ButtonState consiste em uma enumeração usada com vários métodos da classe ControlPaint para indicar como um botão (radio button, checkbox etc) deve ser criado. Nesse caso, o estado do RadioButton é afetado por duas coisas — se o controle está ou não ativado e se o radio button específico está selecionado:
'the ButtonState indicates how the Radio Button should be drawn
Dim bs As ButtonState
'should it be grayed out?
If Not Me.Enabled Then
bs = bs Or ButtonState.Inactive
End If
'should it be selected?
If Me.SelectedIndex = index Then
bs = bs Or ButtonState.Checked
End If
A combinação das configurações é então usada para criar o controle real através do método ControlPaint.DrawRadioButton e, em seguida, o Text é incluído à esquerda do radio button. Uma ótima contribuição a esse controle, que talvez você queira fazer se pretende fornecê-lo como parte de uma biblioteca de controles, consiste em permitir que o usuário configure a posição relativa do texto e do radio button (permitindo que o botão esteja no lado direito do texto, por exemplo):
ControlPaint.DrawRadioButton(gr, New Rectangle(leftPos, topPos, _
m_buttonSize, m_buttonSize), bs)
gr.DrawString(itemText, textFont, textBrush, leftPos + m_buttonSize, _
topPos, myStringFormat)
Deixe o usuário interagir com o controle
O seu controle não será de grande utilidade se o usuário não puder clicar nele para selecionar um item ou navegar até ele por meio das teclas de seta e Tab de seu teclado. Para lidar com esses dois casos, você precisa substituir as rotinas OnClick, OnKeyDown e IsInputKey de seu controle. Concentrando-se primeiro na interação do mouse (capturando um clique de mouse por meio do OnClick), você pode determinar qual item de sua lista foi clicado e torná-lo o item atualmente selecionado (consulte a Listagem 2).
Listagem 2 Definir o item atualmente selecionado
Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
Dim mouseLoc As Point = Me.PointToClient(Me.MousePosition())
mouseLoc.Y += Me.m_currentTopPosition
Dim itemHit As Integer = HitTest(mouseLoc)
If itemHit <> -1 Then
Me.SelectedIndex = itemHit
Me.Focus()
End If
End Sub
Private Function HitTest(ByVal loc As Point) As Integer
Dim i As Integer
Dim found As Boolean = False
i = 0
Do While i < Me.DataManager.Count And Not found
If GetItemRect(i).Contains(loc) Then
found = True
Else
i += 1
End If
Loop
If found Then
Return i
Else
Return -1
End If
End Function
Já o tratamento do teclado é um pouco mais complexo. Primeiro, você precisa substituir a rotina IsInputKey para permitir que o controle saiba quais pressionamentos de tecla você é capaz de tratar, por exemplo:
Protected Overrides Function IsInputKey( _
ByVal keyData As System.Windows.Forms.Keys) As Boolean
Select Case keyData
Case Keys.Down, Keys.Left, Keys.Up, _
Keys.Right, Keys.Enter, Keys.Return
Return True
Case Else
Return MyBase.IsInputKey(keyData)
End Select
End Function
Após substituir IsInputKey, todos os pressionamentos de tecla chegarão por meio da rotina OnKeyDown. Dentro daquela rotina, eu também não faço quase nada; apenas encaminho a tecla pressionada à outra rotina, KeyPressed. Essa rotina decide como as informações sobre a tecla pressionada deverão afetar o item do controle que receberá o foco e o que fazer com o item selecionado se o usuário pressionar as teclas de retorno ou de espaço (consulte a Listagem 3).
Listagem 3 Quem está em foco
Private Sub KeyPressed(ByVal Key As System.Windows.Forms.Keys)
Try
Dim currentSelectedItem As Integer = Me.focusedItem
Dim newPosition As Integer = currentSelectedItem
Dim selectedRow As Integer
Dim selectedColumn As Integer
If Not Me.DataManager.List Is Nothing _
AndAlso Not Me.Parent Is Nothing Then
Select Case Key
Case Keys.Up
If currentSelectedItem = 0 Then
'go to the previous control
Me.Parent.SelectNextControl( _
Me, False, True, True, True)
Else
'go up by one
newPosition -= 1
End If
Case Keys.Down
Dim selected As Boolean
If currentSelectedItem >= _
Me.DataManager.Count - 1 Then
'go to the next control
Me.Parent.SelectNextControl( _
Me, True, True, True, True)
Else
'go down by one
newPosition += 1
End If
Case Keys.Left
'go to the previous control
Me.Parent.SelectNextControl(Me, _
False, True, True, True)
Case Keys.Right
'go to the next control
Me.Parent.SelectNextControl(Me, _
True, True, True, True)
Case Keys.Enter, Keys.Return, Keys.Space
Me.SelectedIndex = newPosition
End Select
If newPosition < 0 Then
newPosition = 0
ElseIf newPosition >= DataManager.Count Then
newPosition = Me.DataManager.Count - 1
End If
If Me.focusedItem <> newPosition Then
Me.focusedItem = newPosition
End If
End If
Catch except As Exception
Debug.WriteLine(except)
End Try
End Sub
Na verdade, eu alterei completamente essa rotina de tratamento do teclado antes de escrever este artigo. Originalmente, o item selecionado era alterado se o usuário usasse as teclas de seta para mover a lista de radio buttons para cima e para baixo, mas eu o modifiquei de modo que o usuário precisasse selecionar explicitamente um item (usando as teclas de espaço ou enter) e, com isso, as teclas de seta apenas movessem o foco. Essa solução me pareceu uma melhor experiência de usuário, mas você pode modificar o código para que ele corresponda ao meu projeto original se preferir outro comportamento.
Vamos tratar agora do redimensionamento e da rolagem
Sempre que criar um controle que possa ser vinculado a uma lista de dados, você precisará lidar com uma situação em que terá mais itens do que caberá no controle (observe a Figura 2). Nela, é adicionada uma scrollbar vertical se AutoSize = False e se o controle for menor do que o necessário para exibir a lista de itens.

Figura 2 Muitos itens
No caso deste controle, costumo lidar com a situação de duas maneiras diferentes: redimensionando automaticamente o controle (configurável com uma propriedade AutoSize) ou adicionando uma scrollbar vertical conforme necessário (consulte a Figura 2). Sempre que a fonte de dados for alterada (porque o número de itens pode ter sido alterado) ou que o controle for redimensionado, a rotina RecalcSizing será chamada para redimensionar automaticamente o controle ou para adicionar uma scrollbar (consulte a Listagem 4).
Como alternativa, você pode usar AddHandler/RemoveHandler para rastrear o evento ValueChanged da scrollbar, mas usando o WithEvents e os Handles, o Visual Basic .NET cuidará das associações de eventos por você.
Listagem 4 Lidando com o redimensionamento do controle
Private WithEvents vSB As VScrollBar
Private Sub RecalcSizing()
If Me.AutoSize Then
Me.Height = m_textHeight
Else
If Me.Height < m_textHeight Then
If vSB Is Nothing Then
vSB = New VScrollBar
Me.Controls.Add(vSB)
End If
With vSB
.Top = 0
.Left = Me.Width - .Width
.Height = Me.Height
.Visible = True
.Maximum = m_textHeight - Me.Height
.Minimum = 0
.Value = 0
.LargeChange = .Maximum \ 10
.SmallChange = 1
End With
Else
If Not vSB Is Nothing Then
Me.Controls.Remove(vSB)
vSB = Nothing
End If
End If
End If
End Sub
Private Sub vScrollChanged(ByVal sender As Object, _
ByVal e As EventArgs) Handles vSB.ValueChanged
Dim sb As VScrollBar
sb = DirectCast(sender, VScrollBar)
Me.m_currentTopPosition = sb.Value
Me.Invalidate()
End Sub
Conclusão
O resultado final de todo esse código (e alguns fragmentos que estão incluídos no download, mas que não merecem ser listados neste artigo) é o controle RadioButtonList de vinculação de dados do Windows Forms. Praticamente qualquer aplicativo pode se beneficiar de mais recursos, e esse controle não é exceção. Você poderia expandi-lo para tratar checkboxes e radio buttons, adicionar rolagem horizontal ou quebra de palavras em legendas, e muitas coisas mais. Da maneira como é construído agora, ele mostra como você pode fazer uso consistente de data binding em toda a interface. Para obter mais informações sobre desenvolvimento em Windows Forms, consulte http://www.windowsforms.net.
Download disponível em www.neoficio.com.br/msdn: XMLFiles0403.exe (139KB)