The main difference between a Dataverse WebResource that must be accessed from outside a form context (e.g. from the navbar, as a modal dialog, or a side pane), and one that must be injected into a form to enrich the form experience, is that the latter must receive Xrm
and FormContext
from the Form itself, and cannot rely on ClientGlobalContext.js.aspx
.
In this quick tutorial we'll see an example on how to do it when we have a React+Typescript+FluentUI WebResource, as described in my previous article.
Let's go! 🚀
The initial setup steps are the same of the previous article, I won't deep dive on those. Just a quick recap.
Prerequisites 🧳
- The folder that will contain your webresources
- PAC CLI and PACX installed on the local machine
- Both PAC CLI and PACX already connected to the target environment
Set-up steps 👨🏻💻
Quickly:
pacx solution create --name master -pp ava # to create the dataverse solution that will contain our customizations
pacx solution setDefault --name master # to set the solution we've just created as default for the environment
pacx wr init # to initialize the webresource project
npx create-react-app account-details --template @_neronotte/cra-template-dataverse-webresource # to create our form-based webresource project
Then let's perform the manual operation steps that are described on the README.md
file and build our project. Then, just run:
cd account-details
npm run build
Your project structure now should look something like:
You can type
npm run start
To run your WebResource locally.
Prepare the WebResource to receive formContext externally 💆🏻♂️
In order to accept both formContext and Xrm object from the parent form, the WebResource must expose a public JS function.
We'll create this public function directly within the /account-details/public/index.html
file. Open that file and, in order:
- Remove line 10
<script src="../../../ClientGlobalContext.js.aspx" type="text/javascript" ></script>
To get the context from the outside, we have a problem: the actual React app gets rendered immediately, while the form context may be passed lately. We want to ensure to render everything only when the actual formContext is injected in our app, to be sure to have everything wired properly.
To achieve this there are various techniques. For the sake of this tutorial, we'll update our index.tsx
file:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { initializeIcons } from '@fluentui/font-icons-mdl2';
initializeIcons();
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
function render() {
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}
const w = window as any;
w.setClientApiContext = function(xrm : Xrm.XrmStatic, formContext : Xrm.FormContext) {
w.Xrm = xrm;
w._formContext = formContext;
render();
}
This script exposes an API called setClientApiContext
to the parent form. When called, this API saves the pointers to Xrm and formContext, and starts the rendering of the app. Until the function is called, the app is not rendered.
Tweak the local SDK 📝
The local sdk shipped with @_neronotte/cra-template-dataverse-webresources
expects Xrm
and GlobalContext
to be provided via ClientGlobalContext.js.aspx
. We need to change it a bit in order to manage formContext injected from the outside.
In a future update of the package we are planning to manage this scenario directly, without any tweak.
First of all, access the src/sdk/GlobalContext.tsx
file, and change the isWired()
function this way:
public static isWired(): boolean {
var w = window as any;
return typeof(w.Xrm) !== 'undefined';
}
Then remove the getGlobalContext()
function and replace it with:
import LocalFormContext from "./LocalFormContext"; // this must be placed on top of the .tsx file
public static getFormContext(): Xrm.FormContext {
var w = window as any;
if (typeof(w._formContext) !== 'undefined') {
return w._formContext;
}
return new LocalFormContext();
}
The LocalFormContext
class referenced above is a class we want to create to stub locally the form context. Just create a file in the same sdk
folder called LocalFormContext.tsx
with the following content:
import LocalPageData from "./LocalPageData";
import LocalPageUi from "./LocalPageUi";
export default class LocalFormContext implements Xrm.FormContext
{
constructor() {
this.data = new LocalPageData();
this.ui = new LocalPageUi();
}
data: Xrm.Data;
ui: Xrm.Ui;
getAttribute(): Xrm.Attributes.Attribute[];
getAttribute<T extends Xrm.Attributes.Attribute>(attributeName: string): T;
getAttribute(attributeName: string): Xrm.Attributes.Attribute;
getAttribute(index: number): Xrm.Attributes.Attribute;
getAttribute(delegateFunction: Xrm.Collection.MatchingDelegate<Xrm.Attributes.Attribute>): Xrm.Attributes.Attribute[];
getAttribute(delegateFunction?: unknown): Xrm.Attributes.Attribute<any> | Xrm.Attributes.Attribute<any>[] {
throw new Error("Method not implemented.");
}
getControl(): Xrm.Controls.Control[];
getControl<T extends Xrm.Controls.Control>(controlName: string): T;
getControl(controlName: string): Xrm.Controls.Control;
getControl<T extends Xrm.Controls.Control>(index: number): T;
getControl(index: number): Xrm.Controls.Control;
getControl(delegateFunction: Xrm.Collection.MatchingDelegate<Xrm.Controls.Control>): Xrm.Controls.Control[];
getControl(delegateFunction?: unknown): Xrm.Controls.Control | Xrm.Controls.Control[] {
throw new Error("Method not implemented.");
}
}
Update the app to show something meaningful
For the sake of this example, let's update App.tsx
to show some info took directly from the formContext
.
import "./App.scss";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import GlobalContext from "./sdk/GlobalContext";
function App() {
return (
<FluentProvider theme={webLightTheme} className="container">
<h1>Form Wired WebResource</h1>
<p>
{GlobalContext.getFormContext().data.entity.getEntityName()}: {GlobalContext.getFormContext().data.entity.getId()}
</p>
</FluentProvider>
);
}
export default App;
Build and push 📦
Now we can build our app and push it in the Dataverse. Open a terminal in the project root folder and type
cd account-details
npm run build
cd ..\ava_\pages # move to the folder that contains the outputs
pacx wr push # i like to use PACX to push the webresources in the dataverse
Inject the formContext 💉
Now it's time to create a JS WebResource containing a function that, bound to the onLoad event of the account form, takes a reference to the webresource and injects the formContext
. Let's do it via PACX:
cd ava_\scripts
pacx wr create js --table account
A file called account.js will be created in the ava_\scripts
folder, with the following content:
class Form {
formType = {
Create: 1,
Update: 2,
ReadOnly: 3,
Disabled: 4,
BulkEdit: 6
};
onLoad(executionContext) {
const formContext = executionContext.getFormContext();
const formType = formContext.ui.getFormType();
}
}
ava = window.ava || {};
ava.account = ava.account || {};
ava.account.Form = new Form();
You wanna update the onLoad method with:
class Form {
formType = {
Create: 1,
Update: 2,
ReadOnly: 3,
Disabled: 4,
BulkEdit: 6,
};
onLoad(executionContext) {
const formContext = executionContext.getFormContext();
const formType = formContext.ui.getFormType();
if (formType === this.formType.Create || formType === this.formType.Update) {
this.setClientApiContext(formContext);
}
}
setClientApiContext(formContext) {
const wrControl = formContext.getControl("WebResource_new_1"); // this is the name of the wr control that will be put in the form, you can change it if you want
if (!wrControl) return;
wrControl.getContentWindow().then(function (contentWindow) {
contentWindow.setClientApiContext(Xrm, formContext);
});
}
}
ava = window.ava || {};
ava.account = ava.account || {};
ava.account.Form = new Form();
now we're ready to push this WR to Dataverse:
pacx wr push
Wire everything together 🔗
Now it's time to:
- Add the
account
entity form into our solution (I won't show you how to do it. If you can't do it, maybe you're in the wrong page 😎) - Open the form editor window, and add a new Section
- Into that section, drag/drop an HTML web resource control, and select the React WebResource we have just created:
- We'll leave that web resource control name default (WebResource_new_1), be sure to set the same value you typed in the
account.js
script.
- Let's add
account.js
to our form
- And bind the OnLoad event to
ava.account.Form.onLoad
- Then save and publish.
- Now... just open any account record... et voilà!
Conclusions 👏🏻
😊 Hope you enjoyed this tutorial 😊!
Drop a comment below if you want to see more about React & Typescript & Fluent UI development with a twist of PACX!