Asynchronous loading of JavaScript Web Resources after U12/POLARIS

UPDATE: This article is outdated - please refer to https://www.develop1.net/public/post/CRM-2013-Script-Loading-Deep-Dive.aspx  We all know that UR12/POLARIS was a monumental release for Dynamics CRM what with the new Process Forms and Cross Browser support, but also included were some performance optimisations. One such improvement was a change to the way JavaScript Web Resources are loaded on forms so that they load and execute asynchronously rather than in the order that they were added to the form. The drawback with this optimisation is that it can cause the '..is undefined' script error if you have scripts that depend on other scripts being loaded first. This post describes the loading behaviour and some possible solutions. How did it work before UR12/POLARIS? In my example I have 3 scripts, each dependant on the last. Mscorlib.js is the Script# system library that is needed before any other libraries can be loaded. I'm not talking about code that runs in the 'onload' event of a form but global code that is run after the script has downloaded used to define the prototypes of the objects that are used by the onload code. In the Client.js, I might have some Script# generated code that requires a core Script# (ss.IEnumerable) type that is defined in a different script file. Xrm.Sdk.DataCollectionOfEntity.registerClass('Xrm.Sdk.DataCollectionOfEntity', null, ss.IEnumerable); For this code to run, the mscorlib.js must be loaded and executed first. The same applies if you are using jQuery and jQuery-UI. The form definition used to have each script added in order so that they would be added to the 'head' section of the page as follows: <head> .. <script src=”/%7B635001685810000000%7D/WebResources/fdocs/js/mscorlib.js” type=”text/javascript”></script> <script src= /%7B635001685810000000%7D/WebResources/fdocs/js/Xrm.js” type=”text/javascript”></script> <script src=” /%7B635001685810000000%7D/WebResources/fdocs_/js/Client.js” type=”text/javascript”></script> ..

The resulting load pattern would be something like:

The script load/execution would be approximately:

Mscrolib.js downloads and then executes Xrm.jsd downloads and then executes after mscrolib.js has completed executing since it follows it in the <HEAD> section of the page Client.js downloads and then executes after Xrm.js has completed executing since again it is added to the <HEAD> section in this order. The On Load event code then runs when all the scripts have loaded and finished executing

 

  What changes with the UR12/POLARIS update? Scripts are no longer in the <HEAD> section but are loaded asynchronously via the 'loadScriptAdv' function: loadScriptAdv('\x2f\x257B635001707050003339\x257D\x2fWebResources\x2ffdocs\x2fjs\x2fmscorlib.js', '\x2f\x257B635001707050003339\x257D\x2fWebResources\x2ffdocs\x2fjs\x2fmscorlib.js', false); loadScriptAdv('\x2f\x257B635001707050003339\x257D\x2fWebResources\x2ffdocs\x2fjs\x2fXrm.js', '\x2f\x257B635001707050003339\x257D\x2fWebResources\x2ffdocs\x2fjs\x2fXrm.js', false); loadScriptAdv('\x2f\x257B635001707050003339\x257D\x2fWebResources\x2ffdocs\x2fjs\x2fClient.js', '\x2f\x257B635001707050003339\x257D\x2fWebResources\x2ffdocs\x2fjs\x2fClient.js', false);

The new asynchronous load behaviour results in the OnLoad being run after a smaller wait time since each script can execute without waiting for the preceding ones. This is a good performance gain but causes scripts to fail if the dependant scripts are not loaded at the point of execution.

The interesting thing is that this wasn't immediately apparent or was only an intermittent problem because once the scripts are loaded into the browser cache, the execution would usually be in the correct order. I can consistenly reproduce the issue by clearing down the browser cache and then disabling all caching. Ribbon JavaScript It's been a common technique to add dependant libraries to Ribbon Commands by adding them as Command Actions with a function of 'isNaN' <Actions> <JavaScriptFunction Library=”$webresource:mscorlib_crm.js” FunctionName=”isNaN”/> <JavaScriptFunction Library=”$webresource:xrm.js” FunctionName=”isNaN”/> <JavaScriptFunction Library=”$webresource:RibbonCommands.js” FunctionName=”someCommand”/> </Actions>

It seems that the same issue exists with these libraries since they are loaded in an asynchronous fashion. Solutions Unfortunately, any solution that uses depending scripts in this way has to be updated – and like with all things, the right solution depends on your specific case. 1) All script in a single library By far the simplest solution is to put all of your scripts into a single file in the correct order. I didn't' settle for single script option for the following reasons:

A single script is harder to maintain Common libraries can't be shared between solutions All code must be downloaded on first use, rather than only downloading what is required at the time.

2) RequireJs or HeadJs There are some good JavaScript loading libraries out there such as RequiresJs or HeadJs. These libraries allow you to dynamically load dependant script before you execute code that needs them. Gayan has an example of this technique on his blog. Maarten also has a good tutorial on his blog. I didn't settle for the RequireJs/HeadJs option for the following reasons:

It requires you to manually set the Cache key to ensure that scripts are not downloaded every time they are needed It completely bi-passes the Dynamics CRM script loading mechanism in an 'unsupported' way There is some possibility that a backward compatibility option may be added into a future release (I'm ever the optimist!). Adopting this approach would make it harder to revert back to the standard script registration approach.

3) Manually wait for required libraries to load The approach I took was to build a simple wait function to wrap code in that prevented execution until the required scripts had loaded. Each time a script completes, it adds its name to a semaphore array that is waited on by other libraries. The result is much the same as before UR12:

