Tuesday, September 29, 2009

CRM 4.0 Deploying a custom assembly


The most common and suggested deployment scenario for custom CRM web extensions (i.e. custom web service or web application) is to deploy them under the ISV folder (e.g. ISV / MyApp) and put the application assembly inside the CRMWeb \ bin folder (or wwwroot / bin when deployed on the default website).

As of rollup2 MS also recommends putting the product assemblies of your custom extensions in their own bin folder (i.e. ISV / MyApp / bin). This option also requires you to add an assembly directive

<%assembly name=”MyApp” %>


to all your pages so the asp.net will be able to bind to your server side code behind.

Here is an example:
Test.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="MyApp.Test" %>
<%@ Assembly Name="MyApp" %>


Deployment Tree:

CRMWeb \ wwwroot
|-- ISV
|-- MyApp
|-- bin
|-- MyApp.dll


Looks simple enough but many find that this does not work and produces the error

[HttpException: Could not load type 'MyApp.Test'].


The reason this does not work is that in order for asp.net to recognize and load your assembly the assembly directive must be the first directive in the page e.g.

<%@ Assembly Name="MyApp" %>
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Test.aspx.cs" Inherits="MyApp.Test" %>


And lastly a few tips to ensure successful deployment for both IFD and On-Premise environments:
Ensure that the MyApp folder is a regular Virtual Directory (not an application) – in other word your app will run under CRMAppPool.
Remove any application level nodes from your application web.config e.g.


Sunday, September 13, 2009

CRM 4.0 Records Per Page Wizard




A few months back I answered a thread on ms forums regarding running a workflow on more then 250 records which is the grid paging limit. I trimmed our record per page wizard and made it available for free on GI company website -> free wizards (left navigation).

If you’re looking for a cool productivity enhancement that lets you control how many records are displays per entity this one is surly something any user can appreciate. Feel free to send us feedbacks (feedback@gicrm.com).

RPP Wizard features:

  • Runs from CRM main application toolbar.

  • Identifies the current entity being browsed and loads with user the settings (either CRM paginglimit or saved setting).

  • Refreshes the current entity view.

  • Supports all views including associated views.

  • Supports quick and advanced find.

Thursday, September 10, 2009

CRM 4.0 Running an On Demand Workflow from JavaScript




Running a workflow from JavaScript is a great alternative to developing server side (web services) components. This is since the workflow designer is able to accomplish many programmable tasks without actually writing complex dot.net code. Implementing a simple JS framework that activates workflows is a great enhancement to any CRM project and can simplify many of your tasks.

The code integrates the notification (yellow ribbon) as a textual progress bar so the user can observe the progress of his actions.
The code also exposes a few properties that control the interval (in milliseconds) that checks the WF status, whether to refresh the page automatically or not and so forth.

The OnCrmPageLoad function below demonstrates the usage of the OnDemandWorkflow class. Paste the entire code in your onload event
and set the OnDemandWorkflow instance with the correct workflow id and properties.

Don’t forget to check your workflow as On Demand if you need to run them from code. You should also consider the logic that implements the client side call since user interaction can cause your workflow to run more then once.

Enjoy…


function OnDemandWorkflow()
{
var odw = this;
var request = null;

odw.DefaultProgress = new ProgressInfo();
odw.WorkflowId = "";
odw.OperationId = "";
odw.EntityId = "";
odw.Name = "";
odw.WorkflowStatus = -1;
odw.OnFinishCallback = null;

function UpdateNotification(msg)
{
if (!odw.DefaultProgress.Visible)
{
return;
}

var notification = document.all.Notifications;
notification.style.display = "inline";
notification.style.width = "100%";
notification.style.height = "20px";
notification.innerHTML = odw.Name + ": " + msg + "";
}

odw.Activate = function(sWorkflowId)
{
UpdateNotification("Activated");

if (sWorkflowId)
{
odw.WorkflowId = sWorkflowId
}

if (!odw.WorkflowId)
{
return UpdateNotification("Missing Workflow ID");
}

if (!odw.EntityId && crmForm.FormType == 2)
{
odw.EntityId = crmForm.ObjectId;
}
else if (!odw.EntityId)
{
return UpdateNotification("Workflow is missing an Entity name");
}

var xmlActReq = new StringBuilder();
xmlActReq.Append(GenerateSoapHeader());
xmlActReq.Append("");
xmlActReq.Append("");
xmlActReq.Append("").Append(odw.EntityId).Append("");
xmlActReq.Append("").Append(odw.WorkflowId).Append("");
xmlActReq.Append("
");
xmlActReq.Append("
");
xmlActReq.Append(GenerateSoapFooter());

odw.Execute("Execute",xmlActReq.ToString(),odw.ActivateEnd);
}

odw.Execute = function(sMethodName,sXmlRequest, fCallback)
{
if (request)
{
request.abort();
}
request = new ActiveXObject("Microsoft.XMLHTTP");
request.Open("POST", "/mscrmservices/2007/CrmService.asmx", true);
request.onreadystatechange = fCallback;
request.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/" + sMethodName);
request.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
request.setRequestHeader("Content-Length", sXmlRequest.length);
request.send(sXmlRequest);
}

odw.ActivateEnd = function()
{
if (request.readyState == 4)
{
if (!validateResponse())
{
return;
}

odw.OperationId = request.responseXML.selectSingleNode("//Response/Id").nodeTypedValue;
odw.CheckStatus();
}
}

odw.CheckStatus = function()
{
var xmlChkReq = new StringBuilder();
xmlChkReq.Append(GenerateSoapHeader());
xmlChkReq.Append("");
xmlChkReq.Append("asyncoperation");
xmlChkReq.Append("{").Append(odw.OperationId).Append("}");
xmlChkReq.Append("");
xmlChkReq.Append("");
xmlChkReq.Append("statuscode");
xmlChkReq.Append("
");
xmlChkReq.Append("
");
xmlChkReq.Append("
");
xmlChkReq.Append(GenerateSoapFooter());

odw.Execute("Retrieve",xmlChkReq.ToString(),odw.CheckStatusEnd);
}

odw.CheckStatusEnd = function()
{
if (request.readyState == 4)
{
if (!validateResponse())
{
return setTimeout(odw.CheckStatus, odw.DefaultProgress.CheckInterval);
}

odw.WorkflowStatus = request.responseXML.selectSingleNode("//q1:statuscode").nodeTypedValue;

switch(parseInt(odw.WorkflowStatus))
{
case 30:
if (odw.DefaultProgress.RefreshWhenDone)
{
if (odw.DefaultProgress.RequestRefresh)
{
window.onbeforeunload = function()
{
return "Operation has succeeded, Do you wish to refresh the page?";
}
}
else
{
crmForm.detachCloseAlert();
}

setTimeout(function(){
window.location.reload();
},1000);
}

UpdateNotification("Operation has succeeded");
if (odw.OnFinishCallback)
{
odw.OnFinishCallback(odw);
}

break;
default:

switch(parseInt(odw.WorkflowStatus))
{
case 32: //canceled
UpdateNotification("Operation was canceled");
break;
case 22: //canceling
UpdateNotification("Operation is being canceled");
break;
case 31: //failed
UpdateNotification("Operation has failed");
break;
case 20: //In progress
UpdateNotification("Operation is in progress");
break;
case 21: //Pausing
UpdateNotification("Operation is pausing");
break;
case 10: //Waiting
UpdateNotification("Operation is waiting");
break;
case 0: //Waiting for resources
UpdateNotification("Operation is waiting for resources");
break;
}

return setTimeout(odw.CheckStatus, odw.DefaultProgress.CheckInterval);
}
}
}

function validateResponse()
{

var error = request.responseXML.selectSingleNode("//error");
var faultstring = request.responseXML.selectSingleNode("//faultstring");

if (error == null && faultstring == null)
{
return true;
}
else
{
odw.DefaultProgress.Visible = true;
if (error)
{
UpdateNotification(error.text);
}
else
{
UpdateNotification(faultstring.text);
}

return false;
}
}

function GenerateSoapHeader()
{
var soapHeader = new StringBuilder();
soapHeader.Append("");
soapHeader.Append(" soapHeader.Append(" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'");
soapHeader.Append(" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>");
soapHeader.Append(GenerateAuthenticationHeader());
soapHeader.Append("");
return soapHeader.ToString();
}

function GenerateSoapFooter()
{
var soapFooter = new StringBuilder();
soapFooter.Append("
");
soapFooter.Append("
");
return soapFooter.ToString();
}

function ProgressInfo()
{
this.RequestRefresh = true;
this.RefreshWhenDone = false;
this.Visible = false;
this.CheckInterval = 2000;
}

function StringBuilder()
{
var parts = [];
this.Append = function( text ){parts[ parts.length ] = text;return this;}
this.Reset = function(){parts = [];}
this.ToString = function(){return parts.join( "" );}
}
}

function OnCrmPageLoad()
{
if (confirm("Run Workflow"))
{
var odWorkflow = new OnDemandWorkflow();
/* The workflow name - for presentation */
odWorkflow.Name = "Create Task";
var odwProgress = odWorkflow.DefaultProgress;
/* true is default - true = user has to approve the page reload. */
odwProgress.RequestRefresh = false;
/* false is default - true = try to refresh when the workflow is done successfully. */
odwProgress.RefreshWhenDone = true;
/* false is default - true = see notification progress */
odwProgress.Visible = true;
/* 2000 is default - ping the server each x milliseconds */
odwProgress.CheckInterval = 1000;
odWorkflow.WorkflowId = "76f5cecb-987c-4635-8d78-66d2caa2f9ae";
/* default Entity Id - the entity instance id (guid) */
odWorkflow.EntityId = crmForm.ObjectId;
odWorkflow.OnFinishCallback = CallmeWhenTheWFIsDone
odWorkflow.Activate();
}
}

function CallmeWhenTheWFIsDone(odw)
{
if (odw.WorkflowStatus == 30)
{
alert('more code here');
}
}

OnCrmPageLoad();

Tuesday, September 8, 2009

Formatting CRM DateTime Field


As you might have noticed CRM DateTime field DataValue returns the following value: “Tue Sep 29 13:00:00 PDT 2009”
Sometimes you need to change the value to another format e.g. dd/mm/yyyy or mm-dd-yyyy hh:MM.
This is quite easy to do with C# since the format string is pretty extensive however the JavaScript Date object is pretty slim and so the only option is to write your own framework / prototype extension.

The following script extends the JavaScript Date Object and adds a toFormattedString function which accepts common format strings such as
dd – days, mm – months, yyyy – full year, hh – hours , MM – minutes , ss – seconds , ms – milliseconds , APM – AM/PM
You can extend the prototype function further if you require additional formatting.

Here is the code and usage example:

Date.prototype.toFormattedString = function(format)
{
var d = this;
var f = "";
f = f + format.replace( /dd|mm|yyyy|MM|hh|ss|ms|APM|\s|\/|\-|,|\./ig ,
function match()
{
switch(arguments[0])
{
case "dd":
var dd = d.getDate();
return (dd < 10)? "0" + dd : dd;
case "mm":
var mm = d.getMonth() + 1;
return (mm < 10)? "0" + mm : mm;
case "yyyy": return d.getFullYear();
case "hh":
var hh = d.getHours();
return (hh < 10)? "0" + hh : hh;
case "MM":
var MM = d.getMinutes();
return (MM < 10)? "0" + MM : MM;
case "ss":
var ss = d.getSeconds();
return (ss < 10)? "0" + ss : ss;
case "ms": return d.getMilliseconds();
case "APM":
var apm = d.getHours();
return (apm < 12)? "AM" : "PM";
default: return arguments[0];
}
});

return f;
}

function OnCrmPageLoad()
{
var d = new Date(crmForm.all..DataValue);
alert(d.toFormattedString("mm-dd-yyyy hh:MM:ss ms"));
alert(d.toFormattedString("dd/mm/yyyy hh:MM APM"));
}

OnCrmPageLoad();

CRM 4.0 Creating a Readonly picklist

CRM Picklist control (HTML select tag) does not have a read-only attribute like other input html controls. The only way to make it read-only is to disable it which grays out the control completely. Once you disable the Picklist you can’t change its border or font color (i.e. make it look like a read-only field). The only way to achieve the functionality is to change the control behavior so each time the user tries to change the Picklist value the Picklist initial value is re-selected.

Here is the JS code that does the job:


function OnCrmPageLoad()
{
ReadOnlyPicklist(crmForm.all.);
}

function ReadOnlyPicklist(picklist)
{
picklist.savedIndex = picklist.selectedIndex;
picklist.onchange = function()

{
this.selectedIndex = this.savedIndex;
}
}

OnCrmPageLoad();

CRM 4.0 Changing Tab Order

A while back i posted a solution for changing vertical tabbing to horizontal tabbing. The code ran a bit slow with lookups and so the code below is a revision of the previous posting with the fix.


function OnCrmPageLoad()
{
ReArangeTabIndex();
}

function ReArangeTabIndex()
{
for( var i = 0 ; i < crmForm.all.length ; i++ )
{
var element = crmForm.all[ i ];
if (element.tabIndex)
{
if (element.className == "ms-crm-Hidden-NoBehavior" || element.tagName == "A")
{
continue;
}

element.tabIndex = 1000 + (i*10);
}
}
}

OnCrmPageLoad();