Creación de una nueva Storage Account en Windows Azure

Dentro de Windows Azure, no solamente podemos subir y consumir aplicaciones sino que, además, tenemos a nuestra disposición la parte de Storage
(almacenamiento). Dentro de Windows Storage se encuentran las
siguientes posibilidades:

  • Tablas: Se utilizan para el almacenamiento de datos estructurados pero no relacionales.
  • Queues: Las colas son útiles para almacenar mensajes en el orden en el que fueron recibidos para el procesamiento secuencial. Los objetos almacenados en Queue se insertan en un extremo y se quitan del otro.
  • Blobs: Sirven para el almacenamiento de archivos independientes.

Para utilizar cada uno de los apartados, primero debemos crear una nueva cuenta de almacenamiento, a través de los siguientes pasos:

1. Accedemos al portal de Windows Azure y seleccionamos Windows Azure y New Service.


2. En este apartado tenemos dos posibilidades: Storage Account y Hosted Services. Seleccionamos la primera de ellas.

3. Llegados a este punto, necesitamos poner un nombre al servicio y una descripción. Es importante tener en cuenta que este no es el nombre público sino más bien una forma de identificar esta cuenta de almacenamiento dentro de nuestra cuenta de Azure.

4. En este apartado debemos elegir qué nombre público deseamos que tenga este almacenamiento.

Para asegurarnos que está disponible, podemos pulsar sobre el botón Check Availability donde nos aparecerá un mensaje confirmando o denegando el nombre que acabamos de introducir.

En la parte inferior, tenemos un apartado muy importante: Dentro de la plataforma de Azure, podemos tener organizados nuestros servicios por grupos de afinidad. Todos aquellos servicios que permanezcan en el mismo grupo se encontrarán en la misma región. Si elegimos que no, nos permitirá seleccionar una región distinta. De lo contrario, podremos elegir uno de los grupos de afinidad que nos ofrece en la parte inferior, si ya tenemos alguno definido, o bien nos permitirá crear uno nuevo.

5. Por último, al pulsar sobre Create en la ventana anterior, nos mostrará un resumen de los datos relativos a la nueva cuenta de almacenamiento que acabamos crear.


Nota: CDN
Significa Content Delivery Network. El beneficio que conlleva es la mejora del rendimiento disminuyendo la latencia en la obtención de los datos del Storage, independientemente de la ubicación geográfica del usuario.

¡Saludos!

Subir una aplicación ASP.NET MVC a IIS 6

Cuando subimos una aplicación ASP.NET MVC a IIS 6 es muy posible que nos topemos con un error de tipo 404 si intentamos navegar por la aplicación:

Para poder solventarlo, necesitamos realizar una asignación de extensión para la aplicación (Application Extension Mapping) de la siguiente manera:

Accedemos al administrador de IIS y abrimos las propiedades de la aplicación ASP.NET MVC. Dentro de la pestaña Directorio virtual, pulsamos sobre el botón Configuración.


Pulsamos sobre el botón Insertar para asignar una extensión al directorio virtual.

Localizamos la dll aspnet_isapi.dll a través del botón Examinar, la cual está ubicada en WINDOWSMicrosoft.NETFrameworkv2.0.50727aspnet_isapi.dll

Deshabilitamos el campo «Comprobar si el archivo existe» y aceptamos.

Ahora todo debe ir sobre ruedas 😉

Para resolver el problema en otras versiones de IIS, podéis consultar el siguiente enlace (inglés): http://www.asp.net/learn/mvc/tutorial-08-cs.aspx

¡Saludos!

Subir una aplicación ASP.NET MVC a Windows Azure

Uno de los primeros artículos que publiqué acerca de Windows Azure fue la manera de subir una aplicación. Actualmente, en el caso de las aplicaciones ASP.NET MVC es necesario realizar una serie de ajustes para que pueda funcionar correctamente, ya que de no ser así no conseguirá arrancar en la nube.

En Visual Studio 2010 Beta 2 ya tenemos disponible la plantilla para añadir un proyecto de tipo ASP.NET MVC en un entorno de nube pero en el caso de Visual Studio 2008 no es así, aunque no es imposible 🙂

Creamos un proyecto de tipo Cloud Service.

No seleccionamos ninguna plantilla puesto que vamos a añadir un proyecto ya creado.

Añadimos un proyecto existente en ASP.NET MVC y, con el botón derecho, seleccionamos Unload Proyect.
Para que Windows Azure reconozca qué tipo de rol tiene este proyecto, debemos agregar la siguiente línea en el archivo csproj.

 

<RoleType>Web</RoleType>

El archivo debe quedar de la siguiente manera:

Volvemos a pulsar con el botón derecho sobre el proyecto para seleccionar Reload Project.
Por otro lado, debemos cambiar la propiedad Copy Local a true de tres dll:

 

Al haber añadido un proyecto a una solución Cloud Service ya generada, necesitamos enlazar ambos proyectos. Para ello, nos posicionamos en la carpeta Roles y con el botón derecho seleccionamos Add => Web Role Project in solution… y elegimos el proyecto ASP.NET MVC.

Una vez realizados estos pasos, podremos subir el proyecto a la nube sin problemas =)

¡Saludos!

Access Control Service en Windows Azure Platform AppFabric

Hace algún tiempo estuve hablando de qué era AppFabric, cuáles eran los componentes que lo formaban e incluso se mostró un pequeño ejemplo de la parte de Service Bus. Para cerrar la base de Windows Azure Platform AppFabric, hoy voy a centrarme en Access Control.

Access Control Service se encarga de la autenticación y autorización de los usuarios para nuestras aplicaciones desde la nube. En esta release inicial existe la posibilidad de autenticar a los clientes a través de claves simétricas, con nombre de usuario y contraseña, y además la autenticación de clientes a través de ADFS v2 (Active Directory Federation Services). En un futuro, esperan poder soportar un número mayor de tokens como Facebook, cuentas de Google, Windows Live ID entre otros.

Antes de comenzar he de decir que parte del código ha sido tomado de alguno de los ejemplos del SDK de AppFabric o bien del Training Kit de Azure como la validación de los tokens o la validación de la cabecera y he intentado simplificar los mismos lo más posible, además de la adicción de comentarios para facilitar la compresión de los ejemplos.

¿CÓMO FUNCIONA ACCESS CONTROL SERVICE?

1. CONFIGURACIÓN DE ACCESS CONTROL SERVICE

Existen tres conceptos en cuanto a configuración se refiere: Token Policy, Issuer y Scope.

Token Policy está compuesto por un ID, un nombre y el tiempo de expiración de los tokens. Se encarga de firmar los tokens que se emiten y establecer el tiempo de vida de los mismos.

I
ssuer es un recurso de Access Control que se utiliza para registrar clientes del servicio. Cada issuer está compuesto por un ID, un nombre y dos claves. Generalmente se utiliza un issuer por cliente y cada cliente tendrá una clave a través de la cual conseguirá el token necesario para poder validar la comunicación con el servicio.

Scope se utiliza para agrupar reglas dentro de Access Control Service, enlazando una politica de token y la dirección a la que aplica. Se identifica por un ID y un nombre. Cada regla indica el Issuer asociado y las operaciones que puede realizar dentro del scope.

Existen varias opciones para crear cada uno de los recursos en el Access Control del namespace: A través de líneas de comandos (PowerShell o CMD) o bien a través de una interfaz que nos facilitan en el Windows Azure Platform Training Kit.
Si optamos por la línea de comandos, debemos dirigirnos a la ubicación donde hemos instalado el SDK de AppFabric, donde estará disponible la herramienta Acm.exe y su archivo de configuración.

En primer lugar, abrimos el archivo de configuración para establecer el nombre del Service Namespace que debemos tener creado en la nube y su clave de administración:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="host" value="accesscontrol.windows.net"/>
<add key="service" value="Service Namespace"/>
<add key="mgmtkey" value="Current Management Key"/>
</appSettings>
</configuration>

Accedemos a la consola de comandos y nos posicionamos en la carpeta Tools del SDK. Los comandos para crear cada uno de los recursos serían los siguientes:

Token Policy

acm create tokenpolicy -name:returngis -autogeneratekey

El name corresponde al nombre descriptivo que queremos darle al Token Policy que vamos a generar y además indicamos que las claves serán autogeneradas.

Scope

acm create scope -name:returngisscope -appliesto:http://localhost/Hello -tokenpolicyid:TOKEN_POLICY_ID

Al igual que el comando anterior, se indica el nombre que queremos asociar al nuevo scope y appliesto corresponde con la URL donde se alojará el servicio que se beneficiará de la autenticación de Access Control.
Cuando se creó el Token Policy en el apartado anterior, la respuesta al finalizar el comando nos facilitó el ID del token policy generado. Para generar el Scope es necesario indicarle qué token policy tiene asociado pasandole como valor el ID.

Issuer

acm create issuer -name:returngisconsumer -issuername:returngisconsumer -autogeneratekey

En el caso de issuer, no está asociado directamente con ningún otro recurso y solamente es necesario etiquetar el issuer como tal y el nombre que queremos utilizar para las credenciales. Las contraseñas serán autogeneradas.

Rule

acm create rule -name:returngisconsumer -scopeid:ID_SCOPE -inclaimissuerid:ID_ISSUER -inclaimtype:Issuer -inclaimvalue:returngisconsumer -outclaimtype:action -outclaimvalue:sayHello

Por último, debemos crear una regla para el issuer y dentro del scope generados anteriormente. En este ejemplo, estamos creando una regla donde el usuario returngisconsumer puede reclamar una acción llamada sayHello.

Desde mi punto de vista, esta opción puede ser bastante pesada ya que debemos ir recuperando a mano cada uno de los ID que nos devuelve cada comando para poder asignarselo al comando oportuno. Por ello, como comentaba anteriormente, disponemos de una interfaz que nos ayuda a realizar estos pasos. Si bien es cierto que la misma tiene algunos defectos, es bastante más cómoda.
Necesitamos descargar el Training Kit, donde disponemos de una serie de ejemplos que además he necesitado para generar este mismo. El proyecto se llama AcmBrowser y está ubicado en WindowsAzurePlatformKitLabsIntroAppFabricAccessControlSourceAssetsAcmBrowser

Para poder ejecutar la aplicación es necesario acceder al proyecto, compilarlo y obtener el ejecutable para poder utilizarlo fuera de Visual Studio. De esta manera, si ejecutamos AcmBrowser.exe desde el raíz, obtendremos la siguiente interfaz:

Ya no es necesario modificar ningún archivo de configuración para añadir las credenciales que nos dan la posibilidad de administrar Access Control. En este entorno, debemos añadir las mismas cada vez que arranquemos la aplicación en los campos Service Namespace y Management Key.
Como opciones, podemos limpiar los valores introducidos en el Namespace, recuperarlos y guardarlos tanto en local como en la nube.

Nota:
En el caso del salvado, cada vez que pulsamos sobre esta acción intenta guardar cada uno de los valores que estamos visualizando en la interfaz, independientemente de si ya están incluidos en la nube o no. Por ello, recomiendo hacer una copia local, a través del disquete sin nube, y luego limpiar la nube y salvar de nuevo. De lo contrario, saltará una excepción 😉

Si pulsamos en la carpeta con la nube (Load from Cloud), y hemos realizado los comandos anteriores a través de Acm.exe, observaremos lo siguiente:

Como era de esperar, aparece cada uno de los recursos introducidos anteriormente agrupados por Issuers, Scopes y Token Policies.

ISSUERS

Si seleccionamosel issuer que creamos a través de la línea de comandos, podemos comprobar el nombre de issuer, el tipo de algoritmo utilizado (por defecto es Symmetric256BitKey) la clave actual y anterior. El cliente necesitará conocer tanto el Issuer Name como la Current Key para obtener un token válido.

TOKEN POLICIES

En este caso observamos que las únicas opciones disponibles son el tiempo de expiración de los tokens y regenerar la clave, además de facilitarnos la actual. Esta clave será necesaria para que el servicio pueda validar la petición del cliente.

Por último, podemos visualizar las opciones configuradas en la regla asociada al scope y al issuer que tenemos actualmente.

CREACIÓN DEL SERVIDOR

Para poder probar este ejemplo, he creado un servicio con un método de lo más sencillo para mostrar el modo de autentificación a través de los tokens que nos ofrece la nube. Es necesario abrir Visual Studio como administrador.

using System.ServiceModel;
namespace ServerACS
{
[ServiceContract]
public interface IService
{
[OperationContract]
string Hello(string name);
}
}

namespace ServerACS
{
public class Service : IService
{
public string Hello(string name)
{
return "Hello " + name;
}
}
}

Para crear el código de la consola, he tomado como referencia el proyecto WCFAuthorizationManager que nos ofrecen como ejemplo del SDK de AppFabric.

using System;
using System.ServiceModel;
using System.ServiceModel.Web;
using ServerACS.ValidationACS;

namespace ServerACS
{
class Program
{
static void Main()
{
const string serviceNamespace = "returngis";
const string tokenPolicyKey = "KEY=";
const string audience = "http://localhost/Hello";
const string claim = "action";
const string value = "sayHello";

var binding = new WebHttpBinding(WebHttpSecurityMode.None);
var address = new Uri(audience);

var host = new WebServiceHost(typeof(Service));
host.AddServiceEndpoint(typeof(IService), binding, address);

//Validación para los tokens, proporcionados por ACS, recibidos en las peticiones de los clientes
host.Authorization.ServiceAuthorizationManager = new AcsAuthorizationManager(
serviceNamespace,
audience,
Convert.FromBase64String(tokenPolicyKey),
claim,
value
);
host.Open();
Console.WriteLine("Service is listening....");
Console.ReadLine();
}
}
}

Para ver todo lo necesario a primera vista, no he realizado modificaciones en el web.config para poder ver claramente lo necesario del lado del servidor.
Lo más interesante es el apartado donde estamos creando un ServiceAuthorizationManager personalizado, facilitado también en el ejemplo del SDK, con algunas simplificaciones y comentarios añadidos para comprender qué hace en cada momento. Así pues recibe el service Namespace, la URL a la que aplica, la clave de token policy, el tipo que se reclama y el valor que contiene el tipo.

using System.Collections.Generic;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace ServerACS.ValidationACS
{
public class AcsAuthorizationManager : ServiceAuthorizationManager
{
private readonly TokenValidator _tokenValidator;
private readonly string _claimType;
private readonly string _claimValue;


public AcsAuthorizationManager(string serviceNamespace, string audience, byte[] tokenPolicyKey, string claimType, string claimValue)
{
_tokenValidator = new TokenValidator(serviceNamespace, audience, tokenPolicyKey);
_claimType = claimType;
_claimValue = claimValue;
}

protected override bool CheckAccessCore(OperationContext operationContext)
{
//Recuperamos las credenciales de la cabecera
string authorizationHeader = null;
if (WebOperationContext.Current != null)
{
authorizationHeader = WebOperationContext.Current.IncomingRequest.Headers[HttpRequestHeader.Authorization];
}
//Si no hay credenciales, no seguimos comprobando. La validación ha fallado.
if (string.IsNullOrEmpty(authorizationHeader)) return false;

//Si no comienza con WRAP (Web Resource Authorization Protocol) no es una cabecera válida.
if (!authorizationHeader.StartsWith("WRAP ")) return false;

//Separamos los elementos de la cabecera
string[] values = authorizationHeader.Substring("WRAP ".Length).Split(new[] { '=' }, 2);

//Si el número de partes es distinto de dos, no es válido.
if (values.Length != 2) return false;

//Si el primer valor no corresponde con access_token, tampoco es correcto.
if (values[0] != "access_token") return false;

//Si el segundo valor no está delimitado por , también sería incorrecto.
if (!values[1].StartsWith(""") || !values[1].EndsWith(""")) return false;

//Recuperamos el token
string token = values[1].Substring(1, values[1].Length - 2);

//Utilizamos el TokenValidator para comprobar el token
if (!_tokenValidator.Validate(token)) return false;

//Recuperamos la acción que se quiere realizar y el nombre de la acción
Dictionary<string, string> claims = _tokenValidator.GetNameValues(token);

//Se intenta recuperar el nombre de la acción que solicita el cliente (sayHello)
string actionValue;
if (!claims.TryGetValue(_claimType, out actionValue)) return false;

//Comprueba que el valor que se reclama es igual que el nombre de la acción que posee el servicio
if (!actionValue.Equals(_claimValue)) return false;

return true;
}
}
}

Como podemos ver, en esta clase se está heredando de ServiceAuthorizationManager y se sobreescribe el método CheckAccessCore para realizar toda la validación de la cabecera de autorización de la petición que se ha recibido. La mayoría de las comprobaciones son bastante simples, comprobando la estructura recibida.
Dentro de este código, debemos prestar especial atención al punto donde se valida el token a través de una instancia de la clase TokenValidation proporcionada también en los ejemplos del SDK.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace ServerACS.ValidationACS
{
public class TokenValidator
{
private const string IssuerLabel = "Issuer";
private const string ExpiresLabel = "ExpiresOn";
private const string AudienceLabel = "Audience";
private const string HmacSha256Label = "HMACSHA256";

private readonly byte[] _tokenKey;
private readonly string _trustedTokenIssuer;
private readonly Uri _trustedAudienceValue;

public TokenValidator(string serviceNamespace, string audience, byte[] tokenKey)
{
_tokenKey = tokenKey;
_trustedTokenIssuer = string.Format("https://{0}.accesscontrol.windows.net/", serviceNamespace.ToLowerInvariant());
_trustedAudienceValue = new Uri(audience);
}

public bool Validate(string token)
{
if (!IsHmacValid(token, _tokenKey))
{
return false;
}

if (IsExpired(token))
{
return false;
}

if (!IsIssuerTrusted(token))
{
return false;
}

if (!IsAudienceTrusted(token))
{
return false;
}

return true;
}

public Dictionary<string, string> GetNameValues(string token)
{
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException();
}

return
token
.Split('&')
.Aggregate(
new Dictionary<string, string>(),
(dict, rawNameValue) =>
{
if (rawNameValue == string.Empty)
{
return dict;
}

string[] nameValue = rawNameValue.Split('=');

if (nameValue.Length != 2)
{
throw new ArgumentException("Invalid formEncodedstring - contains a name/value pair missing an = character");
}

if (dict.ContainsKey(nameValue[0]))
{
throw new ArgumentException("Repeated name/value pair in form");
}

dict.Add(HttpUtility.UrlDecode(nameValue[0]), HttpUtility.UrlDecode(nameValue[1]));
return dict;
});
}

private static ulong GenerateTimeStamp()
{
// Default implementation of epoch time
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToUInt64(ts.TotalSeconds);
}

private bool IsAudienceTrusted(string token)
{
Dictionary<string, string> tokenValues = GetNameValues(token);

string audienceValue;

tokenValues.TryGetValue(AudienceLabel, out audienceValue);

if (!string.IsNullOrEmpty(audienceValue))
{
var audienceValueUri = new Uri(audienceValue);
if (audienceValueUri.Equals(_trustedAudienceValue))
{
return true;
}
}

return false;
}

private bool IsIssuerTrusted(string token)
{
Dictionary<string, string> tokenValues = GetNameValues(token);

string issuerName;

tokenValues.TryGetValue(IssuerLabel, out issuerName);

if (!string.IsNullOrEmpty(issuerName))
{
if (issuerName.Equals(_trustedTokenIssuer))
{
return true;
}
}

return false;
}

private static bool IsHmacValid(string swt, byte[] sha256HmacKey)
{
string[] swtWithSignature = swt.Split(new[] { "&" + HmacSha256Label + "=" }, StringSplitOptions.None);

if ((swtWithSignature == null) || (swtWithSignature.Length != 2))
{
return false;
}

var hmac = new HMACSHA256(sha256HmacKey);

byte[] locallyGeneratedSignatureInBytes = hmac.ComputeHash(Encoding.ASCII.GetBytes(swtWithSignature[0]));

string locallyGeneratedSignature = HttpUtility.UrlEncode(Convert.ToBase64String(locallyGeneratedSignatureInBytes));

return locallyGeneratedSignature == swtWithSignature[1];
}

private bool IsExpired(string swt)
{
try
{
Dictionary<string, string> nameValues = GetNameValues(swt);
string expiresOnValue = nameValues[ExpiresLabel];
ulong expiresOn = Convert.ToUInt64(expiresOnValue);
ulong currentTime = Convert.ToUInt64(GenerateTimeStamp());

if (currentTime > expiresOn)
{
return true;
}

return false;
}
catch (KeyNotFoundException)
{
throw new ArgumentException();
}
}
}
}


A través de la clave del Token Policy, el token facilitado por el cliente, la dirección del service namespace y la uri sobre la que aplica nuestra política es capaz de determinar si es un token válido a través de una serie de filtros.
Cuando un cliente haga una petición, en primer lugar se comprobará la cabecera de la misma, a través del Token Validator se testeará su veracidad y, en caso de ser un token válido, realizará la llamada al método solicitado por el cliente.

CREACIÓN DEL CLIENTE

Como pudimos ver al comienzo de este post, el cliente debe realizar también dos pasos para poder realizar una comunicación con éxito. En primer lugar, debemos solicitar un token proporcionado por el Access Control y, si la obtención del token se hace con éxito, podremos montar la petición para ser enviada al servicio.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Web;
using ServerACS;

namespace Client
{
class Program
{
private const string ServiceName = "returngis";
private const string IssuerKey = "YOUR ISSUER KEY";
private const string AcsBaseAddress = "accesscontrol.windows.net";

static void Main()
{
Console.WriteLine("Enter your name:");
string name = Console.ReadLine();
string token = GetToken();

var binding = new WebHttpBinding(WebHttpSecurityMode.None);
var uri = new Uri("http://localhost/Hello");

var webChannelFactory = new WebChannelFactory<IService>(binding, uri);

var channel = webChannelFactory.CreateChannel();

//Este es el formato que debe seguir la cabecera para
//que el servidor lo reconozca como un token válido
string header = string.Format("WRAP access_token="{0}"", HttpUtility.UrlDecode(token));

using (new OperationContextScope(channel as IContextChannel))
{
WebOperationContext.Current.OutgoingRequest.Headers.Add("authorization", header);

Console.WriteLine(channel.Hello(name));
}
Console.WriteLine("Presh Enter");
Console.ReadLine();

webChannelFactory.Close();
}

private static string GetToken()
{
var client = new WebClient
{
BaseAddress = string.Format("https://{0}.{1}", ServiceName, AcsBaseAddress)
};

var values = new NameValueCollection
{
{"wrap_name", "returngisconsumer"},
{"wrap_password", IssuerKey},
{"wrap_scope", "http://localhost/Hello/"}
};

byte[] responseBytes = client.UploadValues("WRAPv0.9", "POST", values);

string response = Encoding.UTF8.GetString(responseBytes);

Console.WriteLine("nToken From Access Control Service:{0}n", response);

return response
.Split('&')
.Single(value => value.StartsWith("wrap_access_token=", StringComparison.OrdinalIgnoreCase))
.Split('=')[1];

}
}
}

La primera acción que realiza el cliente, nada más recuperar el texto introducido por consola, es intentar recuperar el token a través del método GetToken. En él creamos un objeto de tipo Webclient para realizar una llamada a través de POST con las credenciales generadas cuando creamos el recurso Issuer returngisconsumer. Si obtenemos un token con éxito, se mostrará por pantalla y lo retornaremos a Main.

Tanto la clave de Token Policy como de Issuer podemos recupearlas a través de la consola al finalizar la creación de los recursos o a través de la interfaz de AcmBrowser.

Facilito el ejemplo con los comentarios y algo más simplificados por si fuera de utilidad.

¡Saludos!

Client Side Validation con ASP.NET MVC 2

En un post anterior, IDataErrorInfo y MVC, se hablaba de la posibilidad de validar nuestros objetos desde el modelo y comprobar el resultado de las validaciones través de ModelState.IsValid. Con ASP.NET MVC 2 llegaremos más allá de la mano de JQuery y Data Annotations.

Desde las primeras revisiones de esta segunda versión, tenemos la posibilidad de reutilizar las validaciones descritas en nuestros objetos en el lado del cliente. Para ello, necesitamos especificar las validaciones oportunas en los objetos, utilizando Data Annotations.
Siguiendo con los ejemplos basados en Twitter, en esta ocasión he creado un cliente que recupera el time line de tus amigos y además te permite responderlos o mandarles un mensaje directo.

Los mensajes directos se utilizan para realizar envíos privados a una persona en concreto. Para poder envíar un mensaje de este tipo, es necesario escribir un texto de tamaño limitado y, además, que tenga un destinatario.

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
public class DirectMessage
{
public long ID { get; set; }
[Required(ErrorMessage = "Tweet message is required!")]
[StringLength(131, ErrorMessage = "Your direct message is too long!")]
public string Message { get; set; }
public TwittUser Addressee { get; set; }
}
}

Los dos atributos, Required y StringLength, pertenecen a la propiedad Message donde se está indicando que el mismo es requerido y además que tiene un tamaño máximo de 131 caracteres.
La última propiedad corresponde al remitente, el cual tiene sus propias validaciones en la clase correspondiente.

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
public class TwittUser
{
public long ID { get; set; }
[Required(ErrorMessage = "User name is required!")]
public string UserName { get; set; }
public string PhotoURL { get; set; }
public string Status { get; set; }
}
}

Quizás la opción de decorar las propiedades de una clase dificultan la compresión de la misma. Si queremos hacer una separación de las validaciones, podemos hacer uso del atributo MetaDataType creando una clase independiente.

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
[MetadataType(typeof(TweetReplyMetadata))]
public class TweetReply
{
public long ID { get; set; }
public string Text { get; set; }
public TwittUser Addressee { get; set; }
public Tweet TweetToReply { get; set; }
}
}

Se decorda la principal indicando en el typeof la clase que contiene las validaciones.

using System.ComponentModel.DataAnnotations;

namespace ClientSideValidation.Models.Objects
{
public class TweetReplyMetadata
{
[Required(ErrorMessage = "Reply message is required!")]
[StringLength(131, ErrorMessage = "Your reply is too long!")]
public string Text { get; set; }
}
}

Por otro lado, debemos generar el controlador donde seguiremos utilizando ModelState para comprobar el estado de las validaciones desde el servidor.

using System.Web.Mvc;
using ClientSideValidation.Models;
using ClientSideValidation.Models.Objects;

namespace ClientSideValidation.Controllers
{
public class TweetController : Controller
{
private readonly ITweetService _twitterService;

public TweetController()
{
_twitterService = new TweetService();
}

public ActionResult Index()
{
return View(_twitterService.FetchTweets());
}


public ActionResult Reply(long id)
{
var reply = new TweetReply { TweetToReply = _twitterService.ShowTweet(id) };
return View(reply);
}

[HttpPost]
public ActionResult Reply(TweetReply reply)
{
if (ModelState.IsValid)
{
_twitterService.ReplyTweet(reply);
return RedirectToAction("Index");
}
return View(reply);
}

public ActionResult SendDM(long ID)
{
var DM = new DirectMessage { Addressee = _twitterService.RetrieveUser(ID) };
return View(DM);
}

[HttpPost]
public ActionResult SendDM(DirectMessage DM)
{
if (ModelState.IsValid)
{
_twitterService.SendDM(DM);
return RedirectToAction("Index");
}
return View(DM);
}
}
}

Una vez creadas las vistas, podríamos intentar el envío de un mensaje directo (sin introducir el texto del mismo) y obtendríamos el siguiente resultado:

Actualmente, cuando pulsamos en el botón Send, estamos acudiendo al servidor y ModelState está comprobando si nuestros objetos son válidos o no para mantenernos en la misma vista y mostrar el error o regresar de nuevo al Time Line. Quizás, si un usuario hace una acción de este tipo, no nos interesa siquiera que acuda al servidor. Aquí es donde viene la mágia 🙂

  1. Referenciamos las librerías javascript, ubicadas en la carpeta Scripts, en la Master Page en este orden:
    <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
    <title>
    <asp:ContentPlaceHolder ID="TitleContent" runat="server" />
    </title>
    <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />

    <script src="../../Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>

    <script src="../../Scripts/jquery.validate.min.js" type="text/javascript"></script>

    <script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>

    <script src="../../Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>

    <script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>


    </head>

  2. Habilitamos la línea Html.EnableClientValidation(); en las vistas donde queramos realizar la validación en el lado del cliente.
    <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<ClientSideValidation.Models.Objects.DirectMessage>" %>

    <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Direct Message
    </asp:Content>
    <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <h2>
    Direct Message</h2>
    <%Html.EnableClientValidation(); %>
    <% using (Html.BeginForm())
    {%>
    <div id="tweet">
    <img src="<%=Model.Addressee.PhotoURL%>" />
    <div>
    <%=Html.DisplayFor(m=>m.Addressee.Status) %>
    </div>
    <%=Html.HiddenFor(m=>m.Addressee.ID) %>
    </div>
    <fieldset>
    <legend>
    <%=Model.Addressee.UserName %></legend>
    <div class="editor-label">
    <%= Html.LabelFor(model => model.Message) %>
    </div>
    <div class="editor-field">
    <%= Html.TextAreaFor(model => model.Message) %>
    <%= Html.ValidationMessageFor(model => model.Message) %>
    </div>
    <p>
    <input type="submit" class="buttonStyle" value="Send!" />
    </p>
    </fieldset>
    <% } %>
    <div>
    <%=Html.ActionLink("Back to TimeLine", "Index") %>
    </div>
    </asp:Content>

Si lanzamos de nuevo la aplicación, y realizamos la misma operación que antes ¡El resultado es exactamente el mismo pero sin ir al servidor!
Si observamos el código fuente de la página, observamos que la forma de trabajar es mediante la creación de un objeto JSON donde almacena cada unas de las validaciones alojadas en nuestras clases.

//<![CDATA[CommunityServer
if (!window.mvcClientValidationMetadata) { window.mvcClientValidationMetadata = []; }
window.mvcClientValidationMetadata.push({"Fields":[{"FieldName":"Message","ReplaceValidationMessageContents":true,"ValidationMessageId":"form0_Message_validationMessage","ValidationRules":[{"ErrorMessage":"Your direct message is too long!","ValidationParameters":{"minimumLength":0,"maximumLength":131},"ValidationType":"stringLength"},{"ErrorMessage":"Tweet message is required!","ValidationParameters":{},"ValidationType":"required"}]}],"FormId":"form0","ReplaceValidationSummary":false});
//]]>

Como siempre, facilito el código fuente para poder probar el ejemplo completo.

¡Saludos y feliz 2010!

AsyncController en ASP.NET MVC 2

Una de las novedades previstas para ASP.NET MVC 2 será la posibilidad de crear controladores asíncronos. Por el momento, podemos hacer la primera toma de contacto con la versión Release Candidate de esta versión.

¿Qué conseguimos con los controladores asíncronos?

Imaginemos que necesitamos lanzar una serie de procesos con una duración más prolongada en el tiempo de lo normal. Cuando una petición llega al servidor, uno de los hilos se encarga de la misma. Hasta que la petición no finalice, ese hilo quedará bloqueado y no podrá atender otras peticiones.

En la mayoría de las ocasiones no resulta problemático dado que el pool tiene una cantidad considerable de hilos disponibles para ser bloqueados. Sin embargo, los hilos son limitados y en aplicaciones grandes, donde hay una cantidad considerable de usuarios y pueden existir múltiples hilos con procesos de larga duración, puede llegar a ser un problema produciendo un error de tipo HTTP 503 (Server too busy).

Antes de comenzar, es necesario tener en cuenta una serie de reglas a la hora de crear un controlador de este tipo:

  1. Se crean dos métodos por acción:
    • Uno iniciará el proceso de manera asíncrona y debe contener en su nombre la terminación Async.
    • Otro método, que será invocado cuando la operación asíncrona finalice, será nombrado con  Completed al final.
  2. Ambos métodos deben ser públicos y no pueden ser estáticos.
  3. No está permitido tener parámetros genéricos en las acciones.
  4. No podemos usar sobrecarga de parámetros.Solamente está permitido en los casos que se especifica otro Http Verb o declarando la sobrecarga como NoActionAttribute.
  5. El controlador heredará de AsyncController en vez de Controller.

Para verlo más claro, he creado una pequeña aplicación simulando un cliente de la red social Twitter, utilizando la librería Twitterizer. Las acciones que se podrán realizar a través de la aplicación serán las siguientes:

Para todos estos casos, necesitamos recuperar una serie de datos de la red social que podrá tardar más o menos en función de un conjunto de circunstancias. Si varios usuarios accedieran a la vez a nuestra aplicación y la tarea de recuperar sus tweets, direct messages, mentions, etcétera fuera una tarea costosa, llegaría un momento en el que nuestro servidor no dispondría de threads libres para cubrir el número de peticiones actuales.

Siguiendo las reglas comentadas anteriormente, una de las acciones del controlador podría tener la siguiente estructura:

using System.Collections.Generic;
using System.Web.Mvc;
using AsyncControllerMVC2.Models;
using AsyncControllerMVC2.Models.Objects;

namespace AsyncControllerMVC2.Controllers
{
public class TweetController : AsyncController
{
private readonly TwitterService _twitterService = new TwitterService();

public void TweetsAsync()
{
AsyncManager.OutstandingOperations.Increment();
AsyncManager.Parameters["tweets"] = _twitterService.FetchTweets();
AsyncManager.OutstandingOperations.Decrement();
}

public ActionResult TweetsCompleted(List<TwitterObject> tweets)
{
return View(tweets);
}
}
}

En primer lugar, observamos que efectivamente el controlador hereda de AsyncController para poder ofrecernos sus ventajas. Gracias a esta herencia, disponemos de AsyncManager que nos dará soporte en las operaciones asíncronas.

TweetsAsync será el método encargado de recuperar los tweets. En él podemos ver que se hace uso de OutstandingOperations, que se utiliza para notificar a ASP.NET de cuántas operaciones tiene pendientes en ese momento. Tal y como nos comentan en la documentación de MSDN, es necesario utilizarlo al comienzo del método ya que ASP.NET no es capaz de determinar cuántas operaciones fueron iniciadas por la acción o cuándo fueron completadas.

Entre medias, se realiza la llamada a un repositorio, donde se hace uso de la librería Twitterizer para recuperar el timeline de los amigos del usuario, y se guarda el resultado de la llamada en la propiedad Parameters de tipo diccionario.

using System;
using System.Collections.Generic;
using System.Threading;
using AsyncControllerMVC2.Models.Objects;
using Twitterizer.Framework;

namespace AsyncControllerMVC2.Models
{
public class TwitterService: ITwitterService
{
private const string User = "YOUR USER";
private const string Pass = "YOUR PASS";
private readonly TimeSpan _waitTime = new TimeSpan(0, 2, 0);
private readonly Twitter _twitterAccount;
public TwitterService()
{
_twitterAccount = new Twitter(User, Pass);
}

public List<TwitterObject> FetchTweets()
{
var tweets = new List<TwitterObject>();
foreach (TwitterStatus statusFriend in _twitterAccount.Status.FriendsTimeline())
{
var tweet = new TwitterObject
{
User = statusFriend.TwitterUser.UserName,
Photo = statusFriend.TwitterUser.ProfileImageUri,
Text = statusFriend.Text

};
tweets.Add(tweet);
}
Thread.Sleep(_waitTime);
return tweets;
}
}
}

Para crear un proceso con una duración considerable generé un TimeSpan con un minuto de duración para que, cuando consiga los últimos tweets de la cuenta en cuestión, espere durante un minuto antes de ser retornados de nuevo al controlador.

Cuando la propiedad OutstandingOperations sea igual a cero, ASP.NET finaliza las operaciones asíncronas y llama a la acción TweetCompleted que retorna el resultado recibido como parámetro. Para poder recibir el resultado almacenado en Parameters, es necesario respetar el tipo del objeto guardado.

Con los controladores asíncronos no significa que disminuya el tiempo de espera pero conseguiremos que el servidor no quede completamente bloqueado esperando a los hilos pendientes.

Adjunto el proyecto con todas las acciones de manera asíncrona.

Fuente: http://msdn.microsoft.com/en-us/library/ee728598%28VS.100%29.aspx

InstanceContextMode en WCF

Cuando creamos un servicio con Windows Communication Foundation, debemos conocer  el comportamiento que queremos que tenga en cuanto a sus instancias se refiere. Para controlar el tiempo de vida del contexto disponemos de los siguientes modos:

  • PerCall: Cada cliente creará una nueva instancia por llamada al servicio.
  • PerSession: Se genera una instancia por sesión.
  • Single: Se utiliza la misma instancia para todas las peticiones independientemente del cliente.

 

 

Como he encontrado mucha literatura al respecto y los ejemplos han sido prácticamente el mismo en todos los sitios, he creado uno personalizado donde se realiza el envío de tweets e intento recuperar los mismos utilizando los distintos modos.
Creo un proyecto de tipo WCF Service Library:


Elimino tanto la clase y la interfaz generada por la plantilla y creo una clase Tweet.cs con el rol de DataContract.

using System.Runtime.Serialization;

namespace InstanceContextModeWCF
{
[DataContract]
public class Tweet
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string User { get; set; }
[DataMember]
public string Message { get; set; }
}
}


Una interfaz como ServiceContract

using System.Collections.Generic;
using System.ServiceModel;

namespace InstanceContextModeWCF
{
[ServiceContract]
public interface ITwitter
{
[OperationContract]
void SendTweet(Tweet tweet);

[OperationContract]
List<Tweet> GetMyTweets();
}
}

Y por último la clase que implementa nuestro contrato.

using System.Collections.Generic;
using System.ServiceModel;

namespace InstanceContextModeWCF
{
//[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
//[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
//[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class Twitter : ITwitter
{
private readonly List<Tweet> _tweets = new List<Tweet>();

public void SendTweet(Tweet tweet)
{
_tweets.Add(tweet);
}

public List<Tweet> GetMyTweets()
{
return _tweets;
}
}
}

En la clase Twitter he dejado comentadas las tres posibilidades. ¡Veamos cada una de ellas!

PER CALL

Cuando activamos el modo PerCall nuestro servicio se comportará de la siguiente manera: Creará una instancia para realizar la llamada al método solicitado y, una vez que haya finalizado la llamada, destruirá la instancia creada.

Para este primer ejemplo he activado la primera línea comentada y he seguido los pasos en el orden que señalo en la imagen. Como podemos ver, he creado una conexión con el programa que está actuando como servidor, he rellenado el apartado de mensaje y he enviado el mismo. En este caso, no ha sido posible recuperar el tweet al realizar una nueva llamada con PerCall.

PER SESSION

Con PerSession vamos a utilizar la misma instancia hasta que la sesión finalice. Es decir, un cliente crea un canal y tiene la posibilidad de realizar distintas llamadas a distintos métodos dentro del mismo contexto. Siguiendo el mismo ejemplo, habilitando la segunda línea y deshabilitando la primera, realizamos las mismas acciones desde el cliente.

Como la instancia permanece con vida durante toda la sesión, el cliente es capaz de poder enviar un tweet y después recuperarlo. Sin embargo, si nosotros pulsamos sobre Disconnect  e intentamos recuperar el tweet enviado a través del botón Get All!, la información ya habrá desaparecido al haber creado una nueva sesión.

private void btnConnect_Click(object sender, EventArgs e)
{
_channel = new ChannelFactory<ITwitter>("TwitterService");
_client = _channel.CreateChannel();
lblState.Text = "Connected!";
lblState.ForeColor = System.Drawing.Color.Blue;
}

private void btnDisconnect_Click(object sender, EventArgs e)
{
_channel.Close();
lblState.Text = "Disconnected!";
lblState.ForeColor = System.Drawing.Color.Red;
}

SINGLE

El modo Single se utiliza para mantener una misma instancia desde la primera petición de un cliente cualquiera hasta que el servicio se ha parado completamente.

Si habilitamos el último modo comentado en el código, y deshabilitamos el anterior, podemos realizar los siguientes pasos:

Si cerramos y abrimos de nuevo el cliente, sin cerrar el programa servidor, y pulsamos de nuevo en Get All!  descubrimos que nuestro tweet sigue con vida 😉

Adjunto el ejemplo completo, con el servicio, cliente y servidor. Están los tres proyectos en una misma solución. En cualquier caso, podemos abrir los .exe ubicados en el bin de cada proyecto como administrador o abrir dos Visual Studio con credenciales de administrador para poder debuggear en Vista o Windows 7 =)

¡Saludos!

Routing y ASP.NET MVC

Cuando hablamos de URL amigables estamos pensando en una dirección donde un usuario puede interpretar, de forma relativamente clara, el punto donde se encuentra dentro de un sitio web. Por ejemplo:

http://www.sitioweb.com/index.aspx?accion=comprar&producto=impresora&modelo=f380

http://www.sitioweb.com/Comprar/Impresora/F380

Aunque la acción que van a realizar ambas es la misma, la segunda opción ofrece más ventajas de cara «al exterior» por decirlo de alguna manera. Gracias a las URL amigables o semánticas el usuario obtendrá unas direcciones más fáciles de recordar, a la par que comprensibles, y además conseguiremos mejorar la indexación de nuestro sitio web en los distintos buscadores.

Gracias ASP.NET MVC conseguimos esta adaptación de una forma, en un principio, «transparente» para el desarrollador, nada más empezar a trabajar con un proyecto de este tipo. Para comenzar, debemos situarnos en el archivo Global.asax de nuestra aplicación, donde aparecerá por defecto el método de registro de las rutas disponibles.

using System.Web.Mvc;
using System.Web.Routing;

namespace MovieManager
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801

public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Movie", action = "Index", id = "" } // Parameter defaults
);

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}

 Las rutas de la aplicación deben estar definidas justo antes de poder recibir cualquier petición. Es por ello que, de forma predeterminada, el registro de rutas se lleva a cabo en el archivo Global.asax. En él podemos observar dos métodos: RegisterRoutes y Application_Start. En RegisterRoutes es donde podemos añadir cada una de las rutas personalizadas y sus valores por defecto.

En primer lugar, se está restringiendo las peticiones con extensión .axd, como pueden ser Trace.axd, WebResource.axd, etcétera a través de routes.IgnoreRoute. En la segunda línea, aparece la llamada al método MapRoute que se utiliza para definir las rutas aceptadas. De forma automática, al generar la plantilla de ASP.NET MVC, se da de alta la ruta que aparece en estos momentos, donde podemos ver que recibe el nombre del controlador, la acción y un parámetro llamado id. Justo debajo, aparecen los valores por defecto para cada uno de ellos en el caso de no ser especificados en la petición.
Los nombres entre corchetes son parámetros, datos dinámicos que pueden ser escritos por el usuario o no. De no especificar ninguno, se utilizarán los valores por defecto descritos en la siguiente línea.

Al tener valores por defecto, la flexibilidad aumenta considerablemente y sin modificar absolutamente nada las rutas, podemos solicitar información de la siguiente manera:

Utilizando los valores por defecto:

http://www.sitio.com/

http://www.sitio.com/Movie

http://www.sitio.com/Movie/Index

Utilizando otras peticiones por GET, con valores distintos a los establecidos:

http://localhost:puerto/Movie/Edit/1

http://localhost:49312/Movie/Create

http://localhost:49312/Movie/Details/2

Pero quizás necesitamos llegar más allá o simplemente queremos que nuestras URL tengan una estructura diferente.
Supongamos que necesitamos generar una ruta donde queremos aclarar que la acción a realizar es la búsqueda por Género. Además no queremos que aparezca en dicha ruta el nombre la acción del controlador.

routes.MapRoute(
"ByGenre", // Route name
"List/Movies/Genre/{genre}", // URL with parameters
new { controller = "Movie", action = "Index", genre = (String)null } // Parameter defaults
);

En este caso, cuando solicitemos un resultado a través de List/Movies/Genre/Drama por ejemplo, aparecerán todas aquellas películas que se correspondan con el género indicado en última posición. Como ya comenté, no es necesario si quiera contemplar el nombre del controlador o el nombre real de la acción que queremos invocar ya que esa información se encuentra en los parámetros indicados más abajo.

NOTA: El orden en el cual se registran las rutas es muy importante ya que se tiene en cuenta el mismo. Si nosotros agregamos la nueva ruta para el filtrado por género después de la ruta por defecto ocurriría lo siguiente:

 

 

Por norma general, sería conveniente pensar en el registro de rutas por preferencia de aceptación: La que consideremos más importante ocupará la primera posición y en último lugar aparecerá la ruta por defecto.

Por otro lado, es posible que necesitemos restringir además los datos aceptados por nuestra ruta. Esto se consigue con la ayuda de los constraint.

routes.MapRoute(
"ByDate", // Route name
"List/Movies/Date/{date}", // URL with parameters
new { controller = "Movie", action = "Index", date = (DateTime?)null },
new { date = @"d{2}-d{2}-d{4}" } // Parameter defaults
);

Si por ejemplo intentáramos introducir un valor de tipo string, aparecería la página de error mostrada anteriormente.

Por último, debemos tener en cuenta que, para poder recuperar los valores introducidos, el nombre del parámetro definido en el archivo Global.asax debe corresponder en nombre al que recibe la acción.

public ActionResult Index(string genre, DateTime? date)
{
return View(_movieRepository.ListMovies(genre, date));
}

Facilito el proyecto para probar el ejemplo entero 😉

¡Saludos!

Manipulación de elementos con JQuery

Cuando recuperamos un elemento a través de los selectores puede ser por varios motivos: Añadir un evento, modificar el estilo, enlazar algún efecto e incluso alterar de alguna manera el contenido del mismo.
Centrándome esta vez en la manipulación de elementos, voy a dedicar este post a una serie de funciones que nos ayudarán con nuestro cometido =)

  • html: Recupera el contenido HTML de un elemento.
    $("#btnHTML").click(function(e) {                
    alert($("#parrafoConHTML").html());
    });

    El resultado podría ser algo parecido a esto:

    Del mismo modo, podemos reemplazar el contenido de uno o varios elementos, con el código HTML deseado como parámetro.

    $("#btnAddHtml").click(function(e) {                
    $("#parrafoConHTML").html("codigo <b>HTML</b>");
    });

  • text: Es similiar a html() pero en este caso está obviando las etiquetas HTML.
    $("#btnText").click(function(e) {                
    alert($("#parrafoConHTML").text());
    })

    Si utilizamos el mismo caso que en el ejemplo anterior, comprobamos que no se muestra en el alert ninguna de las etiquetas introducidas en el párrafo:

    Nota: text() no puede usarse con elementos de tipo input. Para ellos, es necesario utilizar el atributo val().
    Si modificamos el contenido del elemento con text, y a su vez intentamos introducir etiquetas HTML, no las interpretará como tal.

    $("#btnAddText").click(function(e) {                
    $("#parrafoConHTML").text("codigo <b>HTML</b>");
    })

    Aunque la estructura es bastante parecida, con text() conseguiríamos lo siguiente:

  • append: Se utiliza para hacer un añadido al código HTML existente en los elementos seleccionados.
    $("#btnAppend").click(function(e){
    $("#spanParaAppend").append("<font color='yellow'>Codigo añadido a traves de <b>append()</b></font>");
    })

  • appendTo: Elegimos un elemento y seleccionamos a otro al cual queremos añadirlo.
    $("#btnAddAppend").click(function(e){
    $("label").appendTo("#spanParaAppend");
    })

    El resultado sería el mismo que con append()

  • prepend: Realiza la misma acción que append pero posicionando los elementos elegidos por delante del elemento destino.
    $("#btnPrepend").click(function(e){
    $("#spanParaPrepend")
    .prepend("<font color='blue'>Codigo añadido a traves de <b>prepend().</b></font>");
    })

  • prependTo:  Pone el elemento seleccionado delante del elemento(s) indicados en segundo lugar.
    $("#btnAddPrepend").click(function(e){
    $("#lblPretend").prependTo("#spanParaPrepend");
    })

  • after: Permite insertar código HTML al final del existente.
    $("#btnAfter").click(function(e){
    $("#spanAfter").after("<font color='pink'> con funciones como <u>after</u></font>");
    })

  • insertAfter: inserta elementos después de aquel tomado como referencia.
    $("#btnInsertAfter").click(function(e){
    $("#lblInsertAfter").insertAfter("#spanAfter");
    })

    En este caso, es importante señalar que se indica en primer lugar lo que queremos insertar y en segundo lugar a qué.

  • before: Coloca código HTML antes del existente.
    $("#btnBefore").click(function(e){
    $("#spanBefore").before("<font color='green'> Insertado con <u>before()</u>.</font>");
    })

  • insertBefore: Posiciona elementos antes del indicado.
    $("#btnInsertBefore").click(function(e){
    $("#lblInsertBefore").insertBefore("#spanBefore");
    })

  • wrap: El significado de wrap en español es envolver. Por ello, lo que conseguimos es «envolver» precisamente con el código HTML introducido a uno o varios elementos. También es posible pasarle como parámetro un elemento.
    $("#btnWrap").click(function(){
    $("#parrafoA").wrap("<a href='#'></a>");
    });

  • wrapAll: En el caso anterior, por cada elemento que encontraba, utilizaba un «envoltorio» por cada uno de ellos. En este caso envuelve todos ellos bajo un código HTML o un elemento pasado por parámetro.
    $("#btnWrapAll").click(function(){
    $("li").wrapAll("<div style='border:solid 1px black';></div>");
    });

  • wrapInner: Localiza el hijo de los elementos seleccionados y aplica el envoltorio sobre ellos, no sobre el padre.
    $("#btnWrapInner").click(function(){
    $("li").wrapInner("<div style='border:solid 1px black';></div>");
    });

  • replaceWith: reemplaza los tags HTML de los elementos por los indicados en la llamada.
    $("#btnReplaceWith").click(function(){
    $("input").replaceWith("<input type='radio'></input>");
    });

    En este caso, reemplaza todos los input que encuentre por uno de tipo radiobutton.

  • replaceAll: Localiza todos los elementos en primera instancia. Esta función realiza la misma acción que replaceWith con los parámetros a la inversa.
    $("#btnReplaceAll").click(function(){
    $("<input type='radio'></input>").replaceAll("input");
    });

  • clone: Como su propio nombre indica, se utiliza para duplicar elementos.
    $("#btnClone").click(function(){
    $(this).clone().insertAfter(this);
    });

    Si le pasamos como parámetro true, clonará además sus eventos asociados.

  • empty: Vacía el contenido de los elementos.
    $("#btnEmpty").click(function(){
    $("#parrafoA").empty();
    });

  • remove: Elimina todo el elemento(s) seleccionado.
    $("#btnEmpty").click(function(){
    $("#parrafoA").remove();
    });

¡Saludos!

AJAX y ASP.NET MVC

Cuando creamos un proyecto de MVC, de forma automática se genera una carpeta llamada Scripts con los siguientes archivos js:

En el momento que queramos hacer uso de ellos, únicamente debemos importarlos, generalmente en la Master Page de nuestra aplicación:

<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>
<asp:ContentPlaceHolder ID="TitleContent" runat="server" />
</title>
<link href="../../Content/Site.css" rel="stylesheet" type="text/css" />

<script src="../../Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>

<script src="../../Scripts/MicrosoftAjax.js" type="text/javascript"></script>

<script src="../../Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script>

</head>

Para mostrar un ejemplo de la forma de trabajar con Ajax en una aplicación MVC, voy a llevar a cabo una serie de pasos para ajustar el contenido de la aplicación Movie Manager usada en otros ejemplos.

PARTIAL VIEW DEL LISTADO DE PELÍCULAS

Selecionamos la carpeta Movie, dentro de Views, y con lo el botón derecho seleccionamos Add => View. Especificamos la siguiente configuración:

Pulsando en Add, se genera una nueva vista con extensión ascx. He modificado ligeramente el contenido para configurar las acciones de detalle, edición y una última para la eliminación de películas de manera asíncrona con Ajax. Además, he añadido tres funciones utilizando JQuery.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<MovieManager.Models.Objects.Movie>>" %>
<%@ Import Namespace="MovieManager.Helpers" %>

<script type="text/javascript">

function beginMovieList(args) {
$('#divMovieList').slideUp('normal');
}
function successMovieList() {
$('#divMovieList').slideDown('normal');
}
function failureMovieList() {
alert("Could not retrieve movies.");
}

</script>

<table>
<tr>
<th>
</th>
<th>
Id
</th>
<th>
Name
</th>
<th>
Genre
</th>
<th>
Synopsis
</th>
<th>
Date
</th>
</tr>
<% foreach (var item in Model)
{ %>
<tr>
<td>
<%=Html.ImageLink("Movie", "Details", "Content\detail.gif", new {item.Id })%>
|
<%=Html.ImageLink("Movie","Edit","Content\edit.gif",new {item.Id }) %>
|
<%= Ajax.ActionLink("Delete", "Delete", new { id = item.Id }, new AjaxOptions { Confirm = "Delete movie?", HttpMethod = "Delete", UpdateTargetId = "divMovieList", OnBegin = "beginMovieList", OnComplete = "successMovieList", OnFailure = "failureMovieList" })%>
</td>
<td>
<%= Html.Encode(item.Id) %>
</td>
<td>
<%= Html.Encode(item.Name) %>
</td>
<td>
<%= Html.Encode(item.Genre) %>
</td>
<td>
<%= Html.Encode(item.Synopsis) %>
</td>
<td>
<%= Html.Encode(String.Format("{0:g}", item.Date)) %>
</td>
</tr>
<% } %>
</table>

En la acción Delete se ha utilizado el helper específico para los controles Ajax. Dentro del mismo, he usado ActionLink donde se usan los mismos parámetros que en un control normal del Html helper a excepción del último parámetro: AjaxOptions. Este objeto, contiene una serie de propiedades para su configuración. Las utilizadas en el ejemplo son:

  1. Confirm: Antes de realizar la acción, se tendrá que confirmar la misma a través de un cuadro de diálogo con el texto introducido en esta propiedad. 
  2. HttpMethod:Se indica el tipo de verbo http que se utilizará en la llamada. En esta ocasión se hace uso de Delete ya que estamos intentando eliminar un recurso.
  3. UpdateTargetId: Id del elemento de nuestra vista que se va a actualizar, como si se tratara de un Update Panel.
  4. OnBegin: Se le pasa el nombre de la función que se ejecutará cuando se inicie la llamada asíncrona. Por ejemplo, podemos llamar a la función beginMovieList para ocultar el listado de películas cuando comience la eliminación de una de ellas.
  5. OnComplete: Si la llamada asíncrona se completa con éxito, se lanzará la función asociada a esta propiedad. En este caso successMovieList volverá a mostrar el listado de películas actualizado.
  6. OnFailure: Si durante la llamada se produce algún tipo de error, se lanzará la función enlazada a esta propiedad en vez de la función añadida en OnComplete, pudiendo avisar al usuario de la incidencia.

MODIFICAR LA VISTA INDEX

Para poder hacer uso de la partial view, y  no perder la funcionalidad hasta ahora conseguida, debemos modificar la vista Movies/Index.aspx de la siguiente manera:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<MovieManager.Models.Objects.Movie>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Index
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>
Index</h2>
<div id="divMovieList">
<% Html.RenderPartial("MovieList"); %>
</div>
<p>
<%= Html.ActionLink("Create New", "Create") %>
</p>
</asp:Content>

Se ha añadido un elemento de tipo div, el cual contiene una llamada a Html.RenderPartial que renderizará la vista parcial que acabamos de crear.
En MovieController creamos una nueva acción para eliminar la película seleccionada.

[AcceptVerbs(HttpVerbs.Delete)]
public ActionResult Delete(int id)
{
var movieToDelete = _movieRepository.GetMovie(id);
_movieRepository.Delete(movieToDelete);
return PartialView("MovieList", _movieRepository.ListMovies());
}

Cuando recupera la pelicula a través de NHibernate y elimina la misma, retorna el nuevo listado de películas a MovieList.

Los métodos Delete y GetMovie han sido añadidos tanto en la interfaz IMovieRepository como en su implementación, siempre en la parte del Modelo.

using System.Collections.Generic;
using MovieManager.Models.Objects;

namespace MovieManager.Models
{
public interface IMovieRepository
{
IList<Movie> ListMovies();
void SaveMovie(Movie movieToSave);
Movie GetMovie(int id);
void Delete(Movie movieToDelete);
}
}

using System.Collections.Generic;
using MovieManager.Models.Objects;
using NHibernate;
using NHibernate.Criterion;

namespace MovieManager.Models
{
public class MovieRepository : IMovieRepository
{
private readonly ISessionFactory _session;

public MovieRepository(ISessionFactory sessionFactory)
{
_session = sessionFactory;
}

public IList<Movie> ListMovies()
{
return _session.GetCurrentSession().CreateCriteria(typeof(Movie)).List<Movie>();
}

public void SaveMovie(Movie movieToSave)
{
_session.GetCurrentSession().SaveOrUpdate(movieToSave);
}

public Movie GetMovie(int id)
{
return (Movie)_session.GetCurrentSession().CreateCriteria(typeof(Movie))
.Add(Restrictions.Like("Id", id)).UniqueResult();
}

public void Delete(Movie movieToDelete)
{
_session.GetCurrentSession().Delete(movieToDelete);
}
}
}

Como hemos visto, podemos trabajar de una forma más simplificada con Ajax, gracias a MVC y JQuery, adaptando la aplicación de una forma poco agresiva 🙂

Adjunto el proyecto por si fuera de utilidad.

¡Saludos!