Mar 082013
 

The XrmSvcToolkit on codeplex is a very small and easy to use toolkit for interacting with the Xrm web-services. TypeScript is a language for application-scale JavaScript development by compiling to plain JavaScript.

Once you’ve downloaded and installed TypeScript, you can use the Xrm 2011 TypeScript Definition from codeplex for the Xrm.Page. You can reference it using:

        /// <reference path=”Xrm2011.d.ts” />

You can reference the below Type Definition for the XrmSvcToolkit the same way:

        /// <reference path=”XrmSvcToolkit.d.ts” />

XrmSvcToolkit.d.ts

/* *****************************************************************************
TypeScript definition file for the XrmSvcToolkit
Author: Carlton Colter - http://www.mscrmblogger.com/
XrmSvcToolkit - http://xrmsvctoolkit.codeplex.com/
***************************************************************************** */

declare module XrmSvcToolkit {
    export function context(): {
        getAuthenticationHeader(): string;
        getCurrentTheme(): string;
        getOrgLcid(): number;
        getOrgUniqueName(): string;
        getQueryStringParameters(): string;
        getServerUrl(): string;
        getUserId(): string;
        getUserLcid(): number;
        getUserRoles(): string[];
        isOutlookClient(): bool;
        isOutlookOnline(): bool;
        prependOrgName(sPath: string): string;
    };
    export function serverUrl(): string;
    export function retrieve(opts: {
        entityName: string;
        id: string;
        select: string;
        expand: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function retrieveMultiple(opts: {
        entityName: string;
        odataQuery: string;
        async: bool;
        successCallback: Function;
        completionCallback: Function;
        errorCallback: Function;
    }): Object[];
    export function createRecord(opts: {
        entityName: string;
        entity: Object;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function updateRecord(opts: {
        entityName: string;
        id: string;
        entity: Object;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function deleteRecord(opts: {
        entityName: string;
        id: string;
        entity: Object;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function associate(opts: {
        entity1Id: string;
        entity1Name: string;
        entity2Id: string;
        entity2Name: string;
        relationshipName: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function disassociate(opts: {
        entity1Id: string;
        entity1Name: string;
        entity2Id: string;
        relationshipName: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function setState(opts: {
        entityName: string;
        id: string;
        stateCode: number;
        statusCode: number;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function execute(opts: {
        executeXml: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function fetch(opts: {
        fetchXml: string;
        async: bool;
        successCallback: (result)=>void;
        errorCallback: Function;
    }): Object[];
}
Oct 122012
 

You can download the unmanged or managed solutions by clicking on unmanged or managed.

There are times when hierarchy matters, when one object rolls up into another, and you need something like the subject lookup. Unfortunately the subject lookup is not an option you can turn on for your own lookups. However, thanks to some JScript from Tanguy you can override the default lookup control, making it launch your own web-interface. Yes, I know this is not supported, but at least the rest of this add-on follows the SDK guidelines.

UR12 / Polaris: the solution above does not work properly with UR12. I have not tested this UR12 solution with previous versions of CRM 2011. It no-longer requires you to specify the Set in the Set name, i.e. Category is just Category, not CategorySet. It uses the XrmSvcToolkit by Daniel Cai instead of the XrmServiceToolkit. You can download an unmanaged solution here. The technique used by Tanguy nolonger works, so I had to access the underlying behavior of the form element. I wish I could tell you that it won’t ever break, but there is no telling, because it is an unsupported modification. If you would like to do something supported, you can put a button on the ribbon to launch the custom web-resource and load the resulting data into the field.

Here is what it looks like:

Now, I won’t go into the details of how the web-resource works (you can look at my code), but here is how you use it:

  1. You can install either the unmanged or managed solution into your CRM
  2. Add the msblg_treelookup.js to your form scripts.
    1. In the form designer for your entity click on the Form Properties button.
    2. Click add, and search for name msblg_treelookup.js, select it, and click ok.
  3. Then add an event handler for the Form OnLoad (by scrolling down and clicking add)

    1. The parameters are:

      1. Attribute Name
      2. Entity Set Name (for the UR12 release, do not use Set, just use the entity name)
      3. Object Type Code
      4. The Id Field
      5. The Name Field
      6. The Parent Field (Lookup to the same entity)
      7. Title for the lookup window
      8. Description for the lookup window
      9. Column Header
      10. A REST filter that will be applied to all queries (not required)

The LookupTree function makes the existing lookup button launch the custom tree-view lookup. If you wanted to use this control in a completely supported manner, with some modifications you could add a button to launch the control.

Aug 072012
 

In response to Yair on a previous comment, here is how I added the GUID to the header. I just added this html webresource and then dropped it on the page. This also shows an example of how to access the parent page’s Xrm property when it is print-previewed.

<HTML xmlns="http://www.w3.org/1999/xhtml">
<HEAD>
  <TITLE>Guid Header</TITLE>
  <LINK rel=stylesheet type=text/css href="/_common/styles/fonts.css.aspx?lcid=1033">
  <LINK rel=stylesheet type=text/css href="/_common/styles/global.css.aspx?lcid=1033">
  <LINK rel=stylesheet type=text/css href="/_common/styles/select.css.aspx?lcid=1033">
  <SCRIPT type=text/javascript>

var copyToClipboard = function(text) 
{ 
  if (window.clipboardData) // IE 
  {   
    window.clipboardData.setData("Text", text); 
  } 
};

var copyguid = function()
{
  var sp = document.getElementById('entityguid');
  copyToClipboard(sp.innerText);
};

var Xrm = {};

var onload = function ()
{
  if (top.location.href.indexOf("/print/print.aspx") != -1) {
    Xrm = window.opener.Xrm;
  } else {
    Xrm = parent.Xrm;
  }
  var id = Xrm.Page.data.entity.getId();
  var sp = document.getElementById('entityguid');
  sp.innerText = id;
};
  </SCRIPT>

  <STYLE type=text/css>
.guid {
  font:18px segoe ui,tahoma,arial;  
  text-align:right;
  width:100%;
}
body {
  background-color:rgb(246, 248, 250);
}
a.copy {
  color: blue;
  font-size: 9px;
  font-decoration: underline
}
  </STYLE>
  <META charset=utf-8>
</HEAD>
<BODY onload=onload();>
  <TABLE cellSpacing=0 cellPadding=0 width="100%">
    <TBODY>
      <TR>
        <TD id=cell_dd>
<P class=guid>ID: <SPAN id=entityguid></SPAN>&nbsp; <A class="copy" href="javascript:copyguid();">Copy ID</A>&nbsp; </P>
        </TD>
      </TR>
    </TBODY>
  </TABLE>
</BODY>
</HTML>
May 102012
 

Do you need the value from a related entity, like a lookup? I had written this for CRM 4 a while back, and people use it, but I occasionally get questions about looking up other values in CRM. This bit of code will help you find those values on a related object. Below are two options to do this. Option 1: using the REST SDK and JSON2, and then Option 2: using Jaimie Ji’s XRM Service Toolkit.

Jaimie Ji’s XRM Service Toolkit provides a comprehensive set of JScript libraries for interacting with the SOAP and REST SDK through JavaScript. While Option 1 in this particular use case is fairly simple, when you start needing to do more advanced things, you may want to look at using that kit. It is fairly comprehensive and he keeps it up to date.

Option 1: Using the REST SDK and JSON2

Using the REST SDK is great, and there are some really good examples in the CRM SDK help file. Now in order to use the below code-block, you need to use JSON.

JSON, or JavaScript Object Notation, is a text format that is used to interchange data. JSON2.js is a lightweight javascript that convert the strings to javascript objects and vice-versa. The CRM REST SDK can return data in JSON format.

To get the code for JSON2, you can go here. You will need to embed this code into your javascript file or include json2.js along with the code below. Without it, you will not be able to parse the data properly, and the code below uses the JSON library in json2.js.

Ok, below we have 3 functions:

  • getServerUrl : which gets the server URL – taken from the CrmRestKit
  • Lookup_Changed : what you call when a lookup is changed
  • retrieveReqCallBack : the callback function that performs any actions

You would put Lookup_Changed in the change event of a lookup field and specify the attributes like one of the following:

  • ‘contactid’,’Contact’,’contactlookup’
  • ‘contactid’,’Contact’,’contactlookup’,[‘Telephone1′]
  • ‘contactid’,’Contact’,’contactlookup’,[‘Telephone1′,’FullName]

Then you’d put your actions inside the retrieveReqCallBack.

function getServerUrl() {  
    // From CrmRestKit.js
    var localServerUrl = window.location.protocol + "/" + window.location.host;
    var context = parent.Xrm.Page.context;

    if (context.isOutlookClient() && !context.isOutlookOnline()) {
        return localServerUrl;
    }
    else {
        var crmServerUrl = context.getServerUrl();
        crmServerUrl = crmServerUrl.replace(/^(http|https):\/\/([_a-zA-Z0-9\-\.]+)(:([0-9]{1,5}))?/, localServerUrl);
        crmServerUrl = crmServerUrl.replace(/\/$/, "");
    }
    return crmServerUrl;
}

function Lookup_Changed(attributeName, entityName, callbackId, columns) {
    var serverUrl = Xrm.Page.context.getServerUrl();
    var ODataPath = serverUrl + "/XRMServices/2011/OrganizationData.svc";
    
    var lookup = Xrm.Page.data.entity.attributes.get(attributeName).getValue();
    if (lookup===null) {
        return false;
    }
  
    var id = lookup[0].id;
    if (id == null) { 
        return false;
    }
    
    var retrieveReq = new XMLHttpRequest();
    
    var url = ODataPath + "/"+entityName+"Set(guid'" + id + "')";
    if (columns !== undefined && columns !== null) {
        url = url + "?$select=" + columns.join(',');
    }
    
    retrieveReq.open("GET", url, true);
    retrieveReq.setRequestHeader("Accept", "application/json");
    retrieveReq.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    retrieveReq.onreadystatechange = function () {
        if (this.readyState == 4 /* complete */) {
            if (this.status == 200) {
                var data;
                var jData;
                jData = JSON.parse(this.responseText);
                if (jData && jData.d && jData.d.results && jData.d.results.length > 0) {
                    data = jData.d.results[0];
                } else if (jData && jData.d) {
                    data = jData.d;
                }
                
                if (data == null) {
                    return;
                }
                retrieveReqCallBack(callbackId, data);
            }
        }
    };
    retrieveReq.send();
    return true;
}

function retrieveReqCallBack(calltype, data) {
    var toLookup=function(data) {
        if (data==null || data=="" || (data.Id==null)) {
            return null;
        }
    
        var result=new Array();
        result[0] = {};
        result[0].id = data.Id;
        result[0].name = data.Name;
        result[0].entityType = data.LogicalName;
        return result;
    }

    switch (calltype) {
        case 'pricelookup':
            Xrm.Page.data.entity.attributes.get("new_pricecategorylookup").setValue(toLookup(data.new_pricecategorylookup);
            break;
        case 'contactlookup':
            Xrm.Page.data.entity.attributes.get("insp_poctelephonenumber").setValue(data.Telephone1);
            break;
        default:
            break;
    }
}

Option 2: Using the XrmServiceToolkit with similar functions

Ok, now let’s assume you are using the XRM Service Toolkit, and you want to do the same thing, using a Lookup_Changed function and the retrieveReqCallBack function. Instead of embedding JSON and a method to get the server url, you can use the following:

function Lookup_Changed(attributeName, entityName, callbackId, columns) {
    var lookup = Xrm.Page.data.entity.attributes.get(attributeName).getValue();
    if (lookup===null) {
        return false;
    }
  
    if (lookup[0].id == null) { 
        return false;
    }
	
	XrmServiceToolkit.Rest.Retrieve(
        lookup[0].id,
        entityName,
        columns,
		null,
        function (result) {
            retrieveReqCallBack(callbackId, result);
        },
        function (error) {
            throw error;
        },
		true
    );
}

function retrieveReqCallBack(calltype, data) {
    var toLookup=function(data) {
        if (data==null || data=="" || (data.Id==null)) {
            return null;
        }
    
        var result=new Array();
        result[0] = {};
        result[0].id = data.Id;
        result[0].name = data.Name;
        result[0].entityType = data.LogicalName;
        return result;
    }

    switch (calltype) {
        case 'pricelookup':
            Xrm.Page.data.entity.attributes.get("new_pricecategorylookup").setValue(toLookup(data.new_pricecategorylookup);
            break;
        case 'contactlookup':
            Xrm.Page.data.entity.attributes.get("insp_poctelephonenumber").setValue(data.Telephone1);
            break;
        default:
            break;
    }
}

The XRM Service Toolkit does a lot, and I would suggest checking it out.

Mar 312012
 

Ok, a lot of people have been asking on the forums about how to display images that are attached to your CRM entity. To help with that, here is a web-resource that will display images and fit them to the area allotted. If you have more than 1 picture it will show some previous and next buttons below the photo. The general idea here is to not only display an image on the form, but work across multiple browsers (and systems). Leveraging JQuery and the REST SDK, this attachment image web-resource is a quick-and-easy image control, granted it doesn’t include all of the features the Silverlight CRMAttachmentImage control does.

Below you can see an example of what it looks like. YOu can download the managed solution here and the unmanaged solution here.

You can specify the height and width in the parameters.

height:200|width:100

The image will resize to scale.

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Image Browser</title>
  <script type="text/javascript" src="jq_1.7.2.min.js">
</script>
  <script type="text/javascript">
//<![CDATA[
    $.fn.sandbox = function(fn) {
        var element = $(this).clone(), result;
        // make the element take space in the page but invisible
        element.css({visibility: 'hidden', display: 'block'}).insertAfter(this);
        // to override any display: none !important you may have been using
        element.attr('style', element.attr('style').replace('block', 'block !important'));
        result = fn.apply(element);
        element.remove();
        return result;
    };

    var GetDataParams = function()
    { //modified version of: http://technet.microsoft.com/en-us/library/gg327945.aspx
      //Get the any query string parameters and load them
      //into the vals array

      var vals = new Array();
      if (location.search != "")
      {
        vals = location.search.substr(1).split("&");
        for (var i in vals)
        {
          vals[i] = vals[i].replace(/\+/g, " ").split("=");
        }
        
        //look for the parameter named 'data'
        var found = false;
        for (var i in vals)
        {
          if (vals[i][0].toLowerCase() == "data")
          {
            found = true; 
            var datavals = decodeURIComponent(vals[i][1]).split("|");
            for (var i in datavals)
            {
              datavals[i] = datavals[i].replace(/\+/g, " ").split("=");
            }
            break;
          }
        }
        if (found) { return datavals; }
      }
      return null;
    }
    
    var ImageBrowser = function () {
      function getImage() {
        function getRecords(query, successCallback) {
            $.ajax({
                type: "GET",
                async: true,
                contentType: "application/json; charset=utf-8",
                url: query,
                datatype: "json",
                beforeSend: function (request) {

                    request.setRequestHeader("Accept", "application/json");
                },
                success: function(data, textStatus, xhr) {
                    if (data && data.d && data.d.results) {
                        successCallback(data.d.results)
                    }
                },
                error: function (xhr, textStatus, errorThrown) {
                    alert("Error : " +
                        xhr.status + ": " +
                        xhr.statusText);
                }
            });
        }
        
        function buildQuery(s,c,f,t,p) {
            function getServerUrl() {  // From CrmRestKit.js
                var localServerUrl = window.location.protocol + "//" + window.location.host;
                var context = parent.Xrm.Page.context;

                if (context.isOutlookClient() && !context.isOutlookOnline()) {
                    return localServerUrl;
                }
                else {
                    var crmServerUrl = context.getServerUrl();
                    crmServerUrl = crmServerUrl.replace(/^(http|https):\/\/([_a-zA-Z0-9\-\.]+)(:([0-9]{1,5}))?/, localServerUrl);
                    crmServerUrl = crmServerUrl.replace(/\/$/, "");
                }
                return crmServerUrl;
            }
            return getServerUrl() + "/XRMServices/2011/OrganizationData.svc/" + s + "/?" +
                "&$filter=" + f + 
                "&$select=" + c.join(',') + 
                "&$top=" + t +
                "&$skip=" + p;
        }
        
        function updateImage(records) {
            var foundpicture = false;
            var picture;
            if (records.length<1) {
                $("#picture").css("display","none");
                picture = $("#nopicture");
                picture.css("height",200);
                picture.css("width",120);
            } else {
                foundpicture = true;
                var source = "data:" + records[0].MimeType + ";base64," + records[0].DocumentBody;
                picture = $("#picture");
                picture.css("height","auto");
                picture.css("width","auto");
            }

            picture.css("display","none");
            
            var pHeight = ImageBrowser.MaxHeight;
            var pWidth = ImageBrowser.MaxWidth;
            
            if (foundpicture) { picture.attr("src",source); }
            
            var pictureSize = picture.sandbox(function(){ return {height: this.height(), width: this.width()}; });
            
            var scale = 1;
            if (pictureSize.height>pHeight) {
                if ((pictureSize.width-pWidth)>(pictureSize.height-pHeight)) {
                    scale = pictureSize.width / pWidth;
                } else {
                    scale = pictureSize.height / pHeight;
                }
            }
            if (scale!=1) {
                var nh = pictureSize.height / scale;
                var nw = pictureSize.width / scale;
                
                picture.css("height",nh + "px");
                picture.css("width",nw + "px");
            }
            
            picture.css("display","block");
        }
        
        function updateFirstLast(records) {
            ImageBrowser.First = (ImageBrowser.Index==0) ? true : false;
            ImageBrowser.Last = (records.length<1) ? true : false;
            
            $("#previous").css("display",ImageBrowser.First ? "none" : "inline");
            $("#next").css("display",ImageBrowser.Last ? "none" : "inline");
        }
      
        var setName = 'AnnotationSet';    
        var columns = ['DocumentBody','MimeType'];
        
        var filter = "ObjectId/Id eq (guid'" + ImageBrowser.EntityId + "') " + 
                     "and startswith(MimeType,'image/') " + 
                     "and ObjectTypeCode eq '" + ImageBrowser.EntityName + "'";
                     
        var top = 1;
        var skip = ImageBrowser.Index;
            
        getRecords(buildQuery(setName,columns,filter,top,skip+1),updateFirstLast);
        getRecords(buildQuery(setName,columns,filter,top,skip),updateImage);
      }
      
      function previous() {
        if (ImageBrowser.Index>0) {
            ImageBrowser.Index = ImageBrowser.Index - 1;
            getImage();
        }
      }
      
      function next() {
        if (ImageBrowser.Last==false) {
            ImageBrowser.Index = ImageBrowser.Index + 1;
            getImage();
        }
      }
      
      function init() {
        ImageBrowser.MaxWidth = $("body").width();
        ImageBrowser.MaxHeight = $("body").height();
        var data = GetDataParams();
        
        for (var i in data)
        {
            switch (data[i][0].toLowerCase())
            {
                case 'height': 
                    var h = parseInt(data[i][1]);
                    var ph = $("body").height();
                    if (h<ph) {
                        ImageBrowser.MaxHeight = h;
                    } else {
                        ImageBrowser.MaxHeight = ph;
                    }
                    break;
                    
                case 'width': 
                    var w = parseInt(data[i][1]);
                    var pw = $("body").width();
                    if (w<pw) {
                        ImageBrowser.MaxWidth = w;
                    } else {
                        ImageBrowser.MaxWidth = pw;
                    }
                    break;
                    
                $("#area").height(parseInt(data[i][1])); break;
            }
        }
        getImage();
      }
      
      return {
            EntityId: parent.Xrm.Page.data.entity.getId(),
            EntityName: parent.Xrm.Page.data.entity.getEntityName(),
            First: true,
            MaxHeight: 0,
            MaxWidth: 0,
            Last: false,
            Index: 0,
            Previous: previous,
            Next: next,
            Initialize: init
      };
    }();
    
  function next() { ImageBrowser.Next(); }
  function prev() { ImageBrowser.Previous(); }
  function onload() {ImageBrowser.Initialize(); }
  
  //]]>
  </script>
  <style type="text/css">
/*<![CDATA[*/
  html, body, div {
    background-color: rgb(246, 248, 250);
    font-family: "Tahoma", "Helvetica", "Arial",  "Verdana", "sans-serif";
    font-size:9px;
    padding: 0px;
    margin: 0px;
  }
  body {
    margin: 3px;
  }
  body, div.area {
    text-align: center;
  }
  img {
    border-style: none;
  }
  #nopicture {
    border: solid 1px black;
    font-size: 9px;
    text-align:center;
  }
  #nopicture p {
    padding-top: 30%;
  }
  /*]]>*/
  </style>
  <meta charset="utf-8" />
</head>

<body onload="onload();" contenteditable="true">
  <div id="area">
  <table cellpadding="0" cellspacing="0">
  <tr><td id="picturearea" colspan="2"><img id="picture" class="picture" src="" /><div id="nopicture" style="display:none;"><p>No Pictures</p></div></td></tr>
  <tr>
    <td style="text-align:right;"><a id="previous" href="javascript:prev();" style="display:none"><img src="img_icons/previous.png" /></a></td>
    <td style="text-align:left;"><a id="next" href="javascript:next();" style="display:none"><img src="img_icons/next.png" /></a></td>
  </tr>
  </table>
  </div>
</body>
</html>

Thanks to Willington’s Blog for the sandbox jquery function.

Nov 012011
 

Recently I’ve received some questions on how to change the optionset (previously called picklist) values in CRM 2011 the same way I did in my previous CRM 4 post about using the OnChange event of a yes/no field to change the values of a picklist.

This technet page shows the methods available for the different types of controls.

Here is an example of how to change the optionset based on a yes/no field:

function BuildOptionSet(picklist, valuelist)
{
	picklist.clearOptions();
	for(var i=0; i<valuelist.length; i++)
	{
		var listitem = valuelist[i];
		picklist.addOption(listitem[0], listitem[1]);
	}
}

var lista = new Array(); 
lista[0] = new Array('Alpha',0);
lista[1] = new Array('Beta',1);

var listb = new Array(); 
listb[0] = new Array('Charlie',2);
listb[1] = new Array('Delta',3);

var uselista = Xrm.Page.data.entity.attributes.get("YesNo").getValue();
var optionset = Xrm.Page.ui.controls.get("OptionSet");

if (uselista==true) {
   BuildOptionSet(optionset, lista);
} else {
   BuildOptionSet(optionset, listb);
}

You just need to change the values of lista and listb and the YesNo and OptionSet lines to reflect the fields on your form, and add it to the onchange event of the dropdown Yes/No form field.

Oct 252011
 

CRM Activity Feeds are here!

CRM’s Activity Feeds provide a quick and easy way to share information using short and quick updates. Users can follow diffrent entities and see what changes occur as well as post and receive quick comments to your peers. Once you have the solution installed you can find your feed in the What’s New section.

CRM 2011 Whats New

Feed status updates can be posted manually by users or automatically based on pre-defined system rules through workflow. Activity Feeds can also be posted to by external applications through the Microsoft Dynamics CRM web services API. Activity Feeds expose Microsoft Office Lync real-time presence functionality so that users can initiate communication activities such as IM, phone calls and emails.

Activity Feeds help increase awareness and that awareness can improve productivity. Now workers can stay informed about what is important to them (and comment on it).

So now that you have them, how do you administer them? Well there are two new sections in settings, Activity Feeds Configuration and Activity Feeds Rules. You can use the activity feed configuration to add additional entities to appear on the wall. You can read the MSDN documentation on activity feeds here.

What’s cooler is the fact that there is already a Windows Phone 7.5 mobile app that takes advantage of the activity feeds.

Microsoft Dynamics CRM Mobile is a Windows Phone 7.5 app which brings Activity Feeds to your mobile device. Microsoft Dynamics CRM Activity Feeds will work with all deployment models for Microsoft Dynamics CRM including on-premise, Microsoft Dynamics CRM Online and partner-hosted.

Check out the picture below. A beautiful client.

Windows Phone 7.5 CRM Client

Oct 042011
 

Here is another web-resource html control to expand your CRM forms. It is a slider control for CRM with step control.

Below is an example of the slider control in action, and you can click here to download a solution to import the web-resources into your CRM deployment (it is a managed solution with no entities, just like the star rating control). If you want the unmanaged solution you can download it here.

Ok, once you have the web-resource loaded into your CRM deployment, you’ll need to create a new field to store the slider value and put it on the form, but uncheck the visible box.

Add the Web-Resource to the form, check the box to display the label on the form, and then enter your parameters. I like using a pipe separator. The available parameters are:

  • min: minimum value
  • max: maximum value
  • field: the CRM field to store the value in
  • step: the step increase
  • stepoverride: don’t use the step when a value is manually entered

Make sure to click on the formatting tab and set the number of rows to 1, scrolling to Never, and uncheck the box to display the border.

Then move the hidden field down and out the way. If you have a lot of fields you are accessing through form scripts you could just drop them into a hidden section.

Here is the code for the slider control HTML web resource.

<html>
<head>
  <title>Slider Control</title>
  <script type="text/javascript">
var Settings = {};
Settings.Min = 0;
Settings.Max = 100;
Settings.Step = 5;
Settings.Value = 50;
Settings.Field = null;
Settings.AllowStepOverride = false;

var Slider = {};
Slider.Drag = false;

Slider.Initialize = function Slider(id,min,max,step,value,onchange) {
  Settings.Min = min;
  Settings.Max = max;
  Settings.Step = step;
  Slider.Value = value;
  
  var ihtml = "";
  ihtml += "<table width='100%' cellspacing='0' cellpadding='0'><tr>";
  ihtml += "<td class='left-bracket'>&nbsp;</td><td class='value'>";
  ihtml += "<input class='value' id='value' type='text' onchange='Slider.SetValue();' />";
  ihtml += "</td><td class='right-bracket'>&nbsp;</td>";
  ihtml += "<td class='slider-left' /><td class='slider-area' id='"+id+"-area' >";
  ihtml += "<div id='"+id+"-selector' class='slider-selector' /></td>"
  ihtml += "<td class='slider-right' /></tr></td></table>";
  var p = document.getElementById(id);
  p.innerHTML = ihtml;

  Slider.Element = document.getElementById(id+"-area");
  Slider.Element.attachEvent("onmousedown",Slider.OnMouseClick);
 
  Slider.Id = id;
  Slider.Selector = document.getElementById(id+"-selector");
  Slider.Selector.attachEvent("onmousedown",Slider.OnMouseDown);
  Slider.Selector.attachEvent("onmousemove",Slider.OnMouseMove);
  Slider.Selector.attachEvent("onmouseup",Slider.OnMouseUp);
  Slider.Selector.attachEvent("onmouseleave",Slider.OnMouseUp);
 
  Slider.UpdatePosition(); 
}

Slider.SliderRatio = function() {
  var num =  Settings.Max - Settings.Min;
  var den = parseFloat(Slider.Element.offsetWidth)-parseFloat(Slider.Selector.offsetWidth);
  return den/num;
};

Slider.UpdatePosition = function () {
  Slider.Position = Math.round(Slider.Value*Slider.SliderRatio());
  Slider.SetPosition();
};

Slider.OnMouseClick = function(e) {
  Slider.Position = e.clientX - (8+Slider.Selector.offsetLeft-parseInt(Slider.Selector.style.left.replace("px","")));
  Slider.SetPosition();
  Slider.OnChange(Slider.Value);
}

Slider.SetPosition = function() {
  if (Slider.Position<0) Slider.Position = 0;
  var max = Slider.Element.offsetWidth-Slider.Selector.offsetWidth;
  if (Slider.Position>max) Slider.Position = max;
  
  Slider.Selector.style.left = Slider.Position + "px";

  var i = Math.round(Slider.Position/Slider.SliderRatio()) + Settings.Min;
  i = (Math.floor(i/Settings.Step))*Settings.Step;

  Slider.Value = i;
  
  var val = document.getElementById('value');
  if (val===null) return;
  val.value = i;
}

Slider.SetValue = function() {
  var val = document.getElementById('value');
  if (Settings.AllowStepOverride) {
    Slider.Value = val.value;
  } else {
    Slider.Value = Math.round(val.value/Settings.Step)*Settings.Step;
  }
  if (Slider.Value<Settings.Min) Slider.Value = Settings.Min;
  if (Slider.Value>Settings.Max) Slider.Value = Settings.Max;
  Slider.UpdatePosition(); 
}

Slider.OnMouseDown = function(e)  {
  Slider.Drag = true;
}

Slider.OnMouseMove = function(e) {
  if (Slider.Drag) {
    Slider.Position = e.clientX - (8+Slider.Selector.offsetLeft-parseInt(Slider.Selector.style.left.replace("px","")));
    Slider.SetPosition();
  }
};

Slider.OnMouseUp = function(e) {
  if (Slider.Drag) {
    Slider.Drag = false;
    Slider.OnChange(Slider.Value);
  }
};

Slider.OnChange = function(val) {
  parent.Xrm.Page.data.entity.attributes.get(Settings.Field).setValue(val);
};

var InitializeSlider = function() {
  var data = WebResource.GetDataParams();
  var sliderBox = document.getElementById("sliderBox");
  
  for (var i in data)
  {
    switch (data[i][0].toLowerCase())
	{
	  case 'min': Settings.Min = parseInt(data[i][1],10); break;
	  case 'max': Settings.Max = parseInt(data[i][1],10); break;
	  case 'step': Settings.Step = parseInt(data[i][1],10); break;
	  case 'value': Settings.Value = parseInt(data[i][1],10); break;
	  case 'field': Settings.Field = data[i][1]; break;
	  case 'stepoverride': Settings.AllowStepOverride = (data[i][1]=='true'); break;
	  default:break;
	}
  }
  
  Slider.Initialize("slider",Settings.Min,Settings.Max,Settings.Step,Settings.Value,null)
  
  if (Settings.Field!=null) {
    var val = parent.Xrm.Page.data.entity.attributes.get(Settings.Field).getValue();
	if (val!==null) {
	  var num = parseInt(val);
	  Slider.Value = num;
	  Slider.UpdatePosition();
	}
  }
};

var WebResource = {};

WebResource.GetDataParams = function()
{ //modified version of: http://technet.microsoft.com/en-us/library/gg327945.aspx
  //Get the any query string parameters and load them
  //into the vals array

  var vals = new Array();
  if (location.search !== "")
  {
    vals = location.search.substr(1).split("&");
    for (var i in vals)
    {
      vals[i] = vals[i].replace(/\+/g, " ").split("=");
    }

    //look for the parameter named 'data'
    var found = false;
        var datavals;
    for (var j in vals)
    {
      if (vals[j][0].toLowerCase() == "data")
      {
        found = true;
        datavals = decodeURIComponent(vals[j][1]).split("|");
        for (var k in datavals)
        {
          datavals[k] = datavals[k].replace(/\+/g, " ").split("=");
        }
        break;
      }
    }
    if (found) { return datavals; }
  }
  return null;
};
  </script>
  <style type="text/css">
html, body {
  padding: 0px;
  margin: 0px;
  border: 0px;
  background-color: rgb(246, 248, 250);
  overflow:hidden;
}
.slider-left, .slider-area, .slider-right {
  background-image:url(img/bar.png);
  background-repeat:repeat-x;
  height:17px;
  padding:0px;
  margin:0px;
  cursor:hand;
}
.slider-left, .slider-right {
  width: 7px;
}
div.slider-selector {
  background-image:url(img/selector.png);
  background-repeat:no-repeat;
  width:15px;
  height:17px;
  position: relative;
  cursor:hand;
}
td.left-bracket, td.right-bracket {
  background-repeat:no-repeat;
  height:17px;
  width:6px;
  display:inline;
}
td.left-bracket {
  background-image:url(img/left_bracket.png);
  
}
td.right-bracket {
  background-image:url(img/right_bracket.png);
}
td.value {
  background-image:url(img/val_bk.png);
  background-repeat:repeat-x;
  height:17px;
  width:30px;
}
input.value {
  border: none;
  font:11px segoe ui,tahoma,arial;
  height:17px;
  width:30px;
  background-color:transparent;
  vertical-align:top;
  padding-top:1px;
  text-align:center;
}
  </style>
</head>
<body onload="InitializeSlider();">
<div id="slider"></div>
</body>
</html>

Thanks to Manish Mistry comment on my star control for refreshing the iframe when you change the value, you could do the same on this control. However, you could also just set the value and update the position. The only reason I get the control’s object and it’s id is because I’m not 100% sure the id will always match, even though all my tests indicate it does.

var control = Xrm.Page.ui.controls.get('WebResource_Score');
var id      = control.getObject().id;
var frame   = document.frames[id];
frame.Slider.Value = 50;
frame.Slider.UpdatePosition();

One of my TODO’s is to join my web resources HTML form controls into a single library once I have enough of them so that people can leverage them in an easier manner. If anyone has any ideas for custom controls for Dynamics CRM, let me know and I’ll see if I can build it.

Sep 152011
 

In a previous post I wrote about a control I posted to codeplex that I used in my demos, a CRM Attachment Image Control.

It stored the images in the notes (still does – and currently does not have an option for storing elsewhere…). I still have not figured out how to put buttons on the ribbon that can fire the upload control, etc. Although, I recently found out that Marco Amoedo (blog: marcoamoedo.com, twitter: marcoamoedo) had leveraged the ImageTools for Silverlight to add WebCam support and I was already starting to play with it to add Gif support. I was able to work with him to get his modifications and add gif support. Now you have even more useful features that you can enable with a simple configuration change and a new silverlight web-resource.

Now, if you look in the bottom right, there is a webcam button next to the upload button that allows you to take a picture instead of uploading one. When you try and take the picture you will receive the following request for access.

So you may be asking: what is ImageTools for Silverlight? where did it come from?, and why are you using it? The answers are simple. According to the codeplex page for the ImageTools for Silverlight, it is "a library, which provides additional functionality for loading, saving and [manipulating] images from different sources and with different formats.&quot The reason to use it is simple, it is open-source, and it’s license allows me to use it (it is realeased under Ms-PL – the same license as my control on codeplex).

If you already use the CRM Attachment Image, you can just overwrite your xap file with the one from codeplex, but you will still need to edit the properties of the web-resource on your form to include |webcam=true. The new example syntax is:

field=new_imageattachmentid|subject='Contact''s Image'|prefix='img-'|webcam=true

So a special thanks to Marco for his help, this is a great change that I think a lot of people will be able to use.

If you need assistance in setting up the image control, I have it documented on codeplex, but here is a simple set of directions that work at the time of this article bieng posted:

  1. Create a solution
  2. Upload the xap as a web-resource
  3. Add your entity to the solution, and create a new text field on the entity that is 50 characters long, not searchable, preferably called something like attachmentid or savedattachmentid – it will be used to store the annotationid or the note that stores the attached image.
  4. Add the field to the form and make it not visible.
  5. Add the Web-Resource to the form
    • Check the checkbox to pass the id and type, etc.
    • In the parameters add the fields and their values seperated by a pipe |
  6. Save and publish the form.
  7. Test it!

The example config for the web-resource is before the instructions, and are very useful because you can just copy them and change the field new_imageattachmentid to your field name and it should work. There are a number of fields available to you if you want to change how the control functions, they are documented on codeplex, and they are:

  • field – The name of the field you are using to store the annotationid of the attachment. It is required, and the field MUST BE ON THE FORM (it does not need to be visible).
  • subject – The subject for attachments – the following substitutions are available (%s will be replaced with the name of the file )
  • prefix – The prefix for all attachment images (this is used to filter the results, etc)
  • hidebuttons – Used to hide the buttons that are in the silverlight control so that you can use the ribbon to control the application
  • savefirst – Used to set the message that is displayed on a form that is in Create Mode
  • webcam – Set to true if you want to enable the webcam it is not currently tide to hidebuttons

I added the following parameters at Bill’s request (Thanks Bill):

  • max-width: maximum width for all images
  • max-height: maximum height for all images
  • max-webcam-width: maximum webcam image width, overrides max-width for web-cam images
  • max-webcam-height: maximum webcam image height, overrides max-height for web-cam images
May 162011
 

Just as an example to help people understand how you can leverage the SoundsLikePlugin, here is how to set it up for Account Name using Metaphone. You should pay attention to your config xml, which I posted in a previous article: SoundsLikePlugin: Soundex & Metaphone Plugin for CRM Online. You can leverage the plugin and duplication detection on other entities. You just need to adjust the configuration and steps accordingly for another entity and fields.

Create a Solution

  1. Create a Solution
  2. Add the Account as an existing entity
  3. Add two new fields, new_SoundexName and new_MetaphoneName and put them on the Account form as read-only
  4. Save and Publish all changes

Register the plugin assembly

The first step is to register the plugin assembly, to do that you will need to use the Plugin Registration Tool, which is included in the sdk (uncompiled).

Compiling the Plugin Registration Tool

  1. Open the solution, pluginregistration.sln in tools\pluginregistration folder inside the SDK with Visual Studio 2010
  2. Compile (Build) the solution
  3. Navigate to the tools\pluginregistration\bin\Debug folder in the SDK.
  4. Launch the Plugin Registration Tool by double-clicking the PluginRegistration.exe

Connecting to CRM Online or OnPremise

  1. Enter the Label, “CRM Online” for CRM Online for example
  2. Enter the discovery url. For CRM Online the discovery url is https://dev.crm.dynamics.com
    • If you have any problems authenticating, try deleting the %userprofile%\LiveDeviceID\LiveDevice.xml

  3. Select the Organization in the Connections in the left window pane

Registering the Assembly

  1. Click the Register button, then click Register New Assembly

  2. Click the … button in Step #1 and select the SoundsLikePlugin.dll file.

  3. Click Register Selected Plugins, then click Ok on the window that says they were successfully registered.

Register the Steps

  1. Select the Assembly, then click Register, then Register New Step

  2. Enter Create for the Message
  3. Enter account for the Primary Entity
  4. Enter your configuration xml
  5. Select Pre-Validation
  6. (Optional) Check the box for Offline Deployment
  7. Register the Step

  8. Select the Assembly again, then click Register, then Register New Step
  9. Enter Update for the Message
  10. Enter account for the Primary Entity
  11. Enter your configuration xml
  12. Click the three dots (…) for the filtering attributes field, make sure to check the checkbox for all of the source and target fields you specify in your configuration
  13. Select Pre-validation
  14. (Optional) Check the box for Offline Deployment
  15. Register the Step

Setup the Duplicate Detection Rule

  1. Click Settings, then Data Management, then Duplicate Detection Rules

  2. Click New to create a new duplicate detection rule
  3. Enter the Name: Account Name sounds like another account name
  4. Select the Base Record Type: Account
  5. Select the Field: Metaphone Name and the Criteria: Exact Match

  6. Save and Publish the rule

Create a Duplicate Detection Job

  1. Click Settings, then Data Management, then Duplicate Detection Jobs

  2. Click New to create a new Duplicate Detection Job
  3. Set it up according to your requirements
  4. Click Next
  5. Click Submit

Run the Duplicate Detection Job and View Duplicates

When the job runs, you can view the duplicates by going back to the Job and clicking View Duplicates