创建代码组件简介
Power Apps 为应用制作者提供了许多用于构建应用的内置功能,但有时您需要创建自定义用户体验来满足独特需求。 例如,将百分比值替换成仪表,显示条码而非 ID,或者将现有控件替换为具有更多功能(例如拖放网格视图)的控件。 此外,您还可以用 Power Apps component framework 继续执行您在 React 或 Angular 等其他 Web 框架中编写的现有组件。
通过创建这些组件,您可以使用整个新式 Web 开发生态系统,其中包括您可能已经熟悉的库、框架和其他工具,还可将这种功能封装到窗体中,使应用制作者可以用您的代码生成应用,就像此代码是平台现成可用的部分。
注意
自定义 Power Apps 组件通常被称为代码组件,因为它们需要使用自定义代码构建。 它们由三个元素组成:清单、实现和资源。
在以下练习中,您将创建一个自定义代码组件,处理您公司的一种场景。 您的公司希望应用程序中窗体的某些字段是只读的,直到用户手动启动对数据值的编辑。 团队发现所有内置控件都无法实现这一目标,因此他们请您创建自定义代码组件。
为了满足此要求,您将创建一个可编辑字段自定义组件,如下图所示。 它的值将是只读的,直到用户选择编辑。
该组件将侦听来自主机应用的更改,并允许用户进行更改,然后将这些更改推送到主机应用。 以下步骤可帮助您构建这个组件。
安装 Power Platform CLI
若要让您的计算机做好生成代码组件的准备,请按照以下步骤操作:
安装 Node.js(随附 npm)。 我们建议您使用 LTS(长期支持)版本,例如此处的版本。 您需要验证您是否已安装 Node/NPM。 您可以通过转到命令提示符并键入以下内容来执行此操作:
// Launch a standard command prompt and type both of the following npm --version Node --version
如果在运行这些命令时收到错误,则需要使用上面的链接安装 Node.js。
成功安装 node 后,当输入上述命令时,将在命令窗口中返回版本号,如下所示:
// If installed properly you will see something like the output below. The version numbers may be slightly different based on the version installed. C:\npm --version 10.5.0 C:\Node --version v20.12.2
安装 Power Platform Tools 扩展。 在安装 Power Platform Tools 之前,请确保已完成 Visual Studio Code 安装。
Power Platform Tools 不允许您在 Visual Studio Code 之外的命令提示符中针对 Power Platform 运行 CLI 命令。 建议您也为 PowerPlatform 安装 CLI MSI。
若要在 Windows 级别安装 CLI,请按照以下说明操作。 您可以同时安装 CLI 和 Power Platform Tools 扩展。
创建新的组件项目
在开始组件生成之前,请验证上面列出的已安装组件是否正常工作。 NPM 和 CLI 命令将在 Visual Studio Code 的终端窗口中运行。 如果您在 VS Code 中正确运行它们时遇到问题,您可以选择在命令提示符中运行以下步骤中提供的终端命令(前提是您已经针对 Power Platform 安装了 CLI)。
在 Visual Studio Code 中,导航到终端 > 新建终端(或按 CTRL+SHFT+`)。 在 VS Code 的终端窗口中,重复 npm 和 Node 版本语句。 如果一切都返回正确的版本号,您可以继续创建解决方案。
// Test installs from Steps 1-3 listed above
// This will verify NPM installation
C:\npm --version
10.5.0
// This will verify Node installation
C:\Node --version
v20.12.2
// This will launch Visual Studio Code from the command line.
C:\Code
现在,您已准备好创建一个新的组件项目,请按照以下步骤开始操作:
创建一个用于生成组件的目录。 在本示例中,您需将组件放置在 C:\source\Editable-pcf 中。 若要创建自己的目录,请使用 Visual Studio Code。 或者,如果您在使用 VS Code 终端时遇到问题,则可以使用命令提示符创建文件夹。
启动 Visual Studio Code。
选择终端并选择新建终端。
您的终端会话将默认为您最后使用的文件夹。 这将在 TERMINAL 命令提示符区域中显示,如下所示:
//Note Your PS will not list exactly what is seen below, but will be specific to your starting path. PS C:\Users\Name\Folder
将目录更改为您想要生成此解决方案的位置。 您可以使用 CD 命令导航到适当的位置。
注意
请注意,执行 NPM 或其他命令的文件夹很重要。 在执行生成命令之前,请务必确保您位于 Projects 文件夹中。 如果不这样做,可能会损坏生成并导致无法产生最佳结果。
若要在默认位置创建新文件夹,请在 VS Code 终端窗口中使用 md(创建目录),如下所示。
md source cd source
这将创建一个名为 source 的目录,并使用 cd(更改目录)命令导航到该目录。
从创建的源目录中,创建名为 editable-pcf 的目录。 这将是您的 PROJECT 目录,其中存储了您的所有项目文件。 我们还将目录更改为新的项目目录。
md editable-pcf cd editable-pcf
通过以下命令使用 Power Platform CLI 初始化您的组件项目:
pac pcf init --namespace SampleNamespace --name EditablePCF --template field
下图显示了一个您应该会看到的输出示例。
警告
如果 PAC PCF INIT 命令无法在 VS Code 的终端窗口中运行,并且您已安装 Power Platform CLI,您可以选择向您的 editable-pcf 目录运行命令提示符和 CD。 到达此位置后,您可以在命令提示符中输入命令,它将正常工作。 您应该会看到与上面列出的输出相同的输出。
使用
npm install
命令安装项目生成工具。 您可能会看到一些警告;但是,您可以忽略它们。 在发出此命令之前,请确保您位于 PROJECT 目录中。npm install
警告
如果 npm 安装无法在 VS Code 的终端窗口中运行,并且您已安装 Power Platform CLI,您可以选择向您的 editable-pcf 目录运行命令提示符和 CD。 到达此位置后,您可以在命令提示符中输入命令,它将正常工作。
您可以通过在 VS Code 的终端窗口中或在命令提示符中执行 DIR 命令来验证一切是否正常,您可以选择在 Visual Studio Code 外部生成。 您应该会在 editable-pcf 目录中看到一系列文件和文件夹。 这是您在上述步骤中创建的项目,我们将使用 VS Code 进行生成。
运行以下命令,以在 Visual Studio Code 中打开项目,或者如果您使用的是命令提示符,则在命令提示符窗口中打开。 这应该会在 VS Code 中启动您创建的项目。
code -a .
项目内容应如下图所示。
更新代码组件的清单
更新清单文件以准确表示您的控件。
展开 EditablePCF 文件夹并打开 ControlManifest.Input.xml 文件。
将 version 更改为 1.0.0,并将 description-key 更改为 Edits project name。
查找 property 节点。
将 name 值更改为 Name,将 display-name-key 更改为 Name,将 description-key 更改为 A name。
查找 resources 节点。
在以下步骤中附上对您将创建的名为 editable-pcf.css 的 CSS 文件的引用。
<css path="css/EditablePCF.css" order="1" />
通过依次选择文件和保存来保存您的更改,或按 CTRL+S 以保存文件。
将样式添加到代码组件
若要将样式添加到代码组件,请按照以下步骤操作:
请确保您仍选中 ControlManifest.Input.xml 文件,然后选择新建文件夹。
将新文件夹命名为 css。
选择您创建的 css 文件夹,然后选择新建文件。
将新文件命名为 EditablePCF.css(或您在上述步骤 6 中为 css 文件命名的任何名称)。
打开您创建的新 EditablePCF.css 文件,并粘贴以下 CSS 片段。 这是您之前将 css 路径代码添加到清单文件时使用的资源引用名称。
.SampleNamespace\.HelloPCF { font-size: 1.5em; }
现在 CSS 文件的内容应如下图所示。
选择文件并选择保存,或按 CTRL+S 以保存文件。
生成您的代码组件
在您可以实施组件逻辑之前,您需要在组件上运行内部版本。 这可确保生成正确的 TypeScript 类型,以匹配 ControlManifest.xml 文档中的属性。
使用以下命令返回到 VS Code 中的终端,并生成您的项目。 如果出于任何原因,您在使用 Visual Studio Code 中的终端时遇到问题,您可以使用命令提示符导航到文件夹并从该文件夹运行命令。
警告
在发出此命令之前,请确保您位于终端中的 PROJECT 文件夹中。
npm run build
该组件已编译到 out/controls/EditablePCF 目录中。 生成工件包括:
css 文件夹
bundle.js - 已捆绑的组件源代码
ControlManifest.xml - 上传到 Microsoft Dataverse 组织的实际组件清单文件
警告
通常接收到的最常见错误是您之前创建的 CSS 文件的文件名拼写错误。 如果发生这种情况,只需相应地重命名文件并重新运行 npm run build 命令,直到它运行完成且没有错误。 针对在 CSS 文件夹中创建的文件检查 COntrolManifest.Input.xml 文件中的 RESOURCE 部分。 它们必须 100% 匹配。
实现代码组件的逻辑
若要实现代码组件的逻辑,请根据上述步骤完成生成后,按照以下步骤操作。 在 Visual Studio Code 中,在 EXPLORER 中查找名为 index.ts 的文件。 我们将从这里开始编写组件的代码。
在 Visual Studio Code 中打开 index.ts 文件。
在 constructor 方法上方,插入以下专用变量:
// The PCF context object\ private context: ComponentFramework.Context<IInputs>; // The wrapper div element for the component\ private container: HTMLDivElement; // The callback function to call whenever your code has made a change to a bound or output property\ private notifyOutputChanged: () => void; // Flag to track if the component is in edit mode or not\ private isEditMode: boolean; // Tracking variable for the name property\ private name: string | null;
查找 public init 方法并将其替换为下述方法。
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) { // Track all the things this.context = context; this.notifyOutputChanged = notifyOutputChanged; this.container = container; this.isEditMode = false; // Create the span element to hold the project name const message = document.createElement("span"); message.innerText = `Project name ${this.isEditMode ? "" :context.parameters.Name.raw}`; // Create the textbox to edit the name const text = document.createElement("input"); text.type = "text"; text.style.display = this.isEditMode ? "block" : "none"; if (context.parameters.Name.raw) { text.value = context.parameters.Name.raw; // Wrap the two above elements in a div to box out the content const messageContainer = document.createElement("div"); messageContainer.appendChild(message); messageContainer.appendChild(text); // Create the button element to switch between edit and read modes const button = document.createElement("button"); button.textContent = this.isEditMode ? "Save" : "Edit"; button.addEventListener("click", () => { this.buttonClick(); }); // Add the message container and button to the overall control container this.container.appendChild(messageContainer); this.container.appendChild(button); } }
警告
您可能会注意到,buttonClick 的 EventListener 带有红色下划线。 别担心,我们将在下面创建用于该事件的方法。 如果您看到其他部分以红色显示,则您需要验证复制或输入的所有内容均正确。
添加按钮选择处理程序方法。 在 init 方法下方添加以下方法。
public buttonClick() { // Get our controls via DOM queries const text = this.container.querySelector("input")!; const message = this.container.querySelector("span")!; const button = this.container.querySelector("button")!; // If not in edit mode, copy the current name value to the textbox if (!this.isEditMode) { text.value = this.name ?? ""; } else if (text.value != this.name) { // if in edit mode, copy the textbox value to name and call the notify callback this.name = text.value; this.notifyOutputChanged(); } // flip the mode flag this.isEditMode = !this.isEditMode; // Set up the new output based on changes message.innerText = `Project name ${this.isEditMode ? "" : this.name}`; text.style.display = this.isEditMode ? "inline" : "none"; text.value = this.name ?? ""; button.textContent = this.isEditMode ? "Save" : "Edit"; }
查找 updateView 方法并将其替换为下述方法。
public updateView(context: ComponentFramework.Context<IInputs>): void { // Checks for updates coming in from outside this.name = context.parameters.Name.raw; const message = this.container.querySelector("span")!; message.innerText = `Project name ${this.name}`; }
查找替换 getOutputs 并将其替换为下述方法。
public getOutputs(): IOutputs { return { // If our name variable is null, return undefined instead Name: this.name ?? undefined }; }
查找 destroy 方法并将其替换为下述方法。
public destroy() { // Remove the event listener we created in init this.container.querySelector("button")!.removeEventListener("click", this.buttonClick); }
您的最终 Index.ts 现在应如以下代码所示:
import { IInputs, IOutputs } from "./generated/ManifestTypes"; export class EditablePCF implements ComponentFramework.StandardControl<IInputs, IOutputs> { /** * Empty constructor. */ // The PCF context object\ private context: ComponentFramework.Context<IInputs>; // The wrapper div element for the component\ private container: HTMLDivElement; // The callback function to call whenever your code has made a change to a bound or output property\ private notifyOutputChanged: () => void; // Flag to track if the component is in edit mode or not\ private isEditMode: boolean; // Tracking variable for the name property\ private name: string | null; 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) { // Track all the things this.context = context; this.notifyOutputChanged = notifyOutputChanged; this.container = container; this.isEditMode = false; // Create the span element to hold the project name const message = document.createElement("span"); message.innerText = `Project name ${this.isEditMode ? "" :context.parameters.Name.raw}`; // Create the textbox to edit the name const text = document.createElement("input"); text.type = "text"; text.style.display = this.isEditMode ? "block" : "none"; if (context.parameters.Name.raw) { text.value = context.parameters.Name.raw; // Wrap the two above elements in a div to box out the content const messageContainer = document.createElement("div"); messageContainer.appendChild(message); messageContainer.appendChild(text); // Create the button element to switch between edit and read modes const button = document.createElement("button"); button.textContent = this.isEditMode ? "Save" : "Edit"; button.addEventListener("click", () => { this.buttonClick(); }); // Add the message container and button to the overall control container this.container.appendChild(messageContainer); this.container.appendChild(button); } } public buttonClick() { // Get our controls via DOM queries const text = this.container.querySelector("input")!; const message = this.container.querySelector("span")!; const button = this.container.querySelector("button")!; // If not in edit mode, copy the current name value to the textbox if (!this.isEditMode) { text.value = this.name ?? ""; } else if (text.value != this.name) { // if in edit mode, copy the textbox value to name and call the notify callback this.name = text.value; this.notifyOutputChanged(); } // flip the mode flag this.isEditMode = !this.isEditMode; // Set up the new output based on changes message.innerText = `Project name ${this.isEditMode ? "" : this.name}`; text.style.display = this.isEditMode ? "inline" : "none"; text.value = this.name ?? ""; button.textContent = this.isEditMode ? "Save" : "Edit"; } /** * 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 { // Checks for updates coming in from outside this.name = context.parameters.Name.raw; const message = this.container.querySelector("span")!; message.innerText = `Project name ${this.name}`; } /** * 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 { // If our name variable is null, return undefined instead Name: this.name ?? undefined }; } /** * 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() { // Remove the event listener we created in init this.container.querySelector("button")!.removeEventListener("click", this.buttonClick); } }
重新生成并运行您的代码组件
若要重新生成并运行您的代码组件,请按照以下步骤操作:
现在,您的组件逻辑已实施,请返回到终端,并使用以下命令进行重新生成。 您可以直接在 VS Code 中或通过命令提示符运行它,只要您先导航到 editable-pcf 文件夹即可。
npm run build
生成应成功。
运行以下命令,以在 Node 的测试工具中运行您的组件。 如果您之前没有执行过此操作,它应该启动一个浏览器并显示新创建的组件。
npm start
注意
您还可以启用观看模式,以确保自动对以下资产进行任何更改,而无需使用
npm start watch
命令重启测试工具。index.ts 文件。
ControlManifest.Input.xml 文件
index.ts 中已导入的库
清单文件中列出的所有资源
测试工具应加载到新的浏览器窗口中。 (窗口应自动打开,不过您也可以引用在命令窗口中找到的地址)。
选择编辑。
输入 Project One,然后选择保存。
您可以更改容器大小。
测试工具现在应如下图所示。
关闭测试工具浏览器窗口。
返回到终端或命令提示符(如果您没有使用 VS Code 终端)并通过按住 [CONTROL] + C 停止观察程序。
键入 Y,然后键入 [ENTER]。