Wednesday, May 30, 2012

Why and How to use relative path in CRM2011 JavaScript

As CRM developers, we need to use the paths of the URLs, web services and other web resources in JavaScript code. I am talking about CRM URLs for e.g. we will use the following URLs to work with  REST(OData) organization data service

https://hp7.crm5.dynamics.com/xrmservices/2011/organizationdata.svc/ (CRM Online)
http://15.218.138.225/HPDECSStaging/xrmservices/2011/organizationdata.svc/ (On premise)

The highlighted part of the URLs represents the server and organization name of the CRM.
Instead of using the hard coded server name, we should use some generic function that can give us the server and  the organization name of the CRM. It will help to deploy the same code for different organizations, servers and deployments without making any changes.
Most of the Microsoft samples use Xrm.Page.context.getServerUrl() function to retrieve the server URL.I have written the following function to display message box with  the oData organization data service URL.

function test()
{
alert (Xrm.Page.context.getServerUrl() + "/xrmservices/2011/organizationdata.svc");
}
I executed this function on CRM Online and On-Premise deployments. I used the following URLs to start the CRM in the browser
I received the following results.

On CRMOnline, the function returned the following message.
image
This worked as expected.

On on-premise deployment, It returned the following message.
image
It returned the server name (aun.....23) instead of IP address(http://15.218.138.225/HPDECSStaging). I was using an IP address in the browser and it returned the server name. If I try to use this URL to retrieve any data, I will get an error message “access denied”. For this javascript to work properly, the server name part of URL in the browser has to match the server name part of the URL returned by Xrm.Page.context.getServerUrl() method. Xrm.Page.context.getServerUrl() returns the servername stored in the deployment manager. Have a look at the following screen shot.

image
So, if you are using the ip address or localhost in a browser to start the CRM and you are using Xrm.Page.context.getServerUrl() to generate the URL, then you will always get ”access denied” error in the JavaScript.
To overcome this problem, we should use the relative path as shown below
"/xrmservices/2011/organizationdata.svc"
There is one more catch to it. It will work with CRM Online as organization name is a part of the server URL(https://hp7.crm5.dynamics.com).
But to make it work with on-premise deployment, we need to add organization name in the relative path. something  like this
“/” + orgname + "/xrmservices/2011/organizationdata.svc".
You can read the organization name from the context by using Xrm.Page.context.getOrgUniqueName() method. Now we have two different URLs one for on premise and one online deployments. When we are writing JavaScript libraries, we want them to work with every deployment without making any changes. Here is the solution, use  Xrm.Page.context.prependOrgName() method . It will sort the organization name problem. Here is my new test function.
function test()
{
alert (Xrm.Page.context.prependOrgName("/xrmservices/2011/organizationdata.svc"));
}
The function will return “/xrmservices/2011/organizationdata.svc” for CRMOnline deployment. “orgname/xrmservices/2011/organizationdata.svc for On premise deployment. In short, use relative paths with  Xrm.Page.context.prependOrgName(), when working with the URLs in javascript. It will work with online, on premise and hopefully IFD. I did not test the code on IFD deployment.

Monday, May 14, 2012

How to trigger a plugin for a many to many relationship

The blog will explain “ How to trigger a plugin for many to many (N:N) relationship. For this blog, I have created a new N:N relationship between account and contact entity. I am displaying this relationship just in account entity. The following screen shot is displaying the properties of the relationship.
a1
To trigger a plugin on association of 2 entities in a n:n relationship, we will use associate plugin message. I am using the developer’s toolkit for writing code from last 6 months. The problem is developer’s toolkit does not support “Associate” message. To create a plugin in developer’s toolkit, you need to choose primary entity and to use “Associate” message primary entity and secondary entity has to be “none”. There is a workaround to register  plugin step on “Associate” message. Here are the steps.
  1. Follow the steps in this blog on how to create a plugin using developer’s toolkit.
  2. In the step 12th of above-mentioned blog, replace the function “ExecutePostAccountCreate” with the following code
    protected void ExecutePostAccount(LocalPluginContext localContext)
    {
    
        if (localContext == null)
        {
            throw new ArgumentNullException("localContext");
        }
    
        // TODO: Implement your custom Plug-in business logic.
        // Obtain the execution context from the service provider.
        IPluginExecutionContext context = localContext.PluginExecutionContext;
        IOrganizationService service = localContext.OrganizationService;
        ITracingService tracingService = localContext.TracingService;
    
        try{
        // The InputParameters collection contains all the data passed in the message request.
        if (!context.InputParameters.Contains("Target")) { return; }
    
        EntityReference ef = (EntityReference)context.InputParameters["Target"];
        if (ef.LogicalName != "account") { return; }
    
        Relationship relationship = (Relationship)context.InputParameters["Relationship"];
        if (relationship.SchemaName != "new_account_contact") { return; }
    
        // Get Related Entities 
        EntityReferenceCollection re = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];
    
        foreach (EntityReference rel in re)
        {
    
            Entity relatedEntity = service.Retrieve("contact", rel.Id, new ColumnSet("address1_city"));
            string city = (string)relatedEntity.Attributes["address1_city"];
            if (city=="Sydney")
            {
                //create a task
                Entity task = new Entity("task");
                task["subject"] = "A new contact from sydney is added to the account";
                task["regardingobjectid"] = ef;
                task["description"] = "A new contact from sydney is added to the account";
                // Create the task in Microsoft Dynamics CRM.
                service.Create(task);
    
            }
        }//endfor
        }
        catch (FaultException ex)
        {
            tracingService.Trace(ex.Message.ToString());
            throw new InvalidPluginExecutionException("An error occurred in the plug-in.", ex);
        }
    
    }//end function
    
  3. Add the following reference to your plugin class.
    using Microsoft.Xrm.Sdk.Query;
  4. The above code will create a task, when we add a contact with “address1_city” field equal to “Sydney” to an account. Just remember we are relating a contact to account using N:N relationship.
  5. Now change the RegisterFile.crmregister file  to register on an “Associate” message. Open the file and look for your plugin definition. It will look something like the following code.
    <Plugin Description="Plug-in to PostAccountCreate" FriendlyName="PostAccountCreate" Name="CRM2011PluginSeries.Plugins.PostAccountCreate" Id="420419bd-9fe7-404e-a5e3-afa582f21dd3" TypeName="CRM2011PluginSeries.Plugins.PostAccountCreate">
              <Steps>
                <clear />
                <Step CustomConfiguration="" Name="PostAccountCreate" Description="Post-Operation of Account Create" Id="b248f264-2b3f-e111-ab54-00155d32042e" MessageName="Create" Mode="Synchronous" PrimaryEntityName="account" Rank="1" SecureConfiguration="" Stage="PostOutsideTransaction" SupportedDeployment="ServerOnly">
                  <Images />
                </Step>
              </Steps>
            </Plugin>
  6. Now change the MessageName="Create" to MessageName="Associate" and PrimaryEntityName="account" to PrimaryEntityName="".
  7. Save the changes and deploy the plugin.
  8. If you get any errors during deployment, just deploy the plugin using plugin registeration tool. Don't forget to leave the "PrimaryEntityName" = none.
  9. Please provide some feedback

Saturday, May 12, 2012

Modifying Duplicate Detection View

Someone asked the question on CRM forum  on “ How to modify the duplicate detection view ?” Here is the answer. Duplicate detection view is combination of static and dynamic columns of an entity. Have a look at the following screen shot.

dd1

  1. “Potential duplicate records” have 2 extra columns than the “My new record” column.  These columns are Status and “Modified On”. These  columns are present on every duplicate detection view. You can’t remove them.
  2. Some of the columns in duplicate detection views come from the “Duplicate Detection Rules” set for an entity. System picks up these columns automatically. You can’t these fields either.
  3. Rest of the columns for e.g. “Primary Contact”, “Address1_City” in the the above screen shot can be changed. we can remove them. we can add more columns to the view. Here is the trick. These columns comes from the “Lookup View” of an entity. we can add/remove columns to the view. Have a look at the “Account Lookup View”.

dd2

Good Luck.

Sunday, April 29, 2012

Extending CrmSvcUtil part 2

Last month, I wrote a blog on how to extend CrmSvcUtil. The blog was about filtering the classes generated by CrmSvcUtil. Here is the link to that blog. I have updated the code to generate the early bind classes for a specific solution. Here is the code..
//<snippetBasicFilteringService>

using System;
using Microsoft.Crm.Services.Utility;
using Microsoft.Xrm.Sdk.Metadata;
using System.Collections.Generic;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Xrm.Sdk.Client;
using Microsoft.Xrm.Sdk.Discovery;
using Microsoft.Xrm.Client;
using System.Xml;
using System.Xml.Linq;
namespace CRMExtensions
{
    /// <summary>
    /// Sample extension for the CrmSvcUtil.exe tool that generates early-bound
    /// classes for custom entities.
    /// </summary>
    public sealed class BasicFilteringService : ICodeWriterFilterService
    {
        //To store the guids belongs to a specified 
        public List<Guid> li = new List<Guid>();
       
        //Put the uniquename of the solution here
        public string sSolution = "ActivityFeeds";

        public BasicFilteringService(ICodeWriterFilterService defaultService)
        {
            this.DefaultService = defaultService;

            // Call the function to generate the the guids for the solution
            getDefaultEntities();

        }

        private ICodeWriterFilterService DefaultService { get; set; }

        bool ICodeWriterFilterService.GenerateAttribute(AttributeMetadata attributeMetadata, IServiceProvider services)
        {
            return this.DefaultService.GenerateAttribute(attributeMetadata, services);
        }

        bool ICodeWriterFilterService.GenerateEntity(EntityMetadata entityMetadata, IServiceProvider services)
        {
            //if (!entityMetadata.IsCustomEntity.GetValueOrDefault()) { return false; }
            //if (entityMetadata.LogicalName!="account") { return false; }
            if (li.Contains(entityMetadata.MetadataId.Value)==false) { return false; }
            
            return this.DefaultService.GenerateEntity(entityMetadata, services);
        }

        bool ICodeWriterFilterService.GenerateOption(OptionMetadata optionMetadata, IServiceProvider services)
        {
            return this.DefaultService.GenerateOption(optionMetadata, services);
        }

        bool ICodeWriterFilterService.GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceProvider services)
        {
            return this.DefaultService.GenerateOptionSet(optionSetMetadata, services);
        }

        bool ICodeWriterFilterService.GenerateRelationship(RelationshipMetadataBase relationshipMetadata, EntityMetadata otherEntityMetadata,
        IServiceProvider services)
        {
            return this.DefaultService.GenerateRelationship(relationshipMetadata, otherEntityMetadata, services);
        }

        bool ICodeWriterFilterService.GenerateServiceContext(IServiceProvider services)
        {
            return this.DefaultService.GenerateServiceContext(services);
        }

        // This function will add guids of solution entities
        public void getDefaultEntities()
        {
            //you can put this in the config file
            //change the connection details with your crm details
            var connection = CrmConnection.Parse("Url=https://org.crm5.dynamics.com; Username=user@live.com; Password=password; DeviceID=deviceid; DevicePassword=password");
            var context = new CrmOrganizationServiceContext(connection);
                         
            QueryExpression query = new QueryExpression()
            {
                Distinct = false,
                EntityName = "solutioncomponent",
                ColumnSet = new ColumnSet(true),
                LinkEntities = 
            {
                new LinkEntity 
                {
                    JoinOperator = JoinOperator.Inner,
                    LinkFromAttributeName = "solutionid",
                    LinkFromEntityName = "solutioncomponent",
                    LinkToAttributeName = "solutionid",
                    LinkToEntityName = "solution",
                    LinkCriteria = 
                    {
                        Conditions = 
                        {
                            new ConditionExpression("uniquename", ConditionOperator.Equal, sSolution)
                        }
                    }
                }
            },
                Criteria =
                {
                    Filters = 
                {
                    new FilterExpression
                    {
                        FilterOperator = LogicalOperator.And,
                        Conditions = 
                        {
                            new ConditionExpression("componenttype", ConditionOperator.Equal, 1)

                        },
                    }
                }
                }
            };

            EntityCollection ec = context.RetrieveMultiple(query);

            foreach (Entity entity in ec.Entities)
            {
                Guid id = (Guid)entity.Attributes["objectid"];
                li.Add(id);

            }
        }//end function
    }
    //</snippetBasicFilteringService>
}

Please provide some feedback.

Friday, April 27, 2012

How to update the parent record when children records are created or updated

Few months ago, I wrote a blog on “How to update the children records when parent record is updated”. Here is link to that blog. In this blog I am posting a code on “How to update the parent record when child record is created or updated”. The scenario is that I want to display the sum of total of "Estimated Revenue” of the opportunities on the parent account record. Here are the steps
  • Create a new attribute named “new_oppamount” on the account entity and place it on the account entity form.
  • Publish the account entity
  • Create the plugin
  • Register this plugin on postcreate, postupdate , postsetstate and postsetstatedynamic  events of opportunity entity.
Here is the code. The code is using the fetch xml to get the sum of “Estimated Revenue” field of the open opportunities. Follow the steps mentioned in here and replace the ExecutePostAccountUpdateContacts method with follwing method
protected void ExecutePostOpportunityCreate(LocalPluginContext localContext)
{
    if (localContext == null)
    {
        throw new ArgumentNullException("localContext");
    }
            

    IPluginExecutionContext context = localContext.PluginExecutionContext;

    //Get a IOrganizationService
    IOrganizationService service = localContext.OrganizationService;

    //create a service context
    var ServiceContext = new OrganizationServiceContext(service);
    //ITracingService tracingService = localContext.TracingService;

    // The InputParameters collection contains all the data passed in the message request.
    if (context.InputParameters.Contains("Target") &&
    context.InputParameters["Target"] is Entity)
    {
        // Obtain the target entity from the input parmameters.
        Entity entity = (Entity)context.InputParameters["Target"];

        //get the customerid
        EntityReference a = (EntityReference)entity.Attributes["customerid"];
                
        decimal totalAmount=0;
                
        try
        {   
            //fetchxml to get the sum total of estimatedvalue
            string estimatedvalue_sum = string.Format(@" 
            <fetch distinct='false' mapping='logical' aggregate='true'> 
                <entity name='opportunity'> 
                    <attribute name='estimatedvalue' alias='estimatedvalue_sum' aggregate='sum' /> 
                    <filter type='and'>
                        <condition attribute='statecode' operator='eq' value='Open' />
                            <condition attribute='customerid' operator='eq' value='{0}' uiname='' uitype='' />
                    </filter>
                </entity>
            </fetch>", a.Id);
            EntityCollection estimatedvalue_sum_result = service.RetrieveMultiple(new FetchExpression(estimatedvalue_sum));

            foreach (var c in estimatedvalue_sum_result.Entities)
            {
                totalAmount = ((Money)((AliasedValue)c["estimatedvalue_sum"]).Value).Value;
            }
                    
            //updating the field on the account
            Entity acc = new Entity("account");
            acc.Id = a.Id;
            acc.Attributes.Add("new_oppamount", new Money(totalAmount));
            service.Update(acc);
                    
                    
        }
        catch (FaultException ex)
        {
                throw new InvalidPluginExecutionException("An error occurred in the plug-in.", ex);
        }
    }

}
Good Luck…

Sunday, April 15, 2012

CRM 2011 Postcode Lookup Solution Version 2.0

This is an extension of a blog I wrote back in June 2011. Here is the link to that blog. There were a few cons to that solution. One of the biggest cons was that the solution was hard coded to fill, only the built-in adress1_city, address1_stateorprovince, address1_country fields. I have updated the code to fill the city/suburb, state and country values to any field including custom fields. These fields have to be text fields.
The updated solution will work with the existing built-in entities as well as the custom entities.Here is the link to download the solution.
The solution consists of 4 files:
  • new_Json2.js (Javascript webresource)
  • new_PostCodeScript(Javascript webresource)
  • new_postcode(custom entity to store postcode information)
  • new_SuburbOptions (HTML webresource)

How does it work:

  • User enters the postcode on postal code field and on onchange event of the field. The solution will retrieve postcode records related to entered postcode.
  • If there is only one record related to entered postcode, the solution will populate the state, suburb and country fields for you.
  • If there are more than one records related to entered postcode, the solution will prompt you to select the appropriate entry.
In the following screen shot, I entered 2000 in postal code field and system prompt to pick the relevant city/suburb.
image

Installation Directions:

  1. Install the postcode solution.
  2. Open the CRM entity form in customization mode.
  3. Double click on attribute that represents the postalcode attribute.
  4. Add new_Json2.js and new_PostCodeScript to form libraries.
  5. Call loadPostCodeRequest function from new_PostCodeScript. Check the  “Pass execution context as first parameter” and pass the 3 attributes that represent the city, state and country attributes in “Comma separated list of parameters that will be passed to the function” as shown in the following screen shot.event
  6. Save the changes and publish the entity.
  7. Test the solution.

Thursday, March 22, 2012

Extending CrmSvcUtil (Filtering the generated classes)

Most people use the CrmSvcUtil  in its native form. But CrmSvcUtil exposes some interfaces to extend its functionality. Here is the list of all the extension interfaces. One of my personal favourite is ICodeWriterFilterService. This interface can control which entity/attribute/relationship etc. would be created by CrmSvcUtil. By default, CrmSvcUtil generates all the entities, attributes and relationships. If you run the CrmSvcUtil for an out of box CRM online organization, it will generate a file of around 5 mb with all the entities and attributes. In reality we need to generate very few entities for our solutions. We do not need all the extra code generated by the utility. The size of the file is even more important when we are writing code for CRM online. In this blog, we are going to learn how to filter the entities generated by the code generation utility.
Here are the steps:
  1. Open up the solution we created in our last blog.
  2. Add a new class file to the project. I named my class file BasicFilteringService.cs. I copied this file from the sdk samples. Here is the code. The code will restrict the code generation utility to generate just the custom entities.
    using System;
    using Microsoft.Crm.Services.Utility;
    using Microsoft.Xrm.Sdk.Metadata;
    namespace CRMExtensions
    {
        /// 
        /// Sample extension for the CrmSvcUtil.exe tool that generates early-bound
        /// classes for custom entities.
        /// 
        public sealed class BasicFilteringService : ICodeWriterFilterService
        {
            public BasicFilteringService(ICodeWriterFilterService defaultService)
            {
                this.DefaultService = defaultService;
            }
    
            private ICodeWriterFilterService DefaultService { get; set; }
    
            bool ICodeWriterFilterService.GenerateAttribute(AttributeMetadata attributeMetadata, IServiceProvider services)
            {
                return this.DefaultService.GenerateAttribute(attributeMetadata, services);
            }
    
            bool ICodeWriterFilterService.GenerateEntity(EntityMetadata entityMetadata, IServiceProvider services)
            {
                if (!entityMetadata.IsCustomEntity.GetValueOrDefault()) { return false; }
                
                return this.DefaultService.GenerateEntity(entityMetadata, services);
            }
    
            bool ICodeWriterFilterService.GenerateOption(OptionMetadata optionMetadata, IServiceProvider services)
            {
                return this.DefaultService.GenerateOption(optionMetadata, services);
            }
    
            bool ICodeWriterFilterService.GenerateOptionSet(OptionSetMetadataBase optionSetMetadata, IServiceProvider services)
            {
                return this.DefaultService.GenerateOptionSet(optionSetMetadata, services);
            }
    
            bool ICodeWriterFilterService.GenerateRelationship(RelationshipMetadataBase relationshipMetadata, EntityMetadata otherEntityMetadata,
            IServiceProvider services)
            {
                return this.DefaultService.GenerateRelationship(relationshipMetadata, otherEntityMetadata, services);
            }
    
            bool ICodeWriterFilterService.GenerateServiceContext(IServiceProvider services)
            {
                return this.DefaultService.GenerateServiceContext(services);
            }
        }
        //
    }
    
  3. Add codewriterfilter parameter to CrmSvcUtil.exe.config file. <add key="codewriterfilter" value="CRMExtensions.BasicFilteringService, CRMExtensions"/>
  4. Press F5 to debug the solution. It will generate a file  with the name specified in “o” or output parameter.
  5. Check the size of the file. It will be very very small as compare to the file generated without codewriterfilter. The size of the file created for default CRM online  organization with codewriterfilter is just 44 kb as compared to 4.7 mb without codewriterfilter.
Note if we replace the following line in BasicFilteringService.cs
if (!entityMetadata.IsCustomEntity.GetValueOrDefault()) { return false; }
with
if (entityMetadata.LogicalName!="account") { return false; }
The utility will generate just account entity. Happy programming.