Saturday, October 14, 2017

dynamics, form programming and an eventbus

If you add multiple web resources to a form, you may need to communicate between the components.
You have a number of ways to do this. Let's assume that a grid that issues an "OnRecordSelect" event and you want "detail" WebResources to display some details of the selection. Hence, the detail components need to know which Contact was selected.
WebResources are loaded in iframes. The iframe structure changed slightly between the web and unified (UCI) UIs in dynamics. In general, form programming has always been a bit hit-or-miss with some of the APIs. v9.0 APIs help, but you may still want to clean up inter-WebResource communication.
A few ways you can handle the communication are:
  • Create a handler that responds to the selection change and sets the URL via .setSrc on a WebResource control (whose getObject() is an iframe in the web UI).
  • Create a handler that responds to the seelction change and calls a well-known function in the global space (hence attached to the widow) of the iframe.
  • Create a handler that responds to the selection change and calls an event bus function. WebResources register for event dispatches when they load into their iframe.
All of these methods work fine but the first two are a bit brittle. Note that setting the URL multiple times on a WebResource iframe refreshes the webresource and may cause flicker. Calling well known functions on the the iframe works well but may have undesirable consequences. It certainly does not scale well if you have multiple events to communicate back and forth.
An event bus can be easily created and attached to a well known location. WebResources can then find the event buss and subscribe to events. Its fairly easy to do. We can use typescript to do this and use some of the usual global variable patterns with namespaces to create a global variable that points to an eventbus.
Webpack can perform the code bundling using a "var" libarary target. As long as you ensure that an onLoad function is called to set the event bus into the proper window location e.g. window.parent.eventbus, you effectively set a "global" variable holding the eventbus into a well know location that subscribers can find. If you are creating a small amount of code and the rest of the project does not have any other large javascript components, webpack is overkill. But if you have alot of custom components and they are complex, webpack can make alot of sense.
WebResources are loaded into their own "client script" iframe which is not the toplevel. WebResources follow the same pattern. The iframe naming and positioning pattern changed slightly between the web and unified UIs. However, both types of client-side artifacts load as siblings and hence the parent iframe (iframe=window) is still a good place to attach a resource that is needed by multiple children. By attaching to the one-up window, you may not be attaching the eventbus instance to the topmost window but it does not matter as long as the sibling loaded scripts can find the eventbus in a well-known location.

Typescript EventBus

We can author a very quick event bus:
/**
 * Simple Dynamics event bus. Designed to be used for simple needs
 * and attached to a window that sub-iframes can access easily so their
 * WebResources can listen for events from across a Dynamics form 
 * e.g. a grid select needs.
 */

/** Subscriber takes a simple event object differentiated by type. */
export type Subscriber = ({type: string}) => void

/** A thunk that unsubscribes when called. */
export type Cancellable = () => void

/** Small pubsub, synchronous dispatch. */
export class EventBus {
    private subscribers: Array<Subscriber> = []

    public unsubscribe(key) {
        this.subscribers = this.subscribers.filter(s => s !== key)
    }
    
    /** Subscriber is just a thunk. Thunk returned unsubscribes. */
    public subscribe(subscriber: Subscriber) {
        if(subscriber) this.subscribers.push(subscriber)
        return(() => this.unsubscribe(subscriber))
    }

    /** Dispatch an event. All subscribers get all events like redux. */
    public dispatch(event) {
        this.subscribers.forEach(sub => {
            if(sub) sub(event)
        })
    }
}

export default EventBus

export interface Window {
    eventbus: EventBus;
}
That's it!

ES6 Module

We need a module to expose a single instance of the EventBus. Let's assume that we want to use capitals for the eventbus name (EventBus) in the global namespace and expose both a single instance usable for the entire form and a function that will set the event bus after form load. It's not critical to have an onLoad function but you may want to perform other processing in the load process such as issue an "load" event. We use an onLoad function below that must be called by the Dynamics form processor and is specified in the form properties event list.
/** 
 * Global event bus. This module should only be loaded once
 * for a Dynamics form typically as part of loading form
 * scripts. The eventbus can be set on the global window
 * using your bundler e.g. webpack with libraryTarget="var".
 */
import EventBus from "../Dynamics/EventBus"

/** Main instance for a form. */
export const eventbus = new EventBus()

/** 
 * Arrange to have this called after the script is loaded.
 * Place the form "global" instance into your designed location
 * Coordinate with your other form components
 * to find it at the designated location.
 */                                                          
export function onLoad(ctx:any): void {
    // @ts-ignore
    window.parent.eventbus = eventbus
}
We call this module EventBus.ts and place it a source directory called "src/form/EventBus.ts". The EventBuss class is located at another location, say, "src/Dynamics/EventBus.ts". The module above exports an eventbus instance and the onLoad event places the instance in lowercase "eventbus". Change this to suit your own conventions.
Our ES6 module exports both the instance from the module as well as a function for the onLoad handler.
Depending on your bundler, you can expose these ES6 module values in different ways. The typescript compiler acts as a simple bundler if you want but we will use webpack. Webpack can expose an object call EventBus (the module/namespace) in the window loads the script.

Webpack Config to implement a "Global"

Webpack can take a ES6 module and expose the content as a var in the global namespace. The webpack output needs to be set to a library type and we specify that a "var" should be created to whatever is exporte from the module above. There are a number of ways to do this in webpack that automatically attaches the module's exports to window.parent.EventBus but we will do it explicitly with a "var." As mentioned at the start, we want to attach the EventBus module exports to window.parent one up from the iframe load point. That's done in our onLoad function. WebResource siblings will need to access it at window.parent.eventbus.
The key part of webpack config is:
const configFragment = {
    entry: {
        "EventBus": path.join(srcdir, "form/EventBus.ts")
    },
    output: {
        path: path.join(__dirname, "dist/js"),
        filename: "[name].js",
        library: "[name]",
        libraryTarget: "var"
    }
})
The name (EventBus) is indicated in the entry point implies that the "var = ..." construct created by webpack will be "var EventBus = ...". I tend to use typescript only as a compiler, always using esnext, and use babel for transpiling down to my target javascript version:
{
    "compilerOptions": {
        "jsx": "react",
        "target": "esnext",
        "strictNullChecks": true
	,"allowJs": true
	,"importHelpers": false
	,"noEmitHelpers": true
	,"sourceMap": true
	,"experimentalDecorators": true
	,"allowSyntheticDefaultImports": true
	,"typeRoots": ["./typings", "./node_modules/@types/"]
	,"sourceMap": true
    }
    ,"exclude": ["node_modules"]
}
and by .babelrc
{
    "presets": [
        ["env", {
            "browsers": ["last 3 major Chrome versions"]
        }],
        "react",
	"flow"
    ],
    "plugins": [
        ["transform-runtime",
         {
             "useESModules": true
         }],
        "transform-object-rest-spread",
        "transform-class-properties"
    ]
}
I've not shown the loaders section but use babel-loader and ts-loader with ts-loader running first of course.
The result of running webpack will be a file that starts with:
var Eventbus = ...a whole bunch of webpack stuff...
Do not forget to run webpack with the -p option to automatically activate UglifyJSPlugin which reduces the size of the webpack output (the bundle). You'll always have a file larger than if you hand crafted the JS or used typescript namespaces, but you you get other benefits from using webpack bundling processing. But you need to choose how you deploy.

EventBus Publishing and WebResource Consumption

In the Dynamics form editor, we would include "publisher_/js/EventBus.js" (the output of the webpack bundling process) and set the onLoad event to call "EventBus.onLoad". We would do this once on the form. While this places the event bus into a well known location we still need to publish and consume events.
To publish, we would load another script that that responds to the select event. In our example we were responding to a grid selection event, say, from an editable grid:
// some other script that is specific to the control we want to broadcast changes about to our eventbus
onRecordChanged(ctx) {
  const fctx = ctx.getFormContext()
  const newId = fctx.data.entity.attributes("contactid").getValue()[0].id // from the grid context for example
  const bus = myeventbus()
  if(bus) {
    bus.dispatch({type:"CONTACT_CHANGED", contactId: newId})
  }
}
function myeventbus() { return window.parent.eventbus } // No nameclashes so could call this function eventbus().
We use a function to reference eventbus because based on the loading sequence, eventbus may not be defined at the time this script is loaded. There are no order guarantees in Dynamics for external form script loading. Notice that the eventbus instance (lowercase) is accessed at the well known location, one-up from the iframe that loads our publishing script.
Another webresource, the one that display details about the selected entity, needs to subscribe to the change event. Typically the widget will be created in code. For example, a UI.js file holds the exports myUI as a namespace and a function called setContactId. We need to arrange for setContactId to be called when an event is received.
<html>
...
<body>
...
<script src="./UI.js"></script>
</body>
...
<script>
if(window.parent.eventbus) {
  window.parent.eventbus.subscribe(event => {
   if(event.type === "CONTACT_CHANGED") { myUI.setContactId(event.contactId) }
  })
}
...
// other code to start your UI
...
</script>
...
</html>
It is possible that the eventbus script that attaches the eventbus instance to window.parent is not loaded at the time that the WebResource script part in the HTML file runs.
You can use jquery's $(document).ready() or window.load event listeners or some other tricks to help sequence the load. However, just like with Xrm, where order may be severely changed based on uncontrollable factors, you should really place subscribe code in a Promise that waits for the eventbus variable to become available. But that's another blog.

Sunday, September 24, 2017

dynamics crm advanced form navigation - pivot instead of hamburger

Sometimes you need to make it easier to switch between tabs in a dynamics crm form. Here's how you can use office fabric ui to make a "pivot". Just add this to your webpack build.
/** Navigation via Pivot from Office UI Fabric. */
import React from "react"
import { render } from "react-dom"

import {Fabric} from "office-ui-fabric-react/lib"
import { Pivot, PivotItem }
from 'office-ui-fabric-react/lib/Pivot';

/**
 * Manage state as best you can given that all changes are side-effects to dynamics.
 * There is not an easy way to detect when the tabs are changed external to
 * react. Tabs hidden at the start are always hidden. Tabs are set once in props once
 * at creation time and cannot be changed.
 */
export class DynamicsPivot extends React.Component {

    static ALL = -1
    
    constructor(props) {
        super(props)

        let alwaysHidden = props.alwaysHidden || []
        if(props.tabs && !props.alwaysHidden) {
            // tabs is a fake array
            for(let i=0; i< props.tabs.getLength(); i++) {
                const t = props.tabs.get(i)
                if(!t.getVisible()) alwaysHidden.push(i)
            }
        }
        
        this.state = {
            alwaysHidden, // indexes to hide, 0-based
            selectedIndex: props.selectedIndex ||
                           ((props.tabs && props.tabs.getLength() > 0) ? 0 : DynamicsPivot.ALL),
            tabs: props.tabs,
            // true=>collapse un-selected tabs but keep visible
            // otherwise un-selected tabs are not visible
            collapse: props.collapse,
            labelMap: props.labelMap, // use these labels instead of actual tab labels
        }
    }

    static defaultProps = {
        alwaysHidden: null,
        collapse: true,
        tabs: null,
        labelMap: {}
    }

    /** Get a label for a tab. Uses labelMap. */
    getLabel = (t) => {
        const lab = t.getLabel()
        const calc = this.state.labelMap[lab]
        if(calc) return calc
        return lab
    }
    
    /** Hide all but selected. This has side effects not visible to react. */
    processTabs = () => {
        const selected = this.state.selectedIndex
        const haveTabs = this.state.tabs
        if(haveTabs) {
            for(let i=0; i < this.state.tabs.getLength(); i++) {
                const shouldHide = this.state.alwaysHidden.includes(i)
                const t = this.state.tabs.get(i)
                if((i !== selected || shouldHide) && selected != DynamicsPivot.ALL) {
                    // hide
                    if(this.state.collapse) t.setDisplayState("collapsed")
                    else t.setVisible(false)
                }
                else if(!shouldHide) {
                    t.setDisplayState("expanded")
                    t.setVisible(true)
                }
            }
            const focusIndex = (selected === DynamicsPivot.ALL &&
                                haveTabs && this.state.tabs.getLength()>0) ? 0 :
                               (haveTabs &&
                                selected < this.state.tabs.getLength() ? selected : -1)
            if(haveTabs && focusIndex >= 0)
                this.state.tabs.get(focusIndex).setFocus()
        }
    }

    handleClick = (pivotItem) =>
        this.setState({selectedIndex: parseInt(pivotItem.props.itemKey)})

    render() {
        const pivots = []
        for(let i=0; i<this.state.tabs.getLength(); i++) {
            const t = this.state.tabs.get(i)
            if(!this.state.alwaysHidden.includes(i))
                pivots.push({linkText: this.getLabel(t), i})
        }
        pivots.push({linkText: "All", i: DynamicsPivot.ALL}) // ALL is last
        this.processTabs() // kind of like a render
        return (
            <Pivot headersOnly onLinkClick={this.handleClick}>
                {
                    pivots.map(p =>
                        <PivotItem linkText={p.linkText} itemKey={p.i} key={p.i}/>)
                }
            </Pivot>
        )
    }
}

export function run({target, collapse, labelMap}) {
    const _xrm = window.parent.Xrm || Xrm
    collapse = collapse || true
    
    const dataStr = Xrm.Page.context.getQueryStringParameters()["data"]
    const data = dataStr ? JSON.parse(dataStr) : {}
    
    collapse = collapse || data.collapse || true
    labelMap = labelMap || data.labelMap || {}
    
    render(
        <Fabric>
            <DynamicsPivot tabs={_xrm.Page.ui.tabs}
                           collapse={collapse}
                           labelMap={labelMap}/>
        </Fabric>,
        target)
}
Here's the HTML entry point:
<html>
    <head>
        <meta charset="utf-8" />
        <title>Navigation Privot</title>
        <script src="../ClientGlobalContext.js.aspx"></script>
    </head>
<body>
    <div id="container"/>
    <script type="text/javascript" src="./js/NavigationPivot.js"></script>
    <script>
     document.addEventListener("DOMContentLoaded", function() {
         var el = document.getElementById("container");
         NavigationPivot.run({target: el});
     })
    </script>
</body>
</html>

Tuesday, May 16, 2017

Tuesday, May 9, 2017

Moving OneNote files around, PC to Cloud and vice-versa

I needed to move some one note files around. Here's my scenario:

  • Onenote notebook Test on my PC. A onenote notebook is really just a folder with a TOC notebook in it. Each tab is another .one file.
    • If you onenote is on another cloud, you could export the onenote to a .onepkg then open that on your desktop to extract the folder of .one files.
  • Wanted to move it to a sharepoint that was only accessible by web or onedrive for business and NOT through the standard "places" approach within onenote itself. Otherwise, I could have just moved it using the internal onenote machinery.

I copied the folder to onedrive for business which makes it appear as a folder up on sharepoint, not as a onenote notebook with the proper icon.

If you you were to stick a txt file on your onedrive for business with the same name as the folder but with a .ms-one-stub extension and the textual content:   

StubID:{C04F1300-7A0E-410D-B7C2-2736A0B9739B}

Then save it, the icon will turn into a one-note icon and you can open it. However, no stub file appears on the sharepoint because the stub file is a special signal to onedrive to sync that content as a onenote sync vs standard filesync.

One sharepoint, create a dummy onenote, then delete it.

You should now have your original folder appearing as a onenote notebook on sharepoint and the folder with your .one files will be removed from your local computer. onedrive performs special syncing--per notebook tab--to be more efficient and this is the approach they use.

Sunday, April 9, 2017

Form notifications in Dynamics CRM

Form Notifications have been covered elsewhere already. When you are on a form, there is an area right below the title area that form messages are displayed.

The messages can be errors, information or warnings. In your web resources, you can add messages to the notification areas using Xrm.Page.ui.setNotification.

The picture below does not have  messages:


If a message is generated:
 
You can create your own messages in JS as mentioned above:


There is also an undocumented function, setFormHtmlNotification, that allows you to set the formatting of the message. While its not official API, its been around for awhile and saves you from having to use something like notifyjs. So if you use  

Xrm.Page.ui.setFormHtmlNotification("<h1>Wow! <button type='button'>Push Me For More!</button></h1>", 'INFO', "100")

then you get:

Don't forget to add some styling via inline styles. It's not stable API of course, but it helps. Hacking into notifications otherwise is a bit non-portable which is why notifyjs exist. Styling can also be a bit awkward.

Sunday, April 2, 2017

Lifecycle for CRM web resources and accessing form data

Dynamics CRM allows you to add your own web resources to a form. The web resource is HTML that can display widgets that add to the form's interface and the user's experience. The web resource often displays information related to the entity being edited in the form.

The web resource is loaded into its own iframe. This means that the javascript that runs will use a different javascript execution context than the main form. This is a standard technique for isolating CSS and javascript effects from the toplevel page.

Under Form Properties, for the overall form, you can load a javascript library and call an initialization function on it.  You also have the option of passing an execution context to the "init" function. The execution context mentioned here is different than Xrm. Xrm, Microsoft's global variable, allows you access to both the general context, via .context, and the form's context via Xrm.Page. However, running javascript by using Form Properties is a capability separate from Web Resources.

There are two key issues you face when you load a web resource:
  • How do you get the "context" information that allows you to make data calls or perform processing based on the entity being edited. You can pass web query parameters to the loaded frame, but there is no equivalent "pass the Xrm execution context" switch to pass the object like with the javascript init function in Form properties.
  • How do you communicate between tabs, even if those tabs are running in their own iframes?

For the first issue, there is of course, the MSDN page here that shows how to get the ClientGlobalContext by including in a special <script> ClientGlobalContext.js.aspx. This only provides the equivalent of Xrm.Page.context and not Xrm.Page as you can get in the Form Properties' execution context. You have to make sure that the relative directory path matches the hierarchical embedding of the web resource. If the web resource name contains slashes, to fake a directory structure, you have to make sure the '../../and so on' prefix before ClientGlobalContext.js.apx matches the level.

How do you get enough access to context information to properly obtain critical information? You have a few choices:
  • Access parent.Xrm which should exist since the iframe is one below the actual entity frame. However, given asynchronous loading, the Page and global context may not have been initialized onto that object yet. You may need to use callbacks to sequence your access.
  • Pass the critical values, such as the entityid or other information, through the query parameters then parse the document.location.search value.
  • Use both with the query parameters having higher precedent than the parent.Xrm object.
You do have an option of passing in the entity id, language id and org id and name into the web resource via query parameters. With these, you can use the web data API and the context brought into scope via ClientGlobalContext to access data directly in the CRM database. But, if you want to connect to the main form's attributes and controls, for example to hook into control events, the context that you get from ClientGlobalContext.js.aspx does not help you as its the general context, not the form context.

So you can manipulate entity data that you get via the web data APIs but it may not be enough.

You probably need to access the toplevel Xrm.Page to get attributes and controls. Hence, you almost always wind up referencing parent.Xrm to obtain the form context at parent.Xrm.Page. You often make a big assumption that you are one level below the toplevel and hence only need one "parent." versus "parent.parent." But this assumption that may break in the future or if your webresource is used differently than you envisioned.

Many plugins that provide web resources actually use both ClientGlobalContext.js.apx, query parameters passed to the Web Resource as well as parent.Xrm.Page (e.g. parent.Xrm.Page.data.entity.getId()).

On the second, issue, communicating between iframes, there are no Microsoft XRM provided solutions. You can use standard javascript techniques such as global variables to setup a pub/sub bus, use a variable to exchange state/callbacks or other similar mechanisms. You are a bit on your own. You might think that you can use a javascript resource that loads via Form Properties to setup a toplevel communication mechanism, but it turns out that these resources also are loaded into a sub-frame, one level below the toplevel. Again, you can make some assumptions and perform some load-time checks to setup your environment. Just recognize that things may break some day.