Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

One of my favorite features of CRM 4.0 is the ability to extend the application using .NET, specifically interacting with other websites through the use of buttons, menu options, and other ISV configuration points.  This has allowed me to develop “Wizards” and other pieces of functionality inside of CRM to help users accomplish certain tasks and enter in more accurate information in CRM. There are a lot of posts on the Internet about how to clone records in CRM. 

I’ve found a pretty simple way of cloning records as well and it really just requires a few lines of .NET code and an .aspx webpage.  Essentially, if you want to clone an exact copy of a record all you need to do is retrieve the Entity from CRM, loop through the properties and remove all references to KeyProperties.  Once you remove all the Keys from the Entity you can call the Create method of the web service.  It will generate a new key and you have a cloned record in CRM.

In the rest of the post, I will walk through setting up the .aspx web page, the required .NET code and how to tie it into CRM with the ISV.Config file.  For another little twist to the project, I used AJAX controls to give a visual indication to the end user that the cloning process is executing.

Step 1:  Setup your web project in Visual Studio 2005 or 2008 (this code was written in VS 2005)
In Visual Studio create a new Web Site project.  One of the options you will have is to setup an AJAX Enabled Web Site.  This ensures that all of the necessary web.config entries are included in your Website in order for the AJAX functionality to work properly.

*If you don’t see this option, then you need to install the AJAX Toolkit for Visual Studio 2005.  The following URL provides information on where to download it and how to install it.
http://naveedmazhar.wordpress.com/2008/01/10/install-ajax-on-machines-running-visual-studio-2005/

Step 2:  Setup your .aspx web page to use the AJAX Controls
1. Add a new .aspx page to your website solution, call it:  Clone.aspx
2. Add references to the following .dll files included in the SDK
      a. Microsoft.Crm.Sdk
      b. Microsoft.Crm.SdkTypeProxy
3. At the top of the code behind page add:
      a. Using Microsoft.Crm.Sdk;
      b. Using Microsoft.Crm.Sdk.Query;
      c. Using Microsoft.Crm.SdkTypeProxy;
4. From the Design Editor of the page, add the following AJAX controls to the page (in this order)
      a. ScriptManager
      b. UpdatePanel
      c. UpdateProgress
5. Drag the following .NET Controls inside of the UpdatePanel object
      a. .NET Button
            i. Enabled = true
            ii. Visible = false
            iii. Name =  Button1
      b. .NET Hyperlink
            i. Enabled = true
            ii. Visible = false
            iii. Name = Hyperlink1
6. Ascentium created a handy animated .gif for CRM 4.0, it looks great and works very well
      a. http://www.ascentium.com/blog/crm/Post189.aspx
7. Drag the animated_2.gif file into the UpdateProgress section of the web page
8. Underneath the animated .gif, type Please Wait…Cloning
9. Your page should now look like the following:

