Sunday, September 14, 2008

CRM 4.0 Multi Picklist

This post is an extension to my previous post. I have to say I had to agree with the client on this one. Generally when you need a multi picklist you can simply create a new entity to hold the values, create a N:N relationship with the relevant entity and use the left navigation link or display the N:N view in an iframe. But what do you do when you need to put 13 “Multi picklists” on a form. This case is even harder to “plead” when the entity truly holds a single attribute. Eventually I built this multi picklist using this simple js which I hope can save you time if your client chooses not to take the easy way out..

I can't disclose the original entity so I integrated the code on the contact's addresstype attribute for the sake of this post.




Because crm does not support multi value storage for a single field you need to add a new attribute that acts as storage for the user selections.
In the example below the original picklist is new_visa and the new_visa_s is used as “storage”.

The Size property defines the picklist.size / container (TD) row span. The picklist original size is 1 (row span) which mean it will occupy a single row like a regular text field. If you want the picklist to spawn more rows you need to create blank spaces under the picklist field. The code changes the form structure so the picklist does not push the next row elements down as it grows in size.



Finally call the transform method to initiate the transformation form a single to a multi picklist
I only used this on a 1:1 form layout so I can’t say if this works for all form layouts.


function OnCrmPageLoad()
{
var multiPicklist = new MultiPicklist("new_visa","new_visa_s");
multiPicklist.Size = 3;
multiPicklist.Transform();
}

function MultiPicklist( picklistId , storageId )
{
var Instance = this;
var getElem = document.getElementById;
Instance.Picklist = getElem( picklistId );
Instance.Storage = getElem( storageId );
Instance.Size = 1;

Instance.Transform = function()
{
if( !Instance.Picklist )
return alert( "Picklist " + picklistId + " is undefined" );

if( !Instance.Storage )
return alert( "Picklist Storage " + storageId + " is undefined" );

//Hide the storage attribute
hideStorage();
//fix the layout so the picklist won’t push the next row down
fixLayout();
//this is what makes the picklist a multi picklist
Instance.Picklist.multiple = true;
Instance.Picklist.size = Instance.Size * 2;
//Attach to the onsave event
crmForm.attachEvent( "onsave" , Instance.OnFormSave );

//parse the option list
var optionList;
if((optionList = getStorageOptions()) == null)
return;
//re-select the picklist options
for( var i = optionList.length - 1 ; i >= 0 ; i-- )
{
for( var k = Instance.Picklist.options.length - 1 ; k >= 0 ; k-- )
{
var option = Instance.Picklist.options[k];
if( option.value == optionList[i] )
option.selected = true;
}
}
}

Instance.OnFormSave = function()
{
//concatenate the selected options
var optionSummery = "";
for( var k = 0 ; k < Instance.Picklist.options.length ; k++ )
{
var option = Instance.Picklist.options[k];
if( option.selected )
optionSummery += option.value + ",";
}
//remove trailing comma
Instance.Storage.DataValue = optionSummery.replace(/,$/gi,"");
}

function fixLayout()
{
/*
The idea is to find the current picklist position cell in the row
Go down the number of rows as specified by the Size Property
Delete the cells below the picklist and change the picklist cell rowspan attribute
*/
var countTD = 0;
var picklist_d = getElem( picklistId + "_d" );
var picklist_c = getElem( picklistId + "_c" );
var currentTR = picklist_d.parentElement;
var currentTD = picklist_c.previousSibling;

while( currentTD && currentTD.tagName == "TD" )
{
countTD++;
currentTD = currentTD.previousSibling;
}

for( var i = 1 ; i < Instance.Size ; i++ )
{
currentTR = currentTR.nextSibling;
if( currentTR )
{
currentTR.deleteCell(countTD);
currentTR.deleteCell(countTD);
}
}

picklist_d.rowSpan = Instance.Size;
picklist_c.rowSpan = Instance.Size;
}

function getStorageOptions()
{
if( Instance.Storage.DataValue == null )
return null;
return Instance.Storage.DataValue.split(',');
}

function hideStorage()
{
getElem( storageId + "_d" ).style.visibility = "hidden";
getElem( storageId + "_c" ).style.visibility = "hidden";
Instance.Storage.style.visibility = "hidden";
}
}

OnCrmPageLoad();

10 comments:

Anonymous said...

I am pasting this into the CRM 4.0 VPC image. I get and error in this code
for( var i = 1 ; i < Instance.Size ; i++ )
{
currentTR = currentTR.nextSibling;
if( currentTR )
{
currentTR.deleteCell(countTD);
currentTR.deleteCell(countTD);
}
}
'

I get an Object Required error.

Thanks!

Anonymous said...

I figured it out - the part about what the size really meant and what it was for escaped me. Once I "made space" for the multi-select to have someplace to appear, it all worked like a charm. So I guess the note to all is if you don't make room for it to appear, you will get an error. But it will be Your Fault!!

Adi Katz said...

Hi Rich,

Glad you got it working.

As you mentioned the picklist size relies on the form's layout (spaces under the picklist).
You can change the for loop and insert extra checks to make sure the objects (TRs) exists.

Adi

Anonymous said...

Adi,
Love your blog by the way - you have some really interesting things!!

2 things:
1.) I created a picklist called favorite color - added 10 colors, so I got a scrollbar on my list (very cool). HOWEVER - when I go back to the form the list is down where the last selected item is. I haven't tried it but I think it would display better if you changed all of your loops to go the opposite way - from bottom to top.

2.) Unrelated to this: - If you put a multi-multi into an iFrame (there are lots of places where people explain how to do it) there is no New option on the grid - any idea how to get one??

Adi Katz said...

Hi Rich,

I revised the code so the first pick would be on top. I implemented the solution on a questioner with very short lists (3-5 options) so I completely missed on the scrolling part.

The N:N IFrame is an option, certainly more supported then this one, but I really like the idea of using a picklist to build the options. The reason why I ruled out the IFrame option is that you first need to create (save) the entity and only then relate the options unless you plug-in to the post entity create event and that didn’t suit my needs. I didn’t find any decent example on the web using multi-multi so I can’t tell you much about the new button.

Anyway, glad you’re enjoying my blog…

Adi

Rich said...

Adi,
Great - Love the new code!! My bad on the N:N - I was trying to make a suggestion for another post and it didn't come out right. The times to use that versus this are totally different.

One more thought on this post - to make it PERFECT - we need to add 1 more field. The filtered view does a great job of enabling you to use it for reporting and other things where you don't need to do joins and other such garbage. But with this the first _s just holds the keys (1,4,5,etc). We need one more to hold the real values so we can report on it.

Please note my email address - I would love to be able to converse outside of these comments if that is ok with you.

Rich

Rich said...

Adi,
If you tried to email me, the account email was turned off - I guess I didn't go out there for awhile!!

It is back on now ;)

Unknown said...

Hello,

I tried this code and was having some trouble getting all but the last picklist value selected to show as selected-- I used alert function to debug and the problem went away without me changing any other code. That suggested there was an issue with the refresh. So I ended up having to add a SetFocus() action in the code to make sure the picklist changes showed up on the screen (see below:)

>>snip<<
//re-select the picklist options
for( var i = optionList.length - 1 ; i >= 0 ; i-- )
{
for( var k = Instance.Picklist.options.length - 1 ; k >= 0 ; k-- )
{
var option = Instance.Picklist.options[k];
if( option.value == optionList[i] )
{
option.selected = true;
//the following is needed to make sure the selections show properly
Instance.Picklist.SetFocus();
}
>>snip<<

Nick Doelman said...

I pasted this code exactly and setup the fields... but I am getting an error pop-up 'An error has occured' when I save the record...

Any ideas on how to fix?

Adi Katz said...

Hi Nick,

Put the debugger keyword inside the OnFormSave Function (line 50) and see where it breaks.

I also strongly recommend implementing the multi select picklist using the following post http://mscrm4ever.blogspot.com/2008/12/crm-40-supported-multi-select-picklist.html.

Adi