我是微軟Dynamics 365 & Power Platform方面的工程師/顧問羅勇,也是2015年7月到2018年6月連續三年Dynamics CRM/Business Solutions方面的微軟最有價值專家(Microsoft MVP),歡迎關注我的微信公眾號 MSFTDynamics365erLuoYong ,回復432或者20210112可方便獲取本文,同時可以在第一間得到我發布的最新博文信息,follow me!
Power Apps component framework的簡要介紹參考官方文檔:Power Apps component framework overview ,我不一一翻譯,簡述如下:
1. 它簡稱PCF,我這后文為了簡便用PCF代替Power Apps component framework,不是Power Apps Component,這是兩個不同的東西,后者只能用於Canvas Apps,不能用於Model-Driven Apps,處於Public Preview階段,而且也推薦用 Component library 代替Power App Component,稍有基礎的非開發者用戶也能使用,前者兩者都能用,需要專業的能寫代碼的開發者的來開發。
2.PCF開發出來的組件可以用於表單(form)上,視圖(view)中,或者儀表盤(dashboard)上,可以用來做更酷的效果,比如用地圖或者日歷替換View來顯示數據,用滾動條在表單上顯示/更改字段的值。
3.PCF對於Model-Driven App來說是已經GA了,對於Canvas App來說目前還處於Pulic Preview階段(預計2021年3月左右GA),而且默認情況下Canvas App並沒有啟用PCF,需要手動啟用,Model-Driven Apps版本PCF支持的API等,對於Canvas App版本的PCF來講不全部支持,Public Preview階段的產品一般不適宜用於生產環境。
4.PCF用於Model-Driven Apps時候僅僅支持UCI,不支持經典界面,實際上當前已經沒有經典界面了。當然,PCF不支持本地部署版本的Dynamics 365 Customer Engagement.
5.PCF在表單上顯示/更改字段字段值,以前我們用HTML Web Resource基本也能做,那PCF有啥優勢?優勢是在當前上下文中與其他組件一起加載,速度更快,能調用豐富的API,也包括能使用相機,地理位置,麥克風等,更靈活,可重復使用性更高,所有文件打包成一個等等。
說了那么多我這里做個簡單的例子,搞個中國特色的例子,將數字轉換為中文大寫,我這里的JavaScript代碼參考 用JavaScript將數字轉換為大寫金額 。
工欲善其事必先利其器,要做PCF開發首先需要安裝Microsoft Power Apps CLI (command-line interface),當然你的環境(Enivornment)必須要有Microsoft Dataverse才能安裝和部署PCF. Microsoft Power Apps CLI的安裝請參考官方文檔,因為比較簡單我就不翻譯了,步驟如下:
Install Power Apps CLI
To get Power Apps CLI, do the following:
-
Install Npm (comes with Node.js) or Node.js (comes with npm). We recommend LTS (Long Term Support) version 10.15.3 or higher.
-
Install .NET Framework 4.6.2 Developer Pack.
-
If you don’t already have Visual Studio 2017 or later, follow one of these options:
- Option 1: Install Visual Studio 2017 or later.
- Option 2: Install .NET Core 3.1 SDK and then install Visual Studio Code.
-
Install Power Apps CLI.
-
To take advantage of all the latest capabilities, update the Power Apps CLI tooling to the latest version using this command:
pac install latest
請參考官方文檔 Create your first component 結合本文一起看。
首先我想好項目名字,我取名為ConvertNumberToUpperCase,命名空間我就用LuoYongNamespace,模板分成field和dataset,我們這里適用於field。
首先是建立好文件目錄,可以用命令,也可以手工建立。我這里用命令,用管理員身份打開CMD,執行如下命令(我這里與官方文檔不同的是我一般建立一個src的子文件來放源代碼):
cd /d D:\Codes mkdir ConvertNumberToUpperCase cd ConvertNumberToUpperCase mkdir src cd src pac pcf init --namespace LuoYongNamespace --name ConvertNumberToUpperCase --template field npm install

執行完畢后就可以用IDE打開了,如果有報錯要糾正,否則到時候編譯會報錯。這里使用 code . 命令使用Visual Studio Code來打開它。最重要的兩個文件就是項目名稱文件夾下面的 ControlManifest.Input.xml 和index.ts。

ControlManifest.Input.xml 中control元素的version屬性很重要,采用這種格式命名版本,部署后更改代碼再次部署測試需要更改這個version屬性的值。
然后重要的就是參數,也就是 property 這個元素,我這里使用一個property即可,使用的代碼如下:
<property name="numberValue" display-name-key="numberValue_Display_Key" description-key="numberValue_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
這個文件的整個代碼是:
<?xml version="1.0" encoding="utf-8" ?> <manifest> <control namespace="LuoYongNamespace" constructor="ConvertNumberToUpperCase" version="0.0.1" display-name-key="ConvertNumberToUpperCase" description-key="ConvertNumberToUpperCase description" control-type="standard"> <external-service-usage enabled="true"> </external-service-usage> <type-group name="numbers"> <type>Whole.None</type> <type>Currency</type> <type>FP</type> <type>Decimal</type> </type-group> <property name="numberValue" display-name-key="numberValue_Display_Key" description-key="numberValue_Desc_Key" of-type-group="numbers" usage="bound" required="true" /> <resources> <code path="index.ts" order="1"/> </resources> </control> </manifest>
我這里是簡單例子,不用額外引用css文件或者resx文件來支持多語言。
然后就是修改index.ts文件了,這里要使用TypeScript來寫代碼,當然可以使用React框架來簡化撰寫的代碼,我這里就用TypeScript來寫,簡單易懂,聲明下我對TypeScript並不是很熟悉。
我這里使用的代碼如下,各個事件的含義我就不解釋了,參考官方文檔。
bound類型的參數值改變要通知外面的話,記得調用 notifyOutputChanged 。
getOutputs 方法返回的就是bound類型參數的輸出值。
import {IInputs, IOutputs} from "./generated/ManifestTypes";
export class ConvertNumberToUpperCase implements ComponentFramework.StandardControl<IInputs, IOutputs> {
// Value of the field is stored and used inside the component
private _value: number;
// Power Apps component framework delegate which will be assigned to this object which would be called whenever any update happens.
private _notifyOutputChanged: () => void;
// label element created as part of this component
private labelElement: HTMLLabelElement;
// input element that is used to create the range slider
private inputElement: HTMLInputElement;
// reference to the component container HTMLDivElement
// This element contains all elements of our code component example
private _container: HTMLDivElement;
// reference to Power Apps component framework Context object
private _context: ComponentFramework.Context<IInputs>;
// Event Handler 'refreshData' reference
private _refreshData: EventListenerOrEventListenerObject;
constructor()
{
}
/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement)
{
this._context = context;
this._container = document.createElement("div");
this._notifyOutputChanged = notifyOutputChanged;
this._refreshData = this.refreshData.bind(this);
this.inputElement = document.createElement("input");
this.inputElement.addEventListener("change", this._refreshData);
//setting the max and min values for the component.
this.inputElement.setAttribute("type", "number");
this.inputElement.setAttribute("id", "txtNumber");
// creating a HTML label element that shows the value that is set on the linear range component
this.labelElement = document.createElement("label");
this.labelElement.setAttribute("id", "lblNumber");
// retrieving the latest value from the component and setting it to the HTML elements.
this._value = context.parameters.numberValue.raw
? context.parameters.numberValue.raw
: 0;
this.inputElement.value =
context.parameters.numberValue.raw
? context.parameters.numberValue.raw.toString()
: "0";
this.labelElement.innerHTML = this.numberToUpperCase(this._value);
// appending the HTML elements to the component's HTML container element.
this._container.appendChild(this.inputElement);
this._container.appendChild(document.createElement("br"));
this._container.appendChild(this.labelElement);
container.appendChild(this._container);
}
public refreshData(evt: Event): void {
this._value = Number(this.inputElement.value);
this.labelElement.innerHTML = this.numberToUpperCase(this._value);
this._notifyOutputChanged();
}
public numberToUpperCase(inNumber:number):string{
let fraction = ['角', '分'];
let digit = [
'零', '壹', '貳', '叄', '肆',
'伍', '陸', '柒', '捌', '玖'
];
let unit = [
['元', '萬', '億'],
['', '拾', '佰', '仟']
];
let head = inNumber < 0 ? '欠' : '';
inNumber = Math.abs(inNumber);
let s:string = '';
for (let i = 0; i < fraction.length; i++) {
s += (digit[Math.floor(inNumber * 10 * Math.pow(10, i)) % 10] + fraction[i]).replace(/零./, '');
}
s = s || '整';
inNumber = Math.floor(inNumber);
for (let i = 0; i < unit[0].length && inNumber > 0; i++) {
let p = '';
for (let j = 0; j < unit[1].length && inNumber > 0; j++) {
p = digit[inNumber % 10] + unit[1][j] + p;
inNumber = Math.floor(inNumber / 10);
}
s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s;
}
return head + s.replace(/(零.)*零元/, '元')
.replace(/(零.)+/g, '零')
.replace(/^整$/, '零元整');
}
/**
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void
{
this._value = context.parameters.numberValue.raw
? context.parameters.numberValue.raw
: 0;
this._context = context;
this.inputElement.value =
context.parameters.numberValue.raw
? context.parameters.numberValue.raw.toString()
: "0";
this.labelElement.innerHTML = this.numberToUpperCase(this._value);
}
/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs
{
return {
numberValue: this._value
};
}
/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void
{
this.inputElement.removeEventListener("change", this._refreshData);
}
}
然后編譯看下是否有錯誤,在Visual Studio Code中點擊命令欄的Terminal > Open Terminal 執行如下命令:
npm run build
確保編譯成功,我這里截圖如下:

然后我們來調試下,使用如下命令,不過我一般是用的是 npm start watch ,這樣對代碼的更改會自動刷新調試界面。
npm start
調試起來的界面如下,可以輸入不同的參數值進行測試,測試沒有問題就准備部署。

很多人調試的時候忘了暫停用什么方法,我一般常用 Ctrl + C 來停止調試(還有一種方法我忘了),按鍵后會出現詢問是否終止batch job的提示,輸入y回車即可。

然后我使用如下命令來打包准備部署:
cd .. mkdir Solution cd Solution pac solution init --publisher-name LuoYong --publisher-prefix ly pac solution add-reference --path D:\Codes\ConvertNumberToUpperCase\src msbuild /t:restore msbuild
后面的build不需要加上 /t:restore了。

然后在Solution文件夾下面的 bin > Debug就可以看到產生的解決方案文件了,將此解決方案導入到Microsoft Dataverse並發布(因為是非托管解決方案,所以導入后是需要發布的)。

我這里導入后如下:

然后我在Model-Driven apps中來使用這個PCF組件,這個可以參考官方文檔:Add code components to a field or entity in model-driven apps 。
我在Account實體的表單中使用這個組件,打開Account實體的Main表單,選擇要顯示的字段,點擊 Change Properties ,在彈出的Field Properties窗口中選擇Controls這個tab,點擊 Add Control.. 。

選擇我們創建的PCF組件,點擊Add按鈕。

注意至少要選擇Web使用這個PCF控件,等會兒看效果才能看到,可以看到我們的綁定參數已經自動設定為要顯示的字段了,點擊OK,然后保存並發布表單,我們去看下效果。

可以看大這個字段的值我是可以改的,改動后表單需要保存才能將記錄保存好。

在Canvas app中使用PCF請參考官方文檔:Code components for canvas apps 。
首先需要為環境啟用Power Apps component framework 這個feature。登錄 https://admin.powerplatform.microsoft.com/ ,
選擇要啟用的 Enivornment,選擇 Settings 。

點擊 Product 下面的 Features 。

啟用 Allow publishing of canvas apps with code components 這個feature后點擊 Save 按鈕。

然后我新建一個Canvas App來使用它。官方文檔說要在這個app的Advanced Settings 啟用Components,但是目前新建的的Canvas App默認都是啟用的了,我認為這步驟可以不做了。
然后就是在Canvas App中點擊 Insert > Custom > Import component 。

切換到Code,待刷新后選擇我們要使用的PCF組件,點擊Import按鈕。

然后切換到 Insert 面板,展開 Code components節點,點擊要插入的PCF組件,這樣就會加入到Canvas App中的當前screen上。

選中剛才添加到Screen上的PCF 組件,右邊的Advanced面板可以設置綁定參數的值,當然可以用表達式,還可以為OnChange事件指定執行的代碼(bound類型參數的值變化會觸發),我這里設置如下。

可以按F5來預覽Canvas App,我這預覽效果如下,雖然不是很嚴謹的程序,但是基本達到了教學的目的。

常見問題:
1. 我的PCF控件在最開始總是顯示錯誤,特別是參數值為默認值val的時候,這個時候請檢查index.ts中Init方法,如果參數值不是想要的格式或者內容不合要求,代碼是否有兼容處理這個問題。如果這時候會導致程序異常,就會看到PCF控件在設置好參數之前會顯示為錯誤。
2.PCF控件我改了代碼后如何更新呢?需要更改 ControlManifest.Input.xml 文件中 control 的Version屬性,

保存后打開Terminal,再切換到Solution文件夾,執行 msbuild 命令,然后將解決方案導入Microsoft Dataverse 並發布。

下次打開時候會有安全警告,點擊 Open app按鈕。


點擊 Update 按鈕,再次預覽canvas app的時候就會發現更改在了。

3. indext.ts中Init方法有時候獲取不到參數的值,我認為是傳遞的參數也是額外獲取的,比如通過在app的OnStart事件或者Screen的OnVisible事件中代碼獲取的Dataverse中數據或者其他數據源比如Sharepoint等數據,或許是異步的原因,在PCF組件的Init方法執行的時候還獲取不到這些值,所以updateView中的代碼要注意,這個代碼是可以獲取到綁定參數的值,這個代碼要做適當處理,獲取后進行必要的處理。
4. TypeScript寫的代碼不夠簡潔,可以用其他JavaScript框架嗎? 用React是可以的,可以參考官方博客:Use of React and Office UI Fabric React in the PowerApps component framework is now available .
5.每次打包solution后還要手工導入解決方案,有命令行方式嗎?有的,請參考官方文檔 Package a code component ,主要是使用類似命令如下,我其實一般使用這種,這種就一個命令搞定了,效率高點,也容易自動化。
pac auth create --url https://luoyongdemo.crm5.dynamics.com pac pcf push --publisher-prefix ly
6. 可有示例參考?How to use the sample components? 和 PCF Gallery 。
7.我的輸出參數很復雜怎么辦?我的建議是用單行文本作為輸出參數,內容為json字符串,這樣Model-Driven App和Canvas App用起來都毫無壓力
8.我改了參數的名稱改了代碼后很多錯誤怎么辦?build一下就可以啊,執行 npm run build 命令后就沒問題了,如果還有錯誤就要糾正了。
