Friday, March 20, 2009

Creating a Network Path Text Control




Dynamics supports several types of text fields. One type which is not available in the current version is a text field that supports a UNC path (\\) or a network drive (Y:) to a shared folder on the server or on the user local machine.

The following example converts a regular text field into a network path control. The control works in conjunction with an Explorer page which contains an IFRAME to the actual network share.

The Explorer(.aspx) should be deployed in the ISV folder. When the user double clicks on the field value a window is opened pointing to the shared folder. The control also provides a comfortable API for managing extra query parameters, changing control style, changing explorer window features and setting an initial network path. The last can be a handy feature if you require the explorer to point to an account personal folder within a shared file system tree.

Here is the Explorer.aspx implementation:


<%@ Page Language="C#" %>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Dynamics File Explorer</title>
<style>
.header
{
background-color:#639ace;
padding-left:10px;
color:White;
font-size:x-large;
font-family:arial;
height:30px;
}

.iframe
{
border:1px solid #94b2de;
height:100%;
width:100%;
}

body
{
margin:0px;
border:0px solid;
}
</style>
</head>
<body scroll="no">
<table border="0" cellpadding="1" cellspacing="0" style="width:100%;height:100%">
<tr>
<td class="header">File Explorer</td>
</tr>
<tr>
<td>
<iframe class="iframe" src="<%=Request["path"] %>"></iframe>
</td>
</tr>
</table>
</body>
</html>


The following script should be added to the entity onload event:


// JScript File
function OnCrmPageLoad()
{
var networkPath = new NetworkPath("fax");
//Example Add query string parameter
networkPath.Params.Add("test","1");
//Example Remove query string parameter
networkPath.Params.Remove("test");
//Example change control default style
networkPath.Style.Add("color","red");
//Transform the text field into a network path field
networkPath.Transform();
}

function NetworkPath(baseControlId)
{
nk = this;
nk.ID = baseControlId;
nk.Control = document.all[nk.ID];
/* Default Path */
nk.Path = "\\\\127.0.0.1\\Files"; //default path
nk.Window = null;
/* File Explorer Features (window.open)*/
nk.Features = new Dictionary(",","=");
/* If you wish to extend the Explorer.aspx and add more querystring parameters */
nk.Params = new Dictionary("&","=");
/* Network path control additional styling rules */
nk.Style = new Dictionary(";",":");

nk.Transform = function()
{
if (nk.Control)
{
/* Assign default path if control value is empty */
if (nk.Control.DataValue == null)
{
nk.Control.DataValue = nk.Path;
}

nk.Control.title = "Double Click to Open Network Explorer";
/* Default Window Settings */
if (!nk.Features.Exist("width"))
{
nk.Features.Add("width",700);
}
if (!nk.Features.Exist("height"))
{
nk.Features.Add("height",400);
}
if (!nk.Features.Exist("resize"))
{
nk.Features.Add("resize","yes");
}
if (!nk.Features.Exist("toolbar"))
{
nk.Features.Add("toolbar","no");
}
if (!nk.Features.Exist("menubar"))
{
nk.Features.Add("menubar","no");
}
if (!nk.Features.Exist("titlebar"))
{
nk.Features.Add("titlebar","no");
}
/* Attach validation to change and save events */
nk.Control.attachEvent( "onchange" , nk.Validate );
crmForm.attachEvent( "onsave" , nk.Validate );

/* Adjust control style */
nk.Style.Add("text-decoration","underline");
if (!nk.Style.Exist("color"))
{
nk.Style.Add("color","blue");
}
nk.Style.Add("cursor","hand");
nk.Control.style.cssText += ";" + nk.Style.ToString();

/* Add double click functionality */
nk.Control.ondblclick = nk.Open;
}
}

nk.Validate = function()
{
if (nk.Control.DataValue == null)
{
return true;
}

/* validate \\ unc path or a netwrok drive Y: */
var regex = new RegExp("^(\\\\|[a-zA-Z]:)");
if (!regex.exec(nk.Control.DataValue))
{
alert("Invalid Network Path or Drive");
return (event.returnValue = false);
}

return true;
}

nk.Open = function()
{
if (nk.Control.DataValue != null)
{
nk.Window = window.open( SERVER_URL + "/isv/explorer.aspx?path=" + nk.Control.DataValue + nk.Params.ToString() , "" , nk.Features.ToString());
}
}

nk.Close = function()
{
nk.Window.close();
}

/* Key Value Pair Collection */
function Dictionary(sep,delim)
{
this.list = [];
this.Seperator = sep;
this.Delimiter = delim

this.Add = function(key , value)
{
this.list[key] = value;
}

this.Remove = function(key)
{
this.list[key] = "";
}

this.Exist = function(key)
{
return this.list[key] != null && this.list[key] != "";
}

this.ToString = function()
{
var result = new StringBuilder();
result.Append(this.Seperator);
for(var key in this.list)
{
if (this.list[key]!="")
{
result.Append(key).Append(this.Delimiter);
result.Append(this.list[key]).Append(this.Seperator);
}
}
var tmp = result.ToString();
return tmp.substring(0,tmp.length-1);
}
}

function StringBuilder()
{
this.data = [];

this.Append = function(text)
{
this.data[this.data.length] = text;
return this;
}

this.Reset = function()
{
this.data = [];
}

this.ToString = function()
{
return this.data.join("");
}
}
}

OnCrmPageLoad();

Thursday, March 19, 2009

Displaying a lookup with related entity fields




As you all know a CRM lookup displays its related entity primary field. Although this can not be changed using existing customizations; in most cases that suffices. However, there are occasions where you want to display more information in order to avoid opening the related entity form. One solution which I posted about was the lookup preview which builds a preview window for each lookup DataValue. I personally think it’s a great solution and we also have a wizard that facilitates the creation of the preview for us. This post offers a different solution which utilizes a plug-in that retrieves the extra information you wish to display and injects it inside the lookup text. The drawback of this solution is that the lookup can only occupy a certain amount of space. So you should consider expanding the lookup colspan before you use it.

The solution makes use of the post retrieve message on the incident entity. My goal in this demo is to show how to extend the customer lookup on the incident form so if you select an account the customer lookup will display the account name , number and primary contact and if you select a contact then the customer lookup displays the salutation , job title and company fields.

The solution is static but might give you a head start when other requirements of similar nature are in need.


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

namespace LookupTextPlugIn
{
public class LookupRetrieveHandler : IPlugin
{
#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
try
{
LookupRetrieveProxy plugin = new LookupRetrieveProxy(context);
plugin.Execute();
}
catch
{
}
}

#endregion
}

public class LookupRetrieveProxy
{
private IPluginExecutionContext Context;
private Customer Customer;
private DynamicEntity CustomerInfo;

public LookupRetrieveProxy(IPluginExecutionContext context)
{
this.Context = context;
}

public void Execute()
{
if (!Context.OutputParameters.Contains(ParameterName.BusinessEntity))
{
return;
}

DynamicEntity entity = (DynamicEntity)Context.OutputParameters[ParameterName.BusinessEntity];

if (!entity.Properties.Contains("customerid"))
{
return;
}

this.Customer = (Customer)entity.Properties["customerid"];
if ((this.CustomerInfo = RetrieveCustomer()) != null)
{
this.Customer.name = ChangeCustomerName();
}
}

private DynamicEntity RetrieveCustomer()
{
ColumnSet customerColumns = new ColumnSet();
switch (this.Customer.type)
{
case "account":
customerColumns.AddColumn("accountnumber");
customerColumns.AddColumn("primarycontactid");
customerColumns.AddColumn("telephone1");
break;
case "contact":
customerColumns.AddColumn("salutation");
customerColumns.AddColumn("jobtitle");
customerColumns.AddColumn("parentcustomerid");
break;
}

ICrmService Service = this.Context.CreateCrmService(true);

TargetRetrieveDynamic targetRetrieve = new TargetRetrieveDynamic();
targetRetrieve.EntityId = this.Customer.Value;
targetRetrieve.EntityName = this.Customer.type;

RetrieveRequest retrieveRequest = new RetrieveRequest();
retrieveRequest.ColumnSet = customerColumns;
retrieveRequest.ReturnDynamicEntities = true;
retrieveRequest.Target = targetRetrieve;

RetrieveResponse retrieveResponse = (RetrieveResponse)Service.Execute(retrieveRequest);
return retrieveResponse.BusinessEntity as DynamicEntity;
}

private string ChangeCustomerName()
{
StringBuilder lookupText = new StringBuilder();
lookupText.Append(this.Customer.name).Append(" ");

switch (this.CustomerInfo.Name)
{
case "account":
lookupText.Append(this.GetProperty("accountnumber")).Append(", ");
lookupText.Append(this.GetProperty("primarycontactid")).Append(", ");
lookupText.Append(this.GetProperty("telephone1"));
break;
case "contact":
lookupText.Append(this.GetProperty("salutation")).Append(", ");
lookupText.Append(this.GetProperty("jobtitle")).Append(", ");
lookupText.Append(this.GetProperty("primarycustomerid"));
break;
}

return lookupText.ToString();
}

private String GetProperty(string propName)
{
if (!this.CustomerInfo.Properties.Contains(propName))
{
return "";
}

Object property = this.CustomerInfo.Properties[propName];

if (property is String)
{
return property.ToString();
}
else if (property is Customer)
{
return ((Customer)property).name;
}
else if (property is Lookup)
{
return ((Lookup)property).name;
}

//not supproted
return String.Empty;
}
}
}

Friday, March 13, 2009

CRM 4.0 Associated View Record / Page Counter




Our Record and Page Counter now supports Associated Views. This AVCR extension complements the RCO solution and provides full record counting coverage. If you require such functionality it is now part of our solution.

The Counter is available for All entities except Activities and All views that fire the RetrieveMultiple event.

Thursday, March 12, 2009

Controlling CRM window sizes


A while back I posted a simple script for changing the window size. Although ms current window settings fit most CRM forms there are situations were a form contains only a few fields and it only seem right to fit the window to its content.

Following is a simple and generic solution which can be used on all entities. The idea is to create an aspx page under the ISV folder that accepts an entity name and returns the script which moves and resizes the window.

The WindowSize.aspx page functions as a repository or a configuration page and does not require compilation. Simply Add the entity names to the Settings Dictionary and you’re done.

The script that calls this page should reside in each entity form (onload event). You may also put the script inside the global.js (not supported) if you don’t want to repeat this action more then once.

WindowSize.aspx code


<%@ Page Language="C#" %>
<%@ IMPORT NAMESPACE="System.Collections.Generic" %>
<script runat="server">
public static class Entity
{
public static Dictionary<String,String> Settings;

static Entity()
{
Settings = new Dictionary<String,String>();
//Parameters Order: Width,Height,Center,posX,posY
Settings.Add("account", "800,600,true");
//Add more entity configuration
Settings.Add("contact", "850,550,false,10,10");
}

public static String Script
{
get
{
return @"function adjWin(width,height,center,posX,posY){if(center==true){posX=(screen.availWidth-width)/2;posY=(screen.availHeight-height)/2;};window.resizeTo(width,height);window.moveTo(posX,posY);};";
}
}
}

public void Page_Load( object sender , EventArgs e)
{
string entityName = Request.QueryString["etn"] + "";
if (Entity.Settings.ContainsKey(entityName))
{
Response.Write(
String.Format("adjWin({0});{1}",
Entity.Settings[entityName],
Entity.Script)
);
}
}


