Archivo de la etiqueta: c#

Generar clave SHA1 con el nombre y password del usuario

Pues eso… en este post te explico cómo generar una clave (usando la clase SHA1CryptoServiceProvider) formada a partir de dos cadenas, normalmente el nombre del usuario y el password (o contraseña), de esta forma se genera una cadena única (de 40 caracteres) de forma que si alguien accede a ella no sabrá nunca cuales fueron las dos cadenas que la formaron (o eso es lo que espero que ocurra, jejeje). Además te mostraré también una función para evitar que el usuario introduzca caracteres no válidos y que pueden se usados para acceder maliciosamente a una base de datos.
Por supuesto, te mostraré el código tanto para Visual Basic como para C#.

El código que te mostraré (al menos el de generar la clave SHA1) está basado en este código publicado en mi sitio (usando .NET 1.1):
El ejemplo de Visual Basic .NET:
comprobar_usuario_usando_base_datos_vb2003
El ejemplo de C#:
comprobar_usuario_usando_base_datos_cs2003

Generar una clave SHA1 usando SHA1CryptoServiceProvider

Para acceder a esta clase necesitas una importación del espacio de nombres System.Security.Cryptography y como en el código usaremos un objeto StringBuilder y UTF8Encoding, también habrá que importar System.Text.

En el código de ejemplo para usar los métodos definidos en la clase UtilSHA1 (que será en la que defino los dos métodos usados) se hará una comprobación de si tanto en el nombre como en el password usado hay caracteres no válidos, los caracteres que compruebo en el método ValidarTextoClave son los caracteres: ?*%’_ y

De esa forma intentamos asegurarnos que no se pueda hacer un SQL injection, es decir, intentar acceder maliciosamente a la base de datos a la que presumiblemente se quiere acceder.

Veamos primero el código que usa los dos métodos, el de comprobar la validez del texto introducido (ValidarTextoClave) y el de generar la clave SHA1 (GenerarClaveSHA1).

Método Main con el código de prueba para usar los métodos de la clase

Como te dije antes, se pide el nombre del usuario y el password a usar para generar la cadena con la clave SHA1 (que será de 40 caracteres convertidos a mayúsculas).

Este sería el código del método Main para Visual Basic, con las importaciones de los espacios de nombres necesarios en todo el código de ejemplo:

Option Strict On
Option Infer On

Imports System

Imports System.Text
Imports System.Security.Cryptography

Module Program
    Sub Main(args As String())
        
        dim valido As Boolean
        dim usuario As String
        dim passw as String

        Do
            Console.Write("Escribe el nombre del usuario: ")
            usuario = Console.ReadLine()
            ' si el nombre del usuario tiene caracteres no permitidos, preguntar de nuevo
            valido = UtilSHA1.ValidarTextoClave(usuario)
            if not valido
                Console.WriteLine("Nombre de usuario NO VÁLIDO.")
            end if
        Loop While Not valido

        Do
            Console.Write("Escribe la clave: ")
            passw = Console.ReadLine()
            ' si la clave tiene caracteres no permitidos, preguntar de nuevo
            valido = UtilSHA1.ValidarTextoClave(passw)
            if not valido
                Console.WriteLine("La clave NO ES VÁLIDA.")
            end if
        Loop While Not valido

        ' generar la clave SHA1 y mostrarla
        dim claveSHA1 = UtilSHA1.GenerarClaveSHA1(usuario, passw)
        Console.WriteLine($"La clave SHA1 es: '{claveSHA1}'.")
            
    End Sub
End Module

 

Este sería el código del método Main para C#, con las importaciones (using) de los espacios de nombres usados en el código:

using System;

using System.Text;
using System.Security.Cryptography;

class Program
{
    static void Main(string[] args)
    {
        bool valido;
        string usuario;
        string passw;

        do
        {
            Console.Write("Escribe el nombre del usuario: ");
            usuario = Console.ReadLine();
            // si el nombre del usuario tiene caracteres no permitidos, preguntar de nuevo
            valido = UtilSHA1.ValidarTextoClave(usuario);
            if (!valido)
                Console.WriteLine("Nombre de usuario NO VÁLIDO.");
        }
        while (!valido);

        do
        {
            Console.Write("Escribe la clave: ");
            passw = Console.ReadLine();
            // si la clave tiene caracteres no permitidos, preguntar de nuevo
            valido = UtilSHA1.ValidarTextoClave(passw);
            if (!valido)
                Console.WriteLine("La clave NO ES VÁLIDA.");
        }
        while (!valido);

        // generar la clave SHA1 y mostrarla
        var claveSHA1 = UtilSHA1.GenerarClaveSHA1(usuario, passw);
        Console.WriteLine($"La clave SHA1 es: '{claveSHA1}'.");
    }
}

 

La clase UtilSHA1 con los métodos para comprobar la validez del texto y generar la clave

A continuación te muestro el código del método para validar el texto del nombre del usuario y el password o contraseña para que no contenga caracteres no deseados.

Para Visual Basic:

''' <summary>
''' Validar caracteres en la clave.
''' No se aceptan ?*%'_ ni --
''' </summary>
Public Shared Function ValidarTextoClave(laClave As String) As Boolean
    Dim sNoVale As String = "?*%'_"

    laClave = laClave.Trim()

    If laClave.IndexOf("--") > -1 Then
        Return False
    End If
    If laClave.IndexOfAny(sNoVale.ToCharArray) > -1 Then
        Return False
    End If

    Return True
End Function

 

Para C#:

/// <summary>
/// Validar caracteres en la clave.
/// No se aceptan ?*%'_ ni --
/// </summary>
public static bool ValidarTextoClave(string laClave)
{
    string sNoVale = "?*%'_";

    laClave = laClave.Trim();

    if (laClave.IndexOf("--") > -1)
        return false;
    if (laClave.IndexOfAny(sNoVale.ToCharArray()) > -1)
        return false;

    return true;
}

 

Y ahora el código con la definición del método GenerarClaveSHA1 en el que indicaremos dos cadenas: el nombre del usuario y el password y a partir de la concatenación de ambas generar el valor SHA1 producido por el método ComputeHash que en realidad devuelve un array de tipo Byte, el cual convertimos en valores hexadecimales (con dos cifras por valor) con idea de que se genere la cadena deseada de 40 caracteres en total, que finalmente convertimos en mayúsculas, pero que bien puedes dejarlo en minúsculas si así te parece mejor.

El código para Visual Basic:

''' <summary>
''' Generar una clave SHA1 para guardarla en lugar del password,
''' de esa forma no se podrá saber la clave.
''' La longitud es de 40 caracteres.
''' </summary>
''' <remarks>
''' Crear una clave SHA1 como la generada por:
''' FormsAuthentication.HashPasswordForStoringInConfigFile
''' Basado en el ejemplo de mi sitio:
''' http://www.elguille.info/NET/dotnet/comprobar_usuario_usando_base_datos_vb2003.htm
''' </remarks>
Public Shared Function GenerarClaveSHA1(nick As String, clave As String) As String
    ' Crear una clave SHA1 como la generada por 
    ' FormsAuthentication.HashPasswordForStoringInConfigFile
    ' Adaptada del ejemplo de la ayuda en la descripción de SHA1 (Clase)
    Dim enc As New UTF8Encoding
    ' Por si el usuario (nick) es nulo
    If String.IsNullOrWhiteSpace(nick) Then
        nick = ""
    Else
        nick = nick.ToLower
    End If
    Dim data() As Byte = enc.GetBytes(nick & clave)
    Dim result() As Byte

    Dim sha As New SHA1CryptoServiceProvider
    ' This is one implementation of the abstract class SHA1.
    result = sha.ComputeHash(data)

    ' Convertir los valores en hexadecimal
    ' cuando tiene una cifra hay que rellenarlo con cero
    ' para que siempre ocupen dos dígitos.
    Dim sb As New StringBuilder
    For i As Integer = 0 To result.Length - 1
        If result(i) < 16 Then
            sb.Append("0")
        End If
        sb.Append(result(i).ToString("x"))
    Next

    Return sb.ToString.ToUpper
End Function

 

El código para C#:

/// <summary>
/// Generar una clave SHA1 para guardarla en lugar del password,
/// de esa forma no se podrá saber la clave.
/// La longitud es de 40 caracteres.
/// </summary>
/// <remarks>
/// Crear una clave SHA1 como la generada por:
/// FormsAuthentication.HashPasswordForStoringInConfigFile
/// Basado en el ejemplo de mi sitio:
/// http://www.elguille.info/NET/dotnet/comprobar_usuario_usando_base_datos_cs2003.htm
/// </remarks>
public static string GenerarClaveSHA1(string nick, string clave)
{
    // Crear una clave SHA1 como la generada por 
    // FormsAuthentication.HashPasswordForStoringInConfigFile
    // Adaptada del ejemplo de la ayuda en la descripción de SHA1 (Clase)
    UTF8Encoding enc = new UTF8Encoding();
    // Por si el usuario (nick) es nulo
    if (string.IsNullOrWhiteSpace(nick))
        nick = "";
    else
        nick = nick.ToLower();
    byte[] data = enc.GetBytes(nick + clave);
    byte[] result;

    SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
    // This is one implementation of the abstract class SHA1.
    result = sha.ComputeHash(data);

    // Convertir los valores en hexadecimal
    // cuando tiene una cifra hay que rellenarlo con cero
    // para que siempre ocupen dos dígitos.
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < result.Length; i++)
    {
        if (result[i] < 16)
            sb.Append("0");
        sb.Append(result[i].ToString("x"));
    }

    return sb.ToString().ToUpper();
}

 

Nota:
El código final del método GenerarClaveSHA1 se puede simplificar para que use dos caracteres hexadecimales sin necesidad de la comparación de si el valor es menor de 16.
El código te lo muestro en el repositorio de github por si quieres intentarlo por tu cuenta 😉

 

Nota importante:
Comentarte que la generación de la clave SHA1 distingue entre mayúsculas y minúsculas, es decir, si al generar la clave SHA1 usaste Guillermo como usuario (o nick) si vuelves a generarla con el nombre en minúsculas (guillermo) el valor generado será diferente.
Esto mismo es aplicable a la contraseña o password.

 

El código completo con los proyectos para usar con .NET 5.0 (tanto con dotnet como con Visual Studio Code o con Visual Studio 2019 v16.8) está publicado en github:
Generar clave SHA1 con el nombre y password del usuario.

 

Y esto es todo… espero que te sea de utilidad… ya sabes que esa es la idea… y si te parece bien (y puedes) no estaría de más que dejaras una propina usando el enlace de PayPal 😉
Gracias.

 

Nos vemos.
Guillermo

La evolución de .Net, una plática de Héctor de León con «El Guille»

Pues eso… hoy he tenido una charla en directo con Héctor de León (del canal hdeleon.net) sobre la evolución de .NET y sobre (la evolución de) un servidor de ustedes… Eso fue a las 19:00 hora de España (12:00 pm hora de México), pero si te lo perdiste o lo quieres volver a ver, aquí te dejo el enlace (y el video insertado), espero que te guste y te sea de utilidad y de paso sabrás un poco más de qué hace ahora el Guille y todas esas cosas… 😉

Aquí te dejo el enlace al susodicho video:

El enlace: La evolución de .Net, una plática con «El Guille» | Invitado Guillermo Som de elguille.info

El video insertado en un iframe:

 

Espero que te sirva, al menos para conocer al que escribe estas cosas tan raras de programación en este blog (www.elguillemola.com) y en el «clásico» de www.elGuille.info

Nos vemos.
Guillermo

sharplab.io una utilidad online para ver el código tal como lo compila .NET

Pues eso… viendo el video de Filip Ekberg sobre los record en C# 9.0 (siempre hay que intentar aprender más) me gustó ver que mientras explicaba el código se veía el código desemsamblado (como lo genera el compilador de .NET) y es usando una herramienta online accesible mediante el enlace a https://sharplab.io/.

Si pones la dirección https://sharplab.io/ en el navegador, te mostrará una ventana con dos paneles.

Con las siguientes capturas te voy explicando un poco cómo configurar el «entorno» de trabajo.

En la figura 1 vemos el entorno ya configurado para usar los compiladores de Roslyn, para eso indicamos master (11 nov 2020) (es la que hay a día de hoy 13 de noviembre) que como ves con más detalle en la figura 2 nos permite seleccionar el «framework» a usar para compilar el código que pongamos en el panel izquierdo.

En la figura 3 puedes ver que se pueden usar los tres lenguajes que se incluyen en .NET, es decir: C#, Visual Basic y F#.

En el panel derecho se muestra el código decompilado, por defecto es en C# (sí, aunque en la izquierda lo escribas con otro lenguaje diferente a C#).

Pero como puedes ver en la figura 4 se puede mostrar de otras formas, eso sí, si quieres ver el código tal como lo trata el compilador, éste será en C#.
Las otras opciones son, entre otras,. el código ensamblado IL o incluso en ejecución (Run).
Ya es cuestión de ir probando cada una de esas opciones de la figura 4 para ir viendo cómo nos muestra el código.

En la figura 5 puedes ver código de Visual Basic (en el panel izquierdo) y el resultado de compilarlo y mostrarlo en C#.

En la figura 6 te muestro un trozo de código de C# 9.0 usando las instrucciones de nivel superior (top-level statements) y los tipos de registro (record).

Pero fíjate que he señalado una clase que (al menos así parece ser) es requerida para poder usar el código de tipo nivel superior, es decir, sin necesidad de crear el método Main, etc.), ya que si quitas ese código te dará error, tal como puedes ver en la figura 7.

Y si te gusta usar el tema oscuro, en la parte inferior derecha (junto a Theme) puedes seleccionar entre Auto, Light y Dark. A mí me ha mostrado el modio claro (Light) cuando lo inicié y ahí abajo me indicaba Auto tal como puedes ver en la figura 1.

Y esto es todo… es cuestión de ir practicando y probar más cosas de esta útil herramienta online.

Figura 1.

Figura 2.

Figura 3.

Figura 4.

Figura 5.

Figura 6.

Figura 7.

Nos vemos.
Guillermo

Novedades de C# 9.0

Pues eso… tal como te dije hace unos días, ya ha llegado el momento de explicarte algunas de las novedades de C# 9.0; concretamente aquí te voy explicar de forma concisa (sin enrollarme mucho) en qué consisten tres de las novedades que trae la versión 9.0 de C#, la que se incluye en .NET 5.0 que en este mes de noviembre estará en modo release o, dicho de otro modo, en versión final.

NOTA:
En realidad hoy día 10 de noviembre lo han publicado, mientras escribía este artículo se hacía la presentación oficial de .NET 5.0 en el .NET Conf 2020 (que puedes ver la grabación usando el enlace).

Estas novedades que te voy a explicar aquí están basadas en la documentación de .NET, concretamente en: Novedades de C# 9.0.
Y las tres novedades que he elegido son las siguientes:
1- Instrucciones de nivel superior (Top-level statements)
2- Registros (Records)
3- Establecedores de solo inicialización (Init only setters)

NOTA:
No me voy a explayar (alargar demasiado) en las explicaciones, si quieres detalles, por favor mira el enlace que te he puesto antes y en la documentación te lo explican con gran lujo de detalles.

Para usar el compilador de C# 9.0 necesitarás tener instalado .NET 5.0 y el código ejecutarlo con Visual Studio 2019 Preview o usando dotnet desde la línea de comandos o con Visual Studio Code.

Instrucciones de nivel superior (top-level statements)

Esta novedad es solo aplicable a un fichero en cada proyecto de C# y viene a ser una forma más simple de escribir el punto de entrada de una aplicación que normalmente se define con el método estático Main. De hecho, si se utiliza esta forma de escribir el código no podrá existir otro punto de entrada diferente, es decir, no podrá haber otro método estático Main que indique por dónde empezar a ejecutar el código.

En cualquier programa de C# nos encontraremos con un código parecido a este (por ejemplo el clásico Hola Mundo:

using System;

namespace novedadescs9_01
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Pero ahora con C# 9.0 lo podemos simplificar de esta forma:

using System;

Console.WriteLine("Hello World!");

Es decir, nos quedamos con el código realmente operativo. En este caso la importación de System para poder acceder al método WriteLine de la clase Console.

Como te comenté antes, esta forma de escribir el código solo se puede hacer en un fichero del proyecto y equivale al método de entrada de dicho proyecto.

Si ese método debe manipular los argumentos de la línea de comando, estos se pueden seguir gestionando con args (aunque no estén indicados en ningún sitio).

Por ejemplo, supongamos que queremos tomar el primer argumento como el nombre al que saludar, lo haríamos de esta forma:

using System;

Console.WriteLine("¡Hola {0}!", args[0]);

De la misma forma, si nuestro método Main (aunque no haya aparentemente ninguno) tiene que devolver un valor, por ejemplo que devuelva 1 si no se ha indicado nada en la línea de comandos o lo indicado está vacío, lo haríamos de la siguiente forma:

using System;

if (args.Length == 0 || string.IsNullOrEmpty(args[0]) )
    return 1;

Console.WriteLine("¡Hola {0}!", args[0]);

return 0;

Si ejecutas el código anterior sin indicar nada (ningún argumento) no hará (aparentemente) nada, pero en realidad si hace, ya que devuelve 1 como resultado de la ejecución. Si se indica algo mostrará el mensaje de saludo y devolverá cero.

Para hacerlo más evidente, podemos cambiar el if de la siguiente forma:

using System;

if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
{
    Console.WriteLine("Debes escribir un nombre.");
    return 1;
}

Console.WriteLine("¡Hola {0}!", args[0]);

return 0;

Ahora estará más claro que no hemos escrito nada como argumento al ejecutar el programa.

NOTA:
Si has elegido usar dotnet puedes hacer las pruebas de la siguientes formas:
(se supone que estás usando la terminal de Visual Studio Code o el símbolo del sistema con el directorio activo donde esté el código.

Si indicas un argumento, será así:
dotnet run Guille
y el resultado será:
¡Hola Guille!

Si no indicas argumentos escribe esto:
dotnet run
y el resultado será:
Debes escribir un nombre.

En la figura 1 puedes ver la salida de las dos formas de usarlo (con o sin argumento).

Figura 1.

 

Registros (Records)

Para escribir el código de la segunda novedad de C# 9.0 vamos a crear un nuevo proyecto, si quieres hacerlo con dotnet desde la línea de comandos, sitúate en la carpeta donde dotnet creará una carpeta con el código necesario para este ejemplo el cual se llamará novedadescs9_02 y escribe lo siguiente:

dotnet new console -o novedadescs9_02

Cambia al directorio recién creado con cd novedadescs9_02 y edita el fichero Program.cs para que tenga el código que te mostraré a continuación, aunque antes un poco de explicación sobre de qué va esto de los registros o records.

Los tipos de registro (record types) es un tipo por referencia que son inmutables de forma predeterminada. Inmutable significa que una vez creados no se pueden modificar.

Hasta ahora los tipos por referencia (clases y tipos anónimos) no contenían los datos que manipulaban si no una referencia a esos datos, mientras que los tipos por valor (entre ellos las estructuras y tuplas) contienen los valores, es decir, si pasamos un tipo por valor a un método se pasa una copia de los datos, a diferencia de los tipos por referencia que pasan una referencia al objeto en memoria que contiene los valores.
Y esto es principalmente lo que hacen los tipos de registro cuando se pasan a un método, no se pasa la referencia, si no una copia de los datos.

De hecho este es uno de los puntos importantes de los tipos record, que al ser inmutables se pueden comparar de la misma forma que comparamos, por ejemplo un tipo entero, y devolverá un valor de igualdad si todos los campos/propiedades que contiene son iguales. Algo que con las clases no ocurre.

Veamos una declaración de un tipo record de la forma más simple (ahora veremos cómo definirlo de una forma parecida a una clase.

NOTA:
Para simplificar voy a usar el código tal y como te he explicado en el primer punto de este artículo como instrucciones de nivel superior (top-level statements).

public record Persona(string Nombre, int Edad);

Esta sería la forma simplificada de definir un registro.
En este caso usamos la palabra clave record y en este caso define un tipo de registro con dos propiedades: Nombre de tipo cadena y Edad de tipo entero.

Para usarla lo haremos de la misma forma que con otros tipos:

var persona1 = new Persona("Guillermo", 63);
Console.WriteLine(persona1);

Fíjate que al mostrar el contenido de la variable persona1 se muestra de una forma ya predefinida, pero como siempre, al forma de actuar de ToString (que es el que se encarga de mostrarlo de esa forma), lo puedes definir a tu antojo.
En ese ejemplo la salida será la siguiente:

Persona { Nombre = Guillermo, Edad = 63 }

Si queremos definir un método ToString personalizado lo haremos de la forma tradicional (ahora tenemos que usar llaves para definir el método):

public record Persona2(string Nombre, int Edad)
{
    public override string ToString()
    {
        return $"{Nombre} nació en {DateTime.Now.Year - Edad}";
    }
}

Por supuesto también podemos usar la herencia con los tipos record, imagina que el tipo Persona2 lo quieres derivar de Persona y añadir la definición de ToString, simplemente lo haríamos así:

public record Persona2(string Nombre, int Edad) : Persona (Nombre, Edad)
{
    public override string ToString()
    {
        return $"{Nombre} nació en {DateTime.Now.Year - Edad}";
    }
}

Aunque aquí no ganamos mucho, pero al menos definimos Persona2 a partir del tipo Persona.

Para ver una clase derivada que añada alguna funcionalidad extra, por ejemplo que tenga otra propiedad más podemos definir el tipo Personaje a partir del tipo Persona, en este caso, el tipo Personaje define una propiedad de tipo entero llamada Desde (además de las dos propiedades que expone el tipo Persona).

public record Personaje(string Nombre, int Edad, int Desde) : Persona(Nombre, Edad)
{
    public override string ToString()
    {
        return $"{Nombre} nació en {DateTime.Now.Year - Edad} y está activo desde {Desde}.";
    }
}

Aquí también definimos el método personalizado ToString, pero si no queremos definirlo, simplemente haríamos lo siguiente:

public record Personaje(string Nombre, int Edad, int Desde) 
                        : Persona(Nombre, Edad);

Al mostrar el contenido del personaje1 el resultado en este caso sería (obviamente) el predeterminado del método ToString.

En la figura 2 tienes la salida del código con la definición propia del método ToString y en la figura 3 la salida sin definir un método ToString para el tipo de registro Personaje.

 

Así que, ya es cosa tuya cómo quieres que se comporte el método ToString.

Por supuesto, puedes definir otros métodos que necesites. Y ya sabes cómo 😉

¿Qué significa que los tipos record son inmutables?

Como te comenté antes, las propiedades definidas en un tipo de registro no se pueden cambiar, son inmutables (como las cadenas de .NET), por tanto si queremos cambiar el valor de una propiedad de un tipo ya en memoria, simplemente NO PODEMOS HACERLO. De hecho, de hacerlo lo que conseguiríamos es crear un nuevo registro con los nuevos valores. Pero vayamos por pasos.
Si tienes la definición de Persona que hemos visto anteriormente y decides cambiar la edad de la persona1 simplemente no podrás, ya que te indicará que Edad es de solo lectura.

Escribe lo siguiente (pongo el código completo para que te resulte más fácil hacer la prueba):

using System;

var persona1 = new Persona("Guillermo", 63);
Console.WriteLine(persona1);
persona1.Edad = 64;
Console.WriteLine(persona1);

public record Persona(string Nombre, int Edad);

Si intentas ejecutar esto desde la línea de comandos con dotnet te mostrará el siguiente error (ver la figura 4):

Figura 4. Error de compilación usando dotnet.

Si ese código lo escribes en Visual Studio 2019 Preview, tal como puedes ver en la figura 5, te mostrará la asignación (lo que te muestro en rojo en el código anterior) como un error que indica que:

Error CS8852: Init-only property or indexer 'Persona.Edad' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
Figura 5. El error tal como lo muestra Visual Studio 2019 Preview.

Ese mensaje nos lo aclarará la siguiente y última sección de este artículo sobre las novedades de C# 9.0. Pero eso dentro de un momento.

var persona2 = persona1 with { Edad = 64 };

Console.WriteLine("La persona2: {0}.",persona2);

Aquí lo que hemos hecho es crear una copia de persona1 con un nuevo valor en la edad, pero realmente son dos objetos en memoria diferentes: Uno con la edad de 63 y el otro con la edad de 64, es decir: ¡hemos clonado al guille con un año más! (de seguir con otro ejemplo más, me jubilaré pronto jejeje).

Fíjate que no hemos usado el tipo Persona para crear el nuevo objeto, simplemente lo hemos creado a partir del que ya estaba definido en persona1, pero con algunos datos diferentes, en este ejemplo simplemente hemos cambiado el valor de Edad.

Ahora veremos lo que te comentaba al principio de la forma de comparar objetos creados a partir de tipos de registro.

Veamos el siguiente código en el que definimos dos objetos del tipo Persona, en los que los hemos inicializado con los mismos datos y después asignamos a la variable iguales el resultado de comparar con == ambos objetos.
como veremos al ejecutar el código es que ambos son iguales, ¡aunque en realidad sean dos objetos diferentes!

var persona1 = new Persona("Guillermo", 63);
Console.WriteLine("La persona1: {0}.", persona1);

var persona2 = new Persona("Guillermo", 63);
Console.WriteLine("La persona2: {0}.", persona2);

var iguales = persona1 == persona2;
Console.WriteLine("persona1 == persona2 es {0}", iguales);


public record Persona(string Nombre, int Edad);

El resultado de ejecutar ese código será el siguiente:

La persona1: Persona { Nombre = Guillermo, Edad = 63 }.
La persona2: Persona { Nombre = Guillermo, Edad = 63 }.
persona1 == persona2 es True

Esa comparación de igualdad la podemos escribir de la siguiente forma:

var iguales = persona1.Equals(persona2);
Console.WriteLine("persona1.Equals(persona2) es {0}", iguales);

El resultado será el mismo que usando el operador de igualdad.

Y para finalizar este apartado de los tipos de registro veamos qué ocurre si tenemos el siguiente código:

var persona2 = persona1 with { Edad = 64 };
Console.WriteLine("La persona2: {0}.", persona2);

var iguales = persona1.Equals(persona2);
Console.WriteLine("persona1.Equals(persona2) es {0}", iguales);

¿Cuál crees que será el valor de iguales?

La respuesta: tendrás que ejecutar el código para comprobarlo 😉

 

Deconstruir tipos de registro

Una cosa interesante con los tipos de registro (record) es lo que se conoce como de-constructor (Deconstruct) (mira la documentación para una explicación detallada) ya que aquí solo te pondré un ejemplo para que sepas que se puede hacer algo como esto:

var persona = new Persona("Guillermo", 63);
Console.WriteLine("La persona: {0}.", persona);

var (n, e) = persona;
Console.WriteLine($"{n} {e}");


public record Persona(string Nombre, int Edad);

Es decir, podemos asignar a una tupla el contenido de un registro, en este ejemplo (tal como puedes comprobar al ejecutar el código) es que se asignan los valores del Nombre y la Edad a la tupla.

 

 

Establecedores de solo inicialización (Init only setters)

Tal como vimos en el penúltimo ejemplo, o mejor dicho en el mensaje de error del penúltimo ejemplo (al que asignamos un valor a la propiedad Edad de un tipo de registro), el mensaje de error indica que las propiedades de solo inicialización solo se pueden asignar en un inicializador de objeto.
Es decir, la propiedad Edad es de solo lectura, pero en este caso, el error nos indica que la propiedad de solo inicialización (init-only property) solo se puede usar en un inicializador de objetos. Y aquí es donde entra esta tercera novedad de C# 9.0 que te quiero comentar, pero veamos primero un código de ejemplo de cómo usar esta nueva instrucción: init.

class Persona
{ 
    public string Nombre { get; init; }
    public int Edad { get; init; }
}

Como ves en el código anterior, en la definición de las propiedades de la clase Persona están los modificadores get e init. Get es, como bien supondrás, la parte que devuelve el valor de la propiedad (lo que hasta ahora hemos tenido) y en este caso init sustituye al modificador set de la propiedad; pero en este caso concreto le indicamos que esa asignación se hará solo y exclusivamente al iniciar una instancia de la clase Persona.

Una forma de definir un objeto persona1 a partir de la clase Persona sería la siguiente:

var persona1 = new Persona { Nombre = "Guillermo", Edad = 63 };

Aquí estamos usando una clase en lugar de un tipo de registro. Por eso esa forma de crear el nuevo objeto. Lo he hecho así, para que no te olvides de cómo crear nuevos objetos a partir de una clase 😉

Como ves el funcionamiento es parecido a lo que hemos visto en la sección anterior, pero el definir una clase es porque los tipos de registro de forma predeterminada tienen el inicializador definido en las propiedades.

Pero el que los tipos de registro sean inmutables no quiere decir que no podamos definir propiedades de lectura/escritura, de hecho podemos hacer algo como esto:

record Persona
{
    public string Nombre { get; set; }
    public int Edad { get; init; }
}

Con el código anterior creamos un record que permite cambiar el valor de la propiedad Nombre, por tanto podemos hacer algo como lo siguiente sin que nos de error:

var persona1 = new Persona { Nombre = "Guillermo", Edad = 63 };
Console.WriteLine(persona1);

persona1.Nombre = "Guille";
Console.WriteLine(persona1);

Y podemos hacerlo porque la propiedad Nombre ya no está definida como solo de inicialización.

Pero aún así, si definimos otro objeto del tipo (record) Persona con los mismos datos, la comparación de igualdad seguirá funcionando como vimos anteriormente.

var persona1 = new Persona { Nombre = "Guillermo", Edad = 63 };
persona1.Nombre = "Guille";

Console.WriteLine(persona1);

var persona2 = new Persona { Nombre = "Guille", Edad = 63 };

var iguales = persona1 == persona2;
Console.WriteLine("persona1 == persona2 es {0}", iguales);

En este caso, el segundo objeto (persona2) lo definimos con «Guille» como nombre, si no, no sería igual que el objeto persona1.

Y seguramente pensarás que ¿Para qué quiero el tipo record, si parece que funciona igual que si lo defino como class?

Bien pensado, pero no, los tipos de registro (record) no funcionan igual que los tipos definidos a partir de una definición de una clase (class), y para muestra un botón.

Veamos el ejemplo anterior usando una clase en lugar de un registro.

using System;

var persona1 = new Persona { Nombre = "Guillermo", Edad = 63 };
persona1.Nombre = "Guille";

Console.WriteLine(persona1);

var persona2 = new Persona { Nombre = "Guille", Edad = 63 };

var iguales = persona1 == persona2;
Console.WriteLine("persona1 == persona2 es {0}", iguales);

class Persona
{
    public string Nombre { get; set; }
    public int Edad { get; init; }
}

Si ejecutas ese código (te recuerdo que Persona está declarado con class en lugar de record) el valor asignado a iguales será false.

Y es False porque en realidad son dos objetos por referencia que no usan las nuevas características de los tipos de registro, en el que la comparación de igualdad se hace comprobando los valores campo a campo (o propiedad a propiedad), mientras que en los tipos «clasicos» por referencia se comprueba si el objeto es el mismo, y en este caso, no son el mismo, cada variable hace referencia a un valor diferente en la memoria.

Otra cosa es que escribamos el siguiente código:

using System;

var persona1 = new Persona { Nombre = "Guillermo", Edad = 63 };
persona1.Nombre = "Guille";

Console.WriteLine(persona1);

var persona2 = persona1;

var iguales = persona1 == persona2;
Console.WriteLine("persona1 == persona2 es {0}", iguales);

var iguales2 = persona1.Equals(persona2);
Console.WriteLine("persona1.Equals(persona2) es {0}", iguales2);

class Persona
{
    public string Nombre { get; set; }
    public int Edad { get; init; }
}

En este caso, tanto el valor de iguales como el de iguales2 será True, ya que solo tenemos un objeto en la memoria, pero dos variables que hacen referencia al mismo objeto y por tanto, si cambiamos el valor de la propiedad Nombre en una de las variables ese cambio se hará efectivo en los dos objetos.

Si no me crees añade el siguiente código antes de la definición de la clase Persona y verás lo que muestra ahora.

Console.WriteLine("Antes de la nueva asignación");
Console.WriteLine(persona1.Nombre);
Console.WriteLine(persona2.Nombre);

persona1.Nombre = "Guillermo";

Console.WriteLine("Después de la nueva asignación");
Console.WriteLine(persona1.Nombre);
Console.WriteLine(persona2.Nombre);

Si ejecutas nuevamente el código con estos cambios, la salida será la siguiente:

Antes de la nueva asignación
Guille
Guille
Después de la nueva asignación
Guillermo
Guillermo

Esto demuestra que las dos variables están apuntando al mismo objeto en memoria.

Para terminar, veamos qué ocurre si en lugar de una clase usáramos un tipo record.

¿Te atreves a dar la respuesta a qué mostraría ese código?
No… o sí… bueno veamos el código y el resultado y así salimos de dudas 😉

using System;

var persona1 = new Persona { Nombre = "Guillermo", Edad = 63 };
persona1.Nombre = "Guille";

Console.WriteLine(persona1);

var persona2 = persona1;

var iguales = persona1 == persona2;
Console.WriteLine("persona1 == persona2 es {0}", iguales);

var iguales2 = persona1.Equals(persona2);
Console.WriteLine("persona1.Equals(persona2) es {0}", iguales2);

Console.WriteLine("Antes de la nueva asignación");
Console.WriteLine(persona1.Nombre);
Console.WriteLine(persona2.Nombre);

persona1.Nombre = "Guillermo";

Console.WriteLine("Después de la nueva asignación");
Console.WriteLine(persona1.Nombre);
Console.WriteLine(persona2.Nombre);

record Persona
{
    public string Nombre { get; set; }
    public int Edad { get; init; }
}

Efectivamente, el resultado es como el anterior.

Ya que al hacer la asignación directamente:

var persona2 = persona1;

Estamos compartiendo la misma dirección de memoria en las dos variables, como puedes comprobar, esto no cambia con los tipos por referencia, sean clases o registros.

Y para terminar, solo comentarte que una asignación como esta:

var persona2 = persona1 with { Nombre = "Guille" };

Solo la podemos hacer con los tipos de registro (record) no con las clases (class).

Si lo intentamos con un tipo Persona definida como class el error sería:

Error CS8858: The receiver type 'Persona' is not a valid record type.

Y con esto te dejo por hoy… espero que te hayas aclarado un poco y si no es así… lo siento, pero no sé cómo explicarlo mejor… y si me surge cómo explicarlo mejor, no dudes que te lo explicaré… todo será cuestión de ir practicando con el nuevo tipo de C# 9.0.

En cualquier caso, como siempre, ¡Espero que te haya sido de utilidad! esa es siempre la intención 😉

Nos vemos.
Guillermo

El código fuente con todo el código usado en el artículo

Lo he publicado en github: Novedades de C# 9.0

El diseñador de WindowsForms de Visual Studio 2019 Preview para aplicaciones .NET Core (.NET 5.0) mejora con la versión 16.8.0 Preview 6

Pues eso… En la preview 6 que la han puesto disponible esta madrugada ya funciona mejor el diseñador de WindowsForms, al menos ya puedes añadir un control, hacer doble-clic en él y se mostrará el método de evento predeterminado.

Hasta ahora no añadía ese método que en el caso de Visual Basic era añadir al final de la definición del método la cláusula Handles con el nombre del evento, y en el caso de C# añadir el evento en el diseñador de formularios.

Private Sub Button1_Click(sender As Object, e As EventArgs)  _ 
            Handles Button1.Click
this.button1.Click += new System.EventHandler(this.button1_Click);

 

Figura 1. El diseñador de formularios (WinForms) en VS2019 v16.8.0 Preview 6 para .NET 5.0 RC2

 

Este fallo lo reporté el 1 de septiembre de este año:
The VB form designer in .NET 5.0 doesn’t add event methods to controls when you double-click the control, it just creates the method, y esta mañana he visto el aviso de que ya está arreglado (fixed) con la nueva versión que han publicado.

Aunque no aparece en la lista de cosas solucionadas en la Preview 6, pero lo importante es saber que ya funciona.

Ahora falta que solucionen el diseñador de menús, barras de herramientas, barras de estado y algún que otro etcétera.

Nos vemos.
Guillermo

gsColorearCodigo

Esta es la última versión de la utilidad gsColorearCodigo (a fecha del 27 de octubre de 2020) en la que he usado Visual Studio 2019 y .NET 4.8.

Pulsa este enlace si quieres ver la versión original (creada para .NET 2.0).

 

Descripción de gsColorearCodigo

Nota del 27/Oct/2020
La revisión actual a la versión 1.0.0.12  (y la instalación de ClickOnce) están ajustadas a la compilación de fecha de hoy martes 27 de octubre de 2020.
También está actualizado el código en github con la «
release» de los ejecutables (por si quieres descargar directamente los binarios y no instalarlos con ClickOnce.
Por supuesto, en github está el código fuente de gsColorearCodigo y de la DLL gsColorearNET creada con .NET Standard 2.0.

 

Nota del 26-oct-2020 20:59:
Esta página estaba originalmente publicada en elguille.info y la he pasado al blog para poder editarla más fácilmente ya que lo que publico en elGuille.info lo hago con un editor de texto normal y corriente… y ¡es un rollo!
Por supuesto, esta misma página aparecerá en elGuille.info, así que… no se notará mucho que no está realmente ubicada allí.

 

NOTA del 24/Oct/2020
Revisión de esta utilidad usando la nueva versión publicada hoy de gsColorearNET en NuGet.
He actualizado también la instalación de ClickOnce.

 

Nota del 12/Sep/2020
Estoy probando la utilidad de colorear en .NET 5.0 Preview 8 y a duras penas ya está operativa…
es que el editor de Visual Studio 2019 Preview está aún muy verde para las aplicaciones de Windows Forms para Visual Basic.

El hacerlo con esa versión es para poder depurar el código de la DLL de colorear, ya que en un proyecto de .NET framework
no se puede… o yo no sé cómo hacerlo… todo hay que decirlo.

Cuando tenga tiempo publicaré en el blog las cosillas que recomiendo hacer hasta que mejoren el editor/diseñador de WinForms para Visual Basic.

Nota del 11/Sep/2020:
Esta nueva versión utiliza la librería de colorear código compilada para .NET Standard 2.0.
Esa DLL la he instalado desde el paquete de NuGet que he creado para la DLL gsColorearNET.
No he probado el instalador de ClickOnce en otro equipo, así que no sé si será totalmente operativo 😉
Al menos lo es en mi propio equipo…
De todas formas, actualizaré el código fuente de la nuevas versión, que aparte de usar esa DLL de colorear,
también tiene otros cambios (mejoras) con respecto a la actualización anterior del 9 de septiembre pasado.

El programa principal y la DLL de colorear están compilados con Visual Studio 2019,
en el caso de la utilidad (gsColorearCodigo.exe) utilizando el .NET Framework 4.8,
y la DLL (gsColorearNET.dll) está compilada para usar .NET Standard 2.0.

 

Entre otras cosas, esta versión (aparte de las mejoras en gsColorearNET) incluye la opción de «Colorear desde RTF» (ver la Figura 1).
Así como algunos cambios en la pestaña de configuración (ver figura 2) y en la ventana de mostrar RTF, que ahora permite
cambiar el código RTF y al pulsar en el botón RTF se muestran los cambios realizados.

 

Opción de colorear desde RTF
Figura 1. Colorear desde RTF

Es la opción que en un 99.99% de las veces utilizo para colorear el código ya que me permite copiar el texto de Visual Studio (es lo que se ve detrás de la utilidad) lo pego en la aplicación y al usar esa opción de Colorear desde RTF lo que hace es colorear para HTML usando el código de RTF, es decir, no comprueba las palabras clave, etc., para generar el código HTML a usar en una página WEB.

 

Opciones de configuración
Figura 2. Pestaña de opciones

 

Instalar gsColorearCodigo (y el código fuente)

Para instalar la utilidad puedes hacerlo de dos formas:

  • Usando el ZIP que hay más abajo, pero tendrás que compilarlo ya que el ejecutable no se incluye, solo el código fuente para Visual Basic.
  • Instalándolo por medio de ClickOnce (recomendado), ya que así podrás recibir notificaciones cuando haya alguna nueva versión e instalarla automáticamente.

En cualquier caso, lo instales directamente o por medio de ClickOnce, desde la ventana de Acerca de puedes comprobar si hay nuevas versiones del programa o también mirando esta página, que al fin y al cabo es la que usa el programa para saber si hay nuevas versiones (o actualizaciones).

Nota:
En realidad la página que mira para ver si hay nuevas actualizaciones es la anterior (la original creada con .NET 2.0) pero he puesto allí la misma versión que en esta… y cuando actualice nuevamente la utilidad, cambiaré el enlace para que mire esta página.
Esto es así porque esta página la he publicado después de compilar y crear el instalador. Smile

 

Más abajo tienes los enlaces con el código completo para Visual Basic usando una solución de Visual Studio 2019 con .NET 4.8 en el que se hace referencia a la DLL gsColorearNET instalada con el package de NuGet.

Espero que te sea de utilidad Smile

Nos vemos.
Guillermo

 

 

El ZIP con el código completo

El código tanto de la aplicación gsColorearCodigo para .NET Framework 4.8 como de la DLL gsColorearNET para .NET Standard 2.0 lo puedes descargar desde GitHub

 

Estos son los enlaces a github para el código fuente:
El código fuente de gsColorearCodigo (la aplicación) para .NET Framework 4.8
El código fuente de gsColorearNET (la DLL) para .NET Standard 2.0

 

Pulsa aquí si la quieres instalar con ClickOnce.

 

gsColorearNET

Nota del 26-oct-2020 20:45:
Esta página estaba originalmente publicada en elguille.info y la he pasado al blog para poder editarla más fácilmente ya que lo que publico en elGuille.info lo hago con un editor de texto normal y corriente… y ¡es un rollo!
Por supuesto, esta misma página aparecerá en elGuille.info, así que… no se notará mucho que no está realmente ubicada allí.

 

Esta es una biblioteca (DLL) compilada para .NET Standard 2.0 y así poder usarla en cualquier plataforma que lo acepte, incluidos los proyectos de .NET Framework 4.6.1 a 4.8 y de .NET Core 2.0 a .NET 5.0, entre otros…
Si quieres ver dónde se pueden usar las DLL compiladas con .NET Standard 2.0 mira este enlace: .NET Standard

Últimas actualizaciones

Nota del 24/Oct/2020 (v1.0.0.14)
He actualizado tanto el paquete de NuGet como el código en GitHub.
También la utilidad de gsColorearCodigo para que use esta nueva versión.

Los cambios principales son para no eliminar las líneas en blanco que haya en el código.
Esto era porque la función Split original de Visual Basic no elimina las líneas en blanco.
Además de usar vbCr en lugar de vbCrLf al examinar cada línea y crear nuevas separaciones, ya que al añadir vbCrLf añadía una línea extra en blanco.

Aunque últimamente se me ha dado el problema que no todos los ficheros tienen el mismo tipo de retorno de carro.
Por tanto he tenido que comprobar antes de hacer el Split qué tipo de retorno de carro tiene: vbCrLf o vbCr o vbLf.

Nota del 16/Sep/2020 (v1.0.0.7)
He actualizado tanto el paquete de NuGet como el código en GitHub.
También la utilidad de gsColorearCodigo para que use esta nueva versión.

Los cambios principales son para no eliminar las líneas en blanco que haya en el código.
Esto era porque la función Split original de Visual Basic no elimina las líneas en blanco.
Además de usar vbCr en lugar de vbCrLf al examinar cada línea y crear nuevas separaciones, ya que al añadir vbCrLf añadía una línea extra en blanco.

Nota del 13/Sep/2020 (v1.0.0.6)
He actualizado tanto el paquete de NuGet como el código en GitHub.
También la utilidad de gsColorearCodigo para que use esta nueva versión.

Con fecha del 12 de septiembre de 2020 he creado un repositorio en gitHub para el código de gsColorearNET.

De ahí puedes descargar el proyecto completo a excepción del fichero de nombre seguro (.snk)

Este es el enlace: gsColorearNET en gitHub.

Y este es el enlace a los paquetes de NuGet: gsColorearNET en NuGet.

Estoy probando la utilidad de colorear en .NET 5.0 Preview 8 y a duras penas ya está operativa… es que el editor de Visual Studio 2019 Preview está aún muy verde para las aplicaciones de Windows Forms para Visual Basic (y para C# tampoco está muy fino).

El hacerlo con esa versión es para poder depurar el código de la DLL de colorear, ya que en un proyecto de .NET framework no se puede… o yo no sé cómo hacerlo… todo hay que decirlo.

Cuando tenga tiempo publicaré en el blog las cosillas que recomiendo hacer hasta que mejoren el editor/diseñador de WinForms para Visual Basic.

NOTA:
Aún la estoy probando desde NuGet ya que usándola como proyecto compila y funciona bien, pero al publicar el paquete en NuGet (aquí tienes el enlace: gsColorearNET en NuGet), no me encuentra los lenguajes de las palabras clave.
¡Esto último ya está solucionado con el paquete 1.0.0.1!
Smile

He publicado la utilidad de colorear código (gsColorearCodigo) para instalar con ClickOnce que utiliza el paquete de NuGet 1.0.0.1 y… ¡funciona a la perfección!

Por ahora no te voy a poner el código fuente, en realidad ha cambiado poco desde lo que publiqué anteayer, y como lo que quiero añadir o corregir aún lo tengo que hacer… pues… habrá que esperar unos días a que lo tenga terminado y así ya la publico de forma, que salvo que haya algunos fallos, no tenga que modificarla en algún tiempo.

Lo que sí haré es actualizar el paquete de NuGet con las correcciones que le vaya haciendo.

Sobre el uso de un proyecto DLL de .NET Standard con un proyecto de .NET Framework

Probando el código de gsColorearCodigo usando el .NET 4.8 en Visual Studio 2019 con el proyecto de gsColorearNET usando .NET Standard 2.0, al producirse un error (o sin que se produzca) y querer depurar en el código de la DLL, el VS no me dejaba…
Así que… para comprobar porqué fallaba el penúltimo cambio que le hice, tuve que
abrir un proyecto de .NET 5.0 (Preview 8) usando el Visual Studio Preview, y en ese entorno si pude depurar y averiguar cuál era el fallo… que después resultó ser algo trivial, pero…

Así qué… ya sabes… si mezclas… no debugues… 😉

Usar código de Visual Basic en .NET Standard

Otra de las cosillas que me he encontrado a la hora de poder compilar el código del proyecto gsColorear para .NET Framework (ya hay que ir haciendo las aclaraciones de que no todo es simplemtente .NET, porque tenemos .NET Framework, .NET Core, .NET Standard y… casi ya… también simplemente .NET refiriéndose a .NET Core) es la definición de algunas de las funciones del ensamblado Microsoft.VisualBasic en concreto de la clase Strings que no están definidas en .NET Standard 2.0, tal como:
Len, Left, Mid, Right, Split, InStr y Trim.

Para no tener que cambiar todo el código que usaba esas funciones (recuerda que el código de colorear lo hice sobre el año 2001, sí, con Visual Basic 6.0, después en diciembre del 2002 lo pasé a la beta de Visual Studio .NET, la aplicación de aquél entonces era HTMLColorCode, aunque debo tener otra aplicación llamada gsEditor… el tiempo no perdona… por la memoria…) así que… me he creado una clase en la que
he definido esas funciones, que (seguramente) pueden mejorarse, pero… así las he programado / codificado / como prefieras decirlo.

Y te pego aquí el código por si te puede ser de ayuda.

'------------------------------------------------------------------------------
' Clase definida en la biblioteca para .NET Standard 2.0            (10/Sep/20)
' Basada en gsColorear y gsColorearCore
'
' VBCompat                                                          (10/Sep/20)
' Clase con instrucciones para compatibilidad con .NET Standard 2.0
'
' Declaro algunas funciones de Microsoft.VisualBasic.Strings
' que no están en .NET Standard 2.0
'
' (c) Guillermo (elGuille) Som, 2020
'------------------------------------------------------------------------------
Option Strict On
Option Infer On
Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Generic
'Imports System.Data
Public Class VBCompat
    ''' <summary>
    ''' Devuelve los caracteres desde la posición (en base 1)
    ''' hasta el final.
    ''' </summary>
    ''' <param name="str"></param>
    ''' <param name="pos"></param>
    Public Shared Function Mid(str As String, pos As Integer) As String
        Return str.Substring(pos - 1)
    End Function
    ''' <summary>
    ''' Devuelve la cadena desde la posición indicada con len caracteres.
    ''' La posición del primer carácter es el 1.
    ''' </summary>
    ''' <param name="str"></param>
    ''' <param name="pos"></param>
    ''' <param name="len"></param>
    ''' <remarks>10/Sep/20/20</remarks>
    Public Shared Function Mid(str As String, pos As Integer, len As Integer) As String
        Return str.Substring(pos - 1, len)
    End Function
    ''' <summary>
    ''' Devuelve el número de caracteres.
    ''' Si es cadena vacía o nulo devuelve 0.
    ''' </summary>
    ''' <param name="str"></param>
    ''' <returns></returns>
    Public Shared Function Len(str As String) As Integer
        If String.IsNullOrEmpty(str) Then Return 0
        Return str.Length
    End Function
    ''' <summary>
    ''' Devuelve los primeros caracteres de la cadena.
    ''' </summary>
    ''' <param name="str"></param>
    ''' <param name="len"></param>
    ''' <returns></returns>
    Public Shared Function Left(str As String, len As Integer) As String
        Return str.Substring(0, If(len > str.Length, str.Length, len))
    End Function
    ''' <summary>
    ''' Devuelve los caracteres indicados desde la derecha.
    ''' </summary>
    ''' <param name="str"></param>
    ''' <param name="len"></param>
    ''' <returns></returns>
    Public Shared Function Right(str As String, len As Integer) As String
        Dim iPos = str.Length - len
        Return str.Substring(iPos, len)
    End Function
    ''' <summary>
    ''' Devuelve la posición (en base 1) de la segunda cadena en la primera
    ''' </summary>
    ''' <param name="str1"></param>
    ''' <param name="str2"></param>
    ''' <returns></returns>
    Public Shared Function InStr(str1 As String, str2 As String) As Integer
        Return str1.IndexOf(str2) + 1
    End Function
    ''' <summary>
    ''' Devuelve la posición (en base 1) de la segunda cadena en la primera 
    ''' empezando en la posición indicada.
    ''' </summary>
    ''' <param name="startPos"></param>
    ''' <param name="str1"></param>
    ''' <param name="str2"></param>
    ''' <returns></returns>
    Public Shared Function InStr(startPos As Integer, str1 As String, str2 As String) As Integer
        Return str1.IndexOf(str2, startPos - 1) + 1
    End Function
    ''' <summary>
    ''' Devuelve una cadena después de haber quitado 
    ''' los espacios delante y detrás.
    ''' </summary>
    ''' <param name="str"></param>
    ''' <returns></returns>
    Public Shared Function Trim(str As String) As String
        Return str.Trim
    End Function
    ''' <summary>
    ''' Divide una cadena en elementos de un array.
    ''' Usando el delimitador indicado.
    ''' </summary>
    ''' <param name="Expression"></param>
    ''' <param name="Delimiter"></param>
    ''' <returns></returns>
    Public Shared Function Split(Expression As String, Optional Delimiter As String = " ") As String()
        Return Expression.Split(Delimiter.ToCharArray, StringSplitOptions.RemoveEmptyEntries)
    End Function
    ''
    '' El código IL de Prueba1 es más corto (y parece que eficiente) que el de Prueba2
    ''
    'Public Shared Function Prueba1(str As String, len As Integer) As String
    '    Return str.Substring(0, If(len > str.Length, str.Length, len))
    'End Function
    'Public Shared Function Prueba2(str As String, len As Integer) As String
    '    If len > str.Length Then
    '        len = str.Length
    '    End If
    '    Return str.Substring(0, len)
    'End Function
End Class

Y esto es todo por ahora…

Espero que te sea de utilidad

Nos vemos.
Guillermo

 

 

El código fuente completo

El código para Visual Basic con el proyecto para Visual Studio 2019 está publicado en gitHub.

 

Este es el enlace: gsColorearNET en gitHub.

 

Y este es el enlace a los paquetes de NuGet: gsColorearNET en NuGet.

 

 

Truco por si asignas la posición del formulario a una guardada y cambias de tipo de pantalla

Pues eso… que hace poco mandé al servicio técnico de LG la pantalla externa que conecto a mi ordenador portátil y estoy teniendo problemas cuando abro las aplicaciones que he estado usando con ese monitor, al menos las que tengo programadas para que al cambiar la posición o el tamaño se guardan en un fichero de configuración para cuando cargue de nuevo esa aplicación se muestre donde estuvo la vez anterior.

Una de las opciones por la que opté fue poner que siempre se mostrasen en el centro de la pantalla, pero con el tamaño indicado en el diseño del formulario.

La idea que se me ha ocurrido esta mañana es la de comprobar dónde está la posición Left y Top guardadas y si estarían fuera del rango de esos mismos valores de las propiedades de el WorkingArea de PrimaryScreen.

Los valores de alto y ancho (Height y Width) no los toco, ya que el usuario podrá cambiar esos tamaños a su antojo, ya que ahora sí que podrá ver la ventana de la aplicación.

Este es el código para Visual Basic con el que hago la comprobación que te he mencionado:

' Asignar el tamaño y última posición
' Comprobar que esté en la parte visible                    (24/Oct/20)
Dim l = cfg.GetValue("Ventana", "Left", Me.Left)
Dim t = cfg.GetValue("Ventana", "Top", Me.Top)
If Screen.PrimaryScreen.WorkingArea.Left < l Then
    Me.Left = cfg.GetValue("Ventana", "Left", Me.Left)
Else
    Me.Left = 0
End If
If Screen.PrimaryScreen.WorkingArea.Top < t Then
    Me.Top = cfg.GetValue("Ventana", "Top", Me.Top)
Else
    Me.Top = 0
End If
Me.Height = cfg.GetValue("Ventana", "Height", Me.Height)
Me.Width = cfg.GetValue("Ventana", "Width", Me.Width)

Los valores de la posición y tamaño del formulario los obtengo de un fichero de configuración y solo asigno el valor Left si la posición Left de WorkingArea es menor, ya que cuando está en el monitor externo (al menos en mi caso) el valor de Left suele ser negativo.
Y con el valor Top hacemos lo mismo, solo asignarlo si no es menor que el valor Top del área de trabajo de la pantalla principal.

 

Y aquí tienes el mismo código para C#:

