Monday, January 4, 2016

Using the FormXML Library

Previously on Dr. Lambda's blog:

With Microsoft Dynamics CRM we can put client-side scripts on forms. Having a JavaScript file, we have to upload the script, go to the form editor, properties, add the dependencies (one at a time), register the call-back, save and publish the form. Finally, we can test our script, at which point we discover that we used the wrong name for the call-back or forgot one of the dependencies, not to mention that the JavaScript itself could have bugs, and we have to go through most of the process again.

module CrmUtils = begin
  val sdk_version : string
  val publish : unit -> unit
  module FormXml = begin
    type form_info
    val init : XRM.systemform -> form_info
    val register_dependency : string -> form_info -> form_info
    type event_type
    val event_type_to_string : event_type -> string
    val register_callback : event_type -> string -> string -> form_info -> form_info
    type validated
    val validate : form_info -> validated
    val commit : validated -> unit
  end
end

Now, the continuation...

Off the Shelf Usage

The library is intended to be usable without being an F# or functional programmer. Let's explore the library through some examples.

Example 1

Say we have a JavaScript file called Test.Feature.js, which we have already uploaded as a webresource. The file contains a function called onload. Now, we want to register a call-back for this funciton, on the main form for the account entity, which is called Account.

xrm.systemformSet.Individuals.Account
|> CrmUtils.FormXml.init
|> CrmUtils.FormXml.register_callback CrmUtils.FormXml.OnLoad "Test.Feature.js" "onload"
|> CrmUtils.FormXml.validate
|> CrmUtils.FormXml.commit
|> CrmUtils.publish

This will register the dependency and the call-back. This was easy, however imagine we need want to use the same feature on a different form, we only need to change the first line and run it again.

Example 2

The advantage of the library is even clearer if the file had had dependencies. For demonstration say it depended on Test.Utilities.js and jquery.js.

xrm.systemformSet.Individuals.Account
|> CrmUtils.FormXml.init
|> CrmUtils.FormXml.register_dependency "jquery.js"
|> CrmUtils.FormXml.register_dependency "Test.Utilities.js"
|> CrmUtils.FormXml.register_callback CrmUtils.FormXml.OnLoad "Test.Feature.js" "onload"
|> CrmUtils.FormXml.validate
|> CrmUtils.FormXml.commit
|> CrmUtils.publish

Going Beyond

While the library is neat off-the-shelf, it also enables us to go even further. But first, let's warm up with some easy, useful, and obvious features to support the library.

Some General CRM Utilities

Currently our CrmUtils is pretty bare-bones so let's extend it with a few extra functions. Here are functions for finding: a solution, the publisher of a solution, and the prefix used by that publisher.

let solution solName =
  xrm.solutionSet
  |> Seq.find (fun s -> s.uniquename = solName)
let solution_publisher solName =
  let sol = solution solName in
  xrm.publisherSet
  |> Seq.find (fun p -> p.Id = sol.publisherid.Id)
let solution_prefix solName =
  solName |> solution_publisher |> fun p -> p.customizationprefix

Form Selection

Selecting forms with the xrm.systemformSet.Individuals.-method has a few disadvantages: Many forms are called Information, only one is accessible with this method, and it is difficult to know which. Therefore, another nice addition would be a complete and convenient way to uniquely select a form. In order to do this we need to know the name and type of the form, and which entity it is on:

let select_form entity t name =
  xrm.systemformSet
  |> Seq.find (fun sf -> sf.objecttypecode = entity && sf.``type`` = t && sf.name = name)

At this point, we can reiterate example 1:

select_form "account" XRM.systemform_type.Main "Account"
|> CrmUtils.FormXml.init
|> CrmUtils.FormXml.register_dependency "jquery.js"
|> CrmUtils.FormXml.register_dependency "Test.Utilities.js"
|> CrmUtils.FormXml.register_callback CrmUtils.FormXml.OnLoad "Test.Feature.js" "onload"
|> CrmUtils.FormXml.validate
|> CrmUtils.FormXml.commit
|> CrmUtils.publish

Restoring

In the last post, we discussed the risks of using the library, and one of the relaxing arguments was that you could always restore a form, using a back. Let us express this in a function taking a form and a filename.

let restore file (form : XRM.systemform) =
  if form.LogicalName <> "systemform" then
    failwith "Not a form";
  let e = Entity () in
  e.Id <- form.Id;
  e.LogicalName <- "systemform";
  e.Attributes.Add("formxml", System.IO.File.ReadAllText(cfg.rootFolder + file));
  xrm.OrganizationService.Update(e);
  CrmUtils.publish ()

Notice that we publish directly, even though it is time consuming. This function is intended for if something goes wrong. If it does, the user would be under a lot of stress, thus restore should be easy to call, and the user shouldn't need to remember anything, like publishing.

Web Resources

Last week I also mentioned that I had functionality for uploading web resources. I did not think they belonged in the FormXml library, however, in this context it fits perfectly.

There are a few caveats. First, web resources need to be encoded in UTF8 base 64. Second, if the web resource is new we want to create it in the appropriate solution, this requires us to pass an extra parameter, SolutionUniqueName. Only, this parameter is only supported on requests, so instead of calling xrm.OrganizationService.Create we have to execute an explicit CreateRequest. Third, the name of the web resource has to be prefixed by the prefix from the chosen solution.

I have chosen to split file-upload into two, as it is useful to be able to upload strings directly from code, as we shall see later.

let upload_text solName name (content : string) =
  let prefix = CrmUtils.solution_prefix solName in
  let already_exists =
    xrm.webresourceSet
    |> Seq.tryFind (fun wr -> wr.name = prefix + "_" + name) in
  let wf = Entity () in
  wf.LogicalName <- "webresource";
  wf.Attributes.Add("name", prefix + "_" + name);
  wf.Attributes.Add("displayname", name);
  wf.Attributes.Add("webresourcetype", OptionSetValue (int XRM.webresource_webresourcetype.``Script (JScript)``));
  wf.Attributes.Add("content", System.Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content)));
  match already_exists with
    | None ->
      let cr = Messages.CreateRequest () in
      cr.Target <- wf;
      cr.Parameters.Add("SolutionUniqueName", solName);
      xrm.OrganizationService.Execute(cr) |> ignore
    | Some wr ->
      wf.Id <- wr.Id;
      xrm.OrganizationService.Update(wf)
let upload_file solName file =
  System.IO.File.ReadAllText(cfg.rootFolder + @"\" + file)
  |> upload_text solName file

Turning it up to eleven

There is still one final function missing in order to solve the original problem. One unifying function that, given a form, will generate the initial JavaScript, upload it to CRM, and finally register the dependency and call-back. But before we can write that we need to define what should be in our JavaScript files.

let javascript t org (fi : CrmUtils.FormXml.form_info) =
  let fname = CrmUtils.FormXml.event_type_to_string t in
  "var " + org + ";\n" +
  "(function (" + org + ") {\n" +
  "  var " + fi.name + ";\n" +
  "  (function (" + fi.name + ") {\n" +
  "    var Form = Xrm.Page;\n" +
  "    function " + fname + "() {\n" +
  "    }\n" +
  "    " + fi.name + "." + fname + " = " + fname + ";\n" +
  "  }) (" + fi.name + " = " + org + "." + fi.name +
  " || (" + org + "." + fi.name + " = {}));\n"+
  "}) (" + org + " || (" + org + " = {}));"

I like to organize my JavaScript into modules, prefixed by the org argument.

This following function does solves the problem, although it also needs the solution name, the call-back type.

let new_script solName t org form =
  let fi = form |> CrmUtils.FormXml.init in
  let filename = org + "." + fi.name + ".js" in
  let prefix = CrmUtils.solution_prefix solName in
  let name = prefix + "_" + filename in
  if System.IO.File.Exists (cfg.rootFolder + @"\" + filename)
  then
    printf "Error: File '%s' already exists\n" filename;
    exit (1)
  let already_in_crm =
    xrm.webresourceSet
    |> Seq.exists (fun wr -> wr.name = name) in
  if already_in_crm
  then
    printf "Error: File '%s' already in crm\n" name;
    exit (1)
  let content = javascript t org fi in
  upload_text solName filename content;
  System.IO.File.WriteAllText(cfg.rootFolder + @"\" + filename, content);
  fi
  |> CrmUtils.FormXml.register_callback t name (org + "." + fi.name + "." + CrmUtils.FormXml.event_type_to_string t)
  |> CrmUtils.FormXml.validate
  |> CrmUtils.FormXml.commit

More Examples

After all that, working with scripts looks like this:

Start by running:

select_form "contact" XRM.systemform_type.Main "Information"
|> new_script "DrLambda" CrmUtils.FormXml.OnLoad "Test"
|> CrmUtils.publish

Then open Test.Information.js and code.

Then you can update the script with:

upload_file "DrLambda" "Test.Information.js"
|> CrmUtils.publish

Then, when you split up your code into multiple files, or start using libraries you just run:

select_form "contact" XRM.systemform_type.Main "Information"
|> CrmUtils.FormXml.init
|> CrmUtils.FormXml.register_dependency "jquery.js"
|> CrmUtils.FormXml.register_dependency "Test.Utilities.js"
|> CrmUtils.FormXml.validate
|> CrmUtils.FormXml.commit
|> CrmUtils.publish

More common operations require less work.

No comments:

Post a Comment