Archivo de la etiqueta: VB.NET

Temas relacionados con Visual Basic para .NET Framework

Generar las clases (de VB o C#) de una tabla de SQL Server o Access (mdb)

Pues eso… este post es para tener actualizada la utilidad CrearClaseTabla que en su día (allá por 2004) creé para generar o crear clases para acceder a una base de datos de SQL Server o de Access.

La idea de esta utilidad (la aplicación y la DLL que es la que hace el trabajo) es crear clases de Visual Basic o C# de cada tabla de la base de datos, con idea de facilitar el acceso por código a esas tablas.

 

Actualizado el código y publicada una release en GitHub

Pues eso, con fecha de hoy 14 de mayo de 2023 he estado puliendo un poco el código tanto de la DLL que se encarga de hacer la conversión como del EXE que hace de intermediario.

En GitHub está todo, tanto el código fuente de VB y C# para la aplicación de Windows con un paquete con el exe para Windows.
Y también el código para .NET MAUI para aplicaciones móviles, ya sabes Windows, iOs y Android.

También he agregado las clases ConversorTipos.vb y ConversorTipos.cs que utiliza el código generado para hacer las conversiones de tipos.

 

En la última actualización de hoy 1 de octubre de 2022 se contempla, entre otras cosas, la definición de variables asignadas sin indicar el tipo (inferencia de tipos) además de convertir adecuadamente las conversiones de tipo de Visual Basic a C# (aunque en el código solo uso CInt).

Nota:
He creado el proyecto para .NET 6.0 (Windows) y está disponible en GitHub: gsCrearClasesTablas.
Por ahora el código es el mismo en este nuevo proyecto como en el que referencio en este post/artículo que es para .NET Framework 4.8.1

El código «base» que utilizo es el que yo uso con Visual Basic y la clase CrearClase apoyada de ConvLag se encarga de generar el código de Visual Basic o el de C#.

Por ejemplo, el código que te muestro primero, en el generador de clases lo defino como te muestro en el segundo bloque de código:

Este es el código en que me he basado:

        cmd.Transaction = tran
        cmd.ExecuteNonQuery()

        ' Si llega aquí es que todo fue bien,
        ' por tanto, llamamos al método Commit.
        tran.Commit()

        msg = "Se ha actualizado el Cliente correctamente."

    Catch ex As Exception
        msg = $"ERROR: {ex.Message}"
        ' Si hay error, deshacemos lo que se haya hecho.
        Try
            If tran IsNot Nothing Then
                tran.Rollback()
            End If
        Catch ex2 As Exception
            msg = $" (ERROR RollBack: {ex.Message})"
        End Try

    Finally
        con.Close()
    End Try

End Using

Return msg

Este es el código interno que uso en el conversor (el método generarClase):
En los comentarios está el código mostrado antes y el equivalente para generar el código de VB o de C#.

sb.AppendLine()
'         cmd.Transaction = tran
sb.AppendFormat("            {0}{1}", ConvLang.Asigna("cmd.Transaction", "tran"), vbCrLf)
'         cmd.ExecuteNonQuery()
sb.AppendFormat("            {0}{1}", ConvLang.Instruccion("cmd.ExecuteNonQuery()"), vbCrLf)
sb.AppendLine()
'         ' Si llega aquí es que todo fue bien,
'         ' por tanto, llamamos al método Commit
sb.AppendFormat("            {0}{1}", ConvLang.Comentario(" Si llega aquí es que todo fue bien,"), vbCrLf)
sb.AppendFormat("            {0}{1}", ConvLang.Comentario(" por tanto, llamamos al método Commit."), vbCrLf)
'         tran.Commit()
sb.AppendFormat("            {0}{1}", ConvLang.Instruccion("tran.Commit()"), vbCrLf)
sb.AppendLine()
'         msg = "Se ha actualizado el Cliente correctamente."
sb.AppendFormat("            {0}{1}", ConvLang.Asigna("msg", """Se ha actualizado un " & nombreClase & " correctamente."""), vbCrLf)
sb.AppendLine()
'     Catch ex As Exception
sb.AppendFormat("            {0}{1}", ConvLang.Catch("ex", "Exception"), vbCrLf)
'         msg = $"ERROR: {ex.Message}"
sb.AppendFormat("              {0}{1}", ConvLang.Asigna("msg", "$""ERROR: {ex.Message}"""), vbCrLf)
'         ' Si hay error, deshacemos lo que se haya hecho
sb.AppendFormat("              {0}{1}", ConvLang.Comentario(" Si hay error, deshacemos lo que se haya hecho."), vbCrLf)
'         Try
sb.AppendFormat("              {0}{1}", ConvLang.Try(), vbCrLf)
' Añadir comprobación de nulo en el objeto tran     (17-abr-21)
'   If tran IsNot Nothing Then
sb.AppendFormat("                  {0}{1}", ConvLang.If("tran", "IsNot", "Nothing"), vbCrLf)
'             tran.Rollback()
sb.AppendFormat("                        {0}{1}", ConvLang.Instruccion("tran.Rollback()"), vbCrLf)
' End If
sb.AppendFormat("                  {0}{1}", ConvLang.EndIf, vbCrLf)
'         Catch ex2 As Exception
sb.AppendFormat("              {0}{1}", ConvLang.Catch("ex2", "Exception"), vbCrLf)
'             msg &= $" (ERROR RollBack: {ex.Message})"
sb.AppendFormat("               {0}{1}", ConvLang.Asigna("msg", "$""ERROR RollBack: {ex2.Message}"""), vbCrLf)
'         End Try
sb.AppendFormat("              {0}{1}", ConvLang.EndTry(), vbCrLf)
sb.AppendLine()
sb.AppendFormat("            {0}{1}", ConvLang.Finally, vbCrLf)
' If Not (con is nothing) then
sb.AppendFormat("              {0}{1}", ConvLang.If("", "Not", "(con Is Nothing)"), vbCrLf)
'     con.Close()
sb.AppendFormat("                  {0}{1}", ConvLang.Instruccion("con.Close()"), vbCrLf)
' End If
sb.AppendFormat("              {0}{1}", ConvLang.EndIf, vbCrLf)
'     End Try
sb.AppendFormat("            {0}{1}", ConvLang.EndTry(), vbCrLf)
sb.AppendLine()
' End Using
sb.AppendFormat("            {0}{1}", ConvLang.EndUsing(), vbCrLf)
sb.AppendLine()
' Return msg
sb.AppendFormat("            {0}{1}", ConvLang.Return("msg"), vbCrLf)

Y el código generado de Visual Basic sería como te he mostrado arriba y el de C# sería más o menos este:

cmd.Transaction = tran;
cmd.ExecuteNonQuery();

// Si llega aquí es que todo fue bien,
// por tanto, llamamos al método Commit.
tran.Commit();

msg = "Se ha actualizado un Producto correctamente.";

}catch(Exception ex){
  msg = $"ERROR: {ex.Message}";
  // Si hay error, deshacemos lo que se haya hecho.
  try{
      if(tran  !=   null ){
            tran.Rollback();
      }
  }catch(Exception ex2){
   msg = $"ERROR RollBack: {ex2.Message}";
  }

finally{
  if(  !  (con  ==   null )){
      con.Close();
  }
}

}

return msg;

Como ves, no está bien formateado, (es el código generado directamente) pero si lo pegas en Visual Studio te lo formateará bien y lo coloreará mejor 😉

Y para muestra, ese trozo de código en un fichero abierto en Visual Studio 2022:
(Aunque todo hay que decirlo, en VB lo formatea bien, aunque solo sea un fichero abierto directamente (sin formar parte de ningún proyecto) mientras que en C# le he tenido casi que dar el formato manualmente, en fin…)

    cmd.Transaction = tran;
    cmd.ExecuteNonQuery();

    // Si llega aquí es que todo fue bien,
    // por tanto, llamamos al método Commit.
    tran.Commit();

    msg = "Se ha actualizado un Producto correctamente.";

}
catch(Exception ex)
{
      msg = $"ERROR: {ex.Message}";
      // Si hay error, deshacemos lo que se haya hecho.
    try
    {
        if (tran != null) 
        {
            tran.Rollback();
        }
    }
    catch(Exception ex2)
    {
        msg = $"ERROR RollBack: {ex2.Message}";
    }

    finally
    {
        if (!(con == null))
        {
          con.Close();
        }
    }
}

Pero la idea es que te quedes con lo que la clase hace.

También es cierto que yo suelo generar el código para Visual Basic y es lo que realmente he probado más, hoy he estado viendo cómo lo generaría para C# y he estado haciendo algunas correcciones (que he indicado en el fichero Revisiones.md publicado con GitHub).

Lo publicado originalmente en elGuilel.info