// Asignar el tamaño y última posición
// Comprobar que esté en la parte visible                    (24/Oct/20)
var l = cfg.GetValue("Ventana", "Left", this.Left);
var t = cfg.GetValue("Ventana", "Top", this.Top);
if (Screen.PrimaryScreen.WorkingArea.Left < l)
    this.Left = cfg.GetValue("Ventana", "Left", this.Left);
else
    this.Left = 0;

if (Screen.PrimaryScreen.WorkingArea.Top < t)
    this.Top = cfg.GetValue("Ventana", "Top", this.Top);
else
    this.Top = 0;

this.Height = cfg.GetValue("Ventana", "Height", this.Height);
this.Width = cfg.GetValue("Ventana", "Width", this.Width);

 

Y esto es todo, espero que te haya sido de utilidad.

 

Nos vemos.
Guillermo

Moderniza tu aplicación con el efecto grisáceo en el texto de los controles con nota de lo que hay que escribir en VB y C#

Pues eso… seguramente habrás visto en algunas aplicaciones que en las cajas de texto (o los ComboBox) se muestra un texto en color gris para indicarte lo que puedes escribir en esa casilla.

Para tener esa funcionalidad en nuestros proyectos no es necesario usar controles especializados ni nada de eso, ya verás que es fácil, y espero que claro, hacerlo mediante unas pocas líneas de código.

Actualizado y corregido el código (24-oct-2020)
Ver más abajo el comentario y el nuevo código de QuitarPredeterminado.

 

En la figura 1 puedes ver el aspecto de esa caja de texto.

Figura 1. La aplicación en modo ejecución

En la imagen anterior puedes ver el texto <Escribe tu nombre> en la caja de texto, cuando empieces a escribir, ese texto informativo se quita y solo estará lo que escribas, y si borras todo el texto (dejas vacía la caja de texto) se volverá a mostrar ese texto informativo.

¿Dónde controlar si se debe mostrar o no el texto predeterminado?

En mi caso, yo hago las comprobaciones en tres eventos del (en este caso) TextBox. A saber:

El evento Enter (cuando toma el foco) ahí se comprueba si debe tener el color grisáceo o el negro del texto.
Se comprueba si el texto es diferente del predeterminado (el de ayuda o informativo), en ese caso se asigna el color del texto a ControlText (definido en SystemColors).

El evento Leave (cuando pierde el foco) ahí compruebo si no hay texto, en cuyo caso, asigno el texto informativo y le asigno a la propiedad ForeColor el color GrayText (también de la clase SystemColors).

El evento TextChange (cuando el texto cambia), hay lo que hay que hacer es más elaborado (menos simple que en los casos anteriores), al menos como yo lo he hecho, que puede que haya un método más fácil y simple de hacerlo. Pero es lo que le da esa vidilla al efecto ;-).

Yo suelo usar una variable a nivel de formulario llamada inicializando que me sirve para evitar la entrada en cascada del evento TextChange (y otros eventos, pero en este ejemplo es el que se produce cuando el texto cambia). Si el valor de esa variable es True, salimos del método.

La primera comprobación es si el texto del control está vacío o tiene el texto predeterminado, en cuyo caso lo ponemos en color gris y le asigno el texto informativo.
Si no se da esa comprobación, el texto ya tiene algo, por ejemplo, si se ha empezado a escribir, y compruebo si el texto que había antes era el texto vacío en cuyo caso le quito el texto predeterminado con idea de que solo se quede lo que el usuario está escribiendo.

La primera idea que tuve fue quitar directamente el texto predeterminado con un Replace(textoPredeterminado, «»), pero eso solo vale si se está escribiendo al principio o al final del texto (que es lo más común), pero si el usuario se pone a escribir en medio del texto predeterminado, no funcionaría… por tanto, he creado una función en la que quito todo el texto predeterminado del texto que haya, para así dejar solo lo que se esté escribiendo, que será la primera letra que el usuario escriba.

Esa función yo la tengo como una extensión de la clase String, pero en el código que te mostraré, es simplemente una función llamada QuitarPredeterminado que recibe dos argumentos, el texto a comprobar y el texto a quitar. De esa forma nos servirá para aplicarla a cualquier control y evitar tener que repetir lo mismo en todos los evento TextChange de los controles a los que queramos dar esta funcionalidad.

Aquí (y en la comprobación anterior) entra en juego la variable inicializando, que es la que evitará que se entre nuevamente mientras modificamos el texto.

Finalmente asignamos a la variable que contiene el texto anterior, el texto actual para que no se quite nada cuando ya no sea el texto predeterminado, ya que es posible que el usuario escriba en el texto ese mismo texto que damos por predeterminado, el problema es que el usuario solo escriba el texto predeterminado, pero… bueno, se supone que no lo escribirá… 😉

Y esto es todo lo que hay que hacer… viendo ahora el código para Visual Basic y C# lo entenderás mejor.

 

Rectificación del código (24-oct-2020)

Pues resulta que hay que hacer una comprobación extra antes de quitar los caracteres del texto predeterminado, si no, puede pasar lo que me pasó anoche, que al asignar la palabra InitializeComponent a la caja de textos donde está la palabra predeterminada Buscar… pues… se encontró con la a (de Buscar) y la quitó…
Así que… ahora compruebo primero si están todas las letras del texto predeterminado y de ser así, entonces hago el reemplazo, si no están todas las letras, simplemente se devuelve el texto original y como si nada.

El código de QuitarPredeterminado que te muestro más abajo ya tiene las modificaciones indicadas.

El código completo, tanto para VB como para C#, publicado en github lo actualizaré en unos minutos.

El código de ejemplo para Visual Basic .NET y C#

Empezaré mostrándote las variables que necesitaremos a nivel de formulario.
Es decir, la que controla si ya estamos dentro del evento, la que contiene el texto predeterminado y la que contiene el texto anterior de la caja de texto.

Estas dos últimas serán diferentes para cada control a los que queramos aplicar el efecto. Al menos la del texto anterior, ya que el texto predeterminado puede ser el mismo para varios controles.

Visual Basic .NET

Private Const textVacio As String = "<Escribe tu nombre>"
Private textAnterior As String = textVacio
Private inicializando As Boolean

C#

private const string textVacio = "<Escribe tu nombre>";
private string textAnterior = textVacio;
private bool inicializando;

 

Ahora te muestro el código de los eventos Enter y Leave.

Visual Basic .NET

Private Sub txtTexto_Enter(sender As Object, e As EventArgs) Handles txtTexto.Enter
    If txtTexto.Text <> textVacio Then
        txtTexto.ForeColor = SystemColors.ControlText
    End If
End Sub

Private Sub txtTexto_Leave(sender As Object, e As EventArgs) Handles txtTexto.Leave
    If String.IsNullOrEmpty(Me.txtTexto.Text) Then '
        Me.txtTexto.ForeColor = SystemColors.GrayText
        Me.txtTexto.Text = textVacio
    End If
End Sub

 

C#

private void txtTexto_Enter(object sender, EventArgs e)
{
    if (txtTexto.Text != textVacio)
        txtTexto.ForeColor = SystemColors.ControlText;
}

private void txtTexto_Leave(object sender, EventArgs e)
{
    if (string.IsNullOrEmpty(this.txtTexto.Text))
    {
        this.txtTexto.ForeColor = SystemColors.GrayText;
        this.txtTexto.Text = textVacio;
    }
}

 

El código del evento TextChanged en el que uso la función para quitar el texto predeterminado esté en la posición que esté.
Después te muestro el código de esa función QuitarPredeterminado.

Visual Basic .NET

Private Sub txtTexto_TextChanged(sender As Object,
                                 e As EventArgs) Handles txtTexto.TextChanged
    If inicializando Then Return
    If txtTexto.Text = "" OrElse txtTexto.Text = textVacio Then
        txtTexto.ForeColor = SystemColors.GrayText
        inicializando = True
        txtTexto.Text = textVacio
        inicializando = False
    Else
        If textAnterior = textVacio Then
            inicializando = True
            txtTexto.Text = QuitarPredeterminado(txtTexto.Text, textVacio)
            inicializando = False

            txtTexto.SelectionStart = txtTexto.Text.Length
        End If
        txtTexto.ForeColor = SystemColors.ControlText
    End If
    textAnterior = txtTexto.Text
End Sub

 

C#

private void txtTexto_TextChanged(object sender, EventArgs e)
{
    if (inicializando)
        return;
    if (txtTexto.Text == "" || txtTexto.Text == textVacio)
    {
        txtTexto.ForeColor = SystemColors.GrayText;
        inicializando = true;
        txtTexto.Text = textVacio;
        inicializando = false;
    }
    else
    {
        if (textAnterior == textVacio)
        {
            inicializando = true;
            txtTexto.Text = QuitarPredeterminado(txtTexto.Text, textVacio);
            inicializando = false;

            txtTexto.SelectionStart = txtTexto.Text.Length;
        }
        txtTexto.ForeColor = SystemColors.ControlText;
    }
    textAnterior = txtTexto.Text;
}

 

Fíjate que antes de asignar un texto al TextBox asigno el valor true a inicializando, de esa forma, cuando se cambie el texto (al asignarlo se cambiará), no entrará nuevamente en el evento.

Y como te comentaba antes, cuando el texto que había anterior es el predeterminado (textVacio), quitamos dicho texto de la propiedad Text de la caja de textos mediante una llamada al método QuitarPredeterminado, al que le pasamos el texto que queremos comprobar (el del control TextBox) y el que queremos quitar.

Veamos el código de ese método… y verás lo casi retorcido que es… 🙂
En serio, lo que hago es recorrer cada carácter del texto a quitar (el predeterminado) y quitarlo del texto, de esa forma, esté donde esté ese carácter que queremos quitar lo hará correctamente.

Aquí tienes el código de la función QuitarPredeterminado.

Visual Basic .NET

''' <summary>
''' Quitar de una cadena un texto indicado (que será el predeterminado cuando está vacío).
''' Por ejemplo si el texto grisáceo es Buscar... y
''' se empezó a escribir en medio del texto (o en cualquier parte)
''' BuscarL... se quitará Buscar... y se dejará L.
''' Antes de hacer cambios se comprueba si el texto predeterminado está al completo 
''' en el texto en el que se hará el cambio.
''' </summary>
''' <param name="texto">El texto en el que se hará la sustitución.</param>
''' <param name="predeterminado">El texto a quitar.</param>
''' <returns>Una cadena con el texto predeterminado quitado.</returns>
''' <remarks>18/Oct/2020 actualizado 24/Oct/2020</remarks>
Public Function QuitarPredeterminado(texto As String, predeterminado As String) As String
    Dim cuantos = predeterminado.Length
    Dim k = 0

    For i = 0 To predeterminado.Length - 1
        Dim j = texto.IndexOf(predeterminado(i))
        If j = -1 Then Continue For
        k += 1
    Next
    ' si k es distinto de cuantos es que no están todos lo caracteres a quitar
    If k <> cuantos Then
        Return texto
    End If

    For i = 0 To predeterminado.Length - 1
        Dim j = texto.IndexOf(predeterminado(i))
        If j = -1 Then Continue For
        If j = 0 Then
            texto = texto.Substring(j + 1)
        Else
            texto = texto.Substring(0, j) & texto.Substring(j + 1)
        End If
    Next

    Return texto
End Function

C#

/// <summary>
/// Quitar de una cadena un texto indicado (que será el predeterminado cuando está vacío).
/// Por ejemplo si el texto grisáceo es Buscar... y
/// se empezó a escribir en medio del texto (o en cualquier parte)
/// BuscarL... se quitará Buscar... y se dejará L.
/// Antes de hacer cambios se comprueba si el texto predeterminado está al completo 
/// en el texto en el que se hará el cambio.
/// </summary>
/// <param name="texto">El texto en el que se hará la sustitución.</param>
/// <param name="predeterminado">El texto a quitar.</param>
/// <returns>Una cadena con el texto predeterminado quitado.</returns>
/// <remarks>18/Oct/2020 actualizado 24/Oct/2020</remarks>
private string QuitarPredeterminado(string texto, string predeterminado)
{
    var cuantos = predeterminado.Length;
    var k = 0;

    for (var i = 0; i < predeterminado.Length; i++)
    {
        var j = texto.IndexOf(predeterminado[i]);
        if (j == -1)
            continue;
        k += 1;
    }
    // si k es distinto de cuantos es que no están todos lo caracteres a quitar
    if (k != cuantos)
        return texto;

    for (var i = 0; i < predeterminado.Length; i++)
    {
        var j = texto.IndexOf(predeterminado[i]);
        if (j == -1)
            continue;
        if (j == 0)
            texto = texto.Substring(j + 1);
        else
            texto = texto.Substring(0, j) + texto.Substring(j + 1);
    }
    return texto;
}

 

Y esto es todo… yo lo estoy usando de varias formas, por ejemplo en los TextBox (en realidad un ComboBox del tipo ToolStripComboBox) para Buscar y otro para Reemplazar, en el primero muestro de forma predeterminada el texto Buscar… y el de reemplazar el texto Reemplazar….

La idea la tomé prestada, con estas mejoras que te he mostrado aquí, del proyecto CSharp2VB de Paul1956.

 

Publicaré Ya está el código completo de ejemplo tanto para Visual Basic como para C# (proyectos para .NET Framework 4.8, aunque sirven igualmente con proyectos para .NET 5.0 RC2) publicado en GitHub: Mostrar-texto-grisaceo.

 

Espero que te sea de utilidad.

 

Nos vemos.
Guillermo

Detectar varias pulsaciones de teclas en aplicación de Windows Forms (código para C# y Visual Basic)

Pues eso… necesitaba saber cómo detectar varias pulsaciones de teclas al estilo de Ctrl+K, Ctrl+C y similares, es decir, se pulsa la tecla Control seguida de la K y se pulsa la tecla Control seguida de la C (como la combinación de Visual Studio para poner comentarios en la selección que haya en el código). Así que… busqué en internet, pero… había ejemplos muy enrevesados… con temporizadores y demás monadas… así que… basándome en algunos ejemplos (seguramente del mismo autor o copiados unos de otros) he hecho algo que puede servir… al menos a mí me sirve, aunque se puede mejorar, como todo.

De la forma que lo he hecho da igual si se pulsa primero Ctrl+K que Ctrl+C, ya que lo que he intentado es que se sepa cuando se han hecho esas pulsaciones, y si entre cualquiera de las dos pulsaciones se ha pulsado otra tecla, no se tiene en cuenta esa combinación. Es decir si quieres detectar Ctrl+K seguida de Ctrl+C (que para el caso del código que te mostraré es lo mismo que si pulsas Ctrl+C seguida de Ctrl+K) pulsas otra tecla o combinación de teclas, no se dará como detectada esa doble pulsación.

Nota:
Precisamente con esas teclas: Ctrl+C y otras automatizadas de edición: Ctrl+V, Ctrl+X, Ctrl+P, etc., habría que tener cuidado o hacerle un seguimiento distinto al que ahora hago para que no la detecte y, por ejemplo pegue el texto si es Ctrl+P).

¿Dónde se hará la comprobación de la tecla pulsada?

Las comprobaciones de qué tecla se está pulsando (o se ha pulsado) la hago en el evento KeyDown del formulario. Y como de forma predeterminada el formulario no intercepta las pulsaciones de las teclas, habrá que hacer una asignación de un valor verdadero (true) a la propiedad KeyPreview del formulario. Eso lo he puesto en el evento Load, con idea de que esté activado si por casualidad cambio el valor en el diseñador (esas cosas suelen ocurrir, y es complicado de saber porqué antes funcionaba y después no).

