Clique aqui para ler este artigo em pdf imagem_pdf.jpg

msdn10_capa.JPG

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.

 

image001.gif

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.

 

image002.gif

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)