Cómo implementar bloqueos de registro en CRM 3.0 (Servicios Web + JScript)

A raíz de una pregunta en los grupos de news de Microsoft CRM 3.0, dónde un compañero preguntaba si había alguna forma de implementar bloqueos de registros en Microsoft CRM 3.0, se me ha ocurrido esta pequeña solución. Que aunque no es una maravilla, comentaremos después algunas de sus debilidades, puede servir para muchos escenarios. Y por lo menos, es muy ilustrativa en la utilización de servicios web desde código JScript en lado cliente.


Antes de nada, para aquellos que este un poco despistados, vamos a repasar como maneja la concurrencia Microsoft CRM 3.0. De serie, la herramienta no implementa ningún tipo de bloqueo, de manera que si dos usuarios abren a la vez un registro para editarlo en ningún momento serán avisados de que otro usuario se encuentra editando el registro. Además, a la hora de guardar los cambios tampoco se realiza ninguna comprobación, de manera que el último que guarda los cambios es el que gana (sus cambios son los que quedan reflejados en el CRM). Está claro que esta forma de tratar la concurrencia tiene sus ventajas y sus inconvenientes, no quiero liarme aquí a discutir sobre esto, de hecho yo la considero ventajosa en la mayoría de los escenarios. Pero el caso es que puede que en algún escenario concreto necesitemos implementar algún tipo de control de concurrencia avisando al usuario de que el registro está siendo editado por un tercero, e incluso impedirle el acceso al registro mientras dure esta situación. Es decir, bloquear un registro mientras está siendo editado por otro usuario.


La solución que propongo es la siguiente. Cuando un usuario intente abrir un registro para editarlo, que ya se encuentre abierto en edición por otro usuario, avisarle de la situación e impedirle abrir el registro. Para esto necesitamos algún mecanismo que nos permita controlar los registros que se encuentren abiertos en modo de edición, incluyendo métodos para solicitar el bloqueo de un registro y su liberación. De esta manera cuando un usuario abra un formulario de edición, se solicitará el bloqueo de un registro; si no es posible bloquearlo porque está siendo editado por otro usuario se le avisará y se cerrará el formulario, y si el registro está libre se marcará como bloqueado y se permitirá que lo edite. Cuando el usuario que tiene bloqueado el registro cierre el formulario de edición (guardando o no los cambios) se liberará el bloqueo para que otros usuarios puedan acceder.


Lo primero será implementar un mecanismo que nos permita llevar el control sobre los registros bloqueados, y que tenga un método para solicitar un bloqueo y otro para liberarlo. Para ello utilizaremos un Servicio Web de ASP.Net muy sencillo, que tendrá una lista genérica de GUIDS para almacenar los registros bloqueados. Algo tal que así:







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Collections.Generic;
using System.Threading;

[WebService(Namespace = «http://update.crm.plainconcepts/»)]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class UpdateNotify : System.Web.Services.WebService
{
private static List<Guid> bloqueos = new List<Guid>();

public UpdateNotify () {
}

/// <summary>
/// Método para solicitud de bloqueos
/// </summary>
/// <param name=»entity»>GUID del objeto</param>
/// <returns>»true» si ha sido bloqueado, «false» si ya estaba bloqueado</returns>
[WebMethod]
public string NotifyEnter(Guid entity)
{
Monitor.Enter(bloqueos);
try
{
if (bloqueos.Contains(entity))
{
return «false»;
}
else
{
bloqueos.Add(entity);
return «true»;
}
}
catch (Exception)
{
return «exception»;
}
finally
{
Monitor.Exit(bloqueos);
}
}

/// <summary>
/// Método para liberación de bloqueos
/// </summary>
/// <param name=»entity»>GUID del objeto</param>
/// <returns>»true» si se ha liberado, «false» si no estaba bloqueado</returns>
[WebMethod]
public string NotifyExit(Guid entity)
{
Monitor.Enter(bloqueos);
try
{
if (!bloqueos.Contains(entity))
{
return «false»;
}
else
{
bloqueos.Remove(entity);
return «true»;
}
}
catch (Exception)
{
return «exception»;
}
finally
{
Monitor.Exit(bloqueos);
}
}
}


Como veis, la clase del servicio web contiene una lista genérica (.Net 2.0) de GUIDS declarada como variable estática para que sea compartida por todos los threads. Y dos métodos, uno para solicitar un bloqueo (que devolverá «true» o «false» dependiendo si se ha podido bloquear o ya estaba bloqueado) y otro para liberar un bloqueo. Como veis los métodos han de usar algún mecanismo para controlar la concurrencia en el acceso a la lista de GUIDS (en este caso la clase Monitor).


Bien, ahora que ya tenemos creado el control de registros abiertos para edición tenemos que usarlo. ¿Cómo? Pues añadiendo código en el lado cliente que llame a los servicios web con nuestro viejo amigo JScript, en los formularios de las entidades del CRM en las que queramos aplicar bloqueos. Y aquí viene uno de los problemas más gordos, necesitamos dos cosas: llamar al servicio web para solicitar un bloqueo al abrir el formulario de edición y llamar al servicio web para liberar el bloqueo al cerrar el formulario. Para lo primero no hay problema, podemos utilizar el método OnLoad() del formulario, pero para lo segundo no nos sirve el método OnSave() ya que puede que el usuario cierre el formulario sin guardar los cambios, y desde la personalización no tenemos acceso a más métodos. Este pequeño problema me ha traído de cabeza un par de días, pero repasando «viejos» libros y documentación de javascript he encontrado una solución. Asignarle una función «anónima» al evento window.onunload (se dispara al cerrar el formulario o al navegar hacia otra página) que se encargue de liberar el bloqueo, de esta forma en el OnLoad(), después de bloquear el registro, le asignamos la función que necesitamos al window.onunload y nos aseguramos la liberación del bloqueo. El código para el OnLoad() del formulario quedaría así:







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var webServiceUrl = «http://localhost:5555/CrmExtensions/UpdateNotify.asmx»;

//Check if the client is online
if (IsOnline())
{

//Check if this is an update form
if (crmForm.FormType == 2)
{
var xmlObj = new ActiveXObject(«Msxml2.DOMDocument») ;
var sXml = «<?xml version=»1.0″ ?>» ;
sXml += «<soap:Envelope «
sXml += «xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance» « ;
sXml += «xmlns:xsd=»http://www.w3.org/2001/XMLSchema» « ;
sXml += «xmlns:soap=»http://schemas.xmlsoap.org/soap/envelope/»>» ;
sXml += «<soap:Body>» ;
sXml += «<NotifyEnter xmlns=»http://update.crm.plainconcepts/»>» ;
sXml = sXml + «<entity>» + crmForm.ObjectId + «</entity>» ;
sXml += «</NotifyEnter></soap:Body></soap:Envelope>»

// Try to parse the XML string into DOM object
xmlObj.loadXML(sXml) ;
var xmlHTTP = new ActiveXObject(«Msxml2.XMLHTTP») ;
xmlHTTP.Open ( «Post», webServiceUrl, false) ;
xmlHTTP.setRequestHeader(«SOAPAction», «http://update.crm.plainconcepts/NotifyEnter») ;
xmlHTTP.setRequestHeader(«Content-Type», «text/xml; charset=utf-8» ) ;
xmlHTTP.Send(xmlObj.xml) ;
var xmlResponse = xmlHTTP.responseXML ;
var result = xmlResponse.selectSingleNode(«soap:Envelope/soap:Body/NotifyEnterResponse/NotifyEnterResult»).text ;

if (result!=«true»)
{
alert(«Atención! El registro está siendo editado por otro usuario»);
window.close();
}
else
{
window.onunload = function () {

var webServiceUrl = «http://localhost:5555/CrmExtensions/UpdateNotify.asmx»;
var xmlObj = new ActiveXObject(«Msxml2.DOMDocument») ;
var sXml = «<?xml version=»1.0″ ?>» ;
sXml += «<soap:Envelope «
sXml += «xmlns:xsi=»http://www.w3.org/2001/XMLSchema-instance» « ;
sXml += «xmlns:xsd=»http://www.w3.org/2001/XMLSchema» « ;
sXml += «xmlns:soap=»http://schemas.xmlsoap.org/soap/envelope/»>» ;
sXml += «<soap:Body>» ;
sXml += «<NotifyExit xmlns=»http://update.crm.plainconcepts/»>» ;
sXml = sXml + «<entity>» + crmForm.ObjectId + «</entity>» ;
sXml += «</NotifyExit></soap:Body></soap:Envelope>»

// Try to parse the XML string into DOM object
xmlObj.loadXML(sXml) ;

var xmlHTTP = new ActiveXObject(«Msxml2.XMLHTTP») ;
xmlHTTP.Open ( «Post», webServiceUrl, false) ;
xmlHTTP.setRequestHeader(«SOAPAction», «http://update.crm.plainconcepts/NotifyExit») ;
xmlHTTP.setRequestHeader(«Content-Type», «text/xml; charset=utf-8» ) ;
xmlHTTP.Send(xmlObj.xml) ;
var xmlResponse = xmlHTTP.responseXML ;
var result = xmlResponse.selectSingleNode(«soap:Envelope/soap:Body/NotifyExitResponse/NotifyExitResult»).text ;
}

}
}
}


Como veis, antes de llamar al servicio web nos aseguramos de que nos encontremos en un cliente que esté OnLine (si no no habrá acceso al servicio web) y que el formulario sea de acutalización. Luego creamos un documento XML con el mensaje SOAP a enviar al Servicio Web solicitando el bloqueo del registro y lo enviamos usando XMLHttp. Si la respuesta es «true» hemos bloqueado el registro, con lo que tenemos que añadir la función que libere el bloqueo mediante otra llamada al Servicio Web en el window.onunload. Si la respuesta es «false» avisamos al usuario de que el registro está siendo editado por otro usuario y cerramos el formulario.



Cómo ya comenté al principio, esta solución tiene algunos problemillas e incomodidades. Los que se me ocurren son estos, pero puede haber más (espero vuestros comentarios J) :



  • Debemos añadir el código para el OnLoad a los formularios de todas las entidades en las que queramos aplicar bloqueos (puede ser un poco coñazo)
  • No estoy seguro de que el utilizar una variable estática asegure que sólo existe una lista de GUIDS (puede haber múltiples cargadores de clase ¿no?), tengo serias dudas sobre esto y creo que debería ser mejorado. Si alguien puede arrojar luz al respecto se lo agradecería, es lo que más me preocupa.
  • Este mecanismo sólo es aplicable para clientes que estén OnLine().
  • Si por alguna razón falla la llamada para liberación del bloqueo, habrá que reiniciar el AppPool del servicio web para que libere todos los bloqueos.
  • No tengo claro que utilizar el evento window.onunload sea una personalización soportada, lo que podría dar problemas en actualizaciones.

De cualquier forma, la solución funciona y es bastante «elegante», y además creo que puede ser bastante ilustrativa sobre la potencia de las personalizaciones en el lado cliente en combinación con llamadas a Servicios Web. Me gustaría mucho que me dieseis vuestra opinión.


Un Saludo,


Marco Amoedo

Un comentario sobre “Cómo implementar bloqueos de registro en CRM 3.0 (Servicios Web + JScript)”

  1. Haz que si está bloqueado el registro, después de avisar, no se cierre el formulario, sino que se abra en modo consulta sin posibilidad de modificarlo. Hay usuarios con permisos de edición, que posiblemente no quisieran modificar datos, sino sólo consultar, y con esta acción, por lo menos, dejas consultar.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *