Visual Basic .NET

Cómo pasarse al Visual Basic de Visual Studio .NET (beta1)
sin tener que pasar a un estado más espiritual...

Fecha: 24~27/Dic/2000
Publicado: 28/Dic/2000



Algunos cambios con respecto a Visual Basic 6.0 (y/o anteriores)

Ahora que tengo la Beta1 del Visual Studio .NET, he convertido algunos proyectos de VB6, para ver que tal quedan en la nueva versión.
Para convertir un proyecto, simplemente hay que abrirlo en con el VS.NET y automáticamente se ejecutará el asistente.
El resultado de la conversión no siempre es "funcional", de hecho creo que pocos proyectos serán funcionales al 100%.
Veamos algunas de las peculiaridades con las que me he encontrado.

Nota: Si has hecho pruebas con el SDK pre-beta, verás que los eventos no funcionan igual que con el VS.NET, eso es debido a que hay que declarar los controles usando Dim WithEvents en lugar de un simple Private o Dim.

  1. Acceder a un fichero con datos de longitud fija (usando un tipo definido)

  2. Manejar colección de datos usando colecciones propias y acceso a ficheros secuenciales.


Al final encontrarás unos links para acceder al código de VB6 y el de VB.NET.
Cuando me refiero a VB6, no sólo es para esa versión, sino también para las versiones anteriores, al menos para la 5 y posiblemente la 4, todo es cuestión de que pruebes...


Acceder a un fichero con datos de longitud fija
(Acceso aleatorio y tipo definido por el usuario):

Este primer proyecto accede a un fichero de acceso aleatorio en el cual se almacenan datos de un tipo definido con cadenas de longitud fija, también se muestra cómo trabajar con un control ListBox y un control de diálogos comunes.
En la versión de VB6 se usa Drag & Drop para leer los datos del fichero soltado, aunque el VB7 no permite esta acción... así que... no hay Drag & Drop en esta primera prueba.

Veamos los "problemillas" encontrados y las soluciones.

El tipo definido tiene esta estructura:

Private Type tColega
    Nombre As String * 50
    Email As String * 128
    FechaNac As String * 11
End Type 

En VB6 para leer los datos, se haría así:

' Abrir el fichero como acceso aleatorio 
Open sFic For Random As nFic Len = Len(tmpColega) 
' El número de registros del fichero 
n = LOF(nFic) \ Len(tmpColega) 
' Recorrer todo el contenido del fichero 
For i = 1 To n 
    Get #nFic, i, tmpColega 
    ' Asignarlo al ListBox 
    List1.AddItem Trim$(tmpColega.Nombre) & vbTab & Trim$(tmpColega.Email) & vbTab & Trim$(tmpColega.FechaNac) 
Next 
Close nFic 

Pero en VB.NET no se permiten cadenas de longitud fija, por tanto todo el código generado por el asistente es prácticamente inoperativo, aunque no del todo desechable...

Este es el código generado por el asistente:

Private Structure tColega 
    Dim Nombre As String 
    Dim Email As String 
    Dim FechaNac As String
End Structure

Dim tmpColega As tColega
'...
' Abrir el fichero para grabar
VB6.Open(nFic, sFic, VB6.OpenMode.Random, , , SizeOf(tmpColega))

Pero esto no funciona, ya que el SizeOf devuelve el tamaño de la estructura y el caso es que la estructura contiene tres campos del tipo cadena y como en VB.NET no se permiten cadenas de longitud fija, no nos sirve... ya que el valor devuelto por SizeOf(tmpColega) siempre es 12, independientemente del contenido de cada una de las cadenas de la estructura.

¿Cómo lo he solucionado?

  1. Para empezar he aprovechado la característica que tiene "Structure" de poder definir propiedades y métodos tal y como se hacen con las Clases. Ahora lo veremos en detalle.
  2. He cambiado la orden Open para que en lugar de abrir el fichero en modo Random lo haga en modo Binary , de esta forma no es necesario indicar el tamaño (SizeOf) de lo que se va a guardar o leer.
  3. He añadido varios métodos o propiedades a la estructura tColega para que:
    Devuelva el tamaño de los datos almacenados (Len)
    Devuelva una cadena con los datos a guardar, de forma que cada campo tenga la longitud que queremos que tenga, 50 para Nombre, 128 para Email y 11 para FechaNac (Put)
    Pasar una cadena leída del fichero y asignarla a cada uno de los campos (Get)

