Drag and drop entre listas con asp.net mvc y jQuery

View

Para hacer la funcionalidad de drag and drop entre listas con jquery basta con una simple instrucción:

$("#left #center #right").sortable({
                connectWith: '.drag-and-drop-connectedSortable'
            }).disableSelection();

Aquí le estamos diciendo que los elementos left, center y right (en mi caso son todas <ul>) son listas conectadas con los elementos que tengan como class a “drag-and-drop-connectedSortable” (que en este caso son las mismas listas), lo que permite tomar elementos de una lista y ponerlos en otra.

Luego agregamos algunos detalles estéticos y logramos algo como:

ui

los nombres en este ejemplo son en agradecimiento a los que participar en la discusión del foro sobre algunos temas relacionados (hasta el momento de la publicación de este post). hilo en altnet-hispano.

Controller

La acción del controller que deseo invocar es la siguiente:

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult SendList(IList<string> left, IList<string> center, IList<string> right)
{
    return Json(new
                    {
                        Right = from i in right select i.ToLower(),
                        Center = from i in center select i.ToLower(),
                        Left = from i in left select i.ToLower()
                    });
}

pero al momento de invocar dicha acción para informarle sobre los elementos seleccionados en cada lista me encontré con algunos inconvenientes que conversamos en altnet-hispano (hilo):

  • Pasar una lista (<ul> <li>) al controller como parámetros de una acción.
  • Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.
Pasar una lista (<ul> <li>) al controller como parámetros de una acción

Aquí vimos que no era posible que asp.net mvc interpretara la lista como valores que el usuario puede modificar ya que como dijo Fernando solo los <input…> son tenidos en cuenta. Con esto ya decidimos hacer la invocación del POST con jquery.

Armar los parámetros del post para que sean correctamente interpretados por asp.net mvc.

Para esto tuvimos agregar la línea: jQuery.ajaxSettings.traditional = true; ya que sino asp.net mvc no puede interpretar correctamente los parámetros de tipo array.

El código

$(document).ready(function() {
    jQuery.ajaxSettings.traditional = true;

    $("#form").submit(function() {
        var leftvalues = [];
        $("#left li").each(function() { leftvalues.push($(this).text()) });
        var centervalues = [];
        $("#center li").each(function() { centervalues.push($(this).text()) });
        var rightvalues = [];
        $("#right li").each(function() { rightvalues.push($(this).text()) });

        var postData = { left: leftvalues, center: centervalues, right: rightvalues };

        $.post('<%= Url.Action("SendList", "DragAndDrop") %>',
            postData,
            function(data) {
                alert("left: "+ data.Left + "\n"+ "center: "+ data.Center + "\n"+ "right: "+ data.Right);
            },
            'json');

        return false;
    });

Cómo comentaba mas arriba, la primera línea es debido a que estamos usando jQuery 1.4.x por lo que la serealización de parámetros de tipo array es diferente a como la espera asp.net mvc, por eso seteamos el modo tradicional. les dejo un link al respecto: http://forum.jquery.com/topic/jquery-1-4-breaks-asp-net-mvc-parameter-posting

Luego, simplemente estamos tomando los valores de las listas, pasándolos a un array y armando el post.

Código completo

http://dl.dropbox.com/u/7345566/MvcAndjQuery.7z

Proceso asincrónico en asp.net mvc con jquery

Lo que vamos a hacer en esta oportunidad es una aplicación asp.net mvc con un proceso de duración considerable como para querer tener una barra de progreso usando jquery y jquery.ui

Básicamente, esto se logra haciendo llamadas asincrónicas (e independientes) desde la view a las acciones del controller.

mvc

Controller

En el controller vamos a tener dos acciones, una que se va a invocar mediante un POST (el proceso principal) y otra que se va a invocar mediante otro POST y va a devolver un JSON (consulta del estado del proceso).

Nota: Vamos a hacer uso de los items de HttpApplication para guardar el estado del mismo y para poder ejecutar varios procesos en forma simultanea es que los mismos tienen un processId. Otra opción sería, por ejemplo, persistirlo en una base de datos.

Proceso principal
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LongProcess(int loops, string processId)
{
    HttpContext.Application["Status_" + processId] = 0;
    HttpContext.Application["Loops_" + processId] = loops;

    for (int i = 0; i < loops; i++)
    {
        Thread.Sleep(1000); //Action

        HttpContext.Application["Status_" + processId] = i + 1; //Update status
    }
    return View("Result", new ResultModel {Loops = loops});
}

donde Thread.Sleep(1000) está representando la duración de una etapa del proceso.

Consulta del estado del proceso

aquí tomamos los valores del estado del proceso, calculamos el porcentaje y armamos una respuesta JSON.

[AcceptVerbs(HttpVerbs.Post)]
public JsonResult GetStatus(string processId)
{
    int status = HttpContext.Application["Status_" + processId] != null
                    ? (int)HttpContext.Application["Status_" + processId]
                    : 0;
    int loops = HttpContext.Application["Loops_" + processId] != null
                    ? (int)HttpContext.Application["Loops_" + processId]
                    : 0;

    int value = 0;
    if (status != 0 && loops != 0)
        value = status * 100 / loops;

    return Json(new { Result = value });
}

View

La vista propiamente dicha contiene un div con los controles que realizan la invocación:

  • un formulario: fromprocess
  • un hidden para guardar el Id de proceso: processId
  • otro hidden para guardar el Id del intervalo que consulta el estado periódicamente: intervalId
  • una caja de texto para ingresar la cantidad de ciclos del loop que estamos simulando: loops.
  • un botón de tipo submit para invocar al post del formulario.

y otro div para mostrar el progreso, el estado y el resultado.

<h2><%= Html.Encode(ViewData["Message"]) %></h2>
<% using (Html.BeginForm("LongProcess", "Home", FormMethod.Post, new {id = "fromprocess"}))
   {%>
    <div id="invoke">
        <fieldset>
            <%=Html.Hidden("processId", Model.ProcessId, new { id = "processId" })%>
            <input id="intervalId" type="hidden" />
            <%= Html.TextBox("loops", 10, new {id = "loops"}) %>
            <input type="submit" value="Run Long process" />
        </fieldset>
    </div>
    <div id="progress">
        <div id="progressbar"></div>
        <div id="status"></div>
        <div id="result"></div>
    </div>
<% } %>

La función javascript para consultar el estado

Esta función consulta el estado del proceso haciendo un POST e interpretando el JSON de respuesta. En url indicamos el link a consultar, en data los parámetros de la acción (en el controller) y cuando la consulta se completa tomamos el resultado (que ya viene parseado a JSON), asignamos la propiedad Result al progressbar y en texto al div status.

function updateProgress() {
    $.ajax({
        url: '<%=Url.Action("GetStatus", "Home") %>',
        data: { processId: $("#processId").val() },
        success: function(data) {
            if (data != null) {
                $("#progressbar").progressbar("option", "value", data.Result);
                $("#status").html("Progress: " + data.Result + "%");
            }
        },
        error: function(msg) {
            $("#status").html("Error: " + msg);
        }
    });
}
La función javascript para hacer el POST asincrónico

cuando la página se carga estamos inicializando algunas propiedades…

  • en la primera línea estamos configurando el div progressbar como una progressbar de jquery.
  • en la segunda estamos ocultando el div progress (será visible cuando se inicie el proceso).
  • la tercer línea es muy importante para que funcione correctamente en IE, sino IE hace un cache de las llamadas ajax y nunca vuelve a consultar el estado al servidor sino que se queda con la primer respuesta. (para FF y Chrome no es necesaria).
  • en la cuarta línea estamos agregando una función al submit del formulario de nuestra página que va a realizar la misma invocación que haría el formulario pero en forma asincrónica.
$(document).ready(function() {
    $("#progressbar").progressbar({ value: 0 });
    $("#progress").hide();

    $.ajaxSetup({ cache: false }); // this line is needed to ajax on IE

    $("#fromprocess").submit(function() {
        $("#invoke").hide(1000);

        $("#progressbar").progressbar("option", "value", 0);
        $("#status").html("loading...");
        $("#progress").show(1000);

        $.ajax({
            type: "POST",
            url: $("#fromprocess").attr("action"),
            data: $("#fromprocess").serialize(),
            success: function(data) {
                clearInterval($("#intervalId").val());
                $("#progressbar").progressbar("option", "value", 100);
                $("#status").html("complete");
                $("#result").html(data);
            },
            error: function(msg) {
                clearInterval($("#intervalId").val());
                $("#status").html("error");
                $("#result").html(msg);
            }
        });

        var intervalId = setInterval("updateProgress()", 100);
        $("#intervalId").val(intervalId);

        return false;
    });
});

la función que acabamos de poner en el submit hace una invocación ajax de tipo POST a url que tiene configurada como action el form fromprocess, en data envía el formulario serializado, esto hace que no nos quedemos esperando el resultado del post sino que indicamos una función js que se va a invocar cuando el proceso se complete.

Luego seteamos un intervalo de ejecución para la función updateProgress y guardamos el Id del intervalo para, que cuando se complete el proceso, detenerlo.

Cuando se completa el proceso:

  • detenemos la consulta de estado
  • seteamos el progressbar en 100%
  • ponemos el status en complete
  • mostramos el resultado en el div result.

Código completo

para descargar el código completo, click aquí