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.

Nov 212011
 

Everybody loves to use InfoPath forms. And let’s face it, we all wish we could just take them and put them into CRM. What I’ve written is a simple way to do that without the hassle of figuring out how to bind the InfoPath form schema to a custom WCF service. It still doesn’t do everything I want it to, but it works, so I figured I’d go ahead and put it out there for everyone to look at, play with, and even use.

If you are interested in this solution, you can download the following:

  • Managed Solution (this is the only thing you need)
  • Unmanaged Solution (in case you want to modify mine, like add grid support, it is on my to-do list)
  • Source Code (this is in case you would prefer to use a WCF service or if you just want to play around with my code)

Once you’ve installed the managed solution, you’ll need to do the following to create / setup an InfoPath form:

  1. For this particular InfoPath integration, you need to create InfoPath Forms that mirror a CRM Entity.
    Note: You can always create a new entity for the form data.

  2. Rename the default form Group to the name of the entity (ex: Contact)

  3. Inside CRM create the InfoPath form (it is an entity included in the solution), select the entity and save the form.

  4. Once it has been saved, you can copy the ID using the “Copy ID” link in the header.

    Note: If you do not select the Allow Any CRM Field checkbox, you will need to add the individual Fields, which can be found in the left navigation.

  5. In the InfoPath Form Designer, add a field named SubmissionCode with the Default Value as the ID you just copied.

    Note: Using the submission code allows us to check and see if the form is valid, active, and allowed.

  6. Add a field for each of the fields in CRM that you want to be available, selecting the appropriate data type.

    Note: Unfortunately I have not had the time to test the fields and have been using the checkbox on the infopath form record to enable any/all CRM fields. However, I did not want to delay releasing the code and information as it currently is. If someone identifies a problem with this or any other section of the code, I’ll be happy to look into it and work on resolving the issue.

  7. Layout the fields on the form, then click File > Submit Options > To Email

  8. Enter the Queue email or where you want new forms to be submitted.

  9. Click Next.

  10. Click Finish.

  11. Save and close the form designer.
  12. Open the form and test submission.

Once the email comes into CRM, the process of reading the form is easy; you just open it up and click the Import InfoPath button on the ribbon.

Usually I would post the source-code and comment on how it works in great detail, but this time, it really depends on which component. There is a plugin and WCF service written in C# as well as two Jscript files, one for the buttons and one for the InfoPath form inside CRM that creates a drop-down of the entities in CRM. I would suggest downloading the unmanaged solution and source-code if you want to delve into those items.

Now, for complex situations or form conversions, binding the InfoPath schema is the way to go. Using that method (the one described in detail on Philo’s Weblog) you can import the InfoPath form schema into Visual Studio:

  1. Create an InfoPath form and lay it out the way you want it.
  2. Extract the form files (File | Extract Form Files) to a location you can find later.
  3. Close the InfoPath form designer.
  4. Open the Visual Studio command prompt.
  5. Change to the directory where you extracted the files
    cd c:\myinfopathfiles
  6. Run xsd against the myschema.xsd in that directory, and specify the namespace you would like to use.
    Note: My WCF namespace is CrmInfoPathService and my plugin namespace is InfoPathPlugin.
    xsd.exe myschema.xsd /classes /l:cs /n:InfoPathPlugin
  7. Then you can deserialize the xml into the object and store it in CRM or any other system using the logic embedded in your code.

The key point to my solution was not to do something that everyone does on occasion, but to make something so that I can leverage it in the future and not have to create a new integration every time I wanted to leverage a new form. Now, on that note, here are the list of things that if you delve into, I’d like to know, a.k.a. my to-do list for this project:

  • Add a button onto the grid for activities
  • Create a way to handle child entities.
  • Modify the plugin/WCF code to link the created entities to the form email using connections.
  • Change the button on the form to monitor for the results (success/failure) and report back to the user.
  • Any great ideas my readers think should be added!
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 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 202011
 

Update:Here is the most up to date version for UR12. It fixes some style-sheet issues that caused the stars not to display.

To expand on the technique used for my previous post on making the state field a drop down here is a star rating field control so that you can get a nice visual star rating control that is configurable.

Below is an example of the star rating 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).

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

Add the Web-Resource to the form, and check the box to display the label on the form.

Then enter your parameters (I like using a pipe as a separator). The available parameters are:

  • min: minimum star rating – really should always be 1
  • max: maximum star rating – maybe 5 or 10
  • field: the CRM field to store the value in
  • showvalue: whether or not to display the numerical value

Make sure to click on the formatting tab and set the number of rows to 1, scrolling to Never, and check 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.

Now that you know how to set it up, let’s go ahead and look at the code.

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Rating Control</title>
  <link href="/_common/styles/fonts.css.aspx?lcid=1033" rel="stylesheet" type="text/css" />
  <link href="/_common/styles/global.css.aspx?lcid=1033" rel="stylesheet" type="text/css" />
  <link href="/_common/styles/select.css.aspx?lcid=1033" rel="stylesheet" type="text/css" />
  <script type="text/javascript">
var Rating = {};

// Default Values
Rating.ShowValue = false;
Rating.Max = 5;
Rating.Min = 1;
Rating.Field = '';
Rating.Value = null;

Rating.Highlight = function(index)
{
  var val = document.getElementById('rating_value');
  if (index===null) { 
    index = -1; 
    val.innerHTML = '[Not Set]';
  } else {
    val.innerHTML = '['+index+'/'+Rating.Max+']';
  }
  for(var i=Rating.Min; i<=Rating.Max;i++)
  {
    if (i<=index) {
      document.getElementById("rating_"+i).className = "staron";
    } else {
      document.getElementById("rating_"+i).className = "staroff";
    }
  }
};

Rating.HolderRollover = function() {
  if (Rating.Value !== null) {
    var r = document.getElementById('rating_remove');
    r.className = 'stardel';
  }
};

Rating.HolderRollout = function() {
  var r = document.getElementById('rating_remove');
  r.className = 'stardel2';
};

Rating.Rollover = function(index) {
  Rating.Highlight(index);
};

Rating.Rollout = function()
  {
  Rating.Highlight(Rating.Value);
};

Rating.Set = function(index) {
  Rating.Value = index;
  if (index<0) {
    parent.Xrm.Page.data.entity.attributes.get(Rating.Field).setValue(null);
  } else {
    parent.Xrm.Page.data.entity.attributes.get(Rating.Field).setValue(Rating.Value);
  }
  Rating.Highlight(index);
};

Rating.Initialize = function() {
  var data = WebResource.GetDataParams();
  for (var i in data)
  {
    switch (data[i][0].toLowerCase())
        {
          case 'min': Rating.Min = parseInt(data[i][1],10); break;
          case 'max': Rating.Max = parseInt(data[i][1],10); break;
          case 'field': Rating.Field = data[i][1]; break;
          case 'showvalue': Rating.ShowValue = (data[i][1] == 'true'); break;
          default:break;
        }
  }
  var d = document.getElementById('starholder');
  for (var j=Rating.Min;j<=Rating.Max;j++)
  {
    var el = "<span id='rating_"+j+"' " +
             "onmouseover='Rating.Rollover("+j+");' " +
             "onmouseout='Rating.Rollout();' " +
             "onclick='Rating.Set("+j+");' />";
    d.innerHTML += el;
  }

  var rel = "<span id='rating_remove' class='stardel2' " + 
            "onclick='Rating.Set(null);' onmouseover='Rating.Rollover(null);' />";
  d.innerHTML += rel;

  var val = "<span id='rating_value' class='value' " + 
            (Rating.ShowValue ? "" : "style='display:none' ") + 
            "/>";
  d.innerHTML += val; 
   
  Rating.Value = parent.Xrm.Page.data.entity.attributes.get(Rating.Field).getValue();
  Rating.Highlight(Rating.Value);
};

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">
.staroff,.staron,.stardel,.stardel2 {
	height:15px;
	margin:1px 1px 1px 2px;
	width:15px;
	display:inline-block;
}

staroff,.staron,.stardel {
	background:no-repeat
}

.starholder {
	background:#FFF;
	padding:1px
}

.stardel {
	background:url(remove.png)
}

.staron {
	background:url(staron.png)
}

.staron,.staroff {
	margin-right:5px
}

.staroff {
	background:url(staroff.png)
}

.value {
	font:11px segoe ui,tahoma,arial;
	color:#000;
	height:17px;
	vertical-align:middle
}
  </style>
</head>

<body onload="Rating.Initialize();">
  <table cellspacing="0"
         cellpadding="0"
         width="100%"
         summary="star table">
    <tr>
      <td id="cell_dd">
        <div id="starholder"
             style="width:100%;"
             onmouseover="Rating.HolderRollover();"
             onmouseout="Rating.HolderRollout();"></div>
      </td>
    </tr>
  </table>
</body>
</html>
These are the image files used: (they are hidden in the managed solution)
Sep 142011
 

Do you have issues receiving request or information in various languages and need it translated to English (or another language)? I found the need to translate something from any language to english just so when someone submits a case or puts some notes in another language it can quickly be translated.

Here is what my bing translator integration looks like.

You can download an unmanaged solution to implement the bing translator for cases, letters, tasks, and emails, you can download it here: Translate_1_0.zip. It will not modify your forms, but if you have unmanaged ribbon buttons on those entities, it will remove them. So to avoid that problem you can install the unmanaged solution with the JScript (that you need to edit to set your bing api key) and the images, and then follow the RibbonDiff guide in the SDK to add the buttons, or install the managed solution to add the button to the ribbons for cases, letters, tasks, and emails.

To start, I created a JScript file to include as a web-resource that I could leverage when a button is clicked on the ribbon. This JScript file utilizes the Bing Translation WebServices to translate text.

YOU WILL NEED TO EDIT THE JSCRIPT FILE AND CHANGE ‘YOUR API KEY GOES HERE’ TO CONTAIN YOUR API KEY.

var bing = {};
bing.popup = {};
bing.language = {};

bing.appId = 'YOUR API KEY GOES HERE';
bing.language.text = '';
bing.language.from = '';
bing.language.to = 'en';

bing.language.detect = function (text,oncomplete)
{
  var src = "https://api.microsofttranslator.com/V2/Ajax.svc/Detect?oncomplete=" + 
      oncomplete + "&appId="+ bing.appId + "&text=" + text;
  var s = document.createElement("script");
  s.src = src;
  s.id = "bingdetect";
  document.getElementsByTagName("head")[0].appendChild(s);
}

bing.language.translate = function(from,to,text,oncomplete)
{
  var src = "https://api.microsofttranslator.com/V2/Ajax.svc/Translate?oncomplete=" + 
      oncomplete + "&appId=" + bing.appId + "&from=" + from + 
      "&to=" + to + "&text=" + text;

  var s = document.createElement("script");
  s.src = src;
  s.id = "bingtranslate";
  document.getElementsByTagName("head")[0].appendChild(s);
}

bing.popup.show = function(text)
{
  var idiv = document.createElement('div');
  idiv.id = "bingtext";
  idiv.style.height = "265px";
  idiv.style.width = "306px";
  idiv.style.align = "left";
  idiv.style.textAlign = "left";
  idiv.style.backgroundColor = "white";
  idiv.style.overflow = "auto";
  idiv.style.padding = "2px";
  idiv.style.margin = "2px";
  idiv.style.border = "1px solid black";
  idiv.innerHTML =  "<p>"+text+"</p>";
    
  var iurl = Xrm.Page.context.getServerUrl();
  iurl += "WebResources/tran_img/translatelogo.gif";
  
  var img = document.createElement('img');
  img.src = iurl;
  
  var div = document.createElement('div');
  div.id = "bingpopup";
  div.appendChild(img);
  div.appendChild(idiv);
  div.innerHTML += "<button style='text-align:center;width:60px;' onclick='bing.popup.copy();'>Copy</button>&nbsp;&nbsp;";
  div.innerHTML += "<button style='text-align:center;width:60px;' onclick='bing.popup.hide();'>Close</button>";
  div.style.position = "absolute";
  div.style.backgroundColor = "#20415F";
  div.style.border="1px solid black";
  div.style.left = "50%";
  div.style.top = "50%";
  div.style.color = "black";
  div.style.padding = "5px";
  div.style.width = "320px";
  div.style.height = "340px";
  div.style.marginLeft = "-160px"; 
  div.style.marginTop = "-170px";
  div.setAttribute("align","center");
  document.getElementsByTagName("body")[0].appendChild(div);
}

bing.popup.hide = function()
{
  var deleteElement = function(id) {
    var el = document.getElementById(id);
    el.parentNode.removeChild(el);
  }

  deleteElement("bingpopup");
  deleteElement("bingdetect");
  deleteElement("bingtranslate");
}

bing.popup.copy = function()
{
  var el = document.getElementById('bingtext');
  
  if (window.clipboardData && window.clipboardData.setData)
  {
    window.clipboardData.setData("Text", el.innerText);
  }
}

bing.getSelectedText = function (p)
{
  if (p.getSelection)
  {
    text = p.getSelection();
  }
  else if (p.selection)
  {
    text = p.selection.createRange().text;
  }
  return text;
}

// get current control
bing.language.toolbarClick = function (ctrl)
{
  var text = '';
  if (window.getSelection)
  {
    text = window.getSelection();
  }
  else {
    text = bing.getSelectedText(document);
  }

  if (text==null || text=='') {
    if (ctrl==null) {
      ctrl = Xrm.Page.ui.getCurrentControl();
    }

    var entityName = Xrm.Page.data.entity.getEntityName();
    if (ctrl==null && entityName.match(/letter|email|task/)) {
      ctrl = Xrm.Page.ui.controls.get('description');
    }

    if (ctrl==null) {
      alert('Please highlight the text to translate.');
      return;
    }
  
    if (ctrl.getControlType()!="standard") {
      return; // only standard
    }
  
    if (ctrl.getName()=="notescontrol") {
      text = bing.getSelectedText(document.frames['notescontrol'].document);
      if (text==null || text == "") {
        alert('Please highlight the text to translate.');
      }
    }
  
    if (text == null || text == "") {
      var attr = ctrl.getAttribute();
      if (attr!=null) {
        var t = attr.getAttributeType();
        if (t.match(/string|memo/))
        {
          text = attr.getValue();
        }
      }
    }
  }
  
  if (text == null || text == "") {
    // Nothing to translate
    return; 
  }  
  
  text = encodeURIComponent(text);
  bing.language.text = text;

  bing.language.from = '';
  bing.language.detect(text,"translate");
}

window.translate = function (response)
{
  bing.language.from = response;
  bing.language.translate(bing.language.from,bing.language.to,bing.language.text,"translated");
}

window.translated = function (response)
{
  bing.popup.show(response);
}

The JScript uses the Bing Translate logo image (WebResources/tran_img/translatelogo.gif – you may need to change the path in the JScript):

I added some images for Microsoft Translate button images:


16×16 image
tran_icontranslate_16x16.png

32×32 image
tran_icontranslate_32x32.png

Then I added the buttons to the ribbons on the entities I wanted to have bing translation capabilities by following the SDK guidelines to add a button. Here is the RibbonDiffXml for a case. You can follow the guide to change it to be on different entities.

<RibbonDiffXml>
  <CustomActions>
    <CustomAction Id="bing.incident.form.Bing.CustomAction" 
                  Location="Mscrm.Form.incident.MainTab.Groups._children" 
                  Sequence="110">
    <CommandUIDefinition>
      <Group Id="bing.incident.form.Bing.Group" 
             Command="bing.incident.form.Bing.Command" 
             Title="$LocLabels:bing.incident.Bing.Title" 
             Sequence="39" 
             Template="Mscrm.Templates.Flexible2" 
             Image32by32Popup="$webresource:tran_icon/translate_32x32.png">
      <Controls Id="bing.incident.form.Bing.Controls">
        <Button Id="bing.incident.form.Bing.Button.BingTranslator" 
                Command="bing.incident.Bing.Button.BingTranslator.Command" 
                Sequence="10" 
                LabelText="$LocLabels:bing.incident.Bing.Button.BingTranslator.LabelText" 
                ToolTipTitle="$LocLabels:bing.incident.Bing.Button.BingTranslator.LabelText" 
                ToolTipDescription="$LocLabels:bing.incident.Bing.Button.BingTranslator.Description" 
                TemplateAlias="isv" 
                Image16by16="$webresource:tran_icon/translate_16x16.png" 
                Image32by32="$webresource:tran_icon/translate_32x32.png" />
      </Controls>
      </Group>
    </CommandUIDefinition>
    </CustomAction>
    <CustomAction Id="bing.incident.form.Bing.Popup.CustomAction" 
                  Location="Mscrm.Form.incident.MainTab.Scaling._children" 
                  Sequence="140">
    <CommandUIDefinition>
      <Scale Id="bing.incident.form.Bing.Popup.1" 
             GroupId="bing.incident.form.Bing.Group" 
             Sequence="85" 
             Size="Popup" />
    </CommandUIDefinition>
    </CustomAction>
    <CustomAction Id="bing.incident.form.Bing.MaxSize.CustomAction" 
                  Location="Mscrm.Form.incident.MainTab.Scaling._children" 
                  Sequence="120">
    <CommandUIDefinition>
      <MaxSize Id="bing.incident.form.Bing.MaxSize" 
               GroupId="bing.incident.form.Bing.Group" 
               Sequence="21" 
               Size="LargeLarge" />
    </CommandUIDefinition>
    </CustomAction>
  </CustomActions>
  <Templates>
    <RibbonTemplates Id="Mscrm.Templates"></RibbonTemplates>
  </Templates>
  <CommandDefinitions>
    <CommandDefinition Id="bing.incident.Bing.Button.BingTranslator.Command">
    <EnableRules />
    <DisplayRules />
    <Actions>
      <JavaScriptFunction Library="$webresource:tran_js/bing.translate.js" 
                          FunctionName="bing.language.toolbarClick">
      <CrmParameter Value="SelectedControl" />
      </JavaScriptFunction>
    </Actions>
    </CommandDefinition>
    <CommandDefinition Id="bing.incident.form.Bing.Command">
    <EnableRules></EnableRules>
    <DisplayRules />
    <Actions />
    </CommandDefinition>
  </CommandDefinitions>
  <RuleDefinitions>
    <TabDisplayRules />
    <DisplayRules />
    <EnableRules />
  </RuleDefinitions>
  <LocLabels>
    <LocLabel Id="bing.incident.Bing.Title">
    <Titles>
      <Title languagecode="1033" 
             description="Bing" />
    </Titles>
    </LocLabel>
    <LocLabel Id="bing.incident.Bing.Button.BingTranslator.LabelText">
    <Titles>
      <Title languagecode="1033" 
             description="Translate" />
    </Titles>
    </LocLabel>
    <LocLabel Id="bing.incident.Bing.Button.BingTranslator.Description">
    <Titles>
      <Title languagecode="1033" 
             description="Translate text area with bing!" />
    </Titles>
    </LocLabel>
  </LocLabels>
</RibbonDiffXml>

Here are the files to download if you want to use it:

May 112011
 

Do you want a drop down box for the state, but you don’t want to break core CRM functionality?

In CRM 4.0 I had posted an article about changing the CRM Address State to a drop down box. While I had gone back and updated the code to work with CRM 2011, some people still had problems using it. Also, it wasn’t the best way to do it in CRM 2011. With the web-resources in CRM 2011 there is a completely supported way of creating a state drop down!

You can use an html web-resource! Below is the source for the html web resource, then you just need to hide the state drop down on your form and add the web resource and set the data parameter to the field id, such as address1_stateorprovince.

This helps by providing a set of default option in a drop-down box. It also supports (** Other) which will show a text box for entering something not in the drop down. This web-resource works on any entity where you need a state drop down.

Here is the configuration for the HTML Web Resource on the Contact form:

State Drop Down Web Resource Properties

State Drop Down Web Resource Properties

StateDropDown.html:

<html>
<head>
<title>State Drop Down</title>
<link href="/_common/styles/fonts.css.aspx?lcid=1033" rel="stylesheet" type="text/css"/>
<link href="/_common/styles/global.css.aspx?lcid=1033" rel="stylesheet" type="text/css"/>
<link href="/_common/styles/select.css.aspx?lcid=1033" rel="stylesheet" type="text/css"/>
<script type="text/javascript">
var datafield;
function getQuerystring(key)
{
    var work = key.replace(/[\[]/,"\\\[").replace(/[\]]/,"\\\]");
    var regex = new RegExp("[\\?&]"+work+"=([^&#]*)");
    var qs = regex.exec(window.location.href);
    if (qs == null) return null;
    return qs[1];
}

function onLoad()
{
    var updatefield = getQuerystring("data");
    if (top.location.href.indexOf("/print/print.aspx") != -1)
    {
        // While this part of the onload is unsupported, there is not another way to do it
        // because Xrm.Page.data.entity is not supported in print-preview
        var data_cell = document.getElementById("cell_dd");
        var crmField = top.frames[0].document.getElementById(updatefield+"_d");
        data_cell.innerHTML = crmField.innerHTML;
        return;
    }
    datafield = parent.Xrm.Page.data.entity.attributes.get(updatefield);
    var value = datafield.getValue();
    var dropdown = document.getElementById("custom_state");

    // fix for outlook issue identified by Ryan
    // do not pass null value to IsOptionInDropDown
    if (value==null || value=="") {
        dropdown.value = "!1"; // Set to select a state by default
        return; // stop processing - empty value
    }

    // If it is a dropdown value, set the dropdown and exit onload
    if (IsOptionInDropDown(value)) return;

    // no value found for drop down, value is not null or empty, therefor set other
    dropdown.value = "other";
    var other = document.getElementById("other_state");
    other.value = value;
    SetOtherVisible(true);
}

function IsOptionInDropDown(value)
{
    // Replace fixes for common name variants
    switch (value) {
        case "d.c.": value="dc";
            break;
        default:
            break;
    }

    // Change to lowercase    
    var lvalue=value.toLowerCase();
    
    var dropdown = document.getElementById("custom_state");
    var options = dropdown.options;
    var i = 0;
    for(i=0;i<options .length;i++)
    {
        if (options[i].value.toLowerCase() == lvalue || options[i].text.toLowerCase() == lvalue)
        {
            dropdown.value = options[i].value;
            SetOtherVisible(false);
            return true;
        }
    }
    return false;
}

function DropDownChanged(value)
{
    if (value !== null && value!=="") {
        if (value.charAt(0)=='!') { // Fixed By Joseph Martine - charAt or substring not [0]
            StateChanged("");// Fixed By Joseph Martine
            return;
        }
        if (value=="other") {
            SetOtherVisible(true);
            return;
        } else {
            SetOtherVisible(false); // Fixed By Joseph Martine
        }
        var os = document.getElementById("other_state");
        os.value = value;
        StateChanged(value);
    }
}

function TextBoxChanged(value)
{
    StateChanged(value);
    IsOptionInDropDown(value); // This also updates the visible in its check...
    return true;
}

function StateChanged(value)
{
    // Update Xrm.Page data field
    datafield.setValue(value);
}

function SetOtherVisible(visibility)
{
    var ddcell = document.getElementById("cell_dd");
    var textcell = document.getElementById("cell_tb");
    if (visibility) {
        ddcell.style.width="50%";
        textcell.style.display="table-cell";
        textcell.style.width="50%";
    } else {
        textcell.style.display="none";
        ddcell.style.width="100%";
        textcell.style.width="100%";
    }
}

</script>
</head>
<body onload="onLoad();">
<table cellspacing="0" cellpadding="0" width="100%"><tr><td id="cell_dd">
<select name="custom_state" req="2" height="4" style="IME-MODE: auto;width:100%;" 
class="ms-crm-SelectBox" id="custom_state" onChange="DropDownChanged(this.value)">
    <option value="!1">---Select a State-------</option>
    <option value="!2">---Canada---------------</option>
    <option value="AB">Alberta</option>
    <option value="BC">British Columbia</option>
    <option value="MB">Manitoba</option>
    <option value="NB">New Brunswick</option>
    <option value="NL">Newfoundland and Labrador</option>
    <option value="NT">Northwest Territories</option>
    <option value="NS">Nova Scotia</option>
    <option value="NU">Nunavut</option>
    <option value="ON">Ontario</option>
    <option value="PE">Prince Edward Island</option>
    <option value="QC">Quebec</option>
    <option value="SK">Saskatchewan</option>
    <option value="YT">Yukon</option>
  <option value="!3">---United States--------</option>
    <option value="AL">Alabama</option>
    <option value="AK">Alaska</option>
    <option value="AZ">Arizona</option>
    <option value="AR">Arkansas</option>
    <option value="CA">California</option>
    <option value="CO">Colorado</option>
    <option value="CT">Connecticut</option>
    <option value="DC">District of Columbia</option>
    <option value="DE">Delaware</option>
    <option value="FL">Florida</option>
    <option value="GA">Georgia</option>
    <option value="HI">Hawaii</option>
    <option value="ID">Idaho</option>
    <option value="IL">Illinois</option>
    <option value="IN">Indiana</option>
    <option value="IA">Iowa</option>
    <option value="KS">Kansas</option>
    <option value="KY">Kentucky</option>
    <option value="LA">Louisiana</option>
    <option value="ME">Maine</option>
    <option value="MD">Maryland</option>
    <option value="MA">Massachusetts</option>
    <option value="MI">Michigan</option>
    <option value="MN">Minnesota</option>
    <option value="MS">Mississippi</option>
    <option value="MO">Missouri</option>
    <option value="MT">Montana</option>
    <option value="NE">Nebraska</option>
    <option value="NV">Nevada</option>
    <option value="NH">New Hampshire</option>
    <option value="NJ">New Jersey</option>
    <option value="NM">New Mexico</option>
    <option value="NY">New York</option>
    <option value="NC">North Carolina</option>
    <option value="ND">North Dakota</option>
    <option value="OH">Ohio</option>
    <option value="OK">Oklahoma</option>
    <option value="OR">Oregon</option>
    <option value="PA">Pennsylvania</option>
    <option value="RI">Rhode Island</option>
    <option value="SC">South Carolina</option>
    <option value="SD">South Dakota</option>
    <option value="TN">Tennessee</option>
    <option value="TX">Texas</option>
    <option value="UT">Utah</option>
    <option value="VT">Vermont</option>
    <option value="VA">Virginia</option>
    <option value="WA">Washington</option>
    <option value="WV">West Virginia</option>
    <option value="WI">Wisconsin</option>
    <option value="WY">Wyoming</option>
    <option value="other">** Other</option>
</select>
</td><td id="cell_tb" style="padding-left:15px;display:none;">
<input class="ms-crm-Text" style="ime-mode: auto;width:100%;" type="text" 
maxLength="50" id="other_state" req="0" onChange="TextBoxChanged(this.value);" />
</td></tr>
</table>
</body>
</html>