The difference here is that the scripts start executing as soon as they are loaded, but then wait using setTimeout until the required scripts have completed. The code will work on pre-UR12 systems as well, but because the wait is unnecessary, the code will simply execute the script without checking dependancies. setTimeout is used to release control to other scripts because Javascript is single-threaded. The Code Each script that has dependencies must be wrapped in the following: waitForScripts("client",["mscorlib", "xrm",], function () { //Original Code goes here });

The first parameter provides the name of the current script, and the array is the list of scripts that must be loaded first. So in the Xrm.js library, you would wrap it in: waitForScripts("xrm",["mscorlib"], function () { //Original Code goes here });

Each script must also include the waitForScript function at the bottom: function waitForScripts(name, scriptNames, callback) { var hasLoaded = false; window.loadedScripts = window.loadedScripts || []; function checkScripts() { var allLoaded = true; for (var i = 0; i < scriptNames.length; i++) { var hasLoaded = true; var script = scriptNames[i]; switch (script) { case "mscorlib": hasLoaded = typeof (window.ss) != "undefined"; break; case "jquery": hasLoaded = typeof (window.jQuery) != "undefined"; break; default: hasLoaded = window.loadedScripts[script]; break; } allLoaded = allLoaded && hasLoaded; if (!allLoaded) { setTimeout(checkScripts, 10); break; } } if (allLoaded) { callback(); window.loadedScripts[name] = true; } } // Only check for async loading of scripts if later than UR12/POLARIS if (typeof(APPLICATIONFULLVERSION)!='undefined' && parseFloat(APPLICATIONFULLVERSION.replace('5.0.',''))>9690.2835) { setTimeout(checkScripts, 0); } else { callback(); window._loadedScripts[name] = true; } } Script# 'Script.template' I almost exclusively use Script# in my Dynamics CRM projects. What made this solution really work well for me was that I just include the code in the Script.template file to wrap the '#include[as-is] "%code%"'. The beauty is that I can now forget it's there and it just gets minified along with the rest of the code when deployed. This issue has affected a number of people I know, and I'm sure it'll start to become more of an issue as people start to upgrade to the latest Rollup. If you have any suggestions/comments, please let me know. The code was originally posted by me in the MSDN forums:  http://social.msdn.microsoft.com/Forums/en-US/crmdevelopment/thread/fdca4779-e866-4e51-bab9-97a159f9cd37   UPDATE: This issue is now fixed in UR15 -http://support.microsoft.com/kb/2843571 @ScottDurow

Debugging HTML Webresources on localhost

When writing HTML Webresources, I will frequently host the html page on a local development server (e.g. localhost:8777) and make calls to the CRM server on a different port (e.g. localhost:5555). I do this to speed up the debug process since there is no need to deploy the webresource so that it is hosted inside Dynamics CRM. Now that UR12 has introduced cross browser support, the same technique is needed for other browsers. Since Chrome enforces cross domain checks even for localhost on different ports – I received the following error when trying to call the SDK webservices: XMLHttpRequest cannot load http://localhost:5555/OrgName/XRMServices/2011/Organization.svc/web. Origin http://localhost:8777 is not allowed by Access-Control-Allow-Origin.

Fortunately, Fiddler has the answer: 1) First Install and run Fidder2 (www.fiddler2.com) 2) Open the 'FiddlerScript' tab and locate the 'OnBeforeResponse' function. 3) Paste the following at the end of the function: oSession.oResponse.headers.Add("Access-Control-Allow-Origin","http://localhost:8777"); oSession.oResponse.headers.Add("Access-Control-Allow-Credentials","true"); oSession.oResponse.headers.Add("Access-Control-Allow-Headers","origin, soapaction, content-type"); if ((oSession.responseCode == 400) && (oSession.oRequest.headers["Access-Control-Request-Method"]=="POST")) { oSession.responseCode = 200; }

Note: Makes sure you put your own port number in the Origin parameter. 4) Click 'Save Script' You requests will now not be blocked by the cross domain policy since Chrome thinks that the server is explicitly allowing the requests to come from a different domain. @ScottDurow

Corrupt images after installing UR12 OnPrem

I had a strange one today after upgrading one customer's Dynamics CRM servers to UR12. All was well apart from there being a few places that the images look corrupt.

We tracked the cause down to an image that had not be replaced during the upgrade at the following location: C:\Program Files\Microsoft Dynamics CRM\CRMWeb_imgs\imagestrips\gridctrlimgs.png The pre-UR12 image was:

The new UR12 image should be:

Because this is an image strip, and the images had moved around, the css was pointing to the wrong place – and hence the corrupted images. Simply replacing the old image with the new one fixed the problem (after a browser cache refresh). If you have the same problem, you can download the updated image from here: gridctrlimgs.png (5.46 kb)

Adding Auto Refresh to Dashboards

This post shows you how to set up a CRM2011 Dashboard 'auto-refresh' feature using the Ribbon Workbench in 10 easy steps (well…11 if you count clicking publish at the end!). We will add an auto refresh function to the 'EnableRule' of the Dashboard refresh button that schedules a refresh using the 'setTimeout' function. The EnableRule is called when the dashboard page is first displayed to check if the Refresh Button should be enabled. We schedule the refresh, and then return true to ensure the button is still enabled. This technique can also be used to add JavaScript to areas of CRM 2011 that are not form based. Let's get started: 1) Create a new solution and add the Application Ribbon to it (Client Extensions->Add Existing->Application Ribbon). 2) Create a new web-resource named 'RefreshRibbon.js' (or something similar) Add the following JavaScript to it: var AUTOREFRESHINTERVAL=30000; /// Schedule the first refresh if we havn't already function autoRefreshDashboards() { var topWindow = window.top; if (typeof (topWindow['refreshRegistered']) === 'undefined') { window.setTimeout(autoRefreshTimeoutCallback, AUTOREFRESHINTERVAL); topWindow['refreshRegistered'] = true; } return true; } // Refresh the dashboards and schedule the next refresh function autoRefreshTimeoutCallback() { try { Mscrm.DashboardRibbonActions.refreshDashboardPage(); window.setTimeout(autoRefreshTimeoutCallback, 30000); } catch ($e1) { // Perhaps the dashboards are no longer loaded. and we are on a different page } }

Note: You can change the refresh interval using the 'AUTOREFRESHINTERVAL' value which is in milliseconds (30000=30 seconds) Your solutions should now look something like:

3) Open the Ribbon workbench, and load up your new solution. 4) Select the 'Application Ribbon' in the 'Entities' panel if not already, then select the 'Dashboards' tab in the Design surface (be sure to select 'Dashboards' and not 'Dashboard') 5) Right click on the 'Refresh All' button and click 'Customise Command' (Not Customise Button)

6) Locate the 'Mscrm.DashboardTools.RefreshCommand' command in the 'Commands' section of the 'Solution Elements' panel. Right click, and select 'Edit Enable Rules' 7) In the Enable Rules dialog click '+Add New' and then select 'Add Step', before selecting 'Custom JavaScript Rule' 8) In the Enable Rule properties, set: FunctionName: 'autoRefreshDashboards' Library : Click the lookup button and select the 'RefreshRibbon.js' (If you don't see it, then you forgot to add the javascript webresource you created above to the solution you loaded into the Ribbon Workbench)

9) Click OK, and OK again. 10) In the 'Solution Elements' panel, expand the 'Enable Rules' and select 'Mscrm.IsDashboardSelected'. In the properties panel, set 'IsCore' = True. This ensures that this rule is not customised in our solution since we only need to customise the Command to add the new enable rule. 11) Click Publish Solution And you're done! This will work both in the Web Browser and the Outlook client. It is important that you remember that auto-refreshing dashboards could place more load on your server if lots of users leave the dashboards open all day! Until next time… @ScottDurow

getServerUrl is Deprecated. Use getClientUrl instead.

Although at the time of writing UR12 is not yet released - I was checking though the changes in the latest SDK documentation. In addition to the Ribbon Workbench being listed (yay!) I noticed the following statement about the getServerUrl function "Deprecated. Use getClientUrl instead. This method is deprecated as of Microsoft Dynamics CRM 2011 Update Rollup 12 and the Microsoft Dynamics CRM December 2012 Service Update."   Out with the old   Using getServerUrl on it's own always had the potential to cause 'Access Denied' messages if the browser domain Url was different to the server Url stored in the CRM database. This was to do with cross domain access restrictions in IE. A work around was to use a method similar to Daniel Cai gives in his post on the subject - http://danielcai.blogspot.co.uk/2012/02/get-right-server-url-in-your-crm-client.html In with the new The SDK described the new getClientUrl as: "Returns the base URL that was used to access the application." The function nolonger blindly return the server Url in the database - but looks at the url that was used to access CRM so that cross domain access issues will be a thing of the past! Read more in the SDK:http://msdn.microsoft.com/en-us/library/d7d0b052-abca-4f81-9b86-0b9dc5e62a66#BKMK_getClientUrl @ScottDurow  

Multi-Language Lookups

Happy New Year! Dynamics CRM has fantastic localisation support and multiple language user interfaces is no exception to that rule. Installable language packs provide translations for out-of-the box labels, whilst customizers are able to translate their own labels for the following elements:

Field Labels Option Set Values Entity Display Names User Interface Messages View Names Ribbon Button Labels Descriptions

More information about this can be found at Customize Labels to Support Multiple Languages, but the purpose of this post is to provide a suggested solution to localising the only item that is missing from this list: Lookup Record Display Names If you had two sales teams that spoke only English or German and they wanted to share the same products, there currently is no way of showing the product name field in the sales persons own language without duplicating product records. My suggested solution is to write a plugin to intercept the results returned from the database when queried and insert the correct language into the name field. The example I show below works for Products and Opportunity Products, but it could equally be applied to other entities as well. Key design objectives were:

Optimise for performance by minimising database queries and caching where the same data where possible. Allow translation of the Product Name attribute of the Product Entity. Show the Product Name on the Opportunity Product form in the user's language. Show the Product Name in Lookup Views in the user's language Show the Product Name title on the Product form in the user's language. Support translated Product Name fields in reports Allow easy translation of products into multiple languages an export/import process.

