Índice de la sección dedicada a .NET (en el Guille) ASP.NET

Crear un servicio Web con un editor de textos

Código para Visual Basic.NET (VB.NET)

Código para C Sharp (C#)

Publicado el 07/Abr/2005
Actualizado el 07/Abr/2005
Autor: Guillermo 'guille' Som

Introducción

En este artículo te voy a explicar cómo crear un servicio Web usando un editor de textos.
Veremos "paso a paso" el código que tenemos que escribir. También veremos cómo compilar desde la línea de comandos y cómo podemos crear un cliente que use este servicio Web, todo por medio de la línea de comandos.

 

¿Por qué usar un editor de textos en lugar de Visual Studio .NET?

No es un capricho. En principio vamos a crear el servicio Web con un editor de textos para "saltarnos" los extras que el Visual Studio .NET añade a un proyecto del tipo Servicio Web, ya que, al menos desde mi punto de vista, le añade cosas de más... que realmente no nos son necesarias.

¿Qué hace Visual Studio que no es necesario?

Cuando creamos un nuevo proyecto del tipo Servicio Web en Visual Studio, éste crea el servicio Web en nuestro servidor local (localhost), pero lo hace creando un "sitio Web", es decir, el servicio Web lo crea en algo parecido a lo que sería un sitio Web "completo", para que lo entiendas, este sitio Web en el que estás leyendo este artículo sería el equivalente al sitio Web que Visual Studio crea para cualquier tipo de proyecto ASP.NET, por tanto, creo que es más interesante que no cree algo que al final acabe de confundirnos, ya que lo que nos interesa es crear el servicio Web para después usarlo en el sitio que tengamos en Internet.

¿Perderemos funcionalidad al hacerlo "a mano"?

No. Porque el hecho de que usemos un editor de textos no nos impedirá que hagamos lo mismo que hace Visual Studio, es decir, vamos a crear el servicio Web usando un ensamblado (DLL) con el código que dicho servicio Web usará, que es al fin y al cabo lo que hace Visual Studio.

Aunque también te explicaré cómo hacerlo más fácil, es decir, sin usar un ensamblado DLL. En ese caso todo el código lo incluiremos en el fichero que finalmente publicaremos en nuestro sitio de Internet.

 

Crear un servicio Web en un solo fichero

Vamos a empezar viendo cómo crear un servicio Web que incluye todo lo que necesita en un mismo fichero, después "desglosaremos" el contenido del servicio Web para que podamos crear un ensamblado con el código.

Nota:
El servicio Web que vamos a crear usará la base de datos Northwind. El servicio Web recibirá como parámetro una cadena de selección con los datos que queremos (la típica cadena SELECT) y devolverá un DataSet con los datos solicitados.

Lo primero que haremos es crear un fichero con la extensión .asmx, esta es la extensión usada para los servicios Web, en nuestro ejemplo, el fichero se va a llamar: Northwind.asmx

En este fichero vamos a crear una clase llamada Northwind con un solo método: Empleados.
Este método recibirá un parámetro del tipo String en el que podemos indicar la cadena de selección o bien podemos usar una cadena vacía, en cuyo caso se usará una cadena de selección que devolverá algunos campos de la tabla Employees. El valor devuelto por esa función será un objeto DataSet con los datos indicados en la cadena de selección.

Nota:
Un servicio Web realmente lo que contiene es una clase con el código que usaremos para darle "funcionalidad", por tanto, siempre debemos crear una clase con cada servicio Web. Aunque el código del servicio Web puede contener más de una clase, solamente una es la que se expone como parte a usar del servicio Web.

Para que .NET se entere de que lo que contiene el fichero es un servicio Web debemos usar una directiva de ASP.NET:

<%@ WebService

Con esto le indicamos que lo que contiene el fichero es un servicio Web, ya que no solo basta que la extensión sea .asmx, aunque si no tiene esa extensión no lo reconocerá como servicio Web.

A continuación, en la misma directiva ASP.NET le decimos el lenguaje de programación que vamos a usar, en el caso de VB indicaremos:

Language = "VB"

Si vamos a usar C#, sustituimos VB por C#:

Language = "C#"

Por último tenemos que indicar el nombre de la clase en la que escribiremos el código que utilizará este servicio Web:

Class = "NorthwindSW"

Y cerramos la directiva de ASP.NET:

%>

A partir de este momento, lo que debemos escribir es el código en el lenguaje indicado para definir la clase y los métodos que el servicio Web expondrá públicamente. Esos métodos serán los que podremos usar en una aplicación cliente.

Como vamos a acceder a una base de datos de SQL Server, añadiremos los espacios de nombres en los que están definidas las clases que vamos a usar, para Visual Basic:

Imports System.Data 
Imports System.Data.SqlClient

Para C#:

using System.Data;
using System.Data.SqlClient;

Como te comentaba antes, cuando definimos un servicio Web realmente estamos definiendo una clase y dentro de esa clase indicaremos que métodos (funciones) queremos exponer, para exponer esas funciones como parte del servicio Web, tendremos que usar unos atributos que están definidos en el espacio de nombres System.Web.Services, por tanto, para ahorrarnos la escritura de algunos caracteres en nuestro código, también incluiremos una importación de ese espacio de nombres:

Imports System.Web.Services	' para VB
using System.Web.Services;	// para C#

Nota:
Como ya sabrás, (si no lo sabes te lo recuerdo yo), las importaciones de espacios de nombres sirve para ahorrarnos escribir el nombre completo de una clase. Ese nombre completo está formado por el espacio de nombres y el nombre de la clase. Es como si incluyéramos un directorio en el PATH del sistema, así podremos acceder a las clases que estén en ese "directorio/espacio de nombres", sin necesidad de tener que estar repitiendo el sitio donde están las clases que queremos usar.
Por ejemplo, para acceder a la clase DataSet, si tenemos la importación de System.Data podemos usarla de forma directa: DataSet, o también usando el nombre completo: System.Data.DataSet.

Una vez indicadas las importaciones de espacios de nombres, definimos la clase, a la que le vamos a aplicar el atributo WebServiceAttribute para indicar el espacio de nombres del servicio Web (recomendable al 100%) y una descripción de lo que hace este servicio Web, con idea de que si alguien lo "localiza", sepa para que sirve.
Las clases que definen un servicio Web deben estar derivadas de System.Web.Services.WebService, por tanto debemos indicarlo también:

Para Visual Basic:

<WebService(Namespace:="http://elGuille/ServiciosWeb/", _
	Description:="Acceso a la base de datos Northwind (local) desde un servicio Web")> _
Public Class NorthwindSW
	Inherits System.Web.Services.WebService

Para C#:

[WebService(Namespace="http://elGuille/ServiciosWeb/", 
	Description="Acceso a la base de datos Northwind (local) desde un servicio Web")]
public class NorthwindSW : System.Web.Services.WebService
{

Nota:
A partir de aquí, te dejo adivinar cual es el código para Visual Basic .NET y cual es para C#. Así que, si ves dos líneas o trozos de código que son parecidos... usa el que tengas que usar dependiendo del lenguaje que hayas elegido.
Una pista: El código de VB no termina en punto y coma (;) ni usa llaves ({ o })

Si no te aclaras, no te preocupes, al final te muestro todo el código al completo, el ir mostrando el código en los dos lenguajes es para que veas que es prácticamente el mismo en VB que en C#, salvo por las diferencias sintácticas.

Para acceder a la tabla de la base de datos usaremos un DataAdapter de SQL:

Private da As SqlDataAdapter
private SqlDataAdapter da;

Ahora definimos la función (o método) que se expondrá en el servicio Web, a estas funciones "expuestas" se las llama métodos Web y debemos aplicarle el atributo WebMethodAttribute, al que también le podemos añadir una descripción para que sepamos que es lo que hace. El único método Web que tendrá nuestra clase, será uno llamado Empleados el cual recibe una cadena como parámetro, y devuelve un objeto DataSet con los datos que hayamos obtenido al usar esa cadena como cadena de selección:

<WebMethod(Description:="Devuelve datos indicados en el parámetro de la base Northwind")> _
Public Function Empleados(sel As String) As DataSet
[WebMethod(Description="Devuelve datos indicados en el parámetro de la base Northwind")]
public DataSet Empleados(string sel)
{

Si el parámetro es una cadena vacía, usaremos una selección predeterminada para extraer los datos de la tabla Employees, aunque en la cadena de selección que pases por parámetro puedes usar la tabla de Northwind que quieras:

If sel = "" Then
	sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees"
End If
if( sel == "" )
	sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees";

Ahora creamos el objeto DataAdapter usando esta cadena de selección y las instrucciones necesarias para que se conecte con el servidor de SQL Server y la base de datos Northwind, en este ejemplo se utiliza la autenticación de Windows:

da = New SqlDataAdapter(sel, _
	"integrated security=true; data source=(local); initial catalog=Northwind")
da = new SqlDataAdapter(sel, 
	"integrated security=true; data source=(local); initial catalog=Northwind");

Si queremos usar un nombre de usuario y un password, también podríamos hacerlo:

da = New SqlDataAdapter(sel, _
	"user id=usuario; password=clave; data source=(local); initial catalog=Northwind")
da = new SqlDataAdapter(sel, _
	"user id=usuario; password=clave; data source=(local); initial catalog=Northwind");

 

Nota:
En este sitio: http://www.connectionstrings.com/ puedes encontrar ejemplos de cadenas de conexión a distintos tipos de bases de datos, como SQL Server, Access, Oracle, MySQL, etc., además usando diferentes "proveedores": OleDb, Odbc, etc.

Para conservar los datos, vamos a usar un DataSet, por tanto tendremos que crear un nuevo objeto de este tipo para después usarlo con el DataAdapter:

Dim ds As New DataSet()
DataSet ds = new DataSet();

Ahora tenemos que "llenar" el DataSet con los datos solicitados, para ello utilizamos el método Fill del DataAdapter. Como no nos fiamos de que todo vaya a funcionar bien, y para no provocar un desastre mayor, es recomendable a la hora de usar ese método, hacerlo dentro de un bloque Try/Catch para poder interceptar los posibles errores que se produzcan:

Try
	da.Fill(ds)
Catch ex As Exception
	Throw ex
End Try
try
{
	da.Fill(ds);
}
catch(Exception ex)
{
	throw ex;
}

Como puedes comprobar, si se produce una excepción, se la devolvemos al cliente que use nuestro servicio Web, así tendrá más cuidado a la hora de usar las cadenas de selección.

Por último devolvemos el DataSet con los datos:

	Return ds
    End Function
End Class
	return ds;
    }
}

Algunas comprobaciones extras para que no nos "la cuelen"

Cuando estemos trabajando con cadenas de SQL, siempre deberíamos tener cuidado de que no se pasen valores incorrectos e incluso "peligrosos".
En este ejemplo, sólo deberíamos admitir sentencias SELECT, por tanto haremos algunas comprobaciones de que la cadena pasada como argumento no tenga código malicioso.
Después del IF de que la cadena está vacía, añade el siguiente código:

' Comprobar que están indicando valores correctos (o casi)
'
' Que no sea una cadena vacía
If sel = "" Then
	Throw New ArgumentException("La cadena no puede ser nula")
End If
' Comprobar que realmente se use SELECT,
If sel.ToUpper().IndexOf("SELECT") = -1 Then
	Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>")
End If
' no permitir comentarios ni algunas instrucciones maliciosas
If sel.IndexOf("--") > -1 Then
	Throw New ArgumentException("No se admiten comentarios de SQL en la cadena de selección")
End If
If sel.ToUpper().IndexOf("DROP") > -1 Then
	Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...")
End If 
// Comprobar que están indicando valores correctos (o casi)
//
// Que no sea una cadena vacía
if( sel == "" )
	throw new ArgumentException("La cadena no puede ser nula");
//
// Comprobar que realmente se use SELECT,
if( sel.ToUpper().IndexOf("SELECT") == -1 )
	throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>");
//
// no permitir comentarios ni algunas instrucciones maliciosas
if( sel.IndexOf("--") > -1 )
	throw new ArgumentException("No se admiten comentarios de SQL en la cadena de selección");
//
if( sel.ToUpper().IndexOf("DROP") > -1 )
	throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...");

 

Guarda el fichero, y cópialo en el directorio del servidor local (localhost) el cual estará en C:\Inetpub\wwwroot

Ahora solo queda hacer un cliente que consuma el servicio Web, además de crear una clase que permita a ese cliente usarlo.

Crear una clase "proxy" para acceder al servicio Web

Una vez que tenemos el servicio Web, debemos crear una clase para usarla en la aplicación cliente. La clase la vamos a generar automáticamente con la utilidad WSDL.exe.
Esta clase servirá para que el compilador sepa dónde está el servicio Web y qué funciones tiene, con idea de que podamos usar la clase como una clase normal y corriente.

Nota:
Si tienes instalado Visual Studio .NET, tendrás un acceso directo a "Símbolo del sistema de Visual Studio .NET 2003" que te carga las variables de entorno necesarias para usar las herramientas de .NET.
Si solo tienes el SDK de .NET, en el directorio bin del SDK habrá un fichero .bat llamado sdkvars.bat que carga los valores adecuados para usar desde la línea de comandos, crea un acceso directo que contenga esta instrucción:
%comspec% /k "<directorio del SDK>\sdkvars.bat"
Y úsalo para compilar y ejecutar las herramientas del SDK.

Abre una ventana de MSDOS con el path a las herramientas de .NET Framework y escribe lo siguiente:

Para Visual Basic:

WSDL /l:vb http://localhost/Northwind.asmx 

Para C#:

WSDL http://localhost/Northwind.asmx

Esto creará una clase llamada de la misma forma que el nombre de la clase definida en el servicio Web: NorthwindSW.vb o NorthwindSW.cs, según el lenguaje indicado.
A esta clase me referiré como la clase "proxy". Que es la que se usará para acceder al servicio Web.

 

Continuará...

 

Bueno, vamos a dejar el tema por ahora... Al final te muestro el código de un cliente de consola que utiliza este servicio Web y las instrucciones para compilarlo desde la línea de comandos.

Nos vemos.
Guillermo


Código para Visual Basic.NET (VB.NET)Código para Visual Basic

El código completo para Visual Basic .NET, tanto del servicio Web como de la aplicación cliente para usarlo.

El servicio Web:

<%@ WebService Language="VB" Class="NorthwindSW" %>
'------------------------------------------------------------------------------
' Servicio Web para acceder a una tabla de Northwind                (15/Mar/05)
'
' ©Guillermo 'guille' Som, 2005
'
' Para crear la clase "proxy":
' WSDL /l:vb http://localhost/Northwind.asmx
'------------------------------------------------------------------------------
Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports System.Web.Services

<WebService(Namespace:="http:"//elGuille/ServiciosWeb/", _
            Description:="Acceso a la base de datos Northwind (local) desde un servicio Web")> _
Public Class NorthwindSW_vb
    Inherits System.Web.Services.WebService
    '
    Private da As SqlDataAdapter
    '
    <WebMethod(Description:="Devuelve datos indicados en el parámetro de la base Northwind")> _
    Public Function Empleados(sel As String) As DataSet
        '
        If sel = "" Then
            sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees"
        End If
        '
        ' Comprobar que están indicando valores correctos (o casi)
        '
        ' Que no sea una cadena vacía
        If sel = "" Then
            Throw New ArgumentException("La cadena no puede ser nula")
        End If
        ' Comprobar que realmente se use SELECT,
        If sel.ToUpper().IndexOf("SELECT") = -1 Then
            Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>")
        End If
        ' no permitir comentarios ni algunas instrucciones maliciosas
        If sel.IndexOf("--") > -1 Then
            Throw New ArgumentException("No se admiten comentarios de SQL en la cadena de selección")
        End If
        If sel.ToUpper().IndexOf("DROP") > -1 Then
            Throw New ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...")
        End If
        '
        da = New SqlDataAdapter(sel, _
                                "integrated security=true; data source=(local); initial catalog=Northwind")
        '
        Dim ds As New DataSet()
        '
        Try
            da.Fill(ds)
        Catch ex As Exception
            Throw ex
        End Try
        '
        Return ds
    End Function
End Class

 

La aplicación cliente (de consola)

Para compilar esta aplicación, necesitamos la clase proxy generada con WSDL.
La línea de comando para compilar es:

vbc ClienteNorthwindSW.vb NorthwindSW.vb /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll

 

'------------------------------------------------------------------------------
' ClienteNorthwindSW                                                (05/Abr/05)
' 
' ©Guillermo 'guille' Som, 2005
'
' Para crear el EXE:
' vbc ClienteNorthwindSW.vb NorthwindSW.vb /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll
'------------------------------------------------------------------------------
Option Strict On
Option Explicit On

Imports Microsoft.VisualBasic
Imports System
Imports System.Data

Public Class ClienteNorthwindSW
    '
    <STAThread> _
    Shared Sub Main(args As String())
        ' punto de entrada del ejecutable
        Dim sw As New NorthwindSW
        Dim sel As String = "SELECT LastName, FirstName, Title, BirthDate FROM Employees"
        '
        ' Si se ha indicado algún parámetro en la línea de comandos
        ' será la cadena de selección
        If args.Length = 1 Then
            ' Si hay uno, se habrá indicado entre comillas dobles
            sel = args(0)
        ElseIf args.Length > 1 Then
            ' Se habrá indicado sin usar comillas dobles
            ' unir todas las cadenas en una sola
            sel = String.Join(" ", args).Trim()
        End If
        '
        Dim ds As DataSet 
        Try
            ds = sw.Empleados(sel)
        Catch ex As Exception
            Console.WriteLine("ERROR: " & ex.Message)
            Return
        End Try
        '
        ' Para ajustar el texto mostrado
        Const mostrar As Integer = 21
        Dim sb As New System.Text.StringBuilder
        '
        ' Mostrar las cabeceras
        For Each columna As DataColumn In ds.Tables(0).Columns
            If columna.DataType.ToString = "System.DateTime" Then
                Console.Write("{0} ", ajustar(columna.ColumnName, 10))
                sb.AppendFormat("{0} ", New String("-"c, 10) )
            Else
                Console.Write("{0} ", ajustar(columna.ColumnName, mostrar))
                sb.AppendFormat("{0} ", New String("-"c, mostrar) )
            End If            
        Next
        Console.WriteLine()
        Console.WriteLine(sb.ToString)
        ' Mostrar los datos
        For Each fila As DataRow In ds.Tables(0).Rows
            For Each columna As DataColumn In ds.Tables(0).Columns
                ' Si es una fecha, usar un formato especial
                If TypeOf fila(columna) Is DateTime Then 
                    Console.Write("{0} ", CType(fila(columna), DateTime).ToString("dd/MM/yyyy"))
                Else
                    Console.Write("{0} ", ajustar(fila(columna).ToString, mostrar))
                End If
            Next
            Console.WriteLine()
        Next
    End Sub

    ' Ajustar la cadena al ancho indicado
    Private Shared Function ajustar(cadena As String, ancho As Integer) As String
        Return ( cadena & New String(" "c, ancho) ).Substring(0, ancho)
    End Function
End Class

 

 


Código para C Sharp (C#)Código para C#

El código completo para C#, tanto del servicio Web como de la aplicación cliente para usarlo.

El servicio Web:

<%@ WebService Language="C#" Class="NorthwindSW" %>
//-----------------------------------------------------------------------------
// Servicio Web para acceder a una tabla de Northwind                (15/Mar/05)
//
// (c)Guillermo 'guille' Som, 2005
//
//-----------------------------------------------------------------------------
using System;
using System.Data;
using System.Data.SqlClient;
using System.Web.Services;

[WebService(Namespace="http:"//elGuille/ServiciosWeb/",
    Description="Acceso a la base de datos Northwind (local) desde un servicio Web")]
public class NorthwindSW : System.Web.Services.WebService
{
    //
    private SqlDataAdapter da;
    //
    [WebMethod(Description="Devuelve datos indicados en el parámetro de la base Northwind")]
    public DataSet Empleados(string sel)
    {
        //
        if( sel == "" )
            sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees";
        //
        // Comprobar que están indicando valores correctos (o casi)
        //
        // Que no sea una cadena vacía
        if( sel == "" )
            throw new ArgumentException("La cadena no puede ser nula");
        //
        // Comprobar que realmente se use SELECT,
        if( sel.ToUpper().IndexOf("SELECT") == -1 )
            throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>");
        //
        // no permitir comentarios ni algunas instrucciones maliciosas
        if( sel.IndexOf("--") > -1 )
            throw new ArgumentException("No se admiten comentarios de SQL en la cadena de selección");
        //
        if( sel.ToUpper().IndexOf("DROP") > -1 )
            throw new ArgumentException("La cadena debe ser SELECT <campos> FROM <tabla>, no DROP y otros comandos no adecuados...");
        //
        da = new SqlDataAdapter(sel,
                                "integrated security=true; data source=(local); initial catalog=Northwind");
        //
        DataSet ds = new DataSet();
        try
        {
            da.Fill(ds);
        }
        catch(Exception ex)
        {
            throw ex;
        }
        return ds;
    }
}

 

La aplicación cliente (de consola)

Para compilar esta aplicación, necesitamos la clase proxy generada con WSDL.
La línea de comando para compilar es:

csc ClienteNorthwindSW.cs NorthwindSW.cs /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll

 

//------------------------------------------------------------------------------
// ClienteNorthwindSW                                                (05/Abr/05)
//
// ©Guillermo 'guille' Som, 2005
//
// Para crear el EXE:
// csc ClienteNorthwindSW.cs NorthwindSW.cs /r:System.dll /r:System.Data.dll /r:System.Web.Services.dll /r:System.Xml.dll
//------------------------------------------------------------------------------

using System;
using System.Data;

public class ClienteNorthwindSW{
    //
    [STAThread]
    static void Main(string[] args) {
        // punto de entrada del ejecutable
        NorthwindSW sw = new NorthwindSW();
        string sel = "SELECT LastName, FirstName, Title, BirthDate FROM Employees";
        //
        // Si se ha indicado algún parámetro en la línea de comandos
        // será la cadena de selección
        if( args.Length == 1 ){
            // Si hay uno, se habrá indicado entre comillas dobles
            sel = args[0];
        }else if( args.Length > 1 ){
            // Se habrá indicado sin usar comillas dobles
            // unir todas las cadenas en una sola
            sel = String.Join(" ", args).Trim();
        }
        //
        DataSet ds;
        try{
            ds = sw.Empleados(sel);
        }catch(Exception ex){
            Console.WriteLine("ERROR: " + ex.Message);
            return;
        }
        //
        // Para ajustar el texto mostrado
        const int mostrar = 21;
        System.Text.StringBuilder sb = new System.Text.StringBuilder();
        //
        // Mostrar las cabeceras
        foreach(DataColumn columna in ds.Tables[0].Columns){
            if( columna.DataType.ToString() == "System.DateTime" ){
                Console.Write("{0} ", ajustar(columna.ColumnName, 10));
                sb.AppendFormat("{0} ", new string('-', 10) );
            }else{
                Console.Write("{0} ", ajustar(columna.ColumnName, mostrar));
                sb.AppendFormat("{0} ", new string('-', mostrar) );
            }
        }
        Console.WriteLine();
        Console.WriteLine(sb.ToString());
        // Mostrar los datos
        foreach(DataRow fila in ds.Tables[0].Rows){
            foreach(DataColumn columna in ds.Tables[0].Columns){
                // Si es una fecha, usar un formato especial
                if( fila[columna] is DateTime ){
                    Console.Write("{0} ", ( (DateTime)fila[columna] ).ToString("dd/MM/yyyy"));
                }else{
                    Console.Write("{0} ", ajustar(fila[columna].ToString(), mostrar));
                }
            }
            Console.WriteLine();
        }
    }
    // Ajustar la cadena al ancho indicado
    private static string ajustar(string cadena, int ancho) {
        return ( cadena + new string(' ', ancho) ).Substring(0, ancho);
    }
}

 


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