Archivo de la etiqueta: vb

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

Crear métodos de evento con expresiones lambda para asignarlos manualmente (VB y C#)

Pues eso… hoy te voy a explicar cómo crear métodos lambda (o métodos de evento que usan expresiones lambda) para asignar a los eventos de los controles. Esto lo estoy usando últimamente hasta que la gente de Visual Studio mejore el diseñador de formularios para .NET 5.0 (y .NET Core 3.1) ya que… deja mucho que desear y da muchos quebraderos de cabeza… nada que ver con el diseñador de WinForm (Windows.Forms Designer) de .NET Framework.

Pero aparte de que el editor tenga sus cosas que hay que arreglar… y la verdad no sé si lo arreglarán, ya que ni con el diseñador de formularios de C# va bien. Y no ya porque esté usando el Visual Studio 2019 Preview (actualmente tengo instalada la versión 16.8.0 Preview 3.1, que es la última que hay a día de hoy 28 de septiembre de 2020), si no porque tampoco va en la versión normal de Visual Studio y con el .NET Core soportado, que es la versión 3.1.

Figura 1. Código del ejemplo en el editor de Visual Studio 2019 (VB y C#)

Antes de seguir con el código de ejemplo, te explico lo que dice la documentación de Microsoft (Microsoft docs) sobre las expresiones lambda en Visual Basic.

Y esta es la definición de las expresiones lambda en C#.

¿Qué son las expresiones lambda?

Definición en la documentación de Visual Basic:

Una expresión lambda es una función o subrutina sin un nombre que se puede usar siempre que un delegado sea válido. Las expresiones lambda pueden ser funciones o subrutinas y pueden ser de una o varias líneas. Puede pasar valores del ámbito actual a una expresión lambda.

Lambda (expresiones) (Visual Basic) en la Documentación de Microsoft.

Definición en la documentación de C#:

Una expresión lambda es una expresión que tiene cualquiera de estas dos formas:
Una lambda de expresión que tiene una expresión como cuerpo:
(input-parameters) => expression

Una lambda de instrucción que tiene un bloque de instrucciones como cuerpo:
(input-parameters) => { <sequence-of-statements> }

Use el operador de declaración lambda => para separar la lista de parámetros de la lamba de su cuerpo. Para crear una expresión lambda, especifique los parámetros de entrada (si existen) a la izquierda del operador lambda y una expresión o bloque de instrucciones en el otro lado.

Expresiones lambda (referencia de C#) en la documentación de Microsoft.

Lo que dice la documentación sobre el operador =>
El token => se admite de dos formas: como el operador lambda y como un separador de un nombre de miembro y la implementación del miembro en una definición de cuerpo de expresión.

Ejemplos de expresiones lambda

Imagina que quieres asignar al evento Click de un botón llamado buttonAbrir y cuando se produzca quieres indicarle que llame al método Abrir, en lugar de crear un método específico, que de forma predeterminada tendrá el siguiente aspecto:

Private Sub buttonAbrir_Click(sender As Object, e As EventArgs) Handles buttonAbrir.Click
Abrir()
End Sub

Puedes hacerlo de esta otra forma (por ejemplo dentro del evento Load del formulario):

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load

    AddHandler buttonAbrir.Click, Sub() Abrir()

End Sub

En este último de ejemplo, se crear el método de evento usando una expresión lambda, que como ves solamente usa Sub(), sin argumentos. Esto es así porque Visual Basic lo permite. Si eso mismo se hiciera con C# habría que indicar expresamente los parámetros de esa expresión lambda, aquí te muestro el mismo código (o casi) pero para C#.

private void Form1_Load(object sender, EventArgs e)
{
    buttonAbrir.Click += (object o, EventArgs e) => Abrir();
}

El casi, es porque en algún sitio del código de C# hay que asignar el método Form1_Load al evento Load del formulario (que sería el equivalente al Handles Me.Load del código de Visual Basic):

this.Load += Form1_Load;

Todo este código mostrado sería la forma más simple de asignar una expresión lambda para indicar el método que manejará el evento.

Utilizar el mismo método de evento (o expresión lambda) en varios controles

Pero imagina que quieres hacer la misma asignación de una expresión lambda en varios controles.

Si los controles están declarados con WithEvents y estás usando el diseñador de Windows Forms de Visual Studio para una aplicación de .NET Framework, podrías hacer algo como esto para que el método variosUndo_Click te sirva para manejar los eventos de un botón llamado buttonUndo y para un menú con nombre menuUndo y estaría definido de esta forma:

Private Sub variosUndo_Click(sender As Object, e As EventArgs) _

                    Handles buttonUndo.Click, menuUndo.Click

    If richTextBoxCodigo.CanUndo Then richTextBoxCodigo.Undo()
End Sub

Nota:
En C# (que yo sepa) no hay equivalente para esto, habría que asignar de forma independiente cada manejador de evento.

Sigamos con el código para usar una expresión lambda para hacer lo mismo que en el ejemplo anterior.

Lo primero es definir la expresión lambda:

Private lambdaUndo As EventHandler =  _
           Sub(sender As Object, e As EventArgs) _
               If richTextBoxCodigo.CanUndo Then richTextBoxCodigo.Undo()

Nota:
He usado los guiones bajos para que el código sea más legible.
La he declarado explícitamente como del tipo EventHandler porque están declaradas en el cuerpo del la clase. Si estuviese dentro de un método puedes declararlas con:
Dim lambdaUndo = Sub

Y la asignación a los eventos de esos dos controles antes mencionados (buttonUndo y menuUndo) la haremos de esta forma:

AddHandler buttonUndo.Click, lambdaUndo
AddHandler menuUndo.Click, lambdaUndo

Y ya que estamos, y para terminar, te muestro el equivalente al código anterior, pero sin haber declarado la variable (con la expresión lambda) lambdaUndo:

' Usando expresión lambda no definida previamente
AddHandler buttonUndo.Click, _
  Sub() If richTextBoxCodigo.CanUndo Then richTextBoxCodigo.Undo() AddHandler menuUndo.Click, _
  Sub() If richTextBoxCodigo.CanUndo Then richTextBoxCodigo.Undo()

C# no permite definir expresiones lambda a nivel de clase si accede a miembros no estáticos

En C# no podemos definir la expresión lambda a nivel de clase si accede a un miembro no estático de dicha clase. En VB si se puede (como has podido comprobar).
Así que, si queremos definirlo a nivel de clase, habrá que tener en cuenta que si accede a algún control u otro objeto declarado en esa clase, deben estar definidos con la cláusula static.

Aquí tienes la forma de hacer lo mismo que en el código de Visual Basic (he añadido la definición de richTextBoxCodigo como estático (compartido, Shared en Visual Basic):

// En C# no permite declararlo fuera del cuerpo de un método
// si no, da error de que el control no está definido (debe ser static)
// A field initializer cannot reference the non-static field, method or property 'Form1.richTextBoxCodigo'
// por tanto: definiendo el richtextbox como static ya funciona

private static RichTextBox richTextBoxCodigo;

private EventHandler lambdaUndo = 
            (object sender, EventArgs e) => 
            {
                  if(richTextBoxCodigo.CanUndo) richTextBoxCodigo.Undo();
            };


    // Usando expresión lambda definida previamente
    buttonUndo.Click += lambdaUndo;
    menuUndo.Click += lambdaUndo;

Nota:
Al definir los parámetros de la expresión lambda en C# no es necesario indicar el tipo de datos de cada argumento, con indicar los nombres de los identificadores (variables) es suficiente, tal como te moestraré en otro ejemplo.

Una cosa interesante, y a tener en cuenta, en la definición de la expresión lambda asignada a la variable lambdaUndo es que se usa una expresión if como resultado. En ese caso es obligatorio usar las llaves de apertura y cierre: { };, si no, dará error.
Cuando se usa una llamada a un método no es necesario ponerlo entre las llaves.

Lo de interesante en el párrafo anterior es porque en los ejemplos que vi en internet, para saber porqué no podía declarar (sin errores) esa expresión lambda, no encontré ninguno que usara una expresión al estilo del if, todos los ejemplos eran usando llamadas a métodos o como si fuesen la definición de una llamada a un método. Y además el único ejemplo que me topé definiendo la expresión lambda en el cuerpo de la clase, llamaba a MessageBox y nada que me aclarara las dudas.

Sí, puedes estar pensando que si hubiese hecho caso al mensaje ese de que un inicializador de un campo no puede referenciar a un método no estático, etc. (A field initializer cannot reference the non-static field, method or property ‘Form1.richTextBoxCodigo’) me lo decía todo, pero es que antes de que me mostrase ese mensaje me decía un montón de cosas que no tenían nada que ver con eso.

Aquí tienes todos los errores que mostraba al asignar ese método anónimo sin usar las llavecitas de las narices…

Lista de los errores mostrados al no usar las llaves alrededor de la expresión if.
Y todos en la misma línea (la 39) que es la que define lambdaUno de esta forma (que ya sabemos que es errónea):
private EventHandler lambdaUndo = (object sender, EventArgs e) => if(richTextBoxCodigo.CanUndo) richTextBoxCodigo.Undo();

Error CS1525  Invalid expression term 'if'
Error CS1003  Syntax error, ',' expected
Error CS1002  ; expected
Error CS8124  Tuple must contain at least two elements.
Error CS0246  The type or namespace name 'richTextBoxCodigo' could not be found (are you missing a using directive or an assembly reference?)
Error CS0246  The type or namespace name 'richTextBoxCodigo' could not be found (are you missing a using directive or an assembly reference?)
Error CS0538  'richTextBoxCodigo' in explicit interface declaration is not an interface
Error CS0501  'Form1.Undo()' must declare a body because it is not marked abstract, extern, or partial

Nota:
Ahora que he copiado la definición de las expresiones lambda en la documentación de C# (para pegarla arriba), al ver lo que en esa documentación llaman lambda de instrucción, veo que ahí se indica que hay que encerrar la expresión entre llaves.

En mi defensa te dirá que la documentación de C# la he leído cuando estaba revisando lo que ya había escrito hasta después del siguiente ejemplo. En fin…
Aunque lo mismo tampoco me hubiese enterado…

¡Qué torpe es el Guille!

La otra solución es definir esa expresión lambda dentro del método que sea, por ejemplo Form1_Load.
En este caso no tenemos que definir el control richTextBoxCodigo como static y la expresión lambda la definimos dentro del evento Load del formulario.
Esto está bien si no necesitamos usar esa expresión lambda imbuida en una variable en algún otro método de esa clase.

Este sería el código de ejemplo:

    private void Form1_Load(object sender, EventArgs e)
    {

        EventHandler lambdaUndo = (sender, e) => 
            {
                if (richTextBoxCodigo.CanUndo) richTextBoxCodigo.Undo();
            };

        // Usando expresión lambda definida previamente
        buttonUndo.Click += lambdaUndo;
        menuUndo.Click += lambdaUndo;

    }

Pero en mi caso en particular esta última solución no me sirve, ya que esa expresión lambda la tenía que usar en varios métodos de la misma clase.

Nota (repetimos):
Al definir los parámetros de la expresión lambda en C# no es necesario indicar el tipo de datos de cada argumento, con indicar los nombres de los identificadores (variables) es suficiente, tal como te muestre en el código anterior.

Y hasta aquí hemos llegado… (el fin) aunque en realidad no es el fin del todo… ya sabes que habrá más, concretamente te mostraré un método de extensión para el tipo ToolStripMenuItem para que sirva para clonar un elemento menú, con idea de asignar una copia a otro menú.
Yo eso lo hago cuando quiero añadir un menú contextual que tenga los mismos (o algunos) elementos que otro menú. Por ejemplo a la hora de usar los comandos de edición en un control de texto, para que se muestren los que me interesen del menú de edición. Pero eso será otro día

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

He publicado el código de ejemplo en GitHub, en la forma de un fichero Form1.
Por supuesto, el código de C# está en Form1.cs y el de Visual Basic en Form1.vb

Nos vemos.
Guillermo

El porqué C# siempre tendrá novedades aunque Visual Basic ya no

Pues eso… el otro día estaba leyendo un artículo de Anthony D. Green (miembro del team de Visual Basic de Microsoft del 2010 al 2018) sobre todo lo relacionado con la que se lio por el tema de que a Visual Basic no se le añadirán nuevas características, (
Future features of .NET Core that require language changes may not be supported in Visual Basic.), pero no es de eso de lo que te quiero hablar aquí. Es de lo que dice Anthony en su artículo.

Concretamente esta parte es la que me ha hecho reflexionar un poco sobre C#:

C# advances the .NET platform.
I’m not begrudging or envying them that role but recognizing that that’s what the language has always done where VB is traditionally focused on innovation in experience.
It’s not that you can’t write a library or a framework in VB (and people have) but the .NET platform is not and will never be dependent on the VB language (or F#) to make leaps.
But if C# is late developing generics, .NET 2.0 doesn’t ship.
If C# hasn’t figured out the shape and behavior of nullable value types those types can’t appear in any new APIs.
If it’s late on LINQ, .NET 3.5 doesn’t ship.
If async isn’t complete, .NET 4.5 can’t ship with new asynchronous APIs.
If C# language design can’t converge on ref returns or nullable annotations the platform teams (like mscorlib) that depend on those features literally cannot ship their platforms for anyone.
And for what it’s worth, C# is ALWAYS late on those things (a fact I learned too late) because getting it right takes all the time in the world.
Which is why it’s insane (in hindsight) to envision an “ideal” world where you couple or serialize a language which does not fulfill that mission critical role with so many downstream dependencies and its customers’ productivity with one that does and expect good things for both of them.

Extracto de: Un manual sobre por qué el sufrimiento crónico de la comunidad de VB.NET no es necesario ni una cuestión de gastos o practicidad

Es decir, C# tiene que estar siempre actualizado ya que la plataforma .NET depende de C# (o casi). El casi es porque es el lenguaje de .NET que Microsoft tiene para poder usar las novedades que se agreguen a .NET.

Y como ahora .NET ya no será una continua actualización de .NET Framework, si no de .NET Core (multiplataforma), es lógico que también deba incorporar las novedades enfocadas a ser utilizado tanto en Windows como en MacOS, Linux, iOS, Android, etcétera.

¿Debo olvidarme de Visual Basic y pasarme a C#?

Esto no significa que a Visual Basic no se le deba seguir añadiendo nuevas características en el lenguaje, que según dice Anthony en su artículo es posible hacerlo si Microsoft dedica un par de profesionales a seguir manteniendo el lenguaje, que de otra parte puede recibir ayuda de la comunidad, como ya lo ha hecho con la última incorporación al lenguaje: Comentarios permitidos en más lugares dentro de las instrucciones que precisamente la ha agregado un participante de la comunidad: Paul1956 (Paul M Cohen).

Lo que sí tendrá Visual Basic son nuevas funcionalidades en el editor de Visual Studio (ver figura 1), pero no tendrá, por ejemplo el equivalente a record (registro) o init (establecedor de solo inicialización) en las propiedades.

Figura 1. Mostrar Inline parameters (parámetros en línea)

Así que… si quieres tener un lenguaje de programación para .NET que esté siempre actualizado a las novedades de la plataforma .NET, debes decantarte por C#.

Si no te gusta programar en C# pero sí en Visual Basic, recuerda que este último se quedará estancado en la versión 16.0, mientras que C# seguirá avanzando.
Eso no significa que no puedas seguir usando Visual Basic.

Pero si eres de los que quiere usar .NET 5.0 o las versiones que sigan apareciendo (cada año habrá una nueva y por tanto tendrá cosas nuevas), recuerda que podrás hacerlo, pero con las cosas que actualmente tiene Visual Basic.

Y si eres de los que no quieren usar nada relacionado con .NET / .NET Core / .NET Standard, podrás seguir usando Visual Basic en .NET Framework 4.8 (o anteriores), aunque este último seguirá estando disponible mientras Windows exista.

Resumiendo:
No te quedarás obsoleto si sigues usando Visual Basic. Pero si quieres poder usar en tu código las novedades agregadas a .NET tendrás que plantearte el cambio a C#, aunque sea para poder crear alguna biblioteca de clases que utilice esas novedades de .NET y las pongas a disposición de tu código de Visual Basic (la cuestión es querer arreglar las cosas 😉 ).

Aprender C#

Y si quieres pasarte a C#, ya sabes que en elGuille.info (y en este blog) tienes un montón de código en C# con su equivalente en Visual Basic (o viceversa), ya que desde enero de 2001 (sí, hace ya 19 añitos de nada) todo (o prácticamente todo) lo que he ido publicando para Visual Basic (trucos, API, utilidades, WPF/XAML, etc.) está con el código para los dos lenguajes. Y, lo más importante: ¡TODO GRATIS! (aunque también puedes hacer una donación con PayPal 😉 ).

Por supuesto también puedes apuntarte a algunos de los muchos cursos que hay en las plataformas de enseñanza online y aprender (pagando) el lenguaje C#.

Cómo lo hagas, si decides hacerlo, es cuestión tuya.

Por mi parte, seguiré usando Visual Basic como lenguaje de programación principal, sin olvidarme de C# e ir aprendiendo las cosas nuevas que vayan apareciendo.
Tanto de uno como de otro lenguaje, iré publicando artículos para que te sirva de aprendizaje o para que te aclares un poco más

En un próximo artículo te explicaré algunas de las novedades de C# 9.0 como son los tipos de registro (record), inicializadores en las propiedades (init) o instrucciones de nivel superior (Top-level statements).

Nos vemos.
Guillermo

P.S.
Traducción (automatizada del párrafo en inglés):

C# avanza la plataforma .NET.
No les estoy regateando ni envidiándoles ese papel, pero reconozco que eso es lo que siempre ha hecho el lenguaje, donde VB se centra tradicionalmente en la innovación en la experiencia.
No es que no pueda escribir una biblioteca o un marco de trabajo en VB (y la gente lo ha hecho), pero la plataforma .NET no depende y nunca dependerá del lenguaje VB (o F #) para avanzar.
Pero si C# desarrolla genéricos tarde, .NET 2.0 no sale.
Si C# no ha descubierto la forma y el comportamiento de los tipos de valores que aceptan valores NULL, esos tipos no pueden aparecer en ninguna API nueva.
Si llega tarde en LINQ, .NET 3.5 no se distribuye.
Si async no está completo, .NET 4.5 no se puede enviar con nuevas API asincrónicas.
Si el diseño del lenguaje C# no puede converger en devoluciones de referencia o anotaciones que aceptan valores NULL, los equipos de la plataforma (como mscorlib) que dependen de esas características, literalmente, no pueden enviar sus plataformas a nadie.
Y por si sirve de algo, C# SIEMPRE llega tarde en esas cosas (un hecho que aprendí demasiado tarde) porque hacerlo bien lleva todo el tiempo del mundo.
Es por eso que es una locura (en retrospectiva) imaginar un mundo «ideal» en el que se acopla o serializa un lenguaje que no cumple esa función crítica con tantas dependencias posteriores y la productividad de sus clientes con uno que sí y espera cosas buenas para ambos.

Fallo en el editor de código de VB y C# en Visual Studio 2019

Pues eso… estaba yo tan tranquilo escribiendo código en un formulario de Windows Forms cuando pasé al diseñador a mirar algún evento o nombre de un control y cuando volví al código… ¡HABÍA DESAPARECIDO!

Noté que después de cambiar del diseñador al panel de código el asterisco (*) de modificado había desaparecido… Y (seguramente) la posición en el editor de código era la misma que cuando abrí el proyecto. Así que… me puse puse a buscar lo que había escrito (por si se hubiese cambiado la posición dentro del editor) y ¡el código no estaba!

Era raro… muy raro…

 

 

POSIBLE SOLUCIÓN (21-oct-2020):
Me han escrito hoy los de Microsoft diciendo que la posible solución a este problema es hacer lo siguiente:

Si tienes el Visual Studio 2019 en español sigue estos pasos (ver figura 5):
En el menú Herramientas>Opciones>Entorno>Características en versión preliminar quitar la marca de la casilla Cargar proyectos más rápidamente.

Figura 5. Quitar la selección de Cargar proyectos más rápidamente

 

Si tienes el Visual Studio 2019 en inglés sigue estos pasos (ver figura 6):
En el menú Tools>Options>Environment>Preview Features quitar la marca de la casilla Load projects faster.

Figura 6. Quitar la selección de Load projects faster

Si lo puedes comprobar, sería de agradecer 😉 

 

Menos mal que se me ocurrió darle al botón DESHACER. ¡Y el código volvió a aparecer! (Y el asterisco de modificado también).

Todo esto era en un proyecto para Visual Basic usando Visual Studio 2019 Community «normal», aunque después probé en la Preview y también ocurre.

El problema

Para un ejemplo de lo que ocurre ver el GIF animado de la figura 1 para que te hagas una idea.

Figura 1. Fallo en el editor de formularios de Visual Studio 2019 usando Visual Basic

Lo de mostrar la versión de Visual Studio es porque este GIF (y los que te voy a mostrar después) los mandé la notificación del problema a la comunidad de desarrollo (Developer Community).
Ya que ellos (después de explicarles varias veces los pasos a seguir) no consiguieron reproducirlo y en sus capturas me mandaban la versión de Visual Studio que estaban usando.

 

Nota del 18/Sep:
Dicen que lo están investigando:
This issue is currently being investigated. Our team will get back to you if either more information is needed, a workaround is available, or the issue is resolved.
A ver qué ocurre…

 

Los pasos a seguir, al menos así es como me ocurría más a menudo (aunque no siempre ocurría) son estos:

1- Con cualquier proyecto de Visual Basic (al principio pensé que solo ocurre en VB, pero después comprobé que también ocurre en C#) deja abierto el formulario en modo de diseño y la ventana de código, de forma que el código tenga el foco antes de cerrar la solución (o el Visual Studio).
2- Al abrir nuevamente Visual Studio, te mostrará la ventana de código.
3- Escribe lo que sea y pasa a la ventana del diseñador de formularios.
4- Vuelve a la ventana del editor y verás que el código antes escrito desaparece.
5- Si lo quieres recuperar, tendrás que pulsar en el botón de deshacer.
5a- En al menos una ocasión (creo que con la versión Preview y un proyecto de C# para .NET 5.0 Preview 8) mostró un aviso de que se perderán los cambios en el diseñador (o algo así, no le presté atención ya que no pensaba que iba a ocurrir esto de perder el código), pero al darle varias veces a deshacer, el código volvió.

Pruebas y más pruebas

Este problema no ocurre siempre, ese es realmente el problema.

Para comprobar los fallos (y porque los de Microsoft me pidieron que les enviara un proyecto en el que ocurriera) creé un nuevo proyecto de Windows Forms para Visual Basic con .NET Framework 4.7.2 y, tras varias pruebas, se reprodujo el problema.

También probé con proyectos de C#, tanto con la versión normal de Visual Studio como con la versión Preview. Pero no era capaz de que volviese ocurrir en C#, pero sí en VB. Así que… pensé que solo ocurría esto en los proyectos de los pobres y ya casi abandonados usuario de Visual Basic.

Pero no: El problema también ocurre con proyectos de C#.
Más sobre esto dentro de un momento.

Usar una máquina virtual en Azure preparada para Visual Studio 2019

Los de Microsoft (concretamente John Q., me preguntaron si esto mismo me ocurría en otros equipos o le ocurría a otros desarrolladores de mi equipo… ¡sic! ¡Solo tengo a un desarrollador en mi equipo! Y no, no tengo otros equipos (máquinas) en la que poder probarlo… Se me ocurrió que podría probarlo en una máquina virtual… Pero me daba pereza crear una máquina virtual, instalar el Visual Studio y probarlo, así que… se me ocurrió probarlo en una máquina virtual de Azure.

Me creé una cuenta «FREE» de Azure (la que yo tenía ya caducó hace meses) con idea de usar allí una máquina virtual, pero la pereza era la misma… tener que crear una máquina virtual y tener que instalar el Visual Studio.

Sé que hay (o al menos había) máquinas virtuales ya preparadas con Visual Studio y que Microsoft las pone a nuestra disposición, así que… me puse a buscarla y encontré esto en la documentación de Microsoft: Imágenes de Visual Studio en Azure.
Uno de los enlaces activos era este: Visual Studio 2019: versión más reciente (16.5), pero el enlace estaba roto y no mostraba nada (salvo un error 404).

Así que… usé el enlace para Azure Marketplace (en la misma página de las imágenes de Visual Studio en Azure) y también daba error 404. Pulsé en el enlace de esa misma página que indica Ir a Azure Marketplace y con un poco de paciencia la página apareció) y entré en el apartado Apps. Filtré para que me mostrase solo las de Microsoft como publisher para el sistema operativo Windows y el tipo de producto Virtual Machine Images.

Y allí estaba: Visual Studio 2019 Latest.

Figura 2. La máquina virtual de VS para Azure


Al pulsar el enlace (no me deja copiarlo) muestra una ventana pop-up con las opciones que hay para seleccionar, primero seleccioné la versión Community para Windows 10, pero no me dejó instalarla, así que… posteriormente elegí la versión Visual Studio 2019 Comunity (latest release) on Windows Server 2019 y esa sí funcionó.

Probando a repetir el problema con Visual Studio en la máquina virtual de Azure

Allí probé el proyecto que les mandé y tras no-se-cuantas-miles-de-pruebas, el problema no se repetía.

Lo más que me ocurrió fue algo que también me ha ocurrido más veces (y seguramente tendrá que estar en una nuevo problema para que lo resuelvan) y es que al pasar o mostrarse directamente la ventana de código al abrir el proyecto, esta se muestra totalmente en blanco, tal como puedes comprobar en la figura (GIF) número 3.

Figura 3. Cuando te quedas en blanco mientras escribes código… o casi 😉

Pero el fallo inicial no se reproducía en el proyecto de prueba de Visual Basic.

El problema también ocurre con proyectos de C#

Así que… me dio por crear un nuevo proyecto de C# para Windows Forms usando .NET Core 3.1, así de paso probaba qué características tiene ese tipo de proyecto, ya que con los de Visual Basic para .NET 5.0 Preview 8, son una caca… también para C#, ya que no puedes hacer casi nada en el diseñador de formularios, al menos no puedes enlazar los eventos ni editar visualmente los menús, etc., pero esa es otra historia.

Y el fallo volvió a ocurrir… ya no recuerdo si fue a la primera o a la segunda… y eso que fue ayer… la cuestión es que funcionó y yo tenía la utilidad ScreenToGif lista para capturar los movimientos.

Y los capturé, y en la figura 4 puedes ver que con un proyecto de C# también ocurre.

Figura 4. El fallo de perder lo escrito en un proyecto de C#

Fíjate en el detalle de que antes de mostrar el diseñador de formularios aparece un mensaje indicando algo como Opening the file… si eso se muestra… ¡es prácticamente seguro que el fallo va a ocurrir!

 

Y esto es todo… a ver si lo solucionan.

Lo que si es cierto es que hay más cosas que pasan, aparte de que escribas código y se pierda o se quede la ventana de código en blanco, ya que también me ocurre que estando en el diseñador de formularios no se muestra la ventana de propiedades y tengo que cambiar a otra pestaña o cerrar y abrir el formulario para que por fin se muestre la ventana de propiedades o que al estar en la ventana de propiedades, en el despegable donde indica qué control estás mostrando esté en blanco y aún así se muestren algunas propiedades… esto último me pasó en el proyecto de C# para .NET Core 3.1 en el que tenía seleccionado un botón que quería moverlo o cambiar el valor de la propiedad Anchor y al no mostrarse esa propiedad pensé que en .NET Core 3.1 no existía esa propiedad… y no, era que fallaba la ventana de propiedades.

Si te ha ocurrido algo de esto, por favor indícalo en la página esa de Developer Community en la que he publicado el fallo (la descripción del problema está en inglés), te repito el enlace para que te sea más cómodo:

When opening a project if I modify the code and then (without saving) I show the form, when returning to the code that code does not exist (any changes made are not shown).

Es un título largo, lo sé… pero… 😉

Gracias.

Nos vemos.
Guillermo

gsCompilarNET: una biblioteca de clases para compilar código de C# o VB

Pues eso… intentando convertir la utilidad de Compilar y ejecutar a .NET 5.0 (Preview 8) me topé que (si no recuerdo mal) el .NET 5.0 no ofrece las clases para compilar (Microsoft.CodeDom.Providers.DotNetCompilerPlatform), así que… me puse a buscar en la web y me topé con un código de ejemplo (en C#) de Laurent Kempé que estaba escrito usando .NET Core 3.0 Preview 2.

Me puse a copiar el código que tenía publicado en el artículo y lo modifiqué para crear un proyecto para .NET Core 3.1 que usara las clases de compilar y ejecutar (Compiler y Runner) desde otra clase estática que es la encargada de llamar compilar y ejecutar el código. Después descubrí que ese código lo tenía publicado en GitHub, pero en realidad ya lo tenía copiado/adaptado usando el código de su web.

El problema inicial con el que me encontré es que solo compilaba código de C#, así que… lo modifiqué para que también compilara código de Visual Basic.

En principio ese código solo compilaba/ejecutaba código de consola. Así que… tuve que modificarlo para que también compilara código de Windows Forms, eso sí, todo el código debía estar en un solo fichero.

La respuesta a esto último que me dio la idea de cómo solucionarlo (o casi) la encontré en la red, pero no recuerdo en qué página, solo sé que era para ejecutar código de Visual Basic contenido en una base de datos, pero en realidad no era para código de Windows Forms, si no de consola, pero eso me sirvió para (además de agregar las referencias necesarias) poder crear el fichero json que necesita dotnet para crear el tipo adecuado de aplicación o usar las bibliotecas necesarias, la verdad es que tampoco me enteré demasiado… solo que haciéndolo, funcionaba 😉

La cuestión es que conseguí hacer operativo el compilador para usar código de Visual Basic (y C#) tanto para consola como para formularios de Windows.

Este es el código (o parte) que hace que esto sea posible:

        ' para ejecutar una DLL usando dotnet, necesitamos un fichero de configuración
        Dim jsonFile = Path.ChangeExtension(outputExe, "runtimeconfig.json")
        Dim jsonText = ""
        If compiler.EsWinForm Then
            Dim version = Compiler.WindowsDesktopApp().Version
            ' Aplicación de escritorio (Windows Forms)
            ' Microsoft.WindowsDesktop.App
            ' 5.0.0-preview.8.20411.6
            jsonText = "
{
    ""runtimeOptions"": {
    ""tfm"": ""net5.0-windows"",
    ""framework"": {
        ""name"": ""Microsoft.WindowsDesktop.App"",
        ""version"": """ & version & """
    }
    }
}"
        Else
            Dim version = Compiler.NETCoreApp().Version
            ' Tipo consola
            ' Microsoft.NETCore.App
            ' 5.0.0-preview.8.20407.11
            jsonText = "
{
    ""runtimeOptions"": {
    ""tfm"": ""net5.0"",
    ""framework"": {
        ""name"": ""Microsoft.NETCore.App"",
        ""version"": """ & version & """
    }
    }
}"
        End If
        Using sw = New StreamWriter(jsonFile, False, Encoding.UTF8)
            sw.WriteLine(jsonText)
        End Using

Hoy me ha dado por convertir el código de C# (ya modificado y operativo) a Visual Basic y he creado un «repositorio» en GitHub con el código de la DLL que he creado para compilar código.

También he publicado el código de la versión de C#, ya que actualmente tengo las dos versiones sincronizadas (es decir, los cambios y mejoras que he añadido en VB los he pasado al proyecto de C# y viceversa).

Así que… si te interesa el código de esta DLL para compilar puedes descargarlo/verlo en el repositorio de GitHub para gsCompilarNET.

Después publicaré también la utilidad de .NET 5.0 para Windows Forms que utiliza esa DLL (y la de gsColorearNET).

 

Espero que te sea de utilidad. 🙂

Nos vemos.
Guillermo

P.S.
He publicado en NuGet la utilidad ya compilada por si quieres usarla en tus proyectos de Visual Studio (final o preview):
gsCompilarNET en NuGet.

P.S.2
Esta es la pagina original de Compilar y ejecutar en elGuille.info (para .NET Framework 4.7.2 usando WPF).

Compilar código en .NET Core (.NET 5.0) para consola y Windows Forms

Pues eso… usando la aplicación que te mostré el otro día (Compilar y ejecutar versión para .NET Core (.NET 5.0)) y después de hacer varias pruebas con otra DLL que compila el código y lo ejecuta sin usar Process.Start, al menos si es una aplicación de consola, (ya que para ejecutar las de Windows Forms, al menos hasta lo que yo he probado, solo puedo lanzarla usando Process.Start); al final me he quedado con la llamada a dotnet para crear el proyecto, compilarlo y ejecutar el código compilado.

Nota:
Abajo te dejo un ZIP con el código fuente del proyecto (Compilar y ejecutar) y la utilidad para colorear (gsColorearCore).
En la carpeta pruebas del proyecto está el código fuente (de VB y C#) de la aplicación de ejemplo.

Compilar con dotnet (desde la línea de comandos)

En las primeras pruebas (las del código que te puse en el post arriba mencionado, usaba aplicaciones de consola, más que nada porque así se lo decía al usar el comando:

dotnet new console -o "AppDir" -lang c#|vb

Ese código lo que hace es crear una aplicación de consola en el directorio indicado por AppDir (si ya existe, podemos añadir –force al final para que genere el contenido aunque sobreesciba lo que ya hubiese.).

Para crear/compilar una aplicación para Windows Forms tendremos que usar una línea de comandos parecida a la anterior, pero indicando winforms en vez de console.
En el siguiente código creamos una aplicación de .NET 5.0 Core usando Visual Basic en el directorio E:\Guille\source\repos\MiAppWinF.

Nota:
Si no tienes instalado el .NET 5.0 y tienes al menos el .NET Core 3.0 o 3.1 simplemente usa el lenguaje C# en lugar de Visual Basic, ya que en esas versiones anteriores al 5.0 solo se permiten aplicaciones de Windows Forms o WPF para C#.

dotnet new winforms -o "E:\Guille\source\repos\MiAppWinF" -lang vb

Nota:
En realidad poner el directorio entre comillas dobles no es necesario, salvo que se utilice el comando dotnet build.

Una vez hecho esto, solo tendremos que modificar el código generado y si queremos que se ejecute el contenido de ese directorio podemos usa la siguiente línea de comandos:

dotnet run -p E:\Guille\source\repos\MiAppWinF

El código usado en la aplicación Compilar y ejecutar NETCore

En el código de la aplicación compilar y ejecutar para .NET Core 5.0 este último paso me lo salto y lo que hago es crear la aplicación (con –force) sustituyo el código. ya que –force o simplemente al crear con new se generan los ficheros en blanco (sin el código que ya tuviera).
En su lugar utilizo build para compilar el código.
Es decir, creo la aplicación con new (uso –force por si ya existiera), guardo el código fuente del editor del programa, lo compilo con build y finalmente lo ejecuto usando Process.Start.

El comando build usado es el siguiente:

dotnet build "E:\Guille\source\repos\MiAppWinF"

Las comillas dobles solo son necesarias si el path o el nombre del proyecto contiene espacios.

Y ese comando habrá creado un ejecutable (aparte de la DLL que siempre genera .NET Core) en el directorio: E:\Guille\source\repos\MiAppWinF\bin\Debug\net5.0-windows cuyo nombre será MiAppWinF.exe
Fíjate que al ser una aplicación para Windows (las aplicaciones de Windows Forms y las de WPF solo se pueden usar en plataformas Windows, es decir, no se pueden usar ni en Linux ni en MacOS) se crea en un directorio aparte de las aplicaciones de consola, que sería en: bin\Debug\net5.0\

Nota:
El directorio de salida puede ser diferente si antes de build (y después de new) cambias la configuración del proyecto.

Un poco de código fuente por favor

El código del método compilar al que se le pasa el código a compilar/ejecutar.

''' <summary>
''' Compilar el código indicado en el parámetro.
''' 
''' Usando dotnet (.NET Core) se hará lo siguiente:
''' Definir un directorio para VB o C#: MiApp_VB o MiApp_CS
''' Usar el comando:
''' dotnet new console -o dir -lang C#|VB --force
''' Copiar en ese directorio el fichero como Program.vb o .cs
''' Usar el comando (y redirigir la salida):
''' dotnet run -p dir
''' </summary>
Private Sub compilar(texto As String, mostrarSoloSalida As Boolean)
    ' Compilar usando la línea de comandos
    Dim ext = If(optCS.IsChecked, "cs", "vb")

    Dim appDir = System.IO.Path.Combine(System.Environment.CurrentDirectory,
                                        $"MiApp_{ext}")

    ' crear un fichero temporal para compilar
    Dim ficProgram = Path.Combine(appDir, $"Program.{ext}")

    Dim dotnet = "dotnet"
    Dim args = "--version"
    txtSalida.Text = $"{dotnet} {args} = {ejecutar(dotnet, args, waitSecs:=5000)}"

    ' Considero que es aplicación de Windows Forms              (07/Sep/20)
    ' si contiene InitializeComponent
    Dim esWF = texto.IndexOf("InitializeComponent()") > -1
    If esWF Then
        args = $"new winforms -o ""{appDir}"" -lang {If(optCS.IsChecked, "c#", "vb")} --force"
    Else
        args = $"new console -o ""{appDir}"" -lang {If(optCS.IsChecked, "c#", "vb")} --force"
    End If

    If mostrarSoloSalida Then
        ejecutar(dotnet, args, waitSecs:=10000)
    Else
        txtSalida.Text &= $"{vbCrLf}{vbCrLf}{ejecutar(dotnet, args, waitSecs:=10000)}"
    End If

    ' Eliminar todos los ficheros del lenguaje
    ' (solo quedará el que se guarde)
    Dim files = Directory.GetFiles(appDir, $"*.{ext}")
    For i = 0 To files.Length - 1
        File.Delete(files(i))
    Next

    ' Guardar el código a compilar
    Using sw As New System.IO.StreamWriter(ficProgram,
                                           False,
                                           System.Text.Encoding.UTF8)
        sw.Write(texto)
    End Using

    args = $"build ""{appDir}"""

    If mostrarSoloSalida Then
        Dim res = ejecutar(dotnet, args, waitSecs:=10000)
        Dim hayErrorComp = False
        If optVB.IsChecked Then
            If res.Contains("error BC") Then
                hayErrorComp = True
            End If
        Else
            If res.Contains("error CS") Then
                hayErrorComp = True
            End If
        End If
        If hayErrorComp Then
            txtSalida.Text &= vbCrLf & res
            txtSalida.Text &= $"{vbCrLf}--- FIN DE LA COMPILACIÓN CON ERRORES ---"
            Return
        End If
        'txtSalida.Text &= $"{vbCrLf}{vbCrLf}{res}"
    Else
        txtSalida.Text &= $"{vbCrLf}{vbCrLf}{ejecutar(dotnet, args, waitSecs:=10000)}"
    End If

    Dim exe = System.IO.Path.Combine(appDir, $"bin\Debug\net5.0\MiApp_{ext}.exe")
    ' Si es aplicación de Windows Forms, usa otro directorio    (07/Sep/20)
    If esWF Then
        'net5.0-windows
        exe = System.IO.Path.Combine(appDir, $"bin\Debug\net5.0-windows\MiApp_{ext}.exe")
        txtSalida.Text &= $"{vbCrLf}{vbCrLf}Aplicación de Windows"
    End If
    txtSalida.Text &= $"{vbCrLf}{vbCrLf}{ejecutar(exe, esWinF:=esWF)}"

    txtSalida.Text &= $"{vbCrLf}--- FIN DE LA COMPILACIÓN ---"

    txtSalida.SelectionStart = txtSalida.Text.Length
    txtSalida.SelectionLength = 0
End Sub

Del método ejecutar tengo dos versiones, la primera es la que uso para llamar al dotnet y en el que redirijo la salida de la consola para capturarla y mostrarla en la caja de textos txtSalida.
Al primero le paso el nombre de la aplicación a ejecutar (dotnet) y los argumentos.
Al segundo le paso el nombre del ejecutable y si es o no aplicación de Windows, en cuyo caso se usa ShellExecute y el estilo de la ventana es normal, aparte de no usar Kill para que no se cierre la aplicación al poco de haberse abierto 🙂

Private Function ejecutar(exe As String, arg As String,
                          Optional conKill As Boolean = False,
                          Optional waitSecs As Integer = 10000) As String
    Dim res = ""
    Using p As New Process
        p.StartInfo.FileName = exe
        p.StartInfo.Arguments = arg

        ' Indicamos que queremos redirigir la salida
        p.StartInfo.RedirectStandardOutput = True
        ' Para redirigir la salida, UseShellExecute debe ser falso
        p.StartInfo.UseShellExecute = False

        p.StartInfo.CreateNoWindow = True

        Try
            ' Iniciamos el proceso
            p.Start()

            ' Esperar a que el proceso finalice
            '
            ' Esperamos waitSecs segundos para que le de tiempo a ejecutarse
            ' como mínimo esperar 2000 (o 5000 si se usa dotnet)
            If waitSecs < 2000 Then waitSecs = 2000
            p.WaitForExit(waitSecs)

            If conKill Then
                p.Kill()
            End If

            res = p.StandardOutput.ReadToEnd()
        Catch ex As Exception

            res = ex.Message
        End Try
    End Using

    Return res
End Function

''' <summary>
''' Ejecutar el código y mostrarlo en la ventana de salida.
''' </summary>
Private Function ejecutar(exe As String,
                          Optional waitSecs As Integer = 2000,
                          Optional esWinF As Boolean = False) As String
    Dim p As New Process

    p.StartInfo.FileName = exe

    If esWinF Then

        With p.StartInfo
            .UseShellExecute = True
            .WindowStyle = ProcessWindowStyle.Normal
        End With

        ' Iniciamos el proceso
        p.Start()

        ' Esperar a que el proceso finalice
        '
        ' Esperamos 2 segundos para que le de tiempo a ejecutarse
        ' Como mínimo 2 segundos
        If waitSecs < 2000 Then waitSecs = 2000
        p.WaitForExit(waitSecs)

        Return ""
    End If

    ' Indicamos que queremos redirigir la salida
    p.StartInfo.RedirectStandardOutput = True
    ' Para redirigir la salida, UseShellExecute debe ser falso
    p.StartInfo.UseShellExecute = False

    ' No usar esto
    ' salvo que se use p.Kill()
    p.StartInfo.CreateNoWindow = True

    ' Iniciamos el proceso
    p.Start()

    ' Esperar a que el proceso finalice
    '
    ' Esperamos 2 segundos para que le de tiempo a ejecutarse
    ' Como mínimo 2 segundos
    If waitSecs < 2000 Then waitSecs = 2000
    p.WaitForExit(waitSecs)
    Try
        p.Kill()
    Catch ex As Exception
        Debug.WriteLine(ex.Message)
    End Try

    ' Convertir la salida usando el código de página 437
    ' que es la usada en MS-DOS (línea de comandos)
    Dim res = p.StandardOutput.ReadToEnd()

    Return res
End Function

Recomendación para usar código de Windows Forms en la utilidad de Compilar y ejecutar

Por último, comentarte que si quieres compilar una app para Windows Forms desde la utilidad de compilar y ejecutar, el código de esa aplicación debe estar en un solo fichero, en el que te recomiendo que incluyas el método Main, la definición del diseñador de Windows Forms y el código que realmente quieres usar.
Esto es así porque el programa/utilidad no está preparado para usar múltiples ficheros.

Te muestro a continuación cómo sería ese código para Visual Basic y para C# y una captura de la salida realizada al compilar desde la utilidad.

Este es el código de Visual Basic del formulario de pruebas.

Imports System
Imports System.Windows.Forms

'
' El punto de entrada del programa
'

Friend Module Program

    <STAThread()>
    Friend Sub Main(args As String())
        Application.SetHighDpiMode(HighDpiMode.SystemAware)
        Application.EnableVisualStyles()
        Application.SetCompatibleTextRenderingDefault(False)
        Application.Run(New Form1)
    End Sub

End Module

'
' El diseñador del formulario
'

<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Partial Class Form1
    Inherits System.Windows.Forms.Form

    'Form overrides dispose to clean up the component list.
    <System.Diagnostics.DebuggerNonUserCode()> _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

    'Required by the Windows Form Designer
    Private components As System.ComponentModel.IContainer

    'NOTE: The following procedure is required by the Windows Form Designer
    'It can be modified using the Windows Form Designer.  
    'Do not modify it using the code editor.
    <System.Diagnostics.DebuggerStepThrough()> _
    Private Sub InitializeComponent()
        Me.TextBox1 = New System.Windows.Forms.TextBox()
        Me.Button1 = New System.Windows.Forms.Button()
        Me.Label1 = New System.Windows.Forms.Label()
        Me.Button2 = New System.Windows.Forms.Button()
        Me.SuspendLayout()
        '
        'TextBox1
        '
        Me.TextBox1.Location = New System.Drawing.Point(12, 12)
        Me.TextBox1.Name = "TextBox1"
        Me.TextBox1.Size = New System.Drawing.Size(182, 23)
        Me.TextBox1.TabIndex = 0
        '
        'Button1
        '
        Me.Button1.Location = New System.Drawing.Point(200, 11)
        Me.Button1.Name = "Button1"
        Me.Button1.Size = New System.Drawing.Size(75, 23)
        Me.Button1.TabIndex = 1
        Me.Button1.Text = "Button1"
        Me.Button1.UseVisualStyleBackColor = True
        '
        'Label1
        '
        Me.Label1.Location = New System.Drawing.Point(12, 38)
        Me.Label1.Name = "Label1"
        Me.Label1.Size = New System.Drawing.Size(263, 23)
        Me.Label1.TabIndex = 2
        Me.Label1.Text = "Label1"
        '
        'Button2
        '
        Me.Button2.Location = New System.Drawing.Point(200, 90)
        Me.Button2.Name = "Button2"
        Me.Button2.Size = New System.Drawing.Size(75, 23)
        Me.Button2.TabIndex = 3
        Me.Button2.Text = "Cerrar"
        Me.Button2.UseVisualStyleBackColor = True
        '
        'Form1
        '
        Me.AcceptButton = Me.Button1
        Me.AutoScaleDimensions = New System.Drawing.SizeF(7.0!, 15.0!)
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font
        Me.CancelButton = Me.Button2
        Me.ClientSize = New System.Drawing.Size(287, 125)
        Me.Controls.Add(Me.Button2)
        Me.Controls.Add(Me.Label1)
        Me.Controls.Add(Me.Button1)
        Me.Controls.Add(Me.TextBox1)
        Me.Name = "Form1"
        Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
        Me.Text = "Form1"
        Me.ResumeLayout(False)
        Me.PerformLayout()

    End Sub

    Friend WithEvents TextBox1 As TextBox
    Friend WithEvents Button1 As Button
    Friend Label1 As Label
    Friend Button2 As Button
End Class

'
' El código del formulario
'

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        TextBox1.Text = "<tu nombre>"

        'AddHandler Button2.Click, AddressOf Button2_Click
        AddHandler Button2.Click, Sub() Me.Close()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim str = TextBox1.Text
        If str = "<tu nombre>" Then
            str = " amigo"
        End If
        Label1.Text = $"¡Hola {ToUpperFirst(str)}!"
    End Sub

    ''' <summary>
    ''' Convierte en mayúsculas el primer carácter de la cadena indicada.
    ''' </summary>
    Private Function ToUpperFirst(str As String) As String
        'Return str(0).ToString().ToUpper() & str.Substring(1)
        if str="" then str=" amigo"
        Return str(0).ToString.ToUpper & str.Substring(1)
    End Function

    'Private Sub Button2_Click(sender As Object, e As EventArgs)
    '    Me.Close()
    'End Sub
End Class

Este es el código de C# del formulario de pruebas.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;

//
// El punto de entrada del programa
//

static class Program
{
    /// <summary>
    ///  The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.SetHighDpiMode(HighDpiMode.SystemAware);
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}


//
// El diseñador del formulario
//

partial class Form1
{
    /// <summary>
    ///  Required designer variable.
    /// </summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>
    ///  Clean up any resources being used.
    /// </summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    #region Windows Form Designer generated code

    /// <summary>
    ///  Required method for Designer support - do not modify
    ///  the contents of this method with the code editor.
    /// </summary>
    private void InitializeComponent()
    {
        this.textBox1 = new System.Windows.Forms.TextBox();
        this.button1 = new System.Windows.Forms.Button();
        this.label1 = new System.Windows.Forms.Label();
        this.button2 = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // textBox1
        // 
        this.textBox1.Location = new System.Drawing.Point(12, 12);
        this.textBox1.Name = "textBox1";
        this.textBox1.Size = new System.Drawing.Size(182, 23);
        this.textBox1.TabIndex = 0;
        // 
        // button1
        // 
        this.button1.Location = new System.Drawing.Point(200, 12);
        this.button1.Name = "button1";
        this.button1.Size = new System.Drawing.Size(75, 23);
        this.button1.TabIndex = 1;
        this.button1.Text = "button1";
        this.button1.UseVisualStyleBackColor = true;
        this.button1.Click += new System.EventHandler(this.button1_Click);
        // 
        // label1
        // 
        this.label1.Location = new System.Drawing.Point(12, 38);
        this.label1.Name = "label1";
        this.label1.Size = new System.Drawing.Size(263, 23);
        this.label1.TabIndex = 2;
        this.label1.Text = "label1";
        // 
        // button2
        // 
        this.button2.Location = new System.Drawing.Point(200, 90);
        this.button2.Name = "button2";
        this.button2.Size = new System.Drawing.Size(75, 23);
        this.button2.TabIndex = 3;
        this.button2.Text = "Cerrar";
        this.button2.UseVisualStyleBackColor = true;
        //this.button2.Click += new System.EventHandler(this.button1_Click);
        // 
        // Form1
        // 
        this.AcceptButton = this.button1;
        this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
        this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
        this.CancelButton = this.button2;
        this.ClientSize = new System.Drawing.Size(287, 125);
        this.Controls.Add(this.label1);
        this.Controls.Add(this.button1);
        this.Controls.Add(this.textBox1);
        this.Controls.Add(this.button2);
        this.Name = "Form1";
        this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
        this.Text = "Form1";
        this.Load += new System.EventHandler(this.Form1_Load);
        this.ResumeLayout(false);
        this.PerformLayout();

    }

    #endregion

    private System.Windows.Forms.TextBox textBox1;
    private System.Windows.Forms.Button button1;
    private System.Windows.Forms.Label label1;
    private System.Windows.Forms.Button button2;
}

//
// El código del formulario
//

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        textBox1.Text = "<tu nombre>";
        
        //button2.Click += => (object o, EventArgs e) this.Close();
        //button2.Click += delegate (object sender, EventArgs e) { this.Close(); };
        button2.Click += (sender, e) => { this.Close(); };
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //label1.Text = $"Hola {ToUpperFirst(textBox1.Text)}!";
        var str = textBox1.Text;
        if( str == "<tu nombre>" ) str = " amigo";
        label1.Text = $"Hola {ToUpperFirst(str)}!";
    }

    /// <summary>
    /// Convierte en mayúsculas el primer carácter de la cadena indicada.
    /// </summary>
    private string ToUpperFirst(string str)
    {
        if(str=="") str = " amigo";
        return str[0].ToString().ToUpper() + str.Substring(1);
    }
}

Esta es la salida del código ejecutándose con la utilidad de compilar y ejecutar para NETCore.

Figura 1. El resultado de compilar y ejecutar una aplicación de Windows Forms para .NET Core 5.0.

Y esto es todo… tengo algo más por ahí, para seguir con la compilación y ejecución del código para .NET Core, pero eso será para otra ocasión 🙂

 

Espero que te haya sido de utilidad… Esa es la idea y ¡eso espero! 🙂

 

Nos vemos.
Guillermo

El ZIP con el código fuente:

ZIP: Compilar_ejecutar_NetCore_20200908_1551.zip (71.8 KB)
MD5 checksum: 52A9AD9A1F1605547D1C8451CADD4CF0

Cálculos aritméticos en tu código con EvaluarExpresiones

Pues eso… ya he corregido y actualizado el código de evaluar expresiones aritméticas que publiqué inicialmente en octubre de 2007. Además he ampliado la funcionalidad que tenía usando algunas cosas que ya hacía en la versión que creé en su día con Visual Basic 6 (cFormulas – Una clase para analizar expresiones numéricas).

Como por ejemplo poder definir variables y funciones propias, además de poder usar contenido entre comillas dobles (solo para mostrar, no es que utilice variables alfanuméricas).

Para usar el «evaluador de expresiones» necesitas tener una referencia a la DLL (elGuille.EvaluarExpresiones.dll) en tu proyecto de .NET (al menos funciona con Visual Basic y C#) y tener instalado al menos el .NET 4.7.

Crear una variable a la que asignes una nueva instancia de elGuille.Developer.EvaluarFunciones.
Dim ev As New elGuille.Developer.EvaluarFunciones.

Si quieres añadir variables antes de empezar a usar el método Evalua, lo puedes hacer con ev.NewVariable(«A», «123+5+2″) y si quieres crear una función, hazlo con ev.NewFunction(«Doble», «N», «N*2»). Si quieres hacer varias asignaciones de un golpe, puedes usar ev.AsignarVariables(«C=A+B: D=12345: F=A*D»).

Y para evaluar las diferentes expresiones usa el método Evalua al que le pasarás como argumento una cadena con el «texto» a evaluar, por ejemplo:
resultado = ev.Evalua(«X=10:A=5*3: P=3.14: X + A + P»)

Descargar el código fuente y la DLL

El código completo de la DLL está en EvaluarExpresiones_src.zip que tiene un tamaño de 191 KB (195.897 bytes) y que puedes descargar pulsando directamente en el enlace anterior (está en el sitio downloads.elguille.info).
Revisión v.58 del 29/Ago/2020

Ese código es un proyecto de Visual Basic .NET.

En otra ocasión actualizaré este artículo para poner algunos ejemplos de uso. Aunque los publicados en los dos enlaces que te puse al principio te pueden servir.

Espero que te sea de utilidad… ya sabes… de eso es de lo que se trata 😉

Nos vemos.
Guillermo

Indicar el Encoding al guardar el contenido de un RichTextBox de WPF

Pues eso… que el otro día te puse un ejemplo de Abrir y guardar archivos usando RichTextBox para WPF y anoche haciendo pruebas con vocales acentuadas, me di cuenta que el formato XAML (DataFormats.Xaml) las tildes se las pasaba por el forro… así que… buscando en la red de redes vi un ejemplo que evita eso… o casi, al menos te permite tener la opción de poder hacerlo.

El problema está (o estaba) en que en el ejemplo de donde saqué el código para guardarlo utiliza esto para crear el Stream de salida: Using fStream As New FileStream(_fileName, FileMode.Create) y usando FileStream no se puede indicar la codificación. O yo no sé cómo hacerlo, que todo hay que decirlo Winking smile

Al abrir el Stream se hace la llamada al método Save del rango (TextRange) y se indica el formato con el que se guardará: range.Save(fStream, formato, True). range.Save precisa de un Stream, pero el ofrecido por StreamWriter, que es el que yo suelo usar para guardar indicando la codificación, no le sirve.

El truco está en guardar primero el contenido del RichTextBox en la memoria (usando MemoryStream) y después pasar ese flujo de caracteres al disco por medio de StreamWriter.

El código final quedaría de la siguiente forma:

''' <summary>
''' Adaptado del ejemplo de la documentación de Microsoft
''' https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
'''     richtextbox-overview
''' </summary>
Private Function SaveRtfFormat(_fileName As String,
                               richTB As RichTextBox,
                               formato As String) As Boolean
    Dim range As TextRange
    Dim guardado As Boolean = False

    range = New TextRange(richTB.Document.ContentStart,
                          richTB.Document.ContentEnd)

    ' Para guardar con el formato que queramos
    ' Adaptado de:
    ' https://social.msdn.microsoft.com/Forums/vstudio/en-US/
    '   a9ef25ef-fada-4cbd-a341-f9eb22fb2f48/
    '   how-to-save-a-rich-text-into-a-sql-server-database-in-a-wpf-application?forum=wpf
    Using stream As New MemoryStream
        Try
            range.Save(stream, formato, True)
            Dim buffer = Encoding.UTF8.GetString(stream.ToArray())
            Using sw As New StreamWriter(_fileName, False, Encoding.Default)
                sw.Write(buffer)
            End Using

            guardado = True
        Catch ex As Exception
            MessageBox.Show("Error el formato no es válido" & vbCrLf &
                            ex.Message,
                            $"Guardar {formato}",
                            MessageBoxButton.OK,
                            MessageBoxImage.Asterisk)
        End Try
    End Using
    'Using fStream As New FileStream(_fileName, FileMode.Create)
    '    Try
    '        range.Save(fStream, formato, True)
    '        guardado = True
    '    Catch ex As Exception
    '        MessageBox.Show("Error el formato no es válido" & vbCrLf &
    '                        ex.Message,
    '                        $"Guardar {formato}",
    '                        MessageBoxButton.OK,
    '                        MessageBoxImage.Asterisk)
    '    End Try
    '    fStream.Close()
    'End Using

    Return guardado
End Function

Al final de la función tienes (comentado) el código anterior.

/// <summary>
///  Adaptado del ejemplo de la documentación de Microsoft
///  https://docs.microsoft.com/es-es/dotnet/framework/wpf/controls/
///     richtextbox-overview
///  </summary>
private bool SaveRtfFormat(string _fileName, 
                           RichTextBox richTB, 
                           string formato)
{
    TextRange range;
    bool guardado = false;

    range = new TextRange(richTB.Document.ContentStart, 
                          richTB.Document.ContentEnd);

    // Para guardar con el formato que queramos
    // Adaptado de:
    // https://social.msdn.microsoft.com/Forums/vstudio/en-US/
    // a9ef25ef-fada-4cbd-a341-f9eb22fb2f48/
    // how-to-save-a-rich-text-into-a-sql-server-database-in-a-wpf-application?forum=wpf
    using (MemoryStream stream = new MemoryStream())
    {
        try
        {
            range.Save(stream, formato, true);
            var buffer = Encoding.UTF8.GetString(stream.ToArray());
            using (StreamWriter sw = new StreamWriter(_fileName, 
                                                      false, 
                                                      Encoding.Default))
            {
                sw.Write(buffer);
            }

            guardado = true;
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error el formato no es válido\r\n" + 
                            ex.Message, 
                            $"Guardar {formato}", 
                            MessageBoxButton.OK, 
                            MessageBoxImage.Asterisk);
        }
    }

    //    using (FileStream fStream = new FileStream(_fileName, 
    //                                               FileMode.Create))
    //    {
    //        try
    //        {
    //            range.Save(fStream, formato);
    //            guardado = true;
    //        }
    //        catch (Exception ex)
    //        {
    //            MessageBox.Show("Error el formato no es válido\r\n" + 
    //                ex.Message, $"Guardar {formato}", 
    //                MessageBoxButton.OK, MessageBoxImage.Asterisk);
    //        }

    //        fStream.Close();
    //    }

    return guardado;
}

Lo curioso del caso es que si lo guardaba como Rtf (DataFormats.Rtf) las vocales acentuadas se guardaban bien… Pero de esta forma, todo se guarda bien, al menos los tres formatos que he probado: Rtf, Xaml y Text.

Y es que en las pruebas que estaba haciendo usaba el código de ejemplo de la utilidad Compilar y ejecutar y tengo en varios sitios escrita la palabra versión y al guardarlo y después volver a abrirlo con formato Xaml, se mostraba como en la figura 1.

Figura 1. Los caracteres raros de la o con tilde ó
Figura 1. Los caracteres raros de la o con tilde ó

Al principio ni me fijé, pero cuando la ristra esa de caracteres raros se hizo más larga, ya que si me fijé Surprised smile

¡Como para no darme cuenta!

Y ya está… ahora modificaré la entrada anterior o pondré una aclaración para que vengas aquí Winking smile (para que se vea que el Guille también se equivoca jajaja)

Espero que te sea de utilidad. Esa es la idea.

Nos vemos.
Guillermo