Solution Summary The technique adds an attribute for each name translation on the product form, and then intercepts Retrieve and Retrieve-Multiple pipeline steps in order to substitute the name field for the correct translation. 1) An attribute for each name translation is added to the Product entity 2) When the product is updated, each translated is packed into the 'name' field via the 'Create' and 'Update' pipeline steps. The name attribute is increased in length to hold 4000 characters to accommodate all the translated names. This is done so that all translations are available when querying the name attribute without going back to the database.newnameen: "Red Shoes"newnamede: "Rote Schuhe"name: "Red Shoes,Rote Schuhe"Each time the name attribute is retrieved, we can always re-write it to include the language that we need without going back to the database to get the newnameen or newnamede field values.Note: A limitation of the primary name field is that it has a maximum length of 4,000 characters. The product name is included in a SQL index and so cannot be more than 450 characters (900 bytes). Due to the nature of this technique, we must be able to fit all our translations into these characters, so if we have 5 languages, then each name cannot be more than 90 characters long – since the default Name length is 100 characters, this doesn't seem that unreasonable – especially if you have less than 5 languages. If you are using this technique on a custom entity attribute, then you can use the full 4000 characters. 3) When the Products are queried (via Lookup dialogs or Advanced Find) the results are changed so that the name field only contains the translation that matches the language of the user. 4) When the Opportunity Product is Retrieved, the name field is re-written to only contains the translation that matches the language of the user. 5) The user's selected language is looked up in the UserSetting entity via the UILanguageId attribute. This contains an LCID that is used to match against the correct translated label. 

Solution Steps First we need to create a project to hold the plugin: 1) Create a Plugin Solution using the CRM Developer Toolkit found in the CRM2011 SDK. 2) Connect to your CRM server and select a solution to add your plugin to. 3) Select the Plugin and Workflow project in turn and open the properties window. Select 'Signing' and check 'Sign the assembly' before selecting '<New…>'. You will be prompted to give your key a name and password. I usually use the name of the project as a name for the key. 4) Open the CRM Explorer window and double click 'Product' to open the Product entity configuration page. 5) Select the 'name' attribute, and change its 'Maximum Length' to 450 characters, and the required level to 'No Constraint' 6) We need to create some attributes to hold the translated names of the products. I am only creating English and German, but you can create however many you need.

Display Name 

Schema Name 

Type 

Name (English) 

newnameen 

Single Line of Text (100) 

Name (German) 

newnamede 

Single Line of Text (100) 

7) Add the two fields to the Product Form as well, and un-check 'Visible by default' on the 'Name' field. 8) Publish the customisations. 9) In your Visual Studio Project, add a new Class named 'MultiLanguageProductPlugin' 10) Paste the following code into your class.IMPORTANT: Change the namespace to match your project namespace // Scott Durow // 1/2/2013 7:55:23 PM // Multi Language Support for Lookups namespace Develop1.Plugins { using System; using System.ServiceModel; using Microsoft.Xrm.Sdk; using System.Text; using Microsoft.Xrm.Sdk.Query;

/// 
/// Plugin to support Multi Language Product Names
///     
public class MultiLanguageProductPlugin: Plugin
{
    private readonly string preImageAlias = "PreImage";
    private readonly string[] languages = new string[] { "en", "de" }; // Languages Supported
    private readonly int[] locales = new int[] { 1033, 1031 }; // LCIDs of each language in the languages array

    /// 
    /// Initializes a new instance of the  class.
    /// 
    public MultiLanguageProductPlugin()
        : base(typeof(MultiLanguageProductPlugin))
    {
        // Registrations for Packing each translation field into the name field
        base.RegisteredEvents.Add(new Tuple&gt;(20, "Create", "product", 
            new Action(PackNameTranslations)));
        base.RegisteredEvents.Add(new Tuple&gt;(20, "Update", "product", 
            new Action(PackNameTranslations)));

        // Registrations for unpacking the name field on Retrieve of Products
        base.RegisteredEvents.Add(new Tuple&gt;(40, "Retrieve", "product", 
            new Action(UnpackNameOnRetrieve)));
        base.RegisteredEvents.Add(new Tuple&gt;(40, "RetrieveMultiple", "product", 
            new Action(UnpackNameOnRetrieveMultiple)));

        // Registrations for unpacking the name field on related Opportunity Products
        // NOTE: You could add registratons for Quotes, Orders, Price Lists here...
        base.RegisteredEvents.Add(new Tuple&gt;(40, "Retrieve", "opportunityproduct", 
            new Action(UnpackNameOnRetrieveRelated)));
        base.RegisteredEvents.Add(new Tuple&gt;(40, "RetrieveMultiple", "opportunityproduct", 
            new Action(UnpackNameOnRetrieveMultipleRelated)));

    }

    /// 
    /// Pack the translations into the name field when a Product is Created or Updated
    /// Each translated name is packed into a comma separated string
    /// This field is unpacked when the product entity is retrieved or related records are retrieved
    /// 
    protected void PackNameTranslations(LocalPluginContext localContext)
    {

        IPluginExecutionContext context = localContext.PluginExecutionContext;

        // Pack the translated labels into the name field en,de
        Entity target = (Entity)localContext.PluginExecutionContext.InputParameters["Target"];
        Entity preImageEntity = (context.PreEntityImages != null &amp;&amp; context.PreEntityImages.Contains(this.preImageAlias)) ? context.PreEntityImages[this.preImageAlias] : null;

        string[] names = new string[languages.Length];

        for (int i = 0; i &lt; languages.Length; i++)
        {
            names[i] = GetAttributeValue("new_name_" + languages[i], preImageEntity, target);

        }

        // Store the packed value in the target entity
        target["name"] = string.Join(",", names);
    }

    /// 
    ///  Unpack the name field when a Product is Retreived
    /// 
    protected void UnpackNameOnRetrieve(LocalPluginContext localContext)
    {    
        IPluginExecutionContext context = localContext.PluginExecutionContext;
        Entity target = (Entity)context.OutputParameters["BusinessEntity"];

        // Re-write the name field in the retrieved entity
        target["name"] = UnpackName(localContext, target.GetAttributeValue("name"));   
    }

    /// 
    /// Unpack the name field when Products are retrieved via Lookup Search or Advanced Find
    /// 
    protected void UnpackNameOnRetrieveMultiple(LocalPluginContext localContext)
    {
       IPluginExecutionContext context = localContext.PluginExecutionContext;
       EntityCollection collection = (EntityCollection) localContext.PluginExecutionContext.OutputParameters["BusinessEntityCollection"];
       foreach (Entity e in collection.Entities)
       {
           if (e.Attributes.ContainsKey("name"))
           {
               e["name"] = UnpackName(localContext, e.GetAttributeValue("name"));
           }
       }
    }
    /// 
    /// Unpack the product lookup name when an Opportunity Producs is Retrieved
    /// 
    protected void UnpackNameOnRetrieveMultipleRelated(LocalPluginContext localContext)
    {
        IPluginExecutionContext context = localContext.PluginExecutionContext;
        EntityCollection collection = (EntityCollection)localContext.PluginExecutionContext.OutputParameters["BusinessEntityCollection"];
        foreach (Entity e in collection.Entities)
        {
            if (e.Attributes.ContainsKey("productid"))
            {
                ((EntityReference)e["productid"]).Name = UnpackName(localContext, e.GetAttributeValue("productid").Name);
            }
        }
    }

    /// 
    /// Unpack the product lookup name when Opportunity Products are retrieved via lookup searches or advanced find
    /// 
    protected void UnpackNameOnRetrieveRelated(LocalPluginContext localContext)
    {
        IPluginExecutionContext context = localContext.PluginExecutionContext;
        Entity target = (Entity)context.OutputParameters["BusinessEntity"];
        if (target.Attributes.ContainsKey("productid"))
        {
            ((EntityReference)target["productid"]).Name = UnpackName(localContext, target.GetAttributeValue("productid").Name);
        }

    }

    /// 
    /// Unpack the product name field
    /// 
    protected string UnpackName(LocalPluginContext localContext, string name)
    {
        // Get the language of the user
        int userLanguageId = 0;
        if (localContext.PluginExecutionContext.SharedVariables.ContainsKey("UserLocaleId"))
        {
            // Get the user language from the pipeline cache
            userLanguageId = (int)localContext.PluginExecutionContext.SharedVariables["UserLocaleId"];
        }
        else
        {
            // The user language isn't cached in the pipline, so get it here
            Entity userSettings = localContext.OrganizationService.Retrieve(
                "usersettings", 
                localContext.PluginExecutionContext.InitiatingUserId, 
                new ColumnSet("uilanguageid"));
            userLanguageId = userSettings.GetAttributeValue("uilanguageid");
            localContext.PluginExecutionContext.SharedVariables["uilanguageid"] = userLanguageId;
        }

        // Split the name
        string[] labels = name.Split(',');

        // Which language is set for the user?
        int labelIndex = Array.IndexOf(locales, userLanguageId);

        // Return the correct translation
        return labels[labelIndex];
    }


    /// 
    /// Get a value from the target if present, otherwise from the preImage
    /// 
    private T GetAttributeValue(string attributeName, Entity preImage, Entity targetImage)
    {
        if (targetImage.Contains(attributeName))
        {
            return targetImage.GetAttributeValue(attributeName);
        }
        else if (preImage != null)
            return preImage.GetAttributeValue(attributeName);
        else
            return default(T);
    }
}

}

11) Locate the RegisterFile.crmregister file in the CRM Solution Project, and paste the following inside the PluginTypes section:IMPORTANT: Change the Name and TypeName to match the namespace of your project.

<Plugin Description="Multi Language Support for Products" FriendlyName="MultiLanguageProductPlugin" Name="Develop1.Plugins.MultiLanguageProductPlugin" Id="00000000-0000-0000-0000-000000000000" TypeName="Develop1.Plugins.MultiLanguageProductPlugin"> <Steps> <clear /> <!-- Pack the translations into the name field when a Product is Created --> <Step CustomConfiguration="" Name="ManageNameFieldPlugin" Description="Pack the translations into the name field when a Product is Created" Id="00000000-0000-0000-0000-000000000000" MessageName="Create" Mode="Synchronous" PrimaryEntityName="product" Rank="1" SecureConfiguration="" Stage="PreInsideTransaction" SupportedDeployment="ServerOnly"> <Images /> </Step> <!-- Pack the translations into the name field when a Product is Updated --> <Step CustomConfiguration="" Name="ManageNameFieldPlugin" Description="Pack the translations into the name field when a Product is Updated" Id="00000000-0000-0000-0000-000000000000" MessageName="Update" Mode="Synchronous" PrimaryEntityName="product" Rank="1" SecureConfiguration="" Stage="PreInsideTransaction" SupportedDeployment="ServerOnly"> <Images> <!-- We need the translated labels even if it isn't updated in this update --> <Image Attributes="newnameen,newnamede" EntityAlias="PreImage" Id="00000000-0000-0000-0000-000000000000" MessagePropertyName="Target" ImageType="PreImage" /> </Images> </Step> <!-- Unpack the Product name field when Retrieved--> <Step CustomConfiguration="" Name="PostProductRetrieve" Description="Unpack the Product name field when Retrieved" Id="00000000-0000-0000-0000-000000000000" MessageName="Retrieve" Mode="Synchronous" PrimaryEntityName="product" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly"> <Images /> </Step> <!-- Unpack the Product name field when Retreived in Lookup/advanced find--> <Step CustomConfiguration="" Name="PostProductRetrieveMultiple" Description="Unpack the Product name field when Retreived in Lookup/advanced find" Id="00000000-0000-0000-0000-000000000000" MessageName="RetrieveMultiple" Mode="Synchronous" PrimaryEntityName="product" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly"> <Images /> </Step> <!-- Unpack the Product name in the Opportunity Product productid lookup when Retreived--> <Step CustomConfiguration="" Name="PostOpportunityProductRetrieve" Description=" Unpack the Product name in the Opportunity Product productid lookup when Retreived" Id="00000000-0000-0000-0000-000000000000" MessageName="Retrieve" Mode="Synchronous" PrimaryEntityName="opportunityproduct" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly"> <Images /> </Step> <!-- Unpack the Product name in the Opportunity Product productid lookup when Retreived in Lookup/advanced find--> <Step CustomConfiguration="" Name="PostProductOpportunityRetrieveMultiple" Description="Post-Operation of Product Opportunity Retrieve" Id="00000000-0000-0000-0000-000000000000" MessageName="RetrieveMultiple" Mode="Synchronous" PrimaryEntityName="opportunityproduct" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly"> <Images /> </Step> </Steps> </Plugin>

12) Build and deploy your project. You should now be able to create products, providing both a German and English name, and see the correct translations depending on your language selection. You can extend this solution to include quotes, orders, price lists so that the lookups to products on those entities will also show the correct translated name. In the same way that you can export translations from a solution to be translated, you can export the products to excel and mark them as being available for re-import. This file can be passed to a translator, updated and then re-imported. If you need multi-language support for product names in Report, you can simply ensure that all the translated name fields (newnameen, newnamede) are included in the query and use an Expression such as :   =IIF(Parameters!CRMUILanguageId.Value=1031,Fields!newnamede.Value,Fields!newname_en.Value)   This is a proposed solution to allowing lookup names to be translated into multiple languages. I welcome any feedback/suggestions/alternatives.Download the full solution from the MSDN Code Gallery. Until next time!  @ScottDurow

Add a Run Dialog Short-Cut Ribbon Button

  As I've mentioned before, users are always asking about making solutions 'less-clicky' - a common request is to provide a button to run a frequently used dialog rather than having to search for it each time in the lookup window. The following solution shows you how to do this quickly using the Ribbon Workbench for CRM2011. http://ribbonworkbench.uservoice.com/knowledgebase/articles/140652-create-a-run-dialog-short-cut-ribbon-button   Happy Ribbon Customising! @ScottDurow

Un-responsive Import Solution Dialog

If you've ever sat watching the import solution dialog with CRM2011 and wondering if it's actually doing anything - provided you are using OnPrem - I've got just the thing for you. Try running the following SQL on your <OrgName>_MSCRM database - you'll get a far more responsive progress indicator! Select top 1 StartedOn,Progress,CompletedOn from ImportJob (nolock) order by startedon desc Thanks for reading!

No-Code Workflow Shortcut Ribbon Button

Users are always asking about making solutions 'less-clicky' - a common request is to provide a button to run a frequently used workflow rather than having to use the look up dialog. The following solution shows you how to do this without writing a single line of code! http://ribbonworkbench.uservoice.com/knowledgebase/articles/132235-create-a-workflow-short-cut-ribbon-button-no-code

Of course it is using the Ribbon Workbench for CRM2011. If you've not already, download it for free now!

Small is beautiful – Useful Ribbon Buttons

I am forever having to copy record link urls and extract the record GUID from the query string and remove the escape characters. Last week was the straw that broke the camel's back so I had to create a Ribbon Button to do it for me. It is my pleasure to present to you the 'Useful Buttons' solution - small and simple - but most useful! It is a managed solution so that you can install when you need - and then remove without trace afterwards! UsefulButtons1001_managed.zip (11.28 kb) Features:

Get Form Record ID – Allows you to copy to the clipboard the GUID of the current form Record Get Selected Record IDs - Allows you to copy to the clipboard all of the currently selected record GUIDs in a home or sub-grid. Refresh Record – Reloads the current form – Like pressing F5! Refresh Ribbon – Refreshes the current Ribbon – useful when developing but you haven't wired up any on-change events yet.

  Of course, this was really easy to build using the Ribbon Workbench for CRM2011! Download it for free if you've not already!