CRM Plugins - Json Kullanımı

Working with JSON objects in Dynamics CRM Plugins


CRM üzerinde çalışan pluginlerimizde json formatında veri döndürdüğümüzü düşünelim .

Genellikle bu tür işlemler için .NET Frameworkun bize sunduğu Newtonsoft.json kütüphanesini kullanırız.

Eğer pluginimizi Sandbox a register edersek pluginde kullandığımız javascriptSerializer metodu aşağıdaki hatayı verecektir.

Unhandled Exception: Microsoft.Crm.CrmException: Unexpected exception from plug-in (Execute): XXXXXXXX.CRM2015.WorkflowActivities.XXXXXXXX: System.MethodAccessException: Attempt by security transparent method ‘XXXXXXXX.CRM2015.WorkflowActivities.XXXXXXXX.SetLocationInfo(Microsoft.Xrm.Sdk.IOrganizationService, Microsoft.Xrm.Sdk.ITracingService, System.String)’ to access security critical method ‘System.Web.Script.Serialization.JavaScriptSerializer..ctor()’ failed.
Assembly ‘XXXXXXXX.CRM2015.WorkflowActivities, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3f9fc15734725b08′ is partially trusted, which causes the CLR to make it entirely security transparent regardless of any transparency annotations in the assembly itself.  In order to access security critical code, this assembly must be fully trusted.
Assembly ‘System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35′ is a conditionally APTCA assembly which is not enabled in the current AppDomain.  To enable this assembly to be used by partial trust or security transparent code, please add assembly name ‘System.Web.Extensions, PublicKey=0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9′ to the the PartialTrustVisibleAssemblies list when creating the AppDomain.
at Microsoft.Crm.Sandbox.SandboxCodeUnit.Execute(IExecutionContext context)

at Microsoft.Crm.Workflow.Services.ProxyCustomActivity.Execute(CodeActivityContext executionContext)


 Hatanın sebebi CRM contextinde 3.party dllere erişilememesidir.

Bu hatayı ILMerge kullanarak çözebiliriz. ( .NET dllerini tek bir dll olarak birleştiren araç)

Alternatif bir çözüm olarak ise ; 3.party çözüm kullanmayarak DataContractJsonSerializer kullanmaktır.

Biz örneğimizde DataContractJsonSerializer yöntemini kullanacağız.


CRM Plugininde DataContractJsonSerializer Kullanımı

Servisimizden gelen verileri tutacağımız bir DataContract classı tasarlayalım.


   [DataContract]
    public class JsonSalesServiceTest
    {
        //datamember name value indicates name of json field to which data will be serialized/from which data will be deserialize

        [DataMember(Name = "Status")]
        public string Status { get; set; }

        [DataMember(Name = "Source")]
        public string Source { get; set; }

        [DataMember(Name = "BusinessUnit")]
        public Reference1 BusinessUnit { get; set; }

        [DataMember(Name = "Customer")]
        public Reference1 Customer { get; set; }

        [DataMember(Name = "CustomerAddress")]
        public string CustomerAddress { get; set; }

        public JsonSalesServiceTest() { }
    }

Json dizimizdeki verileri classımıza atayalım.(Deserialize)

Deseriliaze

 #region Deserialize

                using (MemoryStream DeSerializememoryStream = new MemoryStream())
                {
                    //Json String that we get from web api 
                    string ResponseString = "{\"Status\":\"" + "Create" +
                                          "\",\"Source\":\"" + "Dynamics CRM London" + 
                                          "\",\"BusinessUnit\":\"" + "{\"Name\":null,\"Id\":\"01f4f959-ceea-e511-80ed-005056b503b5\"}" + 
                                          "\",\"Customer\":\"" + "{\"Name\":null,\"Id\":\"d43f26ff-8e2c-e611-80ff-005056b503b5\"}" +
                                          "\",\"CustomerAddress\":\"" + "400005" + "\"}";

                    //initialize DataContractJsonSerializer object and pass Student class type to it
                    DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(JsonSalesServiceTest));

                    //user stream writer to write JSON string data to memory stream
                    StreamWriter writer = new StreamWriter(DeSerializememoryStream);
                    writer.Write(ResponseString);
                    writer.Flush();

                    DeSerializememoryStream.Position = 0;
                    //get the Desrialized data in object of type Student
                    JsonSalesServiceTest SerializedObject = (JsonSalesServiceTest)serializer.ReadObject(DeSerializememoryStream);
                }
                #endregion

Seriliaze

Classımızdaki verileir Json formatına çevirelim.

   #region Serialize
               JsonSalesServiceTest json = new JsonSalesServiceTest();
               json.Status = PluginMessages.Cancel;
               json.Source ="Dynamics CRM AGC";

 if (order.OwningBusinessUnit != null && order.OwningBusinessUnit.Id != Guid.Empty)
                        json.BusinessUnit = new Reference1()
                        {
                            Name = order.OwningBusinessUnit.Name,
                            Id = order.OwningBusinessUnit.Id
                        };

                    if (order.CustomerId != null && order.CustomerId.Id != Guid.Empty)
                    {
                        json.Customer = new Reference1() { Name = order.CustomerId.Name, Id = order.CustomerId.Id };
                    }

 json.CustomerAddress = !string.IsNullOrEmpty(order.BillTo_Line1) ? order.BillTo_Line1 : " ";

                string parameter1 = "";
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(JsonSalesServiceTest));
                    serializer.WriteObject(memoryStream, json);
                    parameter1 = Encoding.Default.GetString(memoryStream.ToArray());
                }

