Impersonación en ensamblados .Net de Workflow en Microsoft CRM

El otro día surgía en los foros la pregunta sobre como impersonar al usuario en las llamadas a los servicios web del CRM dentro del código .Net de un ensamblado de workflow. La documentación del SDK tiene un pequeño error sobre cómo conseguir esto, así que vamos a intentar aclarar cómo podemos conseguir está impersonación y porque es necesaria.


El Escenario


Como bien sabemos, en Microsoft Dynamics CRM disponemos de un motor de workflow que nos permite crear procesos de negocio que automaticen la realización de algunas tareas, y además, este motor de workflow puede ser extendido incluyendo nuevas acciones mediante código .Net 1.1.


El motor de Workflow se ejecuta como un servicio de Windows, «Microsoft CRM Workflow Service», y como cualquier servicio lo hace bajo unas determinadas credenciales de usuario. Por defecto, estas credenciales suelen ser las del Servicio de Red, aunque podemos cambiarlas en cualquier momento a través de la configuración del servicio. Este motor de workflow es el que se encarga de ejecutar las acciones programadas en los procesos de workflow, y por lo tanto será él el que ejecute el código de los ensamblados que hayamos añadido.


Es típico que desde este código personalizado queramos llamar a los servicios web del CRM para completar alguna tarea. Y en la mayor parte de las ocasiones queremos hacerlo en nombre del usuario que ha disparado la regla (impersonation), es decir, ejecutando la llamada a los servicios web con las credenciales del usuario. Sin embargo, si no hacemos nada el código se ejecutará bajo las credenciales que utilice el Servicio de Workflow (Servicio de Red por defecto) con lo que no conseguiremos obtener el resultado deseado.


Las credenciales del servicio de Workflow pueden ser modificadas para utilizar las de un usuario del CRM, de esta manera todas las llamadas a los servicios web desde un ensamblado de workflow utilizarán por defecto esas credenciales. Sin embargo, hacer esto no es una buena práctica, y puede introducir riesgos de seguridad.


Impersonación


Para hacer que las llamadas a los servicios web utilicen otras credenciales disponemos de varias opciones. La primera sería establecer unas nuevas credenciales «a mano» en el proxy del servicio web. Es decir fijar las credenciales de un determinado usuario mediante su nombre de login y contraseña.







1
2
3
4
//Create crm service proxy
CrmService service = new CrmService();
service.Url = «http://localhost:5555/mscrmservices/2006/crmservice.asmx»;

service.Credentials = new System.Net.NetworkCredential(«user», «password», «domain»);


De esta forma las llamadas al servicio web se harán en nombre de este usuario. Pero esto no es lo que buscamos, ya que de esta forma no estamos impersonando al usuario que dispara el workflow sino a un usuario específico del CRM, aunque en muchos casos esta es una solución válida.


En el SDK de Microsoft Dynamics CRM se comenta (ver código a continuación) un método para conseguir esto. Que se basa en fijar las credenciales por defecto, y establecer la propiedad CallerId (una cabecera de los mensajes SOAP del Servicio Web del CRM) con el valor del guid del usuario al que queremos impersonar.







1
2
3
4
5
6
CrmService service = new CrmService();
service.Credentials = System.Net.CredentialCache.DefaultCredentials;

// Get the current user ID.
WhoAmIRequest userRequest = new WhoAmIRequest();
WhoAmIResponse userResp = (WhoAmIResponse) service.Execute(userRequest);

service.CallerIdValue.CallerGuid = userResp.UserId;


Sin embargo, este método no consigue el resultado deseado ya que la utilización del mensaje WhoAmI devuelve, entre otras cosas, el guid del usuario a partir de las credenciales con las que se realiza la llamada (útil para aplicaciones integradas en el CRM), con lo que si lo hacemos desde un ensamblado de workflow nos devolverá el guid de la cuenta sobre la que se esté ejecutando el Servicio de Workflow. Cómo normalmente este se ejecuta con las credenciales del Servicio de Red, nos devolverá un guid que no pertenece a ningún usuario, si no a una cuenta de usuario especial llamada SYSTEM que no tiene permisos para realizar la mayoría de las tareas del CRM.


Entonces, ¿Cómo lo conseguimos? La respuesta está en usar el CallerID (también se menciona en el SDK). Se trata de un XML que contiene el guid del usuario que dispara la regla de workflow, y que puede ser pasado como parámetro por el motor de workflow al método del ensamblado .net. Para ello, a la hora de registrar nuestro ensamblado en el workflow.config, tendremos que especificar un parámetro del tipo caller. Y en nuestro código tendremos que recuperar del XML que viene en este parámetro el guid del usuario, podemos hacerlo mediante una función como la del ejemplo.







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
        private static Guid GetCaller(string callerXml)
{
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(callerXml);

Guid caller = new Guid(xmldoc.DocumentElement.SelectSingleNode(«//caller/userid»).InnerText);
return caller;
}

public bool sendFax(String callerId)
{

//Create crm service proxy
CrmService service = new CrmService();
service.Url = «http://localhost:5555/mscrmservices/2006/crmservice.asmx»;


// Get the user who triggered the workflow rule
Guid user = GetCaller(callerId);

// Impersonate the user
service.Credentials = System.Net.CredentialCache.DefaultCredentials;
service.CallerIdValue = new CallerId();
service.CallerIdValue.CallerGuid = user;
…..


De esta forma conseguimos realmente impersonar al usuario que dispara la ejecución de la regla de workflow, y realizar operaciones en el CRM mediante los servicios web utilizando sus identidad (y sus permisos).


Espero que este rollo haya servido para aclarar un poquito el concepto de impersonación en el CRM y cómo podemos utilizarlo para llamar a los servicios web desde ensamblados personalizados de workflow. A continuación os dejo un ejemplo completo de un ensamblado que crea una actividad de fax utilizando impersonando al usuario. Proximamente, más ejemplos. Espero vuestro comentarios.


Un Saludo,


Marco Amoedo


Ejemplo Completo


– Workflow.config –







1
2
3
4
5
6
7
8
9
10
11
12
13
14
<workflow.config xmlns=»http://microsoft.com/mscrm/workflow/» allowunsignedassemblies=»true»>
  <methods>

….

    <method name=»Send Fax»
      assembly=»NotificationWorkFlow.dll»
      typename=»NotificationWorkFlow.Fax»
      methodname=»sendFax»
      group=»Plain Concepts»
      timeout=»7200″>
      <parameter name=»Caller» datatype=»caller»/>
      <result datatype=»boolean»/>
    </method>
  </methods>
</workflow.config>


– Código .Net –







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
using System;
using System.Text;
using CrmSdk2.crmsdk; //Referencia a libreria con Proxy de los servicios Web, hay que añadirla la dll
using System.Xml;

namespace NotificationWorkFlow
{
public class Fax
{
private static Guid GetCaller(string callerXml)
{
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(callerXml);

Guid caller = new Guid(xmldoc.DocumentElement.SelectSingleNode(«//caller/userid»).InnerText);
return caller;
}

public bool sendFax(String callerId)
{

//Create crm service proxy
CrmService service = new CrmService();
service.Url = «http://localhost:5555/mscrmservices/2006/crmservice.asmx»;

// Get the user who triggered the workflow rule
Guid user = GetCaller(callerId);

// Impersonate the user
service.Credentials = System.Net.CredentialCache.DefaultCredentials;
service.CallerIdValue = new CallerId();
service.CallerIdValue.CallerGuid = user;

//Build the owner property
Owner propietario = new Owner();
propietario.Value = user;
//Build the fax object
fax fax = new fax();

//Fill fax’s properties and set the owning user
fax.subject = «Test Fax from Admin»;
fax.description = «New Fax»;
fax.ownerid = propietario;

// Create the party sending and receiving the fax.
activityparty party = new activityparty();

//Set the properties of Activityparty.
party.partyid = new Lookup();
party.partyid.type = EntityName.systemuser.ToString();
party.partyid.Value = user;

// The party sends and receives the fax.
fax.from = new activityparty[] { party };
fax.to = new activityparty[] { party };

// Create the fax.
Guid createdFaxId = service.Create(fax);

return true;

}


}

}


Más información en el SDK de Microsoft Dynamics CRM.

Continuando con la implementación de bloqueos en CRM 3.0

La semana pasada vimos una forma de implementar un sistema para establecer bloqueos de registros en el CRM. Es decir, evitar que dos usuarios pudiesen editar a la vez el mismo registro, de forma que si un usuario tiene abierto el formulario de edición de un registro otro usuario no lo pueda abrir.


En el post me comentabais que sería mejor abrir el registro en modo de sólo lectura en vez de cerrarle la ventana al usuario (muchas gracias por el comentario), y la verdad es que estoy totalmente de acuerdo en que es una forma mejor de tratar los bloqueos. Así que me he puesto a probar, y con sólo cambiar una línea del código que teníamos, ya solucionamos este problema.


El CRM tiene una página «especial» que permite mostrar un objeto en un formulario en modo sólo lectura, se trata de http://servidorcrm:5555/_forms/readonly/readonly.aspx?objTypeCode=[Código de Entidad]&id=[GUID del Objeto]. Gracias a este «generador» de formularios de sólo lectura, y a que en el código JavaScript disponemos de las propiedades crmForm.ObjectTypeCode y crmForm.ObjectId que nos indican el tipo de entidad y GUID del registro, podemos cambiar la línea 30 del código para que en vez de cerrar el formulario muestre un sin posibilidad de editar.


Línea 30 JavaScript (Antigua): window.close();


Línea 30 JavaScript (Nueva): window.navigate(«http://crm:5555/_forms/readonly/readonly.aspx?objTypeCode=»+crmForm.ObjectTypeCode+»&id=»+crmForm.ObjectId);


Bien, esta es una mejora interesante con respecto a la versión anterior. Pero aún sigue habiendo pequeños inconvenientes en el sistema de bloqueos. Los principales son:



  • El sistema no es escalable porque el servicio web que controla los bloqueos no funcionaría correctamente en un cluster de IIS (por utilizar una variable estática).
  • Seguimos sin disponer de un mecanismo para liberar bloqueos, o por lo menos para ver que usuario tiene bloqueado un registro.
  • Creo que esta solución NO estaría soportada por utilizar el evento window.onunload.
  • Hay que añadir el código JavaScript una por una todas las entidades a las que queramos implementar bloqueos. Eso sí, con la ventaja de que el código es independiente de a qué entidad lo que queramos aplicar, con sólo ponerlo en el OnLoad del formulario listo.

Bueno, los dos primeros problemillas se podrían solucionar si utilizásemos una BBDD de datos para almacenar la lista de bloqueos (espero poder probarlo). Pero de cualquier forma espero que os sirva de ejemplo sobre por dónde pueden ir los tiros para los que necesitéis implementar algo así, y que me dejéis vuestras ideas y opiniones.


Un saludo,


Marco Amoedo

Titan is coming!

Microsoft acaba de anunciar que está comenzando el Technology Adoption Program (TAP) de la siguiente gran versión de Microsoft Dynamics CRM, cuyo nombre en código es «Titan». Durante el primer cuarto de 2007 habrá unos 300 partners participando en el TAP, mientras que para el segundo cuarto se espera que haya en torno a los 1000. Esto hace un buen número de empresas preparándose para el lanzamiento de «Titan» y comprobando la compatibilidad en la migración desde CRM 3.0.

En principio se espera que el lanzamiento de esta versión se produzca a mediados de año, una vez finalizado el TAP. Es de suponer que el primer lanzamiento será en Inglés y que, al igual que pasó con la versión 3.0, haya que esperar algunos meses más hasta que esté disponible en castellano.

Entre las principales novedades que nos trae el «Titan» está su nueva arquitectura «multiempresa» que será capaz de soportar varias organizaciones trabajando sobre el mismo despliegue de Microsoft CRM. Otra gran novedad es que los tres tipos de despliegues que se presentan con esta nueva versión compartirán el mismo código. Esto quiere decir que tanto si optamos por una solución «on-premise» tradicional (alojada en el cliente), como por una solución SaaS(Software as a Service) en la que el CRM se encuentre alojado en los servidores de un proveedor, o la nueva solución CRM Live, las personalizaciones y extensiones que hagamos funcionarán en cualquiera de las tres opciones. De momento poco más se sabe sobre «Titán», algo se comenta sobre mejorar las capacidades de los clientes en especial los móviles, pero durante los próximos seguramente empezaremos a descubrir más.

Desde mi punto de vista, esta versión promete grandes novedades y dará un fuerte empujón al mercado del desarrollo de extensiones para Microsoft CRM, ya que en «teoría» podrán ser utilizadas independientemente del tipo de despliegue elegido (on-premise, SaaS o CRM Live). Sin embargo, hay una creciente desconfianza sobre la plataforma CRM Live!, que permite utilizar Microsoft CRM al estilo de otras soluciones como salesforce.com, ya que muchos partners temen que esto les quite parte del mercado de implantación de la solución ¿Qué os parece a vosotros? En mi opinión es posible que reste mercado, pero a la vez abrirá nuevas posibilidades al permitir a empresas cada vez más pequeñas adoptar la solución CRM de Microsoft, permitiendo que los partners puedan vender sus desarrollos a nuevos clientes. Creo que habrá que esperar a ver como evoluciona, y a ver si la cuota de mercado que se pierde en implantación es compensada por la que se gana en posibles clientes para desarrollos y extensiones.

Por mi parte estoy deseando tenerlo en mis manos, y poder comenzar a cacharrear con «Titan». Estoy intentando participar en el TAP gracias a ser MVP, y si lo consigo prometo manteneros informados siempre que los NDA (acuerdos de confidencialidad) me lo permitan.

Un saludo,

Marco Amoedo

Fuente: Microsoft Press Pass

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

Empezando el año como MVP

Ayer recibí un email en el que me informaban de que he sido reconocido como Most Valuable Professional, un galardón que Microsoft otorga a aquellas personas que participan muy activamente en las comunidades sobre las tecnologías de Microsoft.


 


Quiero daros las gracias a todos los que leéis el blog y participáis en los foros de Microsoft Dynamics CRM porque sin vosotros esto no hubiese sido posible, y porque con vuestras preguntas y comentarios he aprendido un montón. Y en especial a mis colegas y compañeros de trabajo, de los que tanto he aprendido (y aprenderé), que me han apoyado continuamente y me han contagiado su ilusión por participar en la comunidades.


 


Es la primera vez que soy reconocido como MVP (soy novato en esto) y me hace una gran ilusión. Mi propósito para este nuevo año que empieza es poder hacer honor al galardón y participar en la comunidad aún más que en el 2006, ayudando en todo lo que pueda y aprendiendo de todos vosotros.


 


Muchísimas gracias, y mis felicitaciones a todos los nuevos o renovados MVPs


 


Marco Amoedo