Ejecución de código proporcionado por el usuario en C#

Parte 1

Excel es una aplicación ampliamente utilizada en cualquier oficina, entre las muchas funciones con las que cuenta, esta la posibilidad de crear Macros (grupo de instrucciones programadas bajo entorno visual basic para aplicaciones o vba, cuya tarea principal es la automatización de tareas repetitivas y la resolución de cálculos complejos).

C# y .Net framework permiten tomar un texto que contiene código fuente en C# y generar un ensamblado (dll o exe) que puede ser importado y ejecutado en por una aplicación, esto mediante la clase CSharpCodeProvider del espacio de nombre Microsoft.CSharp. Si bien esta funcionalidad no esta pensada para la creación de macros en una aplicación, si puede ser aprovechada para este fin.

Un ejemplo practico.

Imaginemos por un momento que tenemos una aplicación que permite visualizar imágenes en distintos formatos, sin embargo, deseamos que el usuario pueda generar nuevas funcionalidades para esta aplicación, esta sencilla aplicaciones puede verse como sigue:

Vista de la aplicacion

La funcionalidad del primer botón de la barra de herramientas es el abrir la imagen y mostrarla en pantalla, sin embargo, el segundo botón muestra una segunda pantalla que nos solicita tres datos:

Generador de codigo
  1. Nombre de la funcionalidad: Este sera el nombre que se le establecerá al botón generado en la barra de tareas para ejecutar la funcionalidad creada.
  2. Imagen del botón: Este es el icono que se utilizara para crear el botón con la funcionalidad creada.
  3. Código: Script en C# que escribirá el usuario para ejecutar la funcionalidad.

El código que el usuario escribirá parte de la premisa, 

El codigo porporcionado se encontrara dentro de un método denominado Execute, que recibe un parámetro denominado image, el cual contendrá la imagen cargada en la aplicación o nulo en caso de no haber cargado ninguna imagen, este método no regresara ningún valor.

public static void Execute(ref System.Drawing.Image image)

Entrando en materia.

Una vez que el usuario proporcione los datos anteriores es necesario tomar el código del usuario y generar un ensamblado (exe o dll) con el código compilado, generar un botón para la barra de herramientas que el usuario y ejecutar el código cada vez que el usuario presione el botón, como se muestra a continuación.

Generamos una instancia de la clase CSharpCodeProvider, encargado de generar el ensamblado final

var provider = new CSharpCodeProvider();

Generamos una instancia de la clase CompilerParameters, la cual utilizaremos para establecer los parámetros mínimos para la generación del ensamblado: GenerateInMemory igual a true para indicarle que no deberá generar un archivo en disco, si no que el resultado solo estará en memoria y GenerateExecutable en false para indicar que se generara una dll y no un archivo ejecutable.

var parameters = new CompilerParameters()
{
    GenerateInMemory = true,
    GenerateExecutable = false
};

Indicamos las referencias (otros ensamblados) que son necesarios para ejecutar el código

parameters.ReferencedAssemblies.Add("System.dll");
parameters.ReferencedAssemblies.Add("System.Drawing.dll");
parameters.ReferencedAssemblies.Add("System.Windows.Forms.dll");

Solamente estamos referenciando los ensamblados estrictamente necesarios, sin embargo, usted podría solicitar al usuario que otro ensamblado desea o requiere utilizar.

Después instanciamos una variable de tipo string con la estructura completa de una clase que incluirá el código proporcionado por el usuario almacenado en la variable Code.

var code = $@"
using System;
namespace UserCodes
{{
    public static class custom
    {{
        public static void Execute(ref System.Drawing.Image image){{ {Code} }}
    }}
}}";

Generamos el ensamblado al solicitar la compilación del código.

CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);

La instrucción anterior retorna un objeto que puede contener errores en la propiedad Errors, lo que nos indicaría que el código proporcionado contiene algún error de sintaxis por lo que tendríamos que mostrarlo al usuario. En este caso le mostraremos al usuario los errores en un cuadro de dialogo.

if (results.Errors.HasErrors)
{
    StringBuilder sb = new StringBuilder();

    foreach (CompilerError error in results.Errors)
        sb.AppendLine($"Error ({error.ErrorNumber}): {error.ErrorText}");

    MessageBox.Show(sb.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}

En caso de no existir errores entonces el ensamblado esta correctamente generado y solo resta almacenarlo en alguna variable o propiedad para su futura ejecución.

¿Y como lo ejecutamos?

Para generar el botón utilizamos la imagen (almacenado en la variable imagePath) y el nombre proporcionado por el usuario (almacenado en la variable functionalityName).

var btn = new ToolStripButton();
btn.DisplayStyle = ToolStripItemDisplayStyle.Image;
btn.Image = Image.FromFile(imagePath);
btn.ImageTransparentColor = Color.Magenta;
btn.Name = compiler.Id;
btn.Size = new Size(36, 36);
btn.Text = functionalityName;
toolStrip.Items.AddRange(new ToolStripItem[] { btn });

Es necesario agregar al evento click del botón un método, el cual utilize el metodo Invoke de la clase Assembly, para solicitar la ejecución del método Execute generado a partir del código del usuario y almacenado, en este caso, en la variable _assembly.

private void ButtonClick(object sender, EventArgs e)
{
    if (_assembly != null)
    {    
        Type program = _assembly.GetType($"UserCodes.custom");
        var executeMethod = program.GetMethod("Execute");
        if (_executeMethod != null)
            _executeMethod.Invoke(null, new object[] { picture.BackgroundImage; });
        else
            throw new Exception("Cannot execute the functionality");
    }
    else
        throw new Exception("Cannot execute the functionality");
}

El código anterior obtiene el tipo de la clase generada con el código del usuario (UserCodes.custom), y a partir de este obtiene el método Execute.

Después de validar que el método Execute existe, solo resta solicitar su ejecución mediante el método Invoke de la clase MethodInfo.

Con lo anterior el usuario podrá generar funcionalidades como girar la imagen 90 grados

if (image == null)
    System.Windows.Forms.MessageBox.Show("No ha abierto ninguna imagen.");
else 
{
	image.RotateFlip(System.Drawing.RotateFlipType.Rotate90FlipNone);
}

O simplemente almacenar la imagen en un archivo

if (image == null)
    System.Windows.Forms.MessageBox.Show("No ha abierto ninguna imagen.");
else 
{    
    System.Windows.Forms.SaveFileDialog dlg = new System.Windows.Forms.SaveFileDialog();
    if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
        image.Save(dlg.FileName, System.Drawing.Imaging.ImageFormat.Png);
        System.Windows.Forms.MessageBox.Show("Imagen guardada satisfactoriamente");
    }
}

A partir de este momento la imaginación del usuario es lo que dictara el alcance de las funcionalidades a implementar.

El código de esta aplicación esta disponible aqui, para la persona que desee profundizar un poco mas en la implementación de la funcionalidad.

Conclusion

Con lo que hemos visto en este articulo, se podría ampliar por parte del usuario una aplicación, brindando así cierta flexibilidad que muchos usuarios que están acostumbrados a utilizar los macros de Excel, por dar un ejemplo, y desean que tuvieran sus otras aplicaciones.

En la siguiente parte de este articulo hablare sobre las ventajas y desventajas de esta implementación, así como los posibles riesgos de seguridad que pueden presentarse.

Jean Carlo

Entusiasta de la programacion, freelance intermitente, lector asiduo, cinefilo, y recientemente editor en esta pagina.

0 comentarios

Agregar un comentario

;