#endregion

CRM 2016 - Alt Kayıtları Aktif / Pasif Yapma (MSCRMWorkflowUtilities)

CRM arayüzü üzerinden  bir kayda aktif , pasif , birleştir, paylaştır gibi işlemleri kolaylıkla yapabiliriz. Peki bu kaydın altındaki kayıtlara da aynı işlemleri yapmak istersek nasıl yapabiliriz.

Örnek vermek gerekirse bir firmayı pasif yaptığımız zaman altındaki ilgili kişilerini de pasif yapmak istiyoruz.Aynı şekilde aktif yaptığımızda altındaki ilgili kişilerininde aktif olmasını isteyebiliriz.

Herhangi bir kod yazmadan bu işlemleri MSCRMWorkflowUtilities solutionı ile yapabiliyoruz. Tek yapmak gereken çözümü indirip crm e yüklemektir.

Not: CRM versiyonunuza uygun paketi indirmelisiniz.

Çözüm bize SetState operatorunu workflow üzerinde kullanılmasını sağlayacaktır. Bu şekilde kayıtların durumlarını güncelleyebileceğiz.

İlk olarak ana kayıt için bir iş akışı oluşturalım.

Firma kayıtları için alt kayıtlarının durumlarını güncellemek için bir iş akışı oluşturalım.Ardından istediğimiz koşullarda ve birçok basamaklı adım girebiliriz.

Aşağıdaki örnekte , firma aktif olduğunda altındaki ilgili kişileri aktif , firma pasif olduğunda altındaki ilgili kişileri pasif edilecektir.

Not: Firmanın  ilgili kişileri ve aktivitelerini de pasif yapacaksak bunlar için de ek adımlar tanımlayabiliriz.



Eklediğimiz her  Cascade SetState adımı için  3 parametre tanımlamamız gerekir.



1- Alt Varlığın Adı ( Child Entity Name )

SetState işlemini basamaklamak için kullanılacak alt vrlığın adıdır .
Bizim örneğimizde "contact" olacaktır.
Şema adını varlığın tanımından bulabilirsiniz.



2- Ana Varlıkla Kurulan İlişki Alanı ( Child Lookup Attribute To Parent )

Ana varlık ile alt kaydın birbirine bağlandığı arama alanının şema adıdır.
Bizim örneğimizde "parentcustomerid" dir.
Bu adı alt varlığın ana varlığa bağlandığı arama alanının tanımlarından ulaşabilirsiniz.



3- Alt Varlığın Durum Kodu ( Target Child State Code )

Alt varlıkların durumunu içeren değerdir. Bu değer çoğu zaman pasif için 1 , aktif için 0 dır.

Not :Bazı fırsat gibi özel varlıklarda SetState mesajının kullanılması desteklenmez. Tüm özel varlıklar destekler.


CRM 2016 Customizations - Aktivite Butonlarını Düzenleme

Restore Add Activity Buttons 


Aktivite butonları CRM 2013 ten itibaren default olarak ribbon bar da görünmez.  Bu işlemleri aşağıdaki ekran görüntüsünde görüldüğü gibi form üzerindeki Activities tabı altından yapabiliriz.


Bu butonları ribbon barında görebilmek için Ribbon WorkBench üzerinde aşağıdaki işlemleri yapmamız gerekir.

Not : Ribbon WorkBench solutionını buradan indirip crm e import etmeniz gerekecektir.

-- İlk olarak hangi entity formu üzerinde işlem yapacaksak o entitiye air bir solution oluşturalım . Ben account formu için çalışma yapacağım.

 İlk olarak Solutionlara gidelim. Setting > Customization > Solutions



Yeni solution oluşturmak için New butonuna basalım.


Solutionımızın bilgilerini girelim.Save butonuna basalım.

Gelen ekrandan Add Existing butonuna basalım ve sonrasında Entity i seçelim.

Gelen pencereden Accountu seçelim ve OK butonuna basalım.


Gelen ekranda Forms tabı altından Main formumuzu seçelim . Burada sadece Main formunu almak yeterli olacaktır. Finish butonuna basalım.

Missing Required Components penceresinde No , do not include required components i seçelim ve OK butonuna basalım.


Artık solutionımızı oluştıurmuş olduk .


Sıra geldi Ribbon WorkBench ile solutionımızı düzenlemeye;

Daha önce sistemimize yüklediğimiz RibbonWorkBench2013  managed solutionımızı acalım.





 Açılan pencereden Open Solution butonuna basalım ve oluşturduğumuz solutionı açalım.


 Ribbon görünümüne geçmek için Ribbon tabına tıklayalım.


 Daha sonra entitimizin Form görünümünü seçelim.


Bir sonraki adımda activite butonlarının bulunduğu Add tabına tıklayalım.


İlk olarak Task butonunu seçelim ve fare ile sağ tık yaptıktan sonra  Customize Command ı seçelim.


Orta bölümde bulunan Solution Elements altında Commands bölümünü genişletelim ve Task için geçerli olan Mscrm.AddTaskToPrimaryRecord u seçip , görünümünü düzenlemek için Edit Display Rules seçeneğini seçelim.


Display Rules penceresinden sağ panelden Mscrm.HideOnCommandBar ı seçelim ve Remove butonuna basalım.


Yapılan işlemleri tamamlamak için OK butonuna basalım.


Daha sonra bu yaptığımız işlemleri diğer aktivite butonları içinde yapalım . (Fax , Phone Call , Email , Letter , Service Activity , Appointment , Recurring Appointment )

İşlemlerimizi tamamladıktan sonra crme yansıması için Publish butonuna basalım.


Daha sonra işlemlerimizi test etmek için bir account kaydı açalım ve ribbonumuzu kontrol edelim.



Yukarıdaki ekran görüntüsünde görüldüğü gibi aktivite butonlarımız ribbonda görünür duruma gelmiştir.

CRM 2016 - The provided uri did not return any Service Endpoints!

Bazen crme visual studio gibi ortamlardan bağlanmaya çalıştığımızda "The provided uri did not return any Service Endpoints!" hatasını vermektedir.

Çözüm

Hizmeleri açalım.


Microsoft Dynamics Asenkron Servisleri  restart edilmelidir.




CRM - Aktivite Tipinde Varlık Tanımlama

Create a Custom Activity

Varlıklar oluşturulurken standart yada custom aktivite olarak açılabilirler.

 Custom aktivite tanımlarken aşağıdaki ekran görüntüsündeki seçim alanlarını seçmelisiniz.


Varığımızı oluşturduktan sonra etkinlikler kısmında görebileceğiz.

Custom aktiviteler oluşturulurken bilinmesi gerekilenler

  • Custom varlıklar ilk yaratıldığında sadece System Administrator ve System Customizer rolundeki kullanıcılar görebilir. Bunun haricindeki  kullanıcıların rollerıne yetkı verilmelidir.
  • Entity aktivite tipinde yaratıldıktan sonra geri alınamaz.
  • Custom etkinlik varlığı, diğer etkinlik varlıklarına erişimi olan kullanıcılar tarafından kullanılabilir

CRM 2016 - Custom Actions

Kod yazılımı plugın gibi olup sadece sdk nın desteklediği mesajlar üzerinden çağrılmazlar. Yeni mesajlar tanımlanıp bu mesajlar uzerınden cagrılabılırler.Bunun haricinde giriş ve çıkış parametreleri tanımlanabilir . Bu şekilde dışarıdan veri gönderilebilir veya dışarıya veri gönderieblir.

Ayarlar altından Process i seçelim.

Gelen ekrandan yeni bir process oluşturmak için Yeni butonuna tıklayalım.

Açıan pencerede processin adını , category olarak Actionı ve Entity olarak da None(Global) değerini seçelim ve Tamam butonuna basalım.

Açılan pencere üzerinde giriş çıkış parametreleri tanmlayabiliriz.

Parametre olarak aşağıdaki tiplerde parametre tanımlanabilir.

Daha sonra yaptığıız işlemleri kaydedelim ve actionımızı kullanmak için Activated butonuna basalım.


Gelen ekranda Activate butonuna basarak processimizi aktif duruma getirelim.

Daha sonra actionımızın kodlarını yazalım.


 private void ExecuteCreateInterestOrderAction(LocalPluginContext localContext)
        {
            try
            {
                #region Plugin Tanımlamaları
                IPluginExecutionContext context = localContext.PluginExecutionContext;
                IOrganizationService crmService = localContext.OrganizationService;

                if (!context.InputParameters.Contains("OrderId"))
                    return;
                #endregion

                #region Main Code
                Guid newOrderId = Guid.Empty;
                EntityReference orderRef = (EntityReference)context.InputParameters["OrderId"];

                if (orderRef.LogicalName != "salesorder") { return; }
                string[] orderColumns = { "salesorderid", "new_faturano", "new_interestorderamount", "customerid", "new_accountnameid" };

                Entity order = crmService.Retrieve("salesorder", orderRef.Id, new ColumnSet(orderColumns));
                if (order != null && order.Id != Guid.Empty)
                {
                    EntityReference customer = order.GetAttributeValue<EntityReference>("customerid");
                    EntityReference accountName = order.GetAttributeValue<EntityReference>("new_accountnameid");

                    string orderNumber = order.GetAttributeValue<string>("new_faturano");
                    decimal? interestOrderAmount = order.GetAttributeValue<decimal?>("new_interestorderamount");

                    #region Create Interest Order

                    #region Order Fields
                    Entity newOrder = new Entity("salesorder");
                    newOrder["customerid"] = customer;
                    newOrder["new_accountnameid"] = accountName;
                    newOrder["ownerid"] = new EntityReference("systemuser", new Guid(appSetting.GetZaferErtasSystemUserId));
                    newOrder["name"] = "Vade Farkı Faturası";
                    newOrder["new_faturano"] = orderNumber;
                    newOrder["new_productid"] = new EntityReference("product", new Guid(appSetting.GetOtherProductId));
                    newOrder["new_territoryid"] = new EntityReference("territory", new Guid(appSetting.GetTurkeyTerritoryId));
                    newOrder["pricelevelid"] = new EntityReference("pricelevel", new Guid(appSetting.GetTLPesinPrieLevelId));
                    newOrder["new_mainorderid"] = orderRef;
                    #endregion

                    #region Add Order Product
                    EntityCollection cl = new EntityCollection();
                    cl.EntityName = "salesorderdetail";

                    Entity orderProduct = new Entity("salesorderdetail");
                    orderProduct["productid"] = new EntityReference("product", new Guid(appSetting.GetOtherProductId));
                    orderProduct["uomid"] = new EntityReference("uom", new Guid(appSetting.GetPieceUomId));
                    // orderProduct["defaultuomscheduleid"] = new EntityReference("uomschedule", new Guid(appSetting.GetDefaultUomScheduleId));

                    if (interestOrderAmount != null)
                        orderProduct["priceperunit"] = new Money(interestOrderAmount.Value);
                    else
                        orderProduct["priceperunit"] = new Money(0);

                    orderProduct["quantity"] = Convert.ToDecimal(1);
                    orderProduct["ispriceoverridden"] = true;
                    orderProduct["new_ordertip"] = new OptionSetValue((int)OrderTypes.Other);
                    orderProduct["transactioncurrencyid"] = new EntityReference("transactioncurrency", new Guid(appSetting.GetTLCurencyId));

                    cl.Entities.Add(orderProduct);

                    if (cl.Entities.Count > 0)
                        newOrder.RelatedEntities.Add(new Relationship("order_details"), cl);
                    #endregion

                    newOrderId = crmService.Create(newOrder);
                    //crmService.AssignTeamorUser(new EntityReference("systemuser", new Guid()), new EntityReference("salesorder", newOrderId));
                    #endregion
                }

                if (newOrderId != Guid.Empty)
                    context.OutputParameters["InterestOrderId"] = new EntityReference("salesorder", newOrderId);
                #endregion
            }
            catch (Exception ex){
                throw new InvalidPluginExecutionException("An error occured in new_CreateInterestOrder Action" + Environment.NewLine + ex.Message);
            }
        }

Projemizi plugin registration tool üzerinden register edelim ve actionımıza yeni bir step girelim.

Bu kısımda sdk nın mesajları haricinde kendi oluşturduğumuz processimizi seçelim.


Sıra geldi actionımızı nasıl çağıracağımıza .Bunun için 2 yol vardır .

C# kodu üzerinden çağırma

  OrganizationRequest req = new OrganizationRequest("new_CreateInterestOrder");
  req.Parameters.Add("OrderId", new EntityReference("salesorder", new Guid("B5A899C7-0792-E611-80F8-5065F38BE401")));
   OrganizationResponse response = crmService.Execute(req);

    if (response.Results.Contains("InterestOrderId")){
        EntityReference er = (EntityReference)response.Results["InterestOrderId"];
     }

Javascript kod üzerinden çağırma

Bunun için aşağıdaki Process.js kütüphanesini kullanacağız. Kullanımı oldukça basit olup her tarayıcıda sornsuz çalışmaktadır.


Process.js Kullanımı

function CallCreateInterestOrderAction() {
    try {
        // Call an Action
        Process.callAction("new_CreateInterestOrder",
        [{
            key: "OrderId",
            type: Process.Type.EntityReference,
            value: new Process.EntityReference("salesorder", Xrm.Page.data.entity.getId())
        }],

        function (outputParams) {
            // Success
            var formLangId = Xrm.Page.context.getUserLcid();
            if (formLangId != 1055)
                alert("Interest order created.");
            else
                alert("Vade farkı faturası oluşturuldu.");


            if (outputParams != null) {
                if (outputParams["InterestOrderId"] != null && outputParams["InterestOrderId"].id != null) {
                    var options = {
                        openInNewWindow: true
                    };
                    Xrm.Utility.openEntityForm(outputParams["InterestOrderId"].entityType, outputParams["InterestOrderId"].id, null, options);
                }
            }

            Xrm.Utility.openEntityForm("salesorder", Xrm.Page.data.entity.getId());
        },

        function (e, t) {
            // Error
            alert(e);

            // Write the trace log to the dev console
            if (window.console && console.error) {
                console.error(e + "\n" + t);
            }
        });
    }
    catch (e) {
        alert("An error occured in CallCreateInterestOrderAction Function.\nError" + e.message);
    }
}

Process.js

var Process = Process || {};

// Supported Action input parameter types
Process.Type = {
    Bool: "c:boolean",
    Float: "c:double", // Not a typo
    Decimal: "c:decimal",
    Int: "c:int",
    String: "c:string",
    DateTime: "c:dateTime",
    Guid: "c:guid",
    EntityReference: "a:EntityReference",
    OptionSet: "a:OptionSetValue",
    Money: "a:Money",
    Entity: "a:Entity",
    EntityCollection: "a:EntityCollection"
}

// inputParams: Array of parameters to pass to the Action. Each param object should contain key, value, and type.
// successCallback: Function accepting 1 argument, which is an array of output params. Access values like: params["key"]
// errorCallback: Function accepting 1 argument, which is the string error message. Can be null.
// Unless the Action is global, you must specify a 'Target' input parameter as EntityReference
// actionName is required
Process.callAction = function (actionName, inputParams, successCallback, errorCallback, url) {
    var ns = {
        "": "http://schemas.microsoft.com/xrm/2011/Contracts/Services",
        ":s": "http://schemas.xmlsoap.org/soap/envelope/",
        ":a": "http://schemas.microsoft.com/xrm/2011/Contracts",
        ":i": "http://www.w3.org/2001/XMLSchema-instance",
        ":b": "http://schemas.datacontract.org/2004/07/System.Collections.Generic",
        ":c": "http://www.w3.org/2001/XMLSchema",
        ":d": "http://schemas.microsoft.com/xrm/2011/Contracts/Services",
        ":e": "http://schemas.microsoft.com/2003/10/Serialization/",
        ":f": "http://schemas.microsoft.com/2003/10/Serialization/Arrays",
        ":g": "http://schemas.microsoft.com/crm/2011/Contracts",
        ":h": "http://schemas.microsoft.com/xrm/2011/Metadata",
        ":j": "http://schemas.microsoft.com/xrm/2011/Metadata/Query",
        ":k": "http://schemas.microsoft.com/xrm/2013/Metadata",
        ":l": "http://schemas.microsoft.com/xrm/2012/Contracts",
        //":c": "http://schemas.microsoft.com/2003/10/Serialization/" // Conflicting namespace for guid... hardcoding in the _getXmlValue bit
    };

    var requestXml = "<s:Envelope";

    // Add all the namespaces
    for (var i in ns) {
        requestXml += " xmlns" + i + "='" + ns[i] + "'";
    }

    requestXml += ">" +
          "<s:Body>" +
            "<Execute>" +
              "<request>";

    if (inputParams != null && inputParams.length > 0) {
        requestXml += "<a:Parameters>";

        // Add each input param
        for (var i = 0; i < inputParams.length; i++) {
            var param = inputParams[i];

            var value = Process._getXmlValue(param.key, param.type, param.value);

            requestXml += value;
        }

        requestXml += "</a:Parameters>";
    }
    else {
        requestXml += "<a:Parameters />";
    }

    requestXml += "<a:RequestId i:nil='true' />" +
                "<a:RequestName>" + actionName + "</a:RequestName>" +
              "</request>" +
            "</Execute>" +
          "</s:Body>" +
        "</s:Envelope>";

    Process._callActionBase(requestXml, successCallback, errorCallback, url);
}

// Runs the specified workflow for a particular record
// successCallback and errorCallback accept no arguments
// workflowId, and recordId are required
Process.callWorkflow = function (workflowId, recordId, successCallback, errorCallback, url) {
    if (url == null) {
        url = Xrm.Page.context.getClientUrl();
    }

    var request = "<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>" +
          "<s:Body>" +
            "<Execute xmlns='http://schemas.microsoft.com/xrm/2011/Contracts/Services' xmlns:i='http://www.w3.org/2001/XMLSchema-instance'>" +
              "<request i:type='b:ExecuteWorkflowRequest' xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts' xmlns:b='http://schemas.microsoft.com/crm/2011/Contracts'>" +
                "<a:Parameters xmlns:c='http://schemas.datacontract.org/2004/07/System.Collections.Generic'>" +
                  "<a:KeyValuePairOfstringanyType>" +
                    "<c:key>EntityId</c:key>" +
                    "<c:value i:type='d:guid' xmlns:d='http://schemas.microsoft.com/2003/10/Serialization/'>" + recordId + "</c:value>" +
                  "</a:KeyValuePairOfstringanyType>" +
                  "<a:KeyValuePairOfstringanyType>" +
                    "<c:key>WorkflowId</c:key>" +
                    "<c:value i:type='d:guid' xmlns:d='http://schemas.microsoft.com/2003/10/Serialization/'>" + workflowId + "</c:value>" +
                  "</a:KeyValuePairOfstringanyType>" +
                "</a:Parameters>" +
                "<a:RequestId i:nil='true' />" +
                "<a:RequestName>ExecuteWorkflow</a:RequestName>" +
              "</request>" +
            "</Execute>" +
          "</s:Body>" +
        "</s:Envelope>";

    var req = new XMLHttpRequest();
    req.open("POST", url + "/XRMServices/2011/Organization.svc/web", true);

    req.setRequestHeader("Accept", "application/xml, text/xml, */*");
    req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
    req.onreadystatechange = function () {
        if (req.readyState == 4) {
            if (req.status == 200) {
                if (successCallback) {
                    successCallback();
                }
            }
            else {
                if (errorCallback) {
                    errorCallback();
                }
            }
        }
    };

    req.send(request);
}

// Pops open the specified dialog process for a particular record
// dialogId, entityName, and recordId are required
// callback fires even if the dialog is closed/cancelled
Process.callDialog = function (dialogId, entityName, recordId, callback, url) {
    tryShowDialog("/cs/dialog/rundialog.aspx?DialogId=%7b" + dialogId + "%7d&EntityName=" + entityName + "&ObjectId=" + recordId, 600, 400, callback, url);

    // Function copied from Alert.js v1.0 https://alertjs.codeplex.com
    function tryShowDialog(url, width, height, callback, baseUrl) {
        width = width || Alert._dialogWidth;
        height = height || Alert._dialogHeight;

        var isOpened = false;

        try {
            // Web (IE, Chrome, FireFox)
            var Mscrm = Mscrm && Mscrm.CrmDialog && Mscrm.CrmUri && Mscrm.CrmUri.create ? Mscrm : parent.Mscrm;
            if (Mscrm && Mscrm.CrmDialog && Mscrm.CrmUri && Mscrm.CrmUri.create) {
                // Use CRM light-box (unsupported)
                var crmUrl = Mscrm.CrmUri.create(url);
                var dialogwindow = new Mscrm.CrmDialog(crmUrl, window, width, height);

                // Allows for opening non-webresources (e.g. dialog processes) - CRM messes up when it's not a web resource (unsupported)
                if (!crmUrl.get_isWebResource()) {
                    crmUrl.get_isWebResource = function () { return true; }
                }

                // Open the lightbox
                dialogwindow.show();
                isOpened = true;

                // Make sure when the dialog is closed, the callback is fired
                // This part's all pretty unsupported, hence the try-catches
                // If you can avoid using a callback, best not to use one
                if (callback) {
                    try {
                        // Get the lightbox iframe (unsupported)
                        var $frame = parent.$("#InlineDialog_Iframe");
                        if ($frame.length == 0) { $frame = parent.parent.$("#InlineDialog_Iframe"); }
                        $frame.load(function () {
                            try {
                                // Override the CRM closeWindow function (unsupported)
                                var frameDoc = $frame[0].contentWindow;
                                var closeEvt = frameDoc.closeWindow; // OOTB close function
                                frameDoc.closeWindow = function () {
                                    // Bypasses onunload event on dialogs to prevent "are you sure..." (unsupported - doesn't work with 2015 SP1)
                                    try { frameDoc.Mscrm.GlobalVars.$B = false; } catch (e) { }

                                    // Fire the callback and close
                                    callback();
                                    try { closeEvt(); } catch (e) { }
                                }
                            } catch (e) { }
                        });
                    } catch (e) { }
                }
            }
        } catch (e) { }

        try {
            // Outlook
            if (!isOpened && window.showModalDialog) {
                // If lightbox fails, use window.showModalDialog
                baseUrl = baseUrl || Xrm.Page.context.getClientUrl();
                var params = "dialogWidth:" + width + "px; dialogHeight:" + height + "px; status:no; scroll:no; help:no; resizable:yes";

                window.showModalDialog(baseUrl + url, window, params);
                if (callback) {
                    callback();
                }

                isOpened = true;
            }
        } catch (e) { }

        return isOpened;
    }
}

Process._emptyGuid = "00000000-0000-0000-0000-000000000000";

// This can be used to execute custom requests if needed - useful for me testing the SOAP :)
Process._callActionBase = function (requestXml, successCallback, errorCallback, url) {
    if (url == null) {
        url = Xrm.Page.context.getClientUrl();
    }

    var req = new XMLHttpRequest();
    req.open("POST", url + "/XRMServices/2011/Organization.svc/web", true);
    req.setRequestHeader("Accept", "application/xml, text/xml, */*");
    req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");

    req.onreadystatechange = function () {
        if (req.readyState == 4) {
            if (req.status == 200) {
                // If there's no successCallback we don't need to check the outputParams
                if (successCallback) {
                    // Yucky but don't want to risk there being multiple 'Results' nodes or something
                    var resultsNode = req.responseXML.childNodes[0].childNodes[0].childNodes[0].childNodes[0].childNodes[1]; // <a:Results>

                    // Action completed successfully - get output params
                    var responseParams = Process._getChildNodes(resultsNode, "a:KeyValuePairOfstringanyType");

                    var outputParams = {};
                    for (i = 0; i < responseParams.length; i++) {
                        var attrNameNode = Process._getChildNode(responseParams[i], "b:key");
                        var attrValueNode = Process._getChildNode(responseParams[i], "b:value");

                        var attributeName = Process._getNodeTextValue(attrNameNode);
                        var attributeValue = Process._getValue(attrValueNode);

                        // v1.0 - Deprecated method using key/value pair and standard array
                        //outputParams.push({ key: attributeName, value: attributeValue.value });

                        // v2.0 - Allows accessing output params directly: outputParams["Target"].attributes["new_fieldname"];
                        outputParams[attributeName] = attributeValue.value;

                        /*
                        RETURN TYPES:
                            DateTime = Users local time (JavaScript date)
                            bool = true or false (JavaScript boolean)
                            OptionSet, int, decimal, float, etc = 1 (JavaScript number)
                            guid = string
                            EntityReference = { id: "guid", name: "name", entityType: "account" }
                            Entity = { logicalName: "account", id: "guid", attributes: {}, formattedValues: {} }
                            EntityCollection = [{ logicalName: "account", id: "guid", attributes: {}, formattedValues: {} }]
 
                        Attributes for entity accessed like: entity.attributes["new_fieldname"].value
                        For entityreference: entity.attributes["new_fieldname"].value.id
                        Make sure attributes["new_fieldname"] is not null before using .value
                        Or use the extension method entity.get("new_fieldname") to get the .value
                        Also use entity.formattedValues["new_fieldname"] to get the string value of optionsetvalues, bools, moneys, etc
                        */
                    }

                    // Make sure the callback accepts exactly 1 argument - use dynamic function if you want more
                    successCallback(outputParams);
                }
            }
            else {
                // Error has occured, action failed
                if (errorCallback) {
                    var message = null;
                    var traceText = null;
                    try {
                        message = Process._getNodeTextValueNotNull(req.responseXML.getElementsByTagName("Message"));
                        traceText = Process._getNodeTextValueNotNull(req.responseXML.getElementsByTagName("TraceText"));
                    } catch (e) { }
                    if (message == null) { message = "Error executing Action. Check input parameters or contact your CRM Administrator"; }
                    errorCallback(message, traceText);
                }
            }
        }
    };

    req.send(requestXml);
}

// Get only the immediate child nodes for a specific tag, otherwise entitycollections etc mess it up
Process._getChildNodes = function (node, childNodesName) {
    var childNodes = [];

    for (var i = 0; i < node.childNodes.length; i++) {
        if (node.childNodes[i].tagName == childNodesName) {
            childNodes.push(node.childNodes[i]);
        }
    }

    // Chrome uses just 'Results' instead of 'a:Results' etc
    if (childNodes.length == 0 && childNodesName.indexOf(":") !== -1) {
        childNodes = Process._getChildNodes(node, childNodesName.substring(childNodesName.indexOf(":") + 1));
    }

    return childNodes;
}

// Get a single child node for a specific tag
Process._getChildNode = function (node, childNodeName) {
    var nodes = Process._getChildNodes(node, childNodeName);

    if (nodes != null && nodes.length > 0) { return nodes[0]; }
    else { return null; }
}

// Gets the first not null value from a collection of nodes
Process._getNodeTextValueNotNull = function (nodes) {
    var value = "";

    for (var i = 0; i < nodes.length; i++) {
        if (value === "") {
            value = Process._getNodeTextValue(nodes[i]);
        }
    }

    return value;
}

// Gets the string value of the XML node
Process._getNodeTextValue = function (node) {
    if (node != null) {
        var textNode = node.firstChild;
        if (textNode != null) {
            return textNode.textContent || textNode.nodeValue || textNode.data || textNode.text;
        }
    }

    return "";
}

// Gets the value of a parameter based on its type, can be recursive for entities
Process._getValue = function (node) {
    var value = null;
    var type = null;

    if (node != null) {
        type = node.getAttribute("i:type") || node.getAttribute("type");

        // If the parameter/attribute is null, there won't be a type either
        if (type != null) {
            // Get the part after the ':' (since Chrome doesn't have the ':')
            var valueType = type.substring(type.indexOf(":") + 1).toLowerCase();

            if (valueType == "entityreference") {
                // Gets the lookup object
                var attrValueIdNode = Process._getChildNode(node, "a:Id");
                var attrValueEntityNode = Process._getChildNode(node, "a:LogicalName");
                var attrValueNameNode = Process._getChildNode(node, "a:Name");

                var lookupId = Process._getNodeTextValue(attrValueIdNode);
                var lookupName = Process._getNodeTextValue(attrValueNameNode);
                var lookupEntity = Process._getNodeTextValue(attrValueEntityNode);

                value = new Process.EntityReference(lookupEntity, lookupId, lookupName);
            }
            else if (valueType == "entity") {
                // Gets the entity data, and all attributes
                value = Process._getEntityData(node);
            }
            else if (valueType == "entitycollection") {
                // Loop through each entity, returns each entity, and all attributes
                var entitiesNode = Process._getChildNode(node, "a:Entities");
                var entityNodes = Process._getChildNodes(entitiesNode, "a:Entity");

                value = [];
                if (entityNodes != null && entityNodes.length > 0) {
                    for (var i = 0; i < entityNodes.length; i++) {
                        value.push(Process._getEntityData(entityNodes[i]));
                    }
                }
            }
            else if (valueType == "aliasedvalue") {
                // Gets the actual data type of the aliased value
                // Key for these is "alias.fieldname"
                var aliasedValue = Process._getValue(Process._getChildNode(node, "a:Value"));
                if (aliasedValue != null) {
                    value = aliasedValue.value;
                    type = aliasedValue.type;
                }
            }
            else {
                // Standard fields like string, int, date, money, optionset, float, bool, decimal
                // Output will be string, even for number fields etc
                var stringValue = Process._getNodeTextValue(node);

                if (stringValue != null) {
                    switch (valueType) {
                        case "datetime":
                            value = new Date(stringValue);
                            break;
                        case "int":
                        case "money":
                        case "optionsetvalue":
                        case "double": // float
                        case "decimal":
                            value = Number(stringValue);
                            break;
                        case "boolean":
                            value = stringValue.toLowerCase() === "true";
                            break;
                        default:
                            value = stringValue;
                    }
                }
            }
        }
    }

    return new Process.Attribute(value, type);
}

Process._getEntityData = function (entityNode) {
    var value = null;

    var entityAttrsNode = Process._getChildNode(entityNode, "a:Attributes");
    var entityIdNode = Process._getChildNode(entityNode, "a:Id");
    var entityLogicalNameNode = Process._getChildNode(entityNode, "a:LogicalName");
    var entityFormattedValuesNode = Process._getChildNode(entityNode, "a:FormattedValues");

    var entityLogicalName = Process._getNodeTextValue(entityLogicalNameNode);
    var entityId = Process._getNodeTextValue(entityIdNode);
    var entityAttrs = Process._getChildNodes(entityAttrsNode, "a:KeyValuePairOfstringanyType");

    value = new Process.Entity(entityLogicalName, entityId);

    // Attribute values accessed via entity.attributes["new_fieldname"]
    if (entityAttrs != null && entityAttrs.length > 0) {
        for (var i = 0; i < entityAttrs.length; i++) {

            var attrNameNode = Process._getChildNode(entityAttrs[i], "b:key")
            var attrValueNode = Process._getChildNode(entityAttrs[i], "b:value");

            var attributeName = Process._getNodeTextValue(attrNameNode);
            var attributeValue = Process._getValue(attrValueNode);

            value.attributes[attributeName] = attributeValue;
        }
    }

    // Formatted values accessed via entity.formattedValues["new_fieldname"]
    for (var j = 0; j < entityFormattedValuesNode.childNodes.length; j++) {
        var foNode = entityFormattedValuesNode.childNodes[j];

        var fNameNode = Process._getChildNode(foNode, "b:key")
        var fValueNode = Process._getChildNode(foNode, "b:value");

        var fName = Process._getNodeTextValue(fNameNode);
        var fValue = Process._getNodeTextValue(fValueNode);

        value.formattedValues[fName] = fValue;
    }

    return value;
}

Process._getXmlValue = function (key, dataType, value) {
    var xml = "";
    var xmlValue = "";

    var extraNamespace = "";

    // Check the param type to determine how the value is formed
    switch (dataType) {
        case Process.Type.String:
            xmlValue = Process._htmlEncode(value) || ""; // Allows fetchXml strings etc
            break;
        case Process.Type.DateTime:
            xmlValue = value.toISOString() || "";
            break;
        case Process.Type.EntityReference:
            xmlValue = "<a:Id>" + (value.id || "") + "</a:Id>" +
                  "<a:LogicalName>" + (value.entityType || "") + "</a:LogicalName>" +
                  "<a:Name i:nil='true' />";
            break;
        case Process.Type.OptionSet:
        case Process.Type.Money:
            xmlValue = "<a:Value>" + (value || 0) + "</a:Value>";
            break;
        case Process.Type.Entity:
            xmlValue = Process._getXmlEntityData(value);
            break;
        case Process.Type.EntityCollection:
            if (value != null && value.length > 0) {
                var entityCollection = "";
                for (var i = 0; i < value.length; i++) {
                    var entityData = Process._getXmlEntityData(value[i]);
                    if (entityData !== null) {
                        entityCollection += "<a:Entity>" + entityData + "</a:Entity>";
                    }
                }
                if (entityCollection !== null && entityCollection !== "") {
                    xmlValue = "<a:Entities>" + entityCollection + "</a:Entities>" +
                        "<a:EntityName i:nil='true' />" +
                        "<a:MinActiveRowVersion i:nil='true' />" +
                        "<a:MoreRecords>false</a:MoreRecords>" +
                        "<a:PagingCookie i:nil='true' />" +
                        "<a:TotalRecordCount>0</a:TotalRecordCount>" +
                        "<a:TotalRecordCountLimitExceeded>false</a:TotalRecordCountLimitExceeded>";
                }
            }
            break;
        case Process.Type.Guid:
            // I don't think guid fields can even be null?
            xmlValue = value || Process._emptyGuid;

            // This is a hacky fix to get guids working since they have a conflicting namespace :(
            extraNamespace = " xmlns:c='http://schemas.microsoft.com/2003/10/Serialization/'";
            break;
        default: // bool, int, double, decimal
            xmlValue = value || null;
            break;
    }

    xml = "<a:KeyValuePairOfstringanyType>" +
            "<b:key>" + key + "</b:key>" +
            "<b:value i:type='" + dataType + "'" + extraNamespace;

    // nulls crash if you have a non-self-closing tag
    if (xmlValue === null || xmlValue === "") {
        xml += " i:nil='true' />";
    }
    else {
        xml += ">" + xmlValue + "</b:value>";
    }

    xml += "</a:KeyValuePairOfstringanyType>";

    return xml;
}

Process._getXmlEntityData = function (entity) {
    var xml = null;

    if (entity != null) {
        var attrXml = "";

        for (field in entity.attributes) {
            var a = entity.attributes[field];
            var aXml = Process._getXmlValue(field, a.type, a.value);

            attrXml += aXml;
        }

        if (attrXml !== "") {
            xml = "<a:Attributes>" + attrXml + "</a:Attributes>";
        }
        else {
            xml = "<a:Attributes />";
        }

        xml += "<a:EntityState i:nil='true' />" +
            "<a:FormattedValues />" +
            "<a:Id>" + entity.id + "</a:Id>" +
            "<a:KeyAttributes />" +
            "<a:LogicalName>" + entity.logicalName + "</a:LogicalName>" +
            "<a:RelatedEntities />" +
            "<a:RowVersion i:nil='true' />";
    }

    return xml;
}

Process._htmlEncode = function (s) {
    if (typeof s !== "string") { return s; }

    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

Process.Entity = function (logicalName, id, attributes) {
    this.logicalName = logicalName || "";
    this.attributes = attributes || {};
    this.formattedValues = {};
    this.id = id || Process._emptyGuid;
}

// Gets the value of the attribute without having to check null
Process.Entity.prototype.get = function (key) {
    var a = this.attributes[key];
    if (a != null) {
        return a.value;
    }

    return null;
}

Process.EntityReference = function (entityType, id, name) {
    this.id = id || Process._emptyGuid;
    this.name = name || "";
    this.entityType = entityType || "";
}

Process.Attribute = function (value, type) {
    this.value = value || null;
    this.type = type || "";
}