Wednesday, January 14, 2009

Copy and Paste CRM Picklist Values - Part III

Following on from Part II which went over setting up a Visual Studio project to customize the out-of-the-box edit attribute form in Microsoft Dynamics CRM 4.0, here's the last installment in this series - the code you'll need to make it work.

Open the editAll.aspx file you created in Step II and paste in the following markup:

(By the way if the carriage returns don't paste when you copy this into Visual Studio, copy and paste into and out of WordPad or MS Word first).

<%@ Assembly Name="HubKey.Crm.Web.Tools.SystemCustomization.Attributes, Version=, Culture=neutral, PublicKeyToken=PUBLICKEYGOESHERE" %>
<%@ Page language="c#" Inherits="HubKey.Crm.Web.Tools.SystemCustomization.Attributes.EditAllPage"    %>
<%@ Register TagPrefix="frm" Namespace="Microsoft.Crm.Application.Forms" Assembly="Microsoft.Crm.Application.Components.Application" %>
<%@ Register TagPrefix="cnt" Namespace="Microsoft.Crm.Application.Controls" Assembly="Microsoft.Crm.Application.Components.Application" %>
<%@ Register TagPrefix="ui" Namespace="Microsoft.Crm.Application.Components.UI" Assembly="Microsoft.Crm.Application.Components.UI" %>
<%@ Register TagPrefix="loc" Namespace="Microsoft.Crm.Application.Controls.Localization" Assembly="Microsoft.Crm.Application.Components.Application" %>
    <cnt:AppHeader runat="server" id="crmHeader"/>
<frm:DialogForm id="crmForm" runat="server">
    <table cellpadding="0" cellspacing="5" width="100%" style="table-layout: fixed;">
        <col width="100"><col>
                <td class="ms-crm-Field-Required"><label for="txtLabel"><%=HubKey.Crm.Web.Tools.SystemCustomization.Attributes.Globals.LOCID_EDIT_ALL_VALUE_LABEL_PAIRS%><img src="/_imgs/frm_required.gif" alt="<loc:Text Encoding='HtmlAttribute' ResourceId='Forms.Required.AltTag' runat='server'/>"/></label></td>
                <td><ui:TextArea id="txtLabel" Height="270px" runat="server"/></td>

You'll also need to make a change to the manageAttribute.aspx page. At the top of the page, add the following markup to register an "hk" tag prefix:

<%@ Register TagPrefix="hk" Namespace="HubKey.Crm.Web.Tools.SystemCustomization.Attributes" Assembly="HubKey.Crm.Web.Tools.SystemCustomization.Attributes, Version=, Culture=neutral, PublicKeyToken=PUBLICKEYGOESHERE" %>

Next, search for the following element id: "ledtPicklistValues". Insert a new table row / table data cell above the tr container for this element and paste in a new element:

<hk:AppListEditAll id="appListEditAll" runat="server"/>

The enclosing table markup should now look like the following:

<table width="100%" cellspacing="0" cellpadding="0">
<tr class="param">
<td><hk:AppListEditAll id="appListEditAll" runat="server"/></td>
<tr class="param">
<td><app:AppListEdit id="ledtPicklistValues" runat="server"/></td>

The code in the EditAll.cs file is as follows:

using System;
using System.Web.UI;
using System.IO;
using System.Globalization;
using Microsoft.Crm;
using Microsoft.Crm.Application.Controls;
using Microsoft.Crm.Application.Forms;
using Microsoft.Crm.Application.Components.UI;
namespace HubKey.Crm.Web.Tools.SystemCustomization.Attributes
    public static class Globals
        public static readonly string LOCID_EDIT_ALL_BUTTON_TEXT;
        public static readonly string LOCID_EDIT_ALL_DIALOG_TITLE;
        public static readonly string LOCID_EDIT_ALL_DIALOG_DESC;
        public static readonly string LOCID_EDIT_ALL_VALUE_LABEL_PAIRS;
        public static readonly string LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED;
        public static readonly string EDIT_ALL_JS_PATH;
        public static readonly string LOCID_NUMBER_RANGE_VALUE_MASK;
        public static readonly string LOCID_VALUE_NOT_UNIQUE;
        public static readonly char DEFAULT_ITEM_CHAR;
        static Globals()
            //TODO: Localize as needed.
            DEFAULT_ITEM_CHAR = '*';
            LOCID_EDIT_ALL_BUTTON_TEXT = "Edit All";
            LOCID_EDIT_ALL_VALUE_LABEL_PAIRS = "Value / Label Pairs";
            LOCID_EDIT_ALL_DIALOG_TITLE = "Modify List Values";
            LOCID_EDIT_ALL_DIALOG_DESC = "Modify all the values and labels in this picklist. Place each value / label pair on a new line separated by a carriage return. Separate values and labels with a tab. ";
            LOCID_EDIT_ALL_DIALOG_DESC += "If you don't include a value on each line (just the label) a value will be automatically created and incremented. ";
            LOCID_EDIT_ALL_DIALOG_DESC += "*** Warning: if any records use values that you are modifying, you must update those records to use the modified value before you do so here. ***";
            LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED = "Only one value can be default. (Value = '{0}').";
            LOCID_NUMBER_RANGE_VALUE_MASK = "You must enter a whole number between {0} and {1}. (Value = '{2}').";
            LOCID_VALUE_NOT_UNIQUE = "Option values must be unique. The value {0} is already being used by option '{1}'.";
            EDIT_ALL_JS_PATH = "/_static/tools/systemcustomization/attributes/scripts/editall.js";
#if (DEBUG)
            EDIT_ALL_JS_PATH += "?" + Guid.NewGuid().ToString();
    public partial class EditAllPage : AppPage
        protected TextArea txtLabel;
        protected override void ConfigureForm()
            DialogForm currentForm = base.CurrentForm as DialogForm;
            currentForm.DialogTitle = Globals.LOCID_EDIT_ALL_DIALOG_TITLE;
            currentForm.DialogDescription = Globals.LOCID_EDIT_ALL_DIALOG_DESC;
        protected override void ConfigurePage()
            base.CurrentHeader.SetResource("LOCID_NUMBER_RANGE_VALUE_MASK", Globals.LOCID_NUMBER_RANGE_VALUE_MASK);
            base.CurrentHeader.SetResource("LOCID_VALUE_NOT_UNIQUE", Globals.LOCID_VALUE_NOT_UNIQUE);
            base.CurrentHeader.SetResource("DEFAULT_ITEM_CHAR", Globals.DEFAULT_ITEM_CHAR.ToString());
    public class AppListEditAll : CrmUIControl
        AppListEdit _listEdit;
        public AppListEditAll() { }
        protected override void ConfigureHeader()
            CrmUIControlBase.CurrentHeader.SetClientVar("_minValueForCustomPicklists", 1);
            CrmUIControlBase.CurrentHeader.SetClientVar("_maxValueForPicklists", 2147483646);
            CrmUIControlBase.CurrentHeader.SetClientVar("_minValueForSystemPicklists", 200000);
            CrmUIControlBase.CurrentHeader.SetResource("DEFAULT_ITEM_CHAR", Globals.DEFAULT_ITEM_CHAR.ToString());
            _listEdit = this.Page.FindControl("ledtPicklistValues") as AppListEdit;
            if (_listEdit != null)
                this.Disabled = _listEdit.Disabled;
                this.ReadOnly = _listEdit.ReadOnly;
        protected override void Render(HtmlTextWriter output)
            TextWriter innerWriter = output.InnerWriter;
            innerWriter.Write("<div id=\"{0}\" {1}>", CrmEncodeDecode.CrmHtmlAttributeEncode(this.ID), (this.ReadOnly || this.Disabled) ? "disabled" : string.Empty);
            innerWriter.Write("<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr><td>");
            innerWriter.Write("<tr class=\"listEdit_vspacer\"><td></td></tr>");
            innerWriter.Write("<table width=\"100%\" style=\"table-layout:fixed;\" cellspacing=\"0\" cellpadding=\"0\">");
            innerWriter.Write("<tr height=\"100%\"><td></td>");
            innerWriter.Write("<td class=\"listEdit_hspacer\"></td>");
            innerWriter.Write("<td class=\"listEdit_buttons\">");
            innerWriter.Write("<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">");
            this.RenderButton(output, "btnEditAll", (this.Disabled || this.ReadOnly)? "": "editAll();", Globals.LOCID_EDIT_ALL_BUTTON_TEXT);
        private void RenderButton(HtmlTextWriter output, string id, string onClick, string text)
            TextWriter innerWriter = output.InnerWriter;
            innerWriter.Write("<tr><td nowrap style=\"padding-right:5px;\">");
            id = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", CrmEncodeDecode.CrmHtmlAttributeEncode(this.ID), CrmEncodeDecode.CrmHtmlAttributeEncode(id));
            Button button = new Button(id, string.Empty, onClick, false, "listEdit_button");
            button.InnerHtml = "<span class=\"autoellipsis\">" + text + "</span>";
            button.Text = text;
            button.Width = 205;

The last piece is the client side script - editAll.js. Paste the following code into that file:

var _nDefault, _aHiddenValues, _aSystemValues, _iMin, _iMax, _nMax;
function cancel()
function applychanges()
    var oResult = new Object();
        oResult.sText = serializeValues(txtLabel.DataValue)
        window.returnValue = oResult;
    catch (e)
function alertAndThrow(sMessage)
    throw new Error(101, sMessage);
function appendValues(oXmlDoc, oValues, aValues, bEditable)
    var sMessage;
    var iDefault = 0;
    var aDupVals = new Array();
    var aLabels = new Array();
    oValues.setAttribute("default", -2147483648);
    for (var i = 0; i < aValues.length; i+=2)
        var oValue = oXmlDoc.createElement("value");
        var n;
        if (bEditable)
            var s = Trim(aValues[i]);
            if (s.charAt(0) == DEFAULT_ITEM_CHAR)
                s = s.substr(1);
                if (++iDefault > 1)
                    alertAndThrow(formatString(LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED, s));
            n = LocStringToInt(s);
            if (isNaN(n) || n > _iMax || (n < _iMin && !_aSystemValues.contains(n)))
                alertAndThrow(formatString(LOCID_NUMBER_RANGE_VALUE_MASK, AddFormatting(_iMin, 0), AddFormatting(_iMax, 0), s));
            if (iDefault == 1)
                oValues.setAttribute("default", n);
            n = Number(aValues[i]);
        var j = i + 1;
        var sLabel = (j >= aValues.length ? "" : Trim(aValues[j]));
        if (sLabel != "")
            if (bEditable)
                _nMax = Math.max(_nMax, n);
            oValue.setAttribute("value", n);
            oValue.setAttribute("label", sLabel);
            oValue.setAttribute("editable", bEditable? "1" : "0");
    var iDup = aDupVals.indexOfDuplicate();
    if (iDup > -1)
        alertAndThrow(formatString(LOCID_VALUE_NOT_UNIQUE, aDupVals[iDup], aLabels[iDup]));
function serializeValues(sXml)
    var oXmlDoc = CreateXmlDocument();
    var oValues = oXmlDoc.createElement("values");
    var rRegEx = /[\n|\t]/;
    var aValues = _aHiddenValues.join("").split(rRegEx);
    _nMax = 0;
    oXmlDoc.documentElement = oValues;
    appendValues(oXmlDoc, oValues, aValues, false);
    aValues = getTabbedValues(sXml);
    appendValues(oXmlDoc, oValues, aValues, true);
    if (++_nMax > _iMin)
        oValues.setAttribute("next", _nMax);
        oValues.setAttribute("next", _iMin);
    return oValues.xml;
function getTabbedValues(sXml)
    var aValues = new Array();
    if (sXml == null)
        return aValues;
    var aLines = sXml.split(/\n/);
    var iVal = _iMin - 1;
    var iUserVal = iVal;
    for (var i = 0; i < aLines.length; i++)
        var label = Trim(aLines[i]);
        var bDefault = false;
        if (label.charAt(0) == DEFAULT_ITEM_CHAR)
            label = label.substr(1);
            bDefault = true;
        var aPairs = label.split(/\t/);
        if (aPairs != null && aPairs.length > 1)
            iVal = LocStringToInt(aPairs[0]);
            label = aPairs[1];
            iUserVal = Math.max(iVal, iUserVal);
            iVal = Math.max(++iUserVal, ++iVal);
        aValues.push((bDefault ? DEFAULT_ITEM_CHAR : "") + iVal);
    return aValues;
function deserializeValues(oDataXml)
    var oValuesNode = oDataXml.documentElement;
    var iValueNode, oValueNode, oValueNodeList;
    var oValue, aTarget, oAttributes;
    var aValues = new Array();
    _aHiddenValues = new Array();
    _aSystemValues = new Array();
    oValueNodeList = oValuesNode.selectNodes("value");
    var oDefault = oValuesNode.attributes.getNamedItem("default");
    if (!IsNull(oDefault))
        _nDefault = Number(oDefault.value);
    for (iValueNode = 0; iValueNode < oValueNodeList.length; iValueNode++)
        oValueNode = oValueNodeList.item(iValueNode);
        oAttributes = oValueNode.attributes;
        target = (oAttributes.getNamedItem("editable").value == '1' ? aValues : _aHiddenValues);
        var n = Number(oAttributes.getNamedItem("value").value);
        if (target == aValues && n < _iMin)
        if (target == aValues && n == _nDefault)
    var result = aValues.join("");
    return result;
function editAllWindowOnLoad()
    if (typeof(txtLabel) != "undefined")
        Array.prototype.indexOfDuplicate = function() {
            var n = this.length;
            for (var i=0; i<n; i++)
                for (var j=i+1; j<n; j++)
                    if (this[i]==this[j]) return i;
            return -1;
        Array.prototype.contains = function( value ) {
                var n = this.length;
                for (var i=0; i<n; i++)
                    if (this[i]==value) return true;
                return false;
        var oArgs = window.dialogArguments;
        txtLabel.DataValue = oArgs.sText;
        _iMin = oArgs._iMin;
        _iMax = oArgs._iMax;
        _aHiddenValues = oArgs._aHiddenValues;
        _aSystemValues = new Array();
        for(var i=0; i<oArgs._aSystemValues.length; i++)
        txtLabel.attachEvent('onkeydown', txtLabelOnKeyDown);
function txtLabelOnKeyDown()
    if (event.keyCode == 9)
        if (event.srcElement)
            event.srcElement.selection = document.selection.createRange();
            event.srcElement.selection.text = "\t";
        return false;
        return true;
window.attachEvent('onload', editAllWindowOnLoad);
function editAll()
    var oArgs, oResult;
    var oXmlDoc = CreateXmlDocument(false);
    oArgs = new Object();
    _iMin = ((_bIsCustomAttribute + "").toLowerCase() == "true" ? _minValueForCustomPicklists: _minValueForSystemPicklists);
    oArgs.sText = deserializeValues(oXmlDoc);
    oArgs._iMin = _iMin;
    oArgs._iMax = _maxValueForPicklists;
    oArgs._aHiddenValues = _aHiddenValues;
    oArgs._aSystemValues = _aSystemValues;
    oResult = openStdDlg(prependOrgName("/tools/systemcustomization/attributes/editAll.aspx"), oArgs, 500, 450);
    if (oResult == null)
        ledtPicklistValues.DataXml = oResult.sText;

The only thing left to do is find the public key token for your assembly. There are various ways to do this, but if you build your assembly now, the post-build events you copied in earlier will publish the assembly to the GAC. If you browse to the GAC folder after you build the solution with windows explorer and find the published assembly, (e.g. C:\WINDOWS\assembly\HubKey.Crm.Web.Tools.SystemCustomization.Attributes), you'll see the public key token there (you can copy this by viewing the properties of the assembly).

Back in Visual Studio there are two places you'll need to paste in the key token. Do a search and replace in all files for PUBLICKEYGOESHERE and replace that string with the token string.

Build the assembly once more to automatically copy these changes to the right file locations, and you should now see a functioning "Edit All" button on the edit attribute form when you choose picklist as the type from the dropdown. If the build fails because of the the post-build events, try building it one more time (there may be a locked file).

To back out the changes you've made, replace the modified manageAttribute.aspx page with the original page that you copied. Remove the assembly, editAll.js file, and editAll.aspx page from the file locations specified in the post-build events.

Hopefully this helps to save some time for your entity customizing users. If you have any questions or comments, please post them here.

No comments: