Jan 152014
 

If you upgraded your system from CRM 2011 and used one of the tools out there that stores the contact image in the notes, and you want to set the entity image, here is an XrmToolbox plugin to help you. So, everyone that used (or continues to use) the crmattachmentimage or other solutions, you can now use this tool to help you migrate or use the new image field.

The XrmToolBox, created by Tanguy, can be downloaded from codeplex. It is a great set of tools for working with any Microsoft Dynamics CRM system. To upgrade my images that I have attached as notes to entityimage (the new CRM 2013 image field) I created a plugin for the XrmToolBox.

The ImageMigrator XrmToolBox plugin lists the entities that have image fields and will pull any png jpg or gif and attach it to the entity.

ImageMigrator Screenshot

Once it loads, click the refresh button to get a list of entities you can run the tool on, then select and click convert.

You can download just the plugin (dll) here.

You can download my source code for the project here.

Mar 082013
 

The XrmSvcToolkit on codeplex is a very small and easy to use toolkit for interacting with the Xrm web-services. TypeScript is a language for application-scale JavaScript development by compiling to plain JavaScript.

Once you’ve downloaded and installed TypeScript, you can use the Xrm 2011 TypeScript Definition from codeplex for the Xrm.Page. You can reference it using:

        /// <reference path=”Xrm2011.d.ts” />

You can reference the below Type Definition for the XrmSvcToolkit the same way:

        /// <reference path=”XrmSvcToolkit.d.ts” />

XrmSvcToolkit.d.ts

/* *****************************************************************************
TypeScript definition file for the XrmSvcToolkit
Author: Carlton Colter - http://www.mscrmblogger.com/
XrmSvcToolkit - http://xrmsvctoolkit.codeplex.com/
***************************************************************************** */

declare module XrmSvcToolkit {
    export function context(): {
        getAuthenticationHeader(): string;
        getCurrentTheme(): string;
        getOrgLcid(): number;
        getOrgUniqueName(): string;
        getQueryStringParameters(): string;
        getServerUrl(): string;
        getUserId(): string;
        getUserLcid(): number;
        getUserRoles(): string[];
        isOutlookClient(): bool;
        isOutlookOnline(): bool;
        prependOrgName(sPath: string): string;
    };
    export function serverUrl(): string;
    export function retrieve(opts: {
        entityName: string;
        id: string;
        select: string;
        expand: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function retrieveMultiple(opts: {
        entityName: string;
        odataQuery: string;
        async: bool;
        successCallback: Function;
        completionCallback: Function;
        errorCallback: Function;
    }): Object[];
    export function createRecord(opts: {
        entityName: string;
        entity: Object;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function updateRecord(opts: {
        entityName: string;
        id: string;
        entity: Object;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function deleteRecord(opts: {
        entityName: string;
        id: string;
        entity: Object;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function associate(opts: {
        entity1Id: string;
        entity1Name: string;
        entity2Id: string;
        entity2Name: string;
        relationshipName: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function disassociate(opts: {
        entity1Id: string;
        entity1Name: string;
        entity2Id: string;
        relationshipName: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function setState(opts: {
        entityName: string;
        id: string;
        stateCode: number;
        statusCode: number;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function execute(opts: {
        executeXml: string;
        async: bool;
        successCallback: Function;
        errorCallback: Function;
    }): Object;
    export function fetch(opts: {
        fetchXml: string;
        async: bool;
        successCallback: (result)=>void;
        errorCallback: Function;
    }): Object[];
}
Oct 122012
 

You can download the unmanged or managed solutions by clicking on unmanged or managed.

There are times when hierarchy matters, when one object rolls up into another, and you need something like the subject lookup. Unfortunately the subject lookup is not an option you can turn on for your own lookups. However, thanks to some JScript from Tanguy you can override the default lookup control, making it launch your own web-interface. Yes, I know this is not supported, but at least the rest of this add-on follows the SDK guidelines.

UR12 / Polaris: the solution above does not work properly with UR12. I have not tested this UR12 solution with previous versions of CRM 2011. It no-longer requires you to specify the Set in the Set name, i.e. Category is just Category, not CategorySet. It uses the XrmSvcToolkit by Daniel Cai instead of the XrmServiceToolkit. You can download an unmanaged solution here. The technique used by Tanguy nolonger works, so I had to access the underlying behavior of the form element. I wish I could tell you that it won’t ever break, but there is no telling, because it is an unsupported modification. If you would like to do something supported, you can put a button on the ribbon to launch the custom web-resource and load the resulting data into the field.

Here is what it looks like:

Now, I won’t go into the details of how the web-resource works (you can look at my code), but here is how you use it:

  1. You can install either the unmanged or managed solution into your CRM
  2. Add the msblg_treelookup.js to your form scripts.
    1. In the form designer for your entity click on the Form Properties button.
    2. Click add, and search for name msblg_treelookup.js, select it, and click ok.
  3. Then add an event handler for the Form OnLoad (by scrolling down and clicking add)

    1. The parameters are:

      1. Attribute Name
      2. Entity Set Name (for the UR12 release, do not use Set, just use the entity name)
      3. Object Type Code
      4. The Id Field
      5. The Name Field
      6. The Parent Field (Lookup to the same entity)
      7. Title for the lookup window
      8. Description for the lookup window
      9. Column Header
      10. A REST filter that will be applied to all queries (not required)

The LookupTree function makes the existing lookup button launch the custom tree-view lookup. If you wanted to use this control in a completely supported manner, with some modifications you could add a button to launch the control.

May 102012
 

Do you need the value from a related entity, like a lookup? I had written this for CRM 4 a while back, and people use it, but I occasionally get questions about looking up other values in CRM. This bit of code will help you find those values on a related object. Below are two options to do this. Option 1: using the REST SDK and JSON2, and then Option 2: using Jaimie Ji’s XRM Service Toolkit.

Jaimie Ji’s XRM Service Toolkit provides a comprehensive set of JScript libraries for interacting with the SOAP and REST SDK through JavaScript. While Option 1 in this particular use case is fairly simple, when you start needing to do more advanced things, you may want to look at using that kit. It is fairly comprehensive and he keeps it up to date.

Option 1: Using the REST SDK and JSON2

Using the REST SDK is great, and there are some really good examples in the CRM SDK help file. Now in order to use the below code-block, you need to use JSON.

JSON, or JavaScript Object Notation, is a text format that is used to interchange data. JSON2.js is a lightweight javascript that convert the strings to javascript objects and vice-versa. The CRM REST SDK can return data in JSON format.

To get the code for JSON2, you can go here. You will need to embed this code into your javascript file or include json2.js along with the code below. Without it, you will not be able to parse the data properly, and the code below uses the JSON library in json2.js.

Ok, below we have 3 functions:

  • getServerUrl : which gets the server URL – taken from the CrmRestKit
  • Lookup_Changed : what you call when a lookup is changed
  • retrieveReqCallBack : the callback function that performs any actions

You would put Lookup_Changed in the change event of a lookup field and specify the attributes like one of the following:

  • ‘contactid’,’Contact’,’contactlookup’
  • ‘contactid’,’Contact’,’contactlookup’,[‘Telephone1′]
  • ‘contactid’,’Contact’,’contactlookup’,[‘Telephone1′,’FullName]

Then you’d put your actions inside the retrieveReqCallBack.

function getServerUrl() {  
    // From CrmRestKit.js
    var localServerUrl = window.location.protocol + "/" + window.location.host;
    var context = parent.Xrm.Page.context;

    if (context.isOutlookClient() && !context.isOutlookOnline()) {
        return localServerUrl;
    }
    else {
        var crmServerUrl = context.getServerUrl();
        crmServerUrl = crmServerUrl.replace(/^(http|https):\/\/([_a-zA-Z0-9\-\.]+)(:([0-9]{1,5}))?/, localServerUrl);
        crmServerUrl = crmServerUrl.replace(/\/$/, "");
    }
    return crmServerUrl;
}

function Lookup_Changed(attributeName, entityName, callbackId, columns) {
    var serverUrl = Xrm.Page.context.getServerUrl();
    var ODataPath = serverUrl + "/XRMServices/2011/OrganizationData.svc";
    
    var lookup = Xrm.Page.data.entity.attributes.get(attributeName).getValue();
    if (lookup===null) {
        return false;
    }
  
    var id = lookup[0].id;
    if (id == null) { 
        return false;
    }
    
    var retrieveReq = new XMLHttpRequest();
    
    var url = ODataPath + "/"+entityName+"Set(guid'" + id + "')";
    if (columns !== undefined && columns !== null) {
        url = url + "?$select=" + columns.join(',');
    }
    
    retrieveReq.open("GET", url, true);
    retrieveReq.setRequestHeader("Accept", "application/json");
    retrieveReq.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    retrieveReq.onreadystatechange = function () {
        if (this.readyState == 4 /* complete */) {
            if (this.status == 200) {
                var data;
                var jData;
                jData = JSON.parse(this.responseText);
                if (jData && jData.d && jData.d.results && jData.d.results.length > 0) {
                    data = jData.d.results[0];
                } else if (jData && jData.d) {
                    data = jData.d;
                }
                
                if (data == null) {
                    return;
                }
                retrieveReqCallBack(callbackId, data);
            }
        }
    };
    retrieveReq.send();
    return true;
}

function retrieveReqCallBack(calltype, data) {
    var toLookup=function(data) {
        if (data==null || data=="" || (data.Id==null)) {
            return null;
        }
    
        var result=new Array();
        result[0] = {};
        result[0].id = data.Id;
        result[0].name = data.Name;
        result[0].entityType = data.LogicalName;
        return result;
    }

    switch (calltype) {
        case 'pricelookup':
            Xrm.Page.data.entity.attributes.get("new_pricecategorylookup").setValue(toLookup(data.new_pricecategorylookup);
            break;
        case 'contactlookup':
            Xrm.Page.data.entity.attributes.get("insp_poctelephonenumber").setValue(data.Telephone1);
            break;
        default:
            break;
    }
}

Option 2: Using the XrmServiceToolkit with similar functions

Ok, now let’s assume you are using the XRM Service Toolkit, and you want to do the same thing, using a Lookup_Changed function and the retrieveReqCallBack function. Instead of embedding JSON and a method to get the server url, you can use the following:

function Lookup_Changed(attributeName, entityName, callbackId, columns) {
    var lookup = Xrm.Page.data.entity.attributes.get(attributeName).getValue();
    if (lookup===null) {
        return false;
    }
  
    if (lookup[0].id == null) { 
        return false;
    }
	
	XrmServiceToolkit.Rest.Retrieve(
        lookup[0].id,
        entityName,
        columns,
		null,
        function (result) {
            retrieveReqCallBack(callbackId, result);
        },
        function (error) {
            throw error;
        },
		true
    );
}

function retrieveReqCallBack(calltype, data) {
    var toLookup=function(data) {
        if (data==null || data=="" || (data.Id==null)) {
            return null;
        }
    
        var result=new Array();
        result[0] = {};
        result[0].id = data.Id;
        result[0].name = data.Name;
        result[0].entityType = data.LogicalName;
        return result;
    }

    switch (calltype) {
        case 'pricelookup':
            Xrm.Page.data.entity.attributes.get("new_pricecategorylookup").setValue(toLookup(data.new_pricecategorylookup);
            break;
        case 'contactlookup':
            Xrm.Page.data.entity.attributes.get("insp_poctelephonenumber").setValue(data.Telephone1);
            break;
        default:
            break;
    }
}

The XRM Service Toolkit does a lot, and I would suggest checking it out.

Sep 152011
 

In a previous post I wrote about a control I posted to codeplex that I used in my demos, a CRM Attachment Image Control.

It stored the images in the notes (still does – and currently does not have an option for storing elsewhere…). I still have not figured out how to put buttons on the ribbon that can fire the upload control, etc. Although, I recently found out that Marco Amoedo (blog: marcoamoedo.com, twitter: marcoamoedo) had leveraged the ImageTools for Silverlight to add WebCam support and I was already starting to play with it to add Gif support. I was able to work with him to get his modifications and add gif support. Now you have even more useful features that you can enable with a simple configuration change and a new silverlight web-resource.

Now, if you look in the bottom right, there is a webcam button next to the upload button that allows you to take a picture instead of uploading one. When you try and take the picture you will receive the following request for access.

So you may be asking: what is ImageTools for Silverlight? where did it come from?, and why are you using it? The answers are simple. According to the codeplex page for the ImageTools for Silverlight, it is "a library, which provides additional functionality for loading, saving and [manipulating] images from different sources and with different formats.&quot The reason to use it is simple, it is open-source, and it’s license allows me to use it (it is realeased under Ms-PL – the same license as my control on codeplex).

If you already use the CRM Attachment Image, you can just overwrite your xap file with the one from codeplex, but you will still need to edit the properties of the web-resource on your form to include |webcam=true. The new example syntax is:

field=new_imageattachmentid|subject='Contact''s Image'|prefix='img-'|webcam=true

So a special thanks to Marco for his help, this is a great change that I think a lot of people will be able to use.

If you need assistance in setting up the image control, I have it documented on codeplex, but here is a simple set of directions that work at the time of this article bieng posted:

  1. Create a solution
  2. Upload the xap as a web-resource
  3. Add your entity to the solution, and create a new text field on the entity that is 50 characters long, not searchable, preferably called something like attachmentid or savedattachmentid – it will be used to store the annotationid or the note that stores the attached image.
  4. Add the field to the form and make it not visible.
  5. Add the Web-Resource to the form
    • Check the checkbox to pass the id and type, etc.
    • In the parameters add the fields and their values seperated by a pipe |
  6. Save and publish the form.
  7. Test it!

The example config for the web-resource is before the instructions, and are very useful because you can just copy them and change the field new_imageattachmentid to your field name and it should work. There are a number of fields available to you if you want to change how the control functions, they are documented on codeplex, and they are:

  • field – The name of the field you are using to store the annotationid of the attachment. It is required, and the field MUST BE ON THE FORM (it does not need to be visible).
  • subject – The subject for attachments – the following substitutions are available (%s will be replaced with the name of the file )
  • prefix – The prefix for all attachment images (this is used to filter the results, etc)
  • hidebuttons – Used to hide the buttons that are in the silverlight control so that you can use the ribbon to control the application
  • savefirst – Used to set the message that is displayed on a form that is in Create Mode
  • webcam – Set to true if you want to enable the webcam it is not currently tide to hidebuttons

I added the following parameters at Bill’s request (Thanks Bill):

  • max-width: maximum width for all images
  • max-height: maximum height for all images
  • max-webcam-width: maximum webcam image width, overrides max-width for web-cam images
  • max-webcam-height: maximum webcam image height, overrides max-height for web-cam images
Jun 152011
 

Okay, I’m not the best Silverlight developer, I’ve only recently gotten started. And recently I was working on trying to find the appointments for a particular service. If you look at the SDK, there is a Sample: Schedule a Resource. It works great if you are creating a console application, but the same code doesn’t work if you are using Silverlight.

Here is the code from the sample that creates the AppointmentRequest:

AppointmentRequest appointmentReq = new AppointmentRequest
                    {
                        RequiredResources = new RequiredResource[] { vanReq },
                        Direction = SearchDirection.Backward,
                        Duration = 60,
                        NumberOfResults = 10,
                        ServiceId = _plumberServiceId,
                        // The search window describes the time when the resouce can be scheduled.
                        // It must be set.
                        SearchWindowStart = DateTime.Now.ToUniversalTime(),
                        SearchWindowEnd = DateTime.Now.AddDays(7).ToUniversalTime(),
                        UserTimeZoneCode = 1
                    };

Now, there are a lot of fields that they didn’t set, but since they are using the DLL files, they don’t need to. The reason this same request would fail using Silverlight is because the web-service expects the arrays to not be null. Using the DLL files, they end up as just empty arrays, but since we are using the ObservableCollection in Silverlight, we have to initialize the collections by creating empty fields. Then our request will be accepted.

AppointmentRequest appointmentReq = new AppointmentRequest
                    {
                        Objectives = new ObservableCollection(),
                        RequiredResources = new ObservableCollection(),
                        AppointmentsToIgnore = new ObservableCollection(),
                        Constraints = new ObservableCollection(),
                        Sites = new ObservableCollection(),
                        // Required Fields:
                        Duration=duration,
                        Direction=SearchDirection.Forward,
                        NumberOfResults = 20
                    };

You’ll still have to set the TimeZoneCode and adjust the dates accordingly just like they do in the SDK example. My personal preference here is to retrieve the usersettings and use the timezonecode from it.

Now, for all the people out there that use my SilverCRMSoap Library I have on codeplex, I did update the source code to include a BeginExecuteSearchRequest and modified the AppointmentRequest to have the default values specified.

With that, I think my bing maps scheduler will turn out great. Once it is done, I might update this post with a screenshot.

Happy Scheduling!

– mscrmblogger

Apr 262011
 

SilverCrmSoap is a library built using the methods in Walkthrough: Use the SOAP Endpoint for Web Resources with Silverlight. The SilverCrmSoap library simplifies SOAP Silverlight development for CRM 2011 by providing a class that does a lot of the heavy lifting right out of the box.

In essence, the SilverCrmSoap library objectifies FetchXml allowing you to modify it easily in code, adds an XrmHelper for calling the context and page functions from a Silverlight application, a FormHelper for setting values on a form, and a SoapHelper for making all of your SOAP calls. It becomes a real time saver because you don’t have to recompile it. And, if your curious where/when to use it, it’s pretty straight forward. If you need SOAP for CRM, then use the library and save time.

How do you know to use SOAP vs REST?

Use SOAP when…

  • You need to use late binding to create a dynamic Silverlight application that works with any (or multiple) entities.
  • You need to execute a message.
  • You need to assign records.
  • You need to retrieve metadata.

There are benefits to using the REST endpoint too, and being able to quickly leverage both end points in a project can be extremely helpful.

Use REST when…

  • You need to early bind to objects.
  • You want to leverage LINQ with the early bound objects.
  • You want compile time type support and IntelliSense.

You can check out the SilverCrmSoap library on CodePlex or read more about it on the CRM Team Blog.

Enjoy!

-MSCRMBlogger

Sep 302009
 

The Notification Accelerator on Codeplex does not offer a queueitem RSS feed. I’m not sure why, but here is a simple solution to display an RSS feed. It defaults to the current user’s queues, but if the q=blah is specified in address then all queueitems in any queue with blah in it will be displayed. Please feel free to post your comments. I did not spend a lot of time on this and I think sql would be quicker. If you find a method that is quick and easy that displays the queue name and queue items, please post it.

crmsdk is my http://crm/MSCrmServices/2007/CrmService.asmx web reference

using System;
using System.Net;
using System.Text;
using System.Web.UI;
using System.Xml;
using CRMQIRSS.crmsdk;

namespace CRMQIRSS
{
    public partial class _Default : Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            var QueueName = String.Empty;

            try
            {
                QueueName = Request["q"];
            }
            catch
            {
                // Ignore Errors
            }


            // Clear Any Response
            Response.Clear();
            Response.ContentType = "text/xml";

            var xtwFeed = new XmlTextWriter(Response.OutputStream, Encoding.UTF8);
            xtwFeed.WriteStartDocument();
            // Setup Feed

            xtwFeed.WriteStartElement("rss");
            xtwFeed.WriteAttributeString("version", "2.0");

            // Setup Channel
            xtwFeed.WriteStartElement("channel");
            xtwFeed.WriteElementString("title", QueueName);
            xtwFeed.WriteElementString("link", "http://www.mscrmblogger.com");
            xtwFeed.WriteElementString("ttl", "5");
            xtwFeed.WriteElementString("description", "The most technical and solution rich CRM blog.");
            xtwFeed.WriteElementString("copyright", "Copyright 2009 mscrmblogger.com. All rights reserved.");

            // Get the Dataset
            BusinessEntityCollection becQueueItems;
            if (String.IsNullOrEmpty(QueueName))
            {
                becQueueItems = GetQueueItemsByCurrentUser();
            }
            else
            {
                becQueueItems = GetQueueItemsByName(QueueName);
            }

            foreach (var be in becQueueItems.BusinessEntities)
            {
                var qi = (queueitem) be;

                var description = "Created: " + Convert.ToDateTime(qi.createdon.Value).ToString("f");

                xtwFeed.WriteStartElement("item");

                xtwFeed.WriteElementString("title", qi.title);

                xtwFeed.WriteElementString("description", description);

                String objecttypecode;
                switch (qi.objecttypecode.Value)
                {
                    case "incident":
                        objecttypecode = "112";
                        break;
                    case "task":
                        objecttypecode = "4212";
                        break;
                    case "phonecall":
                        objecttypecode = "4202";
                        break;
                    case "letter":
                        objecttypecode = "4207";
                        break;
                    case "serviceappointment":
                        objecttypecode = "4214";
                        break;
                    case "fax":
                        objecttypecode = "4204";
                        break;
                    case "email":
                        objecttypecode = "4202";
                        break;
                    case "campaignactivity":
                        objecttypecode = "4402";
                        break;
                    case "campaignresponse":
                        objecttypecode = "4401";
                        break;
                    default:
                        objecttypecode = "0";
                        break;
                }


                xtwFeed.WriteElementString("link",
                                           "http://crm/CRMReports/viewer/drillopen.aspx?ID=" + qi.objectid.Value +
                                           "&OTC=" + objecttypecode);

                xtwFeed.WriteElementString("pubDate", Convert.ToDateTime(qi.enteredon.Value).ToString("f"));

                xtwFeed.WriteElementString("author", qi.createdby.name);

                //TODO: Lookup the queue...  this would be quicker (and easier) with SQL...
                //xtwFeed.WriteElementString("category", "queue/" + qi.queueid);

                xtwFeed.WriteEndElement();
            }


            // Close all tags
            xtwFeed.WriteEndElement();
            xtwFeed.WriteEndElement();
            xtwFeed.WriteEndDocument();
            xtwFeed.Flush();
            xtwFeed.Close();
            Response.End();
        }

        private CrmService GetCrmServiceConnection(String Organization)
        {
            var token = new CrmAuthenticationToken();
            token.AuthenticationType = 0; //AD
            token.OrganizationName = Organization;

            var crmService = new CrmService();
            crmService.CrmAuthenticationTokenValue = token;
            crmService.Credentials = CredentialCache.DefaultCredentials;

            return crmService;
        }

        private BusinessEntityCollection FetchDataSet(string fetchXml)
        {
            var service = GetCrmServiceConnection("OrganizationXYZ");

            var request = new FetchXmlToQueryExpressionRequest();
            request.FetchXml = fetchXml;

            var response = (FetchXmlToQueryExpressionResponse) service.Execute(request);

            var items = service.RetrieveMultiple(response.Query);

            return items;
        }

        private BusinessEntityCollection GetQueueItemsByName(String queueName)
        {
            var fetchXML = "<fetch mapping=\"logical\" count=\"50\" distinct=\"true\">" +
                           "  <entity name=\"queueitem\">" +
                           "    <all-attributes />" +
                           "    <link-entity name=\"queue\" from=\"queueid\" to=\"queueid\">" +
                           "      <attribute name=\"name\" />" +
                           "      <filter>" +
                           "        <condition attribute=\"name\" operator=\"like\" value=\"%" +
                           queueName +
                           "%\" />" +
                           "      </filter>" +
                           "    </link-entity>" +
                           "  </entity>" +
                           "</fetch>";

            return FetchDataSet(fetchXML);
        }

        private BusinessEntityCollection GetQueueItemsByCurrentUser()
        {
            var fetchXML = "<fetch mapping=\"logical\" count=\"50\" distinct=\"true\">" +
                           "  <entity name=\"queueitem\">" +
                           "    <all-attributes />" +
                           "    <link-entity name=\"queue\" from=\"queueid\" to=\"queueid\">" +
                           "      <attribute name=\"name\" />" +
                           "      <filter>" +
                           "        <condition attribute=\"primaryuserid\" operator=\"eq-userid\" />" +
                           "      </filter>" +
                           "    </link-entity>" +
                           "  </entity>" +
                           "</fetch>";

            return FetchDataSet(fetchXML);
        }
    }
}

Total time spent on this: 45 minutes