Step 3:  Add the necessary code behind for the Clone button functionality
1. Add the following code to the Button1_OnClick()
protected void Button1_Click(object sender, EventArgs e)
 {
  //Parse Query String Variables
  if(Request.QueryString.Count > 0)
  {
  string _org = Request["orgname"].ToString();
  Guid _entityid = new Guid(Request["id"].ToString());
  string _entitytypename = Request["typename"].ToString();
  
  //Setup Target Retrieve Request
  TargetRetrieveDynamic _entitytoclone = new TargetRetrieveDynamic();
  _entitytoclone.EntityName = _entitytypename;
  _entitytoclone.EntityId = _entityid;
  
   
  //Execute Retrieve and Clone Entity
  try
  {
   DynamicEntity _entity;
   RetrieveResponse _response;
   RetrieveRequest _request = new RetrieveRequest();

//We want all columns that contain data, so use AllColumns()
   _request.ColumnSet = new AllColumns();

//We want to use the Dynamic Entity object
   _request.ReturnDynamicEntities = true;
   _request.Target = _entitytoclone;
   
   //Retrieve our Entity To Clone
   CrmAuthenticationToken _token = new CrmAuthenticationToken();
   _token.OrganizationName = _org;
   _token.AuthenticationType = 0;
   
   CrmService _service = new CrmService();
   _service.PreAuthenticate = false;
   _service.CrmAuthenticationTokenValue = _token;
   _service.Credentials = new System.Net.NetworkCredential("administrator", "pass@word1");
   _service.Url = "http://MOSS:5555/mscrmservices/2007/crmservice.asmx";
   
   _response = (RetrieveResponse)_service.Execute(_request);
   _entity = (DynamicEntity)_response.BusinessEntity;
   
  
   //Remove The Key Properties (CustomerAddressID etc)
   //Otherwise, it will throw an error about a PK Violation in SQL
   foreach(Property p in _entity.Properties)
   {
    if(p is KeyProperty)
    {
     _entity.Properties.Remove(p.Name);
    }
   }
   
   //Create Our Cloned Entity
   Guid _newguid = (Guid)_service.Create(_entity);
   string _url = "";
   HyperLink1.Text = "Record Cloned Successfully!  Click Here To Open";
   _url = SetLinkButtonProperties(_entityid.ToString(),_entitytypename,_org);
   HyperLink1.Attributes.Add("onclick","window.open('" + _url + "');window.close();");
   HyperLink1.Visible = true;   
_service.Dispose();
  }
  catch(Exception x)
  {
   //Handle the Exception
  }  
  }
 }

Step 4:  Add the following function to the code behind as well, this will get the URL of our newly created record.

private string SetLinkButtonProperties(string id,string name,string org)
 {
  string path = "../../../";
   switch(name)
   {
    case "account":
     path += org + "/sfa/accts/edit.aspx?id=" + id;
    break;
    case "contact":
     path += "/sfa/conts/edit.aspx?id=" + id;
    break;
    case "opportunity":
     path += "/sfa/conts/edit.aspx?id=" +id;
    break;
    case "lead":
     path += "/sfa/lead/edit.aspx?id=" + id;
    break;
    case "incident":
     path += "/cs/cases/edit.aspx?id=" + id;
    break;
    default:
     path += "/userdefined/edit.aspx?id=" + id + "&etn=" + name; 
    break;
   }
   return path;
 }

Step 5:  Add the following Script block to the .aspx page.  This will cause our Clone button to automatically trigger a postback when the page is loaded.

<script language='javascript' type="text/javascript">
    var _isInitialLoad = true;
    function pageLoad(sender, args)
    {
        if(_isInitialLoad)
        {
            _isInitialLoad = false;
            //  simulate a button click by forcing the postback
            //  causing the updatepanel to update
            __doPostBack('<%= this.Button1.ClientID %>','');       
        }
    }
</script>

Step 6:  Deploy your custom web project to the ISV Folder or to its own individual website. 
In this example, I’ve deployed it to the ISV folder underneath the CRM Website.  I added a new folder underneath the ISV folder called:  CRMExtensions and copied my code files to that location. I then converted the CRMExtensions folder to a Virtual Directory.

Step 7: Update the ISV.Config File
Deploy the following XML to every entity where the cloning button should be enabled in the ISV.Config file; in this sample it’s deployed to the Account Entity.

<Entities>
        <Entity name="account">
          <!-- The Account Tool Bar -->
          <ToolBar ValidForCreate="0" ValidForUpdate="1">
            <Button Icon="/_imgs/ico_18_debug.gif" Url="/isv/crmextensions/clone.aspx" PassParams="1" WinParams="dialogHeight:100px;dialogWidth:300px;" WinMode="1">
              <Titles>
                <Title LCID="1033" Text="Clone Account" />
              </Titles>
              <ToolTips>
                <ToolTip LCID="1033" Text="Clone Account" />
              </ToolTips>
            </Button>
          </ToolBar>
        </Entity>
</Entities>

Step 8:  Testing
Now we’re ready to test.  Browse to an Account in CRM and you should see the Clone Account button on the Account screen.

When you click on the Button, it should open up our Clone Window, complete with the Progress information.

Once the cloning process has completed, our Hyperlink now appears and the user can open the cloned Account record. 

 

Also, if we look in our list of Accounts, we will see our Account Listed twice now as well.

 

While this method only does a basic clone, you can extend this functionality further to allow for users to enter in a new Name or other details as well.  This cloning method also works well for cloning related entities along with the main entity.  That may come in a later post however as it’s a little more involved.  You can also download the solution files and the ISV.Config sample.

Jeremy

** This posting is provided "AS IS" with no warranties, and confers no rights.

 

Attachment: CloningSample.zip
Published 04-27-2009 12:47 PM by Jeremy Winchell

Comments

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Thursday, May 14, 2009 3:19 PM by jodi.cannon@mhctruck.com

This is a brilliant solutions.  I, though admitedly a novice, am trying to copy.  I cannot seem to find microsoft.crm.query.dll anywhere.  The other two dll were contained in the CRM SDK.  Where can I get microsoft.crm.query?

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Thursday, May 14, 2009 3:40 PM by Jeremy Winchell

Jodi,

The Microsoft.Crm.Query is actually enclosed in the Microsoft.Crm.Sdk.dll.  You will add two .dll's (Crm.Sdk & Crm.SdkTypeProxy) as references in your project.  Then at the top of your page you will use the using statement to get to the .Query functions.

ex:

using Microsoft.Crm.Sdk;

using Microsoft.Crm.Sdk.Query

using Microsoft.Crm.SdkTypeProxy;

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Wednesday, July 22, 2009 2:13 AM by Azazello

Hi. I've developed a custom aspx page which use scriptcontrol component e.t.c.

Everything works ideally before I deploy this page onto MS CRM WebSite. When I try to open developed page I receive an error and in log I see following Exception:

[2009-07-08 17:19:28.7] Process: w3wp |Organization:32d223cf-7d19-de11-b449-005056885faf |Thread:    5 |Category: Application |User: 00000000-0000-0000-0000-000000000000 |Level: Error | ErrorInformation.LogError

>MSCRM Error Report:

--------------------------------------------------------------------------------------------------------

Error: Unknown server tag 'asp:ScriptManager'.

Error Message: An error occurred during the parsing of a resource required to service this request.   Please review the following specific parse error details and modify your source file appropriately.

Error Details: Unknown server tag 'asp:ScriptManager'.

Source File:  /Winner-Automotive/ISV/Kladr/Default.aspx

Line Number: 13

Request URL: sql-crm/.../Default.aspx

Stack Trace Info: [HttpException: Unknown server tag 'asp:ScriptManager'.]

  at System.Web.UI.TagPrefixTagNameToTypeMapper.System.Web.UI.ITagNameToTypeMapper.GetControlType(String tagName, IDictionary attribs)

  at System.Web.UI.MainTagNameToTypeMapper.GetControlType2(String tagName, IDictionary attribs, Boolean fAllowHtmlTags)

  at System.Web.UI.MainTagNameToTypeMapper.GetControlType(String tagName, IDictionary attribs, Boolean fAllowHtmlTags)

  at System.Web.UI.RootBuilder.GetChildControlType(String tagName, IDictionary attribs)

  at System.Web.UI.ControlBuilder.CreateChildBuilder(String filter, String tagName, IDictionary attribs, TemplateParser parser, ControlBuilder parentBuilder, String id, Int32 line, VirtualPath virtualPath, Type& childType, Boolean defaultProperty)

  at System.Web.UI.TemplateParser.ProcessBeginTag(Match match, String inputText)

  at System.Web.UI.TemplateParser.ParseStringInternal(String text, Encoding fileEncoding)

[HttpParseException: Unknown server tag 'asp:ScriptManager'.]

  at System.Web.UI.TemplateParser.ProcessException(Exception ex)

  at System.Web.UI.TemplateParser.ParseStringInternal(String text, Encoding fileEncoding)

  at System.Web.UI.TemplateParser.ParseString(String text, VirtualPath virtualPath, Encoding fileEncoding)

[HttpParseException: Unknown server tag 'asp:ScriptManager'.]

  at System.Web.UI.TemplateParser.ParseString(String text, VirtualPath virtualPath, Encoding fileEncoding)

  at System.Web.UI.TemplateParser.ParseReader(StreamReader reader, VirtualPath virtualPath)

  at System.Web.UI.TemplateParser.ParseFile(String physicalPath, VirtualPath virtualPath)

  at System.Web.UI.TemplateParser.ParseInternal()

  at System.Web.UI.TemplateParser.Parse()

  at System.Web.UI.TemplateParser.Parse(ICollection referencedAssemblies, VirtualPath virtualPath)

  at System.Web.Compilation.BaseTemplateBuildProvider.get_CodeCompilerType()

  at System.Web.Compilation.BuildProvider.GetCompilerTypeFromBuildProvider(BuildProvider buildProvider)

  at System.Web.Compilation.BuildProvidersCompiler.ProcessBuildProviders()

  at System.Web.Compilation.BuildProvidersCompiler.PerformBuild()

  at System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath)

  at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)

  at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)

  at System.Web.Compilation.BuildManager.GetVirtualPathObjectFactory(VirtualPath virtualPath, HttpContext context, Boolean allowCrossApp, Boolean noAssert)

  at System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(VirtualPath virtualPath, Type requiredBaseType, HttpContext context, Boolean allowCrossApp, Boolean noAssert)

  at System.Web.UI.PageHandlerFactory.GetHandlerHelper(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)

  at System.Web.UI.PageHandlerFactory.System.Web.IHttpHandlerFactory2.GetHandler(HttpContext context, String requestType, VirtualPath virtualPath, String physicalPath)

  at System.Web.HttpApplication.MapHttpHandler(HttpContext context, String requestType, VirtualPath path, String pathTranslated, Boolean useAppConfig)

  at System.Web.HttpApplication.MapHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()

  at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Could you help me make this mechanism work?

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Friday, July 31, 2009 5:36 PM by Jeremy Winchell

Are the AJAX libraries registered in the GAC on the server?  Also, is the AJAX tools library in the BIN folder of the application?

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Thursday, January 14, 2010 10:34 AM by cj.brooks

This appears to work perfectly, however when I use this to copy a Opportunity I receive an error message. The opportunity then appears to have been created but the Line Product Items have not been copied across.

The Error Message I receive is

Microsoft CRM Error Report:

Error Description:

contact With Id = 13ead1fe-2801-df11-bf3c-00155dc90700 Does Not Exist

Error Details:

contact With Id = 13ead1fe-2801-df11-bf3c-00155dc90700 Does Not Exist

Full Stack:

[CrmObjectNotFoundException: contact With Id = 13ead1fe-2801-df11-bf3c-00155dc90700 Does Not Exist]

  at Microsoft.Crm.BusinessEntities.BusinessProcessObject.Retrieve(BusinessEntityMoniker moniker, EntityExpression entityExpression, ExecutionContext context, Int32[] deletionCodes, Int32 languageCode)

  at Microsoft.Crm.BusinessEntities.BusinessProcessObject.Retrieve(BusinessEntityMoniker moniker, EntityExpression entityExpression, ExecutionContext context, Int32[] deletionCodes)

  at Microsoft.Crm.BusinessEntities.BusinessProcessObject.Retrieve(BusinessEntityMoniker moniker, EntityExpression entityExpression, ExecutionContext context)

[TargetInvocationException: Exception has been thrown by the target of an invocation.]

  at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, SignatureStruct& sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)

  at System.RuntimeMethodHandle.InvokeMethodFast(Object target, Object[] arguments, Signature sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner)

  at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks)

  at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)

  at System.Web.Services.Protocols.LogicalMethodInfo.Invoke(Object target, Object[] values)

  at Microsoft.Crm.Extensibility.InternalOperationPlugin.Execute(IPluginExecutionContext context)

  at Microsoft.Crm.Extensibility.PluginStep.Execute(PipelineExecutionContext context)

  at Microsoft.Crm.Extensibility.Pipeline.Execute(PipelineExecutionContext context)

  at Microsoft.Crm.Extensibility.MessageProcessor.Execute(PipelineExecutionContext context)

  at Microsoft.Crm.Extensibility.InternalMessageDispatcher.Execute(PipelineExecutionContext context)

  at Microsoft.Crm.Extensibility.ExternalMessageDispatcher.Execute(String messageName, Int32 primaryObjectTypeCode, Int32 secondaryObjectTypeCode, PropertyBag fields, CorrelationToken correlationToken, CallerOriginToken originToken, UserAuth userAuth, Guid callerId)

  at Microsoft.Crm.Sdk.RequestBase.Process(Int32 primaryObjectTypeCode, Int32 secondaryObjectTypeCode, CorrelationToken correlationToken, CallerOriginToken originToken, UserAuth userAuth, Guid callerId)

  at Microsoft.Crm.Sdk.RequestBase.Process(CorrelationToken correlationToken, CallerOriginToken originToken, UserAuth userAuth, Guid callerId)

  at Microsoft.Crm.Sdk.CrmServiceInternal.Execute(RequestBase request, CorrelationToken correlationToken, CallerOriginToken originToken, UserAuth userAuth, Guid callerId)

  at Microsoft.Crm.Sdk.InProcessCrmService.Execute(Object request)

  at Microsoft.Crm.Application.Platform.ServiceCommands.PlatformCommand.ExecuteInternal()

  at Microsoft.Crm.Application.Platform.ServiceCommands.RetrieveCommand.Execute()

  at Microsoft.Crm.Application.Platform.DataSource.Retrieve(String entityName, Guid entityId, String[] columns, Boolean retrieveLatest, Boolean useSystemUserContext)

  at Microsoft.Crm.Application.Platform.EntityProxy.Retrieve(String columnSet)

  at Microsoft.Crm.Application.Forms.AppForm.RaiseDataEvent(FormEventId eventId)

  at Microsoft.Crm.Application.Forms.EndUserForm.Initialize(Entity entity)

  at Microsoft.Crm.Application.Forms.CustomizableForm.Execute(Entity entity, String formType)

  at Microsoft.Crm.Application.Forms.CustomizableForm.Execute(Entity entity)

  at Microsoft.Crm.Web.SFA.ContactDetailPage.ConfigureForm()

  at Microsoft.Crm.Application.Controls.AppUIPage.OnPreRender(EventArgs e)

  at System.Web.UI.Control.PreRenderRecursiveInternal()

  at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

[HttpUnhandledException: Exception of type 'System.Web.HttpUnhandledException' was thrown.]

  at System.Web.UI.Page.HandleError(Exception e)

  at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

  at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

  at System.Web.UI.Page.ProcessRequest()

  at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)

  at System.Web.UI.Page.ProcessRequest(HttpContext context)

  at ASP.sfa_conts_edit_aspx.ProcessRequest(HttpContext context)

  at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()

  at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Other Message:

Error Number:

0x80040217

Source File:

Not available

Line Number:

Not available

Date: 01-14-2010

Time: 11:34:16

Server: mcssv008

Request URL:

mcssv008/.../edit.aspx

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Thursday, January 14, 2010 10:50 AM by Jeremy Winchell

cj,

The version of the cloning utility only handles the main record, it doesn't clone child records.  If you want to clone the line items for the Opportunity you would need to update the code to do the following:

1. Clone the Opportunity first

2. Retrieve all Opportunity Items for the original Opportunity

3. Loop through each of those and remove the Key Value (opportunityproductid) and update the Opportunity to the GUID of the new one created in Step 1.

The error message that you are getting is that showing up in the CRM UI or in the cloning window?

# re: Cloning Records In Microsoft CRM 4.0 Using AJAX Controls

Tuesday, January 26, 2010 9:38 AM by jehuebner

Excellent post. How can this be adapted for CRM 3.0? Am currently working on salesorder cloning function.

thx