En la figura 1 tienes una captura del código de ejemplo en funcionamiento (en ese caso la aplicación de C# creada con .NET Framework 4.8).

Figura 1. La aplicación de ejemplo en funcionamiento.

¿Cómo saber si hay varias combinaciones de teclas?

Lo que yo he hecho es crear unas variables para asignarles un valor si la combinación que se quiere detectar se cumple. Esas variables (o campos) definidas a nivel de la clase, las he declarado de tipo entero, (en los ejemplos que vi en la web eran de tipo Boolean, bool en C#), ya que lo que me interesa es saber si algunas de las combinaciones se ha hecho más de una vez, ese es el caso de Ctrl+K, Ctrl+K, es decir: pulsar dos veces la tecla Control y la tecla K.

Si no quieres comprobar si hay una combinación que se haga más de una vez, lo mismo puedes usar variables de tipo Bolean (bool en C#), eso ya a tu discreción (o preferencia).

Aquí te muestro el código con la definición de esas variables (tanto para VB como para C#):

' Para doble pulsación de teclas
Private CtrlK As Integer
Private CtrlC As Integer
Private CtrlU As Integer
Private CtrlL As Integer
Private ShiftAltL As Integer
Private ShiftAltS As Integer
// Para doble pulsación de teclas
private int CtrlK;
private int CtrlC;
private int CtrlU;
private int CtrlL;
private int ShiftAltL;
private int ShiftAltS;

Esas variables las usaremos en el evento KeyDown del formulario, incrementando el valor cuando se cumpla que se han pulsado las teclas indicadas, por ejemplo si queremos detectar la pulsación de Ctrl+K, tendremos que incrementar el valor de la variable CtrlK, ídem con el resto.

Como en el ejemplo hay varias combinaciones de teclas que detectar, puede ser un poco largo de ver, pero prefiero que lo veas completo para que no te líes demasiado.
Ahí se comprueban las tres posibles teclas de «control«, es decir, Control, Shift y Alt. También hago comprobaciones para que, por ejemplo, si queremos detectar Ctrl+Shift se haga en un bloque de código diferente para cuando se detecta, por ejemplo Ctrl+Alt o Shift+Alt.
Creo que el código está bastante claro y no tendrás complicaciones de ver el proceso que se hace.

Aquí tienes el código de VB y C#.

Private Sub Form1_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown
        ' Comprobaciones para Ctrl+Shift

        ' esta de forma simple
        If e.Control AndAlso e.Shift Then
            If e.KeyCode = Keys.V Then
                e.Handled = True

                'MostrarRecortes();
                txtPulsadas.Text = "Capturada: Ctrl+Shift+V" & vbCrLf & txtPulsadas.Text

            End If

            ' Estas son con varias combinaciones

            ' Comprobaciones para Shift+Alt
        ElseIf e.Shift AndAlso e.Alt Then
            ' si se ha pulsado Shift+Alt+L
            If e.KeyCode = Keys.L Then
                e.Handled = True

                ShiftAltL += 1
                ' si se ha pulsado Shift+Alt+S
            ElseIf e.KeyCode = Keys.S Then
                e.Handled = True

                ShiftAltS += 1
            End If
            ' Si se ha pulsado Shitf+Alt+S, Shift+Alt+L
            ' (en cualquier orden)
            If ShiftAltL = 1 AndAlso ShiftAltS = 1 Then
                e.Handled = True

                'ClasificarSeleccion();
                txtPulsadas.Text = "Capturada: Shift+Alt+L, Shift+Alt+S" & vbCrLf & txtPulsadas.Text

            End If

            ' Comprobaciones para solo la tecla Ctrl (sin Shift ni Alt)
        ElseIf e.Control AndAlso Not e.Shift AndAlso Not e.Alt Then
            ' Solo se ha pulsado la tecla Ctrl
            ' comprobar el resto de combinaciones
            ' Forma simple si se ha pulsado Ctrl+B
            If e.KeyCode = Keys.B Then
                e.Handled = True

                ' Esta solo es para detectar
                ' la combinación 'simple' de Ctrl+B
                ' No es necesario llevar la cuenta de las pulsaciones
                txtPulsadas.Text = "Capturada: Ctrl+B" & vbCrLf & txtPulsadas.Text


            ElseIf e.KeyCode = Keys.K Then
                e.Handled = True

                CtrlK += 1
                txtPulsadas.Text = "Ctrl+K - " & txtPulsadas.Text

            ElseIf e.KeyCode = Keys.C Then
                e.Handled = True

                CtrlC += 1
                txtPulsadas.Text = "Ctrl+C - " & txtPulsadas.Text

            ElseIf e.KeyCode = Keys.U Then
                e.Handled = True

                CtrlU += 1
                txtPulsadas.Text = "Ctrl+U - " & txtPulsadas.Text

            ElseIf e.KeyCode = Keys.L Then
                e.Handled = True

                CtrlL += 1
                txtPulsadas.Text = "Ctrl+L - " & txtPulsadas.Text

            End If

            ' Si se ha pulsado Ctrl+K, CtrlC
            If CtrlK = 1 AndAlso CtrlC = 1 Then
                e.Handled = True

                ' Ctrl+K, Ctrl+C
                CtrlK = 0
                CtrlC = 0
                'PonerComentarios(richTextBoxCodigo);
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+C" & vbCrLf & txtPulsadas.Text


                ' Si se ha pulsado Ctrl+K, Ctrl+U
            ElseIf CtrlK = 1 AndAlso CtrlU = 1 Then
                e.Handled = True

                ' Ctrl+K, Ctrl+U
                CtrlK = 0
                CtrlU = 0
                'QuitarComentarios(richTextBoxCodigo);
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+U" & vbCrLf & txtPulsadas.Text


                ' Si se ha pulsado Ctrl+K, Ctrl+L
            ElseIf CtrlK = 1 AndAlso CtrlL = 1 Then
                e.Handled = True

                ' Ctrl+K, Ctrl+L
                CtrlK = 0
                CtrlL = 0
                ' preguntar
                'buttonEditorMarcadorQuitarTodos.PerformClick();
                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+L" & vbCrLf & txtPulsadas.Text


                ' Si se ha pulsado Ctrl+K, Ctrl+K
            ElseIf CtrlK = 2 Then
                e.Handled = True

                ' Ctrl+K, Ctrl+K
                CtrlK = 0
                'MarcadorPonerQuitar();

                txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+K" & vbCrLf & txtPulsadas.Text

            End If
        Else
            txtPulsadas.Text = $"{vbCrLf}No es una de las teclas comprobadas: {e.KeyCode} +{vbCrLf}" &
                               $"    Ctrl: {e.Control}, Shift: {e.Shift}, Alt: {e.Alt}{vbCrLf}" & txtPulsadas.Text

            CtrlK = 0
            CtrlC = 0
            CtrlU = 0
            ShiftAltL = 0
            ShiftAltS = 0

            ' Otras pulsaciones
            ' No están detectadas explícitamente

        End If
    End Sub
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
    // Comprobaciones para Ctrl+Shift

    // esta de forma simple
    if (e.Control && e.Shift)
    {
        if (e.KeyCode == Keys.V)
        {
            e.Handled = true;

            //MostrarRecortes();
            txtPulsadas.Text = "Capturada: Ctrl+Shift+V\r\n" + txtPulsadas.Text;

        }
    }
            
    // Estas son con varias combinaciones

    // Comprobaciones para Shift+Alt
    else if (e.Shift && e.Alt)
    {
        // si se ha pulsado Shift+Alt+L
        if (e.KeyCode == Keys.L)
        {
            e.Handled = true;

            ShiftAltL += 1;
        }
        // si se ha pulsado Shift+Alt+S
        else if (e.KeyCode == Keys.S)
        {
            e.Handled = true;

            ShiftAltS += 1;
        }
        // Si se ha pulsado Shitf+Alt+S, Shift+Alt+L
        // (en cualquier orden)
        if (ShiftAltL == 1 && ShiftAltS == 1)
        {
            e.Handled = true;

            //ClasificarSeleccion();
            txtPulsadas.Text = "Capturada: Shift+Alt+L, Shift+Alt+S\r\n" + txtPulsadas.Text;

        }
    }

    // Comprobaciones para solo la tecla Ctrl (sin Shift ni Alt)
    else if (e.Control && !e.Shift && !e.Alt)
    {
        // Solo se ha pulsado la tecla Ctrl
        // comprobar el resto de combinaciones

        // Forma simple si se ha pulsado Ctrl+B
        if (e.KeyCode == Keys.B)
        {
            e.Handled = true;

            // Esta solo es para detectar
            // la combinación 'simple' de Ctrl+B
            // No es necesario llevar la cuenta de las pulsaciones
            txtPulsadas.Text = "Capturada: Ctrl+B\r\n" + txtPulsadas.Text;

        }

        else if (e.KeyCode == Keys.K)
        {
            e.Handled = true;

            CtrlK += 1;
            txtPulsadas.Text = "Ctrl+K - " + txtPulsadas.Text;

        }
        else if (e.KeyCode == Keys.C)
        {
            e.Handled = true;

            CtrlC += 1;
            txtPulsadas.Text = "Ctrl+C - " + txtPulsadas.Text;

        }
        else if (e.KeyCode == Keys.U)
        {
            e.Handled = true;

            CtrlU += 1;
            txtPulsadas.Text = "Ctrl+U - " + txtPulsadas.Text;

        }
        else if (e.KeyCode == Keys.L)
        {
            e.Handled = true;

            CtrlL += 1;
            txtPulsadas.Text = "Ctrl+L - " + txtPulsadas.Text;

        }

        // Si se ha pulsado Ctrl+K, CtrlC
        if (CtrlK == 1 && CtrlC == 1)
        {
            e.Handled = true;

            // Ctrl+K, Ctrl+C
            CtrlK = 0;
            CtrlC = 0;
            //PonerComentarios(richTextBoxCodigo);
            txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+C\r\n" + txtPulsadas.Text;

        }

        // Si se ha pulsado Ctrl+K, Ctrl+U
        else if (CtrlK == 1 && CtrlU == 1)
        {
            e.Handled = true;

            // Ctrl+K, Ctrl+U
            CtrlK = 0;
            CtrlU = 0;
            //QuitarComentarios(richTextBoxCodigo);
            txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+U\r\n" + txtPulsadas.Text;

        }

        // Si se ha pulsado Ctrl+K, Ctrl+L
        else if (CtrlK == 1 && CtrlL == 1)
        {
            e.Handled = true;

            // Ctrl+K, Ctrl+L
            CtrlK = 0;
            CtrlL = 0;
            // preguntar
            //buttonEditorMarcadorQuitarTodos.PerformClick();
            txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+L\r\n" + txtPulsadas.Text;

        }

        // Si se ha pulsado Ctrl+K, Ctrl+K
        else if (CtrlK == 2)
        {
            e.Handled = true;

            // Ctrl+K, Ctrl+K
            CtrlK = 0;
            //MarcadorPonerQuitar();

            txtPulsadas.Text = "Capturada: Ctrl+K, Ctrl+K\r\n" + txtPulsadas.Text;

        }
    }
    else
    {
        txtPulsadas.Text = $"\r\nNo es una de las teclas comprobadas: {e.KeyCode} +\r\n"+
                           $"    Ctrl: {e.Control}, Shift: {e.Shift}, Alt: {e.Alt}\r\n" + txtPulsadas.Text;
        CtrlK = 0;
        CtrlC = 0;
        CtrlU = 0;
        ShiftAltL = 0;
        ShiftAltS = 0;

        // Otras pulsaciones
        // No están detectadas explícitamente
    }
}

Nota:
En el código están comentadas las funciones / métodos a los que en el programa que uso esta forma de controlar las pulsaciones (múltiples) de teclas llama cuando se produce una. Así sabrás cuándo tienes que actuar cuando se produzca la pulsación esperada.

Una aclaración sobre la diferencia entre KeyCode y KeyValue

Hay gente que no se aclara entre los valores de esas dos propiedades del argumento KeyEventArgs del evento KeyDown (o KeyUp).

KeyCode contiene el código de la tecla pulsada y el del tipo Keys (enumeración).
KeyValue contiene el código de la tecla pulsada y el de tipo Integer (int en C#).

En Visual Basic se puede usar indistintamente sin hacer nada especial, es decir, par saber si se ha pulsado la tecla B puedes hacerlo de estas dos formas:
If e.KeyCode = Keys.B Then o If e.KeyValue = Keys.B

Pero en C# no te permite hacer la comparación del valor int con un valor de la enumeración Keys. Si así lo quieres hacer, tendrías que hacer un cast al tipo entero:
if (e.KeyValue == (int)Keys.B)

Por tanto, es mejor usar e.KeyCode si la intención es compararla con un valor de Keys.

Ya solo me queda ponerte el código completo de esta aplicación de prueba, pero como últimamente estoy haciendo (creo que solo lo he hecho una vez) ese código (tanto el de Visual Basic como el de C#) está en mis repositorios en GitHub, concretamente en varias-pulsaciones-de-teclas.

Dicho código está creado en un proyecto para Visual Studio 2019 usando .NET Framework 4.8, pero también es válido para aplicaciones (de WinForms) creadas para .NET Core, al menos yo lo estoy usando en .NET 5.0 RC1.

Y esto es todo… espero que te sea de utilidad.

Nos vemos.
Guillermo