Wednesday, September 02, 2009

Supported Record Counter For Microsoft Dynamics CRM 4.0

I've already published code of record counter for Microsoft Dynamics CRM 4.0 in this post. Those record counter worked but it had some issues (lookups, advanced finds, form assistant).

I've upgraded the code and now it works perfectly.


UPD: according to comment of Moti Mendelovich to this thread i've one more time updated the code of the plugin and now it works even count of records in fetch response greater then 5000.

Code:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Win32;
using System.Xml;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Metadata;

namespace RecordCounter
{
public class ExecuteHandler : IPlugin
{

#region IPlugin Members

public void Execute(IPluginExecutionContext context)
{
if (context.Depth != 1) //To calculate count of pages and records another one fetch will be executed
return;//so to avoid infinite loops i need to check the depth of request - if it more then 2 - return

if (context.MessageName == "Execute" && context.InputParameters.Contains("FetchXml"))
{
XmlDocument indoc = new XmlDocument();
indoc.LoadXml((string)context.InputParameters["FetchXml"]);

//Retrieve name of entity to display
string entityName = indoc.SelectSingleNode("//fetch/entity").Attributes["name"].InnerText;

if (entityName == EntityName.savedquery.ToString() ||//To make Advanced Find Work
entityName == EntityName.businessunitnewsarticle.ToString() ||//To make Literature work
entityName == EntityName.resource.ToString() ||//To make Service calendar work
entityName == EntityName.systemuser.ToString() ||//To make Service calendar work
entityName == EntityName.equipment.ToString() ||//To make Service calendar work
entityName == EntityName.asyncoperation.ToString())
return;

//Creation of Metadata service - it will be need for retrieving of main attribute of entity
IMetadataService mservice = context.CreateMetadataService(false);

RetrieveEntityRequest request = new RetrieveEntityRequest();
request.RetrieveAsIfPublished = false;
request.LogicalName = entityName;
request.EntityItems = EntityItems.EntityOnly;
string primaryFieldName = ((RetrieveEntityResponse)mservice.Execute(request)).EntityMetadata.PrimaryField;
//CrmService Creation
ICrmService crmService = context.CreateCrmService(true);

//Count of records by page - for calculation of pages count
int pagecount = int.Parse(indoc.DocumentElement.Attributes["count"].InnerText);

//I remove this attributes for retrieve of all records in current view
indoc.DocumentElement.Attributes.Remove(indoc.DocumentElement.Attributes["count"]);
indoc.DocumentElement.Attributes.Remove(indoc.DocumentElement.Attributes["page"]);

foreach (XmlNode node in indoc.SelectNodes("//fetch/entity/attribute"))
indoc.SelectSingleNode("//fetch/entity").RemoveChild(node);

foreach (XmlNode node in indoc.SelectNodes("//fetch/entity/order"))
indoc.SelectSingleNode("//fetch/entity").RemoveChild(node);

foreach (XmlNode node in indoc.SelectNodes("//fetch/entity/link-entity"))
foreach(XmlNode subnode in node.SelectNodes("./attribute"))
node.RemoveChild(subnode);

XmlAttribute aggrAttr = indoc.CreateAttribute("aggregate");
aggrAttr.Value = "true";
indoc.DocumentElement.Attributes.Append(aggrAttr);

XmlNode field = indoc.CreateNode(XmlNodeType.Element, "attribute", null);

XmlAttribute nameAttr = indoc.CreateAttribute("name");
nameAttr.Value = string.Format("{0}id", (entityName == EntityName.activitypointer.ToString() ? "activity" : entityName));
field.Attributes.Append(nameAttr);

XmlAttribute aggregateAttr = indoc.CreateAttribute("aggregate");
aggregateAttr.Value = "count";
field.Attributes.Append(aggregateAttr);

XmlAttribute aliasAttr = indoc.CreateAttribute("alias");
aliasAttr.Value = "C";
field.Attributes.Append(aliasAttr);

indoc.SelectSingleNode("//fetch/entity").AppendChild(field);

//Xml of full result (without paging)
string fullResult = crmService.Fetch(indoc.OuterXml);

XmlDocument fullResultDocument = new XmlDocument();
fullResultDocument.LoadXml(fullResult);

//Total record count by fetch
int totalRecordCount = int.Parse(fullResultDocument.SelectSingleNode("//resultset/result/C").InnerText);
int totalPageCount = (totalRecordCount / pagecount) + ((totalRecordCount % pagecount) == 0 ? 0 : 1);

string result = string.Format("Total records = {0}, Total pages = {1}", totalRecordCount, totalPageCount);

//Result XML which is the result shown in Grid
XmlDocument outdoc = new XmlDocument();
outdoc.LoadXml((string)context.OutputParameters["FetchXmlResult"]);

//Creation of record which will show totals
XmlNode ResultNodeText = outdoc.CreateNode(XmlNodeType.Element, primaryFieldName, null);
ResultNodeText.InnerText = result;

XmlNode ResultNodeId = outdoc.CreateNode(XmlNodeType.Element, string.Format("{0}id", (entityName == EntityName.activitypointer.ToString() ? "activity" : entityName)), null);
ResultNodeId.InnerText = Guid.Empty.ToString();

XmlNode res = outdoc.CreateNode(XmlNodeType.Element, "result", null);
res.AppendChild(ResultNodeText);
res.AppendChild(ResultNodeId);

XmlNode ResultNodeType;

//Following code repair icon for record counter icon
if (entityName == EntityName.activitypointer.ToString())
{
ResultNodeType = outdoc.CreateNode(XmlNodeType.Element, "activitytypecode", null);
ResultNodeType.InnerText = "4212";
res.AppendChild(ResultNodeType);
}

//This code repair report view
if (entityName == EntityName.report.ToString())
{
ResultNodeType = outdoc.CreateNode(XmlNodeType.Element, "reporttypecode", null);
ResultNodeType.InnerText = "1";
res.AppendChild(ResultNodeType);
}

//Adding record with label of count of pages and records as a first record in recordset
outdoc.SelectSingleNode("//resultset").InsertBefore(res, outdoc.SelectSingleNode("//resultset").FirstChild);
context.OutputParameters["FetchXmlResult"] = outdoc.OuterXml;
}
}

#endregion

}
}


Registration of the step for this plugin:



And screenshots:

Advanced find:



Lookup:



Public and private views:



Quick find:



Form assistant:



Auto resolve lookup view:



Source code you can download here.

40 comments:

  1. Hi A33ik,

    i tried your code, and it really works great in Accounts form and Account related advanced find and all...but it is showing an empty record instaed of total number of records and pages for other entities like Contact,Activity,Leads etc.Can you tell me where i am wrong

    ReplyDelete
  2. I see. Try to add primary field of to the views (for contact this is full name field, for activity - subject, e.t.c.) One of the features of this counter )

    ReplyDelete
  3. yes a33ik,

    it worked now ..

    thanks :-).

    ReplyDelete
  4. Hi a33ik,

    Thank you very much for this record counter it works perfectly!

    I am learning a lot about plugins from this.

    Please keep up the good work!

    ReplyDelete
  5. Record count is showing in first row of grid. But I want to show it right near 1 of 50 Seleted, below the grid

    ReplyDelete
  6. This can be done only with unsupported customization. You can buy such counter here - http://geek.hubkey.com/2009/02/ms-dynamics-crm-40-record-counter-page.html

    ReplyDelete
  7. Hi,
    When i try to register the dll,a error message occured.

    Unhandled Exception: System.Web.Services.Protocols.SoapException: Server was unable to process request.
    PluginRegistrationTool.PluginRegistrationForm.btnRegister_Click(Object sender, EventArgs e)

    ReplyDelete
  8. This is strange. Have you build your own project or used mine?

    ReplyDelete
  9. Hi A33IK,

    its quiet strange taht when i tried your updated code i am getting an error message i all my crm pages.!! i used the same code given here and registered it the same way you have shown here! Can you please tell me what can be the reason...

    ReplyDelete
  10. Have you strongnamed your library?

    ReplyDelete
  11. Andriy, thank you for excellent sample!

    ReplyDelete
  12. Great code, can it be disabled from appearing in the Auto resolve lookup view?

    ReplyDelete
  13. I haven't found how to do it but you can try.

    ReplyDelete
  14. I registered the dll. See screen http://www.myimg.de/?img=bild157a08.jpg

    But I see no line with count or anything!

    Any ideas?

    ReplyDelete
  15. You have to register step as shown here - http://1.bp.blogspot.com/_73OmG38HHME/Sp7F402FbBI/AAAAAAAAAQ4/ioZBhrwJjTI/s1600-h/StepRegistration.JPG

    ReplyDelete
  16. Andriy - First off, this is a fantastic plugin! It was simple to register and get working. Though I think I found an odd bug. After registering, my users all had issues using our mail merge templates. I am thinking that the mail merge is reading that record you are using to display the count.

    After unregistering it, they were able to perform the MM so I'm think it is that. Do you have any advice as to how I could work around that?

    ReplyDelete
  17. Hi, Mayhem. I've checked mail merge and definitely my plugin breaks it... Add lines

    if (indoc.DocumentElement.Attributes["count"] == null) return;

    after XML load, rebuild plugin, reregister it. This will fix mail merge.

    ReplyDelete
  18. Andriy,

    Awesome code - very neat functionality. We ran into an issue with certain srs reports after deploying this plugin. Anyone experience that?

    ReplyDelete
  19. Could you send me details of those errors at a33ik@bigmir.net?

    ReplyDelete
  20. The hands of health. For large a gap to ensure Crm

    ReplyDelete
  21. Cool plugin. I have a small problem though...

    As soon as I add the new step for this plugin, some queries to the CRM web service break - 'Server is unable to process request'.

    It only seems to affect FetchXML queries - QueryExpression queries work fine.

    Any ideas ?

    ReplyDelete
  22. Hi Andriy,

    seems to be excatly what i need, but i cant get this plugin to work.

    After registration i get the error: "FileNotFoundException. Could not load the File or assembly RecordCounter..."

    Then, after copying the dll manually to the GAC, the error turns into: System.Net.WebException: The underlying connection was closed: could not establish trust relationship for the SSL/TLS secure channel.

    Do you know how to solve this problem?

    Thanks in advance,
    Christian

    ReplyDelete
  23. >>PC
    You can redevelop this plugin a little bit to make Fetch XML queries to work. Add following lines at the start of plugin's code:

    if (context.CallerOrigin == CallerOrigin.AsyncService ||
    context.CallerOrigin == CallerOrigin.WebServiceApi)
    return;

    ReplyDelete
  24. >>Christian

    Strange error... I haven't seen such behavior in all of my developed plugins...

    ReplyDelete
  25. > You can redevelop this plugin a little bit to make Fetch XML queries to work....

    Excellent. Thanks a lot.

    ReplyDelete
  26. This comment has been removed by the author.

    ReplyDelete
  27. Hi Andriy,

    great plugin. i have a small problem...

    i have registered this plug-in but now when i m trying to modify or edit the notes on any lead, it shows error.
    Please help me out.

    Thanks in advance.

    ReplyDelete
  28. For some reason, when I use this code, my grids return with no records at all. any ideas

    ReplyDelete
  29. Could you make screenshots of plugin and steps registration?

    ReplyDelete
  30. unfortunately no, because the system is on a closed and classified network. However, I double and triple checked the registration and it looks fine.

    ReplyDelete
  31. I'm afraid that I'm unable to help you without all required information...

    ReplyDelete
  32. I think I found the issue. I tried using the Linq to XML objects instead of the old xmlDocument and when I switched back to your version with the xmlDocument it worked.
    So it's either something I did wrong, or another issue related to language (i am using hebrew)

    Thanks,

    anyway

    ReplyDelete
  33. I figured my problem - When I used the linq object (XElement instead of xmlDocument), i needed to pass to the output parameter XElement.ToString(SaveOptions.DisableFormatting). It was the formatting that messed it up.

    ReplyDelete
  34. I found another problem with the plug-in. It will not work correctly if the primary attribute for the entity is not shown in the view.
    I found a solution to this - get the list of attributes from the metadata service and then find a string attribute in the metadata that exists in the fetchXml and write the result to that attribute.
    However, This causes a serious drop in performance as the retrieve action on the metadata is a lot heavier. Any ideas on how to solve this issue?

    Thanks,

    Gabby

    ReplyDelete
  35. I thought of another solution although this one requires some unsupported parts.

    Instead of writing a "fake" row, I use the following code:
    HttpContext httpContext = HttpContext.Current;
    if (httpContext != null)
    {
    httpContext.Response.Write("var totalRowsAndPages = '" + result + "'") [in the Write method, the text is surrounded by html script tags, but I can't write them here].

    What this code does is add a javascript variable that holds the result of the record counter.

    I then add some javascript code when the grid page loads, that takes this value and adds it to the grids status bar, next to the 'selected X of Y' text.

    This works great when the grid page first loads, but when I press the green refresh button, or delete a record (also causes the grid to refresh), the system throws an error, and the grid empties.

    any thoughts on this option? if I can make it work, it would be great for performance and for grids that don't show the primary attribute of an entity.

    ReplyDelete
  36. Hi! I made some fix in BasePlugin.cs - you should add CampaignActivity as activity, because plugin fails for CampaignActivity

    protected List activities =
    new List(new string[]
    { EntityName.activitypointer.ToString(),
    EntityName.task.ToString(),
    EntityName.email.ToString(),
    EntityName.fax.ToString(),
    EntityName.letter.ToString(),
    __EntityName.campaignactivity.ToString()__,
    EntityName.appointment.ToString(),
    EntityName.serviceappointment.ToString(),
    EntityName.bulkoperation.ToString()});

    ReplyDelete