Sunday, December 28, 2008

CRM 4.0 Supported Record / Page Counter for CRM views




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

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

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

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

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

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

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

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

The Plug-in Supports the following views
* Advanced Find

* Single Lookup View

* Multi Lookup View

* Public and Private Views

* Quick Find

* Custom FetchXml and Advanced Find

* Form Assistant

* Auto resolve Lookup View


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

Friday, December 19, 2008

CRM 4.0 Tooltip Wizard


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

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

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

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






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

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

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

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

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

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

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


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

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

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



The Tooltip Wizard Features

  1. Lets you manage and control application wide tooltips.

  2. Allows you to cut design and development time.

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

Functionality

Editing


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


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


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


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

Displaying


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


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

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


Publishing


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


The wizard is now available online.

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

Wednesday, December 3, 2008

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


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

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

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



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

call iisreset

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

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

Copy the *.pdb file to the assembly bin folder

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


That’s it.

CRM 4.0 Supported Multi Select (Picklist) Control




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

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

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

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

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






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





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


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






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

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



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


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


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





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


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


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


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


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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

1. Added a new Class Library application



2. Signed the application by creating a Strong Name key

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













Plug-In Solution XML

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


Plug-In Code:

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

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

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

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

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

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

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

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

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

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

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

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

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

#endregion
}
}


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