Tuesday, November 03, 2009

Custom workflow action which returns current DateTime value

Here is the code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using Microsoft.Crm.Workflow;
using System.Workflow.ComponentModel;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;

namespace CurrentDateTimeWorkflowStep
{
[CrmWorkflowActivity("Current Date Time", "Utiles")]
public class CurrentDateTime : SequenceActivity
{
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Value = CrmDateTime.Now;

return base.Execute(executionContext);
}

public static DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(CrmDateTime), typeof(CurrentDateTime));

[CrmOutput("Value")]
public CrmDateTime Value
{
get
{
return (CrmDateTime)base.GetValue(ValueProperty);
}
set
{
base.SetValue(ValueProperty, value);
}
}
}
}


And here is the source project and built assembly.

Tuesday, October 20, 2009

showModalDialog and postbacks in custom aspx pages in Microsoft Dynamics CRM 4.0

I've developed a custom aspx dialogue page. It has button with postback and when this button is clicked - action is executed but new window is opened. Solution for this case is to add <base target="_self"/> tag under head tag.

Thursday, October 01, 2009

Custom workflow action, team members and 'to' field of email record for Microsoft Dynamics CRM 4.0

There was interesting question and I decided to help author of question and create such functionality.

Here's the code of this custom workflow action:

using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using Microsoft.Crm.Workflow;
using System.Workflow.ComponentModel;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;

namespace FillEmailWithTeamMembers
{
[CrmWorkflowActivity("Fill email with team members")]
public class EmailToFiller : SequenceActivity
{
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
if (Team != null && Email != null)
{
IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext workflowContext = contextService.Context;
ICrmService crmservice = workflowContext.CreateCrmService();

//Retrieve System Users based on Team

QueryExpression query = new QueryExpression(EntityName.systemuser.ToString());
query.ColumnSet = new ColumnSet(new string[] { "systemuserid" });
LinkEntity link = query.AddLink("teammembership", "systemuserid", "systemuserid");
link.LinkCriteria.AddCondition(new ConditionExpression("teamid", ConditionOperator.Equal, Team.Value));

List<BusinessEntity> users = crmservice.RetrieveMultiple(query).BusinessEntities;

email emailInstance = new email();
emailInstance.activityid = new Key(Email.Value);

List<activityparty> receivers = new List<activityparty>();

foreach (systemuser user in users)
{
activityparty toparty = new activityparty();
toparty.partyid = new Lookup();
toparty.partyid.type = EntityName.systemuser.ToString();
toparty.partyid.Value = user.systemuserid.Value;

receivers.Add(toparty);
}

emailInstance.to = receivers.ToArray();

crmservice.Update(emailInstance);

SendEmailRequest request = new SendEmailRequest();
request.EmailId = Email.Value;
request.IssueSend = true;
request.TrackingToken = "";
crmservice.Execute(request);
}

return ActivityExecutionStatus.Closed;
}

public static DependencyProperty TeamProperty = DependencyProperty.Register("Team", typeof(Lookup), typeof(EmailToFiller));

[CrmInput("Team")]
[CrmReferenceTarget("team")]
public Lookup Team
{
get
{
return (Lookup)base.GetValue(TeamProperty);
}
set
{
base.SetValue(TeamProperty, value);
}
}

public static DependencyProperty EmailProperty = DependencyProperty.Register("Email", typeof(Lookup), typeof(EmailToFiller));

[CrmInput("Email")]
[CrmReferenceTarget("email")]
public Lookup Email
{
get
{
return (Lookup)base.GetValue(EmailProperty);
}
set
{
base.SetValue(EmailProperty, value);
}
}

}
}


I've tested this code and it works.

Friday, September 25, 2009

How to restict users from entering special characters in Microsoft Dynamics CRM 4.0

Solution is very simple - just add following script to OnLoad event handler of form:

function SwitchOnCheck(ElementId)
{
var element = document.getElementById(ElementId);
if (element != null)
element.attachEvent("onkeyup", function()
{
var mikExp = /[$\\@\\\#%\^\&\*\(\)\[\]\+\_\{\}\`\~\=\|]/;
var strPass = element.value;
if (strPass == null)
return;
var strLength = strPass.length;
var lchar = element.value.charAt(strLength - 1);
while(lchar.search(mikExp) != -1)
{
strPass = strPass.substring(0, strLength - 1);
if (strPass.length == 0)
break;
strLength = strPass.length;
lchar = strPass.charAt(strLength - 1);
}

element.value = strPass;
});
}

SwitchOnCheck('firstname');
SwitchOnCheck('lastname');

Monday, September 21, 2009

File Repository for Microsoft Dynamics CRM 4.0

I've read this question and decided to develop this functionality.

Steps:
1. Share some folder.
2. Create custom aspx page with IFrame in it which creates folder for some record and then shows it.
3. ISV customization which adds new element in navigation bar.

1. Sharing a folder:




2. Custom aspx page:

Browser.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Browser.aspx.cs" Inherits="Browser" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Document Browser</title>
</head>
<body bgcolor="#d6ebff" onload="OnResizeComplete();" onresize="OnResizeComplete();">
<iframe id="DocumentShare" src="" frameborder="0" runat="server" />
<script type="text/javascript">
function OnResizeComplete()
{
var fr = window.document.getElementById('DocumentShare');
if (fr != null)
{
var wwidth=(window.innerWidth)?window.innerWidth:
((document.all)?document.body.offsetWidth:null);
fr.style.width = wwidth + 'px';

var wheight=get_wh();
fr.style.height = wheight;
}
}

function get_wh()
{
var frameHeight = window.screen.height;
if (self.innerHeight)
frameHeight = self.innerHeight;
else if (document.documentElement && document.documentElement.clientHeight)
frameHeight = document.documentElement.clientHeight;
else if (document.body)
frameHeight = document.body.clientHeight;
return frameHeight - 30;
}
</script>

</body>
</html>


Browser.aspx.cs:

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.IO;

public partial class Browser : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
try
{
string RealDirectory = ConfigurationSettings.AppSettings["Directory"];
string EntityTypeCode = Request.QueryString["type"];
string EntityId = Request.QueryString["id"].Replace("{", "").Replace("}", "");

RealDirectory = Path.Combine(RealDirectory, EntityTypeCode);

if (!Directory.Exists(RealDirectory))
Directory.CreateDirectory(RealDirectory);

RealDirectory = Path.Combine(RealDirectory, EntityId);

if (!Directory.Exists(RealDirectory))
Directory.CreateDirectory(RealDirectory);

string BaseDirectory = ConfigurationSettings.AppSettings["BaseDirectory"];
DocumentShare.Attributes["src"] = string.Format("{0}/{1}/{2}",BaseDirectory, EntityTypeCode, EntityId);
}
catch { }

}
}


Web.config:

<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="Directory" value="E:\DocumentShare"/>
<add key="BaseDirectory" value="\\YourServer\DocumentShare"/>
</appSettings>
<connectionStrings/>
<system.web>
<httpModules>
<add name="MapOrg" type="Microsoft.Crm.MapOrgEngine, Microsoft.Crm, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add name="CrmAuthentication" type="Microsoft.Crm.Authentication.AuthenticationEngine, Microsoft.Crm, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</httpModules>
<identity impersonate="true"/>
<compilation debug="true">
</compilation>
</system.web>
</configuration>


3. ISV Config (for account):