</script>


Entity onload script / global.js


if (crmForm)
{
var wsScript = document.createElement("SCRIPT");
wsScript.src = SERVER_URL + "/ISV/WindowSize.aspx?etn=" + crmForm.ObjectTypeName
document.documentElement.childNodes[0].appendChild(wsScript);
}

Display Fetch in IFRAME – Part 2


My first post about displaying fetch inside an IFRAME has two disadvantages. The first is that the context of the parent window does not follow to the new entity window when you click on add new record (grid) button. This is might be annoying if you’re counting on the default mapping between the two entities. The second problem is that the grid does not refresh after you add (new button) or remove (delete button) a record. This means you are not able to see the changes unless you press on the grid refresh button your self.

I made piece with the fact that the parent context did not follow through to the child window since I was using advanced find. But the fact that the gird does not refresh automatically came as a surprise since I remember this feature working “AS IS” in v3.0. I guess ms did a few changes to the auto function which searched and refreshed the grid. Thanks to Dave Berry who commented / Posted about this and brought it to my attention.

In order to overcome this issue I added two new properties to the FetchViewer class.

The first parameter is called WithParentContext. This property allows you to decide whether the parent window object id is passed to the child window. I made it a public (as opposed to internal) choice since I figured you can display any type of query inside the FetchViewer including a query that does not reflect a direct parent child relationship.

The second parameter is the EntityCode (ObjectTypeCode) of the entity being fetched. If you want the grid to refresh automatically you must supply this value. This is important since the value is eventually passed to MS original auto function which expects this value.

The rest of the fetch viewer class was not changed and you can read all about it here.


function OnCrmPageLoad()
{
window.fetchContacts = new FetchViewer("IFRAME_test");
fetchContacts.WithParentContext = true;
fetchContacts.EntityCode = 2;
fetchContacts.FetchXml = getFetchXml();
fetchContacts.LayoutXml = getLayoutXml();
fetchContacts.Entity = "contact";
fetchContacts.QueryId = "{00000000-0000-0000-00AA-000000666400}";
fetchContacts.RegisterOnTab(0); //IFRAME ON THE FIRST TAB
}

function getFetchXml()
{
return '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false"><entity name="contact"><attribute name="fullname"/><attribute name="telephone1"/><attribute name="contactid"/><order attribute="fullname" descending="false"/><filter type="and"><condition attribute="parentcustomerid" operator="eq" uitype="account" value="' + crmForm.ObjectId + '"/></filter></entity></fetch>';
}

function getLayoutXml()
{
return '<grid name="resultset" object="2" jump="lastname" select="1" icon="1" preview="1"><row name="result" id="contactid"><cell name="fullname" width="300" /><cell name="telephone1" width="125" /></row></grid>';
}

