Menus Dinâmicos em Visual Basic.Net

por Roberto Lopes

Para ler todas as matérias da MSDN Magazine, assine a revista no endereço www.neoficio.com.br/msdn

Este artigo discute:

Este artigo usa as seguintes tecnologias:

Criação de Menus Dinâmicos

Instruções SQL

Acesso a banco de dados

VB.NET, SQL

Chapéu
Menus

 

O objetivo deste artigo é mostrar como criar menus de modo dinâmico, customizado e em tempo de execução. O .Net permite isto sem a necessidade de recorrer à API do Windows ou à alternativa de criar todas as opções possíveis de menu e, via código, ocultar ou exibir as opções adequadas ao contexto. Provavelmente esta última tenha sido a alternativa mais utilizada, pois era mais fácil e rápida para o desenvolvimento e manutenção dos programas.

Definindo o projeto

O programa possui dois formulários e duas classes, sendo uma específica para a manutenção dos itens/sub-itens de menu e a outra para usuários. Também possui um banco de dados Access para armazenar os dados dos usuários e os itens/sub-itens de menu que cada usuário tem acesso. Quando o programa for carregado ou o usuário desconectar-se, as opções de menu para o usuário "Default" deverão ser carregadas do banco de dados. No exemplo, os itens Default criados são: Menu Arquivo contendo os sub-itens Conectar e Sair.

Os menus e itens não terão controle por usuário, mas sim por grupos. Deste modo, todas as opções de menu foram cadastradas para um grupo. Cada usuário deve obrigatoriamente pertencer a um grupo de usuários, identificando assim quais opções de menu a que ele tem direito. Portanto, quando o usuário se "autenticar" no sistema, as opções de menu do grupo a que ele pertence serão obtidas do banco de dados e o menu será montado em tempo de execução.

Início da páginaInício da página

Banco de Dados

O banco de dados MenuD.mdb contém quatro tabelas: Usuario, Grupo, Menu e MenuGrupo (conforme a Figura 1). No relacionamento das tabelas (Figura 2), cada usuário deverá estar associado a um grupo existente na tabela Grupo. A tabela MenuGrupo associa um grupo ao item de menu (tabela Menu) e a tabela Menu contém as estruturas de menu disponíveis aos grupos (conforme a Figura 3).


Figura 1. Tabelas Access


Figura 2. Modelo do banco MenuD.mdb


Figura 3. Tabela Menu

Para um melhor entendimento, veja o descritivo dos campos da tabela Menu (Tabela 1).

ColunaDescrição

MenuID

Identificador do item de menu (poderia ser do tipo AutoNumber)

ColunaID

Identifica a qual coluna do menu o item pertence. Usando o Visual Studio como exemplo, File seria a coluna 1, Edit a coluna 2, etc. Deste modo, todos os itens abaixo de File (inclusive), teriam o valor de ColunaID igual a 1

Descricao

Descrição do item de menu que será inserido (que será apresentado no menu). Inclua também o caractere & antes da letra que será relacionada com uma tecla de atalho.

Nível

Identifica em que sub-nível o item se encontra. Ex. No visual Studio, File seria 0, New seria 1 e Project seria 2, etc.

ParentID

A que MenuID com nível zero este item pertence.

SubOrdem

Para cada coluna de menu, qual a ordem em que os itens devem aparecer.

Início da páginaInício da página

MenuDinamico

Crie um projeto no Visual Studio .NET 2003 do tipo Windows Application chamado MenuDinamico, contendo um formulário chamado frmPrincipal (Figura 4), o qual não precisa ter nenhum tipo de componente inserido, pois uma instância do componente MenuItem (principal do projeto) será criada e inserida em tempo de execução.


Figura 4. Form principal (frmPrincipal)

Como iremos acessar um banco de dados MDB, insira a referência da classe OleDb na primeira linha da janela de códigos:

Imports System.Data.OleDb

Declare os seguintes atributos na classe frmPrincipal:

'Instância do objeto onde o menu será montado 
Private mmnMenuPrincipal As New MainMenu 
'Instância da classe que manipula os menus
Private mndMenuD As New clsMenuDinamico
'Instância da classe que armazena o usuário
Private usrUsuario = New clsUsuario	    
'Instância do objeto de conexão
Private conDB As New OleDbConnection

A utilização de um menu dinâmico proporciona uma grande flexibilidade na manutenção da customização e do controle de acesso ao sistema, onde o gerenciamento do acesso aos módulos do sistema é customizado quase que individualmente. As opções disponíveis no menu do usuário são montadas em tempo de execução, logo após o login.

O código no método frmPrincipal_Load do formulário, monta o menu default no StartUp do programa. As opções do menu default são apenas as opções básicas de acesso ao sistema, por exemplo, opção de login e encerramento do programa.

Private Sub frmPrincipal_Load(_ 
  ByVal sender As System.Object, _ 
  ByVal e As System.EventArgs) _
  Handles MyBase.Load
  'Atribui atributo ao menu do form
  Me.Menu = mmnMenuPrincipal 
  conDB.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data source=..\DB\MenuD.mdb"

  'Monta o menu default (nenhum usuário autenticado)
  Try
    mndMenuD.MontaMenu(0, mmnMenuPrincipal, conDB, _
                       AddressOf EventoMenu_Click)
  Catch ex As Exception
     MessageBox.Show("Ocorreu um erro durante a criação dos menus: " & ex.Message, _
     "Menu Dinamico", MessageBoxButtons.OK, _
     MessageBoxIcon.Error)
  End Try
End Sub

A Sub EventoMenu_Click será a responsável por receber o evento Click de todos os itens de Menu. A opção de concentrar o evento click de todos os itens de menu em apenas uma Sub, facilita o gerenciamento e a manutenção do código, uma vez que todas as chamadas estão concentradas em um mesmo lugar. Porém, esta Sub não precisa obviamente conter o código para todas as funcionalidades do sistema, devendo redirecionar as chamadas para as rotinas pertinentes (por exemplo: a opção conectar, direciona o código para a Sub Conectar).

Private Sub EventoMenu_Click(ByVal sender As Object, ByVal e As System.EventArgs)
  'Apresento o item clicado e, após o casting, 
  'obtenho a propriedade Text do objeto
  Select Case CType(sender, MenuItem).Text
     Case "&Conectar" 
        Conectar(sender, e)
     Case "&Desconectar"
       'Executa o método MontaMenu p/ o menu Default
        Try
           mndMenuD.MontaMenu(0, mmnMenuPrincipal, _
                   conDB, AddressOf EventoMenu_Click)
        Catch ex As Exception
           MessageBox.Show("Ocorreu um erro durante a criação dos menus: " & ex.Message, _
           "Menu Dinamico", MessageBoxButtons.OK, _
           MessageBoxIcon.Error)
        End Try
     Case "Sai&r"
        Me.Close()
     Case Else
        MessageBox.Show("Menu """ & _ 
           CType(sender, MenuItem).Text & _ 
           """ selecionado.", _
           "Menu Dinamico", MessageBoxButtons.OK, _
           MessageBoxIcon.Error)
   End Select
   'É possível manipular os atributos do menu através
   'do método RetornaMenu (collection)
   'Ex: mndMenuD.RetornaMenu("Form1").Enabled = False
   'Não esqueça do Try...Catch
End Sub

A Sub Conectar cria uma nova instância do form de autenticação do usuário (frmLogin). Devido este formulário ser aberto no modo modal (modo síncrono), o código da Sub Conectar interrompe até que a instância do formulário frmLogin seja fechada. Os dois parâmetros recebidos por esta Sub, são os mesmos parâmetros recebidos pelo evento Click do item de menu Conectar.

Private Sub Conectar(ByVal sender As Object, ByVal e As System.EventArgs)
  'Variável para formulário de login
  Dim frm As frmLogin	   
  Dim usrUsuarioLogin As New clsUsuario

  'Nova instância do form de login, usando construtor
  'customizado, passando a instância da classe 
  'usuário e instância do objeto conexâo no BD
  frm = New frmLogin(usrUsuarioLogin, conDB)
  'Carregar form no modo modal
  frm.ShowDialog(Me)

  'Se um usuário foi autenticado, montar menu
  If Not usrUsuarioLogin.Nome Is Nothing Then
     mndMenuD.MontaMenu(usrUsuarioLogin.Grupo, _
     mmnMenuPrincipal, conDB, _
     AddressOf EventoMenu_Click)
  End If
End Sub

Veja a estrutura do formulário frmLogin (Figura 5), o qual possui dois TextBox (txtUsuario e txtSenha) e dois botões (btnOK e btnCancelar).


Figura 5. Form de autenticação (frmLogin)

Antes da declaração da classe frmLogin (no mesmo arquivo/namespace), inclua uma referência para a classe OleDB e outra para a classe System.Text (devido ao uso de StringBuilder):

Imports System.Data.OleDb
Imports System.Text

Declare os seguitnes atributos na classe frmLogin:

'Instância local da classe que armazena o usuário
Private usrUsuario = New clsUsuario	    
'Instância local do objeto de conexão
Private conDB As New OleDbConnection

Uma técnica que utilizo para fazer dados transitarem através das instâncias dos objetos de um projeto é a criação de uma classe específica para esta finalidade, passando uma instância desta classe através de um construtor específico para receber a referência desta ou através de um método. Deste modo, o código em um escopo global fica mais "limpo", utilizando-se pouco do recurso de variáveis globais.

No formulário frmLogin utilizei esta técnica com os dados do usuário. A classe clsUsuario foi criada, e passada para este formulário através de um construtor específico recebendo dois parâmetros. O primeiro é do tipo clsUsuario e é passado por referência (onde o nome e grupo do usuário autenticado serão atribuídos). O segundo do tipo OleDbConnection é passado por valor, visando apenas reaproveitar a instância já configurada no formulário anterior.

Public Sub New(ByRef pUsuario As clsUsuario, ByVal pconBD As System.Data.OleDb.OleDbConnection)
   'Chamo o construtor da classe base para que as 
   'rotinas básicas sejam executadas
   Me.New()
   usrUsuario = pUsuario
   conDB = pconBD
End Sub

No evento Click do botão OK o processo de autenticar o usuário é realizado, executando-se uma chamada à função ValidaUsuario.

Private Sub btnOK_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
  Handles btnOK.Click
  Try
    If UsuarioValido(txtUsuario.Text, txtSenha.Text) Then
       Me.Close()
    Else
       MessageBox.Show("Usuário/senha inválidos.", _
       "Menu Dinamico", MessageBoxButtons.OK, _
       MessageBoxIcon.Information)
       txtUsuario.Focus()
    End If
  Catch ex As Exception
       MessageBox.Show(ex.Message.ToString, "Menu Dinamico", _
       MessageBoxButtons.OK, MessageBoxIcon.Error)
  End Try
End Sub

A Function UsuarioValido utiliza o objeto OleDbDataReader para efetuar a pesquisa no banco de dados e validar o usuário. Como nesta pesquisa não haverá a necessidade de navegar pelo resultado, nem manipular estas informações, um DataReader se faz mais eficiente, além de consumir menos recursos do que um DataSet, por exemplo.

Private Function UsuarioValido(ByVal pstrUsuario As String, ByVal pstrSenha As String) As Boolean
  Dim olecmdCommand As OleDbCommand
  Dim olerdrReader As OleDbDataReader
  Dim strQuery As New StringBuilder
  Dim bolResult As Boolean

  Try
    conDB.Open()
    strQuery.Append("select Nome, GrupoID from usuario ")
    strQuery.Append("where nome = '" & txtUsuario.Text.ToUpper() & "' ")
    strQuery.Append("and senha = '" & txtSenha.Text.ToUpper() & "' ")
    olecmdCommand = New System.Data.OleDb.OleDbCommand(strQuery.ToString(), conDB)
    olerdrReader = olecmdCommand.ExecuteReader()

    If olerdrReader.Read() Then
       usrUsuario.Nome = olerdrReader.Item("Nome")
       usrUsuario.Grupo = olerdrReader.Item("GrupoID")
       bolResult = True
    End If
  Catch ex As Exception
    Throw New Exception("Ocorreu um erro ao autenticar o usuário: " & ex.Message)
    bolResult = False
  Finally
    conDB.Close()
  End Try
  Return bolResult
End Function

No evento Click do botão Cancelar, como o formulário será fechado, é interessante liberar os recursos já desnecessários.

Private Sub btnCancelar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles btnCancelar.Click
   Me.Close()
End Sub

Como já citado anteriormente, a classe clsUsuario foi criada com o objetivo de facilitar a comunicação entre as instâncias dos objetos utilizados no projeto, evitando-se assim o uso desnecessário de atributos de escopo global.

Pensando no objetivo desta classe (conter variáveis para transitar entre as instâncias), eu poderia ter criado uma classe contendo apenas os atributos públicos (Public), onde poderiam ser acessados e manipulados a qualquer momento sem a necessidade do uso de propriedades. Porém, esta não seria uma prática correta, uma vez que estamos trabalhando sob um ambiente orientado a objetos. Além disso, uma validação dos dados que pudesse vir a ser necessária, pode ser incluída neste encapsulamento. Deste modo, além da boa prática de programação, estariamos resguardando a integridade dos dados no sistema.

Public Class clsUsuario
    Private strNome As String
    Private shtGrupo As Short
    Public Property Nome()
        Get
            Return strNome
        End Get
        Set(ByVal Value)
            strNome = Value
        End Set
    End Property

    Public Property Grupo()
        Get
            Return shtGrupo
        End Get
        Set(ByVal Value)
            shtGrupo = Value
        End Set
    End Property
End Class

A classe clsMenuDinamico é a principal classe do projeto. Ela será a responsável pela criação e manutenção dinâmica dos itens do menu. Esta classe foi construída para permitir a sua reutilização em qualquer projeto. Deste modo, para incluir a técnica de utilização de menus dinâmicos em um projeto, basta adicionar esta classe ao projeto, criar uma instância da mesma e do objeto MainMenu. As demais classes utilizadas neste projeto não possuem nenhum vínculo com a classe clsMenuDinamico, podendo ou não serem criadas/utilizadas em seus projetos futuros.

Ela possui apenas um atributo (uma collection) que armazena todos os itens e sub-itens de menu adicionados, permitindo não só a pesquisa, mas a manipulação destes, isto é, se você quiser mudar o atributo de um item de menu (enabled, por exemplo), basta utilizar a collection para alterar este atributo. Após a declaração da classe clsMenuDinâmico, declare o atributo arlMenus do tipo ArrayList e como Private. Note que as duas instruções anteriores à declaração da classe, são as referências para System.Data.OleDb e System.Text (para o StringBuilder).

Imports System.Data.OleDb 
Imports System.Text

Public Class clsMenuDinamico
  'Collection utilizada para armazenar os itens de menu
  Private arlMenus As New ArrayList

O método AdicionarItem (da classe clsMainMenu) adiciona um item ou sub-item de menu no objeto MainMenu. A idéia aqui é identificar o tipo do item a ser incluído (nível zero ou sub-item) e executar o método correspondente para incluir o item ou sub-item no menu. Note que voltamos a utilizar aqui a collection para checar se o item sendo incluído já não fora incluido. Um outro detalhe a ser notado está na última instrução, onde um handler para o item somente é adicionado se uma Sub foi informada no parâmetro.

Public Sub AdicionarItem(ByVal pPai As Object, _
  ByVal pMenuCaption As String, _
  Optional ByRef pEventHandler As Object = Nothing)
  'Um novo item é criado com o caption informado 
  Dim mniMenuItem As New MenuItem(pMenuCaption)

  'Se for um item principal (MainMenu), incluir um
  'item principal (nível zero)
  If TypeOf pPai Is MainMenu Then
     'Se o item já existir, dispara exception
     If Not RetornaMenu(pMenuCaption) Is Nothing Then
        Throw New Exception("Item de menu (" & pMenuCaption & ") já existe")
     End If
     'Adiciona o item criado à instância de MainMenu 
     '(recebido como parâmetro)
     pPai.MenuItems.Add(mniMenuItem)
  Else
     RetornaMenu(pPai).MenuItems.Add(mniMenuItem)
  End If

  'Adiciona o item à collection
  arlMenus.Add(mniMenuItem)

  'Adiciona Handler para o evento Click (apenas se informado)
  If Not pEventHandler Is Nothing Then
     AddHandler mniMenuItem.Click, pEventHandler
  End If
End Sub

O método RetornaMenu (da classe clsMainMenu) faz uma pesquisa na collection da classe, buscando um item que tenha o caption passado como parâmetro, retornando o mesmo ou nothing.

Public Function RetornaMenu(ByVal pCaption As String) As MenuItem
   Dim arlTemp As MenuItem

   For Each arlTemp In arlMenus
       'Verifica se é o item informado
       If arlTemp.Text = pCaption Then    
         'Se for, retorna o item do menu
          Return arlTemp    
       End If
   Next
   Return Nothing
End Function

O método MontaMenu é o principal da classe clsMenuDinamico. Ele recebe alguns parâmetros (veja Tabela 2), pesquisa os itens de menu do grupo informado no banco de dados. Aqui novamente estaremos utilizando um DataReader, pelos mesmos motivos citados anteriormente. A leitura dos dados será feita apenas uma vez, não há a necessidade de navegação entre os dados nem de manipulação do resultado. Para cada item retornado por esta pesquisa, um item é adicionado ao menu através de uma chamada ao método AdicionarItem. Note que quando trata-se de um item de menu (nível zero), a chamada ao método passa no primeiro parâmetro a instância do MainMenu e nenhum handler de função é passado. Quanto se trata de um sub-item, o primeiro parâmetro é a descrição do "Pai" do item é passado (menu nível zero onde este sub-item será anexado) e um handler de função é passado.

Tabela 2: Parâmetros do método MontaMenu
ColunaDescrição

pGrupo

Grupo a ser pesquisado no banco de dados

pMenuPrincipal

Referência à instância do objeto MainMenu, onde os itens serão adicionados

pconDB

Conexão a ser utilizada para a pesquisa

pEventHandler

Qual evento atribuir aos itens (este parâmetro deve ser passado com AddressOf)

Public Sub MontaMenu(ByVal pGrupo As Integer, _
  ByRef pMenuPrincipal As MainMenu, _
  ByRef pconDB As OleDbConnection, _
  ByVal pEventHandler As EventHandler)
  Dim olecmdCommand As OleDbCommand
  Dim olerdrReader As OleDbDataReader
  Dim strQuery As New System.Text.StringBuilder

  'Apaga o conteúdo da collection atual o menu existente
  arlMenus.Clear()
  pMenuPrincipal.MenuItems.Clear()

  'Pesquisa todos os itens para o grupo informado
  Try
    With strQuery
      .Append("SELECT a.colunaid, a.ParentID, a.Nivel, a.Descricao, ")
      .Append("(select descricao from menu where menuid = a.parentid ) ")
      .Append("as ParentDescricao ")
      .Append("FROM Menu AS a, Grupo AS b, MenuGrupo AS c ")
      .Append("WHERE a.MenuID=c.menuid and ")
      .Append("b.GrupoID=c.grupoid and ")
      .Append("b.GrupoID = " & pGrupo & " ")
      .Append("ORDER BY a.colunaid, a.ParentID, a.Nivel, a.SubOrdem;")
    End with

    pconDB.Open()
    olecmdCommand = New  OleDbCommand(strQuery.ToString(), pconDB)
    olerdrReader = olecmdCommand.ExecuteReader()

    'Para cada item retornado, adicionar item no menu
    While olerdrReader.Read()
       'Se item nível zero (Arquivo, Editar, Help, 
       'etc), primeiro parâmetro é o MainMenu
       If olerdrReader.Item("Nivel") = 0 Then
          AdicionarItem(pMenuPrincipal, olerdrReader.Item("Descricao"))
       Else
          'Se sub-item, primeiro parâmetro é o caption 
          'do Item nível zero, isto é, pai do sub-item
          AdicionarItem(olerdrReader.Item("ParentDescricao"), _
                  olerdrReader.Item("Descricao"), _
                  pEventHandler)
       End If
    End While
  Catch ex As Exception
    MessageBox.Show("Ocorreu um erro ao montar os menus: " & ex.Message, _
    "Menu Dinamico", MessageBoxButtons.OK, MessageBoxIcon.Error)
  End Try
  pconDB.Close()
End Sub

Veja o projeto sendo executado (Figura 6).


Figura 6. Imagem do programa em execução

Início da páginaInício da página

Conclusão

Neste artigo vimos uma das potencialidades que a plataforma .NET disponibilizada aos desenvolvedores. Quanto trata-se de um sistema corporativo, onde muitos níveis de acesso devem ser criados e cuidadosamente atribuídos, o uso de menus dinâmicos ampliam as possibilidades no momento de gerenciar e desenvolver os aplicativos.

Roberto Lopes (robertoctlopes@yahoo.ca), MCP em VB é consultor de desenvolvimento. Trabalhou em empresas como Grupo Citibank, Grupo Silvio Santos, Telesp, CST (Vixteam) e Senac-SP (também como instrutor). Atualmente é consultor de desenvolvimento VB/ASP.Net na empresa CGI Inc. em Toronto/Canada.

Referências:
http://msdn.microsoft.com/vbasic/
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnadonet/html/adonetbest.asp
http://www.codeproject.com


Início da páginaInício da página