domingo, 22 de septiembre de 2013

Regular Expressions (expresiones regulares) en PostgreSQL

Cuando las búsquedas con el operador Like no son lo suficientemente flexibles para localizar los registros que necesitamos, PostgreSQL permite las búsquedas de registos mediante el uso de regular expressions (expresiones regulares).

En este par de posts: Regular Expressions en PL/SQL Oracle, parte I y parte II coloque unos ejemplos de la utilización de expresiones regulares en Oracle.

Ahora utilizaré los mismos ejercicios de esas entradas, pero ahora para mostrar las regular expressions en PostgreSQL.

Creamos una tabla llamada membership:

Ahora insertamos algunos registros:

El operador ~

Como primer ejemplo utilizamos el operador ~ (LIKE versión regular expression) en la consulta para buscar en la columna MEMBERSHIP_LASTNAME aquellos registros que empiecen con la letra D.

Aquí un segundo ejemplo en donde buscamos en la columna MEMBERSHIP_LASTNAME aquellos registros que comiencen con las letras de la A a la F.

El operador inverso !

Ahora mostraré los ejemplos anteriores utilizando el operador inverso ! o la negación del operador ~ para el primer ejemplo, esta consulta busca en la columna MEMBERSHIP_LASTNAME aquellos registros que no empiecen con la letra D.

El mismo operador de negación para el segundo ejemplo, esta consulta busca en la columna MEMBERSHIP_LASTNAME aquellos registros que no empiecen con las letras de la A a la F.

El operador case-insensitive *

Tanto ~ como ! son case-sensitive, si necesitas que ambos operadores sean case-insensitive se combinan con el operador * , como en el ejemplo siguiente donde buscamos en la columna MEMBERSHIP_LASTNAME aquellos registros que después del primer caracter tenga la letra O mayúscula repetida una o más veces.

Esta búsqueda no dio resultados porque excepto la primera letra de los registros en la columna MEMBERSHIP_LASTNAME el resto se encuentran en letras minúsculas, entonces para que la consulta encuentre resultados utilizamos el operador *.

Ahora si la consulta anterior mostro los resultados.

viernes, 20 de septiembre de 2013

Regular Expressions en PL/SQL Oracle, parte II

En este post anterior mostré ejemplos de búsqueda con las funciones regexp_like y regexp_instr.

A continuación mostrare ejemplos de las funciones regexp_substr y regexp_replace respectivamente.

Como primer ejemplo de la función regexp_substr, obtendré dos subcadenas (substring), una de la columna MEMBERSHIP_NAME donde el carácter a se repita dos veces en cada registro y otra de la columna MEMBERSHIP_DUE en donde la cantidad comience con un dígito del 1 al 6 y después uno o más dígitos que se repitan.

Como segundo ejemplo de la función regexp_substr obtendré un substring de la columna MEMBERSHIP_DATE cuyos registros terminen con los dígitos del 1 al 6 repetidos una o dos veces.

Como primer ejemplo de la función regexp_replace buscaré dentro de la concatenación de las columnas MEMBERSHIP_NAME y MEMBERSHIP_LASTNAME los registros que tengan un carácter o repetido a partir de la posición 4 y lo sustituiré por la cadena (**found**).

Como último ejemplo ejecutamos la función regexp_replace en un texto y reemplazamos la palabra ‘fox’ por la palabra ‘SUPER CAT’ a partir de la posición 1.

lunes, 16 de septiembre de 2013

Regular Expressions en PL/SQL Oracle, parte I

Una expresión regular (regular expression) es un conjunto de caracteres (signos) conocido como patrón que al buscarse coincide una o más veces en una cantidad considerable de texto, estos patrones se construyen con una notación de caracteres ordinarios y metacaracteres, los cuales tienen un significado especial dentro de la expresión regular e indican las reglas a las que deben someterse los caracteres ordinarios para su interpretación estos bloques básicos de construcción son similares a una expresión algebraica o a un mini lenguaje de programación.

A continuación algunos de los metacaracteres y su significado:

  • ^ coincide el patrón de búsqueda al inicio de una línea.
  • $ coincide el patrón de búsqueda al final de una línea.
  • . coincide cualquier caracter en cualquier lugar.
  • [] especifica un rango de caracteres
  • ? ubica un caracter opcional.
  • + ubica uno o más caracteres.
  • - ubica cero o más caracteres.
  • {n} ubica un caracter que aparece n veces.
  • {n,} ubica un caracter que aparece n o más veces.
  • {n,m}ubica un caracter que aparece de n a m veces.
  • | disyunción o sea un or lógico entre caracteres.

Las expresiones regulares son ampliamente utilizadas en Linux o en otros lenguajes Open Source, además de plataformas como Java y.NET y en bases de datos como PostgreSQL y Oracle. En Oracle las expresiones regulares son utilizadas cada vez que necesites operaciones de búsqueda demasiado complicadas en donde los comandos SELECT y LIKE no sean suficientes. Oracle tiene las siguientes cuatro funciones para su utilización:

  • REGEXP_LIKE: es la versión de expresiones regulares del comando LIKE. Una función booleana que regresa TRUE,FALSE o NULL si en el texto existe una coincidencia con la expresión regular.
  • REGEXP_INSTR: esta función regresa la posición del caracter en el texto donde se encontró una coincidencia con la expresión regular.
  • REGEXP_SUBSTR: extrae una coincidencia de texto encontrada con la expresión regular.
  • REGEXP_REPLACE: ejecuta una operación de búsqueda y reemplazo si se encuentra una coincidencia en el texto.

Como ejemplo de su uso, creamos la siguiente tabla:

Después insertamos los siguientes registros para comenzar a utilizar las funciones.

Elizabeth  Bishop 36736-36738 976.063 02/08/1911
Charles Dickens 36734-5461 2244.789 07/02/1812
Jack London 5462-37314 898.127 12/01/1876
Joseph Conrad 37315-5463 1193.493 03/12/1857
Gustave Flaubert 37313-37316 1435.384 12/12/1821
John Milton 37317-37296 1348.582 09/12/1608
Samuel Taylor 37292-37318 207.449 21/10/1772
Virginia Wolf 37061-106 2077.947 25/01/1882
Walter  Scott 37319-37320 412.72 15/08/1771
Robert Louis  Stevenson 37945-37946 1033.54 13/11/1850
Joseph Rudyard  Kipling 37947-12556 382.41 30/12/1865
Arthur Conan Doyle 12557-10964 1844.945 22/05/1859
George  Orwell 54722-3236 2139.874 25/01/1903

Como primer ejemplo utilizamos la función REGEXP_LIKE para obtener de la columna MEMBERSHIP_LASTNAME los regitros que comienzan con la letra D.La consulta es:

Ahora utilizamos la función REGEXP_LIKE para obtener de la columna MEMBERSHIP_DUE los regitros que terminan con el número 3.La consulta es:

Por último, utilizamos la función REGEXP_LIKE para obtener de la columna MEMBERSHIP_LASTNAME los regitros que tengan las letras de la A a la F. La consulta es:

Ahora ejemplos con la función REGEXP_INSTR. En el primer ejemplo buscamos los registros que en la columna MEMBERSHIP_NAME tengan de 1 a 2 veces la letra A y cuyo posición de coincidencia del texto sea mayor a 0.

En este segundo ejemplo con REGEXP_INSTR buscamos todos los registros que en la columna MEMBERSHIP_NAME comiencen con la letra J o la letra E.

Un último ejemplo con REGEXP_INSTR buscamos todos los registros que en la columna MEMBERSHIP_DATE terminen en el penúltimo dígito del 0 al 9 y en el último dígito del 1 al 2.

domingo, 25 de agosto de 2013

Obtener el primer día del mes en curso con una función first_day() con PL/SQL en Oracle.

En PL/SQL existe la función LAST_DAY() con la que se obtiene el último día del mes en curso, entonces si tenemos que cumplir un requerimiento en donde necesitamos un campo o una variable con el último día del mes en curso simplemente ejecutamos esta función:

Ahora bien, si el requerimiento a cumplir se trata de obtener el primer día del mes en curso, no existe en PL/SQL Oracle una función predeterminada, por lo que para llegar a ese resultado basta con restarle un mes a la fecha en curso con la función ADD_MONTHS(), le aplicamos la función LAST_DAY() para obtener el último día del mes anterior y sumarle un día.

Aquí otra forma alterna de llegar al mismo resultado.

lunes, 19 de agosto de 2013

ADO.NET Helper para Oracle (Update)

Todos los programas que utilizan las clases e interfaces del proveedor predeterminado de ADO.NET para Oracle que se encuentran en el ensamblado System.Data.Oraclient deben actualizarse por las clases e interfaces que se encuentran en el ensamblado Oracle.DataAccess.Client, que es el proveedor nativo de Oracle para ADO.NET, este ensamblado se descarga del siguiente enlace Oracle Data Provider for .NET

A continuación el código actualizado de las clases Helper de ADO.NET que publique en este post.

El código de la clase OracleDataBase

El código de la clase OracleDataBaseCommand

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using Oracle.DataAccess.Client;

namespace OracleHelper
{
    internal sealed class OracleDataBaseCommand
    {

internal static int Insert(string commandText, Dictionary parameters, 
System.Data.CommandType cmdtype)
{
            
int resp = 0;
try
{
    using (OracleConnection conn = OracleDataBase.GetInstance().GetConnection())
    {
        using (OracleCommand cmd = new OracleCommand(commandText, conn))
        {
            cmd.CommandType = cmdtype;
            if (parameters != null)
            {
                foreach (KeyValuePair pair in parameters)
                {
                    cmd.Parameters.Add(new OracleParameter(pair.Key, pair.Value));
                }
            }
            resp = cmd.ExecuteNonQuery();
        }
    }
}
catch (OracleException ex)
{
    Logger.LogWriteError(ex.Message);
    throw ex;
}
return resp;
}
internal static int Update(string commandText, Dictionary parameters, 
System.Data.CommandType cmdtype)
{
            
try
{
    return Insert(commandText, parameters, cmdtype);
}
catch (OracleException ex)
{
    Logger.LogWriteError(ex.Message);
    throw ex;
}
}
internal static OracleDataReader GetReader(string commandText, Dictionary parameters,
System.Data.CommandType cmdtype)
{
OracleDataReader reader = null;
try
{
    OracleConnection conn = OracleDataBase.GetInstance().GetConnection();
    using (OracleCommand cmd = new OracleCommand(commandText, conn)) {
        if (parameters != null)
        {
            foreach (KeyValuePair pair in parameters)
            {
                cmd.Parameters.Add(new OracleParameter(pair.Key, pair.Value));
            }
        }
        reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
    }
}
catch (Exception ex) {
    Logger.LogWriteError(ex.Message);
    throw ex;
}
return reader;
}

internal OracleDataReader GetParameterizedReader(string commandText, OracleParameter[] parameters, 
System.Data.CommandType cmdtype)
{
OracleDataReader reader = null;
OracleConnection conn = OracleDataBase.GetInstance().GetConnection();
using (OracleCommand cmd = new OracleCommand(commandText, conn))
{
    cmd.CommandType = cmdtype;
    if (parameters != null)
        cmd.Parameters.AddRange(parameters);
    reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
}
return reader;
}
}
}

Como ejemplo del uso del helper, tenemos el código de la clase CustomerDAC cuya funcionalidad es tener los métodos CRUD para una entidad Customer.

El código de la clase Customer

Para finalizar el código de una aplicación de consola que muestra el uso de la clase CustomerDAC.

miércoles, 14 de agosto de 2013

ADO.NET Helper para Oracle

Hace bastante tiempo que hice unas clases de ADO.NET para Oracle (un tipo helper de manera elemental). quizá no es la manera más optima de acceder a una base de datos, pero al menos en ambientes restringidos en donde no es posible instalar frameworks u otro proveedor de ADO.NET para Oracle que no sea el que viene predeterminado por .NET.

Aquí esta la clase para manejar la conexión, se llama OracleDataBase

Fig 1. Clase para manejar la conexion a la base de datos.

Utilizo otra clase llamada OracleDataBaseCommand para auxiliarme con los comandos.
Fig 2. Clase auxiliar para ejecutar los comandos en la base de datos.

Su utilización dentro de una clase que sirva para persistir o extraer datos sería de la siguiente manera.

Fig 3. Clase que utiliza las clases auxiliares para guardar un objeto.

Fig 4. Clase principal que crea, actualiza y consulta un cliente en la base de datos.

viernes, 2 de agosto de 2013

Entendiendo Unboxing en C# con GTK#

Es la operación en donde un tipo por referencia se convierte a un tipo simple se denomina Unboxing, esta conversión al igual que el Boxing puede ser explícita o implícita según se requiera.

A diferencia del Boxing el Unboxing requiere muchos menos recursos ya que no requiere la reserva de memoria adicional porque solamente se obtiene un apuntador hacia el tipo simple contenido dentro del objeto.

Internamente el proceso de Unboxing lleva los siguientes pasos:

  1. Se comprueba si existe una referencia hacia un objeto creado mediante Boxing.
  2. Si esto es verdadero se obtiene un apuntador para el tipo simple contenido dentro del objeto.
  3. Se le copia el valor del objeto al tipo simple también se copian los campos del objeto del managed heap al stack.
  4. Si no existe una referencia al objeto se lanza una excepción del tipo NullReferenceException.
  5. Si el tipo simple a la que se le asigna el valor del objeto no tiene la suficiente longitud para almacenar el valor del objeto se lanza una excepción del tipo InvalidCastException.

Como ejemplo del uso de esta técnica el siguiente programa GTK# solicita dos números de doble precisión y determina cuál es el mayor de ellos.

Al ejecutar el programa se muestra la siguiente ventana:

Al ingresar ambos valores y pulsar el botón "Ok" el programa comparará ambos números.

En el metódo HandleBtnOkClicked se encuentra toda la funcionalidad, es aquí donde utilizamos la técnica de Unboxing

void HandleBtnOkClicked (object sender, EventArgs e)
{
 lblError.Text = string.Empty;
 try
 {
 double x,y,temp;
 x = Double.Parse (txtNumber1.Text);//Unboxing string to double
 y = Convert.ToDouble(txtNumber2.Text);//Unboxing 
 if(y > x)
 {
  temp = x;
  x = y;
  y = temp;
 }
 lblMsg.Text = x.ToString() + " greater than " + y.ToString();//Boxing
}catch(FormatException ex){
 lblMsg.Text = string.Empty;
 lblError.Text = ex.Message;    
}
}

En la siguientes líneas, de un tipo por referencia como la propiedad Text del Widget Entry, lo convertimos a un tipo por valor:

 double x,y,temp;
 x = Double.Parse (txtNumber1.Text);//Unboxing string to double
 y = Convert.ToDouble(txtNumber2.Text);//Unboxing 

Es una buena práctica utilizar el código que realiza el Unboxing dentro de un bloque try/catch, esto para manejar la excepción en caso de que el valor del tipo por referencia (objeto) no sea del mismo tipo que el tipo simple que lo almacenará.

jueves, 1 de agosto de 2013

Entendiendo Boxing en C# con GTK#

Todos los tipos de datos en C# heredan de la clase base Object, tanto los tipos de datos por valor (ejem: primitivos) como los tipos por referencia (ejem:clases), esta herencia permite convertir un tipo de dato por valor a uno por referencia y a la inversa, a estas operaciones se les conoce como Boxing y Unboxing.

Estas operaciones son importantes dentro del sistema de tipos de C# ya que unifican la forma de trabajar con ellos, puesto que será posible invocar a métodos comunes en cada uno de ellos sin importar su tipo.

El siguiente programa GTK# muestra estos conceptos, el programa emula a un sencillo administrador de cuenta bancaria solicita una cantidad para deposito o retiro y según el tipo de movimiento pone un signo de `+’ si es deposito y un signo de ‘-‘si es retiro.

El programa consta de una estructura MovementItem y de la clase GtkBoxingSample.

El código fuente de la estructura MovementItem.

Listado 1. Clase MovementItem en C#.

El código fuente de la clase GtkBoxingSample.

Listado 2. Clase GtkBoxingSample para mostrar boxing en C#.

Al ejecutar el programa se verá la siguiente ventana con un saldo inicial de 1000

Al ingresar un cantidad y dar click en el botón "Deposit", se mostrará el movimiento con un signo "+" sumando el monto al saldo.

Lo mismo sucederá con el botón "WithDraw", solo que se muestra el movimiento con un signo "-" restando el monto al saldo.

Se usan las técnicas de Boxing/Unboxing en el método ChangeBalance

Listado 3. Boxing/unboxing en C#.

En .NET toda estructura o tipo simple es un tipo por valor y como la propiedad Text del Widget Entry admite únicamente el tipo String que es un tipo por referencia, por lo que para mostrar la cantidad en el campo de texto Balance, utilizamos el Boxing como en la siguiente línea:

txtBalance.Text = Convert.ToString(balance); //boxing

Después para agregar un item en el ListStore utilizamos el método AppendValues(), el cuál recibe como argumento un arreglo de objetos, nuevamente tenemos que utilizar el Boxing para cada que cada tipo simple de la estructura se convierta en un tipo por referencia, esto se hace con la siguiente línea:

_store.AppendValues (item.Date.ToString("dd/MM/yyyy"), //boxing date to string
                Convert.ToString(item.Sign), //boxing char to string
                item.Amount.ToString()); //boxing double to string

Internamente el proceso de Boxing lleva los siguientes pasos:

  1. Se reserva una cantidad de memoria adicional al tipo de variable para las operaciones adicionales que diferencian a aun objeto de un tipo simple.
  2. Se crea una variable de objeto temporal en donde se almacenará el valor de la variable.
  3. Se copia el valor de la variable al valor del objeto creado.
  4. Se regresa la dirección del objeto ya que en este momento el tipo por valor es un tipo por referencia.

El Boxing puede ser llevado a cabo de forma implícita por ejemplo:

item.Date.ToString("dd/MM/yyyy");
O de manera explícita, por ejemplo:
object objAmount = amount;
objAmount.ToString();

El proceso de Boxing es costoso ya que requiere de una memoria adicional que afecta al performance de la aplicación, además ocasiona que el código IL producido sea mucho mayor.

jueves, 11 de julio de 2013

Entendiendo Nullable Types con C#

Existen dos tipos de datos en .NET: los tipos por referencia (reference types) y los tipos por valor (value types), como ejemplo de tipos por valor encontramos a las estructuras (struct), los enumerados (enum) y los todos los tipos simples como son los caracteres (char) y los tipos númericos (double,int,float,short,long), como ejemplo de tipos de referencia tenemos a la clase cadena (string), la clase object y la clases definidas por el usuario (user defined classes).

Una de las diferencias entre ellos es que los tipos por valor siempre deben tener un valor y nunca pueden contener un valor nulo (null), mientras que para los tipos por referencia siempre tienen el valor null al crearse o bien se les puede asignar durante la ejecución del programa.

El problema se presenta cuando se trabaja con fuentes de persistencia y se necesita asignar el valor de un tipo de dato de estas fuentes hacia un tipo de dato simple de C#, por ejemplo en un archivo XML o en una base de datos un valor entero puede representarse como vacío o null, entonces no es posible representar este valor en un tipo simple de C# ya que en el CLR un tipo por valor no puede ser nunca nulo.

Desde la versión 2.0 de .NET es posible trabajar con nullable types en donde los tipos simples tienen la capacidad de tener valores nulos, esto se logra agregándoles el modificador (?) al tipo de dato, así por ejemplo las siguientes variables además de ser del tipo simple son Nullable Types.

int? a = null;
boolean b? = null;

A estos tipos también se les puede aplicar el operador (??) llamado null coalescing operator (operador de coalescencia) el cuál le aplica la comparativa al valor, si este es nulo entonces le asigna un valor predeterminado, por ejemplo si tienes un arreglo de objetos de tipo factura y necesitas que ciertas propiedades tengan al menos un valor predeterminado, aplicamos la comparativa utilizando el operador (??) para las que las propiedades con valores nulos tengan un valor predeterminado, esto se muestra en el siguiente programa:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestCoalescing
{
public class Invoice
{
public int InvoiceID { set; get; }
public DateTime? StartDate { set; get; }
public string MeasureUnit { set; get; }
public Decimal? TaxPercent { set; get; }
}

class Program
{
static void Main(string[] args)
{
Invoice[] invoices = { new Invoice {
  InvoiceID = 1,
  StartDate = new DateTime(1999,06,10),
  MeasureUnit = "Box"
 },
 new Invoice {
  InvoiceID = 2,
  StartDate = new DateTime(2012,06,06),
  TaxPercent = 13.00M
 },
    new Invoice {
        InvoiceID = 3
    }
};
foreach (Invoice i in invoices)
{
    PrintInvoice(i);
}
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}

static void PrintInvoice(Invoice invoice)
{
Console.WriteLine("InvoiceID: {0}", invoice.InvoiceID);
Console.WriteLine("Start Date: {0}", invoice.StartDate ?? DateTime.Today);
Console.WriteLine("Measure Unit: {0}", invoice.MeasureUnit ?? "NONE");
Console.WriteLine("Tax percent: {0}", invoice.TaxPercent ?? 16.00M);
Console.WriteLine("----+----------+-----");
}

}
}

Dentro del método PrintInvoice se muestra la utilización del operador (??), para tres propiedades del objeto factura:

Console.WriteLine("Start Date: {0}", invoice.StartDate ?? DateTime.Today);
Console.WriteLine("Measure Unit: {0}", invoice.MeasureUnit ?? "NONE");
Console.WriteLine("Tax percent: {0}", invoice.TaxPercent ?? 16.00M);

Este operador no solo trabaja con nullable types sino también con tipos por referencia como es el caso de la propiedad MeasureUnit la cual es del tipo String un tipo por referencia, como en el siguiente fragmento de código:

Console.WriteLine("Measure Unit: {0}",invoice.MeasureUnit ?? "NONE");

Los nullable types en realidad corresponden a una estructura genérica del CLR llamada System.Nullable

miércoles, 5 de junio de 2013

Utilizando el control RadNumericTextBox de Telerik

El RadNumericTextBox es uno de los controles que pertenecen al conjunto  de controles RadInput de telerik, estos controles además de permitir al usuario ingresar y editar información textual como si fuera un control TextBox ASP .NET tradicional,  poseen propiedades adicionales como: eventos del lado del cliente, marca de agua, botón para aceptar, botones para selección de rango, mensaje predeterminado, tooltip, entre otras que extienden la funcionalidad básica proporcionada por el control TextBox ASP.NET estándar.

La funcionalidad principal del RadNumericTextBox es filtrar la entrada de datos permitiendo únicamente datos numéricos sin necesidad de programación Javascript o el uso de validators ASP .NET, este control además puede configurarse para utilizar determinados formatos o configuraciones regionales. Entre las propiedades de configuración se encuentran:

  • Type Indica el formato del texto, Number para formato númerico, Currency para formato moneda, Percent para un formato porcentaje.
  • Culture Establece el soporte para la globalización y localización del valor.
  • MinValue y MaxValue Indican los valores minimos y maximos de un rango de valores.
  • ShowSpinButtons Estable la propiedad para mostrar un control de selección de flechas para el incremento o decremento del valor.

Como ejemplo del uso de este control escribí una página ASP.NET que calcula los montos de un producto deacuerdo a su cantidad y su precio, mostrando caracteristicas utiles del RadNumericTextBox como la globalización y algunas funciones del lado del cliente. (client-side)

El código de la página ASP.NET
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="Default.aspx.cs"
Inherits="TestRadNumericTextBox._Default" %>
<%@ Register Assembly="Telerik.Web.UI"
Namespace="Telerik.Web.UI" TagPrefix="telerik" %>
<!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>Test RadNumeric Text Box</title>
</head>
<body>
<form id="form1" runat="server">
<telerik:RadScriptManager ID="RadScriptManager1"
runat="server">
<Scripts>
<asp:ScriptReference Assembly="Telerik.Web.UI"
Name="Telerik.Web.UI.Common.Core.js">
</asp:ScriptReference>
<asp:ScriptReference Assembly="Telerik.Web.UI"
Name="Telerik.Web.UI.Common.jQuery.js">
</asp:ScriptReference>
<asp:ScriptReference Assembly="Telerik.Web.UI"
Name="Telerik.Web.UI.Common.jQueryInclude.js">
</asp:ScriptReference>
</Scripts>
</telerik:RadScriptManager>
<telerik:RadScriptBlock ID="RadScriptBlock1"
runat="server">
<script type="text/javascript">
function Calculate() {
var ctrlQuantity = $find("txtQuantity");
var ctrlPrice = $find("txtPrice");
var ctrlSubtotal = $find("txtSubtotal");
var ctrlTaxPercent = $find("txtTax");
var ctrlTaxAmount = $find("txtTaxAmount");
var ctrlTotal = $find("txtTotal");
var quantity = parseInt(ctrlQuantity.get_value());
var price = parseFloat(ctrlPrice.get_value());
var subtotal = quantity * price;
ctrlSubtotal.set_value(subtotal);
var percent = parseFloat(ctrlTaxPercent.get_value()) * .01;
var taxAmount = subtotal * percent;
ctrlTaxAmount.set_value(taxAmount);
var total = subtotal + taxAmount;
ctrlTotal.set_value(total);
}
</script>
</telerik:RadScriptBlock>
<div>
<telerik:RadComboBox ID="cmbCultureCurrency"
runat="server"
onselectedindexchanged="cmbCultureCurrency_SelectedIndexChanged"
EmptyMessage="Select your country"
AutoPostBack="true">
<Items>
<telerik:RadComboBoxItem Text="Brazil"
Value="pt-BR" />
<telerik:RadComboBoxItem Text="United Kingdom"
Value="en-GB" />
<telerik:RadComboBoxItem Text="United States"
Value="en-US" />
<telerik:RadComboBoxItem Text="Spain"
Value="es-ES"/>
</Items>
</telerik:RadComboBox>
</div>
<br />
<table>
<tr>
<td>Product</td>
<td>
<telerik:RadComboBox ID="cmbProduct"
EmptyMessage="Select a product"
runat="server">
<Items>
<telerik:RadComboBoxItem Value="1"
Text="Potatoes" />
<telerik:RadComboBoxItem Value="2"
Text="Beans" />
<telerik:RadComboBoxItem Value="3"
Text="Apples" />
<telerik:RadComboBoxItem Value="4"
Text="Oranges" />
</Items>
</telerik:RadComboBox>
</td>
</tr>
<tr>
<td>Quantity</td>
<td>
<telerik:RadNumericTextBox ID="txtQuantity"
Runat="server"
Type="Number"
DataType="System.Uint32">
<NumberFormat ZeroPattern="n"></NumberFormat>
</telerik:RadNumericTextBox>
</td>
</tr>
<tr>
<td>Tax</td>
<td>
<telerik:RadNumericTextBox ID="txtTax"
Runat="server"
Type="Percent"
MinValue="1"
MaxValue="20"
Value="16"
ShowSpinButtons="true">
<NumberFormat ZeroPattern="n %"></NumberFormat>
</telerik:RadNumericTextBox>
</td>
</tr>
<tr>
<td>Price</td>
<td>
<telerik:RadNumericTextBox ID="txtPrice"
Runat="server"
Type="Currency"
ClientEvents-OnBlur="Calculate">
</telerik:RadNumericTextBox>
</td>
</tr>

<tr>
<td>Subtotal</td>
<td>
<telerik:RadNumericTextBox ID="txtSubtotal"
Runat="server"
Type="Currency"
DataType="System.Decimal"
ReadOnly="true">
<NumberFormat ZeroPattern="$n"></NumberFormat>
</telerik:RadNumericTextBox>
</td>
</tr>
<tr>
<td>Tax amount</td>
<td>
<telerik:RadNumericTextBox ID="txtTaxAmount"
runat="server"
ReadOnly="true">
</telerik:RadNumericTextBox>
</td>
</tr>
<tr>
<td>Total</td>
<td>
<telerik:RadNumericTextBox ID="txtTotal"
Runat="server"
Type="Currency"
DataType="System.Decimal"
ReadOnly="true">
</telerik:RadNumericTextBox>
</td>
</tr>
</table>
<div>Selected currency: <asp:Label ID="lblSelectedCurrency"
runat="server"></asp:Label></div>
</form>
</body>
</html>
El código de la clase CodeBehind
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Globalization;

namespace TestRadNumericTextBox
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{

}

protected void cmbCultureCurrency_SelectedIndexChanged(object sender, 
Telerik.Web.UI.RadComboBoxSelectedIndexChangedEventArgs e)
{
CultureInfo info = null;
if (string.IsNullOrEmpty(cmbCultureCurrency.SelectedValue))
    info = System.Threading.Thread.CurrentThread.CurrentCulture;
else
    {
    info = new CultureInfo(cmbCultureCurrency.SelectedValue);
    lblSelectedCurrency.Text = cmbCultureCurrency.SelectedValue;
    txtQuantity.Culture =
    txtPrice.Culture =
    txtTaxAmount.Culture =
    txtSubtotal.Culture =
    txtTaxAmount.Culture =
    txtTotal.Culture = info;
    }
}
}
}
        

En la página ASP.NET, en el bloque JavaScript, dentro de la función Calculate() obtengo la referencia a los controles mediante la función $find(“[nombre del control]”) después utilizo las funciones get_value() y set_value() para obtener o establecer el valor. Estas funciones son del lado del cliente, porque del lado del server se hace con la propiedad Text.

function Calculate() {
var ctrlQuantity = $find("txtQuantity");
var ctrlPrice = $find("txtPrice");
var ctrlSubtotal = $find("txtSubtotal");
var ctrlTaxPercent = $find("txtTax");
var ctrlTaxAmount = $find("txtTaxAmount");
var ctrlTotal = $find("txtTotal");
var quantity = parseInt(ctrlQuantity.get_value());
var price = parseFloat(ctrlPrice.get_value());
var subtotal = quantity * price;
ctrlSubtotal.set_value(subtotal);
var percent = parseFloat(ctrlTaxPercent.get_value()) * .01;
var taxAmount = subtotal * percent;
ctrlTaxAmount.set_value(taxAmount);
var total = subtotal + taxAmount;
ctrlTotal.set_value(total);
}

En el control txtQuantity utilizo la propiedad type=”Number” lo que le da el formato numérico al valor

<telerik:RadNumericTextBox ID="txtQuantity"
Runat="server"
Type="Number"
DataType="System.Uint32">
<NumberFormat ZeroPattern="n"></NumberFormat>
</telerik:RadNumericTextBox>

El control txtTax demuestra el uso de la propiedad type=”percent”, las propiedades para el valor mínimo (MinValue), valor máximo (MaxValue) y la propiedad ShowSpinButtons.

<telerik:RadNumericTextBox ID="txtTax"
Runat="server"
Type="Percent"
MinValue="1"
MaxValue="20"
Value="16"
ShowSpinButtons="true">
<NumberFormat ZeroPattern="n %"></NumberFormat>
</telerik:RadNumericTextBox>

Los controles txtSubtotal, txtTotal y txtPrice muestran el uso de la propiedad type=Currency para dar formato de tipo moneda al valor, además en el control txtPrice utilizo el evento ClientEvents-OnBlur del lado del cliente para invocar al método Calculate definido en el bloque JavaScript.

<telerik:RadNumericTextBox ID="txtPrice"
Runat="server"
Type="Currency"
ClientEvents-OnBlur="Calculate">
</telerik:RadNumericTextBox>

Por último en el code-behind utilizo la propiedad Culture para establecer el formato en los controles RadNumericTextBox según las preferencias de la cultura seleccionada.

Al ejecutar la página ASP.NET observaras los siguientes resultados,  si el país es  Brazil, las cantidades se formatean deacuerdo a la configuración de ese país.

Si el páis seleccionado es United Kingdom, lo mismo sucede con las cantidades y con las monedas en este caso aparece el símbolo de la libra (pound).

Si el país es United States,  en las cantidades aparece el símbolo del Dollar.

Y por último si la opción del país es Spain, aparece el símbolo del Euro.

viernes, 31 de mayo de 2013

Utilizando localización y globalización (localization and globalization) en .NET

La globalización y la localización (localization and globalization) son características que permiten que las aplicaciones .NET se adapten a múltiples idiomas y culturas utilizando las diferentes configuraciones regionales de cada uno de los equipos en donde se ejecutan, con el objetivo de que los usuarios visualicen de forma correcta los datos mostrados en la interfaz de usuario de acuerdo con sus preferencias locales.

El mundo se encuentra dividido en múltiples regiones y culturas, una cultura (cultura) en el contexto de .NET se refiere al idioma y al conjunto de preferencias regionales para calendarios, cadenas, números y monedas.

.NET cumple con el estándar RFC 1766 del IETF para la identificación de culturas mediante un identificador que se construye con las siguientes reglas: a) Existen códigos de idiomas o culturas neutrales que se representan por letras minúsculas como ejemplo la siguiente tabla:

Código Idioma
en Inglés
de Alemán
es Español
fr Francés

b) Además de los códigos de idiomas existen códigos de subculturas específicas que corresponden a una región geográfica, estas subculturas se representan en letras mayúsculas como ejemplo la siguiente tabla:

Subcultura Región
GB Great Britain
CA Canadá
MX México
BR Brasil

Los códigos de los idiomas (a) se combinan con los códigos de las regiones (b) asociándolos mediante un (-) entre ellos por lo que un mismo lenguaje puede ser hablado en diferentes regiones así por ejemplo: Español en España (es-ES), Español en México (es-MX), Español en Argentina (es-Ar) o Inglés en UK (en-UK), Inglés en USA (en-USA), Inglés en Canadá (en-CA) por mencionar algunos casos. Uno de los primeros pasos para implementar la localización y globalización en las aplicaciones .NET es el uso de la clase CultureInfo la cual encapsula información referente a una cultura específica, información acerca de su idioma, el calendario, los formatos de números y monedas, la dirección del texto entre otros valores. Como ejemplo escribí la rutina de un programa de nómina para una compañía que tiene sucursales en USA, UK, Canadá y Brasil, este programa calcula el salario bruto y el pago neto de un empleado de acuerdo a su tarifa por hora, también calcula el número de horas trabajadas y la retención del impuesto correspondiente. El código a continuación:

using System;
using System.Globalization;
using System.Threading;

namespace TestGlobalization
{
class Program
{
public static void Main(string[] args)
{
try
{
    DateTime startDate, finishDate;
    TimeSpan workingTime;
    int workingDays, taxRate;
    Decimal salaryRate, netIncome, grossSalary, taxDeduction;
    CultureInfo ci = null;
    Console.WriteLine("Please select your country:");
    Console.WriteLine("\n");
    Console.WriteLine("a) USA \tb) Canada");
    Console.WriteLine("c) UK  \td) Brasil");
    Console.WriteLine("\n");
    ConsoleKeyInfo option = Console.ReadKey(true);
    switch (option.Key) { 
        case ConsoleKey.A:
            ci = new CultureInfo("en-US");
            break;
        case ConsoleKey.B:
            ci = new CultureInfo("fr-CA");
            break;
        case ConsoleKey.C:
            ci = new CultureInfo("en-GB");
            break;
        case ConsoleKey.D:
            ci = new CultureInfo("pt-BR");
            break;
        default:
            ci = Thread.CurrentThread.CurrentCulture;
            break;
    }
    Thread.CurrentThread.CurrentCulture = ci;
    Console.Write("Start date: ");
    startDate = Convert.ToDateTime(Console.ReadLine());
    Console.Write("Finish date: ");
    finishDate = Convert.ToDateTime(Console.ReadLine());
    Console.Write("Salary rate: ");
    salaryRate = Convert.ToDecimal(Console.ReadLine());
    Console.Write("Tax rate %: ");
    taxRate = Convert.ToInt32(Console.ReadLine());
    workingTime = finishDate - startDate;
    workingDays = workingTime.Days;
    grossSalary = workingDays * salaryRate;
    taxDeduction = grossSalary * Convert.ToDecimal(taxRate * .01);
    netIncome = grossSalary - taxDeduction;
    Console.WriteLine("\n");
    Console.WriteLine("Start date:   \t{0:D}", startDate);
    Console.WriteLine("Finish date:  \t{0:D}", finishDate);
    Console.WriteLine("Working days: \t{0:N}", workingDays);
    Console.WriteLine("Gross Salary: \t{0:C}", grossSalary);
    Console.WriteLine("Net income:   \t{0:C}", netIncome);
}
catch (Exception ex) {
    Console.WriteLine(ex.Message);
}
Console.Write("Press any key to continue . . . ");
Console.ReadKey(true);
}
}
}

Este código muestra la utilización de la clase CultureInfo primero invoca al ensamblado System.Globalization

using System.Globalization;

Segundo solicita al usuario que seleccione un país que está asociado con el código de una cultura soportada por el .NET Framework.

Console.WriteLine("a) USA \tb) Canada");
Console.WriteLine("c) UK  \td) Brasil");
Console.WriteLine("\n");
ConsoleKeyInfo option = Console.ReadKey(true);

La cultura seleccionada se establece en el hilo principal de ejecución, cuyo valor predeterminado es el de la configuración regional del sistema operativo. Esto lo hace el siguiente bloque de código:

switch (option.Key) { 
   case ConsoleKey.A:
        ci = new CultureInfo("en-US");
        break;
   case ConsoleKey.B:
        ci = new CultureInfo("fr-CA");
        break;
   case ConsoleKey.C:
        ci = new CultureInfo("en-GB");
        break;
   case ConsoleKey.D:
        ci = new CultureInfo("pt-BR");
        break;
   default:
        ci = Thread.CurrentThread.CurrentCulture;
        break;
    }

Al ejecutar la aplicación como resultado se pueden ver las diferencias entre los formatos de fechas, números y monedas de acuerdo a la cultura seleccionada.

Resultado de la clase CultureInfo con la opción “en-US”, el formato de fechas es mes, día y año. (mm/dd/yyyy)
Resultado de la opción "fr-CA", las fechas de entrada tienen el mismo formato que la cultura “en-US” , pero hay diferencia en el idioma de las fechas y en la moneda.
Resultado de la opción “en-GB”, (english-Great Britain) el formato de las fechas de entrada son diferentes al de USA y Canadá ya que utiliza día, mes y año (dd/mm/yyyy), para las cantidades muestra el símbolo de la libra esterlina.
Resultado de la clase CultureInfo con la opción “pt-BR” (portugués Brasil) se ve la diferencia en las fechas y en la moneda ya que antepone una R de Real la moneda de Brasil.

En caso de que el formato de algún dato introducido no coincida con el formato de la cultura establecida, el programa arrojará una exception, en este caso seleccione la opción USA e intenté introducir las fechas con el formato día, mes y año (dd/mm/yyyy) coincidió en la primera fecha (aunque no era la fecha que quería) y arrojo la excepción en la segunda fecha por tener un formato incorrecto.

domingo, 21 de abril de 2013

Usando el control RadComboBox de Telerik con PostgreSQL

Aunque ASP.NET ofrece una extensa colección de Web Controls que proporcionan al desarrollador una programación fácil, estandarizada y orientada a objetos, también existe como parte del ecosistema de ASP.NET la suite de controles Telerik que complementan y ofrecen más funcionalidad que los Web Controls predeterminados incluidos en el conjunto estándar de ASP.NET.

Del conjunto de controles de la suite Telerik, mostraré brevemente el uso del control RadComboBox de Telerik, el cual extiende la funcionalidad proporcionada por el control DropDownList de ASP.NET al tener no solamente las características de este control sino capacidades adicionales como:la carga de datos de diferentes colecciones de datos, API con funciones de servidor y de cliente, el uso de templates para mejorar la aparencia visual y la propiedad de cargar items bajo demanda para mejorar el rendimiento.

A continuación un sencillo ejemplo para mostrar dos características básicas de este control:

  • La carga de items bajo demanda (Load on Demand)
  • El uso de templates para la aparencia visual de los items.
El ejemplo consiste de una página ASP.NET que consulta una tabla de products que contiene una lista de productos clínicos de una base de datos myinvoices, la tabla tiene el siguiente esquema:

Consultamos la lista de productos

El ejemplo esta compuesto por 6 clases: DataManager, Logger, PostgreSQLCommand,PostgreSQLDataBase, Product, ProductFilters y dos páginas ASP.NET: Default.aspx y Default2.aspx.

El código de la clase DataManager

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Npgsql;
using System.Data;
using System.Text;

namespace TestRadComboBox
{
public class DataManager
{
public List GetProducts(ProductFilters filters) {
    StringBuilder commandText = new StringBuilder("SELECT product_id, ")
    .Append("product_code, ")
    .Append("product_name ")
    .Append("FROM Products ")
    .AppendFormat("WHERE product_name Like '%{0}%'",filters.ProductName.ToUpper());
    List productList = null;
    Product p = null;
    using(NpgsqlDataReader reader = PostgreSQLCommand.GetReader(commandText.ToString(),
        null,
        CommandType.Text)){
        productList = new List();
        while (reader.Read()) {
            p = new Product();
            p.Product_ID = Convert.ToInt32(reader["product_id"]);
            p.ProductCode = reader["product_code"].ToString();
            p.ProductName = reader["product_name"].ToString();
            productList.Add(p);
        } 
    }
    return productList;
}
}
}

El código de la clase Logger

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace TestRadComboBox
{
public class Logger
{
public static void LogWriteError(string s)
{
using (FileStream stream = new FileStream("log.txt", 
    FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
    StreamWriter sw = new StreamWriter(stream);
    sw.BaseStream.Seek(0, SeekOrigin.End);
    sw.Write(s);
    sw.Flush();
    sw.Close();
}

}
}
}

El código de la clase PostgreSQLCommand

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using Npgsql;

namespace TestRadComboBox
{
internal sealed class PostgreSQLCommand
{

internal static NpgsqlDataReader GetReader(string commandText,
NpgsqlParameter[] parameters,System.Data.CommandType cmdtype){
NpgsqlDataReader resp = null;
try{
 NpgsqlConnection conn = PostgreSQLDataBase.GetInstance().GetConnection();
 using (NpgsqlCommand cmd = new NpgsqlCommand(commandText, conn))
 {
            if (parameters != null)
                cmd.Parameters.AddRange(parameters);
  resp = cmd.ExecuteReader(CommandBehavior.CloseConnection);
 }
 return resp;
}catch(Exception ex){
 Logger.LogWriteError(ex.Message);
 throw ex;
}
}
}
}

El código de la clase PostgreSQLDataBase

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.Configuration;
using Npgsql;
using NpgsqlTypes;

namespace TestRadComboBox
{
internal sealed class PostgreSQLDataBase
{
static NpgsqlConnection _conn = null;
static PostgreSQLDataBase Instance = null;
string ConnectionString = null;
private PostgreSQLDataBase()
{
    try
    {
        ConnectionString = ConfigurationManager.
            ConnectionStrings["myinvoices"].ConnectionString;
    }
    catch (Exception ex)
    {
        Logger.LogWriteError(ex.Message);
        throw ex;
    }
}
private static void CreateInstance()
{
    if (Instance == null)
    { Instance = new PostgreSQLDataBase(); }
}
public static PostgreSQLDataBase GetInstance()
{
    if (Instance == null)
        CreateInstance();
    return Instance;
}
public NpgsqlConnection GetConnection()
{
    try
    {
        _conn = new NpgsqlConnection(ConnectionString);
        _conn.Open();
        return _conn;
    }
    catch (Exception ex)
    {
        Logger.LogWriteError(ex.Message);
        throw ex;
    }
}
}
}

El código de la clase Product

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace TestRadComboBox
{
public class Product
{
    public int Product_ID { set; get; }
    public string ProductCode { set; get; }
    public string ProductName { set; get; }
}
}

El código de la clase ProductFilters

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace TestRadComboBox
{
public struct ProductFilters
{
    public string ProductName { set; get; }

}
}

El código de la página ASP.NET Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Import Namespace="TestRadComboBox" %>
<%@ Register assembly="Telerik.Web.UI" 
namespace="Telerik.Web.UI" 
tagprefix="telerik" %>
<!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></title>
</head>
<script runat="server">

    protected void cmbProducts_OnItemsRequested(object sender,
        RadComboBoxItemsRequestedEventArgs args) {
        DataManager dm = new DataManager();
        ProductFilters filter = new ProductFilters { 
            ProductName = args.Text
        };

        if (cmbProducts.DataSource == null)
        {
            cmbProducts.DataSource = dm.GetProducts(filter);
            cmbProducts.DataValueField = "Product_ID";
            cmbProducts.DataTextField = "ProductName";
            cmbProducts.DataBind();
        }
    }
</script>
<body>
<form id="form1" runat="server">
<telerik:RadScriptManager ID="RadScriptManager1" runat="server">
    <Scripts>
<asp:ScriptReference Assembly="Telerik.Web.UI" Name="Telerik.Web.UI.Common.Core.js">
</asp:ScriptReference>
<asp:ScriptReference Assembly="Telerik.Web.UI" Name="Telerik.Web.UI.Common.jQuery.js">
</asp:ScriptReference>
<asp:ScriptReference Assembly="Telerik.Web.UI" Name="Telerik.Web.UI.Common.jQueryInclude.js">
</asp:ScriptReference>
</Scripts>
</telerik:RadScriptManager>
<div>
Please select a product:
    <telerik:RadComboBox ID="cmbProducts" 
    DropDownWidth="333px"
    OnItemsRequested="cmbProducts_OnItemsRequested"
    EnableLoadOnDemand="true"
    Runat="server">
    </telerik:RadComboBox>
</div>
</form>
</body>
</html>


La Carga de items bajo demanda
Una de las capacidades más utilizadas del control RadComboBox es la capacidad de cargar las opciones o items bajo demanda es decir construir la consulta en base a las coincidencias de los items y lo teclado por el usuario, esta capacidad se programa con el metódo cmbProducts_OnItemsRequested

protected void cmbProducts_OnItemsRequested(object sender,
        RadComboBoxItemsRequestedEventArgs args) {
        DataManager dm = new DataManager();
        ProductFilters filter = new ProductFilters { 
            ProductName = args.Text
        };

        if (cmbProducts.DataSource == null)
        {
            cmbProducts.DataSource = dm.GetProducts(filter);
            cmbProducts.DataValueField = "Product_ID";
            cmbProducts.DataTextField = "ProductName";
            cmbProducts.DataBind();
        }
    }

En este metódo se contruye una clase ProductFilter que contine el texto tecleado por el usuario en el RadComboBox, una vez creada esta clase se pasa como argumento al metódo GetProducts de la clase DataManager dentro de este metódo se construye la consulta SQL que filtra mediante un comando LIKE la consulta de la tabla Products hacia la lista (List) que sirve de DataSource para el RadComboBox.

//Here we past the filter in order to build the SQL Query
public List GetProducts(ProductFilters filters) {
    StringBuilder commandText = new StringBuilder("SELECT product_id, ")
    .Append("product_code, ")
    .Append("product_name ")
    .Append("FROM Products ")
    .AppendFormat("WHERE product_name Like '%{0}%'",filters.ProductName.ToUpper());
    List productList = null;
    Product p = null;
    using(NpgsqlDataReader reader = PostgreSQLCommand.GetReader(commandText.ToString(),
        null,
        CommandType.Text)){
        productList = new List();
        while (reader.Read()) {
            p = new Product();
            p.Product_ID = Convert.ToInt32(reader["product_id"]);
            p.ProductCode = reader["product_code"].ToString();
            p.ProductName = reader["product_name"].ToString();
            productList.Add(p);
        } 
    }
    return productList;
}

Al ejecutar la página vemos el resultado como en las siguientes imágenes:

Se realiza la búsqueda de las coincidencias entre lo tecleado por el usuario y el listado de productos

Es característica mejora el rendimiento de los formularios.



Uso de un template para los items.

Uno de los aspectos visuales más llamativos de este control Telerik es el utilizar un template para personalizar la aparencia de la lista de elementos seleccionables, como ejemplo en la siguiente página Default2.aspx utilizamos la misma funcionalidad de la página Default.aspx con la diferencia de utilizar una tabla como template para los items.

El código de la página ASP.NET Default2.aspx que muestra el uso de un template para los items.

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Import Namespace="TestRadComboBox" %>
<%@ Register assembly="Telerik.Web.UI" namespace="Telerik.Web.UI" 
tagprefix="telerik" %>
<!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></title>
</head>
<script runat="server">


protected void cmbProducts_OnItemsRequested(object sender,
    RadComboBoxItemsRequestedEventArgs args) {
    DataManager dm = new DataManager();
    ProductFilters filter = new ProductFilters { 
        ProductName = args.Text
    };

    if (cmbProducts.DataSource == null)
    {
        cmbProducts.DataSource = dm.GetProducts(filter);
        cmbProducts.DataValueField = "Product_ID";
        cmbProducts.DataTextField = "ProductName";
        cmbProducts.DataBind();
    }
}
</script>
<body>
<form id="form1" runat="server">
<telerik:RadScriptManager ID="RadScriptManager1" runat="server">
<Scripts>
<asp:ScriptReference Assembly="Telerik.Web.UI" Name="Telerik.Web.UI.Common.Core.js">
</asp:ScriptReference>
<asp:ScriptReference Assembly="Telerik.Web.UI" Name="Telerik.Web.UI.Common.jQuery.js">
</asp:ScriptReference>
<asp:ScriptReference Assembly="Telerik.Web.UI" Name="Telerik.Web.UI.Common.jQueryInclude.js">
</asp:ScriptReference>
</Scripts>
</telerik:RadScriptManager>
<div>
Please select a product:
<telerik:RadComboBox ID="cmbProducts" 
DropDownWidth="333px"
OnItemsRequested="cmbProducts_OnItemsRequested"
EnableLoadOnDemand="true"
HighlightTemplatedItems="true"
Runat="server">
<HeaderTemplate>
<table cellspacing="3" width="99%">
<tr>
<td width="44%">Code</td>
<td>Name</td>
</tr>
</table>
</HeaderTemplate>
<ItemTemplate>
<table cellspacing="3" width="99%">
<tr>
<td width="44%"><%#DataBinder.Eval(Container.DataItem, "ProductCode")%></td>
<td><%#DataBinder.Eval(Container.DataItem, "ProductName")%></td>
</tr>
</table>
</ItemTemplate>
</telerik:RadComboBox>
</div>
</form>
</body>
</html>

Al ejecutar la página Default2.aspx, veremos el resultado como en la siguiente imágen:

Por último el fragmento de código del archivo Web.conf del ejemplo, en donde se pone la cadena de conexión.

  <connectionStrings>
    <add connectionString="Server=127.0.0.1;Port=5432;Database=myinvoices;User ID=postgres;Password=Pa$$W0rd" name="myinvoices" />
  </connectionStrings>

lunes, 8 de abril de 2013

Utilizando refcursors (cursores de referencia) en PostgreSQL

Esta una entrada complementaria a un post anterior acerca del uso de cursores en PostgreSQL.

Después de que se han obtenido los registros de un cursor abierto con el comando FETCH entonces ya podemos asignar los valores de esos registro a ciertos tipos de variables, los tipos de variable que se utilizan para el trabajo con cursores son:

  1. Mismo tipo de datos que cada una de las columnas obtenidas por el cursor.
  2. TYPE
  3. RECORD
  4. ROWTYPE
Como ejemplo para el primer tipo de variable programaremos una función PL/SQL con refcursors,en PostgreSQL un refcursor (cursor de referencia) es un cursor que se construye en runtime(tiempo de ejecucción), como su nombre indica es la referencia a un cursor.
Usaremos el siguiente schema como parte del ejemplo:

Este schema representa una estructura básica para la contabilidad de pagos y documentos, en donde cada pago recibido debe ser aplicado a un documento y reducir su monto.

Consultamos la tabla invoices

Consultamos los pagos

La aplicación de un pago hacia un documento debe generar un registro en la tabla payments_docs así que el próposito del código PL/SQL como ejemplo es asociar un pago aplicado a una factura y actualizar su monto. Esta funcionalidad la creamos con el siguiente código PL/SQL, que consiste de dos funciones de apoyo getinvoice(), getpayment() y una función principal setpaymentdoc(p_inv_id numeric,p_pay_id numeric). Con esta función regresamos un cursor de referencia con los datos de una factura de la tabla invoices.

Similar a la función anterior pero ahora regresamos un pago de la tabla payments.

Ahora el código PL/SQL de la función principal la cuál establece la relación entre el pago y la factura y actualiza su balance.

Al ejecutar las funciones getinvoice() y getpayment(), ambas regresan un cursor de referencia.

Antes de ejecutar el procedimiento consultamos el pago y la factura de las cuales se obtendrán los datos para crear el registro en la tabla de relación payments_docs, como resultado de la ejecucción del procedimiento.

En la primera función getinvoice() observamos como el tipo de valor devuelto es un cursor de referencia, esto mediante la sentencia RETURNS refcursor AS

CREATE FUNCTION getinvoice(p_inv_id numeric) RETURNS refcursor AS 

Declaramos la variable con el tipo refcursor(cursor de referencia) para contener el cursor devuelto por la función

 v_cursor_inv refcursor;

El cursor devuelto lleva asociado las columnas de la consulta a la tabla invoices, la cuál se encuentra asociada al cursor.

OPEN v_cursor_inv FOR SELECT invoice_id,invoice_total FROM invoices 
 WHERE invoice_id = p_inv_id;

finalmente devolvemos la variable a la cual le asignamos el cursor.

return v_cursor_inv;

La función getpayment() es similar a la función getinvoice() lo que cambia es la consulta asociada a su cursor de referencia. En la función principal setpaymentdoc(p_inv_id numeric,p_pay_id numeric) declaramos las variables que almacenarán los cursores de referencia y las variables que almacenarán a las columnas que cada cursor tenga asociadas como parte de su consulta SQL.

DECLARE

resp int;
 v_payment refcursor;
 v_invoice refcursor;
 v_invoice_id int;
 v_invoice_total numeric(9,2);
 v_pay_id int;
 v_pay_amount numeric(9,2);
 v_balance numeric(11,2);

Ahora almacenamos los cursores devueltos por las funciones getinvoice() y getpayment() en las variables v_invoice y v_payment respectivamente.


v_invoice := getinvoice(p_inv_id);
v_payment := getpayment(p_pay_id);

Una vez guardado el cursor en la variable, podemos extraer los valores de las columnas y almacenarlos en su variable correspondiente, esto lo hacemos con el comando FETCH INTO.

FETCH [cursor name] INTO [variable1, variable2, ….n].

FETCH v_invoice INTO v_invoice_id,v_invoice_total;
 CLOSE v_invoice;

Es obligatorio que la variable que guarda el valor de la columna sea del mismo tipo de dato que la columna extraída del cursor. Por último insertamos la relación en la tabla utilizando los valores guardados en las variables, y regresando el número de secuencia.

v_balance := v_invoice_total - v_pay_amount;
 raise NOTICE 'balance : %',v_balance;
 INSERT INTO payments_docs(pay_id,inv_id,balance,date_created)
 VALUES(v_pay_id,v_invoice_id,v_balance,now());
 resp := currval('payments_docs_payments_docs_id_seq');

Al ejecutar el procedimiento con todos sus argumentos se mostrará el resultado como se ve en la siguiente imagen:

Ahora consultamos la tabla de relación, para ver el registro creado, resultado de la ejecución del procedimiento.

jueves, 28 de marzo de 2013

Cursores (Cursors) y Funciones (Functions) PLpg/SQL en PostgreSQL

Un cursor es el nombre que recibe un apuntador (pointer) de solo lectura hacia un conjunto de datos ( resultset) que se obtiene de una consulta SQL asociada, para los cursores pensemos en términos de arreglos similar a los arreglos de un lenguaje de programación, los cursores nos sirven para procesar una a una las filas que componen un conjunto de resultados en vez de trabajar con todos los registros como en una consulta tradicional de SQL.

Los cursores pueden declararse con una consulta SQL sin parámetros o con parámetros, en donde el tamaño del conjunto de datos depende del valor de los parámetros de la consulta. Así por ejemplo declaramos:

Los cursores pueden emplearse dentro de funciones PL/SQL para que las aplicaciones que accedan a PostgreSQL puedan utilizarlos más de una vez.

Como ejemplo tenemos una base de datos llamada myinvoices, que contiene las siguientes tablas:

invoices
invoicedetails

Ahora como ejemplo de un cursor utilizado dentro de una función, declaramos una función utilizando un cursor con parámetros para devolver el total de facturas emitidas dentro de un rango de fechas.

Existen dos errores comunes cuando trabajamos con cursores:

  1. Si tratas de abrir un cursor que ya se encuentra abierto PostgreSQL enviará un mensaje de “cursor [name] already in use”
  2. Si tratas de ejecutar FETCH en un cursor que no ha sido abierto, PostgreSQL enviará un mensaje de “cursor [name] is invalid”

Cuando utilizamos el comando FETCH obtenemos una por una las filas del conjunto de resultados después de cada fila procesada el cursor avanza a la siguiente fila y la fila procesada puede ser entonces utilizada dentro de una variable.

Consultamos los datos de la tabla invoices

Ejecutamos la función, usando los siguientes argumentos:
Obtenemos los siguientes resultados:

miércoles, 6 de marzo de 2013

Entendiendo Transacciones (Transactions) con ADO .NET y PostgreSQL

En .NET las transacciones son representadas por la clase Transaction que implementa la interfaz IDbTransaction definida dentro del ensamblado System.Data esta interfaz proporciona los métodos:

Commit: Confirma la transacción y persiste los datos.
Rollback: Regresa los datos a un estado anterior a la transacción.


Esta interfaz se utiliza para crear clases Transaction asociadas con un proveedor especifico, así para SQL Server tenemos SqlTransaction, para Oracle OracleTransaction y para PostgreSQL NpgsqlTransaction.

La ventaja de crear transacciones en .NET y no en la bases de datos es proporcionar a la aplicaciones la capacidad de las transacciones en caso de utilizar una base de datos que no proporcione o soporte esa característica.
Como ejemplo escribimos un programa en MonoDevelop que utiliza las tablas: Invoices e InvoiceDetails que se utilizaron en esta entrada este programa utiliza una transaction y 4 comandos SQL con los que guarda una factura (invoice) con dos detalles (invoice details), actualizando el total de la factura conforme a la cantidad de productos y su precio.

using System;
using System.Collections.Generic;
using System.Text;
using Npgsql;
using NpgsqlTypes;
using System.Data;
using System.Globalization;


namespace MonoTransExamples
 {
  class Program
  {
  static string connString = "Server=127.0.0.1;Port=5432;Database=myinvoices;
User ID=postgres;Password=Pa$$W0rd";

  static string commandText1 = "INSERT INTO invoices(invoice_number,invoice_date,
invoice_total)" + "VALUES(:number,:date,:total)";

  static string commandText2 = "SELECT MAX(invoice_id) FROM invoices";
  static string commandText3 = "INSERT INTO invoicedetails
(invoice_id,invoiced_description,invoiced_quantity,invoiced_amount)" +
"VALUES(:id,:description,:quantity,:amount)";

static string commandText4 = "UPDATE invoices SET invoice_total =
:total WHERE invoice_id = :id"; static void Main(string[] args) { bool success = false; int recordsAffected = 0; Invoice invoice = new Invoice { Invoice_number = 2099, Invoice_date = new DateTime(2013,01,29,6,6,6,100,Calendar.CurrentEra) }; Invoicedetails[] details = { new Invoicedetails{ Invoice = invoice, Invoiced_description = "walkie-talkie 22-Channel", Invoiced_quantity = 3, Invoiced_amount = 19.99M }, new Invoicedetails{ Invoice = invoice, Invoiced_description = "2 GB SD Memory Card", Invoiced_quantity = 4, Invoiced_amount = 6.99M } }; NpgsqlTransaction transaction = null; NpgsqlConnection conn = null; try { conn = new NpgsqlConnection(connString); conn.Open(); transaction = conn.BeginTransaction(); using (NpgsqlCommand cmd1 = new NpgsqlCommand(commandText1, conn, transaction)) { cmd1.CommandType = CommandType.Text; cmd1.Parameters.Add("number", NpgsqlDbType.Integer, 4).Value = invoice.Invoice_number; cmd1.Parameters.Add("date", NpgsqlDbType.Timestamp).Value = invoice.Invoice_date; cmd1.Parameters.Add("total", NpgsqlDbType.Money).Value = invoice.Invoice_total; recordsAffected = cmd1.ExecuteNonQuery(); } Console.WriteLine("{0} invoiced inserted",recordsAffected); if (recordsAffected > 0) { using (NpgsqlCommand cmd2 = new NpgsqlCommand(commandText2, conn, transaction)) { cmd2.CommandType = CommandType.Text; invoice.Invoice_id = Convert.ToInt32(cmd2.ExecuteScalar()); } Console.WriteLine("Invoice Id {0} ",invoice.Invoice_id); } if (invoice.Invoice_id > 0) { recordsAffected = 0; foreach (Invoicedetails invd in details) { invd.Invoice.Invoice_id = invoice.Invoice_id; using (NpgsqlCommand cmd3 = new NpgsqlCommand(commandText3, conn, transaction)) { cmd3.CommandType = CommandType.Text; cmd3.Parameters.Add("id", NpgsqlDbType.Integer, 4).Value = invd.Invoice.Invoice_id; cmd3.Parameters.Add("description", NpgsqlDbType.Varchar, 512).Value = invd.Invoiced_description; cmd3.Parameters.Add("quantity", NpgsqlDbType.Smallint).Value = invd.Invoiced_quantity; cmd3.Parameters.Add("amount", NpgsqlDbType.Money).Value = invd.Invoiced_amount; recordsAffected += cmd3.ExecuteNonQuery(); } invoice.Invoice_total += invd.Invoiced_amount * invd.Invoiced_quantity; } Console.WriteLine("Total: {0} ,{1} records affected ",invoice.Invoice_total,recordsAffected); } if (recordsAffected == details.Length) { using (NpgsqlCommand cmd4 = new NpgsqlCommand(commandText4, conn, transaction)) { cmd4.CommandType = CommandType.Text; cmd4.Parameters.Add("total",NpgsqlDbType.Money).Value = invoice.Invoice_total; cmd4.Parameters.Add("id",NpgsqlDbType.Integer).Value = invoice.Invoice_id; recordsAffected = cmd4.ExecuteNonQuery(); } Console.WriteLine("Updated invoice {0} with total {1} ", invoice.Invoice_id,invoice.Invoice_total); } if(recordsAffected > 0) success = true; }catch(NpgsqlException ex){ Console.WriteLine("Error {0} ", ex.Message); }finally{ if (success) transaction.Commit(); else transaction.Rollback(); if (conn != null) if (conn.State == ConnectionState.Open) conn.Close(); } Console.WriteLine("Done"); Console.ReadLine(); } } class Invoice { public int Invoice_id { set; get; } public int Invoice_number { set; get; } public DateTime Invoice_date { set; get; } public Decimal Invoice_total { set; get; } } class Invoicedetails { public int Invoiced_id { set; get; } public Invoice Invoice { set; get; } public string Invoiced_description { set; get; } public int Invoiced_quantity { set; get; } public Decimal Invoiced_amount { set; get; } } }

Este programa asocia una transacción con una conexión abierta.

conn = new NpgsqlConnection(connString);
conn.Open();
transaction = conn.BeginTransaction();
Se crea cada uno de los comandos SQL dentro del alcance de la transacción
using (NpgsqlCommand cmd1 = new NpgsqlCommand(commandText1, conn, transaction))
using (NpgsqlCommand cmd2 = new NpgsqlCommand(commandText2, conn, transaction))
using (NpgsqlCommand cmd3 = new NpgsqlCommand(commandText3, conn, transaction))
using (NpgsqlCommand cmd4 = new NpgsqlCommand(commandText4, conn, transaction)) 
Si todos los comandos se ejecutan correctamente se llama al método Commit() de lo contrario se llama al método Rollback y se cierra la conexión.
  if (success)
            transaction.Commit();
        else
            transaction.Rollback();
        if (conn != null)
            if (conn.State == ConnectionState.Open)
                conn.Close();
Es importante recordar que la transacción queda pendiente hasta que no se confirme (commit) o se cancele (rollback), si se cierra la conexión mediante el método Close se ejecuta un rollback en todas las transacciones pendientes.

martes, 5 de marzo de 2013

Entendiendo Transacciones con PostgreSQL.

Hay operaciones en los sistemas de bases de datos (DBMS) que no pueden expresarse como una única operación SQL sino como el resultado de un conjunto de dos o más operaciones SQL, cuyo éxito depende de que cada una de esas operaciones se ejecute correctamente ya que si una de ellas falla se considera que toda la operación fallo.
El control de transacciones es una característica fundamental de cualquier DBMS (como PostgreSQL,MS SQL Server ú Oracle) esto permite agrupar un conjunto de operaciones o enunciados SQL en una misma unidad de trabajo discreta , cuyo resultado no puede ser divisible ya que solo se considera el total de operaciones completadas, si hay una ejecución parcial el DBMS se encarga de revertir esos cambios para dejar la información consistente.

Una transacción tiene cuatro características esenciales conocidas como el acrónimo ACID:

  • Atomicity(Atomicidad): Una transacción es una unidad atómica o se ejecutan las operaciones múltiples por completo o no se ejecuta absolutamente nada, cualquier cambio parcial es revertido para asegurar la consistencia en la base de datos.
  • Consistency (Consistencia): Cuando finaliza una transacción debe dejar todos los datos sin ningún tipo de inconsistencia, por lo que todas las reglas de integridad deben ser aplicadas a todos los cambios realizados por la transacción, o sea todas las estructuras de datos internas deben de estar en un estado consistente.
  • Isolation (Aislamiento o independencia): Esto significa que los cambios de cada transacción son independientes de los cambios de otras transacciones que se ejecuten en ese instante, o sea que los datos afectados de una transacción no están disponibles para otras transacciones sino hasta que la transacción que los ocupa finalice por completo.
  • Durability (Permanencia): Después de que las transacciones hayan terminado, todos los cambios realizados son permanentes en la base de datos incluso si después hay una caída del DBMS.

Las transacciones en PostgreSQL utilizan las siguientes palabras reservadas:

BEGIN: Empieza la transacción

SAVEPOINT [name]: Le dice al DBMS la localización de un punto de retorno en la transacción si una parte de la transacción es cancelada. El DBMS guarda el estado de la transacción hasta este punto.

COMMIT: Todos los cambios realizados por las transacciones deben ser permanentes y accesibles a las demás operaciones del DBMS.

ROLLBACK [savepoint]: Aborta la actual transacción todos los cambios realizados deben ser revertidos.


Para poner en práctica estos comandos utilizaremos dos tablas relacionadas Invoices y InvoiceDetails.

Agregamos un par de registros, cada uno dentro de una transacción en el primer registro el commit (confirmación) se realiza de forma automática al terminar la transacción con el comando END.

En el segundo registro utilizamos el comando COMMIT de forma explicita para hacer los cambios permanentes.
Ahora insertamos un nuevo registro y eliminamos un par pero en vez de confirmar la transacción con COMMIT deshacemos los cambios y regresamos los registros a su estado original, utilizando ROLLBACK.
Aquí otro ejemplo del uso de ROLLBACK.

En el siguiente bloque de PL/SQL anónimo vamos a utilizar los comandos anteriores además del comando SAVEPOINT el cuál permite deshacer parcialmente los cambios hechos dentro de una transacción y no toda la transacción por completo.
Persistimos entonces solo los cambios antes del SAVEPOINT, los cambios realizados después serán revertidos por el comando ROLLBACK.

martes, 19 de febrero de 2013

Nuevo número de la revista ATIX de divulgación del software libre (número 21)

Ya se encuentra en línea, el nuevo número de la revista Atix de divulgación del software libre.

Los mirrors para la descarga gratuita se encuentran en este enlace.

El código fuente del tutorial de la revista se publico en la revista se encuentra en este post.

lunes, 18 de febrero de 2013

Trabajando con Binary Large Object (BLOB) en PostgreSQL con GTK# y MonoDevelop II

En el número 20 de la revista Atix se publicó acerca de como leer y escribir datos binarios en PostgreSQL con C#, ahora en este documento complementamos la información anterior mostrando un programa en GTK# que hace uso de los mismos conceptos salvo que ahora los integra con las capacidades ofrecidas por Monodevelop para la creación de formularios GTK#.

Fig. 1 Monodevelop proyecto GTK#

Fig. 2 Diseño del formulario

Fig. 3 Muestra de la solución

Fig. 4 La aplicación ejecutándose

Fig. 5 Consulta de un registro

Fig. 6 Alta de un registro

domingo, 27 de enero de 2013

Entendiendo cursors (cursores) en PostgreSQL.

Para el manejo de grandes cantidades de datos tanto en PostgreSQL como en otras bases de datos relacionales existe el concepto de cursors (cursores) los cuales representan un resultset (conjunto de datos) que son asociados a una variable, una variable de tipo cursor. Esta variable representa un apuntador hacia una tabla virtual representada por una consulta y su respectivo comando SELECT asociado.

La diferencia entre un comando SELECT no asociado a un cursor y uno asociado, es que en el primero la consulta regresará todos los registros a la vez y si queremos limitar la cantidad de registros para procesar debemos correr la consulta nuevamente agregando WHERE, BETWEEN o cualquier otra instrucción para filtrar los resultados. En el caso del comando asociado a un cursor este nos permite desplazarnos y limitar la cantidad de registros para procesar dentro del resultset sin necesidad de un nuevo comando SELECT.

Como ejemplo tenemos una tabla Books dentro de una base de datos llamada MyBooks, aqui esta el script para su creación.

CREATE TABLE books
(
  bookid serial NOT NULL,
  title character varying(512),
  publishyear smallint,
  isbn character varying(13),
  created date DEFAULT ('now'::text)::date,
  CONSTRAINT books_pkey PRIMARY KEY (bookid)
)
Para mostrar la diferencia entre una consulta sin cursor y una consulta asociada a un cursor, ejecutamos la siguiente consulta.

SELECT  bookid, title, publishyear, isbn, created FROM Books.
Esta consulta nos devuelve el siguiente resultado.

Ahora mostramos los comandos que pueden utilizarse al asociar una consulta a un cursor. Para el trabajo con cursores es necesario que estos se encuentren dentro del ámbito de una transacción. Empezamos con una transacción.

BEGIN;
Declaramos el cursor como una variable de tipo cursor y le asociamos una consulta SQL.
DECLARE mycursor CURSOR FOR SELECT * FROM Books;
Ahora podemos navegar entre los registros del resultset, con el comando FETCH, de este comando la sintaxis es:
FETCH [FORWARD | BACKWARD |  ALL | NEXT]
A continuación unos ejemplos de su utilización.

Obtenemos un par de registros.
FETCH 2 FROM MyCursor;

Obtenemos los siguientes 4 registros y nos desplazamos:
FETCH 4 FROM MyCursor;

Obtenemos 3 registros hacia atrás a partir del último registro.
FETCH BACKWARD 3 FROM MyCursor;

Avanzamos de nuevo y obtenemos los próximos 6 registros:
FETCH FORWARD 6 FROM MyCursor;
Obtenemos el próximo registro

FETCH NEXT FROM MyCursor;
También podemos avanzar de forma negativa con FORWARD -[n] lo que es equivalente a BACKWARD [n].
FETCH FORWARD -10 FROM MyCursor;
Obtenemos todos los registros del cursor:
FETCH ALL FROM MyCursor;
Por último cerramos el cursor y confirmamos (o abortamos) la transacción.
CLOSE MyCursor;
COMMIT;

viernes, 11 de enero de 2013

Trabajando con Binary Large Object (BLOB) en PostgreSQL con MonoDevelop

En la actualidad existen aplicaciones que además de almacenar y procesar datos basados en caracteres, requieren también almacenar y procesar archivos de gran tamaño de tipo binario o texto, tales archivos como los multimedia (gráficas, imágenes, audio, vídeo) o los generados por programas de oficina (documentos, presentaciones, hojas de calculo), para estos casos en el estándar SQL están definidos dos tipos de datos para guardar grandes cantidades de información: el CLOB (CHARACTER LARGE OBJECT) que se utiliza para información de tipo texto y el BLOB (BINARY LARGE OBJECT) que se utiliza para información de tipo binario.

En el último número de la revista Atix dedicada a divulgar acerca del software libre, escribí un breve tutorial acerca de como escribir y leer este tipo de datos binarios en PostgreSQL mediante un programa C# construido con Monodevelop.

En la página web de la revista Atix, están los enlaces para descargar la revista en PDF. Abajo pongo el enlace del código fuente para el ejemplo del tutorial.