Revista MSDN Magazine Edição 10 - Data Binding de Radio Buttons em uma lista com Windows Forms

Artigo Originalmente Publicado na MSDN Magazine Edição 10

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)

Artigos relacionados