Sunday, December 28, 2008

CRM 4.0 Supported Record / Page Counter for CRM views




Are you a Microsoft dynamics partner? Find out more about how to obtain a copy of this Wizard through our reseller program.

This is certainly a feature that dynamics is missing. I had doubts about this solution before I developed it since I wanted to create a supported solution that would easily integrate into our product and upgrade seamlessly with dynamics. I was aware of dynamics limitations regarding this issue. Never the less, I found a cool way to integrate a record count and page count in any CRM grid in a totally supported way.

The Plug-in that manages the Counters is highly configurable. You can decide whether an entity displays the record counter and if it does you can also specify whether to display a total page count (the records counter is the default display). You can also specify the labels for each language to support multiple languages.

The Counter does not show if the record count is smaller then amount of records per page. This means that if the current page shows 25 records per page and the record count is 10 then you won’t see the counter since CRM already displays all the information you need.

The images below Display how the plug-in works. I’m putting it for here as a shared Plug-in, I figure this is a feature that many CRM implementations require. You may try to develop it your self and spend the time and money doing so, or purchase it as your own company property under our T&C and enjoy it immediately.

It takes about 20 seconds to implement the solution.
The configuration is made directly on top of the plug in registration tool.

If you have any further questions please send them to support@gicrm.com and specify the plug-in you’re interested in.

To play back the flash movie simply right click on the movie (*.SWF file) click rewind, and then click Play.

The Plug-in Supports the following views
* Advanced Find

* Single Lookup View

* Multi Lookup View

* Public and Private Views

* Quick Find

* Custom FetchXml and Advanced Find

* Form Assistant

* Auto resolve Lookup View


The Record counter plug-in in now available on GI's company website.

Friday, December 19, 2008

CRM 4.0 Tooltip Wizard


When I first weighted the reasons for building an application wide tooltip solution I had a different idea in mind. Since many of our product entities are comprised of complex forms I needed a system that can deliver both informative text about the current field and much more importantly a next step trigger mechanism which will interact with the user and guide her through the next step or functions.

The wizards will undoubtedly save you hundreds of development hours, speed up you delivery and much more. Since I’m testing this initiative for the time being I wanted to give you a taste and share this post with you. If you find this interesting, share your thought and questions with me (comment or send an email to support@gicrm.com).

In order to achieve the desired result we built the tooltip on top of ms createPopup window. That enabled us to add interactive next step scripts to each tooltip which eventually sets the focus to the next field or triggers a desired functionality.

The following images describe best the tooltips you can build using the tooltip wizard. You can create context menus and buttons, add custom logic or just be satisfied with simple informative html.






Another reason I considered while contemplating on building such a wizard was that I could not know in advance where I would need to put interactive tooltips. In order to make this decision easy or eliminate the need to even think about it I came to a conclusion that such a wizard is the best way to go.

There are many more good enough reasons to implementing this type of functionality. Here is a partial list or incentives which I consider important and eventually made my decision easy:

Tooltips are considered to be preliminary validation steps. They teach the user how to interact with the system in the most efficient way.

Tooltips are pro-active mechanisms as opposed to validation alerts which are considered re-active since they only pop after the user makes her move.

Tooltips play a very important role in application acceptance. You don’t have to be Freud to understand that users like to be self sufficient. A robust
tooltip mechanism helps the user solve most of her daily application stuff on her own.

Tooltips support organization shifts in regulations and accepted practice. Consider situations where the user’s daily routine of filling new account
information changed due to a new regulation. What would be best if not revising the tooltip that guides the user with the new practice?

I’m sure there are more incentives then mentioned above. The bottom line is that such a system is more then just a nice to have feature.
If you think the way I do then you’ll find the tooltip wizard a true companion.


Specifications and technical stuff:
The tooltip wizard enables you to fully control the creation and editing of tooltips through out the Dynamics application.

The wizard supports dynamics multi-lingual environment and is available in all base languages. It’s also supports Dir LTR (Left to Right) and RTL (Right to Left).

To rewind the flash presentation simply right click on the flash movie and click rewind.



The Tooltip Wizard Features

  1. Lets you manage and control application wide tooltips.

  2. Allows you to cut design and development time.

  3. Allows you to enrich application look and feel by designing highly elaborated and richly colored tooltips.

Functionality

Editing


  • The Tooltip Wizard contains a fully-featured easy to use HTML editor which lets you choose the font size face and color.


  • A link option lets you create various hyperlinks. You can switch between HTML source and Text modes at any time with a click of the mouse, and any changes you have made will instantly be reflected in both modes.


  • You can adjust the ToolTip's background to match the appearance of your application. Preview mode lets you instantly see what your edited tooltip will look like.


  • Compare option make translation convenient by letting you compare to your sibling tooltips in other system languages.

Displaying


  • The wizard lets you decide whether the tooltip will be displayed at the top, bottom, left or right of the control or the label.


  • Dynamic layout mode - adjust the tooltip size to the control or the label which it's attached to.

  • Fixed layout mode - fine-tune (while previewing) of the tooltips size. Fadeout Interval - adjust when fading will start after hovering out of the tooltip control or label.


Publishing


  • Publish and Un-publish are done at the entity level. While a single field tooltip will be viewed according to its Active \ De-active state.


The wizard is now available online.

Please send your comments / enquiries to ttw@gicrm.com.

Wednesday, December 3, 2008

Expediting Plug-In Development Using VS Pre/Post Build Events


No doubt, CRM 4.0s new plug-in engine is a giant leap compared to the callout facility that was available on the 3.0 version.
However, a few things still hinder the development process. One of which is the fact that you need to reset the Internet information services (IIS) and copy your project files to the assembly bin folder each time you compile your code.

In order to overcome and automate the process you can add Pre and/or Post Build events to you plug-in project.
Build event are no more then command line instructions you need to run before and/or after you project builds.

The nice thing about Post events is that you can set them to run only if the build is successful. Which means the iisreset won’t run and the files won’t be copied unless the build is complete. Following are the Post build command instruction I add to every plug-in project we build.



1.Open the Build Events Property Page: The Build events are located on the Project Properties page under the build events Tab.
2.Add the following commands to the Post Build Event (make sure you run the command only on successful build)
Resets Internet information Services

call iisreset

Copy the *.dll file to the assembly bin folder.

xcopy "$(TargetPath)" "C:\[CRM INSTALL FOLDER]\Server\bin\assembly" /i /d /y

Copy the *.pdb file to the assembly bin folder

xcopy "$(TargetDir)$(TargetName).pdb" "C:\[CRM INSTALL FOLDER]\Server\bin\assembly" /i /d /y


That’s it.

CRM 4.0 Supported Multi Select (Picklist) Control




As you all know Dynamics does not support multi select controls. Of course this is not entirely true since from a design perspective having the ability to create many to many associations and using ms grid is a control none the less. Having that said, Customers will find unbelievable reasons to why they need or prefer to use a multi select control and not the out of the box grid.

I was wondering what it would take to create a fully supported multi select control (in other words a listbox)
using dynamics OOB/supported capabilities.

Before I start let me say that the idea of creating such a control does not justify the amount of code needed to make this solution run inside ms supported boundaries. But then again, once you have it, it takes approximately 20 minutes (per listbox) to implement, which is fairly reasonable.

In order to prove my point and satisfy my wonderings I devised a simple scenario of a credit card listbox on the contact entity.
The idea was to be able to relate multiple credit card types to a single contact using a regular listbox.

The first thing I did is create the necessary customizations needed to support the solution.
STEP 1 - Customizations:
1. A new entity called credit card.






2. A new N:N relationship with the contact entity (e.g. gi_gi_creditcard_contact). When you create the relationship don’t change its name. The way ms represent its relationships (e.g. prefix_entity_entity) will help you to better understand this solution.





3. Added a few Credit card types to complete the list.


4. On the contact entity I created a credit card IFRAME:






5. Added a new contact attribute called creditcarditems; The reason for this attribute is that it is used as a transport that will hold the current selected options.

Note: Notice that the attribute schema name (gi_creditcard) is the same as the credit card entity schema name. This is important since the multipicklist.aspx refers to its parent contact transport attribute by using the Entity2Name witch is gi_creditcard. You may also add a query string parameter to the contact onload event that opens the multipicklist.aspx and use that instead.



6. Added the following script to the contact entity. The script hides the transport attribute and points the iframe to an external multipicklist.aspx.
The multipicklist.aspx receives the following querystring parameters:
contact objected (crmForm.ObjectId)
The name of the entity (crmForm.ObjectTypeName)
The name of the related entity (gi_creditcard)
The name of the storage (transport) attribute (gi_creditcard).
Organization Name e.g. ORG_UNIQUE_NAME


crmForm.gi_creditcard.style.display = "none";
var url = prependOrgName("/isv/controls/multipicklist.aspx");
url += "?toentity=gi_creditcard" ;
url += "&toattribute=gi_card";
url += "&fromentity=" + crmForm.ObjectTypeName;
url += "&id=" + (crmForm.ObjectId?crmForm.ObjectId:"");
url += "&orgname=" + ORG_UNIQUE_NAME;
document.all["IFRAME_CreditCards"].src = url;


STEP 2 – Build MultiPicklist.aspx Page (control)
1. Added a new asp.net web application and created a vdir under the isv folder.





2. Changed the default.aspx page to multipicklist.aspx and added the following html. The html contains a runat server listbox ,a script that binds to the parent (contact) onsave event and fills the transport attribute with selected values and a few styling rules to make the listbox occupy the entire iframe space.


<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<style type="text/css">
.LISTBOX
{
width:100%;
height:103%;
border:0px solid;
}
</style>
<script language="javascript">


MultiPicklist = {};
MultiPicklist.CrmForm = null;
MultiPicklist.Storage = null;
MultiPicklist.OnLoad = function()
{
MultiPicklist.CrmForm = parent.document.all.crmForm;
MultiPicklist.Storage = MultiPicklist.CrmForm.<%=Entity2Name%>;
MultiPicklist.CrmForm.attachEvent( "onsave" , MultiPicklist.OnSave );
}
MultiPicklist.OnSave = function()
{
var items = "";
var MultiBox = document.all.MultiBox;
for( var i = 0 ; i < MultiBox.options.length ; i++ )
{
if( MultiBox.options[i].selected == true )
items += MultiBox.options[i].value + ",";
}
MultiPicklist.Storage.DataValue = items.replace(/,$/i,"");
}


</script>
</head>
<body leftmargin="0" onload="MultiPicklist.OnLoad()" scroll="no" topmargin="0">
<form id="form1" runat="server">
<ASP:LISTBOX CSSCLASS="listbox" ID="MultiBox" SELECTIONMODE="Multiple" RUNAT="server">
</ASP:LISTBOX>
</form>
</body>
</html>


3. In the code behind I added functionality which:
a. Validates the query string parameters (Page_ValidateParameters())
b. Retrieves the credit card list from CRM (Page_RetrievePicklistOptions())
c. Marks the selected options if they exist (Page_SetSelectedOptions())


using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy;

namespace Controls
{
public partial class multipicklist : System.Web.UI.Page
{
/// <summary>
/// Current Organizaton Name
/// </summary>
private String OrganizationName;
/// <summary>
/// The Primary Entity (contact) ObjectId (crmForm.ObjectId)
/// </summary>
private Guid ObjectId;
/// <summary>
/// The Primary Entity (contact) Name (typename)
/// </summary>
private String Entity1Name;
/// <summary>
/// The Related N:N Entity (gi_creditcard) Name
/// This should also be the name of the transport attribute
/// on the Primary Entity (gi_creditcard attribute on contact entity)
/// </summary>
protected String Entity2Name;
/// <summary>
/// The Related Entity PrimaryField Name (gi_card)
/// This is the Picklist Option Text
/// </summary>
private String Entity2TextAttribute;
/// <summary>
/// The Primary Entity PrimaryKey Name (contactid)
/// </summary>
private String Entity1KeyAttribute;
/// <summary>
/// The Related N:N Entity PrimaryKey Name (gi_creditcardid)
/// </summary>
private String Entity2KeyAttribute;
/// <summary>
/// CrmService
/// </summary>
private CrmService Service;

protected void Page_Load(object sender, EventArgs e)
{
Page_ValidateParameters();
Page_CreateService();
Page_RetrievePicklistOptions();
Page_SetSelectedOptions();
}
/// <summary>
/// Retrieve Options From Relationship Entity Using Fetch Message
/// And Set the Listbox Selected Items
/// </summary>
private void Page_SetSelectedOptions()
{
if (!ObjectId.Equals(Guid.Empty))
{
String relationShipName = String.Format("gi_{0}_{1}", Entity2Name, Entity1Name);
StringBuilder fetchXml = new StringBuilder();
fetchXml.Append("<fetch mapping='logical'>");
fetchXml.Append("<entity name='").Append(relationShipName).Append("'>");
fetchXml.Append("<all-attributes />");
fetchXml.Append("<filter>");
fetchXml.Append("<condition attribute='contactid' operator='eq' value ='");
fetchXml.Append(ObjectId).Append("' />");
fetchXml.Append("</filter>");
fetchXml.Append("</entity>");
fetchXml.Append("</fetch>");

String resultXml = Service.Fetch(fetchXml.ToString());
XmlDocument fetchResult = new XmlDocument();
fetchResult.LoadXml(resultXml);
XmlNodeList relatedResults = fetchResult.SelectNodes("resultset/result");

foreach (XmlElement item in relatedResults)
{
XmlElement Entity2KeyAttributeNode = (XmlElement)item.SelectSingleNode(Entity2KeyAttribute);
ListItem CurrentItem = MultiBox.Items.FindByValue(Entity2KeyAttributeNode.InnerText.ToLower());
if( CurrentItem != null ) CurrentItem.Selected = true;
}
}
}
/// <summary>
/// Retrieve the listbox options from the related entity using
/// queryexpression and fill the listbox.
/// </summary>
private void Page_RetrievePicklistOptions()
{
QueryExpression creditQuery = new QueryExpression(Entity2Name);
creditQuery.ColumnSet.AddColumn(Entity2TextAttribute);
creditQuery.ColumnSet.AddColumn(Entity2KeyAttribute);
creditQuery.Distinct = true;
creditQuery.Orders.Add(new OrderExpression(Entity2TextAttribute, OrderType.Ascending));

RetrieveMultipleRequest retrieveMultipleRequest = new RetrieveMultipleRequest();
retrieveMultipleRequest.Query = creditQuery;
retrieveMultipleRequest.ReturnDynamicEntities = true;
RetrieveMultipleResponse retrieveMultipleResponse =
(RetrieveMultipleResponse)Service.Execute(retrieveMultipleRequest);

MultiBox.Items.Add( "" );
foreach (DynamicEntity creditcard in retrieveMultipleResponse.BusinessEntityCollection.BusinessEntities)
{
Key key = creditcard.Properties[Entity2KeyAttribute] as Key;
ListItem item = new ListItem(
creditcard.Properties[Entity2TextAttribute] + "",
key.Value.ToString("B"));
MultiBox.Items.Add(item);
}
}
/// <summary>
/// Create the crmservice
/// </summary>
private void Page_CreateService()
{
CrmAuthenticationToken token = new CrmAuthenticationToken();
token.AuthenticationType = 0;
token.OrganizationName = OrganizationName;

Service = new CrmService();
Service.CrmAuthenticationTokenValue = token;
Service.PreAuthenticate = false;
Service.UnsafeAuthenticatedConnectionSharing = true;
Service.Credentials = System.Net.CredentialCache.DefaultCredentials;

Service.Url = String.Format("{0}://{1}:{2}/mscrmservices/2007/crmservice.asmx", Request.Url.Scheme, Request.Url.Host, Request.Url.Port);
}
/// <summary>
/// Validate the listbox control parameters
/// </summary>
private void Page_ValidateParameters()
{
Entity1Name = Request.QueryString["fromentity"];
if (Entity1Name + "" == String.Empty)
throw new Exception("Entity1 Name is Missing");

Entity2Name = Request.QueryString["toentity"];
if (Entity2Name + "" == String.Empty)
throw new Exception("Entity2 Name is Missing");

OrganizationName = Request.QueryString["orgname"];
if (OrganizationName + "" == String.Empty)
throw new Exception("Organization Name is Missing");

ObjectId = Request.QueryString["id"] + "" == String.Empty ? Guid.Empty :
new Guid(Request.QueryString["id"]);

Entity2TextAttribute = Request.QueryString["toattribute"];
if (Entity2TextAttribute + "" == String.Empty)
throw new Exception("Picklist Text Attribute is Missing");

Entity1KeyAttribute = Entity1Name + "id";
Entity2KeyAttribute = Entity2Name + "id";
}
}
}


STEP 3 – Closing the Cycle Using a Plug-in
So why do we need a Plug-in? The main reason is that from the client side perspective CRM will create the contact id (GUID) only after the record is saved! This leaves us with no other option than to associate selected options on the server using a Plug-in.

1. Added a new Class Library application



2. Signed the application by creating a Strong Name key

3. Added Plug-In code that removes existing options (Disassociates) and saves (Re-Associates) the current contact with the selected listbox (credit card) options. The plug-in receives the existing transport attribute saved options from the PreImage and disassociates these options then gets the newly selected options from the PostImage and Re-Associate them with the contact. The Plug-in needs to run on both Post Create and Post Update events. Following is the Plug-in Specifications, SolutionXml and Code













Plug-In Solution XML

<Register LogFile="Plug-in Registration Log.txt" Server="http://moss:5555" Org="MicrosoftCRM" Domain="" UserName="administrator">
<Solution SourceType="1" Assembly="MPPlugIn.dll" Id="28f2c17f-2a16-4ce7-9a98-8e627ad2aecf">
<PluginTypes>
<Plugin TypeName="MPPlugIn.MPHandler" FriendlyName="3fe8c6c8-f928-43e3-b2e1-ccb55d651d6c" Id="93ecf707-c794-47df-a76e-9df3a3ee8e00">
<Steps>
<Step PluginTypeName="MPPlugIn.MPHandler" PluginTypeFriendlyName="3fe8c6c8-f928-43e3-b2e1-ccb55d651d6c" CustomConfiguration="" SecureConfiguration="" Description="Create of contact in Parent Pipeline" FilteringAttributes="" ImpersonatingUserId="" InvocationSource="0" MessageName="Create" Mode="0" PrimaryEntityName="contact" SecondaryEntityName="none" Stage="50" SupportedDeployment="0" Rank="1" Id="a0631824-7fc0-dd11-b17c-0003ff2f0264">
<Images>
<Image EntityAlias="gi_gi_creditcard_contact" ImageType="1" MessagePropertyName="Id" Attributes="contactid,gi_creditcard" Id="405a617d-7fc0-dd11-b17c-0003ff2f0264" />
</Images>
</Step>
<Step PluginTypeName="MPPlugIn.MPHandler" PluginTypeFriendlyName="3fe8c6c8-f928-43e3-b2e1-ccb55d651d6c" CustomConfiguration="" SecureConfiguration="" Description="Update of contact in Parent Pipeline" FilteringAttributes="" ImpersonatingUserId="" InvocationSource="0" MessageName="Update" Mode="0" PrimaryEntityName="contact" SecondaryEntityName="none" Stage="50" SupportedDeployment="0" Rank="1" Id="a0d1a8fe-82c0-dd11-b17c-0003ff2f0264">
<Images>
<Image EntityAlias="gi_gi_creditcard_contact" ImageType="2" MessagePropertyName="Target" Attributes="contactid,gi_creditcard" Id="f0d85819-83c0-dd11-b17c-0003ff2f0264" />
</Images>
</Step>
</Steps>
</Plugin>
</PluginTypes>
</Solution>
</Register>


Plug-In Code:

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

namespace MPPlugIn
{
public class MPHandler : IPlugin
{
#region IPlugin Members

/// <summary>
/// A Multipicklist context Object
/// </summary>
public class MPContext
{
/// <summary>
/// MS Context
/// </summary>
public IPluginExecutionContext context;
/// <summary>
/// The Entity 1 (Primary: contact in this case) Primary Key e.g. contactid
/// </summary>
public String Entity1PrimaryAttributeName;
/// <summary>
/// The Entity 2 (related: gi_creditcard in this case) Name
/// </summary>
public String Entity2Name;
/// <summary>
/// An Entity 1 Attribute that is used as a transport form selected options.
/// in this case i called it gi_creditcard (same and the related entity name)
/// You can give it any name you like.
/// </summary>
public String Entity1PicklistOptionsAttribute;
/// <summary>
/// The relationship name of contact and gi_creditcard
/// e.g. gi_gi_creditcard_contact. this is also used as the
/// PreImage and PostImage ALIAS names.
/// </summary>
public String N2NRelationShipName;
/// <summary>
/// Entity 1 (Primary) Name
/// Primary means the entity where the multipicklist is put.
/// Related means the entity from which the multipicklist options are taken.
/// </summary>
private String _Entity1Name;
public String Entity1Name
{
get
{
return this._Entity1Name;
}
set
{
this._Entity1Name = value;
this.Entity1PrimaryAttributeName = value + "id";
}
}
/// <summary>
/// Initialize a new MP context
/// </summary>
/// <param name="context"></param>
public MPContext(IPluginExecutionContext context) {
this.context = context;
}
}
/// <summary>
/// PlugIn Implementation:
/// 1. Create a new MPContext
/// 2. Fill the MPContext parameters
/// 3. Disassociate old options
/// 4. Associate new options
/// </summary>
/// <param name="context"></param>
public void Execute(IPluginExecutionContext context)
{
MPContext mpContext = new MPContext(context);
mpContext.Entity1Name = "contact";
mpContext.Entity2Name = "gi_creditcard";
mpContext.Entity1PicklistOptionsAttribute = "gi_creditcard";
mpContext.N2NRelationShipName = "gi_gi_creditcard_contact";

DisAssociateMultiplePicklist(mpContext);
AssociateMultiplePicklist(mpContext);
}
/// <summary>
/// Retrieves the existing options from the storage (transport) attribute
/// on the primary entity (contact) and call DisassociateEntitiesRequest
/// to remove the association
/// </summary>
/// <param name="context"></param>
private void DisAssociateMultiplePicklist(MPContext context)
{
IPluginExecutionContext msContext = context.context;
/*
Validates that the relationship name (e.g. gi_gi_creditcard_contact)
is the name you gave the PreImage Alias.
*/
if (msContext.PreEntityImages.Properties.Contains(context.N2NRelationShipName) == true)
{
//Retrieve the PreImage dynamic (Primary) entity 1 (contact)
DynamicEntity Entity1 = msContext.PreEntityImages.Properties[context.N2NRelationShipName] as DynamicEntity;
/*
Validates that the image contains the primarykey (contactid) in the property bag.
the contactid represents one side of the link (Moniker).
*/
if (Entity1.Properties.Contains(context.Entity1PrimaryAttributeName) == false)
return;
/* Extract the id (contactid) */
Guid primaryEntityId = ((Key)Entity1.Properties[context.Entity1PrimaryAttributeName]).Value;
/*
If the options (transport) attribute is missing
meaning that the its value is null then exit
*/
if( Entity1.Properties.Contains( context.Entity1PicklistOptionsAttribute ) == false )
return;
/* get the existing options */
String strOptions = Entity1.Properties[context.Entity1PicklistOptionsAttribute] + "";
if (strOptions.Equals(String.Empty)) return;

/* create an option array */
String[] mpOptions = strOptions.Split(',');
/* create a crmservice */
ICrmService Service = msContext.CreateCrmService(true);

/* for each option disassociate (remove) the link */
foreach (String option in mpOptions)
{
Guid relatedEntityId = new Guid(option);
DisassociateEntitiesRequest assocRequest = new DisassociateEntitiesRequest();
assocRequest.Moniker1 = new Moniker(context.Entity1Name, primaryEntityId);
assocRequest.Moniker2 = new Moniker(context.Entity2Name, relatedEntityId);
assocRequest.RelationshipName = context.N2NRelationShipName;
Service.Execute(assocRequest);
}
}
}
/// <summary>
/// Associates the new selected options with the primary entity (contact)
/// using the selected options taken from the PostImage Propery bag.
/// See DisAssociateMultiplePicklist method for code details.
/// </summary>
/// <param name="context"></param>
private void AssociateMultiplePicklist( MPContext context )
{
IPluginExecutionContext msContext = context.context;

if (msContext.PostEntityImages.Properties.Contains(context.N2NRelationShipName) == true)
{
DynamicEntity Entity1 = msContext.PostEntityImages.Properties[context.N2NRelationShipName] as DynamicEntity;

if (Entity1.Properties.Contains(context.Entity1PrimaryAttributeName) == false)
return;

Guid primaryEntityId = ((Key)Entity1.Properties[context.Entity1PrimaryAttributeName]).Value;

if (Entity1.Properties.Contains(context.Entity1PicklistOptionsAttribute) == false)
return;

String strOptions = Entity1.Properties[context.Entity1PicklistOptionsAttribute] + "";
if (strOptions.Equals(String.Empty)) return;

String[] mpOptions = strOptions.Split(',');
ICrmService Service = msContext.CreateCrmService(true);

foreach (String option in mpOptions)
{
Guid relatedEntityId = new Guid(option);
AssociateEntitiesRequest assocRequest = new AssociateEntitiesRequest();
assocRequest.Moniker1 = new Moniker(context.Entity1Name, primaryEntityId);
assocRequest.Moniker2 = new Moniker(context.Entity2Name, relatedEntityId);
assocRequest.RelationshipName = context.N2NRelationShipName;
Service.Execute( assocRequest );
}
}
}

#endregion
}
}


In order to add another relationship multi select control you only need to repeat step 1 ( customizations ) and part of step 3 ( Plug-In Registration and duplicating the Execute Method).

Friday, November 21, 2008

Change Cases Associated Default View

This is a complementary post to the “Changing Activity / History Default View”.
The code sample uses exactly the same concept as the former post so if you’re using that then you should integrate both solution to avoid duplicate functions.


//Case History statecode options
var ServiceOptions =
{
Active : "0",
Resolved : "1",
Canceled : "2",
All : "All"
}

var _loadarea = loadArea;
loadArea = function(sArea, sParams, sUrl, bIsvMode)
{
//load the iframe
_loadarea(sArea, sParams, sUrl, bIsvMode);

if( sArea != "areaService" ) return;

//create the iframe object
var iframe = document.getElementById(sArea + "Frame");
//wait until the iframe is fully loaded ("complete")
iframe.onreadystatechange = function()
{
if( iframe.readyState == "complete")
{
var picklist,option;
//reference to the iframe document
var iframeDoc = iframe.contentWindow.document;
switch(sArea)
{
case "areaService":
picklist = iframeDoc.all.statecode[0];
option = ServiceOptions.All;
break;
}
picklist.value = option;
picklist.FireOnChange();
}
}
}

Friday, November 14, 2008

Creating an UpDown Control

The updown control is merely a control that enables the user to select a number from a predefined range either by direct assignment or by using up and down selectors (arrows). This is usually very useful when the changing of the number has an immediate affect of some sort on the form where it’s placed. This is just a UI enhancement that should be used only where appropriate.

The idea behind the mechanics of the updown control is that it uses a span with an overflow-y: scroll. The span has an inner span which is set to hidden but its height is set to the updown max value. This hack actually creates the range for you. When the user scrolls down the scrollTop attribute reflects the current position (selected number) which is written back to the text field. An opposite approach is used when the users assigns a value directly, then the span scrollTop is updated with the current field datavalue.





/*
parameters: fieldId - crm field id
minValue - positive number
maxValue - positive number
*/
function UpDownControl( fieldId , minValue , maxValue )
{
var Instance = this;
/* crm field */
var upDownCtrlBox = crmForm.all[ fieldId ];
/* field parent element field_d */
var parent = upDownCtrlBox.parentElement;
/* this span affects the spanUpDown scroll */
var span = document.createElement( "<SPAN style='visibility:hidden;width:1;'>" );
var spanUpDownHeight = upDownCtrlBox.clientHeight - 2;
/* this span is the scroller */
var spanUpDown = document.createElement( "<SPAN style='margin-left:-18px;overflow-y:scroll;width:1;height:" + spanUpDownHeight + ";'>" );
/* max range */
Instance.Max = maxValue;
/* min range */
Instance.Min = minValue;
/* fires when the scroll is pressed */
function onScrollChange()
{
/* get the current scroll position */
var scrollTop = parseInt( spanUpDown.scrollTop );
/* adjust size if value is outside of the scroll limits */
if( scrollTop < Instance.Min ){
scrollTop = Instance.Min;
spanUpDown.scrollTop = scrollTop;
}
else if( scrollTop > Instance.Max ){
scrollTop = Instance.Max;
spanUpDown.scrollTop = scrollTop;
}
/* assign the scroll value back to the field */
upDownCtrlBox.DataValue = scrollTop;
}
/* fires when the field value changes */
function onValueChange()
{
/* if the value is valid */
if( upDownCtrlBox.DataValue != null )
{
/* get the value */
var dataValue = parseInt( upDownCtrlBox.DataValue );
/* check that the vlaue is within permitted limits */
if( dataValue < Instance.Min ){
dataValue = Instance.Min;
upDownCtrlBox.DataValue = dataValue;
return false;
}
else if( dataValue > Instance.Max ){
dataValue = Instance.Max;
upDownCtrlBox.DataValue = dataValue;
return false;
}
/* assign the field value back to the scroll */
spanUpDown.scrollTop = dataValue;
}
/* assign min value is the datavalue is null */
else spanUpDown.scrollTop = Instance.Min;
}

/* fire onValueChange when the field is changed */
upDownCtrlBox.attachEvent( "onchange" , onValueChange );
/* adjust inner span height = maxValue + spanHeight */
span.style.height = maxValue + spanUpDownHeight;
/* construct the updown control */
spanUpDown.appendChild( span );
/* fire onScrollChange when the user presses the scrollbar */
spanUpDown.attachEvent( "onscroll" , onScrollChange );
parent.appendChild( spanUpDown );
/* call onValueChange when the form loads / control is created */
onValueChange();
}

widthUpDownCtrl = new UpDownControl("gi_width" , 20 , 10000 );

Thursday, November 13, 2008

Creating a Dynamic Picklist Control

There are situations where you need to create a Picklist with values from an external source or just don’t want to keep a static list on the Picklist attribute. Initially I posted a simple solution which illustrates how to convert a text attribute into a Picklist.

This post offers a different and more robust solution to the creation of dynamic Picklists.
I call the control a TextList which obviously is a hybridization of a text and a Picklist fields.

The TextList also contains a Dependent Field Collection whose fields are bind to its onchange event. This way, each time the Picklist changes, the option properties are mapped to the form dependent fields. In the code sample bellow I’ve created 2 additional attributes on each option. The first is called Color and the second is called JustANubmer. When the user selects an option the color and number are written to two dependent fields (gi_dependentfield1 and gi_dependentfield2). The TextList also exposes other wrapper methods on the inner Picklist control so you won’t have to deal with two objects.

I’ve added comments which explain the methods functionality.


// JScript File
function OnCrmPageLoad()
{
/* Change gi_name text field to a textlist */
var nameTextList = new TextList( "gi_name" );
/* Set 2 dependent fields */
nameTextList.DependentFields.Add( "gi_dependentfield1" , "JustANumber" );
nameTextList.DependentFields.Add( "gi_dependentfield2" , "Color" );
/* Add blank option */
nameTextList.Add( "" , "" );
for( var i = 0 ; i < 6 ; i++ )
{
/* Fill some values */
var option = nameTextList.Add( "Text " + i , "Value " + i );
/* create 2 new attributes on each option (Color,JustANumber) */
option.Color = "A" + i + "B" + i + "C" + i;
option.JustANumber = i.toString();
}
/* Set the initial value */
nameTextList.SetInitValue(true);
/* Finish the job */
nameTextList.Transform();
}

/*
Holds a list of all dependent fields.
Each textlist has a dependent fields collection
*/
DependentFieldCollection = function()
{
this.List = [];
this.Add = function( name , prop ){
this.List.push( new DependentField( name , prop ) );
}
}
/*
Single Dependent Field.
The Object maps between an option attribute an a crmForm field
*/
DependentField = function( name , prop )
{
this.Property = prop;
this.Name = name;
}
/* Text list control */
TextList = function( txtId )
{
var textList = this;
/* Referech the original text field */
var textCtrl = crmForm.all[ txtId ];
if (!textCtrl) return alert("TextList: " + txtId + " Is Missing");

/* Create a new Picklist control */
textList.Picklist = document.createElement("SELECT");
textList.Picklist.id = textCtrl.id;
textList.Picklist.req = textCtrl.req;
textList.Picklist.tabIndex = textCtrl.tabIndex;
textList.Picklist.className = "ms-crm-SelectBox ";
textList.Picklist.defaultSelected = textCtrl.DataValue;
textList.DependentFields = new DependentFieldCollection();

/* Add the new picklist and remove the text control */
textCtrl.parentElement.appendChild( textList.Picklist );
textCtrl.parentElement.removeChild( textCtrl );

/*
Finish the textlist transformation
Create inline dependent fields properties on the textlist
Attach an update function to the picklist onchange event
*/
textList.Transform = function()
{
for( var i = 0;i < textList.DependentFields.List.length;i++ ){
var depField = textList.DependentFields.List[ i ];
textList[ depField.Name ] = crmForm.all[ depField.Name ];
}
textList.Picklist.attachEvent( "onchange" , UpdateDependentFields );
}
/* Sets the picklist value */
textList.SetValue = function( value )
{
textList.Picklist.DataValue = value;
textList.Picklist.FireOnChange();
}
/*
Sets the picklist initial value.
This should be called when the page load after you fill the picklist options
*/
textList.SetInitValue = function( fireEvent )
{
textList.Picklist.DataValue = textList.Picklist.defaultSelected;
if(fireEvent) textList.Picklist.FireOnChange();
}
/* Add a new picklist Option */
textList.Add = function( text , value )
{
return textList.Picklist.AddOption( text , value );
}
/* Remove a picklist Option by its value */
textList.Remove = function( value )
{
textList.Picklist.DeleteOption( value );
}
/* Clear all Picklist Options */
textList.Clear = function()
{
textList.Picklist.length = 0;
}
/* Helper property for attaching to an event from within the picklist control */
textList.Attach = function( targetId , eventName , callback )
{
var target = document.all[ targetId ];
if ( target ) target.attachEvent( eventName , callback );
}
/* Updates the dependent fields when the picklist onchange fires */
function UpdateDependentFields()
{
if ( textList.Picklist.selectedIndex > -1 )
for( var i = 0;i < textList.DependentFields.List.length;i++ )
{
var depField = textList.DependentFields.List[ i ];
textList[ depField.Name ].DataValue = textList.Picklist.SelectedOption[ depField.Property ];
textList[ depField.Name ].FireOnChange();
}
}
}


OnCrmPageLoad();

Wednesday, November 12, 2008

Creating Mini Form Sections


The mini sections are a great way to create smaller logical areas inside a CRM section.
If you take a look at the field level security wizard you’ll see that we have used the mini section to create headings for each and every step.

Creating the mini section is fairly simple. All you need to do is create a new text attribute for each section and hide its label from within the customization. The code gets the hidden label text, places it inside the attribute control (DataValue) and formats the layout.

The end result looks like this:




function OnCrmPageLoad()
{
/* Convert the text attribute ‘gi_ministep1’ to a mini section */
CreateMiniSection("gi_ministep1");
}

function CreateMiniSection( ctrlId )
{
var Control = crmForm.all[ ctrlId ];
if ( !Control ) return;
Control.DataValue = " " + crmForm.GetLabel(Control);
Control.Disabled = true;
Control.style.cssText = "font-weight:900;border:1px solid #94b2de;filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#eff3ff,EndColorStr=#cedfff)";
}

OnCrmPageLoad();

Restricting access to IFRAME (read-only IFRAME)


This is a complementary post to my original post about creating a read-only iframe. The former post opens a new window outside of the users reach and steals the IFRAME focus. The following code makes use of the SECURITY=”restricted” IFRAME attribute that was introduced in IE6.
Since you can’t set this attribute from script once the IFRAME is on the form you need to remove the original IFRAME and create a new one instead.
The following code tells the tail.

One thing to notice is that the IFRAME will be completely inaccessible. This means that you can not script against the IFRAME at all. If you’re using my IFRAME viewer utility this solution won’t work for you.


function OnCrmPageLoad()
{
RestrictAccessToIFrame("IFRAME_Test");
}

function RestrictAccessToIFrame(iframeId)
{
/* Reference the relevant IFRAME */
var testIframe = document.all[iframeId];
/* get the iframe parent container */
var testIframeParent = testIframe.parentElement;

/* if the IFRAME exist */
if (testIframe)
{
/* Save the Original Iframe HTML */
var testIframeHTML = testIframe.outerHTML;
/* Remove the Original IFRAME */
testIframeParent.removeChild(testIframe);
/* Create a new IFRAME Instead */
testIframe = document.createElement(testIframeHTML);
/* Add an IFRAME restriction attribute to the new IFRAME Element */
testIframe.setAttribute("SECURITY","restricted");
/* IF needed rebuild the IFRAME url */
testIframe.src = "/MicrosoftCRM/_root/homepage.aspx?etc=1";
/* Append the IFRAME to its original parent element */
testIframeParent.appendChild(testIframe);
}
}

OnCrmPageLoad();

Tuesday, November 11, 2008

CRM 4.0 Field Level Security Wizard


My company uses the dynamics platform as basis for its products. From a product point of view, dynamics is an awesome foundation but is lacking many important functionalities which our soon to be customers require. In order to fill those gaps and achieve a fast pace product integration with as little development effort as possible we build functional wizards, in-house Utility Add-ons, to a variety of development tasks and business needs. Our goal eventually is to enable our integration team accomplish those tedious tasks in just a few clicks. A good example of that is the Field Level Security wizard which is presented herein.

As we already put time and effort in building those smart solutions, I thought it would be great to somehow share them with you on a “shareware” basis. In simple terms I decided to uncover those wizards with their full source code and installation instructions for a small and very affordable one time fee. Once you acquire the rights you can utilize and implement each wizard in any of your dynamics solutions as if it is your company own property.

The wizards will undoubtedly save you hundreds of development hours, speed up you delivery and much more. Since I’m testing this initiative for the time being I wanted to give you a taste and share this post with you. If you find this interesting, share your thought and questions with me (comment or send an email to support@gicrm.com).

The Field Level Security Wizard is a fully functional solution which enables you to define security settings throughout the organization hierarchy. In other words you can create FLS templates for each business unit, override or extend those settings for each security role and more importantly create exceptions at the user level. This gives you a much tighter security control over the entire dynamics application.

One of the most common tasks partners need to accomplish in every CRM project is hiding or showing specific form function (e.g. tab, toolbar button or left navigation link) depending on the user business unit, role and user. The FLS wizard can also facilitates this type of tasks with ease.

In the following demonstration I implemented a simple scenario on the account entity. First I create a simple FLS template on the business unit level by hiding the Main Phone and City fields, then I override those settings for the system administrator role by giving the role limited access to those fields and finally I create a specific exception for the administrator (me) and override/enable these fields completely.

In order to rewind the video right click on the flash movie, click rewind and then play.



FLS Features and information:
1. Supported Entities - All entity types (System, Customizable and Custom).
You can find the entire entity list inside CRM advanced find window.

2. Supports both online and offline modes.

3. Supports the following CRM modules:
  1. Entity Form (Can be extended using client side API)
  2. Entity Print Form
  3. Entity Views
  4. Entity Views Print page
  5. Static Export to Excel
  6. Advanced Find
  7. Mail Merge
  8. Merge
  9. Filtered View (For Reports)
  10. Entity Associated views
  11. Lookup views
  12. CRM Workflows

4. Supported Languages - FLS Customizations are available in all base languages – no language packs needed, But you’ll need to translate them your self.

5. Supported Layouts - Dir RTL ( Right to Left ) and LTR ( Left to Right ) layouts

6. Supports the following field level security modes:
  1.  Default – Field is not set
  2.  Hidden – Field is hidden on both crm form and views
    a.  Keep layout – the field space is kept
    b.  Collapse layout – the field space is collapsed for better presentation
  3.  Missing – Field is disabled , the user can not see the data
  4.  Disabled – Field is disabled , the user can see the data
  5.  Enabled – Override existing settings, the user has full permissions on the field.

7. Supports “Formless” Entities – Entities that do not have a form like activity

8. Supports Security Hierarchy:


  1. Business Unit level: Creation of FLS Templates that affect the entire business unit.
  2. Role Level: Inheritable security roles with an option to extend or override business unit settings
  3. User Level: Inheritable User settings with the option to create exceptions for users within the same role

9. Fields View - Intelligent Orientation.


The user can filter by:
  1. Field Type: Presents all available field types
  2. Existing: Presents existing Settings
  3. Placement: Presents fields that exist ON and OFF the CRM Form
  4. Tabs: Presents All Fields Categorized by Tab Name
  5. Sections: Presents All Fields Categorized by Section Name

The wizard also supports Internet Facing Deployment (IFD) and IE8

As you can see, we give a lot of attention to the entire usability issue. The idea is to enable the integrator / developer to achieve his FLS goals as fast as possible.

The wizard is now available online. I’m sure you’ll find it worthwhile. Feel free to comment here or send your questions to support@gicrm.com.

Wednesday, October 8, 2008

Objectizing Ajax (XML) results





Definition: Objectize - To make an object of; to regard as an object; to place in the position of an object.


Objectizing XML documents is a very common concept incorporated into dot.net server side framework. Whether you’re creating a web service proxy or desterilizing objects what you’re doing is objectizing xml. Wouldn’t it be the coolest thing if you could do the same on the client side, regard xml results coming back from Ajax requests as objects and manipulating then with ease. So instead on dealing with Xml DOM and writing code that looks like this:


Root.childNodes[0].childNodes[0].firstChild.attributes[“name”]


You can actually write:


Root.resultset.result.businessunitid.name


While creating my own objectizer I came across Yusuke Kawasaki ObjTree.js free class library and decided to check it out.
I modified it slightly to support ms resulting xml schema and made it a bit leaner to so only the objectizing part is included.


The OnCrmPageLoad includes a very simple example of how to create the ObjTree object, passing it an xml document and receiving an object representation of that document.The post uses the “Ajax using Fetch Message” to demonstrate the usage.

Part of Yusuke Kawasaki ObjTree.js Library


if ( typeof(XML) == 'undefined' ) XML = function() {};

// constructor

XML.ObjTree = function () {
return this;
};

// object prototype
XML.ObjTree.prototype.xmlDecl = '<?xml version="1.0" encoding="UTF-8" ?>\n';
XML.ObjTree.prototype.attr_prefix = '';

// method: parseXML( xmlsource )
XML.ObjTree.prototype.parseXML = function ( xml ) {
var root;
if ( window.ActiveXObject ) {
xmldom = new ActiveXObject('Microsoft.XMLDOM');
xmldom.async = false;
xmldom.loadXML( xml );
root = xmldom.documentElement;
}
if ( ! root ) return;
return this.parseDOM( root );
};

// method: parseDOM( documentroot )
XML.ObjTree.prototype.parseDOM = function ( root ) {
if ( ! root ) return;

this.__force_array = {};
if ( this.force_array ) {
for( var i=0; i<this.force_array.length; i++ ) {
this.__force_array[this.force_array[i]] = 1;
}
}

var json = this.parseElement( root ); // parse root node
if ( this.__force_array[root.nodeName] ) {
json = [ json ];
}
if ( root.nodeType != 11 ) { // DOCUMENT_FRAGMENT_NODE
var tmp = {};
tmp[root.nodeName] = json; // root nodeName
json = tmp;
}
return json;
};

// method: parseElement( element )
XML.ObjTree.prototype.parseElement = function ( elem ) {
// COMMENT_NODE
if ( elem.nodeType == 7 ) {
return;
}

// TEXT_NODE CDATA_SECTION_NODE
if ( elem.nodeType == 3 || elem.nodeType == 4 ) {
var bool = elem.nodeValue.match( /[^\x00-\x20]/ );
if ( bool == null ) return; // ignore white spaces
return elem.nodeValue;
}

var retval;
var cnt = {};

// parse attributes
if ( elem.attributes && elem.attributes.length ) {
retval = {};
for ( var i=0; i<elem.attributes.length; i++ ) {
var key = elem.attributes[i].nodeName;
if ( typeof(key) != "string" ) continue;
var val = elem.attributes[i].nodeValue;
if ( ! val ) continue;
key = this.attr_prefix + key;
if ( typeof(cnt[key]) == "undefined" ) cnt[key] = 0;
cnt[key] ++;
this.addNode( retval, key, cnt[key], val );
}
}

// parse child nodes (recursive)
if ( elem.childNodes && elem.childNodes.length ) {
var textonly = true;
if ( retval ) textonly = false; // some attributes exists
for ( var i=0; i<elem.childNodes.length && textonly; i++ ) {
var ntype = elem.childNodes[i].nodeType;
if ( ntype == 3 || ntype == 4 ) continue;
textonly = false;
}
if ( textonly ) {
if ( ! retval ) retval = "";
for ( var i=0; i<elem.childNodes.length; i++ ) {
retval += elem.childNodes[i].nodeValue;
}
} else {
if ( ! retval ) retval = {};
for ( var i=0; i<elem.childNodes.length; i++ ) {
var key = elem.childNodes[i].nodeName.replace("#","");
if ( typeof(key) != "string" ) continue;
var val = this.parseElement( elem.childNodes[i] );
if ( ! val ) continue;
if ( typeof(cnt[key]) == "undefined" ) cnt[key] = 0;
cnt[key] ++;
this.addNode( retval, key, cnt[key], val );
}
}
}
return retval;
};

// method: addNode( hash, key, count, value )
XML.ObjTree.prototype.addNode = function ( hash, key, cnts, val ) {
key = this.key_qualify(key);
if ( this.__force_array[key] ) {
if ( cnts == 1 ) hash[key] = [];
hash[key][hash[key].length] = val; // push
} else if ( cnts == 1 ) { // 1st sibling
hash[key] = val;
} else if ( cnts == 2 ) { // 2nd sibling
hash[key] = [ hash[key], val ];
} else { // 3rd sibling and more
hash[key][hash[key].length] = val;
}
};

XML.ObjTree.prototype.key_qualify = function( key ){
return key.replace(/\W/gi,"");
}
// method: xml_escape( text )
XML.ObjTree.prototype.xml_escape = function ( text ) {
return String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
};

/*
Copyright (c) 2005-2006 Yusuke Kawasaki. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the Artistic license. Or whatever license I choose,
which I will do instead of keeping this documentation like it is.
*/



The original xml result looks like this


<resultset morerecords=\"0\" paging-cookie=\"&lt;cookie page=&quot;1&quot;&gt;&lt;fullname last=&quot;SYSTEM&quot; first=&quot;INTEGRATION&quot; /&gt;&lt;systemuserid last=&quot;{D874E288-2C8C-43D5-AEBA-5404888BC185}&quot; first=&quot;{B2C53269-CFF5-4F26-A4E5-669284EA6E96}&quot; /&gt;&lt;/cookie&gt;\">
<result>
<fullname>INTEGRATION</fullname>
<businessunitid dsc=\"0\" name=\"MicrosoftCRM\">
{80E4E0DF-65C1-DC11-B67A-0003FFBB057D}
</businessunitid>
<systemuserid>{B2C53269-CFF5-4F26-A4E5-669284EA6E96}</systemuserid>
</result>
<result>
<fullname>LitwareInc Administrator</fullname>
<businessunitid dsc=\"0\" name=\"MicrosoftCRM\">
{80E4E0DF-65C1-DC11-B67A-0003FFBB057D}
</businessunitid>
<systemuserid>{9BF5E0DF-65C1-DC11-B67A-0003FFBB057D}</systemuserid>
</result>
</resultset>



Usage Example:



function OnCrmPageLoad()
{
/* Get All Users */
var fetchXml = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">';
fetchXml += '<entity name="systemuser">';
fetchXml += '<attribute name="fullname"/>';
fetchXml += '<attribute name="businessunitid"/>';
fetchXml += '<attribute name="title"/>';
fetchXml += '<attribute name="address1_telephone1"/>';
fetchXml += '<attribute name="systemuserid"/>';
fetchXml += '<order attribute="fullname" descending="false"/>';
fetchXml += '</entity>';
fetchXml += '</fetch>';

/* Make the fetch and retrieve xml result */
var resxml = Fetch(fetchXml);
/* Create an ObjTree Object */
var xotree = new XML.ObjTree();
/* Objectize xml result */
var tree = xotree.parseXML( resxml );

/*
if the original node contains only data then the property is treated as string
*/
alert( tree.resultset.result[0].fullname )
/*
if the original node contains attributes or children then you should treat it as
object and reference its properties e.g. object.text and object.propertyName
*/
alert( tree.resultset.result[0].businessunitid.text ); //GUID
alert( tree.resultset.result[0].businessunitid.name ); //OrgName
}

function Fetch( xml )
{
var Xml = "<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\">"
Xml += GenerateAuthenticationHeader()
Xml += "<soap:Body>";
Xml += "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">";
Xml += "<fetchXml>";
Xml += _HtmlEncode(xml); // Microsoft _HtmlEncode function
Xml += "</fetchXml>";
Xml += "</Fetch>";
Xml += "</soap:Body>";
Xml += "</soap:Envelope>";

var XmlHttp = CreateXmlHttp(); // Microsot CreateXmlHttp function
XmlHttp.open("POST", "/mscrmservices/2007/crmservice.asmx", false ); //Sync Request
XmlHttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
XmlHttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
XmlHttp.send(Xml);

return XmlHttp.responseXML.text
}

OnCrmPageLoad();

Friday, October 3, 2008

Playing with Notes


This post is more of an example about how one can utilize the “Show Fetch in IFRAME” post in order to consolidate customer information in a focal location.

Consider a scenario where a contact center service rep needs to see the customer’s related notes (annotations) as part of the case service workflow. In order to achieve that using out of box functionality the rep needs to open the customer’s account page, navigate to the notes tab and select the desired document out of all the notes (text and annotations). Most customers find it an unacceptable behavior as do I.

In order to transform the above behavior into a “One Click Process” procedure I’ve added a new IFRAME to the case entity called IFRAME_relatednotes and attached the iframe results to the customer’s lookup onchange event. Each time the customer lookup changes the iframe is filled with the selected customer annotations.

Follow the “Display Fetch in IFRAME” post in order to assign valid FetchViewer Notes Parameters.I’ve also added the FetchViewer Class functionally as part of this example for the sake of completeness.






var customerLookup;
function OnCrmPageLoad()
{
customerLookup = crmForm.all.customerid;

window.CustomerRelatedNotes = new FetchViewer("IFRAME_relatednotes");
CustomerRelatedNotes.LayoutXml = getLayoutXml();
CustomerRelatedNotes.Entity = "annotation";
CustomerRelatedNotes.QueryId = "{DDDFF6AE-2F52-4640-B2BB-2BA59DA0777C}";
//First Time
SetRelatedNotesFetchXml();
CustomerRelatedNotes.RegisterOnTab(0); //IFRAME ON THE DEFAULT TAB
//Consequent lookup selections
crmForm.all.customerid.attachEvent( "onchange" , function(){
SetRelatedNotesFetchXml();
CustomerRelatedNotes.Refresh();
});
}

/*
The function construct a valid fetchexml depending on the customer selection taking the customer type into account.
If the customer lookup does not contain data then the fetch uses an empty GUID
*/
function SetRelatedNotesFetchXml(){

var customerGuid = "{00000000-0000-0000-0000-000000000000}"
var customerType = "account"

if( customerLookup.DataValue != null )
{
customerGuid = customerLookup.DataValue[0].id;
customerType = customerLookup.DataValue[0].typename;
}

var fetchXml = '<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true">';
fetchXml += '<entity name="annotation">';
fetchXml += '<attribute name="subject"/>';
fetchXml += '<attribute name="notetext"/>';
fetchXml += '<attribute name="filename"/>';
fetchXml += '<attribute name="annotationid"/>';
fetchXml += '<order attribute="subject" descending="false"/>';
fetchXml += '<filter type="and">';
fetchXml += '<condition attribute="isdocument" operator="eq" value="1"/>';
fetchXml += '</filter>';
fetchXml += '<link-entity name="' + customerType + '" from="' + customerType + 'id" to="objectid" alias="aa">';
fetchXml += '<filter type="and">';
fetchXml += '<condition attribute="' + customerType + 'id" operator="eq" uitype="' + customerType + '" value="' + customerGuid + '"/>';
fetchXml += '</filter>';
fetchXml += '</link-entity>';
fetchXml += '</entity></fetch>';

CustomerRelatedNotes.FetchXml = fetchXml;
}

function getLayoutXml(){
return '<grid name="resultset" object="5" jump="" select="1" icon="1" preview="1"><row name="result" id="annotationid"><cell name="subject" width="200" /><cell name="notetext" width="200" /><cell name="filename" width="80" /></row></grid>';
}

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

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 );
m_iframeDoc = getIframeDocument();
m_iframeDoc.body.scroll = "no";
m_iframeDoc.body.style.padding = "0px";
}

function getIframeDocument(){
return Instance.Iframe.contentWindow.document;
}
}

OnCrmPageLoad();