El código del tipo definido quedaría así:

Private Structure tColega
     ' Variables privadas para almacenar los valores de las propiedades
    Private _Nombre As String
    Private Const LenNombre As Integer = 50    
    Private _Email As String
    Private Const LenEmail As Integer = 128
    Private _FechaNac As String
    Private Const LenFechaNac As Integer = 11
    '
    ' Esta función devolverá una cadena con el tamaño indicado
    Private Function ProperLen(ByVal sElement As String, ByVal nLen As Integer) As String
        Return VB.Left(sElement & Space(nLen), nLen)
    End Function
    '
    ' Propiedades públicas,
    ' se devuelve y asigna el tamaño que queramos que tengan           
    Property Nombre() As String    
        Get 
            Return ProperLen(_Nombre, LenNombre)
        End Get
        Set
            _Nombre = ProperLen(Value, LenNombre)
        End Set  
      End Property
    Property Email() As String    
        Get
            Return ProperLen(_Email, LenEmail)
        End Get
        Set 
            _Email = ProperLen(Value, LenEmail)
        End Set
    End Property
    Property FechaNac() As String    
        Get
            Return ProperLen(_FechaNac, LenFechaNac)
        End Get
        Set 
            _FechaNac = ProperLen(Value, LenFechaNac)
        End Set
    End Property
     ' Len devolverá el tamaño total que queremos que tenga
    Function Len() As Integer    
        Return  LenNombre + LenEmail + LenFechaNac
    End Function  
    ' Put devolverá el contenido de la estructura
    ' con el tamaño adecuado para cada uno de los elementos
    ' de la estructura.    
    Function Put() As String    
        Return  Nombre & Email & FechaNac
    End Function  
    ' Esta propiedad recibirá una cadena
    ' y la desglosará en cada uno de los campos
    ' El Get hay que ponerlo entre corchetes para indicarle
    ' que es un nombre de propiedad.       
    WriteOnly Property [Get]() As String     
        Set
            Dim s As String    
            s = ProperLen(Value, Me.Len())
            Nombre = VB.Left(s, LenNombre)
            Email = Mid(s, LenNombre  + 1, LenEmail)
            FechaNac = VB.Right(s, LenFechaNac)
        End Set
    End Property
End Structure

Como ves, he creado unas variables y constantes privadas, para almacenar los valores de las 3 propiedades (o campos) y para indicar el tamaño de cada una de ellas; lo del tamaño es necesario ya que en el fichero hay que guardar la cantidad de caracteres que queremos que tenga cada uno de los campos, por eso mismo, las propiedades devuelven y o asignan esa cantidad de caracteres, para que al "leer" todo el contenido de nuestra estructura (con Put), se asignen los caracteres que hemos indicado... todo este follón se evitaría si el vb.NET admitiera cadenas de longitud fija... pero como no es ese el caso... ¡hay que buscarse la vida!

Verás también que hay algunas cosas extrañas, por ejemplo para obtener el elemento "i" de la lista, el "asistente" hace algo como esto:
s = VB6.GetItemString(List1, i) & ControlChars.Tab,
pero puedes cambiarlo por esto otro, que es más intuitivo...
s = List1.Items(i) & ControlChars.Tab

También me he topado con este código, que la verdad no se a cuento de qué lo ha puesto así:
d = VB6.GetDefaultProperty(VB.Left(s, j - 1))
Cuando es algo tan simple como esto otro:
d = VB.Left(s, j - 1)

Fíjate que tanto al Left como al Right, el asistente le añade VB., en cambio, al Mid no le añade nada...

Estos serían los procedimientos para leer y guardar los datos en el fichero.

Private Sub Guardar(ByVal sFic As String)
    ' Guardar el contenido del ListBox en el fichero indicado
    Dim j, i, n As Integer
    Dim tmpColega As tColega
    Dim nFic As Integer
    Dim s As String
    Dim d As String
    
    ' Si el nombre del fichero no es una cadena vacia
    If CBool(Len(sFic)) Then
        On Error Resume Next
	'
	' Si existe el fichero, borrarlo
	If CBool(Len(Dir(sFic))) Then
	    VB.Kill(sFic)
	End If
	'
	Err.Number = 0
	'
        ' Obtener un canal libre
        nFic = VB6.FreeFile
	' 
        ' Abrir el fichero para grabar
        VB6.Open(nFic, sFic, VB6.OpenMode.Binary)
        If Err.Number = 0 Then
            ' Recorrer todos los elementos del ListBox
            For i = 0 To List1.Items.Count - 1
                ' Cada uno de los elementos,
                ' se añade un vbTab para asegurarnos de que tenga uno al final
		s = List1.Items(i) & ControlChars.Tab
                ' son tres los datos en cada elemento del ListBox
                For n = 1 To 3
                    ' Buscar el primer vbTab
                    j = InStr(s, ControlChars.Tab)
                    d = ""
                    ' Si hay alguno...
                    If CBool(j) Then
                        d = VB.Left(s, j - 1)
                        s = Mid(s, j + 1)
                    End If
                    ' Asignar el dato correspondiente
                    If n = 1 Then
                        tmpColega.Nombre = d
                    ElseIf n = 2 Then
                        tmpColega.Email = d
                     Else
                        tmpColega.FechaNac = d
                    End If
                Next
                VB6.FilePut(nFic, tmpColega.Put())
            Next
            VB6.Close(nFic)
         Else
            Err.Clear()
        End If
     End If
End Sub
Private Sub Leer(ByVal sFic As String)
    ' Leer el contenido del ListBox en el fichero indicado
    Dim i As Integer
    Dim n As Integer
    Dim tmpColega As tColega
    Dim nFic As Integer
    Dim s As String
    ' 
    ' Si el nombre del fichero no es una cadena vacia
    If CBool(Len(sFic)) Then
        ' Borrar el contenido del ListBox
        List1.Items.Clear()
        ' Obtener un canal libre
        nFic = VB6.FreeFile
        ' 
        On Error Resume Next
        ' Abrir el fichero como acceso aleatorio
        VB6.Open(nFic, sFic, VB6.OpenMode.Binary)
        If Err.Number = 0 Then
            ' Asignar el número de registros del fichero
            n = VB6.LOF(nFic) \ tmpColega.Len()
            ' Recorrer todo el contenido del fichero
            s = Space(tmpColega.Len())
            For i = 1 To n
                VB6.FileGet(nFic, s)
                tmpColega.Get = s
                ' Asignarlo al ListBox
                List1.Items.Add(Trim(tmpColega.Nombre) & ControlChars.Tab & Trim(tmpColega.Email) & ControlChars.Tab & Trim(tmpColega.FechaNac))
             Next
            VB6.Close(nFic)
         Else
            Err.Clear()
         End If
    End If
End Sub

Como puedes comprobar, es poco lo que hay que modificar, además de que hay que tener en cuenta de que es una versión BETA1, con lo cual esperamos que la cosa mejore... no sólo en el asistente, sino en el propio lenguaje.

Sobre Drag & Drop
El asistente no ha convertido el código para aceptar ficheros soltados en el formulario o en el listbox.
En la documentación que acompaña a esta versión Beta no informa cómo implementar Drag & Drop, así que, habrá que esperar a una nueva versión o bien "indagar" en los grupos de noticias... En cuanto tenga información lo indicaré.

Array de controles y controles Line:
En VB.NET no existen los arrays de controles y tampoco los controles del tipo Shape, pero el asistente los migra bien... es decir de forma transparente para nosotros, por tanto no debemos preocuparnos demasiado si nuestro proyecto tiene array de controles.

Otra cosa son los controles Line, en este caso, los convierte en Labels y la verdad sea dicha, salvo que hay que cambiar poca cosa, el efecto es el mismo.
Para que tenga la apariencia de línea 3D de separación, hay que hacer que la línea Blanca tenga un ancho 2 y esté debajo de la línea Gris, la cual tendrá un ancho 1.

Para manejar los arrays, el asistente añade un nuevo "objeto" para cada uno de los arrays que tenemos, por ejemplo para el caso del array Text1, hace esto:
Me.Text1 = New Microsoft.VisualBasic.Compatibility.VB6.TextBoxArray()

Cada uno de los controles que tiene el formulario los crea con nombres independientes:
Me.Text1_0 = New System.WinForms.TextBox()

Después cada uno de los elementos los va añadiendo de esta forma:
Text1.SetIndex(Text1_0, 0%)

Con lo cual podemos acceder a cualquiera de los elementos del array de la forma habitual:
Este sería el código del evento Text1_GotFocus, que en VB.NET sería Text1_Enter:

Private Sub Text1_Enter(ByVal eventSender As System.Object, ByVal eventArgs As System.EventArgs)
    Dim Index As Short
    Index = Text1.GetIndex(eventSender)
    ' Seleccionar el texto al entrar en el control
    With Text1.Item(Index)
	.SelectionStart = 0
	.SelectionLength = Len(.Text)
    End With
End Sub

Y para asignar un texto al control con índice n:
Text1.Item(n).Text = d
Los índices siguen empezando por cero. Salvo que se haya indicado lo contrario al asignar el control con SetIndex.

El código mostrado, funciona tal y como está porque Option Strict está Off, es decir, que no se comprueba de "forma severa" las asignaciones entre distintos tipos de datos...

Por ejemplo, para asignar a un String el contenido de un elemento del ListBox, con Option Strict Off podemos hacerlo de esta forma:
s = List1.Items(i)
Pero con Option Strict On habría que hacerlo de esta otra:
s = List1.Items(i).ToString

En el caso de averiguar el número de datos que hay guardados en el fichero, tendriamos que hacer esto otro:
n = VB6.LOF(nFic).ToInt16 \ tmpColega.Len()

De todas formas, el IDE del Visual Studio .NET nos avisa, mostrándolas en la ventana "Task List", de las cosas que no "cuadran".
En algunas ocasiones será más fácil la conversión que en otras... por ejemplo este error necesitaría un poco de "casting", es decir, especificar o convertir el dato dado al esperado:
Este código es del evento cmdGuardar_Click, que es un array de controles del tipo Button y está "convertido" por el asistente, por tanto usa el método GetIndex para saber que "número de índice" le corresponde a este botón en cuestión, pero ese método espera un objeto del tipo System.WinForms.Button y sin embargo el parámetro es del tipo System.Object, por tanto hay que hacer una conversión o mejor dicho, hay que "pasar" como parámetro el interface correspondiente (que es lo que me imagino que hace CType).
Por tanto este código, que es bastante simple y claro:
Index = cmdGuardar.GetIndex(eventSender)
quedaría de esta forma algo más enrevesada:
Index = cmdGuardar.GetIndex(CType(eventSender, System.WinForms.Button))

Personalmente, al igual que en las versiones anteriores usaba el Option Explicit, voy a usar el Option Strict On, (valor por defecto, salvo que se indique lo contrario con: Option Strict Off), y en los casos en los que no haya más remedio, porque tenga que usar "Late Bound", habrá que "desconectar" esa opción, pero en el resto de los casos... a sufrir un poco haciendole las cosas más fáciles al VB.NET... Y por supuesto, seguiré suando Option Explicit.

Con estos links puedes ver el código de las dos versiones, la de VB6 y la de VB.NET Beta1



Manejar colecciones propias y acceso a ficheros secuenciales.

En esta segunda prueba, vamos a ver cómo crear clases del tipo colección, es decir una clase que actúa como colección de otra clase.
También veremos cómo grabar o leer de un fichero secuencial.
Además de usar una clase para "suplantar" al cuadro de diálogos comunes, al menos en las opciones de Abrir (ShowOpen) y Guardar (ShowSave).


Empecemos por las colecciones.
El VB.NET no permite propiedades por defecto, salvo que éstas tengan parámetros no opcionales. Además de que, a diferencia del VB normal, no permite que esa "propiedad" sea un método o función, es decir: las propiedades por defecto deben ser definidas como: Property.
Esto nos impide a tener una clase y hacer una asignación directa a la propiedad por defecto. Por supuesto, la razón de esa "negación" es porque en VB.NET para asignar un objeto a otro ya no es necesario usar SET, sino que se asignan como si de una variable normal se tratara y con ello se evita tener que "decidir" que es lo que queremos hacer: asignar un objeto o asignar el valor a la propiedad por defecto...
En el caso de las propiedades que permiten parámetros es otro cantar... porque se supone que no existe esa "dualidad"; y digo se supone, porque si ese objeto es parte de un array... ¿cómo saber que estoy asignando a un objeto a un elemento del array? (esto último no lo he probado, así que por ahora no te daré la respuesta)

Lo dicho al principio: empecemos por las colecciones y después veremos el tema de las propiedades predeterminadas (o por defecto).
Para que una clase se comporte como una colección, al menos en el caso de lo que a los bucles For Each ... se refiere, hay que Importar la librería de Colecciones, ya que en esa librería está declarada la interface IEnumerator que es el tipo devuelto por un método de la colección.
Lo mismo que en el VB6 usamos NewEnum que devuelve un objeto del tipo IUnknown, en vb.NET hay que declarar una función llamada GetEnumerator que devuelva un objeto del tipo IEnumerator, veamos los ejemplos del VB6 y del VB.NET:

' Para VB5 y VB6 (en VB4 esto no se puede hacer)
Public Function NewEnum() As IUnknown
    ' Debe ser un miembro oculto y
    ' el id del procedimiento debe ser -4
    '
    Set NewEnum = m_col.[_NewEnum]
End Function


' Para vb.NET
Public Overridable Function GetEnumerator() As IEnumerator
    Return m_col.GetEnumerator
End Function

Como puedes apreciar, en ambas versiones se devuelve un "elemento" de la colección que tiene el mismo nombre que la función.

En cuanto al tema de las propiedades por defecto, lo único que tendrás que hacer es buscar cada uno de los "avisos" de error que te mostrará el Visual Studio y añadir el nombre de la propiedad en cuestión, salvo que sea una propiedad que reciba como mínimo un parámetro. Como por ejemplo la propiedad Item de la colección, que suele recibir como parámetro el índice al que queremos acceder:

'
Default Public ReadOnly Property Item(ByVal sIndex As String) As cMensaje
    Get
	' Método predeterminado
	Dim tMensaje As cMensaje

	' Iniciamos la detección de errores
	On Error Resume Next

	Item = CType(m_col.Item(sIndex), cMensaje)
	If CBool(Err.Number) Then
    	' no existe ese elemento
	    Err.Clear()
	    ' Creamos un nuevo objeto
	    tMensaje = New cMensaje()
	    tMensaje.ID = sIndex
	    ' lo añadimos a la colección
	    m_col.add(tMensaje, tMensaje.ID)
	    Item = tMensaje
	    ' Eliminamos el objeto
	    tMensaje = Nothing
	End If
    End Get
End Property

El CType que hay en la asignación de Item es porque tengo puesto Option Strict en On y hay que "decirle" que tipo de datos debe devolver de la colección, ya que los objetos almacenados en las colecciones son del tipo Object.
Sí, ya se que si además de que hay que repasar lo que VB.NET convierte encima hay que repasar el código para que sea más estricto es una verdadera lata, pero... se supone que al no haber compilación tardía (late bound), el código irá más rápido... De todas formas, no tienes porqué indicar el On en Option Strict, puedes seguir dejándolo en Off, ya que funciona igualmente bien y como en las versiones betas eso de hacer test de velocidad no tiene sentido, además de que lo "prohíbe" el acuerdo, pues... en fin...

Cómo acceder a un fichero de acceso secuencial.
Ahora vamos a ver el código para leer el contenido de un fichero y asignarlo a la colección de Mensajes y a continuación asignarlo al ListBox, no te asustes si ves que podía haberlo hecho sin necesidad de usar la colección, pero es que, precisamente, esta prueba es para ver cómo trabajar con colecciones, además de acceder a ficheros secuenciales...

Primero mostraré el código de VB6 y a continuación el de VB.NET.