function FetchViewer( iframeId )
{
var Instance = this;
var vDynamicForm;
var m_iframeTab;
var m_iframeDoc;
var m_iframeShowModalDialogFunc = null;
var m_windowAutoFunc = null;

Instance.WithParentContext = false;
Instance.EntityCode = 0;
Instance.Entity = "";
Instance.Iframe = null;
Instance.FetchXml = "";
Instance.QueryId = "";
Instance.LayoutXml = "";

Instance.RegisterOnTab = function( tabIndex )
{
Instance.Iframe = document.getElementById( iframeId );

if( !Instance.Iframe )
{
return alert( "Iframe " + iframeId + " is undefined" );
}

m_iframeDoc = getIframeDocument();
var loadingGifHTML = "<table height='100%' width='100%' style='cursor:wait'>";
loadingGifHTML += "<tr>";
loadingGifHTML += "<td valign='middle' align='center'>";
loadingGifHTML += "<img alt='' src='/_imgs/AdvFind/progress.gif'/>";
loadingGifHTML += "<div/><b>Loading View...</b>";
loadingGifHTML += "</td></tr></table>";
m_iframeDoc.body.innerHTML = loadingGifHTML;

if( parseInt( "0" + tabIndex ) == 0 )
{
Instance.Refresh();
}
else
{
Instance.Iframe.attachEvent( "onreadystatechange" , RefreshOnReadyStateChange );
}
}

function RefreshOnReadyStateChange()
{
if( Instance.Iframe.readyState != 'complete' )
{
return;
}

Instance.Refresh();
}

Instance.Refresh = function()
{
if( !Instance.Iframe )
{
return alert( "Iframe " + iframeId + " is undefined" );
}

m_iframeDoc = getIframeDocument();

Instance.Iframe.detachEvent( "onreadystatechange" , RefreshOnReadyStateChange );

var create = m_iframeDoc.createElement;
var append1 = m_iframeDoc.appendChild;
vDynamicForm = create("<FORM name='vDynamicForm' method='post'>");

var append2 = vDynamicForm.appendChild;
append2(create("<INPUT type='hidden' name='FetchXml'>"));
append2(create("<INPUT type='hidden' name='LayoutXml'>"));
append2(create("<INPUT type='hidden' name='EntityName'>"));
append2(create("<INPUT type='hidden' name='DefaultAdvFindViewId'>"));
append2(create("<INPUT type='hidden' name='ViewType'>"));
append1( vDynamicForm );

vDynamicForm.action = "/" + ORG_UNIQUE_NAME + "/AdvancedFind/fetchData.aspx";
vDynamicForm.FetchXml.value = Instance.FetchXml;
vDynamicForm.LayoutXml.value = Instance.LayoutXml;
vDynamicForm.EntityName.value = Instance.Entity;
vDynamicForm.DefaultAdvFindViewId.value = Instance.QueryId;
vDynamicForm.ViewType.value = 1039;
vDynamicForm.submit();

Instance.Iframe.attachEvent( "onreadystatechange" , OnViewReady );
}

function OnViewReady()
{
if( Instance.Iframe.readyState != 'complete' )
{
return;
}

Instance.Iframe.style.border = 0;
Instance.Iframe.detachEvent( "onreadystatechange" , OnViewReady );

if (Instance.WithParentContext == true)
{
getIframeWindow().open = OnWindowOpen;
}

if (m_iframeShowModalDialogFunc == null)
{
m_iframeShowModalDialogFunc = getIframeWindow().showModalDialog;
getIframeWindow().showModalDialog = OnIframeShowModalDialog;
}

if (Instance.EntityCode > 0)
{
if (m_windowAutoFunc == null)
{
m_windowAutoFunc = window.auto;
window.auto = OnWindowAuto;
}
}

m_iframeDoc = getIframeDocument();
m_iframeDoc.body.scroll = "no";
m_iframeDoc.body.style.padding = "0px";
}

function OnWindowOpen(url, name, features)
{
//new window
if (url.indexOf('?') == -1)
{
if (url.indexOf('userdefined') == -1 )
{
url = url + "?_CreateFromType=" + crmForm.ObjectTypeCode + "&_CreateFromId=" + crmForm.ObjectId;
}
else
{
url = url + "?_CreateFromType=" + crmForm.ObjectTypeCode + "&_CreateFromId=" + crmForm.ObjectId + "&etc=" + Instance.EntityCode + "#";
}
}

return window.open(url, name, features);
}

function OnIframeShowModalDialog(sUrl, vArguments, sFeatures)
{
m_iframeShowModalDialogFunc(sUrl, vArguments, sFeatures);
Instance.Refresh();
}

function OnWindowAuto(otc)
{
if ( otc == Instance.EntityCode )
{
getIframeDocument().all.crmGrid.Refresh();
}

m_windowAutoFunc(otc);
}

function getIframeDocument()
{
return getIframeWindow().document;
}

function getIframeWindow()
{
return Instance.Iframe.contentWindow;
}
}

OnCrmPageLoad();