Power Apps component framework (PCF) 手把手入門實例


我是微軟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:

  1. Install Npm (comes with Node.js) or Node.js (comes with npm). We recommend LTS (Long Term Support) version 10.15.3 or higher.

  2. Install .NET Framework 4.6.2 Developer Pack.

  3. If you don’t already have Visual Studio 2017 or later, follow one of these options:

  4. Install Power Apps CLI.

  5. 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 命令后就沒問題了,如果還有錯誤就要糾正了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM