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!

IDataErrorInfo y ASP.NET MVC

Actualización con ASP.NET MVC 2 aquí

En la mayoría de las aplicaciones es probable que el usuario final necesite rellenar un formulario, modificar datos requeridos, etc. Como es normal, en muchas de estas ocasiones no se introducen los datos de forma correcta, obviamos alguno de los campos requeridos, etcétera.

La interfaz IDataErrorInfo nos ofrece la posibilidad de generar errores personalizados y poder mostrarlos en la interfaz de usuario correspondiente. Cuando creamos una vista de manera automática con MVC,  está preparada para mostrar estos errores gracias a los siguientes elementos: 

  • ValidationSummary: nos permite mostrar todos los errores producidos a modo de resumen en la vista.
  • ValidationMessage: puede resultar útil a la hora de mostrar cada error de forma particular, enlazando el mismo con un control de nuestra interfaz.
  • ModelState: se encargará de recopilar todos los errores producidos en nuestro objeto.

 Si generamos una vista de tipo Create o Edit, podemos ver ValidationSummary al comienzo y ValidationMessage en cada uno de los controles.

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

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Edit
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>
Edit</h2>
<%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
<% using (Html.BeginForm())
{%>
<fieldset>
<legend>Fields</legend>
<p>
<label for="Name">
Name:</label>
<%= Html.TextBox("Name", Model.Name) %>
<%= Html.ValidationMessage("Name", "*") %>
</p>
<p>
<label for="Genre">
Genre:</label>
<%= Html.TextBox("Genre", Model.Genre) %>
<%= Html.ValidationMessage("Genre", "*") %>
</p>
<p>
<label for="Synopsis">
Synopsis:</label>
<%= Html.TextBox("Synopsis", Model.Synopsis) %>
<%= Html.ValidationMessage("Synopsis", "*") %>
</p>
<p>
<label for="Year">
Year:</label>
<%= Html.TextBox("Date", String.Format("{0:g}", Model.Date)) %>
<%= Html.ValidationMessage("Date", "*") %>
</p>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
<div>
<%=Html.ActionLink("Back to List", "Index") %>
</div>
</asp:Content>

 Para mostrar un ejemplo, voy a implementar IDataErrorInfo en la clase Movie para controlar la creación y edición de películas.

using System;
using System.ComponentModel;

namespace MovieManager.Models.Objects
{
public class Movie : IDataErrorInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Genre { get; set; }
public string Synopsis { get; set; }
public DateTime? Date { get; set; }

public string this[string columnName]
{
get
{
var result = string.Empty;
switch (columnName)
{
case "Name":
{
if (string.IsNullOrEmpty(Name))
result = "Movie Name is required";
break;
}
case "Genre":
{
if (string.IsNullOrEmpty(Genre))
result = "Movie Genre is required";
break;
}
case "Year":
{
if (Date.HasValue)
{
if (Date.Value == DateTime.MinValue)
{
result = "Movie Date is must be real";
}
}
break;
}
}
return result;
}
}

public string Error
{
get { return string.Empty; }
}
}
}

Debemos crear dos propiedades:

  • Item: Devuelve el mensaje de error de la propiedad solicitada entre corchetes. En este caso, se están cubriendo aquellas propiedades que se consideran requeridas o, en el caso de Date, con una fecha correcta. Un string vacío se considera como acertado.
  • Error: Devuelve el mensaje de error por el cual el objeto es incorrecto. En esta ocasión, nos interesa más el caso particular de cada propiedad. Por ello, retornamos solamente un string.empty, ya que sería su valor por defecto.

Cuando llamamos a una acción desde la vista, y bindeamos la respuesta dentro de un objeto te tipo Movie, automáticamente comprueba que cada propiedad bindeada cumpla las condiciones implementadas en la propiedad ítem de nuestra clase. Si alguna de ellas no cumple las condiciones establecidas, quedará registrada en ModelState como una propiedad inválida.

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude = "Id")] Movie newMovie)
{
if (ModelState.IsValid)
{
_movieRepository.SaveMovie(newMovie);
return RedirectToAction("Index");
}
return View();
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([Bind(Exclude = "Id")] Movie newMovie)
{
if (ModelState.IsValid)
{
_movieRepository.SaveMovie(newMovie);
return RedirectToAction("Index");
}
return View();
}

 Si intentamos crear una nueva película y no rellenamos los campos obligatorios obtendríamos la siguiente imagen:

¡Saludos!