<ImportExportXml version="4.0.0.0" languagecode="1033" generatedBy="OnPremise">
<Entities>
</Entities>
<Roles>
</Roles>
<Workflows>
</Workflows>
<IsvConfig>
<configuration version="3.0.0000.0">
<Root />
<Entities>
<Entity name="account">
<NavBar ValidForCreate="0" ValidForUpdate="1">
<NavBarItem Icon="/_imgs/ico_18_debug.gif" Url="/ISV/DocumentShare/Browser.aspx" Id="navItem" PassParams="1">
<Titles>
<Title LCID="1033" Text="File Share" />
</Titles>
</NavBarItem>
</NavBar>
</Entity>
<Entity name="contact" />
<Entity name="lead" />
<Entity name="opportunity" />
<Entity name="list" />
<Entity name="campaign" />
<Entity name="campaignactivity" />
<Entity name="campaignresponse" />
<Entity name="incident" />
<Entity name="quote" />
<Entity name="salesorder" />
<Entity name="invoice" />
</Entities>
<!-- Microsoft Customer Relationship Management Service Management Customization -->
<ServiceManagement>
<AppointmentBook>
<SmoothScrollLimit>2000</SmoothScrollLimit>
<TimeBlocks>
<!-- All CSS Class mapping for Service activities -->
<TimeBlock EntityType="4214" StatusCode="1" CssClass="ganttBlockServiceActivityStatus1" />
<TimeBlock EntityType="4214" StatusCode="2" CssClass="ganttBlockServiceActivityStatus2" />
<TimeBlock EntityType="4214" StatusCode="3" CssClass="ganttBlockServiceActivityStatus3" />
<TimeBlock EntityType="4214" StatusCode="4" CssClass="ganttBlockServiceActivityStatus4" />
<TimeBlock EntityType="4214" StatusCode="6" CssClass="ganttBlockServiceActivityStatus6" />
<TimeBlock EntityType="4214" StatusCode="7" CssClass="ganttBlockServiceActivityStatus7" />
<TimeBlock EntityType="4214" StatusCode="8" CssClass="ganttBlockServiceActivityStatus8" />
<TimeBlock EntityType="4214" StatusCode="9" CssClass="ganttBlockServiceActivityStatus9" />
<TimeBlock EntityType="4214" StatusCode="10" CssClass="ganttBlockServiceActivityStatus10" />
<!-- All CSS Class mapping for Appointments -->
<TimeBlock EntityType="4201" StatusCode="1" CssClass="ganttBlockAppointmentStatus1" />
<TimeBlock EntityType="4201" StatusCode="2" CssClass="ganttBlockAppointmentStatus2" />
<TimeBlock EntityType="4201" StatusCode="3" CssClass="ganttBlockAppointmentStatus3" />
<TimeBlock EntityType="4201" StatusCode="4" CssClass="ganttBlockAppointmentStatus4" />
<TimeBlock EntityType="4201" StatusCode="5" CssClass="ganttBlockAppointmentStatus5" />
<TimeBlock EntityType="4201" StatusCode="6" CssClass="ganttBlockAppointmentStatus6" />
</TimeBlocks>
</AppointmentBook>
</ServiceManagement>
</configuration>
</IsvConfig>
<EntityMaps />
<EntityRelationships />
<Languages>
<Language>1033</Language>
</Languages>
</ImportExportXml>


And the result:

Wednesday, September 09, 2009

How to call/launch/execute workflow and wait until its execution to make some action with JavaScript in Microsoft Dynamics CRM

Author of code for workflow executing is Jian Wang. I wrote a code that waits for the workflow to be completed.

So the code:

ExecuteWorkflow = function(entityId, workflowId)
{
var xml = "" +
"<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
GenerateAuthenticationHeader() +
" <soap:Body>" +
" <Execute xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
" <Request xsi:type=\"ExecuteWorkflowRequest\">" +
" <EntityId>" + entityId + "</EntityId>" +
" <WorkflowId>" + workflowId + "</WorkflowId>" +
" </Request>" +
" </Execute>" +
" </soap:Body>" +
"</soap:Envelope>" +
"";

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Execute");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);
var resultXml = xmlHttpRequest.responseXML;
return(resultXml.selectSingleNode('//soap:Envelope/soap:Body/ExecuteResponse/Response/Id').nodeTypedValue);
}

WaitWorkflowExecution = function(workflowId)
{
var xml = "<?xml version='1.0' encoding='utf-8'?>"+
"<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'"+
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'"+
" xmlns:xsd='http://www.w3.org/2001/XMLSchema'>"+
GenerateAuthenticationHeader() +
"<soap:Body>"+
"<Retrieve xmlns='http://schemas.microsoft.com/crm/2007/WebServices'>"+
"<entityName>asyncoperation</entityName>"+
"<id>"+workflowId+"</id>"+
"<columnSet xmlns:q1='http://schemas.microsoft.com/crm/2006/Query' xsi:type='q1:ColumnSet'>"+
"<q1:Attributes>"+
"<q1:Attribute>statuscode</q1:Attribute>"+
"</q1:Attributes>"+
"</columnSet>"+
"</Retrieve>"+
"</soap:Body>"+
"</soap:Envelope>";

var xHReq = new ActiveXObject("Msxml2.XMLHTTP");
xHReq.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xHReq.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Retrieve");
xHReq.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xHReq.setRequestHeader("Content-Length", xml.length);
xHReq.send(xml);
var resultXml = xHReq.responseXML;

var errorCount = resultXml.selectNodes('//error').length;
if (errorCount != 0)
{
setTimeout("WaitWorkflowExecution('" + workflowId + "');", 1000);

}
else
{
var executionstatus = resultXml.selectSingleNode("//soap:Envelope/soap:Body/RetrieveResponse/RetrieveResult/q1:statuscode").nodeTypedValue;


if (executionstatus == 30)//Workflow completed
{
alert("Workflow Completed!");
}
else
{
setTimeout("WaitWorkflowExecution('" + workflowId + "');", 1000);
}
}
}

var theWorkflowId = "6eb47269-4139-40b3-a96d-255ef2d78e6a"; //change to your workflow Id
var workflowInstanceId = ExecuteWorkflow(crmForm.ObjectId, theWorkflowId);

WaitWorkflowExecution(workflowInstanceId);

Sunday, September 06, 2009

Overriding Records per page count for Microsoft Dynamics CRM 4.0

I've decided to use idea and code from one of my previous posts.

Idea:
1. Add field to user entity which will contain count of records to show.
2. Replace system-defined count of records with value from 1 trough catching and handling Execute message.

Field:





Code of the plugin:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using System.Xml;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace RecordPerPage
{
public class RecordCounterExtender : IPlugin
{
#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
if (context.MessageName == "Execute" && context.InputParameters.Contains("FetchXml"))
{
//I create a crmservice instance
ICrmService crmservice = context.CreateCrmService(true);

//I form a query to retrieve field with count of records
TargetRetrieveDynamic target = new TargetRetrieveDynamic();
target.EntityId = context.UserId;
target.EntityName = EntityName.systemuser.ToString();

RetrieveRequest request = new RetrieveRequest();
request.ColumnSet = new ColumnSet(new string[] { "new_recordscount" });
request.Target = target;
request.ReturnDynamicEntities = true;

//I retrieve field
DynamicEntity currentuser = (DynamicEntity)((RetrieveResponse)crmservice.Execute(request)).BusinessEntity;

//If the field is empty it would not be in response
//and this must be handled
if (currentuser.Properties.Contains("new_recordscount"))
{
//I create document to manipulate request query
XmlDocument indoc = new XmlDocument();
indoc.LoadXml((string)context.InputParameters["FetchXml"]);

//There can not be such attribute
//and this must be handled
if (indoc.DocumentElement.Attributes["count"] != null)
{
//I replace standard value with field from user form
indoc.DocumentElement.Attributes["count"].Value = ((CrmNumber)currentuser["new_recordscount"]).Value.ToString();
//and return modified request to the context
context.InputParameters["FetchXml"] = indoc.OuterXml;
}
}
}
}

#endregion
}
}


Step registration:



Demonstration:






And source code and customization: