Welcome to Comunidad .NET de Cd. Juárez Sign in | Join | Help

Browse by Tags

All Tags » .net

Cómo acceder a controles dentro de un GridView

Esta entrada salió como resultado de una pregunta que hicieron en el Foro de la Comunidad .NET.  La pregunta, esencialmente, es: ¿Cómo accedo a un control que tengo en una columna templeteada dentro de un GridView de ASP.NET?

Me pareció buena la pregunta, así que hice un pequeño ejemplo para ilustrarlo.   Una columna templeteada (TemplateField) es distinta a una normal (BoundField) en que tienes más control sobre los controles que aparecen en la columna.  En otras palabras, tú especificas qué quieres que se muestre cuando el renglón está en modo "normal" (ItemTemplate) o cuando está en modo "edición" (EditItemTemplate).  Puedes tener múltiples controles dentro de la columna, en lugar de solo uno que represente el dato.

La siguiente página contiene un GridView con tres columnas que vienen de un SqlDataSource.  Los datos provienen de la clásica base de datos Northwind.   En este caso estoy recuperando datos de la tabla de productos:

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="Default.aspx.vb"
  Inherits="_Default" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
  <title>Accediendo a Elementos en un GridView</title>
  <style type="text/css">
    #miGrid
    {
      width: 450px;
      float: left;
    }
  </style>
</head>
<body>
  <form id="form1" runat="server">
  <asp:Button runat="server" ID="leerElementosButton" Text="Leer elementos" />
  <div id="miGrid">
    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
      DataKeyNames="ProductID" DataSourceID="SqlDataSource1" 
      EmptyDataText="No hay registros que mostrar.">
      <Columns>
        <asp:BoundField DataField="ProductID" HeaderText="ID Producto" 
          InsertVisible="False" ReadOnly="True" />
        <asp:BoundField DataField="ProductName" HeaderText="Nombre Producto" />
        <asp:TemplateField HeaderText="Descontinuado">
          <EditItemTemplate>
            <asp:CheckBox ID="CheckBox1" runat="server" 
              Checked='<%# Bind("Discontinued") %>' />
          </EditItemTemplate>
          <ItemTemplate>
            <asp:CheckBox ID="CheckBox1" runat="server" 
              Checked='<%# Bind("Discontinued") %>' Enabled="false" />
          </ItemTemplate>
        </asp:TemplateField>
      </Columns>
    </asp:GridView>
    <asp:SqlDataSource ID="SqlDataSource1" runat="server" 
      ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>"
      ProviderName="<%$ ConnectionStrings:NorthwindConnectionString.ProviderName %>"
      SelectCommand="SELECT [ProductID], [ProductName], [Discontinued] FROM [Products]">
    </asp:SqlDataSource>
  </div>
  <div id="resultados">
    <asp:Label runat="server" ID="resultadosLabel" />
  </div>
  </form>
</body>
</html>

Para acceder, por ejemplo, al CheckBox que está dentro de la columna templeteada, primero necesitas una referencia al renglón, ya que el ID del control se repetirá n veces (una por cada renglón del Grid). 

Esto se puede hacer de varias formas, por ejemplo, si tienes un manejador para un evento clic en alguno de los controles dentro del renglón pues en ese caso el sender viene siendo el renglón en sí, entonces únicamente te falta encontrar el CheckBox de ese renglón para leer sus propiedades.   Otra forma de hacerlo, como en el siguiente ejemplo, es leer todos los renglones y por cada uno encontrar el CheckBox que le corresponde al renglón:

Partial Class _Default
  Inherits System.Web.UI.Page
 
  Protected Sub leerElementosButton_Click(ByVal sender As Object, _
    ByVal e As System.EventArgs) Handles leerElementosButton.Click
 
    Dim grid As GridView = Me.GridView1
    Dim checkBox As CheckBox
    Dim resultado As New StringBuilder
 
    For Each renglon As GridViewRow In grid.Rows
      checkBox = CType(renglon.FindControl("CheckBox1"), CheckBox)
      If checkBox.Checked Then  'Hacer algo con esta información
        resultado.Append(String.Format( _
          "El renglón {0} está descontinuado <br />", renglon.DataItemIndex))
      End If
    Next
 
    resultadosLabel.Text = resultado.ToString()
 
  End Sub
End Class

En resúmen, la idea clave aquí es que el GridView tiene una colección de renglones (de tipo GridViewRow) y cada uno de estos renglones tiene su correspondiente colección de controles que están dentro de ese renglón.  El GridViewRow también te da acceso al DataItem que contiene los datos asociados al renglón, en caso de que lo requirieras.

El resultado de correr el código anterior (dando clic al botón) es el siguiente:

Espero les ayude.

Enjoy smile_shades

Posted from Diario de un Dotnetero | 0 Comments
Filed under: ,

Cómo leer archivos planos con ADO.NET (versión Visual Basic 2005)

Hace poco más de un año escribí este artículo que describe una técnica para leer archivos planos utilizando el OleDB provider de ADO.NET.  Es uno de los artículos de este sitio que ha recibido más comentarios, y entre ellos está uno que dejó fredy que me hizo re-hacer el ejemplo en Visual Basic 2005 para comprobar que no fuera un error de código—en realidad él hizo la mayor parte de la chamba para "traducir" la rutina.

No voy a explicar mucho la lógica del código—para eso te dejo de tarea que leas el artículo original—aquí simplemente te comparto cómo se vería la rutina en VB:

Imports System.IO
Imports System.Data
Imports System.Data.OleDb
 
Module Utilerias
   Public Enum TipoDeArchivoPlano
        Delimited
        Fixed
   End Enum
 
   Public Function LeerArchivoPlano(ByVal archivo As FileInfo, _
        ByVal tieneEncabezado As Boolean, _
        ByVal tipoDeArchivo As TipoDeArchivoPlano) As DataTable
 
        If (Not archivo.Exists) Then
            Throw New FileNotFoundException("No se encontró el archivo especificado")
        End If
 
        Dim conEncabezado As String = IIf(tieneEncabezado, "YES", "NO")
 
        Dim connectionString As String = _
            String.Format("Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};" + _
            "Extended Properties='text;HDR={1};FMT={2}'", _
            archivo.DirectoryName, conEncabezado, tipoDeArchivo.ToString())
 
        Dim dt As DataTable = New DataTable("miTabla")
 
        Using conn As OleDbConnection = New OleDbConnection(connectionString)
            Using da As OleDbDataAdapter = New OleDbDataAdapter( _
               "SELECT * FROM " + archivo.Name, conn)
 
               da.Fill(dt)
            End Using
        End Using
 
        Return dt
   End Function
End Module

Para probarla, hice una aplicación sencilla en ASP.NET que mostrara los datos de un archivo .CSV que está dentro de un subdirectorio del sitio web.

El archivo jason.csv contiene:

Producto,Cantidad,Precio
Sierra eléctrica,1,250
Máscara de hockey,1,15.50
Machete,5,2.70
Detergente para ropa (con quita-manchas),1,10
Delantal,2,7.25
Afilador,3,5

La página dentro de la solución que en realidad solo tiene un GridView.  Este es el contenido de Default.aspx:

<%@ Page Language="VB" AutoEventWireup="false" 
   CodeFile="Default.aspx.vb" Inherits="_Default" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
   <title>Leer un archivo plano con VB 2005</title>
</head>
<body>
   <form id="miFormulario" runat="server">
   <div>
        <asp:GridView ID="miGridView" runat="server">
        </asp:GridView>
   </div>
   </form>
</body>
</html>

Finalmente, para mandar llamar la rutina y bindear—¿enlazar?—los datos al GridView, solo agregué esto en el code-behind:

Partial Class _Default
   Inherits System.Web.UI.Page
 
   Protected Sub miFormulario_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles miFormulario.Load
 
        If Not Page.IsPostBack Then
            Dim archivo As FileInfo = _
               New FileInfo("D:\WebSites\LeerArchivosPlanosVB\Archivos\jason.csv")
 
            Dim tabla As New DataTable
            tabla = Utilerias.LeerArchivoPlano(archivo, True, TipoDeArchivoPlano.Delimited)
 
            If tabla.Rows.Count > 0 Then
               miGridView.DataSource = tabla
               miGridView.DataBind()
            Else
               Response.Write("No hay datos para mostrar.")
            End If
 
        End If
   End Sub
End Class

El resultado de correr la página:

 

Whew! Funcionó smile_teeth

Enjoy. smile_shades


PD.  Puedes descargar el código de este ejemplo del sitio de la Comunidad .NET de Cd. Juárez.

Posted from Diario de un Dotnetero | 0 Comments
Filed under: , , ,

¿Ya estás listo para Visual Studio 2008?

Con el anuncio ayer que hizo Microsoft de que Visual Studio 2008 y el .NET Framework 3.5 estarán disponibles para suscriptores de MSDN a finales de este mes (can you say: wohoo! thumbs_up) es hora de comenzar a prepararse para varias novedades que trae.Tecnologías en las diferentes versiones del .NET Framework

En lo personal me tiene emocionado el lanzamiento porque creo que le agregaron cosas que ya urgían.  Comenzando por el soporte nativo a las tecnologías de .NET 3.0 y ASP.NET AJAX—no más andar instalando extensiones y adiciones por separado.  Por ejemplo, me acaban de dar una máquina nueva en el trabajo, y solo de Visual Studio tuve que instalar:  Visual Studio 2005, Visual Studio 2005 SP1, .NET 3.0 Framework Runtime, Windows SDK February 2007 update, Visual Studio Extensions for WCF & WPF November 2006 CTP (que no funciona muy bien), Visual Studio Extensions for WF, ASP.NET AJAX y ASP.NET AJAX Control Toolkit.  Cuéntalas: OCHO cosas que se reducirán a uno o dos.

Además, finalmente le agregaron soporte decente para CSS y un editor y debuggeador de JavaScript excelente, lo cual es música para los oídos de desarrolladores web como yo.

"¡Pero aún hay más!", como dirían los vendedores: LINQ—una tecnología que promete cambiar cómo lidean los programadores con el acceso a datos, sin tener que salirse del paradigma de C# o VB—, soporte para Silverlight y más (whew!).

En fin, como una imágen dice más que mil palabras, aquí está un poster (PDF) que pueden descargar e imprimir de los tipos y espacios de nombre más comúnes con .NET 3.5.

Además, el 14 de noviembre—¡osea en menos de dos semanas!—tendremos en Ciudad Juárez el Tour Tecnológico 2007 de Microsoft.  Este es un evento como el CodeCamp que tuvimos en abril de este año o el Community Dev Day del año pasado así que no te lo pierdas. 

Tour Tecnológico Microsoft 2007

La agenda es:
8:30  Registro
8:45 Bienvenida
9:00  Características principales de VS 2008 y el .Net framework 3.5
10:00 Construyendo Clientes inteligentes usando Visual Studio 2008, WPF y Expression Blend
11:00 Receso
11:30 Construyendo aplicaciones móviles usando Visual Studio 2008 y el .NET Compact Framework 3.5
12:30 Introducción al ciclo de desarrollo con Visual Studio Team System

Para registrarte ve a esta dirección: http://www.mslatam.com/spanish/msdn/mexico/tour2007/ luego selecciona Lista de Eventos > Chihuahua > Cd. Juárez.

OJO, que en esta ocasión el evento será en uno de los edificios de la UACJ (el que está por la Avenida del Charro), NO será en el Tec de Monterrey, donde normalmente tenemos las reuniones y eventos.

Espero verlos ahi. smile_wink

Ya están disponibles las presentaciones y archivos de ASP.NET

Ya tenía un buen sin poder escribir por andar con las carreras del trabajo, pero de alguna forma tuve tiempo de dar el tema y el taller del mes de Septiembre y de Octubre de la Comunidad, donde el tema fue, primero ASP.NET "para novatos", y luego ASP.NET "intermedio".

El primer tema surgió, porque había muchas personas interesadas en comenzar a aprender ASP.NET pero que solo necesitaban una orientada de por dónde empezar.  Así que ese fue el espíritu de la reunión.  Vimos los conceptos fundamentales, basicotes pero necesarios para que entendieran qué estaba sucediendo.  Vimos algunos de los controles básicos, manejo de eventos, y hasta algo de DataBinding con controles como el GridView y DetailsView para hacer una página maestro/detalle.  Para la presentación tomé prestadas algunas de las diapositivas del material que viene en el Desarrollador Cinco Estrellas (que la verdad está excelente), pero le agregué algunas otras ilustraciones que a mi me ayudaron mucho.  Eventualmente, si Diosito me da licencia lo convertiré en uno o dos artículos para este blog porque el rollo que mareador que yo tiro no sale en los archivos jejeje.

 

Como la gente se quedó "picada", votaron por un tema "intermedio".  Así que en la siguiente reunión les platiqué acerca de controles de usuario (user controls), que son controles compuestos a partir de otros controles, de Master Pages, que es una manera de aplicar elementos comunes a todo tu sitio y también sobre todo el armazón de seguridad que agregaron con ASP.NET 2.0.  Escogí esos temas porque creo que son de los más útiles y prácticos en la chamba del día a día.  En el taller vimos con detalle y calma cómo instalar la base de datos de membresía, cómo aplicar seguridad a diferentes áreas del sitio y cómo utilizar todos los controles de membresía y navegación que vienen con Visual Studio.

En fin, me divertí bastante.  Si no pudiste asistir, aún puedes descargar las presentaciones y las soluciones de los talleres desde área de descargas del sitio de la Comunidad.

Enjoy.

Lo que todo desarrollador debería saber sobre serialización a XML en .NET (y cómo afecta a los Web Services)

En la plática y taller de este mes, alguien hizo una pregunta muy común: "Quiero construir un Web Service en .NET que haga X, ¿por dónde comienzo?"

Así que le prometí escribir un artículo al respecto. Sin embargo, en cuanto comencé, me di cuenta que hay un concepto más básico que debe ser amaestrado para en verdad entender lo que está sucediendo: Serialización. Específicamente serialización a XML.

¿Qué es serialización y cómo se utiliza?

Serialización, no es más que una palabra dominguera que significa transformar una instancia de una clase a una serie de bytes con un formato determinado. En este caso estoy hablando de agarrar una instancia de una clase y transformarla a un documento XML.

En la plática, algunas personas se sorprendieron cuando les dije que prácticamente cualquier clase podía ser serializada casi automáticamente, de una manera relativamente sencilla. Solo hay un "pero" del que debes estar consciente: únicamente los miembros y propiedades públicas pueden ser serializados. En otras palabras, no convierte métodos, indexadores, campos privados o protegidos o propiedades solo-lectura (excepto colecciones solo-lectura).

Toma como ejemplo una clase sencilla como la siguiente:

 

using System;
 
public class MiClase
{
    protected string _campo1 = "campo1 es protegido";
    public string _campo2 = "campo2 es publico";
    private string _campo3 = "campo3 es privado";
    private int _propiedad1 = 1000;
    private int _propiedad2 = 9999;
    public int _propiedad3 = 7777;
 
    public int UnaPropiedad
    {
        get { return _propiedad1; }
        set { _propiedad1 = value; }
    }
 
    protected int OtraPropiedad
    {
        get { return _propiedad2; }
        set { _propiedad2 = value; }
    }
 
    public int PropiedadPublica
    {
        get { return _propiedad3; }
    }
 
    // Se requiere un constructor publico default para que 
    // funcione la serializacion
    public MiClase() { }
}

Podemos serializarla (transformarla a XML pues) con un código como este:

 

using System;
using System.IO;
using System.Xml.Serialization;
 
class Program
{
    static void Main(string[] args)
    {
        MiClase m = new MiClase();
 
        XmlSerializer serializador = new XmlSerializer(typeof(MiClase));
        StringWriter escritor = new StringWriter();
 
        // se puede serializar a casi cualquier tipo de Stream o Writer
        // p.ej. System.IO.StreamWriter, System.Xml.XmlWriter, etc.
        serializador.Serialize(escritor, m);
        Console.WriteLine(escritor.ToString());
    }
}

Si corremos el programa, el resultado será este:

O, visto de una manera más amigable (utilizando el visualizador de XML de Visual Studio):

Nota que únicamente el _campo2, _propiedad3 y UnaPropiadad fueron serializados, ya que eran los únicos con un nivel de acceso public. PropiedadPublica, a pesar de ser public, era solo-lectura y por lo tanto no fue serializada. También nota que automáticamente construyó el documento utilizando el nombre de la clase (MiClase) como el elemento raíz y tomó el nombre de cada campo para los elementos hijos (_campo2, _propiedad3, UnaPropiedad).

La magia la hace la clase XmlSerializer, la cual tiene 3 métodos interesantes:

  • Serialize() convierte una instancia de una clase a XML.
  • Deserialize() hace lo contrario, toma un documento XML y lo transforma en una instancia de una clase.
  • CanDeserialize() regresa un booleano para probar si el XML en realidad se puede des-serializar.

Ahora, puedes controlar varios aspectos de la serialización, aplicando atributos:

  • [Serializable] no solo indica que la clase puede ser serializada, sino que también revisa que todos los tipos contenidos dentro de la clase (otra clase, por ejemplo) también sean serializables. Si no lo son, entonces arroja un SerializationException.
  • [NonSerialized] y [XmlIgnore] indican que no queremos que se serialize el campo o propiedad. El primero afecta al SoapFormatter (el utilizado por WebServices), y el segundo afecta a XmlSerializer.
  • [XmlRoot], que su vez tiene parámetros como Namespace y ElementName para especificar el nombre y espacio de nombres del elemento raíz. Este se aplica solo a la clase.
  • [XmlElement], es parecido al anterior, pero se aplica a los campos o propiedades. Puedes especificar el tipo de dato de XML Schema que debe aplicar y si es o no nulleable mediante los parámetros DataType y IsNullable. También puedes especificar el espacio de nombres y el nombre del elemento como en XmlRoot.
  • [XmlAttribute] indica que quieres serializar el valor como un atributo, en lugar de un elemento.

Para ver el impacto, vamos a aplicar estos conceptos al ejemplo anterior:

 

using System;
using System.Xml.Serialization;
 
[Serializable()]  //indica explicitamente que esta clase es serializable
[XmlRoot(Namespace = "http://comunidadnetjuarez.org/2007/04",
    ElementName = "MiClaseSerializada")] // indica el namespace y nombre 
public class MiClase                     // del elemento raiz
{
    protected string _campo1 = "campo1 es protegido";
 
    [XmlElement(ElementName = "SegundoCampo",
        DataType = "string",
        IsNullable = false)]  // indica el nombre y tipo del elemento
    public string _campo2 = "campo2 es publico";
 
    private string _campo3 = "campo3 es privado";
    private int _propiedad1 = 1000;
    private int _propiedad2 = 9999;
 