Los enlaces originales en www.elguille.info son estos:
– La página principal de la utilidad: Generar clases para acceder a una tabla.
– La página con el código y esas cosas: Utilidad para generar clases para acceder a una tabla.
– La página de actualización de cómo conseguir el código fuente: Esta me da error y estaba en CodePlex, ahora está en GitHub.

El final (del post)

Una captura de la utilidad tal como la tengo a día 1 de octubre de 2022.

Figura 1. La utilidad en funcionamiento a día de hoy 1 de octubre de 2022

Y esto es todo amigo (o amiga), ya sabes, si quieres participar en el proyecto para mejorarlo, puedes hacerlo, creo que en algún sitio indico cómo avisarme de los errores que encuentres y cómo actualizar el fichero Revisiones.txt que ahora es Revisiones.md.

Y si quieres usarlo sin más aportaciones, estaría bien que hicieras una pequeña aportación monetaria en PayPal (no es obligatorio, pero es de agradecer).

En breve publicaré en GitHub el ejecutable compilado con .NET Framework 4.8.1.

Nota:
Ya está publicado: gsCrearClaseTabla_20221001_1523.

Y ya sabes, si quieres ver el código fuente, está en el proyecto de GitHub (CrearClaseTabla).

Por cierto, en el proyecto (los dos) he incluido un fichero de nombre seguro (strong name) para firmar los ensamblados, ese fichero (elGuille_compartido.snk) lo puedes usar «libremente» (ya sabes todo está con la licencia MIT) para firmar los ensamblados con nombre seguro.

Espero que te sirva de utilidad.

Nos vemos.
Guillermo

P.S.
Sería interesante convertir el proyecto para .NET 6 (o 7) y también usando el código completamente en C#.
Actualmente está creado para usar con .NET Framework 4.8.1 y escrito enteramente en Visual Basic.

P.S.2
Ya está creado el proyecto para .NET 6.0 (net6.0-windows) y publicado en GitHub (gsCrearClasesTablas).

Serializar/deserializar con Json.Serialization (ejemplo para C#)

Pues eso… ahora esto de los ficheros con la extensión .json es lo que está en la «onda» y… pues habrá que aprovecharlo que incluso es fácil usarlo en las aplicaciones de .NET.

Espero que no sirva de precedente, pero en este post no hay ejemplo para Visual Basic, solo para C#. No es porque yo abandone mis END IFs y me pase definitivamente a las llaves y puntos y comas, es porque acabo de terminar una clase «serializable» y la he escrito en C# y por vagancia, no he querido crear un ejemplo en Visual Basic. Eso sí, esta clase la utilizo desde código de Visual Basic, ya que en C# solo he hecho la clase, y como he comprobado que los ejemplos que he consultado no hacían bien el trabajo, me he decidido a escribir esta entra en el blog (post).

La clase que serializo es muy simple, es para «recordar» los tamaños de las columnas de, en mi caso, un DataGridView. Esa cuadrícula utiliza distintos tipos de nombres y anchos de columnas, por tanto, el objeto que contiene los valores está formado por un diccionario en el que la clave es una cadena (string), para saber el tipo de datos mostrados, y los valores es otro diccionario de tipo entero en la clave y el valor que guardará serán el índice de la columna y el ancho de la misma.

Esa propiedad está definida de la siguiente forma:

/// <summary>
/// Diccionario para el tipo de listado y los valores de cada columna 
/// (por índice) y el ancho de la columna.
/// </summary>
[JsonPropertyName("anchos")]
public Dictionary<string, Dictionary<int, int>> Anchos { get; set; } = new();

El atributo JsonPropertyName es el que le indica al compilador que esa propiedad es serializable y que está enlazada con el valor anchos del fichero .json.

Esta es la parte fácil.

Ahora hay que leer y guardar los datos en el fichero de texto con la extensión que queramos, pero que en este ejemplo utilizo el valor estándar: .json.

Y estos son los métodos principales para guardar (Save) y leer (Load) el contenido de la clase en el fichero. Save guarda el contenido de la clase en el fichero (lo serializa) y Load lee el contenido del fichero (lo de-serializa) y lo asigna a un objeto del tipo de la clase del tipo donde está definida esa propiedad. No te líes. Es más sencillo el código que explicar lo que hace… 😉

/// <summary>
/// Carga los valores del fichero indicado.
/// </summary>
/// <param name="fileName">El path del fichero a leer y devolver el contenido.</param>
/// <returns>El objeto leído del fichero indicado.</returns>
private static AnchoColumnas Load(string fileName)
{
    // Abrir el fichero para leer, compartido para lectura y escritura.
    using var stream = new FileStream(fileName, 
                                      FileMode.OpenOrCreate, 
                                      FileAccess.Read, 
                                      FileShare.ReadWrite);
    // Si tiene contenido, deserializarlo, si no, devolver un valor nulo.
    if (stream.Length > 0)
        return JsonSerializer.Deserialize<AnchoColumnas>(stream);
    else
        return null;
}

/// <summary>
/// Guarda los datos de tipo AnchoColumnas indicado.
/// </summary>
/// <param name="anchosColumnas"></param>
/// <param name="fileName"></param>
/// <returns></returns>
private static void Save(AnchoColumnas anchosColumnas, string fileName)
{
    // Abrir el fichero para escribir, compartido para lectura y escritura.
    using var stream = new FileStream(fileName, 
                                      FileMode.OpenOrCreate, 
                                      FileAccess.Write, 
                                      FileShare.ReadWrite);
    // Que se indente el contenido.
    var options = new JsonSerializerOptions { WriteIndented = true };
    // Guardar (serializar) el contenido de la clase.
    JsonSerializer.Serialize<AnchoColumnas>(stream, anchosColumnas, options);
}

En mi caso, he creado otros dos métodos llamados Guardar y Leer en el que asigno el nombre del fichero que contendrá los datos serializados de la clase.
Ese fichero está en el path del ejecutable.

Este es el código:

/// <summary>
/// Guardar los datos de los anchos de los listados.
/// </summary>
/// <param name="anchosColumnas"></param>
public static void Guardar(AnchoColumnas anchosColumnas)
{
    // El fichero está en la carpeta del ejecutable.
    var fic = Path.Combine(Application.StartupPath, "AnchosColumnas.json");
    Save(anchosColumnas, fic);
}

/// <summary>
/// Leer los anchos de las columnas.
/// </summary>
/// <returns></returns>
public static AnchoColumnas Leer()
{
    // El fichero está en la carpeta del ejecutable.
    var fic = Path.Combine(Application.StartupPath, "AnchosColumnas.json");
    return Load(fic);
}

Como puedes comprobar, el método Guardar recibe como parámetro la clase a serializar y el método Leer devuelve un objeto con la clase deserializada (o un valor nulo en caso de que aún no tenga contenido el fichero).

La parte interesante está en los métodos Save y Load, ya que utilizo código «seguro» a la hora de leer o escribir en un fichero que no exista.
Y el truco está en crear el objeto de tipo Stream usando FileStream en lugar de usar los métodos OpenRead u OpenWrite de la clase File, ya que, para usar esos dos métodos habría que hacer comprobaciones si existen, si están compartidos, etc., etc., etc.

Por último, en esa misma clase tengo un método (también compartido o estático) para acceder a la información. Es una propiedad de solo lectura, que se encarga de asignar/leer el objeto si debe hacerlo (cuando inicialmente no está asignado).

Este es el código:

private static AnchoColumnas _AnchosColumnas = null;
/// <summary>
/// Los anchos de las columnas de los listados.
/// </summary>
public static AnchoColumnas AnchosColumnas
{
    get
    {
        if (_AnchosColumnas == null)
        {
            _AnchosColumnas = Leer();
            if (_AnchosColumnas == null)
            {
                _AnchosColumnas = new();
            }
        }
        return _AnchosColumnas;
    }

Y finalmente te muestro cómo uso esa clase desde el código que tengo en Visual Basic:

Primero para asignar las columnas al objeto DataGridView (en el código indicado por lvDatos) y después cuándo guardar los nuevos valores, cosa que hago en el evento ColumnWidthChanged.

La asignación de los anchos guardados lo hago en un método (con más código del mostrado) en el que asigno los valores de las columnas, tanto el texto a mostrar como el ancho, y por supuesto el número de las mismas.

Este es el código para leer los valores guardados y asignarlos a las columnas del objeto lvDatos:

' Asignar los anchos que estén guardados.           (22/abr/22 20.34)
If AnchoColumnas.AnchosColumnas.Anchos.ContainsKey(value.ToString()) = False Then
    AnchoColumnas.AnchosColumnas.Anchos.Add(value.ToString(),
                                            New Dictionary(Of Integer, Integer))
End If
Dim cols = AnchoColumnas.AnchosColumnas.Anchos(value.ToString())
If cols.Keys.Count = 0 Then
    For i = 0 To lvDatos.Columns.Count - 1
        cols.Add(i, lvDatos.Columns(i).Width)
    Next
Else
    For i = 0 To cols.Keys.Count - 1
        lvDatos.Columns(i).Width = cols(i)
    Next

d End If

El valor de la variable «value» es el nombre de la enumeración que utilizo y que es un valor asignado como parámetro de este método (recuerda que esto es solo un extracto en el que se asignan los valores leídos, si es que existe el fichero con esos valores ya guardados).

Y para finalizar, el código del evento ColumnWidthChanged, en el que, tengo puesto una comprobación de si se está inicializando (cuando se carga el formulario) con idea de que no se guarden los valores iniciales que tenga.

Private Sub lvDatos_ColumnWidthChanged(sender As Object, e As DataGridViewColumnEventArgs) Handles lvDatos.ColumnWidthChanged
    ' Cambiar el ancho de las columnas de los totales       (10/mar/22 04.28)
    ' al cambiar el del principal.
    If inicializando Then Return

    ' Asignar y guardar los valores.                        (22/abr/22 20.39)
    Dim value = TipoListado.ToString()
    With lvDatos
        If AnchoColumnas.AnchosColumnas.Anchos.ContainsKey(value) = False Then
            AnchoColumnas.AnchosColumnas.Anchos.Add(value, New Dictionary(Of Integer, Integer))
        End If
        ' Asegurarse que se asignan correctamente.
        Dim cols = AnchoColumnas.AnchosColumnas.Anchos(value)
        cols.Clear()
        For i = 0 To .Columns.Count - 1
            cols.Add(i, .Columns(i).Width)
        Next
        AnchoColumnas.Guardar(AnchoColumnas.AnchosColumnas)
    End With
End Sub

Y esto es todo… otro día pondré el código para Visual Basic de la clase y un ejemplo de cómo usarla escrito en C#, pero eso será en otra ocasión 😉

Nos vemos.
Guillermo

Saber las unidades externas (USB pendrive fijo o extraíble)

Pues eso… yo pensaba que todas las unidades (de tipo pendrive) USB devolverían un valor del tipo DriveType.Removable, pero resulta que no. Al igual que ocurre con los discos duros conectados por USB, los pendrives también pueden ser de tipo fijo (DriveType.Fixed).

Y lo que te voy a mostrar es el código para C# y Visual Basic (usando .NET 5.0 con Visual Studio 2019) para saber todas las unidades conectadas por USB (sean fijas o extraíbles), además de una función para saber si el path indicado está o no en una unidad extraíble.

También incluyo un ejemplo de cómo saber las unidades (y el tipo de unidad que es) usando el «clásico» método para saber las unidades instaladas con DriveInfo.GetDrives().

En la siguiente captura puedes ver el programa funcionando en mi equipo.

Imagen 1. El programa funcionando.

En mi equipo tengo conectado 2 discos duros por usb (letras S y T), un pendrive de tipo fijo (letra F) y uno de tipo extraíble (letra G).

En la primera lista (usando el método GetDrives de la clase DriveInfo) el único que se muestra como «extraíble» es el disco G, el resto se muestran como fijos (fixed).
En la segunda lista se muestran las unidades extraíbles, sean o no fijas.

Ya sin más rollos te muestro el código tanto para C# como para Visual Basic.

El código lo puedes descargar de GitHub (UnidadesExternas), pero si quieres crearlo por tu cuenta, decirte que he tenido que añadir una referencia al paquete de Nuget System.Management versión 5.0 ya que estos proyectos los he creado con Visual Studio 2019 y esa versión de Visual Studio no utiliza versiones posteriores a .NET 5.0.

El tipo de proyecto es Consola de Windows, aunque he tenido que cambiar el tipo del proyecto, ya que al elegir que sea para .NET 5.0 ha intentado usarlo para cualquier plataforma.
En el fichero del proyecto (de C# o de VB) busca <TargetFramework> y cambia net5.0 por net5.0-windows, ya que System.Management solo se puede usar en el sistema operativo Windows.

 

Nota:
Comentarte que el código (de C#) está basado en dos ejemplos encontrados en la red y de esos dos códigos de ejemplo he sacado el código del método GetUsbDriveLetters.
Estos son los enlaces que he usado:
El primero es prácticamente el usado en ese código, pero añadiendo la comprobación de que MediaType también pueda ser External, que saqué del segundo.
https://stackoverflow.com/a/31560283/14338047
https://stackoverflow.com/a/10018438/14338047

 

Código de C# de la utilidad para saber las unidades externas

public class USBUtil
{
    /// <summary>
    /// Comprueba si es una unidad externa.
    /// </summary>
    /// <param name="elPath">Path completo del que se extraerá la letra de unidad.</param>
    /// <returns></returns>
    public static bool EsUnidadExterna(string elPath)
    {
        var disco = Path.GetPathRoot(Path.GetFullPath(elPath));
        var usbD = GetUsbDriveLetters();
        return usbD.Contains(disco);
    }

    // Código basado en dos ejemplos de "la red".
    // https://stackoverflow.com/a/31560283/14338047
    // https://stackoverflow.com/a/10018438/14338047

    /// <summary>
    /// Las letras de las unidades externas conectadas por USB.
    /// </summary>
    /// <returns></returns>
    public static List<string> GetUsbDriveLetters()
    {
        // Hay que usar External para que también tenga en cuenta los USB de tipo fijo.
        var usbDrivesLetters = from drive in new ManagementObjectSearcher("select * from Win32_DiskDrive WHERE MediaType like '%External%' OR InterfaceType='USB'").Get().Cast<ManagementObject>()
                               from o in drive.GetRelated("Win32_DiskPartition").Cast<ManagementObject>()
                               from i in o.GetRelated("Win32_LogicalDisk").Cast<ManagementObject>()
                               select string.Format("{0}\\", i["Name"]);

        return usbDrivesLetters.ToList();
    }

}

 

Código de Visual Basic de la utilidad para saber las unidades externas

Public Class USBUtil

    ''' <summary>
    ''' Comprueba si es una unidad externa.
    ''' </summary>
    ''' <param name="elPath">Path completo del que se extraerá la letra de unidad.</param>
    ''' <returns></returns>
    Public Shared Function EsUnidadExterna(elPath As String) As Boolean
        Dim disco = Path.GetPathRoot(Path.GetFullPath(elPath))
        Dim usbD = GetUsbDriveLetters()
        Return usbD.Contains(disco)
    End Function

    ''' <summary>
    ''' Las letras de las unidades externas conectadas por USB.
    ''' </summary>
    ''' <returns></returns>
    Public Shared Function GetUsbDriveLetters() As List(Of String)
        ' Hay que usar External para que también tenga en cuenta los USB de tipo fijo.
        Dim usbDrivesLetters = From drive In New ManagementObjectSearcher("select * from Win32_DiskDrive WHERE MediaType like '%External%' OR InterfaceType='USB'").[Get]().Cast(Of ManagementObject)()
                               From o In drive.GetRelated("Win32_DiskPartition").Cast(Of ManagementObject)()
                               From i In o.GetRelated("Win32_LogicalDisk").Cast(Of ManagementObject)()
                               Select String.Format("{0}\", i("Name"))
        Return usbDrivesLetters.ToList()
    End Function

 

Nota:
Debes añadir importación de los espacios de nombres:
System
System.Collections.Generic
System.IO
System.Linq
System.Management (de NuGet)

 

Código de ejemplo usar estas los métodos de USBUtil.

class Program
{
    static void Main(string[] args)
    {
        Console.Title = "Ejemplo de Visual C# usando .NET 5.0 (net5.0-windows) y System.Management Version=5.0.0";

        Console.WriteLine("Mostrar las unidades usando DriveInfo.GetDrives().");
        foreach (var dr in DriveInfo.GetDrives())
            Console.WriteLine("Unidad: {0}, tipo: {1}", dr.Name, dr.DriveType);

        Console.WriteLine();

        Console.WriteLine("Mostrar las unidades extraíbles.");
        var usb = USBUtil.GetUsbDriveLetters();
        foreach (var s in usb)
            Console.WriteLine("{0}", s);

        Console.WriteLine();
        Console.ReadKey();
    }

Module Program
    Sub Main(args As String())
        Console.Title = "Ejemplo de Visual Basic usando .NET 5.0 (net5.0-windows) y System.Management Version=5.0.0"

        Console.WriteLine("Mostrar las unidades usando DriveInfo.GetDrives().")
        For Each dr In DriveInfo.GetDrives()
            Console.WriteLine("Unidad: {0}, tipo: {1}", dr.Name, dr.DriveType)
        Next
        Console.WriteLine()

        Console.WriteLine("Mostrar las unidades extraíbles.")
        Dim usb = USBUtil.GetUsbDriveLetters()

        For Each s In usb
            Console.WriteLine("{0}", s)
        Next

        Console.WriteLine()
        Console.ReadLine()
    End Sub
End Module

Y esto es todo, espero te resulte de utilidad 😉

Nos vemos.
Guillermo