[Tutorial] WebForms & jQueryUI con Drag & Drop
Hola, hoy veremos un tutorial de como implementar un pequeño sistema en donde vamos a utilizar Drag & Drop con jQueryUI. El escenario es el siguiente, tenemos por un lado la columna de Salas y por otro lado la columna de Pacientes.

Los requerimientos son:
- Las salas deben obtenerse desde la base de datos
- Los pacientes deben obtenerse desde la base de datos
- Se debe permitir arrastrar un paciente a una sala y registrar su posicion
- Se debe permitir el movimiento de pacientes entre salas
- Se debe permitir sacar al paciente desde una sala (arrastrar a la columna de pacientes)
- Se debe regitrar todo movimiento del paciente
Con estos requrimientos,, comenzamos a construir nuestra modelo base de datos, el cual es bastante simple y cumple con lo que necesitamos:

Para facilitar el manejo de los datos , voy a utilizar LinQtoSQL, para ello vamos a tener el siguiente contexto:

Con esto ya tenemos nuestro modelo de datos, veamos ahora los métodos que nos van a permitir obtener la lista de Salas y Pacientes que asociaremos a DataList y Repetears.
EL primer método hace una busqueda de todos los pacientes que estan en “la sala de espera” a esa sala le puse el Id=4, y también llamamos al metodo obtSalasAll, que se obtiene todas las salas (menos la sala 4, es la sala de espera)
Public Sub bindAll()
DataListPaciente.DataSource = obtPacientesAll(4)
DataListPaciente.DataBind()
DataListSalas.DataSource = obtSalasAll()
DataListSalas.DataBind()
End Sub
Public Function obtPacientesAll(ByVal sala As Integer) As List(Of PacienteUI)
Dim contexto As New contextoDataContext
Dim lista = (From p In contexto.Pacientes Where p.IdSala = sala
Select IdPaciente = p.IdPaciente,
Nombre = p.NombrePaciente,
color = p.Prioridade.ColorPrioridad,
IdSala = p.IdSala)
Dim listaPacienteUI As New List(Of PacienteUI)
For Each obj In lista
listaPacienteUI.Add(New PacienteUI() With {.Color = obj.color,
.NombrePaciente = obj.Nombre, .IdPaciente = obj.IdPaciente,
.IdSala = obj.IdSala})
Next
Return listaPacienteUI
End Function
Public Function obtSalasAll() As List(Of Sala)
Dim contexto As New contextoDataContext
Dim lista = (From p In contexto.Salas Order By
p.NombreSala Where p.IdSala <> 4)
Return lista.ToList
End Function
Para el mostrar los pacientes dentro de la sala de espera, tengo un objeto definido para ese fin, si quieres podrias mostrar el mismo que entrega el contexto.
Class PacienteUI
Private _IdPaciente As Integer
Public Property IdPaciente() As Integer
Get
Return _IdPaciente
End Get
Set(ByVal value As Integer)
_IdPaciente = value
End Set
End Property
Private _nombre As String
Public Property NombrePaciente() As String
Get
Return _nombre
End Get
Set(ByVal value As String)
_nombre = value
End Set
End Property
Private _color As String
Public Property Color() As String
Get
Return _color
End Get
Set(ByVal value As String)
_color = value
End Set
End Property
Private _IdSala As Integer
Public Property IdSala() As Integer
Get
Return _IdSala
End Get
Set(ByVal value As Integer)
_IdSala = value
End Set
End Property
End Class
Para cada cambio de posición, ya sea entre salas y a la sala de espera, vamos a updatear mediante una llamada mediante ajax a un webservice indicando el paciente y la sala. Esta llamada la voy a hacer mediante el registro de la referencia del WebService en el ScriptManager. Luego mediante JavaScript invocaremos al WService.
Este WebService como puedes ver actualiza la posición del paciente y luego inserta en la tabla de moviemientos su nueva Sala, siempre y cuando sea distinta a a que ya tenia asignada. Si bien esto lo vamos a validar tambien en el lado del cliente, es importante hacerlo acá, ya que se podría consumir este servicio desde otra fuente.
El código del WebService:
Imports System.Web
Imports System.Web.Services
Imports System.Web.Services.Protocols
<System.Web.Script.Services.ScriptService()> _
<WebService(Namespace:="http://tempuri.org/")> _
<WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
Public Class RegistraMovimientosWS
Inherits System.Web.Services.WebService
<WebMethod()> _
Public Sub RegistraPosicion(ByVal IdPaciente As Integer,
ByVal IdSala As Integer)
Dim contexto As New contextoDataContext
Dim paciente As New Paciente
paciente = (From p In contexto.Pacientes Where p.IdPaciente = IdPaciente).Single
If paciente.IdSala <> IdSala Then
paciente.IdSala = IdSala
Dim movimiento As New Movimiento
movimiento.IdPaciente = IdPaciente
movimiento.IdSala = IdSala
movimiento.Fecha = Date.Now
contexto.Movimientos.InsertOnSubmit(movimiento)
contexto.SubmitChanges()
End If
End Sub
End Class
Veamos ahora la estructura de nuestro WebForms, primero la columna de Sala de Espera de Pacientes (Columna derecha).
<form id="form1" runat="server">
<asp:ScriptManager ID="sm1" runat="server" >
<Services>
<asp:ServiceReference InlineScript="false"
Path="~/RegistraMovimientosWS.asmx" />
</Services>
</asp:ScriptManager>
<asp:Button ID="Button1" style="visibility:hidden" runat="server" Text="Button" />
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<div>
<div id="salas">
<asp:DataList ID="DataListSalas" EnableViewState="false"
runat="server" RepeatColumns="1"RepeatDirection="Horizontal">
<ItemTemplate>
<div id="<%#Eval("IdSala")%>" class="block"
style="background-color: #<%#Eval("ColorSala")%>">
<div style="text-align: center; font-size: 18px; border: solid 1px">
<%#Eval("NombreSala")%></div>
<asp:HiddenField ID="hidsala" runat="server"
Value='<%# eval("IdSala") %>' />
<asp:Repeater ID="DataListPacientesSalas" runat="server"
EnableViewState="false">
<ItemTemplate>
<div class="pacienteEnSala arrastra"
sala="<%#Eval("IdSala")%>" id="<%#Eval("IdPaciente")%>">
<%#Eval("NombrePaciente")%></div>
</ItemTemplate>
</asp:Repeater>
</div>
</ItemTemplate>
</asp:DataList>
</div>
<div id="pacientes">
<asp:DataList ID="DataListPaciente" runat="server"
RepeatColumns="3" RepeatDirection="Vertical">
<ItemTemplate>
<div id="<%#Eval("IdPaciente")%>"
sala="<%#Eval("IdSala")%>"
class="blockPaciente arrastra"
style="background-color: #<%#Eval("Color")%>">
<div style="text-align: center; font-size: 18px; border: solid 1px;">
<%#Eval("NombrePaciente")%></div>
</ItemTemplate>
</asp:DataList>
</div>
</div>
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" EventName="Click" />
</Triggers>
</asp:UpdatePanel>
</form>
En vista de diseño veríamos algo así:

Puedes notar también que existe un boton que está oculto mediante un estilo, este servirá para generar un postback asincrono para recargar los datos que tienen los elementos datalist y repeaters.
Veamos el código que va a dar soporte a la funcionalidad
Primero que nada, vamos a incluir las librerias jQuery y JqueryUI en nuestro proyecto
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js" type="text/javascript"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"type="text/javascript"></script>
Luego vamos a definir la funcionalidad de Drag & Drop mediante el plugin de JqueryUI. Debemos definir cual es el estilo que será seleccionado para atachar el comportamiento de arrastrar (draggable), en mi caso el estilo arrastra,lo mismo ocurre con droppable, en donde definimos la funcionalidad a realizar cuando se hace el drop:
-
Obtenemos el Id del objeto arrastrable, que corresponde al IdPaciente
-
Obtenemos el Id del objeto que acepta al elemento arrastrable, es decir la sala.
-
Obtenemos el Id de la sala que ya tiene asociado el IdPaciente, esto es importante para no hacer la llamada asincrona cuando movemos el objeto dentro del mismo contenedor.
-
Removemos la clase para mostrar un elemento más pequeño dentro de la sala.
-
LLamamos al Webservice mediante Microsoft Ajax (Recuerda que ya lo registramos en el ScriptManager)
-
Hacemos un PostBack Asíncrono para refrescar los webcontrols.
Lo mismo se hace con el la columna de pacientes, el concepto del Drop de jQueryUI es que definas el selector del objecto que va a ser draggable y el objeto que será droppable y cual es el objeto que acepta: “arrastra”, entonces, así por ejemplo el estilo Block acepta que se arrastren dentro de él objetos “arrastra”.
En la columna pacientes, la sala de espera es la Sala 4, esto se puede mejorar en el código (en eso estoy pero queria postear rápido este ejemplo).
<script type="text/javascript">
window.Error = function(msg) {
alert(msg);
}
function UpdPanelUpdate() {
__doPostBack("<%= Button1.ClientID %>", "");
}
function creaComportamiento() {
$(".arrastra").draggable({ revert: 'invalid' });
$(".block").droppable({
accept: '.arrastra',
activeClass: 'droppable-active',
hoverClass: 'droppable-hover',
drop: function(event, ui) {
var idPaciente = parseInt($(ui.draggable).attr('id'));
var idSala = $(this).attr('id');
var idSalaAnterior = $(ui.draggable).attr('sala');
if (idSala != idSalaAnterior) {
$(ui.draggable).removeClass("blockPaciente");
RegistraMovimientosWS.RegistraPosicion(idPaciente,
idSala);
UpdPanelUpdate();
}
}
});
$("#pacientes").droppable({
accept: '.arrastra',
drop: function(event, ui) {
$(ui.draggable).addClass("blockPaciente");
var idSalaAnterior = $(ui.draggable).attr('sala');
if (4 != idSalaAnterior) {
var idPaciente = parseInt($(ui.draggable).attr('id'));
RegistraMovimientosWS.RegistraPosicion(idPaciente, 4);
UpdPanelUpdate();
}
}
});
}
$(document).ready(function() {
creaComportamiento();
});
</script>
Ya habrás notado que todo el comportamiento está encapsulado en “crearComportamiento”, esto tiene una razón de ser, y es que cada vez que el updatePanel hace un post asíncrono , vuelve a crear los objetos del DOM , y no se vuelve a ejecutar el $(document).ready lo que conlleva que este código como está solo se ejecuta la primera vez, una vez hecho el postback deja de funcionar, debido a que no existe nuevamente el bind del comportamiento que proporciona jqueryui a los componentes. La solución es sencilla, y es que cada vez que llamamos al UpdPanelUpdate() en donde ocupamos como trigger un postback con el botón oculto, llamamos a un método que hace el bindAll y luego llama a “crearComportamiento” mediante el ScriptManager.RegisterStartupScript, con esto nos aseguramos que siempre se vuelva a cargar la funcionalidad despues de los refresh del UpdaPanel.
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
bindAll()
ScriptManager.RegisterStartupScript(Me, Me.GetType, "script",
"creaComportamiento();", True)
End Sub
Y listo! tenemos la funcionalidad que deaseamos cubriendo las necesidades de los requerimientos que planteamos al principio 
Descarga el ejemplo completo con los CSS utilizados acá:
Espero que te sea útil, nos vemos!
@chalalo