    [NonSerialized()] // no serializar cuando se use SoapFormatter
    [XmlIgnore()]     // no serializar cuando se use XmlSerializer
    public int _propiedad3 = 7777;
 
    [XmlAttribute(AttributeName = "valor")] // indica que deseamos que
    public int UnaPropiedad                 // se serialize como atributo XML
    {                                       // y no como elemento XML
        get { return _propiedad1; }
        set { _propiedad1 = value; }
    }
 
    protected int OtraPropiedad
    {
        get { return _propiedad2; }
        set { _propiedad2 = value; }
    }
 
    public int PropiedadPublica
    {
        get { return _propiedad3; }
    }
 
    // Se requiere un constructor publico default para que 
    // funcione la serializacion
    public MiClase() { }
}

El resultado sería el siguiente:

La cosa comienza a ponerse más interesante mientras más complejidad le agreguemos a la clase. Por ejemplo, ¿qué pasa si mi clase tiene una colección o lista de cosas? Bueno, pues resulta que únicamente las colecciones que implementen ICollection o IEnumerable podrán serializarse (p.ej. un ArrayList). Si implementan IDictionary (como un HashTable) estas no son serializadas (de hecho levanta una excepción).

Agreguemos el siguiente miembro a la clase:

 

    public string[] arreglo = { "arreglo uno", 
                                "arreglo dos", 
                                "arreglo tres" };

El resultado sería el siguiente:

Y finalmente veamos qué pasa si aplicamos [XmlElement]:

 

    [XmlElement(ElementName = "UnArreglo")]
    public string[] arreglo = { "arreglo uno", 
                                "arreglo dos", 
                                "arreglo tres" };

Creación automática de una clase serializable

Como puedes ver, tienes bastante control sobre la conversión de una clase .NET a un documento XML. Sin embargo, mientras más complejo sea el documento XML que quieras producir, más complejo y cuidadoso tendrás que ser con la aplicación de los atributos para obtener el resultado que quieres.

Muchas veces quizá es más sencillo comenzar con un documento XML de ejemplo o mejor aún con el contrato XML Schema que debe cumplir el documento XML que quieres producir cuando se serialice tu clase. Una vez que tienes esto, puedes utilizar la utilería xsd.exe para automáticamente generar los tipos adecuados en C# (o VB.NET).

Veamos un ejemplo. Supongamos que tienes un archivo llamado SchemaEjemplo.xsd con el siguiente contenido:

 

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="personas">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="persona"
                    type="personaType"
                    minOccurs="1"
                    maxOccurs="unbounded">
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:complexType name="personaType">
    <xs:sequence>
      <xs:element name="titulo"
                  type="tituloType"
                  minOccurs="0">
      <xs:element name="nombre"
                  type="nombreType">
      <xs:element name="edad"
                  type="xs:integer">
      <xs:element name="direccion"
                  type="direccionType">
      <xs:element name="genero"
                  type="generoType">
    </xs:sequence>
    <xs:attribute name="colorOjos"
                  type="xs:string">
  </xs:complexType>
  <xs:simpleType name="tituloType">
    <xs:restriction base="xs:string">
      <xs:enumeration value="Sr.">
      <xs:enumeration value="Sra.">
      <xs:enumeration value="Señorita">
    </xs:restriction>
  </xs:simpleType>
  <xs:complexType name="direccionType">
    <xs:sequence>
      <xs:element name="calle"
                  type="xs:string">
      <xs:element name="numero"
                  type="xs:string">
      <xs:element name="numeroDepartamento"
                  type="xs:string"
                  minOccurs="0">
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="nombreType">
    <xs:all>
      <xs:element name="primerNombre"
                  type="xs:string">
      <xs:element name="segundoNombre"
                  type="xs:string"
                  minOccurs="0">
      <xs:element name="apellidos"
                  type="xs:string">
    </xs:all>
  </xs:complexType>
  <xs:complexType name="generoType">
    <xs:choice>
      <xs:element name="hombre"
                  type="xs:boolean">
      <xs:element name="mujer"
                  type="xs:boolean">
    </xs:choice>
  </xs:complexType>
</xs:schema>

La siguiente línea de comando de Visual Studio 2005 genera las clases necesarias:

 

xsd.exe SchemaEjemplo.xsd /classes /language:CS



El archivo resultante (SchemaEjemplo.cs) se ve algo así como este (solo muestro una parte dado que es muy largo el fragmento de código). Nota que define una clase por cada elemento o complexType definido en el Schema:

 

//------------------------------------------------------------------------------
// 
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.42
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// 
//------------------------------------------------------------------------------
 
using System.Xml.Serialization;
 
// 
// This source code was auto-generated by xsd, Version=2.0.50727.42.
// 
 
 
/// 
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.42")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class personas
{
 
    private personaType[] personaField;
 
    /// 
    [System.Xml.Serialization.XmlElementAttribute("persona", 
        Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public personaType[] persona
    {
        get { return this.personaField; }
        set { this.personaField = value; }
    }
}
 
/// 
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.42")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
public partial class personaType
{
 
    private tituloType tituloField;
    private bool tituloFieldSpecified;
    private nombreType nombreField;
    private string edadField;
    private direccionType direccionField;
    private generoType generoField;
    private string colorOjosField;
 
    /// 
    [System.Xml.Serialization.XmlElementAttribute(
        Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public tituloType titulo
    {
        get { return this.tituloField; }
        set { this.tituloField = value; }
    }
 
    /// 
    [System.Xml.Serialization.XmlIgnoreAttribute()]
    public bool tituloSpecified
    {
        get { return this.tituloFieldSpecified; }
        set { this.tituloFieldSpecified = value; }
    }
 
    /// 
    [System.Xml.Serialization.XmlElementAttribute(
        Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public nombreType nombre
    {
        get { return this.nombreField; }
        set { this.nombreField = value; }
    }
 
    /// 
    [System.Xml.Serialization.XmlElementAttribute(
        Form = System.Xml.Schema.XmlSchemaForm.Unqualified, DataType = "integer")]
    public string edad
    {
        get { return this.edadField; }
        set { this.edadField = value; }
    }
 
    /// 
    [System.Xml.Serialization.XmlElementAttribute(
        Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public direccionType direccion
    {
        get { return this.direccionField; }
        set { this.direccionField = value; }
    }
 
    /// 
    [System.Xml.Serialization.XmlElementAttribute(
        Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public generoType genero
    {
        get { return this.generoField; }
        set { this.generoField = value; }
    }
 
    /// 
    [System.Xml.Serialization.XmlAttributeAttribute()]
    public string colorOjos
    {
        get { return this.colorOjosField; }
        set { this.colorOjosField = value; }
    }
}
 
// ... así continúa ...

Bueno, ¿pero qué carajos tiene que ver con Web Services?

Pues sin ahondar mucho en detalles (eso lo haré en otro artículo), resulta que uno de los primeros pasos al crear un Web Service es precisamente definir el formato de los mensajes que se intercambiarán entre el cliente y el servicio (despues de todo, Web Services, visto desde un punto de vista sobre-simplificado no son más que mensajes XML sobre HTTP). ASP.NET automáticamente hace este tipo de serialización hacia XML.

También es importante comprender la serialización, porque la siguiente generación de herramientas (Windows Communication Foundation) utiliza otros mecanismos para transformar tipos de .NET a XML y hay algunas diferencias. Así que solo hemos comenzado con el tema.

Cómo validar tu XML contra un Schema usando .NET

En el taller de este mes estábamos utilizando una de mis herramientas preferidas para manejar XML: XMLPad de WMHelp.com, o como yo le llamo, el "Visual Studio para XML".

Sin embargo, parecía que de repente se ponía chiple al intentar validar un documento XML contra un XML Schema.  Afortunadamente yo traía por ahí (bueno, por ahí no, porque luego me alburean) una pequeña herramienta que desarrollé hace tiempo, XML Validator.

Este es un programita super sencillo que básicamente toma un documento XML (que pudiera estar en un archivo), e intenta validarlo contra un XML Schema.  El programa permite que edites el documento en la primera caja de texto para que puedas sacar las pulgas de tu documento.

Ejemplo de uso de XmlValidator

XML Validator y su código fuente están disponibles para descargarse en:
http://comunidadnetjuarez.org/files/folders/ejemplos/entry523.aspx

La mayoría del código, aunque no lo creas, es para manejar los eventos de los botones, etcétera.  En realidad el pedazo interesante, el que en realidad hace la validación es el siguiente:

try
{
    Cursor.Current = Cursors.WaitCursor;
 
    XmlSchemaSet schemaSet = new XmlSchemaSet();
    schemaSet.Add(String.Empty,
        XmlReader.Create(new StreamReader(xmlSchemaFileTextBox.Text)));
 
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.Schemas = schemaSet;
    settings.ValidationType = ValidationType.Schema;
 
    XmlReader reader = XmlReader.Create(
        new StringReader(inputXmlTextBox.Text), settings);
 
    while (reader.Read()) { }
 
    resultTextBox.Text = "Document is valid :o)" +
    DateTime.Now.ToLongDateString();
}
catch (XmlSchemaException schemaEx)
{
   resultTextBox.Text = "Document is NOT VALID:" +
      Environment.NewLine + schemaEx.Message;
}
catch (Exception ex)
{
   resultTextBox.Text = ex.ToString();
}
finally
{
   Cursor.Current = Cursors.Default;
}

Como puedes ver, lo único que tienes que hacer es crear un XmlValidatingTextReader e iterar por todo el documento.  En la versión 2.0 del .NET Framework, esto se puede lograr utilizando el método Create() de la clase abstracta XmlReader y especificando un objeto XmlReaderSettings con la configuración para el reader. El objeto de settings debe tener especificada la propiedad ValidationType y un XmlSchemaSet, que es la colección de XML Schemas contra las que se validará el documento.


PD. Si no sabes ni de qué $#^%&! estoy hablando con esto de XML Schema, validaciones y todo eso, entonces quizá quieras echarle un ojo al material de la reunión del mes de abril 2007 de la Comunidad .NET de Cd. Juárez.

Editor de code snippets para C# y VS2005

Hace rato había platicado de la chidez de los code snippets en Visual Studio 2005.

Hoy se me ofreció hacer uno rápidamente. Y aunque es relativamente sencillo hacerlo a pata, en realidad no tenía tiempo de echarme la documentación para hacerlo. Necesitaba un "editor" para sinppets, pero el único que hasta hoy conocía es para Visual Basic 8.

Afortunadamente, este vato se creó un programita llamado Snippy. Puedes descargarlo de aquí.

El programita te genera el archivo .snippet; lo grabas en un directorio debajo de My Documents/Visual Studio 2005/Code Snippets/Visual C# y listo. Ni siquiera tienes que reiniciar Visual Studio.

Cómo leer archivos planos con ADO.NET

(Antes que me la rieguen, sí, ya sé que son "archivos de texto sencillo" pero este blog está en pocho, ¿no? Digo, no hay archivos de computadora planos, o cúbicos o esféricos, que yo sepa, pero así les dicen muchos desarrolladores)

Aunque hoy en día ya todo mundo debería de estar utilizando (según yo) XML para el intercambio de información, sigue siendo muy comun que para mandar información de un sistema a otro se haga a través de archivos "planos" de texto.

Si alguna vez has lideado con sistemas legacy o en plataformas chiples (mainframe, or SAP anyone?) estoy seguro que sabes de lo que hablo. Otro caso común es cuando requieres que tu usuario genere algún archivo para cargar esos datos en tu aplicación. Harta cantidad de gerentoides y chalanes no saben ni (bleep) de sistemas o de computadoras, pero eso sí, son masters sensai del Excel, así que es fácil decirles que le den un "Save As... CSV" a un archivo para subir sus datos.

Ahora, ¿cómo le harías para leer esos archivos con .NET? Si eres entusiasta probablemente luego luego te las ingeniarías para usar un FileStream y parsear el contenido línea por línea, dar 3 maromas... qué se yo. Sin embargo, existe un truco sencillo que puede ahorrarte broncas: utilizar el OLEDB Provider de ADO.NET para hacerlo. Esta técnica funciona bastante bien para leer archivos CSV o archivos de texto con columnas en posiciones fijas (si son secuenciales, ya valiste cake).

Leyendo un archivo CSV

Por ejemplo, imagina que eres achichincle del Jason (el de las películas de terror) y te manda a hacer sus compras, de lo contrario terminarás con tu cabeza y cuerpo en diferentes sectores de la ciudad. El vato hace su "chopin list" en Excel y lo guarda en el siguiente archivo llamado jason.csv:

Producto,Cantidad,Precio
Sierra eléctrica,1,250
Máscara de hockey,1,15.50
Machete,5,2.70
Detergente para ropa (con quita-manchas),1,10
Delantal,2,7.25
Afilador,3,5

Entonces, como buen dotnetero, podrías leerlo con una rutina como esta:

// asumiendo que tenemos
// using System.Data.OleDb;
// using System.Data;
 
// en este connection string:
//     HDR=Yes       : indica que el primer registro contiene los encabezados 
//                     (nombres) de las columnas, no datos.
//     FMT=Delimited : indica que el los campos están delimitados por un caracter
//                     (coma por default).
string connectionString =
    @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\DirectorioDeArchivosCSV;" +
    "Extended Properties='text;HDR=Yes;FMT=Delimited'";
 
DataTable dt = new DataTable("miTabla");
using (OleDbConnection conn = new OleDbConnection(connectionString))
using (OleDbDataAdapter da = new OleDbDataAdapter("SELECT * FROM jason.csv", conn))
{
    da.Fill(dt);
}
 
// hacer algo con los datos en el DataTable

El código es bastante sencillo, es el patrón estándar para usar un DataAdapter. Lo único de especial que tiene es que utiliza el OLEDB Data Provider, y que en el connection string le especificamos el directorio donde se encuentra el archivo, así como el formato que tiene. Nota que emites un SELECT de SQL común y corriente, por lo cual podrías agregar una cláusula WHERE si así lo quisieras.

En fin, para comprobar que en realidad funcionara el código, puse un breakpoint e invoqué el DataSet Visualizer desde Visual Studio. El resultado:

Pero, ¿qué tan inteligente es el OLEDB Provider? ¿Adivinó correctamente el tipo de mis datos?


Leyendo un archivo de texto con posiciones fijas

Ahora, asume que el méndigo Jason te la puso más difícil y en lugar de darte un archivo CSV, te da un archivo de texto sencillo como este (jason.txt):


El código sería muy similar al anterior:

// asumiendo que tenemos
// using System.Data.OleDb;
// using System.Data;
 
// en este connection string:
//     HDR=Yes   : indica que el primer registro contiene los encabezados 
//                 (nombres) de las columnas, no datos.
//     FMT=Fixed : indica que el los campos están en posiciones fijas y el tamaño
//                 de cada campo se especifican con un archivo SCHEMA.INI
//                 en el mismo directorio donde está el archivo a leer.
string connectionString =
    @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\DirectorioDeArchivosTXT;" +
    "Extended Properties='text;HDR=Yes;FMT=Fixed'";
 
DataTable dt = new DataTable("miTabla");
using (OleDbConnection conn = new OleDbConnection(connectionString))
using (OleDbDataAdapter da = new OleDbDataAdapter("SELECT * FROM jason.txt", conn))
{
    da.Fill(dt);
}
 
// hacer algo con los datos en el DataTable

De hecho, lo único que cambió fue el parámetro FMT en el connectionString, el nombre del archivo y el directorio donde localizarlo. Sin embargo, para que esto funcione con archivos de posiciones fijas es necesario un paso adicional: especificar el tamaño (y tipo) de las columnas en el archivo.

Esto se hace mediante un archivo schema.ini que debe estar en el mismo directorio que el archivo que vas a leer. Consulta esta página para saber todas las opciones disponibles. En nuestro caso un archivo como el siguiente sería suficiente:

[jason.txt]
Format=FixedLength
Col1=Producto Char Width 40
Col2=Cantidad Long Width 10
Col3=Precio Double Width 10

Factorizando código

Si generalizamos el código un poco, podemos extraer una función sencilla que pueda ser reutilizada en varios de nuestros programas. Esa rutina podría ser como esta, en donde le pasas como parámetros el tipo y archivo a leer y te regresa un DataTable poblado ya con los datos:

// asumiendo que tenemos 
// using System.Data.OleDb;
// using System.Data;
// using System.IO;
 
public enum TipoDeArchivoPlano { Delimited, Fixed }
 
public static DataTable LeerArchivoPlano(
    FileInfo archivo, bool tieneEncabezado, TipoDeArchivoPlano tipoDeArchivo )
{
    if (!archivo.Exists)
        throw new FileNotFoundException(
            "No se encontró el archivo especificado");
 
    string conEncabezado = tieneEncabezado ? "YES" : "NO";
 
    string connectionString = String.Format(
        @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};" +
        "Extended Properties='text;HDR={1};FMT={2}'",
        archivo.DirectoryName, conEncabezado, tipoDeArchivo.ToString());
 
    DataTable dt = new DataTable("miTabla");
    using (OleDbConnection conn = new OleDbConnection(connectionString))
    using (OleDbDataAdapter da =
        new OleDbDataAdapter("SELECT * FROM " + archivo.Name, conn))
    {
        da.Fill(dt);
    }
 
    return dt;
}

De manera que pueda ser llamado así:

DataTable dt = LeerArchivoPlano(
    new FileInfo(@"C:\DirectorioDeArchivosCSV\jason.csv"),
    true, TipoDeArchivoPlano.Delimited);

En fin, la idea es esa. Enjoy.

Y tú, ¿dominas tus aplicaciones?

Hace unos días, Eber Irigoyen escribió un post con un ejemplo muy padre de cómo invocar un método privado de una clase desde otra y otro de cómo instanciar una clase que tenga un constructor privado utilizando reflection (reflexión, pues).

En realidad es muy sencillo hacerlo, así que si eres tan neurótico como yo, lo primero que se te viene a la mente es "¡Ah jijos! Pero debe haber alguna manera de evitar eso, ¿no? Digo, ¿apoco cualquiera puede agarrar mi código y 'hackearlo' así tan pelada?" La respuesta es "sí hay manera de evitarlo, pero no es tan sencillo", debes tener una buena idea de cómo el CLR carga y ejecuta Assemblies, cómo funcionan los mecanismos de Code Access Security (CAS) que trae el .NET Framework, cómo se relacionan con los Application Domains, Reflection, etcétera.

Sería bastante material para un humilde post, así que para que no te me aburras voy a seguir la filosofía de "¿Cómo te comes un elefante?" y espero poder escribir varios artículos al respecto en los próximos días (bueno, semanas)--sí ya sé que todavía tengo pendientes terminar los de ADO.NET, no creas que se me ha olvidado.

Parte de este artículo está adaptado y (MUY) sintetizado del capítulo 8 del libro para la certificación MCTS, de Tony Nurthup [et.al.], que fue de donde yo lo aprendí. No me estoy pirateando su texto, pero me gustó mucho la manera en que explicó la "teoría", así que me basé en el, lo resumí y le agregué de lo mío. Sin embargo, lo menciono porque quizá te interese echarle un ojo a ese libro.

Assemblies y Application Domains

Cuando compilas código de C# o VB.NET--o cualquier otro lenguaje que cumpla con el CLS--el resultado es un Assembly (¿ensamblaje?), o como decimos los mortales: un .EXE o .DLL. Sin embargo, ese ejecutable o DLL no está en código máquina, sino en código intermedio o MSIL (se pronuncia "misil").

Cuando quieres ejecutar o invocar el código en ese assembly, el Common Language Runtime (CLR) hace varias cosas, entre ellas:

  • Carga el assembly y, de ser necesario, crea un Application Domain (dominio de aplicación) para ejecutarlo.
  • Intenta "adivinar" los permisos con los que debe correr ese assembly (el nivel de seguridad) mediante evidencia y de acuerdo a la póliza de seguridad de la máquina.
  • Hace una compilación Justo A Tiempo (JIT) del assembly a código nativo de acuerdo a la plataforma donde esté corriendo (x86, x64, ARM, etc.)
  • Invoca el Entry Point (punto de entrada) del assembly para iniciar la ejecución.

Bueno, ¿pero qué carajos es un Application Domain? Es el análogo a un proceso del sistema operativo, pero para el .NET Runtime. Es decir, es un contenedor lógico que te permite correr varios assemblies bajo un mismo proceso (de S.O.), pero conservando muchos de los beneficios (p.ej., separación de memoria y de acceso a recursos).

Normalmente, el .NET Framework se encarga de crear el AppDomain default para tu assembly usando uno de los anfitriones que trae de cajón: ASP.NET Worker Process, Internet Explorer o Windows. Sin embargo, lo que muchos no saben es que puedes crear AppDomains a partir de tus propios assemblies que correrán contenidos dentro del AppDomain default.

Un buen ejemplo de esto son las aplicaciones web. Si tienes una aplicación web de ASP.NET y es accedida por N personas, el aspnet_wp.exe creará N instancias del assembly de tu aplicación, cada una dentro de un AppDomain separado.

Tú puedes hacer lo mismo en tus aplicaciones de una manera bastante sencilla. ¿Para qué? Se me ocurren 3 razones muy buenas:

  • Seguridad: Puedes especificar evidencia (que determina el nivel de seguridad) al crear un AppDomain o al cargar un assembly en el AppDomain. Si estás usando, por ejemplo, un componente de terceros, es buena práctica no darle permisos FullTrust nomás porque sí, y limitarlo ya sea a nivel AppDomain o a nivel Assembly.
  • Eficiencia: Si el assembly fue cargado en el AppDomain default, no podrás liberar su memoria hasta que termine el proceso anfitrión. Pero si lo cargas en un AppDomain separado, sí podrás hacerlo. Esto se puede ofrecer, por ejemplo, si tienes un servicio de Windows que hace uso de assemblies externos pero por un periodo corto de tiempo; te puede convenir liberar esa memoria cuando el servicio no esté chambeando.
  • Confiabilidad: Puedes aislar código inestable que pudiera tronar tu aplicación (Crystal Reports anyone?).
Usando AppDomain

Como podrás haber adivinado, dado que los AppDomains son importantes, el .NET Framework trae una clase especial para manejarlos: System.AppDomain

Estas son algunas de sus propiedades más interesantes (en mi humilde opinión):

  • Evidence: Expone la evidencia asociada al AppDomain y que será verificada contra la póliza de seguridad de la máquina. Más sobre esto en el siguiente post.
  • SetupInformation: Expone la configuración que se utilizó al crear el AppDomain.
  • CurrentDomain: Devuelve una referencia al AppDomain del hilo actual de ejecución (current Thread).
  • FriendlyName: Como su nombre lo dice, expone un nombre amigable que le puede asignar al crear el AppDomain. Si no lo especifican utiliza uno por omisión que quizá hayan visto en los mensajes del debugger(miassembly.vshost.exe)

Y de sus métodos, estos son los que me parecen más relevantes:

  • CreateDomain: Sirve para, duh, crear un AppDomain. AppDomain usa el patrón Factory, así que en lugar de utilizar su constructor, utilizas este método.
  • ExecuteAssembly y ExecuteAssemblyByName: Ejecutan un assembly dentro del AppDomain (otro duh). La única diferencia entre estos dos es que uno acepta la ruta al archivo del assembly y el otro solo el nombre (lo cual requiere que tengas una referencia a él en tu proyecto).
  • Load: Carga un assembly dentro del AppDomain.
  • Unload: Descarga el AppDomain completito. OJO: No puedes descargar assemblies o tipos individualmente.
  • CreateInstance: Crea una instancia de algún tipo (clase) definida en un assembly.
  • GetAssemblies: Expone los assemblies que han sido cargados en el contexto de ejecución actual.

Ahora sí, después del rollote, vamos aponerlo en práctica.

Hice una solución en en Visual Studio con dos proyectos de tipo Windows Console Application, uno llamado Anfitrion y otro llamado OtraAplicacion. Anfitrion tiene una referencia a OtraAplicacion, lo cual nos permite ejecutarlo por nombre. OtraAplicacion solo escribe un mensaje a la consola de "Hola, soy la Otra Aplicación".

// Crear el nuevo dominio. AppDomain usa el patron factory, así que se necesita
// utilizar CreateDomain en lugar de su constructor.
AppDomain d = AppDomain.CreateDomain("OtroDominio");
 
// Mostremos el nombre del dominio anfitrión y el del dominio que fue creado
Console.WriteLine("Dominio anfitrion: {0}", AppDomain.CurrentDomain.FriendlyName);
Console.WriteLine("Dominio creado: {0}", d.FriendlyName);
 
// Cargar y ejecutar un assembly, sin especificar evidencia, en el nuevo dominio.
d.ExecuteAssemblyByName("OtraAplicacion");
 
// Descargar el dominio
AppDomain.Unload(d);



Cómo restringir un AppDomain

Ahora sí, veamos qué tiene que ver todo esto con seguridad de código. Para correr un dominio o un assembly con privilegios limitados, necesitamos especificar evidencia en el momento de crear el AppDomain o de invocar los métodos Load, ExecuteAssembly o ExecuteAssemblyByName.

Para no desviar mucho del tema, solo te diré por ahora que evidencia se refiere a "pistas" que le puedes dar al .NET Framework acerca de la identidad del assembly y su procedencia. Basado en estas pistas, el runtime determinará a qué grupo de código pertenece y por lo tanto con qué permisos correrá. Si este párrafo solo te hizo pelotas, no te preocupes, intentaré explicarlo más a detalle en otro post.

Ahora, cambiemos OtraAplicacion para que haga algo más travieso que simplemente escribir un mensaje:

using System;
using System.IO;
 
namespace OtraAplicacion
{
    class Program
    {
        static void Main(string[] args)
        {
            //Console.WriteLine("Hola, soy la OtraAplicacion");
            File.Delete(@"C:\boot.ini");
        }
    }
}

Podemos protegernos de él si cambiamos el código que lo invoca de la siguiente manera:

// asumiendo que se tienes
// using System.Security.Policy
 
// crear un objeto de evidencia para limitar permisos.
object[] pistas = { new Zone(System.Security.SecurityZone.Internet) };
Evidence evidencia = new Evidence(pistas, null);
 
// crear dominio con permisos restringidos
AppDomain d = AppDomain.CreateDomain("OtroDominio", evidencia);
 
try
{
    // también se puede especificar la evidencia al 
    // cargar o ejectuar el assembly.  Por ejemplo
    // d.ExecuteAssemblyByName("OtraAplicacion", evidencia, null);
    d.ExecuteAssemblyByName("OtraAplicacion");
}
catch (System.Security.SecurityException ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    AppDomain.Unload(d);
}



Aunque este error no es el que esperaba (UIPermission se debe a que una aplicación corrida desde la zona de Internet no puede ejecutar aplicaciones con interfaz, ni siquiera de consola), sirve para demostrar que nuestro assembly está corriendo ahora en un AppDomain restringido.

Habrás notado que para crear la evidencia tuve que utilizar un arreglo de object. Esto es porque puedes especificar varios tipos de objetos como "pistas" y estas pistas pueden ser cualquier cosa, strings, int's o tipos. Los más sencillos de usar son los tipos que vienen en el espacio de nombres System.Security.Policy, como Zone, que fué el que utilicé en el ejemplo.

Otra cosa importante que quizá notaste es que esta misma técnica podría utilizarse para correr una aplicación con permisos MENOS restrictivos ya que en realidad estamos proveyendo evidencia falsa sobre nuestro assembly. Después veremos cómo impedir esto.

Por ahora, espero haberte dado al menos una idea de cómo cargar y administrar tu código utilizando dominios de aplicación y su relación con la seguridad de tu código. En la próxima nos clavamos ya un poco más con lo que son los permisos, permission sets, códigos de grupo, etcétera.

Enjoy.

Cursos gratis de .NET Framework 3.0

No sé si te enteraste, pero de aquí al lanzamiento al público de Windows Vista (en enero), Microsoft está ofreciendo gratis 3 cursillos sobre las tecnologías principales del .NET Framework 3.0 (en inglés). Cada uno de ellos dura como 2 horas. Hay uno para Windows Presentation Foundation, otro para Windows Workflow Foundation y otro para Windows Communication Foundation.

Así que si después de la plática de ayer en el Developer Community Day te quedaste con las ganas de aprender un poco más, pues inviértele unas cuantas horas, que al fin y no pagas nada ;)

PD. ¡Gracias a Abe por su reseña del evento! (casi me chiveo con sus palabras jejeje)

Where Workflow Foundation really shines

Rocky tiene unas ideas bastante interesantes de cómo encaja Workflow Foundation en la arquitectura de un sistema. Vale la pena echarle el ojo.

De su artículo:
Today we all write these non-interactive processes in code. Maybe with a set of objects working in concert, but more often as a linear or procedural set of code. If a change is needed to the process, we have to alter the code itself, possibly introducing unintended side-effects, because there's little isolation between steps.

Personally I think this is where WF fits in. It is really good at helping you create and manage non-interactive processes.

Yes, you have to think about those non-interactive processes in a different way to use WF. But it is probably worth it, because in the end you'll have divided each process into a set of discrete, autonomous steps. WF itself will invoke each step in order, and you have the pleasure (seriously!) of creating each step as an independent unit of code.

From an OO design perspective it is almost perfect, because each step is a use case, that can be designed and implemented in isolation - which is a rare and exciting thing!

UTC y zonas horarias con Web Services

Este post es en respuesta a este caso de Edgar. Desafortunadamente no lo pude poner como comentario en su blog porque Blogger beta me dió este hermoso mensajito al intentarlo:

Así que esta es mi respuesta:

No estoy seguro si esto es más bien un problema de lógica/programación, más que del framework en sí.

Mi primera recomendación cuando tienes apps que varían significativamente en la zona horaria es que tanto el servicio como el cliente hablen en UTC (Coordinated Universal Time). De esa forma te olvidas de plano de conversiones de zona horaria.

El .NET Framework (desde v1.0) trae un soporte bastante robusto para manejar esto en el tipo DateTime--ve los métodos ToUniversalTime() y ToLocalTime().

Ahora si por "problema que ya se resolvió en .NET 2.0" te refieres a los cambios hicieron a la forma en que se serializan los tipos DateTime a XML, entonces échale un ojo a este artículo, que contiene algunos workarounds para tu situación.

En resumen: si usas tiempo local en v1.0 y 1.1 siempre se serializan DateTime con todo y timezone, incluso cuando solo usas la fecha. Y aunque sí hay workarounds--medio feos--para ello, te ahorras muchas más broncas si usas UTC. Esto podría no ser posible de acuerdo a impacto, restricciones de tiempo/presupuesto o de qué tanto control tengas sobre el web service.

En fin, espero te de un norte.
Posted from El otro lado del Kamikaze | 0 Comments
Filed under: ,

Usando using

El día de hoy aprendí un tip bastante bueno de como usar using gracias a Eber Irigoyen.

Todo comenzó porque él hizo un comentario en el último artículo recalcando la importancia de usar using para TODOS los objetos relacionados con la base de datos (conexiones, comandos, transacciones, etcétera). A mí se me hacía un poco de overkill, pero ahora comprendo que en realidad sí es un buen consejo.

Por si no sabes ni de qué hablo, esta instrucción define un alcance (scope) explícito para un objeto, fuera del cual el objeto es automáticamente "dispuesto" (manda llamar su método Close() o Dispose()), incluso si se levanta una excepción dentro de ese bloque.

Aunque estrictamente hablando no es necesario, ya que hay un scope implícito cada vez que abres y cierras llavecitas en C#, para recursos limitados como conexiones de red, el scope explícito que crea el using ayuda a que el garbage collector reclame esos recursos más fácil y rápidamente.

Si no lo tuvieras, tendrías que hacer todo a punta de bloques try/catch/finally. Eso era algo que no me gustaba de VB 7--bueno VB .NET o "VB 2003", como le quieran llamar. Afortunadamente VB 8 (2005) ya lo trae.

En el artículo original no la quise complicar mucho con esto, pero así quedaría el "patrón de uso de un DataReader" usando try/catch/finally:

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
string miQuery = "SELECT * FROM Peliculas;";
 
SqlDataReader speedy = null;
SqlConnection miConexion = null;
SqlCommand miCommando = null;
try
{
    // en cualquiera de las instrucciones de este bloque se podría
    // levantar una excepción
 
    miConexion = new SqlConnection(miStringDeConexion);
    miCommando = new SqlCommand(miQuery, miConexion);
    miConexion.Open();
    speedy = miCommando.ExecuteReader();
    while (speedy.Read())
    {
        // hacer mi desmater 
    }
}
catch (SqlException)
{
    // hacer algo para intentar recuperarme o re-lanzar exception
// si no pienso hacer nada, es mejor omitir el catch
}
catch
{
    // hacer algo para intentar recuperarme o re-lanzar exception
// si no pienso hacer nada, es mejor omitir el catch
}
finally
{
    // haya o no haya ocurrido excepción, siempre debemos asegurarnos
    // de cerrar las conexiones, DataReaders, etc.
    if (!speedy.IsClosed)
        speedy.Close();
    if (miConexion != null)
        miConexion.Close();
    if (miCommando != null)
        miCommando.Dispose();
}

Como puedes ver, es medio engorroso. Por eso el using es tan cool.

Pero, yo tenía mis reservas, porque cuando tienes varios objetos, es fácil--pensaba yo--que acabes con un chorro de usings anidados, lo cual no me gustaba:

// asumiendo que TipoA, TipoB y TipoC son clases que
// implementan IDisposable
using (TipoA a = new TipoA())
{
    using (TipoB b = new TipoB())
    {
        using (TipoC c = new TipoC())
        {
            // hacer mi desmater
        } // using dispone c
    } // using dispone b
} // using dispone a

Ahora, también sabía que using soporta declarar varios objetos del mismo tipo dentro del paréntesis. Algo así como esto:

// De nuevo, asumiendo que TipoA implementa IDisposable
using (TipoA a1 = new TipoA(), a2 = new TipoA(), a3 = new TipoA())
{
    // hacer mi desmater
} // using dispone a1, a2 y a3

Lo cual no es muy útil cuando tienes objetos de distintos tipos, como en el patrón de uso de un DataReader--donde tienes que usar SqlCommand, SqlConnection y SqlDataReader.

Así que por eso se me hizo tan chido cuando Eber me comentó que using soporta una sintaxis más o menos así:

// asumiendo que TipoA, TipoB y TipoC son clases que
// implementan IDisposable
using (TipoA a = new TipoA())
using (TipoB b = new TipoB())
using (TipoC c = new TipoC())
{
   // hacer mi desmater
} // using dispone a, b y c

De esa forma podría re-escribir el ejemplo de esta forma:

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
string miQuery = "SELECT * FROM Peliculas;";
 
using(SqlConnection miConexion = new SqlConnection(miStringDeConexion))
using(SqlCommand miCommando = new SqlCommand(miQuery, miConexion))
{
    miConexion.Open();
    using (SqlDataReader speedy = miCommando.ExecuteReader())
    {
        while (speedy.Read())
        { // hacer mi desmater 
        }
    } // using manda llamar speedy.Close()
} // using manda llamar miConexion.Close() y miComando.Dispose()

Ahora, nomás de payaso modifiqué un poco el código para verificar que en realidad se estuvieran mandando llamar los Dispose(), agregando manejadores para el evento Disposed tanto de la conexión como del comando de la siguiente forma:

public static void UsandoUsing() {
 
    string miStringDeConexion =
        @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
        "Integrated Security=True";
    string miQuery = "SELECT * FROM Peliculas;";
 
    using(SqlConnection miConexion = new SqlConnection(miStringDeConexion))
    using(SqlCommand miCommando = new SqlCommand(miQuery, miConexion))
    {
        miConexion.Disposed += new EventHandler(miConexion_Disposed);
        miCommando.Disposed += new EventHandler(miCommando_Disposed);
 
        miConexion.Open();
        using (SqlDataReader speedy = miCommando.ExecuteReader())
        {
            while (speedy.Read())
            { // hacer mi desmater 
            }
        } // using manda llamar speedy.Close()
    } // using manda llamar miConexion.Close() y miComando.Dispose()
}
 
static void miConexion_Disposed(object sender, EventArgs e)
{
    Console.WriteLine("miConexion liberada");
}
 
static void miCommando_Disposed(object sender, EventArgs e)
{
    Console.WriteLine("miComando liberado");
}

¿El resultado?
Oh yeah.

ADO.NET para novatos

ADO.NET es la tecnología de acceso a datos que viene en el .NET Framework. Y aunque su nombre proviene de su predecesor (ADO, ActiveX Data Objects), no tiene casi nada en común con esa tecnología. De hecho, maneja un paradigma conceptual completamente opuesto.

ADO, RDO, DAO y similares manejaban un modelo “conectado”, es decir, tu ejecutabas un query en la base de datos y declarabas objetos que mantenían un “cursor” o un apuntador al registro que querías leer. ADO te proveía métodos para navegar hacia enfrente o hacia atrás en el grupo de datos que te regresaba tu query para que pudieras leerlos y/o alterarlos. Todo esto se hacía en vivo y en directo sobre la base de datos. Las desventajas de esta tecnología eran algunas, pero la principal es que necesitas mantener una conexión abierta en todo momento para manipular los datos. Así que si tenías, digamos 20 licencias para conectarte a tu base de datos, pues fácilmente podías saturarlas con 20 usuarios que estuvieran utilizando la aplicación con alguna pantalla abierta, aunque no estuvieran realizando ninguna actividad.

Es como cuando tienes un vecino muy platicador y con muy poca vida propia. Llegas tu casa del trabajo y estás metiendo el auto en tu cochera, cuando te lo topas y lo saludas:
—¿Qué tal vecino, qué cuentas?
—Pos fíjate que la suegra del primo de la hermana de la vecina wachumaru la gallina blah blah blah blah blah bla…
(continuando por al menos 30 minutos).

Y tú ya te quieres meter a tu casa porque te estás cayendo de sueño, pero el maldito vecino no se calla y ya mejor no quieres ni hablar porque sabes que si le haces cualquier otra pregunta o haces la más mínima muestra de interés en la conversación vas a estar ahí otra hora y él todavía está hablando de lo gracioso que se ve su bebé recién nacido cuando regurgita y… ¡aaaAAARGHHH!

Ejem… ¿en qué estaba? ¡Ah sí!

Sin embargo, ADO.NET maneja un modelo “desconectado”, más que nada por que el otro no es el más óptimo en cierto tipo de aplicaciones como las de Web o dispositivos móviles. Es decir, la idea aquí es que te conectes a la base de datos (o cualquier otro almacén de datos), hagas lo que tengas que hacer y te desconectes, liberando lo más rápido posible recursos de red, licencias, etcétera. Puedes mantener si gustas una copia cacheada de los datos en memoria, y luego cuando estés conectado nuevamente simplemente sincronizas tus datos con los del servidor—más sobre esto cuando veamos los DataSets.

Así que en lugar del vecino guacamaya, es como cuando te topas un compa de toda la vida en la calle:
—¿Qué rollo, vato?
—Cero balero. ¿Qué cuentan las morritas?
—Nada, ahí andan todas bien felices.
—Órale. Qué buen pex. Me las saludas… nos vemos.
(Y ya, tan tan)

¿DataWhat?

En cuanto comienzas a trabajar con ADO.NET verás un montón de clases con nombres similares: DataReader, DataAdapter, DataSet, DataTable, DataRow, DataRowView, DataCongal… bueno ese último quizá no sea real, pero el resto de las clases sí existen y debes conocerlas si quieres utilizar tus DataDatos en tu DataAplicación. Está bueno pues, ya fueron suficientes DataPayasadas.

Pero antes de meternos en ese tema, hay otros dos objetos que son más primitivos y que permiten que te conectes e interactúes con una base de datos: Connection y Command.

Connection permite que entables un canal de comunicación con tu base de datos, y Command permite que ejecutes alguna instrucción sobre el motor de la base de datos, por ejemplo un query SQL de SELECT, INSERT, UPDATE o DELETE, o ejecutar un Stored Procedure.

Ilustración conceptual de Connection y Command.  Da clic para ver la imagen en tamaño normal.
La clase exacta que utilizarás para establecer tu conexión y comandos dependerá del proveedor de datos que utilices, y el proveedor dependerá del tipo de base de datos a la que te quieras conectar: SQL Server, Oracle, Access, MySQL, etcétera. Y aunque hay una forma en el .NET Framework 2.0 de hacer tu código “independiente del proveedor de base de datos”, no hay que mezclar el caldo con la sopa todavía; lo veremos en otro artículo.

Por ahora, si quieres interactuar con una base de datos SQL Server, tendrás que utilizar SqlConnection y SqlCommand; si quieres platicar con Oracle tendrás que usar OracleConnection y OracleCommand; y si quieres hacerlo con Access, puedes usar OleDbConnection y OleDbCommand. Sin importar cuál selecciones, los pasos para utilizarlo siempre son los mismos:
  1. Abres la conexión
  2. Haces tu desmadre (o sea, ejecutas tus comandos)
  3. Cierras la conexión
Usando SQL Server sería algo como esto:

// asumiendo que al principio se declaró
// using System.Data.SqlClient; 
// para usar más fácilmente clases del SQL Server Data Provider
 
SqlConnection miConexion = new SqlConnection("MiStringDeConexion");
miConexion.Open();
 
// Hago mis cosas
 
miConexion.Close();

La conexión requerirá de un string de conexión, de acuerdo al proveedor de datos, para indicar a qué base de datos conectarse y con qué credenciales. Por ahora utilizaré "MiStringDeConexión" para mostrar donde va ese valor.

Una práctica muy recomendada para evitar que se nos olvide cerrar la conexión es utilizar la instrucción using de C# (VB 2005 también la tiene). Ojo: No es la misma que la que se utiliza para importar un espacio de nombres.

using (SqlConnection miConexion = new SqlConnection("MiStringDeConexion"))
{
    miConexion.Open();
    // Hago mis cosas
 
    // using, automáticamente invoca los finalizadores, Close() en este caso,
    // en cuanto se termina el scope de la instrucción.
}

El comando en sí se especifica utilizando la propiedad CommandText. Además, puedes especificar el tipo del comando que estás ejecutando con la propiedad CommandType:
  • Text—Cualquier query, incuyendo comandos para manipular la base de datos (CREATE TABLE, DROP TABLE, etc.)
  • StoredProcedure—Para ejecutar un procedimiento almacenado.
  • TableDirect—Para leer una tabla completita. Este no es soportado por algunos proveedores de datos como el de SQL Server.
using (SqlConnection miConexion = new SqlConnection("MiStringDeConexion"))
{
    miConexion.Open();
    //
    // hago mis cosas:
    //
    SqlCommand cmd1 = new SqlCommand();
    cmd1.Connection = miConexion;
    cmd1.CommandType = System.Data.CommandType.Text;
    cmd1.CommandText = "SELECT * FROM 'Peliculas'";
    // no lo estamos ejecutando todavia
 
    SqlCommand cmd2 = new SqlCommand();
    cmd2.Connection = miConexion;
    cmd2.CommandType = System.Data.CommandType.StoredProcedure;
    cmd2.CommandText = "SelectPeliculas"; // nombre de mi SP en mi DB
    // falta ejecutarlo
 
} // using manda llamar miConexion.Close()

Para ejecutar tus instrucciones, la clase Command te da 3 métodos principales:
  • ExecuteNonQuery()—Para cuando tu instrucción no regresa una respuesta, p. ej. un INSERT, UPDATE o DELETE
  • ExecuteScalar()—Para cuando tu instrucción regresa UN SOLO registro con UN SOLO campo con un valor numérico, por ejemplo el resultado de un “SELECT COUNT(*) FROM MiTabla”
  • ExecuteReader()—Para cuando quieres leer el resultado de uno o varios SELECTs usando un DataReader.
// la práctica recomendada es alamacenar este string de conexión
// en el archivo .config de mi aplicación (web.config o app.config).
//
// en este ejemplo, me estoy conectando a la instancia local 
// SQLEXPRESS, a una base de datos llamada AdoNetDemo utilizando 
// las credenciales del usuario que ejecuta mi aplicación
string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
using (SqlConnection miConexion = new SqlConnection(miStringDeConexion))
{
    miConexion.Open();
 
    // un INSERT utilizando SQL
    int registrosAfectados = 0;
    string miQuery =
      "INSERT INTO Peliculas (Titulo, FechaDeAdquisicion, Precio) " +
      "VALUES ('El Santo y Blue Demon vs. las Vampiras','2006-08-06',15.65);";
 
    SqlCommand cmd = new SqlCommand(miQuery, miConexion);
    cmd.CommandType = System.Data.CommandType.Text;
 
    registrosAfectados = cmd.ExecuteNonQuery();
    // registrosAfectados ahora tiene valor de 1;
 
 
    // ahora un SELECT COUNT(*) usando SQL
    int numDeRegistros = 0;
    string otroQuery = "SELECT COUNT(*) FROM Peliculas;";
 
    SqlCommand comandoParaContar = new SqlCommand(otroQuery, miConexion);
    numDeRegistros = Convert.ToInt32(comandoParaContar.ExecuteScalar());
 
 
    // finalmente, un ejemplo de como leer todos los registros de una tabla
    SqlCommand miCommando =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
    SqlDataReader speedy;
    speedy = miCommando.ExecuteReader();
} // using cerrará la conexión

¡Un momento! ¡Ya salió el DataReader!

Supongo entonces, que sería buena idea hablar de ellos.

DataReaders y DataSets
Cuando quieres leer datos, tienes de dos sopas en .NET: usar un DataReader o usar un DataSet con un DataAdaper. Para ambos necesitarás una Connection y al menos un Command (para el SELECT), pero la diferencia está en qué puede hacer uno y qué puede hacer otro.

DataReader es un objeto ligero y súper rápido que te permite leer UN registro a la vez, leyendo hacia enfrente solamente. Es lo que se conoce como un firehose reader, porque avienta los datos como si fuera una manguera a presión. En otras palabras DataReader es como Speedy Gonzáles: chiquito, prieto y medio feo pero bien rápido el desgracia’o.

DataReader vs. DataSet. Da clic para ver la imagen en tamaño normal.
Por otro lado, el DataSet es como Porky Pig: medio ghey y más choncho—porque contiene algunos sub-objetos—pero que puede hacer más graciosadas. Con esos nos vamos a clavar en unos momentos más (sin albur), pero primero vamos a ver el patrón clásico para utilizar un DataReader:

Ilustración conceptual del uso de un DataReader. Da clic para ver la imagen en tamaño normal.
Que en código se vería más o menos como lo siguiente:

// declarar conexion y comando;
// declarar DataReader;
// "abrir" el DataReader con el método ExecuteReader() del comando;
// leer los datos, uno por uno:
while (reader.Read())
{
    // hacer algo con los datos que leo
}
// cerrar el DataReader

El método Read() se encarga de leer los datos y al mismo tiempo, mover el “cursor” al siguiente registro. El método regresa true si logró leer el siguiente o false si ya no hay más registros que leer.

A pesar de ser sencillo y eficiente, el DataReader tiene algunas “desventajas” como son:
  • Es solo-lectura. Si quieres actualizar datos tendrías que ejecutar un Command por separado.
  • No te dice información sobre los datos que estás leyendo, por lo que no solo tienes que conocer el tipo de datos que estás leyendo, sino el orden de las columnas.
Suponiendo que tuviéramos la siguiente tabla con la información de tu colección de películas:

Da clic para ver la imagen en tamaño normal.
Tendrías que leerlos así:

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
using (SqlConnection miConexion = new SqlConnection(miStringDeConexion))
{
    // variables para poner los valores de cada columna
    int id = 0;
    string titulo = string.Empty;
    DateTime fechaDeAdquisicion;
    Decimal precio = 0;
 
    miConexion.Open();
 
    SqlCommand miCommando =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
    using (SqlDataReader speedy = miCommando.ExecuteReader())
    {
        while (speedy.Read())
        {
            id = speedy.GetInt32(0);      // la primer columna es el ID
            titulo = speedy.GetString(1); // la segunda columna es el Titulo
            fechaDeAdquisicion = speedy.GetDateTime(2); // etc...
            precio = speedy.GetDecimal(3);
 
            // hacer algo con los valores leidos.
            //
            // las variables ahora tienen el valor del registro actual
            // en esta iteración.
        }
    } // using cerrará el DataReader
} // using cerrará la conexión

Así que si lo único que necesitas es leer datos de manera eficiente, Speedy… digo… el DataReader es el que te conviene.

Pero habrá ocasiones en las que tengas que manipular varios datos a la vez, o necesites ver la relación de datos entre varias tablas. Es ahí cuando los DataSets y los DataTables son bastante útiles.

Ilustración conceptual del uso de un DataAdapter con un DataSet. Da clic para ver la imagen en tamaño normal.
Un DataSet puede contener una o varias DataTable que representen datos de tu base de datos. Cada DataTable, tiene una colección de DataRows que representan los registros que fueron leídos.

El papel del DataAdapter es el de “sincronizador de datos” por medio de sus dos métodos principales, Fill() y Update(). La forma de trabajar sería:
  1. Llenas tu DataSet
  2. Haces tu desmadre—actualizas, insertas, borras registros
  3. Actualizas tu base de datos
Una ventaja al usar un DataAdapter es que no tengo que preocuparme por andar abriendo y cerrando la conexión. Estos métodos lo hacen automáticamente.

Con Fill() puedes llenar todo tu DataSet—o una DataTable específica de tu DataSet—con los datos de la base de datos. Para usarlo, debes primero configurarle al DataAdapter su SelectCommand, que es el que indica qué query debe ejecutar para traerse los datos. Si no planeas actualizar datos, pues este es el único comando que necesitas configurarle.

// asumiendo que al principio se declaró
// using System.Data.SqlClient; 
// para usar más fácilmente clases del SQL Server Data Provider y
// using System.Data;
// para usar DataSet, DataTable, DataRow, et. al.
 
string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
SqlConnection miConexion = new SqlConnection(miStringDeConexion);
 
SqlCommand miCommando =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
SqlDataAdapter miAdaptador = new SqlDataAdapter(miCommando);
// tambien habriamos podido utilizar la propiedad SelectCommand
// miAdaptador.SelectCommand = miComando;
 
DataSet porky = new DataSet();
 
// llenar mi DataSet de acuerdo al SelectCommand
miAdaptador.Fill(porky);
 
// el DataAdapter automáticamente creó un DataTable en mi DataSet
// con la misma estructura que el resultado de mi query,
// incluyendo nombres de columnas y tipos de datos
//
// ahora puedo leer los registros de la siguiente manera:
foreach (DataRow registro in porky.Tables[0].Rows) 
{
    //registro["ID"];
    //registro["Titulo"];
    //registro["FechaDeAdquisicion"];
    //registro["Precio"];
}

La manera más sencilla de comprobar que mi DataSet o DataTable tiene ya datos es utilizando el visualizador que viene con Visual Studio:

Imagen de cómo invocar el DataSet Visualizer. Da clic para ver la imagen en tamaño normal.Imagen del DatSet visualizer. Da clic para ver la imagen tamaño normal.
Con Update() el DataAdapter intenta propagar los cambios que hayas hecho en tu DataSet hacia la base de datos.

¿Cómo lo hace? Polvos de hada.

Bueno, en realidad usa una propiedad de cada DataRow llamada RowState, que puede tener valores como “nuevo”, “sin cambios”, “modificado” y “eliminado”. El estado de cada DataRow se ajusta en cuanto haces algún cambio a los datos. De esa forma lo único que tiene que hacer el DataAdapter, cuando llamas Update() es recorrer todos los rows del DataSet e invocar el InsertCommand, UpdateCommand o DeleteCommand, de acuerdo a si el DataRow es nuevo, modificado o eliminado, respectivamente. Obviamente para que esto funcione, necesitas haberle configurado previamente esos comandos al DataAdapter.

Aunque podrías especificarlos manualmente, si quieres evitar la hueva errores al hacerlo, puedes utilizar un CommandBuilder, que automáticamente los inferirá—el Insert, Update y DeleteCommand—en base al SelectCommand.

string miStringDeConexion =
    @"Data Source=.\SQLEXPRESS;Initial Catalog=AdoNetDemo;" +
    "Integrated Security=True";
 
SqlConnection miConexion = new SqlConnection(miStringDeConexion);
 
SqlCommand miCommandoSelect =
        new SqlCommand("SELECT * FROM Peliculas;", miConexion);
 
SqlDataAdapter miAdaptador = new SqlDataAdapter(miCommandoSelect);
 
SqlCommandBuilder miConstructor = new SqlCommandBuilder(miAdaptador);
 
DataSet porky = new DataSet();
 
// llenar mi DataSet de acuerdo al SelectCommand
miAdaptador.Fill(porky);
 
// modificar algunos de los registros
 
// al primer registro, ponerle el título en mayúsculas
porky.Tables[0].Rows[0]["Titulo"] = 
    porky.Tables[0].Rows[0]["Titulo"].ToString().ToUpper();
// eliminar el 4to registro
porky.Tables[0].Rows[3].Delete(); 
 
 
// actualizar la base de datos en base a los cambios
// hasta antes este momento la BD sigue intacta
miAdaptador.Update(porky);
 
// los cambios fueron propagados a la BD.

Los datos despúes de la actualización. Da clic para ver la imagen en tamaño normal.
Para ser un poco más completos, hay que mencionar que DataSet tiene muchas más monerías que no alcanzaremos a ver en este artículo. Por ejemplo, puede representar varias tablas de tu base de datos con todo y relaciones para mantener la integridad referencial.

Ilustración conceptual de un DataSet. Da clic para ver la imagen en tamaño normal.
Como en el siguiente ejemplo:

Un ejemplo de un DataSet visto en el designer de Visual Studio. Da clic para ver la imagen en tamaño normal.
En otras palabras, puedes representar toda o una parte de tu base de datos por medio de un DataSet. Sin embargo, cuando manejas varias tablas al mismo tiempo, las cosas comienzan a ponerse interesantes cuando quieres llenarlas o actualizarlas.

Quizá es por eso que en Visual Studio 2005—incluyendo las versiones Express—ahora trae soporte para TableAdapters, que será el tema del próximo artículo en esta serie.



Este es el primero de varios artículos que tratan sobre ADO.NET y las monerías que trae la versión 2.0 del .NET Framework. Si quieres averiguar más sobre el tema, haz una búsqueda en este blog o dale clic a la etiqueta de ado.net.

Si tienes sugerencias, correcciones u opiniones sobre este artículo o si tienes sugerencias para artículos nuevos de temas que te gustaría ver en este blog, deja un comentario mediante los enlaces de abajo.

Las imágenes y nombres de los personajes "Speedy Gonzales" y "Porky Pig" son propieadad de Warner Bros. y tomados sin permiso para fines de no-lucro. Así que si piensan demandarme, ni se molesten, porque saldría corriendo y gritando como Flanders para quitar las imágenes y referencias de este blog.

Evento de Workflow Foundation en El Paso

El Grupo de Usuarios de .NET en El Paso nos está invitando a un evento el próximo Martes 29 de Agosto, de 6pm - 8:30 pm en el Buisness Building de UTEP, room 111.

Al evento vendrá una persona de Micorosft, J Sawyer, que es un "evangelista" de .NET, para hablar sobre Windows Workflow Foundation. (más info aquí).

Si se perdieron la plática en la reunión de mayo, o si quieren conocer más sobre el tema entonces échense una vuelta. Esta plática va a durar más que la que tuvimos nosotros (como 2 horas y media).

Los que estén interesados en acudir tienen que enviarle un correo a Tiffany Cynor a más tardar este viernes 25 de agosto para confirmar su asistencia. Su correo es:

Tiffany Cynor tmcynor@hotmail.com

More Posts Next page »