' El código de VB6
Private Sub Leer(ByVal sFic As String)
    ' Leer el contenido de la colección
    Dim nFic As Long
    Dim tMensaje As cMensaje
    Dim s As String
    Dim j As Long
    '
    On Error Resume Next
    
    nFic = FreeFile
    Open sFic For Input As nFic
    If Err Then
        Err = 0
        MsgBox "Se ha producido el error: " & vbCrLf & Err.Description
        Exit Sub
    End If
    ' Leer el primer dato para comprobar si es un fichero de mensajes
    Line Input #nFic, s
    If s <> "cMensajes" Then
        MsgBox "No es un fichero con datos de Mensajes"
        Close nFic
        Exit Sub
    End If
    ' El número de elementos
    Line Input #nFic, s
    j = Val(s)
    ' Eliminar los mensajes de la colección
    m_Mensajes.Clear
    ' Leer cada uno de los mensajes
    Do While Not EOF(nFic)
        Line Input #nFic, s
        s = Trim$(s)
        If Len(s) Then
            Set tMensaje = New cMensaje
            tMensaje.ID = s
            Line Input #nFic, s
            tMensaje.Contenido = s
            m_Mensajes.Add tMensaje
        Else
            ' Si el ID era una cadena vacía, leer el siguiente dato
            Line Input #nFic, s
        End If
    Loop
    Close nFic
    ' Añadir los mensajes al ListBox
    With List1
        .Clear
        For Each tMensaje In m_Mensajes
            .AddItem tMensaje.ID & vbTab & tMensaje.Contenido
        Next
    End With
    If j <> m_Mensajes.Count Then
        MsgBox "Se esperaban " & CStr(j) & " mensajes y se han leido: " & CStr(m_Mensajes.Count)
    End If
End Sub



' El código de VB.NET
Private Sub Leer(ByVal sFic As String)
	' Leer el contenido de la colección
	Dim nFic As Integer
	Dim tMensaje As cMensaje
	Dim s As String
	Dim j As Integer
	' 
	On Error Resume Next

	nFic = VB6.FreeFile
	VB6.Open(nFic, sFic, VB6.OpenMode.Input)
	If CBool(Err.Number) Then
		Err.Clear()
		MsgBox("Se ha producido el error: " & ControlChars.CrLf & Err.Description)
		Exit Sub
	End If
	' Leer el primer dato para comprobar si es un fichero de mensajes
	s = VB6.LineInput(nFic)
	If s <> "cMensajes" Then
		MsgBox("No es un fichero con datos de Mensajes")
		VB6.Close(nFic)
		Exit Sub
	End If
	' El número de elementos
	s = VB6.LineInput(nFic)
	j = CInt(Val(s))
	' Eliminar los mensajes de la colección
	m_Mensajes.Clear()
	' Leer cada uno de los mensajes
	Do While Not VB6.EOF(nFic)
		s = VB6.LineInput(nFic)
		s = Trim(s)
		If CBool(Len(s)) Then
			tMensaje = New cMensaje()
			tMensaje.ID = s
			s = VB6.LineInput(nFic)
			tMensaje.Contenido = s
			m_Mensajes.Add(tMensaje)
		Else
			' Si el ID era una cadena vacía, leer el siguiente dato
			s = VB6.LineInput(nFic)
		End If
	Loop
	VB6.Close(nFic)
	' Añadir los mensajes al ListBox
	With List1
		.Items.Clear()
		For Each tMensaje In m_Mensajes
			.Items.Add(tMensaje.ID & ControlChars.Tab & tMensaje.Contenido)
		Next tMensaje
	End With
	If j <> m_Mensajes.Count Then
		MsgBox("Se esperaban " & CStr(j) & " mensajes y se han leido: " & CStr(m_Mensajes.Count))
	End If
End Sub

Y con esto y un bizcocho... hasta mañana a las ocho... o casi... en fin...
La cuestión es que la cosa queda aquí, al menos por hoy... a ver cuando lo publico, espero que mañana día 28... si no me da problemas la lenta conexión que tengo...

Si quieres ver el código completo del formulario, tanto para VB6 como para VB.NET, pulsa en estos links.

Nos vemos.
Guillermo


Ir al índice de vb.net

la Luna del Guille o... el Guille que está en la Luna... tanto monta...