jueves, 3 de julio de 2014

El Demo del Día: Convertir Lista de Objetos en un Archivo DBF y Viceversa

Convertir Lista de Objetos en un Archivo DBF y Viceversa

Requerimiento

Se desea leer datos de una Base de Datos de SQL Server y a partir de estos crear un archivo DBF.

Solución

Usamos una Lista de Objetos para recibir los datos de SQL Server y luego usamos OLEDB y Reflection para crear una tabla e insertar los registros en esta.

Crear la Clase con los Métodos de Conversión

Crear una aplicación Windows Forms en C# llamada "Exportar_DBF" y luego crear una clase con 2 métodos estáticos, uno que genere un archivo DBF a partir de una lista de objetos y otro que a partir de un archivo DBF genere una Lista de Objetos, tal como se muestra en el siguiente código:

using System;
using System.Text; //StringBuilder
using System.Collections.Generic; //List<T>
using System.Reflection; //PropertyInfo, Assembly
using System.Data; //CommandBehavior
using System.Data.OleDb; //OledbConnection, OledbCommand, OleDbDataReader
using System.IO; //File, Path

namespace Exportar_DBF
{
    public class ucListaDBF<T>
    {
        public static void ListaADBF(List<T> lista,string nombreTabla)
        {
            StringBuilder sbCreate = new StringBuilder();
            sbCreate.Append("Create Table ");
            sbCreate.Append(nombreTabla);
            sbCreate.AppendLine(" (");
            PropertyInfo[] propiedades = lista[0].GetType().GetProperties();
            for (int i = 0; i < propiedades.Length; i++)
            {
                sbCreate.Append(propiedades[i].Name);
                sbCreate.Append(" ");
                sbCreate.Append(convertirTipo(propiedades[i].PropertyType.ToString()));
                if (i < propiedades.Length - 1) sbCreate.AppendLine(",");
            }
            sbCreate.AppendLine("");
            sbCreate.AppendLine(")");

            string ruta = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            string archivo = string.Format("{0}\\{1}.dbf", ruta, nombreTabla);
            if (File.Exists(archivo)) File.Delete(archivo);
            using (OleDbConnection con = new OleDbConnection
                ("provider=Microsoft.Jet.Oledb.4.0;data source=" + ruta + ";
                  extended properties=dBase IV"))
            {
                con.Open();
                OleDbCommand cmd = new OleDbCommand(sbCreate.ToString(), con);
                cmd.ExecuteNonQuery();

                StringBuilder sbInsert = new StringBuilder();
                for (int j = 0; j < lista.Count; j++)
                {
                    sbInsert.Clear();
                    propiedades = lista[j].GetType().GetProperties();
                    sbInsert.Append("Insert Into ");
                    sbInsert.Append(nombreTabla);
                    sbInsert.Append(" Values (");
                    for (int i = 0; i < propiedades.Length; i++)
                    {
                        if (propiedades[i].PropertyType.ToString().Contains("String")
                            || propiedades[i].PropertyType.ToString().Contains("DateTime"))
                            sbInsert.Append("'");
                        sbInsert.Append(propiedades[i].GetValue(lista[j], null).ToString().Replace("'",""));
                        if (propiedades[i].PropertyType.ToString().Contains("String")
                            || propiedades[i].PropertyType.ToString().Contains("DateTime"))
                            sbInsert.Append("'");
                        if (i < propiedades.Length - 1) sbInsert.Append(",");
                    }
                    sbInsert.AppendLine(")");
                    cmd.CommandText = sbInsert.ToString();
                    cmd.ExecuteNonQuery();
                }
            }
        }

        private static string convertirTipo(string tipoNET)
        {
            string tipoOLEDB = "";
            tipoNET = tipoNET.ToLower();
            if (tipoNET.Contains("int")) tipoOLEDB = "Long";
            else
            {
                if (tipoNET.Contains("decimal")) tipoOLEDB = "Double";
                else
                {
                    if (tipoNET.Contains("datetime")) tipoOLEDB = "DateTime";
                    else
                    {
                        tipoOLEDB = "Text(100)";
                    }
                }
            }
            return (tipoOLEDB);
        }

        public static List<T> DBFALista(string nombreTabla)
        {
            List<T> lista = new List<T>();
            Type tipo = typeof(T);
            string ruta = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            using (OleDbConnection con = new OleDbConnection
                ("provider=Microsoft.Jet.Oledb.4.0;data source=" + ruta + ";
                  extended properties=dBase IV"))
            {
                con.Open();
                OleDbCommand cmd = new OleDbCommand("Select * From " + nombreTabla, con);
                OleDbDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
                if (drd != null)
                {
                    object obe = null;
                    string tipoDato;
                    PropertyInfo propiedad;
                    while (drd.Read())
                    {
                        obe = Activator.CreateInstance(tipo);
                        for (int i = 0; i < drd.FieldCount; i++)
                        {
                            propiedad = obe.GetType().GetProperty(drd.GetName(i).ToLower(),
                                BindingFlags.IgnoreCase|BindingFlags.Public|BindingFlags.Instance);
                            tipoDato = drd[i].GetType().ToString().ToLower();
                            if (tipoDato.Contains("int16")) propiedad.SetValue(obe, drd.GetInt16(i));
                            else
                            {
                                if (tipoDato.Contains("int32")) propiedad.SetValue(obe, drd.GetInt32(i));
                                else
                                {
                                    if (tipoDato.Contains("decimal")) propiedad.SetValue
                                      (obe, drd.GetDecimal(i));
                                    else
                                    {
                                        if (tipoDato.Contains("datetime")) propiedad.SetValue
                                          (obe, drd.GetDateTime(i));
                                        else
                                        {
                                            if (tipoDato.Contains("double")) propiedad.SetValue
                                              (obe, (int)drd.GetDouble(i));
                                            else propiedad.SetValue(obe, drd.GetString(i));
                                        }
                                    }
                                }
                            }
                        }
                        lista.Add((T)obe);
                    }
                }
            }
            return (lista);
        }
    }
}

Nota: Por defecto OLEDB crea en mayúsculas el nombre de la tabla y los campos, es por eso que para leer la propiedad se usa el método "GetProperty" con 2 parámetros: el nombre del campo y un flag que indica que no sea case sensitive: BindingFlags.IgnoreCase|BindingFlags.Public|BindingFlags.Instance.

Crear el Procedimiento Almacenado en la Base de Datos de SQL Server

Create Procedure [dbo].[uspEmployeesListar]
As
Select EmployeeID,LastName,FirstName,
IsNull(BirthDate,'1/1/1900') As BirthDate
From Employees Order By 1

Crear la Clase con la Entidad del Negocio

using System;
namespace Exportar_DBF
{
    public class beEmpleado
    {
        public int IdEmpleado { get; set; }
        public string Apellido { get; set; }
        public string Nombre { get; set; }
        public DateTime FechaNac { get; set; }
    }
}

Nota: Para el caso de trabajar con DBF tener cuidado que los nombres de las propiedades tengan como máximo 10 caracteres, de lo contrario el método "DBFALista" generará un error.

Crear la Clase de Datos

using System;
using System.Data; //CommandType
using System.Data.SqlClient; //SqlConnection, SqlCommand, SqlDataReader
using System.Collections.Generic; //List

namespace Exportar_DBF
{
    public class daEmpleado
    {
        public List<beEmpleado> listar(SqlConnection con)
        {
            List<beEmpleado> lbeEmpleado = null;
            SqlCommand cmd = new SqlCommand("uspEmployeesListar", con);
            cmd.CommandType = CommandType.StoredProcedure;
            SqlDataReader drd = cmd.ExecuteReader(CommandBehavior.SingleResult);
            if (drd != null)
            {
                lbeEmpleado = new List<beEmpleado>();
                int posIdEmpleado = drd.GetOrdinal("EmployeeID");
                int posApellido = drd.GetOrdinal("LastName");
                int posNombre = drd.GetOrdinal("FirstName");
                int posFechaNacimiento = drd.GetOrdinal("BirthDate");
                beEmpleado obeEmpleado;
                while (drd.Read())
                {
                    obeEmpleado = new beEmpleado();
                    obeEmpleado.IdEmpleado = drd.GetInt32(posIdEmpleado);
                    obeEmpleado.Apellido = drd.GetString(posApellido);
                    obeEmpleado.Nombre = drd.GetString(posNombre);
                    obeEmpleado.FechaNac = drd.GetDateTime(posFechaNacimiento);
                    lbeEmpleado.Add(obeEmpleado);
                }
                drd.Close();
            }
            return (lbeEmpleado);
        }
    }
}

Crear la Clase con las Reglas del Negocio

using System;
using System.Collections.Generic;
using System.Data.SqlClient; //SqlConnection
using System.Configuration; //ConfigurationManager

namespace Exportar_DBF
{
    public class brEmpleado
    {
        public string Conexion { get; set; }

        public brEmpleado()
        {
            Conexion = ConfigurationManager.ConnectionStrings
                ["conNW"].ConnectionString;
        }

        public List<beEmpleado> listar()
        {
            List<beEmpleado> lbeEmpleado = null;
            using (SqlConnection con = new SqlConnection(Conexion))
            {
                try
                {
                    con.Open();
                    daEmpleado odaEmpleado = new daEmpleado();
                    lbeEmpleado = odaEmpleado.listar(con);
                }
                catch (SqlException ex)
                {
                    //Capturar el error y grabar un Log
                }
            } //con.Close(); con.Dispose(); con = null;
            return (lbeEmpleado);
        }
    }
}

Modificar el Archivo de Configuración de la Aplicación

Abrir el archivo App.Config y aumentar la cadena de conexión a Northwind (conNW):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
      <add name="conNW" providerName="SQLServer"
         connectionString="uid=UsuarioNW;pwd=123456;
         data source=DSOFT\Sqlexpress;database=Northwind"/>
    </connectionStrings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

Crear el Formulario Windows Forms de Prueba

Cambiar el nombre del formulario a "frmListaDBF" y arrastrar 3 controles:
- Un DataGridView llamado "dgvEmpleado"
- Un Button llamado "btnExportarDBF"
- Un Button llamado "btnCargarDBF"


Escribir el siguiente código en el formulario:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace Exportar_DBF
{
    public partial class frmListaDBF : Form
    {
        private List<beEmpleado> lbeEmpleado;

        public frmListaDBF()
        {
            InitializeComponent();
        }

        private void cargarDatosSQL(object sender, EventArgs e)
        {
            brEmpleado obrEmpleado = new brEmpleado();
            lbeEmpleado = obrEmpleado.listar();
            dgvEmpleado.DataSource = lbeEmpleado;
        }

        private void exportarListaDBF(object sender, EventArgs e)
        {
            ucListaDBF<beEmpleado>.ListaADBF(lbeEmpleado, "Empleado");
            MessageBox.Show("DBF creado");
        }

        private void cargarDBF(object sender, EventArgs e)
        {
            lbeEmpleado = ucListaDBF<beEmpleado>.DBFALista("Empleado");
            MessageBox.Show("Lista creada");
        }
    }
}

Ejecutar y Probar el Formulario Creado

Grabar la aplicación, compilar y ejecutar, clic al botón "Exportar a DBF" y se creará en el directorio de la aplicación "Exportar_DBF\bin\Debug" el archivo "EMPLEADO.DBF", luego para comprobar que el DBF fue creado clic al botón "Cargar DBF" que lee el DBF en una lista de objetos y lo muestra en la misma grilla.


Comentario Final

Usando OLEDB y Reflection hemos creado dinámicamente un archivo DBF con datos almacenados en una lista de objetos, también hemos realizado la carga del archivo DBF en una Lista de Objetos. El único cuidado que debemos tener es que los nombres de campos no deben pasar los 10 caracteres porque hay una limitación de los campos en un archivo DBF.

Descarga:
Demo09_Exportar_DBF

Charlas Gratuitas en Universidades e Institutos

Charlas Gratuitas en Instituciones Educativas

A pedido de algunas personas, estoy tomando la iniciativa de nuevamente difundir Programación en .NET para los estudiantes de Institutos y Universidades del Perú.

Las charlas en cada institución serán entre 2 y 3 horas y serán libres, aquellos interesados que quieren llevar a sus centros de estudios una charla deberán enviarme un mail a: Luis.duenash@gmail.com con el asunto: "Charlas en Instituciones Educativas", indicando el Centro de Estudios, Lugar, Contacto, Fecha propuesta y Numero aproximado de participantes.

Las charlas deberán ser entre Lunes y Viernes en la tarde o en la noche. Para tener alguna constancia de la charla seria bueno al final un Certificado de participación como expositor. Por ahora sera en Lima ya que para provincias es un poco complicado, pero se podría llegar a un acuerdo.

Mi misión es compartir el conocimiento especializado en el desarrollo de aplicaciones, lo cual lo vengo haciendo mas de 20 años, pero el sector con el que siempre he trabajo ha sido el profesional, es decir, los que ya trabajan desarrollando, pero creo que los estudiantes que son los que recién empiezan son los que mas necesitan ver en lo que van a pasar muchos años de su vida y hay que hacerlo lo mejor posible.

Saludos.

El Libro del Día: Learning jQuery Deferreds

El Libro del Día: 2014-07-03

Titulo: Learning jQuery Deferreds
Autor: Terry Jones, Nicholas H Tollervey
Editorial: O'Reilly
Nro Paginas: 131

Capítulos:
1. Introduction
2. The jQuery Deferred API
3. Deferred Recipes
4. More Time in the Mental Gymnasium
A. Hints for Selected Challenges
B. The Promises/A+ Specification
C. Converting an ArrayBuffer to Base 64

Descarga:
Learning_jQuery_Deferreds

miércoles, 2 de julio de 2014

El Libro del Día: MCSD Certification Toolkit Exam 70483 Programming in C#

El Libro del Día: 2014-07-02

Titulo: MCSD Certification Toolkit Exam 70483 Programming in C#
Autor: Tiberiu Covaci, Gerry O'Brien, Rod Stephen, Vincent Varallo
Editorial: Wrox
Nro Paginas: 650

Capítulos:
Chapter 1: Introducing the Programming C# Certification
Chapter 2: Basic Program Structure
Chapter 3: Working with the Type System
Chapter 4: Using Types
Chapter 5: Creating and Implementing Class Hierarchies
Chapter 6: Working with Delegates, Events, and Exceptions
Chapter 7: Multithreading and Asynchronous Processing
Chapter 8: Creating and Using Types with Reflection, Custom Attributes, the CodeDOM, and Lambda Expressions
Chapter 9: Working with Data
Chapter 10: Working with Language Integrated Query (LINQ)
Chapter 11: Input Validation, Debugging, and Instrumentation
Chapter 12: Using Encryption and Managing Assemblies
APPENDIX: Answers to Sample Test Questions

Descarga:
MCSD_CertificationToolkit_Exam70483_ProgrammingC#

martes, 1 de julio de 2014

El Demo del Día: Proteger el Archivo de Configuración

Proteger el Archivo de Configuración

Problema de Seguridad

En .NET, el archivo de configuración de las aplicaciones (App.config o Web.config) se divide en secciones como connectionStrings para guardar cadenas de conexion o appSettings para guardar variables de aplicación, y muchas veces necesitamos proteger la información de una o ambas secciones para que no sea vista fácilmente, por ejemplo el usuario y clave de la cadena de conexión.

Solución al Problema de Seguridad

Podemos implementar un código que permita cifrar la cadena de conexión y luego descifrarla, pero sería muy trabajoso y no tan eficiente. Desde .NET Framework 2.0 podemos proteger una sección del archivo de configuración usando el método "ProtectSection" de la clase "SectionInformation", el cual marca la sección para que se cifre, de modo que se escriba en el disco en formato cifrado.

Sintaxis: ConfigurationElement.SectionInformation.ProtectSection(ProveedorProtección)

Los proveedores de protección incluidos son los siguientes: DPAPIProtectedConfigurationProvider y RSAProtectedConfigurationProvider.

Nota: Por defecto, sino se coloca ningún parámetro con el Proveedor de Protección (nulo o vacío), se
usa por defecto "RSAProtectedConfigurationProvider", pero en .NET Framework 2 y 3.5 puede generar un error, mejor es especificar el proveedor.

Más informaciónSectionInformation.ProtectSection (Método)

Crear una Aplicación Windows Forms de Prueba

Creamos una aplicación WinForms en C# llamada "Seguridad_AppConfig" y llamamos al formulario "frmPrueba" sobre el cual arrastramos 2 controles Label y 2 Textbox, similar a la siguiente figura:


Escribimos el siguiente código para el formulario:
using System;
using System.Drawing;
using System.Windows.Forms; //Form
using System.Data.SqlClient; //SqlConnection
using System.Configuration; //ConfigurationManager

namespace Seguridad_AppConfig
{
    public partial class frmPrueba : Form
    {
        public frmPrueba()
        {
            InitializeComponent();
        }

        private void cargarDatos(object sender, EventArgs e)
        {
            using (SqlConnection con = new SqlConnection
                (ConfigurationManager.ConnectionStrings["conNW"].ConnectionString))
            {
                con.Open();
                txtCadenaConexion.Text = con.ConnectionString;
                txtRutaLog.Text = ConfigurationManager.AppSettings["RutaLog"];
            }
        }
    }
}

Nota: La función controladora "cargarDatos" esta asociado al evento "load" del "formulario".

Archivo de Configuración

Definimos las siguiente secciones en el archivo de configuración (App.Config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="conNW" providerName="System.Data.SqlClient" connectionString="uid=UsuarioNW;
      pwd=123456;data source=servidor;initial catalog=Northwind"/>
  </connectionStrings>
  <appSettings>
    <add key="RutaLog" value="C:\Logs\"/>
  </appSettings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

Clase con Métodos para Proteger Secciones del Archivo de Configuración

Creamos una clase llamada: "ProtegerConfig" con 3 métodos estáticos:
1. ProtegerConexion: Cifra solo la sección "connectionStrings".
2. ProtegerVariables: Cifra solo la sección "appSettings".
3. ProtegerConexionVariables: Cifra las 2 secciones, "connectionStrings" y "appSettings".

using System;
using System.Configuration;
namespace Seguridad_AppConfig
{
    public class ProtegerConfig
    {
        public static void ProtegerConexion()
        {
            Configuration configura = ConfigurationManager.OpenExeConfiguration
                (ConfigurationUserLevel.None);
            if (!configura.ConnectionStrings.SectionInformation.IsProtected)
            {
                configura.ConnectionStrings.SectionInformation.ProtectSection
                    ("RSAProtectedConfigurationProvider");
                configura.Save(ConfigurationSaveMode.Full, true);
            }
        }

        public static void ProtegerVariables()
        {
            Configuration configura = ConfigurationManager.OpenExeConfiguration
                (ConfigurationUserLevel.None);
            if (!configura.AppSettings.SectionInformation.IsProtected)
            {
                configura.AppSettings.SectionInformation.ProtectSection
                    ("RSAProtectedConfigurationProvider");
                configura.Save(ConfigurationSaveMode.Full, true);
            }
        }

        public static void ProtegerConexionVariables()
        {
            Configuration configura = ConfigurationManager.OpenExeConfiguration
                (ConfigurationUserLevel.None);
            bool grabar = false;
            if (!configura.ConnectionStrings.SectionInformation.IsProtected)
            {
                configura.ConnectionStrings.SectionInformation.ProtectSection
                    ("RSAProtectedConfigurationProvider");
                grabar = true;
            }
            if (!configura.AppSettings.SectionInformation.IsProtected)
            {
                configura.AppSettings.SectionInformation.ProtectSection
                    ("RSAProtectedConfigurationProvider");
                grabar = true;
            }
            if(grabar) configura.Save(ConfigurationSaveMode.Full, true);
        }
    }
}

Nota: Antes de proteger una sección primero averiguamos si ya fue protegida, ya que si grabamos una sección que ya fue protegida generará una excepción.

Modificar la llamada en el método Main de la clase "Program"

Aumentar en el método Main() de la clase Program.cs la llamada a los 3 método estáticos:

using System;
using System.Windows.Forms;

namespace Seguridad_AppConfig
{
    static class Program
    {
        /// <summary>
        /// Punto de entrada principal para la aplicación.
        /// </summary>
        [STAThread]
        static void Main()
        {
            //ProtegerConfig.ProtegerConexion();
            //ProtegerConfig.ProtegerVariables();
            //ProtegerConfig.ProtegerConexionVariables();
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new frmPrueba());
        }
    }
}

Nota: Están comentados para probar uno por uno y ver los resultados.

Probar la aplicación que cifra el archivo de configuración

Primero ejecutamos la aplicación con los métodos comentados y se mostrará la figura:


Ingresar al directorio donde se crea el ejecutable de la aplicación: "Seguridad_AppConfig\bin\Debug\" y abrir con el bloc de notas el archivo de configuración: "Seguridad_AppConfig.exe.config" observar que esta igual como se definió al inicio (su tamaño es 1kb).

Parar la ejecución, descomentar la línea ProtegerConfig.ProtegerConexion(); del método Main(), grabar y compilar nuevamente. Ejecutar el exe desde fuera del Visual Studio, es decir el archivo: "Seguridad_AppConfig.exe" de la carpeta bin y observar el archivo de configuración, que ha cambiado (su tamaño es ahora 58 kb) y se ha cifrado la sección "connectionStrings".

Parar la ejecución, descomentar la línea ProtegerConfig.ProtegerVariables(); del método Main() y comentar la anterior, grabar y compilar nuevamente. Ejecutar el exe desde fuera del Visual Studio, es decir el archivo: "Seguridad_AppConfig.exe" de la carpeta bin y observar el archivo de configuración, que ha cambiado (su tamaño es ahora 58 kb) y se ha cifrado la sección "appSettings".

Finalmente, parar la ejecución, descomentar la línea ProtegerConfig.ProtegerConexionVariables(); del método Main() y comentar la anterior, grabar y compilar nuevamente. Ejecutar el exe desde fuera del Visual Studio, es decir el archivo: "Seguridad_AppConfig.exe" de la carpeta bin y observar el archivo de configuración, que ha cambiado (su tamaño es ahora 59 kb) y se ha cifrado ambas secciones "connectionStrings" y "appSettings".

Comentario Final

Aunque el demo es sobre WinForms, se puede usar el mismo código en todos los tipos de aplicaciones (ASP .NET WebForms, WPF, WCF, Workflow, etc) para cifrar el archivo de configuración usando un algoritmo asimétrico como RSA. Espero les sirva.

Descarga:
Demo08_Seguridad_AppConfig

El Libro del Día: JavaScript and JSON Essentials

El Libro del Día: 2014-07-01

Titulo: JavaScript and JSON Essentials
Autor: Sai Srinivas Sriparasa
Editorial: Packt
Nro Paginas: 120

Capítulos:
Chapter 1: JavaScript Basics
Chapter 2: Getting Started with JSON
Chapter 3: Working with Real-time JSON
Chapter 4: AJAX Calls with JSON Data
Chapter 5: Cross-domain Asynchronous Requests
Chapter 6: Building the Carousel Application
Chapter 7: Alternate Implementations of JSON
Chapter 8: Debugging JSON

Descarga:
JavaScript_JSON_Essentials

lunes, 30 de junio de 2014

El Libro del Día: jQuery Mobile Web Development Essentials

El Libro del Día: 2014-06-30

Titulo: jQuery Mobile Web Development Essentials
Autor: Raymond Camden, Andy Matthews
Editorial: Packt
Nro Paginas: 243

Capítulos:
Chapter 1: Preparing Your First jQuery Mobile Project
Chapter 2: Working with jQuery Mobile Pages
Chapter 3: Enhancing Pages with Headers, Footers, and Toolbars
Chapter 4: Working with Lists
Chapter 5: Getting Practical – Building a Simple
Chapter 6: Working with Forms and jQuery Mobile
Chapter 7: Creating Modal Dialogs and Widgets
Chapter 8: Moving Further with the Notekeeper Mobile Application
Chapter 9: jQuery Mobile Configuration, Utilities, and JavaScript Methods
Chapter 10: Working with Events
Chapter 11: Enhancing jQuery Mobile
Chapter 12: Creating Native Applications
Chapter 13: Becoming an Expert – Building an RSS Reader Application

Descarga:
jQueryMobile_WebDevelopment_Essentials