Thursday, 20 December 2007

CRM 4.0 - Installation with no Internet Access

The current set of downloadable files for CRM 4.0 do not include some pre-requisite software (including .Net 3.0 framework and Visual C++ redistributable). If this software is not already installed and the setup program can't find it locally, the CRM setup program can download the files from the Internet, which is fine, if you have Internet access from the CRM server...

If you don't have Internet access you can pre-install these components, except the Visual C++ redistributable which cannot be reliably detected, so the CRM setup program will always try to install it.

Therefore, without Internet access, you have to have a local copy of the Visual C++ redistributable locally. Philip Richardson's blog describes where to put these files, so I won't bother repeating it here, but there are a couple of extra things that it might help to know:
  1. CRM setup checks the version of the local copy of the redistributable, and if it's not what it expects, will again try to connect to the Internet. You can identify this scenario from the setup log; the message is "Error when checking signature", though oddly it's marked as an Info message
  2. CRM setup only checks for local files when it starts. So if, like me, you get to the screen where it expects to download the files, realise it won't be able to, then copy them locally, then go Back, Forward, it won't help. Instead, exit setup completely and start over again

As to Internet access from the CRM server, in most circumstances this can be achieved from a VPC image with the use of more than one network adapter. See my earlier post

CRM 4.0 - Client script changes from CRM 3.0

This post is intended to be a dumping ground that I'll update as and when I, or my colleagues, find issues that may affect the upgrade of CRM 3.0 implementations with client script.

RemoteCommand - removal of some options
In CRM 3.0 it was possible to make unsupported use of the client-side RemoteCommand function to make server-side calls, e.g. the following got the current CRM user id

var cmd = new RemoteCommand("SystemUser", "WhoAmI", "/MSCRMServices/");
var res = command.Execute();
if (res.Success)
return res.ReturnValue.UserId;

What this did was make a request to the SystemUser.asmx (1st parameter) file in the MSCRMServices (3rd parameter) directory. Almost all of these files have been removed in CRM 4.0, so code like that above will fail.

I can't see any obvious equivalent calls to make in CRM 4.0, but as the use of RemoteCommand is unsupported, I wouldn't expect there to be. My preferred option has always been to write a server-side wrapper around calls the CrmService web service, and call them directly using the XmlHttp ActiveX object, thus keeping within supported functionality.

CRM 4.0

As the RTM code for CRM 4.0 is now available (http://philiprichardson.org/blog/post/CRM-40-Downloads.aspx), I figure it's time to start recording some of the changes from CRM 3.0, and associated upgrade issues.

As such I'm going to create a few posts that I'll add to as we find out more about CRM 4.0; the first ones will be on client script changes, server code and upgrade issues

Stored procedures in SSIS; the need for nocount

I've been doing a lot of SSIS work lately, and met an odd problem when using stored procedures in an OLEDB source. The general idea was to have a stored procedure that did some data modification, then finished with a SELECT statement to output logging information.

When previewing the OLEDB source everything was fine, but running the package would give the following error in the pre-execute phase:
'A rowset based on the SQL command was not returned by the OLE DB provider'

It took a while to work out, but eventually the problem came down to the way that SQL Server returns information about the number of rows affected. The resolution is to put the following SQL statement at the start of the stored procedure defnition to prevent output of the number of rows:

SET NOCOUNT ON

After that it was all fine.

Friday, 14 December 2007

Automating Local Data Query Creation

I've been invited to post on the Microsoft Dynamics CRM Team Blog again. This time I posted about how to automate the creation of the local data groups that specify what data each user of the Laptop client take offline.

Rather than repeating the text, you can read the article here

I've posted sample code for the article on CodePlex

Monday, 3 December 2007

Providing different default views for different CRM users

Or, how to modify CRM grid pages via client script.

CRM provides 2 standard ways to add custom client script into the CRM application; via form events, and through menu items and buttons in ISV.Config. Neither of these allows you to write code to control the default views for CRM entities

However, there is a way round this. The approach is to create a host HTML (or ASP .Net) page that has no interface components itself, but contains the CRM page in an IFrame. This will appear exactly the same as the standard CRM page, but allows you to write client script in your host page that controls the CRM page, and hence change which view is displayed.

Note that this approach has to be considered as unsupported, as it involves programmatic control over a picklist outside of a CRM form.

Building the host page
There are 2 aspects of the host page; providing the HTML to host the IFrame without adding extraneous borders or padding, and adding client script that runs once the IFrame has loaded.

The following HTML shows how to host the IFrame:

<body style="margin:0" onload="Init();">

<iframe onreadystatechange="ors();" id="ifr" src="about:blank" width="100%" height="100%" frameborder="0" leftmargin="0">
</body>

In this example I’m creating a generic page that can host most CRM grid pages, and I’m setting the src property of the IFrame programmatically:

function Init()
{
var etc = getQS('etc');
if (etc != null)
{
document.all.ifr.src = '/_root/homepage.aspx?etc=' + etc;
}
}

This src is the standard way to display most entity grids, with etc as the object type code of the entity.

I’m using the onreadystatechange event to determine whether the IFrame has loaded:

function ors()
{
if (event.srcElement.readyState == 'complete')
{
var sView = getQS('view');
if (sView != null)
SetView(sView);
document.all.ifr.style.visibility = 'visible';
}
else
{
document.all.ifr.style.visibility = 'hidden';
}
}


This checks the readyState property of the IFrame, which will equal ‘complete’ when the IFrame contents have loaded, and the IFrame contents after they have been modified.

The following code shows how to modify which view is selected:

function SetView(sView)
{
var ifDoc = document.frames['ifr'].document.all; // access IFrame contents
var oSel = ifDoc['SavedQuerySelector']; // picklist control to select view
if (oSel != null)
{
var v = GetSelectValue(oSel, sView);
if (v)
{
oSel.DefaultValue = v;
oSel.DataValue = v;
oSel.FireOnChange(); // need to fire this event to apply changes
}
}
}
function GetSelectValue(oSel, sText) // helper function to select item in picklist
{
for (var i=0;i<osel.options.length;i++)

{
if (oSel.options[i].text == sText) return oSel.options[i].value;

}
}

The code uses another helper function to access parameters passed on the query string. This function is oversimplified in that it doesn’t cope with all possible encoding issues, but is sufficient for this example:

function getQS(name)
{
var ret = '';
if (window.location.search != null && window.location.search.length > 1)
{
var aQS = window.location.search.substring(1).split('&');
if (aQS != null)
for (var i=0;i<aQS.length;i++)
if (aQS[i].indexOf(name + '=') == 0)
ret = aQS[i].substring(name.length + 1).replace('%20', ' ');
}
return ret;
}

Putting this all together, we have a page that will display an entity grid, and change the default view, based on 2 query string parameters. For example

defaultViewChanger.htm?etc=1&view=Active%20Accounts

Will display the account grid, and set the view to the Active Accounts view (%20 is the encoding of a space, which is not permitted in a url).

Setting different default views for different users
We now have a page that can programmatically change the default view. There are 2 ways this can be used to provide different default views to different users: a programmatic way that identifies the current user (and probably team or role membership) and hence determines the default view, or via permissions in SiteMap.

I’m not intending the cover the programmatic route in detail here; my preference is to convert the page to an ASP .Net page that identifies the current user and their role, and populates the parameter to SetView in server code; an alternative approach in client code can be found here.

Another approach is to make use of the Privilege element in SiteMap to display different navigation links to different sets of users based on their permissions, as described in more detail here. Let’s assume we have 2 groups of users who want different default views of the account entity, group A (who are members of security role ‘roleA’) want to see My Active Accounts, and group B (members of ‘roleB’) who want to see Active Accounts.

We can create a dummy entity in CRM called exc_acctsecurity and grant roleA write rights on the exc_acctsecurity entity, and grant roleB assign rights. Then we can modify SiteMap, replacing the SubAreas for the account entity to the following:

<SubArea Id="nav_accthostA" Title="Accounts" Url="http://server/defaultViewChanger.htm?etc=1&amp;view=My%20Active%20Accounts" Icon="/_imgs/ico_18_1.gif">
<Privilege Entity="exc_acctsecurity" Privilege="Write" />
</SubArea>
<SubArea Id="nav_accthostB" Title="Accounts" Url="http://server/defaultViewChanger.htm?etc=1&amp;view=Active%20Accounts" Icon="/_imgs/ico_18_1.gif">
<Privilege Entity="exc_acctsecurity" Privilege="Assign" />
</SubArea>

Note the use of XML encoding (&amp; instead of &).

One drawback of this approach is that anybody in the System Administrator role will see both SubAreas.

A complete example of the HTML and SiteMap can be found on CodePlex.

Viewing all files in CRM related to an account

I've recently published a second CRM related reporting services project on codeplex. This one illustrates how to get a list of all files related to a CRM account. This includes files attached directly to the account or via related objects (e.g. contacts, opportunities, cases), and also includes both file and email attachments.

Most of the work is done in the SQL, which I've written as a set of SQL views for ease of reading, rather than embedding all the SQL in the report.

The project also illustrates how to link to CRM objects, within a report, as the report contains reporting services actions to display the CRM entity forms, and also actions on each file to link to the CRM file download dialog.

This report is written so as to take parameters from a ISV.Config button, as described here