TeraBIThia

30/11/07

70-528. Resumen. XML

Archivado en: General — crisfervil @ 1:41 pm

Cuarta parte del resumen sobre el temario para el examen de certificación 70-528 para desarrollo web.


Clases XML (Todas en System.Xml en System.Xml.dll)

XPathNavigator
XmlDocument
XmlDataDocument
XPathDocument
XslTransform
XmlTextReader
XmlTextWriter

XmlDataDocument hereda de XmlDocument

DTD (Document Type Definition): Documento que contiene el esquema de un documento Xml. (http://en.wikipedia.org/wiki/Document_Type_Definition o http://es.wikipedia.org/wiki/Document_Type_Definition)

XSD (Xml Schema Definition): Es algo así como una nueva versión de DTD, más completa. Recomendad por W3C. (http://en.wikipedia.org/wiki/XML_Schema o http://es.wikipedia.org/wiki/XML_Schema)

XDR (Xml Data Reduced) es otro formato de definición de esquemas para Xml.

XSL, XSLT (Extensible StyleSheet Languaje)  Lenguaje que permite realizar transformaciones sobre documentos XML.

 

XPathDocument es una cache de sólo lectura que viene bien para usar con consultas XPath.

XmlConver es una clase con un montón de métodos estáticos que permiten realizar conversiones de datos acordes con estándares Xml.

XPathNavigator, permite recorrer un documento Xml de manera eficiente usando consultas XPath.

XmlNodeReader: Permite un acceso secuencial (forward-only) para recorrer un documento a partir de un nodo determinado.

XmlTextReader: Proporciona acceso secuencial (no chacheado) a un documento xml.

XmlTextWriter: Proporciona acceso secuencial, no chacheado para la escritura de documentos xml.

XmlReader: Permite leer y validar contenido  conforme a un DTD, XDR o XSD.

XslTransform: Permite transformar un documento Xml a partir de lo especificado en otro XSL.

Crear un documento Xml desde cero. (Código autoexplicativo, es que, está claro que es mucho más entretenido leer código, que un documento)

// Instancia de un nuevo XmlDocumnent
XmlDocument xmlDoc = new XmlDocument();
// Crear la declaración del tipo de documento
xmlDoc.AppendChild(xmlDoc.CreateXmlDeclaration("1.0", "utf-8", null));
// crear el nodo raíz
XmlElement rootNode = xmlDoc.CreateElement("Clientes");
// anexarlo al documento
xmlDoc.AppendChild(rootNode);
// Ahora, anexar cada uno de los clientes
for (int i = 0; i < 5; i++)
{
    // Un element para cada cliente
    XmlElement customerElement = xmlDoc.CreateElement("Cliente");
    // El id, lo estableceremos como atributo del elemento
    XmlAttribute idAttribute = xmlDoc.CreateAttribute("Id");
    // cada atributo debe tener un valor
    idAttribute.Value = i.ToString("000");
    // y cada atributo con su padre elemento
    customerElement.Attributes.Append(idAttribute);
    // El nombre en cambio,lo anexaremos como sub elemento de Cliente
    XmlElement nameElement = xmlDoc.CreateElement("Nombre");
    // Los sub elementos pueden a su vez tener atributos
    // o bien, almacenar el valor en como texto en medio de las
    // etiquetas de apertura y cierre del elemento
    nameElement.InnerText = string.Format("Nombre cliente {0}", i.ToString("000"));
    // cada sub-elemento con su padre elemento
    customerElement.AppendChild(nameElement);
    // y cada elemento, a su nodo raíz
    rootNode.AppendChild(customerElement);
}
// Guardar el documento
string fileName = "..\\..\\Customers.xml";
xmlDoc.Save(fileName);

El código de arriba, genera el siguiente documento xml:

<?xml version="1.0" encoding="utf-8"?>
<Clientes>
  <Cliente Id="000">
    <Nombre>Nombre cliente 000</Nombre>
  </Cliente>
  <Cliente Id="001">
    <Nombre>Nombre cliente 001</Nombre>
  </Cliente>
  <Cliente Id="002">
    <Nombre>Nombre cliente 002</Nombre>
  </Cliente>
  <Cliente Id="003">
    <Nombre>Nombre cliente 003</Nombre>
  </Cliente>
  <Cliente Id="004">
    <Nombre>Nombre cliente 004</Nombre>
  </Cliente>
</Clientes>

Recorriendo un documento Xml mediante DOM.
Ahora, el ejemplo opuesto. Dado un documento Xml, ¿podemos recorrerlo y extraer información acerca de él?

Usaremos el xml de Clientes, generado en el ejemplo anterior. Intentaremos copiar los datos a un DataTable (lo cual es una soberana tontería, ya que el DataTable viene con un método que permite cargar datos y estructura desde un xml)

XmlDocument xmlDoc = new XmlDocument();
string fileName = "..\\..\\Customers.xml";
// Cargamos el documento xml desde el fichero
xmlDoc.Load(fileName);
// generamos el DataTable en el que cargaremos los datos del xml
DataTable customersTable = new DataTable("Clientes");
// Configuramos el datatable, le agregamos las columnas, etc.
customersTable.Columns.Add(new DataColumn("Id", typeof(int)));
customersTable.Columns.Add(new DataColumn("Nombre"));
// Recorremos los hijos del nodo raíz
foreach (XmlElement element in xmlDoc.DocumentElement.ChildNodes)
{
    // cada element, es un cliente, y debe tener un atributo llamado id
    string id = element.Attributes["Id"].Value;
    // cada element cliente, tenía un sub-elelement con el nombre
    XmlNodeList childNodes = element.GetElementsByTagName("Nombre");
    string nombre = childNodes[0].InnerText;
    // agregamos la informaciónj obtenida al datatable
    customersTable.Rows.Add(new object[] { XmlConvert.ToInt64(id),nombre });
}

Qué Datatable genera esto?

image 

El ejemplo anterior lo podemos realizar con la clase XPathNavigator. La ventaja de usar esta clase es que el recorrido es mucho mas óptimo, y consume menos recursos.

XmlDocument xmlDoc = new XmlDocument();
string fileName = "..\\..\\Customers.xml";
// Cargamos el documento xml desde el fichero
xmlDoc.Load(fileName);
// generamos el DataTable en el que cargaremos los datos del xml
DataTable customersTable = new DataTable("Clientes");
// Configuramos el datatable, le agregamos las columnas, etc.
customersTable.Columns.Add(new DataColumn("Id", typeof(int)));
customersTable.Columns.Add(new DataColumn("Nombre"));
// Creamos el objeto xPathNavigator
XPathNavigator xPathNav = xmlDoc.CreateNavigator();
// Al crearlo, estamos en el rootNode.
// Por lo tanto, nos desplazaremos al primer elemento
xPathNav.MoveToFirstChild();
// El primer elemento será el document element "Clientes" en
// nuestro caso, con lo cual necesitamos volver a 
// desplazarnos esta vez hasta el primer elemento "Cliente"
xPathNav.MoveToFirstChild();
// en este punto, estamos en el primer cliente
do
{
    // Recordemos que cliente, almacena su id como atributo
    xPathNav.MoveToFirstAttribute();
    int id = xPathNav.ValueAsInt;
    // Y el nombre estaba como sub-elemento
    // Antes de ver los sub-elementos, hay que volver atrás.
    // Recordemos que ahora mismo estamos sobre el atributo
    xPathNav.MoveToParent();
    // ahora volvemos a estar sobre Cliente
    // Ya nos podemos desplazar al primer hijo, que será
    // el nombre
    xPathNav.MoveToFirstChild();
    string nombre = xPathNav.Value;
    // agregamos la informaciónj obtenida al datatable
    customersTable.Rows.Add(new object[] { id, nombre });
    // antes de iterar al siguiente cliente, habrá que
    // volver al padre
    xPathNav.MoveToParent();
}
// intentamos movernos al siguiente nodo, y si lo conseguimos, 
// volvemos a repetir el proceso
while (xPathNav.MoveToNext());

Crossposted from crisfervil.com

29/11/07

Pantalla táctil

Archivado en: General — crisfervil @ 7:22 pm

A mi me gustan mucho estos cacharros futuristas.

Visto en http://www.perceptivepixel.com/

Crossposted from crisfervil.com

28/11/07

70-528. Resumen. ADO.net

Archivado en: General — crisfervil @ 4:48 pm

Tercera parte del resumen sobre el temario para el examen de certificación 70-528 para desarrollo web.


Clases “Desconectadas” de ADO.net

DataTable
DataColumn
DataRow
Constraint
DataView
DataSet
DataRelation

Cata DataRow posee estos estados: Detached, Added, Unchanged, Modified, Deleted.

Cada DataRow mantiene hasta cuatro versiones del mismo registro:
Default, Original, Proposed y Current.

El método HasVersion de DataRow indica la existencia de una versión en particular.

El método AcceptChanges de DataRow cambia su estado a Unchanged.

El método GetChanges DataTable devuelve otro DataTable que contiene únicamente los registros actualizados.

El método Copy de DataTable devuelve una copia de la tabla y todos sus registros. (Qué estado tienen sus DataRows? r: Added)

El método Clone de DataTable devuelve una copia de la estructura de la tabla. Se omiten los registros.

El método Import de DataTable copia los datos de otro DataTable. Se copian la versión Current y la Original. Si ya existe algún registro con la misma PK, se lanza una ContraintException. La estructura de la DataTable origen y destino deben ser iguales.

El método WriteXml de DataTable serializa toda la tabla a un archivo Xml.

La propiedad ColumnMapping de DataRow indica la manera en que deben serializarse los valores de esa columna en un archivo XML. Puede ser Attribute, Element, Hidden, SimpleContent

El la propiedad RowStateFilter de DataView permite seleccionar sólo aquellas filas que cumplan un criterio de estado determinado. Se puede especificar (uno o más a la vez usando el operador | para c# ó or para vb ): Added, CurrentRows, Deleted, ModifiedCurrent, ModifiedOriginal, None, OriginalRows, Unchanged.

Los objetos DataSet poseen una colección de objetos DataRelation que permiten relacionar dos tablas dentro del DataSet. Estas relaciones permiten asegurar la integridad referencial.

El método WriteXml tanto del DataTable como del DataSet permiten almacenar los valores originales y modificados de las filas especificando el valor XmlWriteMode.DiffGram en el parámetro mode.

Para generar un Xml de tipo DiffGram que incluya únicamente los registros modificados, llamar primero al método GetChanges, y a continuación a WriteXml.

El DiffGram es generalmente utilizado en aquellos entornos en los que el acceso a datos es ocasionalmente conectado y se requiere realizar sincronizaciones en el momento de la conexión. Cuando hay conexión, se obtienen los datos del servidor. A continuación, se realizan las modificaciones convenientes. Si al finalizar la aplicación no se ha vuelto a realizar una sincronización, se persisten tanto los datos originales como los modificados, para que estén disponibles cuando se vuelva a iniciar la aplicación.

Al deserializar, si necesitamos deserializar desde un DifGram, debemos indicarlo en el parámetro mode de ReadXml.

XmlReadMode puede tomar los siguientes valores: Auto, DiffGram, Fragment, IgnoreSchema, InferSchema, InferTypedSchema, ReadSchema.

La propiedad RemotingFormat tanto en objetos DataSet como DataColumn permite especificar el formato, xml o binario, que se utilizará para la serialización.

La serialización binaria permite persistir la información completa del DataSet o el DataTable, incluyendo el RowState de los DataRows.

Tanto DataSet como DataTable proporcionan el método Merge, que permite sincronizar 2 objetos DataTable o DataSet. La sincronización se realiza a través de las claves principales. El parámetro preserveChanges indica si los cambios en el objeto destino sobreescriben lo que hubiere en el objeto copiado.
El parámetro MissingSchemaAction indica lo que hacer si las estructuras de los objetos sincronizados no coinciden. Este valor puede ser: Add, AddWithPrimaryKey, Error, Ignore. Si los objetos DataTable de los que vayamos a sincronizar no tienen PKs definidas, se lanzará una excepción.

Mientras escribía este resumen, he ido haciendo pruebas en una aplicación de consola. Este es el código:

// Cómo crear e instanciar una tabla
DataTable customers = new DataTable("Customers");

// Crear e instanciar una columna
DataColumn idCol = new DataColumn("Id");
idCol.Caption = "id";
// Así establecemos la longitud máxima de la columna
idCol.MaxLength = 5;
// Así agregamos la columna a la tabla
customers.Columns.Add(idCol);

DataColumn nameCol = new DataColumn("Name");
nameCol.Caption = "Nombre";
customers.Columns.Add(nameCol);

DataColumn lastNameCol = new DataColumn("LastName");
lastNameCol.Caption = "Apellidos";
customers.Columns.Add(lastNameCol);

DataColumn bornDateColumn = new DataColumn("BornDate");
bornDateColumn.Caption = "Fecha de nacimiento";
// Así establecemos un tipo de dato distinto de string
bornDateColumn.DataType = typeof(DateTime);
customers.Columns.Add(bornDateColumn);

// Esta es una columna cuyos valores son calculados (ExpressionColumn)
DataColumn nameAndLastNameCol = new DataColumn("NameAndLastName");
nameAndLastNameCol.Caption = "Nombre y Apellidos";
nameAndLastNameCol.Expression = "Name + ' ' + LastName";
customers.Columns.Add(nameAndLastNameCol);

// Así establecemos las columnas que forman parte de la PK
customers.PrimaryKey = new DataColumn[] { idCol };

// Así agregamos datos a la tabla
// Un item del array por cada columna
customers.Rows.Add(new object[] { "001", "Cristhian", "Fernández Villalba", new DateTime(1982,2,17), null });

// Esta es otra manera de agregar datos a la tabla
DataRow newCustomer = customers.NewRow();
newCustomer[idCol] = "002";
newCustomer[1] = "Pepito";
newCustomer["LastName"] = "Pérez";
customers.Rows.Add(newCustomer);

// Esta es otra manera de insertar.
// Con LoadDataRow también es posible Actualizar datos.
// Lo permite el segundo parámetro
customers.LoadDataRow(new object[] { "002", "Pepito", "Pérez Pérez", new DateTime(1982,2,17), null }, LoadOption.Upsert);

// Todo lo insertado hasta ahora es lo original
customers.AcceptChanges();
// A partir de este momento todas las filas tendrán estado Unchanged

// Cambiar el estado de una fila
// Qué RowState tiene la fila 0 antes del cambio?
Console.WriteLine(customers.Rows[0].RowState); //(Unchanged)
customers.Rows[0][nameCol] = "Cristhian Baltazar";
// Qué RowState tiene la fila cero después del cambio?
Console.WriteLine(customers.Rows[0].RowState); // (Modified)

// Si copio todos los datos a otra DataTable, con qué
// RowState se copiarán las filas modificadas?
DataTable customersCopy = customers.Copy();
// Miramos el RowState de la fila modificada
Console.WriteLine(customersCopy.Rows[0].RowState); // Modified
// Existe una versión Default para la fila cero? // True
Console.WriteLine(customersCopy.Rows[0].HasVersion(DataRowVersion.Default));
// Existe una versión Original para la fila cero? // True
Console.WriteLine(customersCopy.Rows[0].HasVersion(DataRowVersion.Original));
// Existe una versión Proposed para la fila cero? // False
Console.WriteLine(customersCopy.Rows[0].HasVersion(DataRowVersion.Proposed));
// Existe una versión Current para la fila cero? // True
Console.WriteLine(customersCopy.Rows[0].HasVersion(DataRowVersion.Current));

// Y cómo se serializan las versiones de las filas
customersCopy.WriteXml("Customers.xml");
// Al abrir el xml generado no existen versiones.

// Y al deserializar, en qué estado queda la fila cero?
// Ojo, como ReadXml carga registros, hay que eliminar los
// que existen, sino, se lanza una ConstraintException
customersCopy.Rows.Clear();
customersCopy.ReadXml("Customers.xml");
Console.WriteLine(customersCopy.Rows[0].RowState); // Added

// Volvemos a serializar. Esta vez incluyendo el esquema
customersCopy.WriteXml("Customers.xml", XmlWriteMode.WriteSchema);

// Agregamos algún dato más
customers.Rows.Add(new object[] { "003", "Jorge", "Jiménez", new DateTime(2000,6,6), null });

// Generamos una vista del DataTable
DataView olderCustomers = new DataView(customers);
// Establecemos un filtro
olderCustomers.RowFilter = "BornDate < '01/01/1990'";
// Y un criterio de ordenación
olderCustomers.Sort = "Name DESC";

// Podemos especificar un filtro de estado
olderCustomers.RowStateFilter = DataViewRowState.Added | DataViewRowState.Deleted;

// Así creamos un DataSet
DataSet dsPepitoSL = new DataSet("PepitoSL");

// Así le agregamos Tablas
dsPepitoSL.Tables.Add(customers);

// Creamos otra tabla
DataTable invoices = new DataTable("Facturas");

DataColumn invoiceIdCol = new DataColumn("IvoiceId",typeof(Guid));
invoices.Columns.Add(invoiceIdCol);

DataColumn customerIdCol = new DataColumn("CustomerId");
customerIdCol.MaxLength = 5;
invoices.Columns.Add(customerIdCol);

DataColumn amountCol = new DataColumn("Amount", typeof(Single));
invoices.Columns.Add(amountCol);

dsPepitoSL.Tables.Add(invoices);

// Así crearmos una relación entre dos tablas
DataRelation relCustomersInvoices = new DataRelation("CustomersInvoices", idCol, customerIdCol);

// Así la agregamos al DataSet
dsPepitoSL.Relations.Add(relCustomersInvoices);

// Serializamos el DataSet
// Se omitirán en la serialización los valores originales
// (Si la fila no ha cambiado se serializa la fial original)
dsPepitoSL.WriteXml("PepitoSL.xml");

// Si necesitamos serializar valores originales y modificados
dsPepitoSL.WriteXml("PepitoSL.xml", XmlWriteMode.DiffGram);

// Como estarán las filas modificadas si deserializamos en este momento?
dsPepitoSL.ReadXml("PepitoSL.xml", XmlReadMode.DiffGram);
Console.WriteLine(dsPepitoSL.Tables["Customers"].Rows[0].RowState); // Modified

// Serializamos el DataSet en binario
dsPepitoSL.RemotingFormat = SerializationFormat.Binary;
using (FileStream fs = new FileStream("PepitoSL.bin", FileMode.Create))
{
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(fs, dsPepitoSL);
}

// Al deserializar lo serializado en binario, cómo están
// los RowStates de las filas modificadas?
using(FileStream fs = new FileStream("PepitoSL.bin", FileMode.Open))
{
    BinaryFormatter bf = new BinaryFormatter();
    dsPepitoSL = (DataSet)bf.Deserialize(fs);
}
Console.WriteLine(dsPepitoSL.Tables["Customers"].Rows[0].RowState); // Modified !!

// Y cómo sincronizaría 2 DataSets?
// Copiamos solo la estructura...
DataSet disconnectedPepitoSL = dsPepitoSL.Clone();
// y a continuación sincronizamos...
disconnectedPepitoSL.Merge(dsPepitoSL);
// vemos qué tiene el DataSet desconectado...

 

DataSet tipados: Para agregar un DataSet tipado a la solución, menú Project/Add new item/DataSet y sobre la superficie del diseñador, arrastrar las tablas desde el explorador de servidores.

Clases “Conectadas” de ADO.net

DbConnection
DbCommand
DbDataAdapter
DbProviderFactory
DbProviderFactories

Proveedores de datos que vienen de serie en el .net Framework

OleDb (System.Data.OleDb)
Odbc (System.Data.Odbc)
SQL Server (System.Data.SqlClient)
Oracle (System.Data.OracleClient)

También existen aparte de estas, y fuera del .net Framework, proveedores de terceros que pueden ser descargados y utilizados, para db2, mySQL, etc.

Interfaces, clases base e implementaciones

Interface Clase Base
(System.Data.Common)
Clases Concretas
IDbConnection DbConnection SqlConnection
OracleConnection
OleDbConnection
OdbcConnection
IDbCommand DbCommand SqlCommand
OracleCommand
OleDbCommand
OdbcCommand
IDataReader/IDataRecord DbDataReader SqlDataReader
OracleDataReader
OleDbDataReader
OdbcDataReader
IDbTransaction DbTransaction SqlTransaction
OracleTransaction
OleDbTransaction
OdbcTransaction
IDbDataParameter DbParameter SqlParameter
OracleParameter
OleDbParameter
OdbcParameter
IDataParameterCollection DbParameterCollection SqlParameterCollection
OracleParameterCollection
OleDbParameterCollection
OdbcParameterCollection
IDbDataAdapter DbDataAdapter SqlDataAdapter
OracleParameter
OleDbDataAdapter
OdbcDataAdapter
  DbCommandBuilder SqlCommandBuilder
OracleCommandBuilder
OleDbCommandBuilder
OdbcCommandBuilder
  DbConnectionStringBuilder SqlConnectionStringBuilder
OracleConnectionStringBuilder
OleDbConnectionStringBuilder
OdbcConnectionStringBuilder
  DbDataPermission SqlPermission
OraclePermission
OleDbPermission
OdbcPermission

 

La gestión de la pool de conexiones se realiza mediante la cadena de conexión. Los parámetros que se pueden especificar son:

(http://msdn2.microsoft.com/es-es/library/system.data.sqlclient.sqlconnection.connectionstring(VS.80).aspx)

Connection Timeout: en segundos. Por defecto 15 segundos.

Min Pool Size: Por defecto 5. Es recomendable usar un valor no demasiado alto, cercano al valor por defecto.

Max Pool Size: Por defecto 100.

Pooling: Indica si se debe activar o no la pool de conexiones. Por defecto es true.

Connection Reset: Indica que la conexión debe ser reseteada cuando se extraiga de la pool. Por defecto es true.

Load Balancing Timeout, Connection Lifetime: El tiempo máximo en segundo que la conexión puede existir en la pool.  Este valor es evaluado en el momento en que la conexión vuelve a la pool. Este parámetro es muy útil a la hora de conectarse a un cluster con balanceador de carga, pues obliga a que cada cierto tiempo la conexión se destruya, y vuelva a crearse, usando el servidor activo en ese momento. Por defecto es cero.

Enlist: true indica que el agrupador de conexión da de alta automáticamente la conexión en el contexto de transacción actual del subproceso de creación (ein?? me he quedado igual). Creo que significa que la conexión se crea teniendo en cuenta si se está trabajando con transacciones o no. Por defecto true.

Reglas a tener en cuenta en la gestión de pool de conexiones:

La cadena de conexión debe ser EXACTAMENTE la misma.

El usuario debe ser el mismo para los que accedan a la pool. Esto aplica aunque el acceso se realice mediante autenticación windows. (entonces, si tengo una aplicación web, en la que el acceso se realiza mediante autenticación windows, cada usuario tiene un id distinto, la cadena de conexión es la misma, no puedo tener una pool única en toda la aplicación?)

El Id de proceso debe ser el mismo. No es posible compartir pool de conexiones entre procesos.

Encriptar configuración

Para encriptar el web.config, usar el comando aspnet_regiis de la línea de comandos de visual studio.

aspnet_regiis -pef “connectionStrings” “C:\…\EncryptWebSite”

Para desencriptarlo

aspnet_regiis -pdf “connectionStrings” “C:\…\EncryptWebSite”

 

SQL: Structured Query Languaje.

DML: Data Manipulation Languaje. Para la manipulación de datos. Selectet, Insert, Delete, Update, etc.

DDL: Data Definition Languaje. Para operar sobre los objetos de la base de datos. Crear tablas, etc.

DCL: Data Control Languaje. Para operar sobre el acceso a los datos. Asignar, denegar permisos, etc.

El objeto DbConnection expone el método CreateCommand, que es el que se recomienda sea usado para la creación de objetos DbCommand. De este modo se genera un código más desacoplado.

Del mismo modo, DbCommand expone el método Parameters.Add, que es el que debería usarse para crear comandos adecuados para él.

SQL Provider admite que los parámetros establecidos en la colección Parameters de DbCommand sigan cualquier orden, siempre que los nombres coincidan con los definidos en el procedimiento almacenado.

En cambio, OleDb Provider OBLIGA a que el orden sea el mismo al definido en el procedimiento almacenado.

DbCommand expone el método ExecuteNonQuery que debe ser usado cuando el comando a ejecutar no devolverá resultados. Este método sólo devuelve el número de filas afectadas.

ExecuteScalar el emétodo de DbCommand para ejecutar comandos en los que se espera un único resultado. Como un total, un valor, etc.

ExecuteReader en cambio debe ser usado para las situaciones en las que el comando devuelve un conjunto de registros. Este comando devuelve un DbDataReader.

DbDataReader es un cursor de servidor, de sólo lectura y de solo avance (forward-only).

MARS: Multiple Active Resulsets. Es una característica que permite tener más de un DbDataReader abierto a la vez. Esta se debe habilitar en la cadena de conexión, mediante la clave MultipleActiveResultSets=True. Habilitar esta característica tiene un impacto negativo en el rendimiento, así que de ser posible, debe ser evitado.

SqlBulkCopy. Esta  clase proporciona un método de alto rendimiento para la copia masiva de registros en una base de datos SQL Server. Este método es WriteToServer.

DbDataAdapter es la clase que permite obtener y actualizar datos entre un origen de datos y un DataTable.
DbDataAdapter necesita que le especifiquemos un SelectCommand, InsertCommand, UpdateCommand y DeleteCommand (todos de tipo DbCommand) que son los que usará para realizar el mantenimiento y la extracción de los datos.

El método Fill de DbDataAdapter es el que se encarga de la operación de consulta de los datos desde el origen al DataTable.
El método Update realiza las demás operacíones.
UpdateBachSize permite indicar al DbDataAdapter la manera en la que deberá gestionar el número de actualizaciones que enviará al servidor en cada llamada. Un valor 0, indica que intentará enviar el mayor número de actualizaciones cada vez. Un valor superior, indica el número de actualizaciones que se deberá enviar cada vez.

DbProviderFactory (System.Data.Common) . Clase base que permite la implementación de factorías para la creación de Clases específicas de acceso a datos. Las implementaciones de DbProviderFactory, al menos las que vienen de fábrica, suelen implementarse como Singleton, para mantener una única instancia.

La clase DbProviderFactories permite obtener la lista de todas las DbProviderFactory registradas y disponibles. Las DbProviderFactory se registran en %WINDIR%\Microsoft.NET\Framework\\v2.0.50727\CONFIG\machine.config. El método para obtenerlas es GetFactoryClasses.

Curiosidad:
En machine.config, las DbProviderFactory se registran mediante el elemento <add>, y esta configuración está disponible para todas las aplicaciones. Si necesitamos que una aplicación no permita un elemento dado, usaremos el elemento remove, de la forma <remove invariant=”System.Data.Odbc” />  y si necesitamos eliminar todos, <clear/>.

DbProviderFactory proporciona la propiedad CanCreateDataSourceEnumerator que nos indica si la factory admite la llamada a CreateDataSourceEnumerator. Este método, devuelve una instancia del correspondiente DbDataSourceEnumerator.

DbDataSourceEnumerator permite obtener los orígenes de datos disponible para un determinado Provider. La obtención se realiza mediante el método GetDataSources() que devuelve un DataTable con la información.

DbException es la clase de la que heredan todas las excepciones que se generan en las clases de Ado.net, con lo cual, en el código de acceso a datos de tipo genérico, bastará con capturar las excepciones de este tipo. Se puede utilizar la propiedad Data de la excepción para obtener información adicional, o bien, la propiedad Message.

SqlConnection posee el evento InfoMessage que puede ser utilizado para obtener información general y sobre errores en la conexión. El delegado de este evento admite recibirá un parámetro de tipo SqlInfoMessageEventArgs que expone la propiedad Message y Errors, que es una colección de SqlError.

SqlError (System.Data.SqlClient) expone las propiedades Class, LineNumber, Message, Number, Procedure, Server, Source y State.

Y.. cómo quedaría el código de acceso a datos para una aplicación “Provider Idependent”?? pues más o menos así:

// Obtener del archivo de configuraciones el atributo ProviderName de la 
// entrada con name="Main". Es necesario referenciar System.Configuration.dll
string providerName = ConfigurationManager.ConnectionStrings["Main"].ProviderName;
// Buscar un DbPRoviderFactory para el Provider Especificado
DbProviderFactory dbf = DbProviderFactories.GetFactory(providerName);
// Si no hay provider, no podemos hacer nada...
if (dbf == null) throw new Exception("Provider especificado en configuración no encontrado.");
// Intentar instanciar la conexión mediante el provider
using (DbConnection cnn = dbf.CreateConnection())
{
    // Aplicar la cadena de conexión especificada en el config
    cnn.ConnectionString = ConfigurationManager.ConnectionStrings["Main"].ConnectionString;
    cnn.Open();
    // La conexión nos servirá para crear un DbCommand adecuado
    using (DbCommand cmd = cnn.CreateCommand())
    {
        // Configurar el comando...
        cmd.CommandText = "SELECT * FROM CUSTOMERS";
        // Volvemos a tirar de la ProviderFactory para crear un DataAdapter
        using (DbDataAdapter da = dbf.CreateDataAdapter())
        {
            // Configuramos el Adapter y cargamos un DataTable con él
            da.SelectCommand = cmd;
            DataTable customersTable = new DataTable("Customers");
            da.Fill(customersTable);
        }
    }
}

Este código requiere un App.config que tenga definida la sección connectionStrings:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
    </configSections>
    <connectionStrings>
        <add name="Main"
            connectionString="Data Source=XXXX;Initial Catalog=Customers;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
</configuration>

 

Transacciones

El método BeginTransaction de DbConnection inicia una transacción y devuelve una referencia a DbTransaction.

La instancia de DbTransaction devuelta debe asignarse a la propiedad Transaction del DbCommand para que el comando se ejecute en el contexto de dicha transacción.

Cómo debería estructurarse el código de acceso a datos para ejecutar un comando en el contexto de una transacción de una manera segura? más o menos así:

// GetConnection() se encarga de instanciar e inicializar la conexión
using (System.Data.Common.DbConnection cnn = GetConnection())
{
    using (System.Data.Common.DbTransaction trans = cnn.BeginTransaction())
    {
        using (System.Data.Common.DbCommand dc = cnn.CreateCommand())
        {
            try
            {
                // Configurar command
                // ...
                dc.Transaction = trans;
                // Ejecutar
                dc.ExecuteNonQuery();
                // Commit
                trans.Commit();
            }
            catch
            {
                // RollBack
                trans.Rollback();
                throw;
            }
        }
    }

La ejecución asíncrona de comandos es una característica que viene de fábrica para el SqlProvider.

Para ejecutar comandos de manera asíncrona, es necesario que la cadena de conexión tenga establecida las claves Asynchronous Processing=true y async=true. De lo contrario se lanzará una excepción.

La clase SqlCommand expone los métodos BeginExecuteNonQuery, BeginExecuteReader que inician la ejecución asíncrona de un comando.

Para determinar el fin de la ejecución del comando, se debe recoger el valor IAsyncResult devuelto en el inicio de la ejecución. También se puede suscribir al evento StatementCompleted.

Más sobre ejecución asíncrona en: http://msdn2.microsoft.com/en-us/library/ms379553(VS.80).aspx.

Para la obtención de valores de columnas BLOB (Bynary Large Object) debe procederse igual que con columnas normales. Es decir, la conexión, el command y el dataReader se instancian y se inicializan de la misma manera. El tratamiento especial se aplicará en el momento de llamar al ExecuteReader, ya que debemos especificar el parámetro CommandBehavior.SequentialAccess. Para acceder a la columna que contiene el dato BLOB, el dataReader expone el método GetBytes(), en el que especificaremos el índice de la columna que necesitamos leer, el número de bytes y un array de bytes que será el buffer en el que se copiarán los datos. 

string providerName = ConfigurationManager.ConnectionStrings["Main"].ProviderName;
DbProviderFactory dbf = DbProviderFactories.GetFactory(providerName);
using (DbConnection cnn = dbf.CreateConnection())
{
    cnn.ConnectionString = ConfigurationManager.ConnectionStrings["Main"].ConnectionString;
    cnn.Open();
    using (DbCommand cmd = cnn.CreateCommand())
    {
        cmd.CommandText = "SELECT DocumentID, FileExtension, Document FROM Production.Document";
        // Hasta este punto, la codificación es exactamente la misma que para cualquier otro caso
        // La primera diferencia viene en la siguiente línea: 
        // No olvidar especificar el parámetro CommandBehavior.SequentialAccess
        using (DbDataReader dr = cmd.ExecuteReader(CommandBehavior.SequentialAccess))
        {
            while (dr.Read())
            {
                string documentId = Convert.ToString(dr["DocumentId"]);
                string fileExtension = (string)dr["FileExtension"];
                string fileName = string.Format("..\\..\\Documents\\{0}{1}", documentId, fileExtension);
                // escribir lo que haya en la columna con índice 2
                // en el archivo fileName
                SaveBlob(dr, 2, fileName);
            }
        }
    }
}

Para mantener apartadas las “complejidades” (tampoco es que sea demasiado complejo) hemos amontonado el código en la función SaveBlob().

private void SaveBlob(DbDataReader dr, int colIndex, string fileName)
{
    const int chunkSize = 1024; // (1Kb) número de bytes a leer en cada petición
    byte[] buffer = new byte[chunkSize];
    int pos = 0; // posición actual dentro del BLOB
    int readed = 0; // número de bytes leídos en el último acceso.

    using (FileStream fs = new FileStream(fileName, FileMode.Create))
    {
        // realizamos una primera lectura
        readed = (int)dr.GetBytes(colIndex, 0, buffer, 0, chunkSize);
        while (readed != 0) // mientras hayamos leído algo...
        {
            // escribimos lo leído én el archivo
            fs.Write(buffer, 0, readed);
            // indicamos la posición de inicio en la próxima lectura
            pos += readed;
            // Realizamos una nueva lectura.
            readed = (int)dr.GetBytes(colIndex, pos, buffer, 0, chunkSize);
        }
        // cerramos el archivo
        fs.Close();
    }
}

Para actualizar, procedemos de la misma manera. Hay que definir un parámetro del tipo adecuado, y al establecer su valor, asignar un array de bytes, que será el buffer usado en la lectura de BLOB.

string providerName = ConfigurationManager.ConnectionStrings["Main"].ProviderName;
DbProviderFactory dbf = DbProviderFactories.GetFactory(providerName);
using (DbConnection cnn = dbf.CreateConnection())
{
    cnn.ConnectionString = ConfigurationManager.ConnectionStrings["Main"].ConnectionString;
    cnn.Open();
    using (DbCommand cmd = cnn.CreateCommand())
    {
        // Generamos la consulta SQL con parámetros
        // También podríamos usar un Procedimiento Almacenado
        cmd.CommandText = "UPDATE Production.Document SET Document=@DocumentContent WHERE DocumentID=@DocumentId";
        // Creamos el objeto que contendrá información sobre el primer parámetro
        DbParameter IdParam = cmd.CreateParameter();
        IdParam.ParameterName = "@DocumentId";
        IdParam.DbType = DbType.Int16;
        IdParam.Direction = ParameterDirection.Input;
        cmd.Parameters.Add(IdParam);
        // Creamos el objeto que contendrá información sobre el segundo parámetro
        DbParameter documentParameter = cmd.CreateParameter();
        documentParameter.ParameterName = "@DocumentContent";
        documentParameter.DbType = DbType.Binary;
        documentParameter.Direction = ParameterDirection.Input;
        cmd.Parameters.Add(documentParameter);
        // Establecemos los valores de los parámetros
        IdParam.Value = 1;
        string fileName = string.Format("..\\..\\Documents\\{0}{1}", 1, ".doc");
        // La función devuelve el fichero en forma de array de bytes
        documentParameter.Value = GetBlob(fileName);
        // Ejecutar la consulta
        cmd.ExecuteNonQuery();
    }
}

La funcion GetBlob simplemente devuelve el fichero en forma de array de bytes:

private static byte[] GetBlob(string fileName)
{
    using (FileStream fs = new FileStream(fileName, FileMode.Open,  FileAccess.Read))
    {
        const int chunkSize = 1024;
        byte[] buffer = new byte[chunkSize];
        // Stream que usaremos para ir guardando el fichero en memoria
        MemoryStream outStream = new MemoryStream();
        // realizamos una primera lectura
        int readed = fs.Read(buffer, 0, chunkSize);
        while (readed != 0) // si hemos conseguido leer algo...
        {
            // guardamos lo leído en stream de salida
            outStream.Write(buffer, 0, readed);
            // volvemos a intentar una nueva lectura...
            readed = fs.Read(buffer, 0, chunkSize);
        }
        // cerramos el stream del fichero
        fs.Close();
        // devolvemos lo que haya en el stream de salida
        return outStream.ToArray();
    }
}

Crossposted from crisfervil.com

27/11/07

Aplicaciones XBAP

Archivado en: Uncategorized — crisfervil @ 12:08 pm

Pues resulta que no he podido resistirme a la descarga de la RTM de Visual Studio 2008, y bueno, entre las cosas que me gustaron me encontré las aplicaciones XBAP.
¿Y qué son estas famosas aplicaciones XBAP? pues son aplicaciones xaml que se ejecutan directamente  sobre el navegador (sólo IE7). Como silverlight, pero con la diferencia de que la aplicación sólo muestra xaml. Silverlight es un plug-in del navegador, al estilo flash, que muestra contenido xaml y puede alternarse con contenido tradicional; html, javascript, etc.

Y nada, me puse manos a la obra para realizar una primera prueba. Pero ando un poco verde de teoría.  Así que me dediqué a mirar las virguerías que hace la gente por allí.

Para encontrar este tipo de aplicaciones publicadas, basta con poner un filtro en google: filetype:xbap

Hay un montonazo de ejemplos. Ojo: sólo se pueden ver con IE7.

http://www.google.es/search?sourceid=navclient&hl=es&ie=UTF-8&rlz=1T4GGIH_esES214ES215&q=filetype%3axbap

Alguno de los que he visto:

Este tres en raya.

Demos de la suite de Infragistics. Impresionante!.

Este otro tres en raya, también muy bueno.

Una visita virtual de una ciudad, o algo así.

 

Actualización: Pues es que me he equivocado. Sin darme cuenta, he abierto una xbap desde FireFox y resulta que ha funcionado.  Ya no tengo claro bajo qué requisitos de Software funcionan estas aplicaciones.

image

Para que conste como prueba.

Crossposted from crisfervil.com

26/11/07

70-528. Resumen. Controles

Archivado en: Uncategorized — crisfervil @ 6:58 pm

Segunda parte del resumen sobre el temario para el examen de certificación 70-528 para desarrollo web.


CallBack: Llamada al servidor.

RoundTrip: Ciclo que comprende desde la llamada que realiza el cliente al servidor, hasta el proceso de la respuesta del servidor por parte del cliente.

VieState: Mecanismo que permite a las páginas asp.net mantener información de estado entre llamadas a las páginas.

Ciclo de vida de la página (orden de ejecución de los eventos)
LoadControlState->LoadViewState->LoadPostData->Load(OnLoad)->RaisePostDataChangedEvent(ejecución de eventos pendientes)->RaisePostBackEvent(ejecución del evento que ha disparado el postback)->PreRender(OnPreRender)->SaveControlState->SaveViewState->Render->Dispose->UnLoad

En cuanto a eventos propiamente dichos, he hecho la siguiente prueba, y este ha sido el resultado:

En una Página cualquiera: He creado funciones para cada uno de los eventos posibles de la clase SystemWeb.UI.Page con el formato Page_<NombreEvento> para los eventos siguientes:
Load
PreInit
PreLoad
PreRender
PreRenderComplete
LoadComplete
InitComplete
Init
SaveStateComplete
UnLoad

En el archivo aspx he puesto la directiva AutoEventWireup="true" para que el compilador entienda que las funciones que tengan firma de tipo EventHandler* y que sigan la directiva de nombrado Page_<NombreEvento> son en efecto, procedimientos de evento de la página.

* He probado crear las funciones sin parámetros y funciona igual. Luego he probado poner un parámetro object pepito y no ha funcionado. Finalmente he probado definir las funciones sin parámetros y con valor de retorno bool y tampoco ha funcionado. Lo que me induce a pensar que la directiva AutoEventWireup engancha a  las funciones que devuelvan void y no tengan parámetros, o tengan los parámetros de EventHandler.

public partial class _Default : System.Web.UI.Page
{
    private void Page_Load(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_Load");
    }
    private void Page_PreInit(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_PreInit");
    }
    private void Page_PreLoad(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_PreLoad");
    }
    private void Page_PreRender(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_PreRender");
    }
    private void Page_PreRenderComplete(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_PreRenderComplete");
    }
    private void Page_LoadComplete(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_LoadComplete");
    }
    private void Page_InitComplete(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_InitComplete");
    }
    private void Page_Init(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_Init");
    }
    private void Page_SaveStateComplete(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_SaveStateComplete");
    }
    private void Page_Unload(object sender, EventArgs e)
    {
        System.Diagnostics.Debug.WriteLine("Page_Unload");
    }
}

Al ejecutarlo, produce la siguiente salida en la ventana de deupuración:

Page_PreInit

Page_Init

Page_InitComplete

Page_PreLoad

Page_Load

Page_LoadComplete

Page_PreRender

Page_PreRenderComplete

Page_SaveStateComplete

Page_Unload

Que en efecto, es el orden de ejecución de los eventos de la página.

Menos InitComplete, LoadComplete, PreInit, PreRenderComplete y SaveStateComplete, los demás, son heredados de Control y de TemplateControl.

Dejo para más adelante una prueba de la ejecución de los eventos de la página y de sus controles componentes.

http://msdn2.microsoft.com/es-es/library/system.web.ui.page_events(VS.80).aspx

 

image 

Jerarquía de controles ASP.net

  • System.Web.UI.Control
    • WebControl
    • HtmlControl
    • TemplateControl
      • Page
    • LiteralControl
    • OtherSpecializedControls

Jerarquía de HTML Server Controls

image 

  • System.Web.UI.Control
    • System.Web.UI.HtmlControl
      • HtmlContainerControl
        • HtmlGenericControl
      • HtmlImage
      • HtmlInputControl
        • HtmlInputHidden
        • HtmlInputButton
          • HtmlInputSubmit
          • HtmlInputReset
        • HtmlInputText
      • HtmlLink

Los HTML Server control han sido creados principalmente para permitir una migración sencilla de aplicaciones HTML o ASP a ASP.net. La manera de realizar la migración es añadir el atributo runat="server" a cada control y eliminar el action del form.

Los HTML Server control deben estar ubicados dentro de la etiqueta form con runat="server". Sólo puede existir una etiqueta de este tipo en una página ASP.net.

La propiedad Visible de un HTML Server control indica si el control debe renderizarse o no. Si es false, no se renderiza. Este comportamiento quizá pueda inducir a la creencia errónea de que el control se renderiza con el atributo visible a false.

Hay que recordar que si el control no se renderiza, y existe código cliente (JavaScript, JScript, VBScript, etc.) que referencia a dicho control, es posible que falle.

Jerarquía de Web Server Controls

ClassDiagram1

  • System.Web.UI.Control
    • System.Web.UI.WebControl
      • Button
      • Label
      • CheckBox
      • TextBox
      • Image
      • BaseDataBoundControl
        • DataBoundControl
          • AdRotator
          • ListControl
            • DropDownList
            • ListBox
          • CompositeDataBoundControl
            • GridView

System.Web.HttpUtility.HtmlEncode : http://msdn2.microsoft.com/es-es/library/system.web.httputility.htmlencode(VS.80).aspx

System.Web.HttpUtility.Decode: http://msdn2.microsoft.com/es-es/library/system.web.httputility.htmldecode(VS.80).aspx

permiten el tratamiento de cadenas que contienen código html.

La propiedad DescriptionUrl del control Image, permite especificar un archivo html en el que insertar descripción detallada adicional al texto alternativo (propiedad AlternateText). Usar el método GenerateEmptyAlternateText para establecer el atributo alt="", lo que desde la perspectiva de la accesibilidad, indica que la imagen no contribuye al significado de la página, si no que más bien se usa con objeto de presentación (píxeles transparentes, bordes de contornos, etc.)

En las rutas de archivos o carpetas, la tilde (~) indica que la ruta es relativa al directorio de la aplicación.

Jerarquía de controles de acceso a datos de Asp.net:

  • System.Web.UI.Control
    • DataSourceControl (IDataSource, IListSource)
      • ObjectDataSource
      • SqlDataSource
        • AccessDataSource
    • HierarchicalDataSourceControl (IHierarchicalDataSource)
      • XmlDataSource (IDataSource, IListSource)
      • SiteMapDataSource (IDataSource, IListSource)

System.Web.UI.ListControl.AppendDataBoundItems indica si los elementos de la lista se borran antes del enlace a datos.

Controles Asp.net

LiteralControl: Sirve para mostrar contenido estático.

Table: Se renderiza como una tabla HTML. La gracia está en que esta tabla se puede generar dinámicamente desde el lado del servidor.

Image: Una imagen cualquiera.

ImageButton: Un botón que en lugar de mostrar el botón clásico, es una imagen.

ImageMap: Un control que permite trabajar con mapas de imágenes.

Calendar: Lo que me ha parecido más destacable de este control es que no sólo sirve para seleccionar fechas, sino también para mostrar elementos en un calendario. Permite selección de múltiples fechas.

FileUpload: Permite subir al servidor archivos sin un límite de tamaño máximo. El botón de selección de archivo no provoca PostBack.

Panel: Sirve para agrupar controles que se desean mostrar, ocultar o posicionar como uno solo. Se renderiza como un div.

MultiView: Permite manejar en un único control varias vistas (algo así como paneles). Varios conjuntos de controles que se mostrarán, ocultarán o posicionarán conjuntamente.

Wizard: Algo parecido al MultiView. Pensado para representar asistentes de varios pasos.

Controles Asp.net enlazados a datos

DataBindind con Templates: Algunos controles enlazados a datos permiten que la representación de los datos se realice mediante plantillas. Estas plantillas son las que se irán renderizando para cada uno de los datos del origen de datos al que se ha enlazado. El contenido de la plantilla puede ser HTML, controles asp.net, etc.

El mecanismo para indicar la inclusión de los valores del elemento mostrado viene proporcionado por la clase DataBinder. Esta clase posee el método estático Eval que es el que devuelve el valor de la propiedad o la columna correspondiente a elemento actual. La llamada a este método se realiza mediante la siguiente expresión (dentro de la plantilla):

<%# Bind("Id") %>

AccessDataSource: Permite a los controles enlazables a datos, obtenerlos desde una base de datos en un archivo mdb.

SqlDataSource: Permite acceder bases de datos ODBC, OLEDB, SQL Server, Oracle.

XmlDataSource: Permite acceder a fuentes de datos contenidas en archivos xml.

ObjectDataSource: Permite acceder a datos contenidos en objetos de negocio de la solución; en general cualquier objeto que implemente IEnumerable, IListSource, IDataSource, IHierarchicalDataSource. Permite acceder a objetos DataSet y DataTable.

SiteMapDataSource: Permite exponer el arbol de navegación del sitio, que debe estar definido en un xml con el formato adecuado.

ListControl: Clase abstracta que permite implementar listas de elementos dentro de las páginas. Heredan de esta clase BulletedList, DropDownList, RadioButtonList, CheckBoxList, ListBox.

BulletedListControl: Muestra una lista de elementos dentro de un párrafo señalado con viñetas y o números. Lo que viene siendo una lista identada.

AdRotator: Permite gestionar banners dentro de la página, indicando en un archivo xml las imágenes disponibles, así como la frecuencia con la que deseamos que se muestren.

Xml: Renderiza un documento xml dentro del documento html. Se le puede aplicar un archivo de transformación xslt y  en lugar de renderizar xml, renderiza lo que el xslt especifique.

GridView: El grid de toda la vida. Lo que no cocía de este control es que permite controlar el renderizado de cada celda, con lo cual podríamos pintar cualquier tipo de contenido.

DetailsView: Viene siendo lo contrario que el GridView. Si el GridView muestra listas de registros, DetailsView muestra registros independientes. En cuanto a funcionamiento y características, son bastante parecidas.

FormView: Al igual que DetailsView, muestra registros uno a uno, con la diferencia de que permite especificar mediante plantillas la manera en que se mostrarán los registros.

System.Web.UI.Controls.TreeView: Quizá la característica que más me ha llamado la atención sobre este control es que permite la carga de nodos de manera asíncrona. Ver http://msdn2.microsoft.com/en-us/library/system.web.ui.webcontrols.treeview.populatenodesfromclient.aspx

System.Web.UI.Controls.Menu: Al igual que TreeView, muestra registros de forma jerárquica. Al igual que TreeView, sólo enlaza a DataSources jerárquicos, que implementen IHierarchicalDataSource.

Crossposted from crisfervil.com

Entradas más antiguas »

Blog de WordPress.com.