精通微软-Dynamic365-全-

精通微软 Dynamic365(全)

原文:annas-archive.org/md5/4b7740950c4db25f9fa2b4a0e4722a06

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

本书适用于需要在 Microsoft Dynamics 企业资源规划ERP)之上创建应用程序和定制的 Dynamics 365 Business Central 开发人员。

本书首先解释了 Microsoft Dynamics 365 Business Central 平台(重点介绍软件即服务SaaS)平台)以及现代开发环境(Visual Studio Code、AL 语言和其他工具,如 Docker)。然后,本书涵盖了所有开发者需要了解的扩展和自定义 ERP 的主题,重点是如何编写更好的代码,遵循最佳实践,如何调试和部署扩展,以及如何编写自动化测试。

本书还涵盖了与集成、云服务和无服务器处理相关的高级主题(包括使用 Dynamics 365 Business Central 与 Office 365 应用程序,如 Power BI、Flow 和 PowerApps 的集成;与机器学习功能的集成;以及如何将 DevOps 技术应用到开发过程中)。

本书的最后一个主题是架构,分析了将现有解决方案迁移到基于扩展的新开发模型的最佳方法。

本书的读者对象

本书面向具有业务流程和 C/AL 编程高级知识的 Microsoft Dynamics NAV/Dynamics 365 Business Central 开发人员和解决方案架构师。了解 Web 服务编程(API)和 C# 会是加分项。

本书涵盖的内容

第一章,Microsoft Dynamics 365 Business Central 概述,提供了 Dynamics 365 Business Central 架构(云端和本地)的技术介绍,并介绍了新的开发平台。

第二章,掌握现代开发环境,概述了适用于 Dynamics 365 Business Central 的现代开发环境,并提供了如何在开发扩展时高效使用 Visual Studio Code 的技巧和窍门。

第三章,基于在线和容器的沙盒,详细介绍了如何为开发创建 Dynamics 365 Business Central 沙盒环境,如何使用在线沙盒进行测试,以及如何使用 Docker 改善开发过程。

第四章,扩展开发基础,提供了新的扩展模型的概述,解释了与过去的不同、新的对象类型,以及如何使用 AL 创建对象。

第五章,为 Dynamics 365 Business Central 开发定制化解决方案,指导你使用 Visual Studio Code 和 AL 语言从实际业务案例出发,开发完整的 Dynamics 365 Business Central 解决方案。在本章中,你将看到如何创建一个完整的解决方案,以及如何为客户定制解决方案,而不修改基础代码。

第六章,高级 AL 开发,讲解了开发扩展时需要了解的高级编程主题,如文件管理、使用.NET 对象、使用 AL 调用 Web 服务、处理 XML 和 JSON、处理媒体文件、处理通知、异步编程等。

第七章,使用 AL 进行报表开发,介绍了如何在新的扩展模型中处理报表(新报表的创建和现有报表的定制)。

第八章,安装和升级扩展,教你如何处理 Dynamics 365 Business Central 扩展的安装和升级逻辑。

第九章,调试,教你如何调试扩展以及如何检查代码和变量。

第十章,使用 AL 进行自动化测试开发,详细介绍了如何为 Dynamics 365 Business Central 扩展编写和执行自动化测试。

第十一章,与 Business Central 的源代码管理和 DevOps,讲解了在为 Dynamics 365 Business Central 开发解决方案时,如何高效地处理源代码管理技术,并概述了如何处理持续集成/持续交付CI/CD)和 DevOps 技术,适用于你的扩展项目。

第十二章,Dynamics 365 Business Central API,介绍了 Dynamics 365 Business Central 的 API 框架。它展示了如何使用现有的 API,如何创建新的 API 来扩展平台,以及一些高级 API 主题,如绑定操作和 Webhooks。

第十三章,使用 Business Central 和 Azure 实现无服务器业务流程,描述了如何将 Azure Functions 与 Dynamics 365 Business Central 结合使用,在云中执行.NET 代码并实现无服务器处理解决方案。

第十四章,使用 Azure Functions 进行监控、扩展和 CI/CD,教你如何监控 Azure Functions,如何处理可扩展性以提高性能,以及如何使用 Azure DevOps 实现 CI/CD 过程。

第十五章,Business Central 与 Power Platform 的集成,展示了将 Dynamics 365 Business Central 与 Dynamics 365 Power Platform 结合使用的方式。我们将概览 Power Platform 应用,并通过与 Dynamics 365 Business Central 配合使用 Power Platform 的一些实际应用。

第十六章,将机器学习集成到 Dynamics 365 Business Central,概述了如何将机器学习功能集成到 Dynamics 365 Business Central 扩展中。

第十七章,将现有 ISV 解决方案迁移到新的扩展模型,讲解了如何将一个现有的 ISV 解决方案(使用旧的 C/AL 代码编写)迁移到新的扩展模型。内容详细讲解了架构方面的内容,并提供了如何正确开始该过程的建议。

第十八章,AL 开发者的实用工具,介绍了一些对开发经验非常有帮助的第三方工具。

获取本书最大收益

你应该熟悉使用 C/AL 编程语言进行 Dynamics NAV 或 Dynamics 365 Business Central 的开发,并且能够使用 PowerShell 和 Azure 门户等工具。了解云计算概念将大大帮助你理解某些话题。

本书将指导你如何创建完整的解决方案并解决任务。如果你希望从本书中获得最大收益,至少在试用环境中跟随提供的示例进行操作是必须的。

下载示例代码文件

你可以从你的帐户在 www.packt.com 下载本书的示例代码文件。如果你是从其他地方购买本书的,可以访问 www.packtpub.com/support 注册并让我们直接通过电子邮件将文件发送给你。

你可以按照以下步骤下载代码文件:

  1. 登录或注册到 www.packt.com

  2. 选择 Support 标签。

  3. 点击 Code Downloads。

  4. 在 Search 框中输入书名并按照屏幕上的指示操作。

下载文件后,请确保使用最新版本的解压工具解压文件夹:

  • Windows 版的 WinRAR/7-Zip

  • Zipeg/iZip/UnRarX for Mac

  • Linux 版的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,地址是 github.com/PacktPublishing/Mastering-Microsoft-Dynamics-365-Business-Central。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。

我们还有来自丰富书籍和视频目录的其他代码包,可以在 github.com/PacktPublishing/ 查找。快去看看吧!

下载彩色图片

我们还提供一份包含本书中使用的截图/图表的彩色图像的 PDF 文件,您可以在这里下载:static.packt-cdn.com/downloads/9781789951257_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码词语、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户名。举个例子:“这是一个 docker run 命令的输出,其中该镜像在本地不可用。”

一段代码如下所示:

mcr.microsoft.com/businesscentral/onprem:1910-cu1-au-ltsc2019

所有命令行输入或输出都以以下方式书写:

docker network create -d transparent transpNet

粗体:表示新术语、重要单词或屏幕上出现的词语。例如,菜单或对话框中的词语在文本中是这样显示的。举个例子:“只需浏览 dynamics.microsoft.com/en-us/business-central/overview/,然后在所需的许可模块下点击 'Find a partner'。”

警告或重要提示以此方式出现。

提示和技巧以此方式出现。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何部分有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com与我们联系。

勘误表:虽然我们已尽力确保内容的准确性,但错误难免。如果您在本书中发现错误,我们将非常感激您能向我们报告。请访问 www.packt.com/submit-errata,选择您的书籍,点击 “Errata 提交表单” 链接,并输入详细信息。

盗版:如果您在互联网上发现任何形式的非法复制我们作品的情况,我们将非常感激您能提供相关网址或网站名称。请通过 copyright@packt.com 联系我们并附上相关内容的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com

评论

请留下评论。阅读并使用本书后,为什么不在您购买书籍的网站上留下评论呢?潜在读者可以通过您的公正意见做出购买决策,我们可以了解您对我们产品的看法,作者也能看到您对其书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问 packt.com

第一部分:Dynamics 365 Business Central - 平台概述与现代开发基础

在本节中,我们将介绍 Dynamics 365 Business Central 的架构(云端与本地部署)以及新的开发平台。

本节包含以下章节:

  • 第一章,Microsoft Dynamics 365 Business Central 概述

  • 第二章,掌握现代开发环境

  • 第三章,在线与基于容器的沙盒

第一章:Microsoft Dynamics 365 Business Central 概述

Microsoft Dynamics 365 Business Central 是一款顶尖的基于云的 企业资源规划ERP)应用软件,专为 中小企业SMB)市场设计。该应用基于 软件即服务SaaS)模式,并通过 云解决方案提供商CSP)合作伙伴进行销售。

潜在客户可以随时启动试用租户,或联系 CSP 合作伙伴购买并分配按用户计费的许可。

本章将涵盖以下主题:

  • 客户视角:什么是 Dynamics 365 Business Central,涵盖了哪些功能领域,以及许可方式

  • 合作伙伴视角:Business Central 管理中心概述及其使用

  • Microsoft 视角:深入了解 Microsoft Dynamics 365 Business Central 的技术细节

  • 未来视角:未来几年可能会发生的变化以及如何为实现这些变化做出贡献

到本章结束时,你将对 Microsoft Dynamics 365 Business Central 平台有一个清晰而深入的概述。

理解客户视角

针对中小企业,Dynamics 365 Business Central 的核心设计依赖于 Microsoft Azure 和 Office 365 平台。该应用的核心代码和业务流程来源于 Microsoft Dynamics NAV(通常称为 Navision)30 多年来功能增强的演变:这是中小企业领域最坚固的本地部署 ERP 软件之一。

潜在客户——或者那些只是想体验一下该应用的人——可以通过 trials.dynamics.com/Dynamics365/Signup/BusinessCentral 提供与 Office 365 订阅绑定的电子邮件地址和电话号码,快速设置并试用该产品。30 天试用期结束后,产品需要购买。

正式授权仅通过经过 CSP 项目认证的 Microsoft 合作伙伴分配。只需浏览 dynamics.microsoft.com/en-us/business-central/overview/ 并在所需的许可模块下点击“查找合作伙伴”,如以下截图所示:

Microsoft Dynamics 365 Business Central 提供开箱即用的功能模块,按每个用户每月固定价格收费。根据不同的功能和应用模块,有三种按用户/每月计费的选择:Essentials(基础版)、Premium(高级版)和 Team Members(团队成员)。Essentials 和 Premium 是完整用户,而 Team Members 只是具有有限功能的额外用户。

以下是 Essentials 模块的属性(功能集合)(从每月 $70 起):

  • 财务管理

  • 客户关系管理

  • 项目管理

  • 供应链管理

  • 人力资源管理

  • 仓库管理

这些是 Premium 模块的属性(每月起价 100 美元):

  • 财务管理

  • 客户关系管理

  • 项目管理

  • 供应链管理

  • 人力资源管理

  • 仓库管理

  • 服务管理

  • 制造业

当前,在同一租户中不可能拥有 Essential 和 Premium 混合的用户体验。可以从 Essential 模块升级到 Premium 模块,但不能从 Premium 降级到 Essential。如果你已经至少有一个用户被授权为 Essential 或 Premium,那么可以将外部用户作为命名许可的 Team Member 添加到相同的模块(Essential 或 Premium)中。

这就是作为 Team Member 获得的内容(每月起价 8 美元):

  • Essential 或 Premium(取决于添加团队成员的用户模块)。

  • 消耗数据或报告的能力、完成轻任务(如时间或费用条目及人力资源记录更新),并使用 PowerApps for Dynamics 365。

  • 从技术上讲,他们可能对所有表格具有读取权限,但只能对最多三张表具有插入/更新权限。

有关许可类型及其包含内容的所有详细信息已在官方 Microsoft Dynamics 365 Business Central 许可指南中描述(截至写作时的最新审查为 2019 年 10 月),可通过以下链接找到:mbs.microsoft.com/Files/public/365/Dynamics365BusinessCentralLicensingGuide.pdf

一旦客户开始使用他们的试用版或生产版租户,他们将获得一个富有生产力、直观且用户友好的 Web 客户端界面,如下图所示:

最佳的浏览器体验由 Microsoft Edge 或 Google Chrome 提供。

客户还可以在几乎所有现代设备上受益于通用应用程序部署类型,如平板电脑、智能手机和手机。这是通过从 Windows Store、Google Play 或 Apple Store 下载名为 Dynamics 365 Business Central Universal App 的应用程序实现的。要安装移动应用程序,请访问 docs.microsoft.com/en-us/dynamics365/business-central/install-mobile-app

打开此网站后,我们会看到安装移动应用程序的三个选项。您可以从 Microsoft 安装、从 Apple Store 下载,或从 Google Play Store 获取。以下是选择从 Microsoft 安装应用程序时您将看到的屏幕截图片段:

截至写作时,Microsoft Dynamics 365 Business Central 已在 18 个国家/地区(按发布日期排序)由微软正式本地化并发布:

2018 年 4 月 2018 年 7 月 2018 年 10 月
美国 澳大利亚 墨西哥
加拿大 新西兰 挪威
英国 冰岛
丹麦
荷兰
德国
西班牙
意大利
法国
奥地利
瑞士
比利时
瑞典
芬兰

从 2018 年 10 月更新开始,CSP 合作伙伴现在可以为 Dynamics 365 Business Central 尚未发布或尚未成为微软本地化目标的国家创建自己的本地化版本。

这些本地化版本以全球标准应用基础(称为 W1)为起点,并通过 Microsoft Dynamics 365 Marketplace 分发,称为 AppSource。像通过 AppSource 部署的任何扩展(或应用程序)一样,所有的应用程序和技术支持由通过 AppSource 销售该应用程序的合作伙伴提供。

你可以在docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/readiness/readiness-develop-localization#service-availability-in-additional-countries查看更多内容。

以下是当前额外的 CSP 本地化国家(截至写作时)的应用程序和发布者名称列表:

爱沙尼亚 爱沙尼亚语包 爱沙尼亚增值税报告本地化 爱沙尼亚企业登记本地化 爱沙尼亚银行格式本地化 爱沙尼亚 Intrastat 报告本地化 Estonian Dynamics Partners
香港特别行政区 香港繁体中文语言包 香港繁体中文包 香港繁体中文语言 Tectura Hong Kong LimitedPacific Business Consulting, Inc.K-Solve IT Solutions Limited
印尼 印度尼西亚税务计算本地化 Wahana Ciptasinatria
日本 日本语包 J-Pack – 日本本地化 Pacific Business Consulting, Inc.
马来西亚 ADS 报告(入门)本地化 ADS 本地税(入门)本地化 ADS Global SSO Sdn Bhd
波兰 波兰语包 波兰功能性–入门包 IT.integro sp. z o.o.
葡萄牙 SOFTSTORE 本地化语言包 SOFTSTORE 本地化包 Softstore SA
塞尔维亚 塞尔维亚语包 塞尔维亚本地化 Adacta d.o.o.
新加坡 新加坡本地化 AFON GST 本地化 新加坡 Dalstech GST 本地化 IBIZ Consulting Pte LtdAFON Systems Pte LtdDalstech Pte Ltd
南非 南非发票 Braintree by Vox
韩国 韩国语包 韩国增值税本地化 DEEX Korea Co LtdMAVEN Korea Co., Ltd.
台湾 台湾繁体中文语言包政府统一发票GUI)台湾 台湾工资系统 Knowledge & Strategy Information Co., Ltd.
泰国 泰国增值税(VAT)和预扣税(WHT)本地化 Triple P Application Co., Ltd.、AVISIONTHU、biz Solution Co., Ltd.
阿联酋 阿联酋增值税本地化 Alfazance Consulting

你可以在docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/compliance/apptest-countries-and-translationsappsource.microsoft.com/en-us/marketplace/apps?product=dynamics-365%3Bdynamics-365-business-central&page=1上阅读更多相关信息。

现在我们了解了 Dynamics 365 Business Central 是什么以及它为客户提供什么,让我们深入探讨一下合作伙伴的视角。

理解合作伙伴的视角

CSP 使合作伙伴能够访问一系列可销售的 Microsoft 云服务。在此计划中,提供了一些管理和支持这些云服务的工具。其中一项在线服务是 Dynamics 365 Business Central。

你可以在docs.microsoft.com/en-us/partner-center/csp-overview上了解更多有关合作伙伴中心的信息。

在 SaaS 提议中,潜在客户只能通过 CSP 合作伙伴或其经销商购买 Dynamics 365 Business Central 的许可证,并将试用许可证转换为按使用付费的许可证,或直接支付用户的月费。

在每个 CSP 中,合作伙伴或经销商在Azure Active DirectoryAAD)中由唯一的租户记录表示。AAD 是一个多租户身份验证服务,提供身份和访问功能,适用于在 Microsoft Azure 和 Microsoft 本地环境中运行的应用程序。

在此特定的 AAD 租户记录中,合作伙伴可以定义不同类型或类别的用户(通常称为支持代理),这些用户主要分为两个不同的组(即所谓的代理组):管理员帮助台组。

与合作伙伴类似,客户也有自己独特的 AAD 租户记录。当在 Dynamics 365 Business Central 租户中订阅 Essential 或 Premium 计划时,每个客户都同意 CSP 合作伙伴与客户 AAD 租户之间建立特殊的信任关系。

关系的方向是从客户租户到合作伙伴租户,如果需要,可以由客户撤销和/或管理。

在客户的 AAD 租户中,定义并管理用户、角色和订阅实体。角色由客户分配给用户,这些角色反映了他们在订阅的产品中的能力。要在客户 AAD 租户中订阅 Dynamics 365 Business Central 等在线产品,CSP 合作伙伴还需要将特定的在线产品许可证分配给用户。

这些任务通过Business Central 管理门户执行。客户或 CSP 合作伙伴都可以直接访问该门户。CSP 合作伙伴也可以通过Partner Center门户访问:

  1. 使用 Partner Center 门户,有几种方式可以浏览到 Dynamics 365 Business Central 管理中心门户。其中一种方式是通过“服务管理”选项卡。服务管理选项卡包含指向与特定客户 AAD 租户相关的各种管理门户的链接,例如 Exchange 或 Office 365。它还显示产品的服务健康状态,这些产品与门户或管理员控制台相关联,例如 Exchange Online、身份服务和 Dynamics 365 Business Central。

  2. 通过点击 Dynamics 365 Business Central 链接,合作伙伴将被直接重定向到 Dynamics 365 Business Central 管理中心门户。在合作伙伴中心门户中,CSP 合作伙伴还可以查看客户的订单历史记录,并查看他们所属的订阅内容。还可以选择 Dynamics 365 Business Central 的计费频率——例如按月或每年一次——并代表该客户订阅不同的在线服务。

  3. 订阅按提供类型(级别)进行划分。例如,在 Dynamics 365 Business Central 中,可以选择 Essential 或 Premium 计划。

  4. 在“用户和许可证”部分,CSP 合作伙伴可以手动添加用户或从文件中批量上传用户。对于每个用户,可以分配不同的服务许可证。

  5. 一旦分配了许可证,用户就可以开始使用 Dynamics 365 Business Central,并且该应用将出现在他们的首页 home.dynamics.com 上。点击 Dynamics 365 Business Central 图标会将用户重定向到第一次登录页面,他们可以立即在生产租户中开始工作。你可能会注意到,URL 的定义通过易于识别的固定客户端端点和客户租户带来了好处,且其 Dynamics 365 Business Central 管理门户应如下所示:

GUID 标识了你在 Partner Center 门户中所在的相同客户环境。

自 2019 年秋季更新以来,如果您有多个生产环境,当您点击首页上的 Dynamics 365 Business Central 图标(home.dynamics.com)时,系统会提示您选择要选择的环境名称。环境端点应如下所示:businesscentral.dynamics.com/<EnvironmentName>

那么,谁可以访问 Dynamics 365 Business Central 管理中心门户?答案如下:

  • 拥有与客户租户的有效授权关系的 CSP 合作伙伴管理员和帮助台代理

  • 客户的 AAD 全局管理员

拥有 Dynamics 365 Business Central 许可的用户将无法访问管理门户。产品许可证与 Dynamics 365 Business Central 管理中心门户访问之间没有关系。

客户的 AAD 租户全局管理员可以登录,而合作伙伴 AAD 租户管理员和帮助台用户可以作为委托管理员访问。委托管理员可以作为合作伙伴执行提升的任务,但他们没有客户全局管理员的相同权限。简而言之,委托管理员不是租户的全局管理员。

Dynamics 365 Business Central 管理中心门户示例

以一个快速示例来说,委托管理员不能在客户租户中创建新公司,而应当请客户管理员(拥有超级权限)创建新公司,或由客户提升权限,以便添加适当的 Dynamics 365 Business Central 权限。一旦新公司创建完成,委托管理员可以登录并执行他们有权进行的管理任务。

合作伙伴委托管理员可以通过输入 Dynamics 365 Business Central 固定端点,businesscentral.dynamics.com,然后输入客户租户的 AAD 名称(例如,businesscentral.dynamics.com/customerAADtenantname.onmicrosoft.com),来登录与其有关系的特定客户租户。这是必要的,因为合作伙伴可能需要处理多个客户的管理任务,并且在日常活动中可能需要快速连接、断开连接和重新连接。

Dynamics 365 Business Central 管理中心门户目前包含四个部分(环境通知接收者遥测和报告的故障),如下面截图中侧边栏所示:

我们将在接下来的部分中逐一讲解这些内容。

环境

环境列出了特定客户的所有 Dynamics 365 Business Central 生产和沙箱租户。对于每个租户,展示了其状态、供应国家、版本和升级窗口。

截至写作时,可用的操作如下:

  • 新建。创建新的生产环境或沙盒租户。目前,最多可以为相同或不同国家创建三个生产租户,以及最多三个沙盒租户。可以将沙盒作为生产数据库的副本进行创建。

    根据生产租户中的数据量,从生产租户创建沙盒的操作可能需要较长时间。

  • 删除。您可以选择删除一个沙盒或生产租户。

截至写作时,特定租户的可用操作如下:

  • 设置更新窗口。为特定租户配置一个本地时间的更新窗口。更新窗口允许您选择开始时间和结束时间。更新完成后,微软会发送通知。该通知通常包含更新是否成功执行的详细信息,如果失败,则会说明失败的原因。如果更新失败,通知中将添加一份可操作的故障报告,供合作伙伴和/或客户采取相应措施。

  • 安排更新。当有新更新时,微软会向管理员中心接收者发送通知,通过此操作,可以安排更新。

  • 删除。删除当前的生产环境或沙盒租户。

  • 报告生产环境故障。这是自 2019 年秋季更新以来的新功能。如果用户无法连接到生产环境,只需按下生产故障按钮,即可提交记录(工单)请求,向 Dynamics 365 Business Central 运营中心寻求即时帮助。该工单将被优先处理,以便及时解决问题。

  • 管理支持联系人。用于为特定环境添加支持联系人。用户将在 Business Central 帮助与支持页面上看到此信息。可以为每个租户选择不同的支持联系人,或选择一个适用于所有租户的联系人。

由于微软不断添加新操作,已宣布以下功能将在 2019 年 10 月正式发布后推出:

  • 能够下载生产租户的 Azure SQL 备份(BACPAC)。可以将其还原到本地,以便进行离线故障排除、数据分析以及商业智能BI)驱动的任务。

欲了解更多信息,请访问 docs.microsoft.com/en-us/dynamics365-release-plan/2019wave2/dynamics365-business-central/planned-features

通知接收者

通知接收人列出了特定客户的所有 Dynamics 365 Business Central 通知接收人。该列表显示接收人的姓名和电子邮件地址。当某个生命周期任务完成时,这些用户将收到通知,例如,当应用了小更新或有新版本的应用程序升级时。

如果希望开发人员和测试人员在某些事情发生或即将发生时得到通知,以便有足够的时间审查他们的自定义开发与新的标准应用程序代码的匹配情况,这个功能会非常有用。

遥测

这显示了一个过滤面板,包含特定客户的日期、时间和事件类型。

可以设置过滤器来指定特定环境(生产或沙箱),并通过指定回溯的分钟数来回溯到过去。该列表报告以下内容:

  • 时间戳:表示操作何时被记录的值。

  • 级别:一个整数值,表示错误、警告和信息。

  • 操作码名称:应用程序操作类型(例如,启动或停止)。

  • 对象类型和 ID:表示生成日志的对象类型和 ID。

  • 对象扩展名称和 ID:如果没有显示任何值,表示遥测日志直接来自遗留应用程序代码(C/AL)。

  • 功能名称:表示生成日志的对象功能名称。

  • 失败信息:如果没有显示任何值,通常表示这是关于某个特定对象功能的启动或停止操作的信息消息。

对于指定的时间范围,还可以在列中搜索特定操作或错误消息。这通常用于沙箱或预发布环境,以查找标准基础应用程序与已开发的自定义扩展之间是否存在不一致或运行时错误。

报告的生产中断

这是 2019 年秋季更新中的新功能。它报告了生产中断票据及其状态。可以过滤显示过去 30 天、14 天或 7 天内的记录。

更多信息可以在 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/tenant-admin-center 查阅。

除了通过门户的用户界面UI)登录客户端并执行操作外,合作伙伴中心和 Dynamics 365 Business Central 管理门户还提供了一组强大的 API,可以用来创建一个自定义外观,以现代化、完全自动化的方式处理客户创建、许可证分配及其他任务。

即使对 PowerShell 或 Visual C# 有基本了解,并且没有高级开发技能,仍然可以迈出创建自定义仪表板的第一步,并自动化创建新客户用户、分配或撤销许可证等操作。

例如,我们可以使用以下端点:api.businesscentral.dynamics.com/v1.2/admin/applications/BusinessCentral/environments/Sandbox

它是如何在幕后工作的

固定的 Web 服务端点全局服务被调用,并将信息重定向到全局租户管理器全球服务,该服务会执行广播操作并确定请求属于哪个区域控制平面、数据平面和租户。

在检索所需信息后,固定的 Web 服务端点会直接将请求来回传递到所选的区域控制平面。换句话说,全球服务仅执行了第一次路由信息任务(一个简单的代理任务)。

当固定的 Web 服务端点被路由到与特定区域控制平面通信时,它将开始与租户管理员后台服务进行交互。

您可以在docs.microsoft.com/en-us/partner-center/develop/docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/administration-center-api上了解更多信息。

在撰写本文时,一些合作伙伴已经在他们的项目中实现了 Dynamics 365 Partner Center SDK,并在 Dynamics 365 Business Central 本地环境中(使用 .NET 互操作性)消费这些 API,以实现一个完全集成的客户租户管理仪表板。这也可以用于演示目的。

现在我们已经释放了一些微软为其合作伙伴提供的最佳功能,接下来让我们来看看 Dynamics 365 Business Central 背后的运作方式。

Dynamics 365 Business Central 的幕后运作

微软在去年进行了大量投资,并继续投资于现代化和精简化的 Dynamics 365 Business Central 架构,以便拥有一个易于部署和升级的 ERP 云服务解决方案。

在撰写本文时,统计数据非常鼓舞人心,甚至超出了预期。

基本上,每 180 秒就会创建一个新的 Dynamics 365 Business Central 租户。每分钟会产生 400,000 个度量数据,约有 8 TB 的日志每天生成。这些日志随后会被预处理、聚合,并有大约 4 TB 的数据通过 Azure Data Lake 服务上传到 Cosmos DB 进行大数据分析。

这些只是 Dynamics 365 Business Central 所产生的一些数据,和一些用于提供世界上最佳在线 ERP 体验的 Microsoft 云服务。

以目前的进展来看,在不久的未来,人工智能AI)可能会在所有平台和应用程序级别上自发触发微服务调整。

更深入地考虑,由于涉及到 Azure 技术,截至撰写本文时,每个微服务集合中协调的资源总共有 20 个。这显示了向用户和开发者提供的复杂环境是如何以最简化的方式呈现的。

Dynamics 365 Business Central 开发团队的主要目标是将可扩展性负担转移给合作伙伴和客户。合作伙伴和客户无需再关注数据存储的位置和方式,也不需要关心用于收集、转换和升级数据的技术。相反,他们应该专注于扩展应用程序。无需平台技能;只需点击刷新并重复“开发者,开发者,开发者……”。

下面是一个关于 Azure 资源的表格概览,列出了每个平台服务所使用的资源、它们的用途,以及每个资源的更多信息链接:

Azure 资源 通用用途 链接
Azure 服务面料 分布式系统平台,便于部署和管理可扩展的微服务和容器。 docs.microsoft.com/en-us/azure/service-fabric/service-fabric-overview
Azure 密钥保管库 用于加密和解密应用程序中的数据以及其他若干安全相关功能。 docs.microsoft.com/en-us/azure/key-vault/key-vault-whatis
应用网关 用于智能分配应用程序调用的 Web 流量负载均衡器。 docs.microsoft.com/en-us/azure/application-gateway/overview
SQL 弹性数据库池 用于 Azure SQL 数据库的资源优化器,适用于客户和应用程序租户。 docs.microsoft.com/en-us/azure/sql-database/sql-database-elastic-pool
应用洞察 一组用于收集日志信息并将其作为遥测数据发送的工具。 docs.microsoft.com/en-us/azure/application-insights/app-insights-overview
Azure Machine Learning (ML) 服务 基于 SaaS 的实验室,用于开发和应用机器学习模型及其结果。 docs.microsoft.com/en-us/azure/machine-learning/service/overview-what-is-azure-ml
Azure 搜索 为应用程序和微服务实现高级搜索的 API。 docs.microsoft.com/en-us/azure/search/search-what-is-azure-search
Azure 存储 提供存储层抽象,以根据安全性和隐私性保存数据。这些存储反映了区域性的法律模型。 docs.microsoft.com/en-us/azure/storage/
Azure Active Directory (AD) 微软基于云的身份和访问管理服务。保证安全、可靠的登录和资源访问。 docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-whatis
Azure Function 提供特定例程/功能的 API,运行在隔离环境中。建议作为 Dynamics 365 Business Central 和 .NET 互操作性的替代方案。 docs.microsoft.com/en-us/azure/azure-functions/functions-overview
Traffic Manager 基于 DNS 的流量负载均衡器,主要目的是将流量负载优化地分配到全球 Azure 区域的服务,同时保证高可用性和响应性。 docs.microsoft.com/en-us/azure/traffic-manager/traffic-manager-overview
Azure 负载均衡器 用于保证微服务的高可用性。负载均衡器支持入站和出站场景,并提供低延迟和高吞吐量。它可扩展至数百万个 TCP 和 UDP 应用的流量。 docs.microsoft.com/en-us/azure/load-balancer/load-balancer-overview
Azure SQL 数据库 用于管理进出云存储的数据的关系型数据库。 docs.microsoft.com/en-us/azure/sql-database/sql-database-technical-overview
Azure 容器注册表 存储所有类型容器部署的基础镜像。通常用于存储沙盒镜像,供开发目的下载使用。 docs.microsoft.com/en-us/azure/container-registry/
Azure 数据湖存储 Gen1 用于分析产生的大量遥测数据。 docs.microsoft.com/en-us/azure/data-lake-store/data-lake-store-overview
Azure 服务总线 一种消息中介解决方案,用于解耦应用程序和服务之间的关系,数据在不同的应用程序和服务之间传输。 docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview
健康监控 Azure 服务面板引入了一套可扩展的分析工具,用于监控系统和/或服务的健康状况。可以根据特定规则创建警报,并发送给值班的运维工程师。 docs.microsoft.com/en-us/azure/service-fabric/service-fabric-diagnostics-overview
Azure 虚拟网络 使许多类型的 Azure 资源(例如 Azure 虚拟机)能够安全地相互通信以及与互联网通信。 docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-overview
Azure 数据工厂 一种基于云的数据集成服务,允许创建基于数据的工作流,用于自动化数据移动和数据转换。 docs.microsoft.com/en-us/azure/data-factory/introduction
Cosmos DB 用于聚合遥测数据并进行进一步分析。 docs.microsoft.com/en-us/azure/cosmos-db/introduction

全球服务只是一些不存储任何数据、仅执行处理活动的服务。它们只是代理,不持有任何数据,主要用于将请求重定向到适当的控制平面和数据平面。

全球服务主要用于将请求重定向到适当的控制平面;实际上,它们仅在用户登录时路由信息。固定的客户端端点根据凭证负责将请求路由到适当的控制平面和数据平面,不需要其他额外信息。

不同世界地区有多个全球服务实例,但它们都通过相同的端点进行访问。流量管理器在它们前面,将请求重定向到最近的实例。这使得 Dynamics 365 Business Central 服务非常高效且性能优越。从统计数据来看,每小时约有 30,000 次请求通过 Dynamics 365 Business Central 全球服务进行路由。接下来,让我们了解这些服务的工作原理和位置。

区域控制平面

这些是具有各自功能并执行特定任务(例如配置、扩展、监控和身份验证)的微服务集合,用于管理应用程序访问、分发和运行时。它们都是内置的,由 Azure Service Fabric 管理。每个区域都有这些服务的子集,因它们遵循所属国家或地区的隐私和法律准则,因此被称为区域服务。

这样做有两个好处:

  • 在处理数据时,遵守特定区域的隐私和安全法律。

  • 靠近数据存储意味着延迟减少,网络性能提高。

控制平面是管理特定数据平面的服务集合。因此,它们与数据平面位于同一区域,并在该区域内分布。

以下是当前构成控制平面的服务:

  • 数据库监控:用于监控租户和应用程序数据库的健康状况,并将遥测数据和统计信息上传到内部分析工具。

  • 弹性池优化器:从弹性池中提取统计信息,并将其上传到内部工具。

  • 扩展管理服务:这是控制平面的核心服务,负责同步守护进程路由和完成服务调用。大致来说,它是一个信息容器。它包含所有数据平面集群和库存的注册表(包括哪些租户在什么集群中,等等)。它的职责是按需创建、升级和管理租户。通常,这项服务会响应同步守护进程的请求或来自区域控制平面的其他服务。

  • 扩展验证服务:对每个租户的扩展进行编译,针对即将推出的应用服务进行检查。这将确定基础应用程序即将发生的更改是否会破坏为该租户创建的私有 IP。这些错误可能会通过 CSP 合作伙伴门户显示给合作伙伴。

  • 健康监控:跟踪租户的状态,如果检测到不健康的 ping,它会发送内部警报。

  • 管理门户:基于 Dynamics 365 Business Central Web 客户端平台的内部仪表板门户。它管理客户租户,并提供一个 UI 以对这些租户执行操作。

  • 管理服务:服务编排的核心。它包含一个目录,列出租户可以/应该执行的活动类型。这些活动包括创建、复制、升级和删除等。

  • 配置服务:历史上,它由一组用于 Azure 虚拟机配置的 PowerShell 脚本组成。如今,它主要用于执行扩展验证。

  • 同步守护进程:这是 Dynamics 365 Business Central 最古老的服务之一。全球服务通常会与该服务在区域控制平面中进行交互,它决定是否创建新租户或将请求路由到适当的数据平面。它拥有一个操作数据库,用于排队创建新租户的请求,以防无法立即处理。

  • 租户管理员后台服务:执行与管理中心相关的活动。例如,它负责验证 Web 服务请求,如内容的完整性和业务逻辑的合理性。

  • 租户缓冲服务:通常由管理服务处理。它不断检查已经创建了多少租户,并用新的缓冲租户替换原有租户数量,以应对高峰期并保持新租户创建与现有租户分配之间的正确平衡。

  • 租户维护服务:用于租户维护。例如,当客户决定从试用版转到付费订阅时,此服务将启动。维护服务随后会将租户从标准级别迁移到性能更强的高级版。如果试用版过期、AAD 中的许可证被移除,或客户停止付款,维护服务将把租户置于暂停状态。租户有 90 天的赎回期,但这可能因各国的数据保留政策而有所不同。宽限期结束后,租户将被从服务层中移除,并且经过一段时间后将被删除。

  • 租户升级器:在 CSP 合作伙伴指定的时间升级窗口中安排并触发更新任务。目前,租户的升级日期由微软决定,无法更改。

开发团队不断添加新服务或拆分现有服务,以腾出空间为新功能提供支持,或优化 Dynamics 365 Business Central 的维护和可扩展性。在接下来的部分,我们将看到哪些区域数据平面与这些服务配合使用。

区域数据平面

区域数据平面是服务的集合,用于实现安全的客户数据存储。将数据保存在与客户接近的、位于相同国家/地区的 Azure 数据中心,并且位于与客户相同的隐私和合规区域中非常重要。数据安全性至关重要。例如,在西欧地区创建了两个数据平面,这两个数据平面都由四个数据中心提供支持。它们是标准且公开可用的,任何人——包括开发团队——都可以使用它们,释放 Azure 服务、API 及其扩展性的潜力。

在 SaaS 解决方案中,采用隔离的微服务非常重要,这样才能在各个模块中快速部署更新和变更。数据平面同样适用,可以通过所谓的内部安全部署实践应用所需的原子性,进行计划中的更新。

数据平面资源专门用于处理客户数据。这些资源通过大量的遥测参数进行持续测量。所有数据平面的管理,例如创建和升级租户,由其他区域实体(称为控制平面)执行。

这是当前区域数据平面(截至 2019 年 10 月)及其托管的本地化版本列表:

西欧 北欧 美国 加拿大 亚洲/****中东 非洲 大洋洲
奥地利 丹麦 墨西哥 加拿大 香港(CSP) 南非(CSP) 澳大利亚
比利时 爱沙尼亚(CSP) 美国 日本(CSP) 新西兰
法国 芬兰 马来西亚(CSP)
德国 冰岛 泰国(CSP)
意大利 挪威 韩国(CSP)
荷兰 瑞典 台湾(CSP)
塞尔维亚(CSP) 阿联酋(CSP)
西班牙 印度尼西亚(CSP)
瑞士 新加坡(CSP)
英国
波兰(CSP)
葡萄牙(CSP)

一个数据平面集群包含所有冗余的虚拟机和用于应用程序和客户租户的 Azure SQL 数据库。目前,通过为每个数据平面负载均衡五个 Azure 虚拟机,确保了高可用性。

由于这些统计数据表明租户大约每三分钟生成一个,如何在多个请求同时到达同一数据平面时应对并扩展如此高的负载呢?这一过程通过相当智能的方式处理。当一个区域控制平面指示创建新租户时,管理服务会在数据平面集群中预留一个缓冲租户。什么是缓冲租户?基本上,它是一个已经创建的租户,数据库中有一个带有示范数据的公司,充当预包装模板。缓冲租户不与任何应用服务绑定(未挂载)。

当预留一个缓冲租户时,它不能被任何其他管理服务调用占用,并且只需通过更改一些配置参数并为特定客户添加生产公司名称,即可将其转变为生产租户。完成后,该租户将与生产服务挂载,当其运行时,连接就绪。

简而言之:预留、配置、挂载、运行,准备就绪。不需要冗长的数据库创建或恢复过程。

目前,生产和沙盒环境中的数据平面集群环境是不同的。这是由于不同环境的特性及其需求不同。它们主要在性能上有所不同,因为不同的 Azure SQL 数据库层级。试用版和沙盒租户目前属于 Azure S 级别。切换到生产环境时,这些租户将被移动到更高性能,并且由 Azure SQL 团队推荐的 P 级别。

单个数据平面集群是微软云技术的聚合体,逻辑上分为计算层和数据层。让我们来探索数据平面的解剖:

服务名称 通用目的
公共 IP 地址 这些根据用户调用(浏览器/ Web 服务)的不同或由控制平面在内部实例化而不同。
应用程序网关 这是一个智能、智能且复杂的第 7 层负载均衡器,能够分析 cookie 和检查有效负载。它用于用户调用,并且仅支持 HTTP 调用。
Azure 负载均衡器 这是用于区域控制平面调用的内部使用。
VM 规模集 默认由五个 Azure 虚拟机组成。可以在 VM 规模集内部署更多 Azure 虚拟机,从而实现无限扩展并能够处理高服务负载或隔离。整个 Azure 虚拟机集合都定义在相同的可用性集合内。这意味着如果发生硬件故障,不会同时影响所有虚拟机,从而保证高服务可用性。VM 规模集内的每个 VM 都包含一个 Dynamics 365 Business Central Web 服务器和 Dynamics 365 Business Central。这有助于优化 Web 服务器和 Dynamics 365 Business Central 之间的流量。每个 Azure 虚拟机还包含一个监控服务,用于收集遥测数据和许可服务,以避免存储用于访问 AAD 租户的证书私钥。
虚拟网络 这用于允许 Azure 虚拟机在 VM 规模集内相互通信。
存储帐户 这包含来自 VM 和服务健康数据的遥测数据。
Azure 服务 Fabric 控制器 这用于管理和编排每个集群中的服务部署。例如,在需要时,可以指示它在规模集中提供新的 VM。
应用程序数据库 这包含标准应用程序代码,并绑定到每个 VM 中的 Dynamics 365 Business Central 服务。即使这看起来像是数据层的一部分,但应用程序数据库实际上并不存储任何客户数据。这就是为什么它与其他计算部分项目一起列出的原因。
网络安全组 这主要用于为每个集群提供额外的安全层。通常,开发团队不允许通过终端服务进行任何远程连接,即使是来自他们自己。Dynamics 365 Business Central 的遥测服务提供有关虚拟机或服务状态的信息,并通过特定的端点提供可操作的洞察。换句话说,数据处理的安全性是完全有保障的。

了解了这个详尽的列表后,我们继续查看数据层级别。

数据层

最简单的数据层就是所谓的每个单独的数据库。这非常容易解释和理解:例如,在 Sx 层级中创建一个 Azure SQL 数据库,如果客户需要更多的性能,你只需将其扩展到 Sx+n,根据需要的资源(你希望处理完成的速度)。

单个数据库的缺点在于,当你在 Azure SQL 中创建了数据库后,所有的资源都会被分配出去,这些资源既不会共享也不会释放,而客户——或者你——都需要为这些资源支付费用,无论是否使用它们。

当你需要处理成千上万(甚至数十万个)数据库时,就像在每个多租户的 A 类产品中一样,资源分配设计应该足够智能,能够在需要时分配或释放资源。否则,这将变成一个对所有人来说的成本杀手:客户、合作伙伴,甚至微软自己。

Dynamics 365 Business Central 数据库资源分配是智能的。它依赖于 Azure SQL 弹性池技术。基本上,通过 Azure SQL 弹性池,可以定义一个在池内共享的资源总量,并为每个数据库租户设置一个范围(最小值和最大值)。云资源治理器会在池内智能分配资源。这非常高效、性能优越且成本效益高。

值得一提的是,存在 标准Sx)和 高级Px)弹性池数据层。当需要时,Sx 的租户会被移动到性能更高的 Px 池中。

所有生产数据库当前都运行在 Px 弹性池数据层中。

数据层通过基于 WCF 的 Navision 服务层NST)进行访问,NST 安装在另一个名为 VM 扩展集的微服务中。以下是单个虚拟机扩展集的当前结构:

Web 服务器 Dynamics 365 Business Central Web 服务器组件
NST Dynamics 365 Business Central 服务器服务。出于安全原因,它在 Hyper-V 容器中以主机模式隔离(如小型虚拟机)。这防止了恶意代码访问用户的秘密。
监控代理服务 用于收集来自当前 Azure 虚拟机状态的遥测数据。此服务还会收集来自平台和应用程序日志的遥测数据,涵盖 Web 服务器和服务器服务组件。
许可服务 这是 2018 年秋季更新中为安全原因引入的一项服务。该服务负责检查 AAD 中是否存在有效的许可证,其 API 由 NST 组件调用。
租户目录 这是一个包含租户名称及其连接字符串的集合。通常由另一个服务(如许可服务)访问,以避免通过应用数据库租户列表从 NST 进行破坏性攻击或直接调用。
混合代理 该功能启用混合复制,使我们可以将本地数据迁移到云端。你可以在docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/about-intelligent-edgedocs.microsoft.com/en-us/dynamics365/business-central/about-intelligent-cloud了解更多。
扩展服务 该服务允许为每个租户异步安装扩展。
差异服务 该服务使对 Dynamics 365 Business Central OData 服务端点的差异查询成为可能。
浏览器客户端 该组件托管了 Web 服务器的静态部分(例如 .js 文件等)。
网关服务 这是一个以性能为驱动的服务工件,用于智能请求路由。该服务如果同一租户已经创建了其他会话,会将请求重定向到所谓的“预热”服务。它通过应用程序和数据对象预热内存缓存。
任务触发服务 该服务用于优化计划任务的执行,并提高任务执行时(加速启动)和执行上下文中的性能(即,如果存在,将其路由到一个预热的 NST)。

现在我们已经解锁了服务并了解了 Dynamics 365 Business Central 的底层架构,让我们快速回顾一下它未来的发展方向。 |

理解未来的视角

Azure 和 Office 365 现在被认为是成熟且稳定的,并且为客户带来了可观的 投资回报率 (ROI)。 |

它们是微软收入的最佳来源之一,也是微软最重视的投资领域。所有微软服务都要求与微软战略对齐,促进这两项旗舰服务的使用。

Dynamics 365 Business Central 完美契合了微软的战略:它为潜在的 SMB ERP 客户提供了一个平台,旨在加速 Microsoft 一流云服务的使用。 |

换句话说,潜在的 Dynamics NAV 和 Dynamics GP 客户强烈建议订阅 Dynamics 365 Business Central 的 Essential 或 Premium 版本,而不是选择传统的本地部署方案。 |

在最近的微软及非微软活动中宣布了一项推动在线采用的巨大产品转型。这些行动主要针对现有的 Dynamics NAV 和 Dynamics 365 Business Central 本地客户。以下是最近在 Directions EMEA 上展示的产品当前路线图(www.directionsemea.com/):

在 2019 年秋季发布时,微软实现了一个具有挑战性的目标,即缩小本地部署和在线部署之间的差距。现在这两种产品的部署能力几乎相同:

根据开发团队的公告,当前的计划是将现有的遗留 C/AL 代码迁移到 AL,并提供作为微软本地化扩展的标准代码,或者更可行的是,作为一系列依赖应用程序提供。

通过这种方式,本地和在线客户端的访问在用户和开发体验方面将大致相同。然后,本地与在线之间的差异将非常小,且从本地迁移到在线只需一步即可轻松完成。

由于这项工作是一个持续的进程,Dynamics 365 Business Central 的开发团队始终保持开放,并以主动和反应性服务倾听所有客户和合作伙伴的请求。这是为了涵盖所有不同的视角和观点。在接下来的部分,我们将具体了解微软如何倾听并采取行动响应客户和合作伙伴的请求。

主动场景(微软倾听)

让我们来看一下微软 Dynamics 功能和版本的一些主动场景:

  • 新功能建议以及现有功能和能力的增强建议: 这会直接在 Dynamics 365 Business Central 的工程积压中创建一个内部记录。你还可以对现有的建议进行投票,这会提高该功能的优先级和排名。此项工作直接由开发团队处理(aka.ms/bcideas)。

  • 在应用程序或平台的预览版或测试版中发现的错误或问题: 仅限 独立软件供应商ISV)/增值经销商VAR)。请注意,不接受任何咨询或建议请求(docs.microsoft.com/en-us/collaborate/)。

  • 在 Visual Studio Code 中的 AL 预览版或测试版中发现的 Bugs 或错误:需要注意的是,不接受任何咨询或顾问请求(github.com/microsoft/al)。

新的事件请求

功能暴露请求将添加到预览版发布中,而不会添加到当前的在线版本。如果需要将更改回移植到当前的在线堆栈,应提出一个响应性请求(github.com/microsoft/ALAppExtensions/)。

响应性场景(微软行动)

以下是微软针对响应性场景的规则:

  • 在 GA 版本和主流支持版本中发现的应用程序或平台中的 Bugs 或错误:仅适用于与微软签订 高级合作伙伴支持 (ASfP) 合同的 ISVs/VARs。

  • 回移植在 GA(正式发布)版本和主流支持版本中暴露的新事件或功能:在线客户必须联系其销售商或 CSP 合作伙伴以获得支持和/或要求他们提交响应性支持请求。

更多信息可以在此找到:community.dynamics.com/business/b/financials/archive/2018/12/04/find-the-right-resources-and-provide-feedback

总的来说,考虑到仍在使用本地部署的 Dynamics NAV 和 Dynamics GP 的现有客户,我们可以提供给客户和合作伙伴组织的最佳建议如下:

  • 使用和请求事件。

  • 尽可能将所有现有的私有 IP 移到事件驱动开发的标准代码之外。如果这需要在标准应用程序中新增事件,请通过适当渠道提出请求。

  • 将遗留代码迁移到扩展中。

  • 所有可以隔离成事件驱动开发的内容,都可以打包成扩展。尽可能将私有 IP 移到扩展中。这项任务有一个广泛接受的技术术语,叫做 SaaS 化

  • 重构所有代码,以使其在 Web 客户端中工作,并将所有技能集中于基于 Web 的开发。

  • 使 Web 客户端成为你的主要客户端,并将思想转向 Web 导向开发。

  • 培训所有销售人员、安装人员、开发人员、功能应用专家以及其他所有使用或演示 Web 客户端的人。让他们在 Dynamics 365 Business Central Web 客户端中“活在其中”。

  • 合作并利用社交媒体。

  • 保持 LinkedIn、Twitter 和 Yammer 上的最新动态。注意你自己的业务流程瓶颈,并通过积极参与官方和非官方论坛以及专门的产品活动,与 Dynamics 365 Business Central 社区和开发团队分享。

  • 最后但同样重要的是,主要针对合作伙伴和自由职业者,习惯并专注于现代技术。

我们建议开发人员掌握以下相关技能:

  • Visual Studio Code 和 AL

  • JavaScript 和基于 Web 的开发

  • 人工智能与机器学习技术

  • 针对开发者的 Azure 服务(如 Azure Functions 和 Cognitive Service)

  • Git 和 Azure DevOps

开发者和架构师应熟悉以下内容:

  • Docker 容器

  • Azure 计算服务(如 Azure 虚拟机和 Azure 存储)

  • Office 365 服务和 Dynamics 365

  • 通用数据模型CDM)/通用数据服务CDS

强烈建议合作伙伴订阅 Dynamics 365 Business Central Ready to Go 项目,并从其不断更新的学习目录中受益。您可以在docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/readiness/readiness-ready-to-go?tabs=learning了解更多信息。

总结

本章节介绍了目前和未来在 Dynamics 365 Business Central 中可用的功能。

首先,我们从合作伙伴和客户的角度出发,重点介绍了 Dynamics 365 Business Central。这将有助于您理解该产品在本地化、功能以及您想要针对的小型企业市场领域方面能够提供的价值。

接下来,我们介绍了合作伙伴中心和 Dynamics 365 Business Central 管理中心门户以及如何使用它们。然后,我们讲解了 Microsoft Dynamics 365 Business Central 的主要技术特性,并对架构元素进行了概述。本章最后简要介绍了未来可能发生的变化、如何为此贡献力量以及如何成为 Microsoft SaaS 解决方案演进的一部分。

在下一章中,我们将深入探讨 Visual Studio Code、AL 语言扩展以及现代开发环境。

第二章:精通现代开发环境

在上一章中,我们介绍了 Dynamics 365 Business Central,并揭示其骨架是 Microsoft 云微服务。

在本章中,我们将深入了解开发环境。我们将讨论与 Visual Studio Code(官方开发平台)以及 AL 语言(开发语言扩展)相关的主要快捷键、技巧和窍门。Visual Studio Code 与 AL 语言的结合定义了所谓的现代开发环境。

AL 是 Microsoft 提供的官方扩展,免费通过在线市场提供。该扩展于 2017 年正式发布,用于扩展当时被称为 Dynamics 365 for Financials 的功能,现在它已经成为一个完整的开发语言,扩展了 Dynamics 365 Business Central。它提供了许多功能,大大提升了开发者的生产力和编码质量。

本章的主要目标是帮助 Dynamics 365 Business Central 开发人员了解开发平台所提供的功能,释放他们的全部潜力,并提高他们在日常编码活动中的熟练度。

在本章中,你将学习以下内容:

  • Visual Studio Code 用户界面由哪些部分组成,每个部分的作用是什么

  • 如何熟练使用 Visual Studio Code 中最强大的编辑功能

  • AL 语言扩展是什么,它包含了哪些内容

精通 Visual Studio Code

Visual Studio Code 是全球使用最广泛的开发环境之一。它的设计目的是让云端和基于 Web 的应用程序设计变得简单快速,支持多种可扩展的编程语言。该应用程序专注于最大化代码编辑效率,同时通过提供有用的快捷键,帮助开发者在特定开发场景下快速访问所需的功能,充分释放开发者的潜力。

当你启动 Visual Studio Code 时,首次安装后,它会显示典型的欢迎页面:

欢迎页面包含以下内容:

  • 开始:创建和打开文件及文件夹的快捷键

  • 最近:最近打开的文件和文件夹列表

  • 帮助:文档单页、产品文档、视频和有用资源的列表

  • 自定义:如何通过扩展、键盘快捷键、背景色主题等来自定义 Visual Studio Code

  • 学习:与最常用命令相关的学习资源快捷键,以及如何掌握用户界面

每次你以新窗口运行 Visual Studio Code 时,都会加载欢迎页面(Ctrl + Shift + N)。你可以通过取消勾选启动时显示欢迎页面,或点击“文件 | 偏好设置 | 设置”并搜索“欢迎页面”来更改此行为。

Visual Studio Code 环境分为五个主要区域:

  • 代码编辑器

  • 状态栏

  • 查看栏

  • 侧边栏

  • 面板区域

接下来我们将分别讨论这些内容。

代码编辑器

代码编辑器是你编写代码并花费大部分时间的地方。当创建新文件或打开现有文件或文件夹时,它会被激活。

你可以只编辑一个单独的文件,或者你也可以加载并同时处理多个文件,排成并排的方式:

有几种方法可以查看多个文件;这里提到的三种是:

  • 在 EXPLORER 栏中选择一个文件名,然后右键点击并选择“在侧边打开” (Ctrl + Enter)。

  • 在 EXPLORER 栏中 Ctrl + 点击文件名。

  • Ctrl + ** 将编辑器分割为两部分。

它可以容纳多个文件,将空间平均分配给它们。你可以通过简单地按下 Ctrl + 1Ctrl + 2Ctrl + 3,……,Ctrl + N 在不同的文件编辑器之间切换。

编辑器窗口可以根据你的需求进行调整大小、重新排序和缩放。要缩放,按 Ctrl + +Ctrl + -,或者通过 View | Zoom in / Zoom out。

缩放适用于所有 Visual Studio Code 区域,而不仅仅是代码编辑器。

Visual Studio Code 还提供了一种便捷的方式来通过快捷方式在文件之间导航。最快的方法是按 Ctrl + Tab。这将打开自 Visual Studio Code 启动以来打开过的文件列表。

状态栏

状态栏通常包含关于当前选中文件或文件夹的信息。它还提供一些可操作的快捷方式:

从左到右,状态栏包含以下信息:

  1. 如果启用了 Git,它将报告版本控制信息,例如当前分支。

  2. 当前代码中检测到的错误和/或警告数量。

  3. 光标位置(行号和列号)。

  4. 缩进大小和类型(空格或制表符)。

  5. 当前选中文件的编码。

  6. 行终止符:回车符CR)和/或 换行符LF)。

  7. 用于处理选中文件中代码的语言。如果点击该语言,菜单会出现,你应该能够更改处理的编程语言。

  8. 反馈按钮,你可以通过它在 Twitter 上分享关于 Visual Studio Code 的反馈。

  9. 通知图标。它显示新的通知数量,通常与产品更新有关。

状态栏有一种常规的颜色显示,它会根据当前正在处理的内容而变化。打开文件时是紫色,打开文件夹时是蓝色,调试时是橙色,等等。

视图栏

这是工作区的左侧,包含通往侧边栏的快捷方式。如果点击一个快捷方式,属于所选工具的侧边栏将变得可见。再次点击,或按 Ctrl + B,它将消失。

侧边栏

侧边栏是你与代码编辑器互动最多的地方。它是上下文敏感的,你会在视图栏中看到五个标准活动,每个活动都由相应的图标启用。

EXPLORER (Ctrl + Shift + E)

EXPLORER 提供了你当前正在使用的文件夹和文件的结构化和组织化视图。OPEN EDITORS 子视图包含代码编辑器中活动文件的列表。在此部分下方,可能会有另一个子视图,显示已打开文件夹的名称:

如果你将鼠标悬停在 OPEN EDITORS 子视图上,将显示三个操作按钮:切换垂直/水平编辑器布局(Shift + Alt + O)、全部保存(Ctrl + K + S)和关闭所有文件(Ctrl + KCtrl + W)。它们都是显而易见的:

将鼠标悬停在文件夹名称上(在此示例中为PW_V2),四个操作按钮将变得可见:

从左到右,分别是新建文件、新建文件夹、刷新和全部折叠。它们是显而易见的。

右键单击文件夹或文件名,将打开一个上下文菜单,显示常用命令,如在资源管理器中显示(Shift + Alt + R),它将打开包含选定文件的文件夹。你还可以通过复制路径(Shift + Alt + C)来复制文件路径。

在 EXPLORER 栏的下方,还有一个叫做 OUTLINE 的部分。它为特定文件提供了非常有用的树形视图,显示了文件中的成员和类型。请参见以下截图:

当你正在开发复杂对象并希望通过点击一下跳转到特定区域时,这确实是一个强大的选项。

搜索 (Ctrl + Shift + F)

这是一个强大的工具,用于在文件中搜索和替换文本。你可以选择一个或多个关键字进行简单搜索,还可以使用通配符如*和?。或者,你可以选择基于正则表达式(regex)创建复杂搜索。还有高级选项可以包括或排除文件或文件类型。

当开发者在扩展文件夹中搜索where used字段或变量时,这一功能非常有帮助。请参见以下截图:

搜索结果以树形视图列出,显示包含搜索关键字的所有文件,并显示与该行相关的小段代码。树形视图中的关键字匹配处以及代码编辑器中的匹配处都会被高亮显示。通过点击“全部折叠”按钮,可以将所有结果折叠。

你可以通过点击“清除搜索结果”按钮来重置搜索结果。

源代码控制 (Ctrl + Shift + G)

Visual Studio Code 提供了与最广泛使用的源代码控制管理系统之一:Git 的原生集成。Git 的基础知识和集成将在第十一章中讨论,源代码控制管理与业务中心的 DevOps

调试 (Ctrl + Shift + D)

Visual Studio Code 不仅仅是一个用于编辑文件的代码编辑器。它还提供了一个开箱即用的集成调试框架,可以扩展用于调试不同的平台和语言。

Visual Studio Code 并不提供针对 Dynamics 365 Business Central 的调试功能。这项功能嵌入在 Visual Studio Code 的 AL 语言扩展中,扩展了现有的 .NET 核心调试器。在第九章中,调试,我们将详细讨论这一议题。

EXTENSIONS(Ctrl + Shift + X)

扩展用于浏览 Visual Studio Code 的在线市场,市场中包括了不断增长的额外语言、调试器、工具、助手等各种扩展。AL 是 Microsoft 开发的一个 Visual Studio Code 扩展。在 Visual Studio Code 市场中,你还可以下载几个有用的扩展,它们扩展了(对扩展的扩展)AL 语言扩展,帮助 Dynamics 365 Business Central 开发者提高工作效率,提升生产力,并且编写专业的代码。

请参考以下截图,展示了为 Dynamics 365 Business Central 安装的典型 Visual Studio Code 扩展:

在 EXTENSIONS 栏中,可以搜索在线市场或手动安装扩展。你还可以查看已安装、过时、推荐和禁用的扩展,并根据不同标准对其进行排序。

一些扩展包旨在下载并安装一组扩展。在 Dynamics 365 Business Central 中,你可以考虑从 marketplace.visualstudio.com/items?itemName=waldo.al-extension-pack 下载并安装 AL 扩展包,或从 marketplace.visualstudio.com/items?itemName=StefanoDemiliani.sd-extpack-d365bc 下载并安装 Dynamics 365 Business Central 的 SD 扩展包。

也可以通过右键点击单个扩展来执行相关操作。扩展可以被启用、禁用、按工作区禁用(工作区可以是项目或文件夹)等。最新、最酷的功能之一是能够安装该扩展的另一个版本。

这对于 Dynamics 365 Business Central 开发者非常有用,特别是在高版本 AL 语言扩展中存在回归行为或错误时。请参考以下截图,展示当前在线 AL 语言扩展版本:

这在开发针对特定平台版本的功能时也非常有用。

管理

管理按钮以齿轮图标显示在视图栏的最底部:

如果你点击它,会弹出一个菜单,列出可用的命令。这些命令用于自定义 Visual Studio Code 或查找更新。

命令面板

命令面板是 Visual Studio Code 中最重要的工具之一。它的作用是快速访问标准和扩展命令。运行命令面板有不同的方式:

  • 管理 | 命令面板

  • 查看 | 命令面板

  • 键盘快捷键:Ctrl + Shift + P(大多数开发者常用)

命令面板不仅可以显示菜单命令,还可以执行其他操作,例如安装扩展。你可以浏览命令面板,查看可用命令的庞大列表。命令是按索引排列的,并且可以搜索。只需输入几个字母,就能得到筛选后的列表。值得一提的是,大多数这些命令都有对应的键盘快捷键。

了解命令面板时,有一件非常重要的事需要知道,那就是 > 符号的使用。当你按下 Ctrl + Shift + P 时,命令面板会弹出,并带有 > 符号,显示可用命令的列表。请看以下截图:

如果你移除 > 符号,Visual Studio Code 会使用命令面板显示最近打开的文件列表。以下截图展示了这一点:

这一功能的强大之处在于,在不使用鼠标的情况下,你可以打开命令面板,运行命令,移除 **>** 字符,并选择要编辑的文件。这对提升开发效率非常有帮助。

面板区域

Visual Studio Code 不仅显示与代码相关的详细分析和信息,还能访问并展示来自其他来源的信息,例如 Git、已安装的扩展和调试器。这些输出会记录到面板区域,默认情况下显示在底部,但可以通过右键单击面板标题栏并启用“Move Panel Right”按钮轻松将其移动到工作区的一侧。可以通过点击“Move Panel to Bottom”按钮恢复原始布局,或者甚至通过按 Ctrl + J 隐藏面板。

面板区域默认不可见。当请求所需信息时,通常会启用并显示此区域,例如启用调试器时。

在面板区域,有四个不同的窗口:PROBLEMS、OUTPUT、DEBUG CONSOLE 和 TERMINAL。我们将在接下来的章节中逐一讲解它们。

PROBLEMS

对于具有高级编辑功能的语言,例如 AL,Visual Studio Code 能够在输入时识别代码问题。问题所在的行会有特定的颜色标记。通知类型有三种:错误、警告和信息。所有这些都可以在 PROBLEMS 窗口中显示。以下截图展示了 PROBLEMS 窗口显示三条错误的示例:

通常,阻塞错误会以红色显示,而警告则以绿色标记。

OUTPUT

OUTPUT 面板是 Visual Studio Code 通常在命令执行期间或执行后显示消息的地方。

由于内置工具操作和多个扩展命令可以并行运行,因此可以利用 OUTPUT 面板中的下拉框更改视图,查看每个标准或基于扩展的命令的输出。

以下截图显示了面板区域中的 OUTPUT 窗口:

通常,在处理 Dynamics 365 Business Central 扩展时,会选择 AL 语言。

DEBUG CONSOLE

这是一个由本地和基于扩展的调试器(例如 AL 语言调试器)使用的特殊窗口,用于显示关于代码执行的信息。此窗口及其输出将在第九章中详细分析,调试

TERMINAL

Visual Studio Code 允许我们像在命令提示符中一样直接在开发环境内执行命令。默认情况下,终端会话基于 PowerShell。

现在我们已经了解了与 Visual Studio Code 相关的所有元素,可以进入下一部分,分析它所提供的强大编辑功能。

Visual Studio Code – 编辑功能

Visual Studio Code 提供了你所期待的顶级代码编辑器的许多功能。如果你熟悉 Visual Studio,你可能已经注意到一些功能是从这个 IDE 继承过来的,或者是以类似的方式设计的。

由开发人员为开发人员开发,Visual Studio Code 为几乎所有编辑命令提供键盘快捷键,让你可以更快地编辑代码,完全摆脱鼠标的依赖。

让我们在接下来的章节中研究这些功能。

注释行

Visual Studio Code 提供了开箱即用的文本选择和专业编辑命令,这些命令位于编辑菜单中。编辑菜单还包括切换行注释(Ctrl + U),该命令为选中的行添加行注释。这意味着,如果你选择了 10 行代码,Visual Studio Code 会添加 10 行注释。这个命令的妙处在于它也能反向操作。如果你选择了 10 行注释并按下切换行注释,注释就会神奇地被移除。

对于使用 CSIDE 的开发人员来说,CSIDE 是 Dynamics 365 Business Central 传统本地语言,此命令等同于注释选择(Shift + Ctrl + K)和取消注释选择(Shift + Ctrl + O)。

定界符匹配

Visual Studio Code 能够检测配对的定界符,如括号和圆括号。如果你想划定代码块,这个功能非常有用,当鼠标靠近某个定界符对时,它会自动触发:

上述代码是定界符匹配的示例。

文本选择

选择菜单也有与文本选择相关的命令,但大多数用于移动或复制选中行的代码。

如果你将光标放置在 AL 函数、变量或常量附近,可以使用“添加下一个出现位置”(Ctrl + D)、“添加上一个出现位置”或“选择所有出现位置”(Shift + Ctrl + D)来选择所选项的所有出现位置,并且这些出现位置会以不同的颜色高亮显示。

在代码编辑器中,你还可以按Ctrl + D选择光标右侧的单词或标识符。然后,你可以轻松地扩展(Shift + Alt + →)或收缩(Shift + Alt + ←)分隔符内的文本块。

代码块折叠

如果你将鼠标悬停在代码编辑器中的行号上,-符号会出现在代码块的起始部分旁边。点击它进行折叠,此时会出现+符号。点击这个符号,代码块会展开:

上图展示了代码块折叠的情况,使用了+符号。

多光标(或多重光标)

每个光标都是独立操作的。Alt + 点击将在目标位置生成一个次要光标。

最常见的开发场景是,当你需要在同一源文件中的不同位置添加或替换相同的文本时,你可以使用多光标。下图展示了在编辑DataClassification属性时,三个光标的使用情况:

对于 AL 语言开发者来说,这是一个很棒的功能,特别是当他们需要在同一个地方多次写下相同的句子时(例如,在表格对象中的CaptionDataClassification,以及每个表格字段中)。

迷你地图

有时,当处理非常长的文件(例如报告源文件 RDL 或代码单元)时,很难知道指针应该定位到哪里——或者它当前位于源文件的哪个位置。Visual Studio Code 提供了一个完整的迷你地图功能:源代码文件的一个小预览。以下是一个 RDL 示例:

迷你地图功能可以通过视图 | 切换迷你地图进行启用或禁用,或者通过运行命令面板(Ctrl + Shift + P)并选择“视图: 切换迷你地图”来操作。

面包屑

“显示面包屑”命令可以在视图菜单中找到。在 AL 文件中,代码编辑器的左上角有一个图标。可以展开它,以便重新检查属性、函数、字段、键等的定义:

如果你点击展开列表中的某个元素,光标将跳转到该元素的主定义位置,使得代码导航非常快捷。

IntelliSense(智能感知)

在视觉编辑器中,IntelliSense 是一个单词自动补全工具,它会在你输入时作为弹出列表出现。Visual Studio Code 的 IntelliSense 可以提供智能建议,显示与特定元素相关的定义和用途——就像在线帮助一样。下图展示了 IntelliSense 的使用情况:

IntelliSense 是上下文敏感的,如果你需要直接启用它而不输入任何内容,只需按 Ctrl + 空格键。根据光标所在的上下文,IntelliSense 会显示该上下文中可以使用的所有项。例如,在 Table Field 声明内部,它会列出所有特定字段属性,如 CaptionCaptionML,而在空代码单元定义中,它会显示代码单元对象暴露的所有属性。

单词自动补全

通过 IntelliSense 功能,Visual Studio Code 中的代码编辑器实现了对所有原生(如 JSON)和基于扩展支持的语言(如 AL)的单词补全。只需按 EnterTab 即可插入建议的单词:

上面的截图展示了单词自动补全引擎推荐的一个 AL 变量。

定义跳转

这是一个超级酷、必须了解的功能。你可以用鼠标悬停在变量、常量、函数或任何你想要的代码元素上,然后按 Ctrl,该单词或标识符(也称为符号)会神奇地变成一个超链接。

如果你在按下 Ctrl 的同时点击该单词,你将自动跳转到定义该单词的代码位置。Ctrl + 鼠标悬停在代码元素上也会启用跳转到定义功能。

启用此功能的其他可能方式如下:

  • 选择一个代码元素并按 F12

  • 右键点击一个代码元素,然后从上下文菜单中选择“跳转到定义”。

查找所有引用

查找所有引用可以非常方便地解析一个对象、函数或任何代码元素在源代码中被使用的次数和位置。你只需右键点击任何变量、函数或元素名称,然后选择“查找所有引用”,或者使用快捷键 Shift + Alt + F12

当它被启用时,代码编辑器会在活动栏中创建一个结果列表,显示该项被引用的次数,以及在哪些对象文件和位置(s)中引用过。侧边栏中会创建一个名为“引用”的快捷图标。下图展示了如何查找 AL 文件中特定变量的所有引用:

如果你在左侧的引用列表中展开一个出现项并点击记录,代码编辑器会打开引用该项的文件,并将光标定位到编辑模式,选择该文件中搜索到的元素。

参考列表可以清除并刷新,你可以折叠其中的所有元素。如果清除列表,你可以随时重新运行先前的搜索,因为系统会为你保留历史记录。

Peek Definition

想象一下你有大量的代码文件,你需要编辑当前正在使用的变量或字段的定义。在许多其他编辑器或开发环境中,你可能需要将所有文件保存为文本格式,然后搜索所有这些代码文件,确保替换该变量名。这个任务不仅可能很烦人,而且会让你分心,远离你原来正在编写的代码。

Visual Studio Code 通过提供 Peek 功能来解决这个问题,可以通过不同的方式启用:

  • 右键单击变量、字段或函数名,然后选择 Peek Definition。

  • 使用 Alt + F12 键盘快捷键。

应该会弹出一个交互式弹出窗口,显示定义所选元素的源代码。以下截图显示了报表中表源 Peek Definition 的窗口:

然后,你可以看到已经编写的内容,也可以直接编辑它。

重命名符号

对于开发人员,重命名变量、常量、字段或函数名是非常常见的。这些编码元素在技术上称为符号。Visual Studio Code 提供了一个非常强大的功能来重命名符号。

如果你在想要重命名的编码元素上按 F2,或者右键然后选择重命名符号,一个小的交互式弹出窗口会出现在编辑模式中。在那里,你可以写入新的元素名称,而无需使用分散注意力的对话框窗口,让你可以集中精力编写代码。所有对该代码元素的引用都将相应地被重命名。以下截图显示了重命名 XMLport 符号引用的过程:

到目前为止,所展示的所有功能都是 Visual Studio Code 提供的最有用的功能,支持 AL 开发人员进行高效的代码编辑。

在这个阶段,重要的是仔细看一下 AL 语言扩展,并了解如何配置它。

理解 AL 语言扩展

AL 现在是一个跨平台语言,通过 Visual Studio Code 的扩展部署。这个扩展不仅支持在 Windows 操作系统上部署,还支持 macOS 版本的 Visual Studio Code。

免费的 AL 语言扩展 (marketplace.visualstudio.com/items?itemName=ms-dynamics-smb.al) 可在 Visual Studio Code 市场上下载。这为 Dynamics 365 Business Central 扩展开发提供了优化的体验,并包括了构建应用程序(从现在起称为扩展的同义词)所需的所有支持和工具,包括调试器。

获取扩展并安装的最简单方法是打开任何 Dynamics 365 Business Central 代码文件(.al),并按照 Visual Studio Code 在检测到该文件类型有可用扩展时显示的说明操作:

同样,你可能想要安装其他扩展,以便为 AL 语言扩展添加其他语言(如 PowerShell)、工具(如 Docker)或增强的编辑功能。关于与 AL 语言结合使用的最有用的市场扩展列表将在第十八章中提供,AL 开发者的有用且高效工具

接下来,让我们了解 AL 语言中的这些扩展。

AL 语言

AL 语言是由 Dynamics 365 Business Central 开发团队创建的,它是为小型单租户个性化和复杂的垂直解决方案(通过在线 Dynamics 365 Business Central AppSource 市场部署)开发应用程序的官方 Visual Studio Code 扩展。

它可以通过两种不同的方式进行部署:

  • 直接作为 Visual Studio Code 市场中的可下载包。

  • 手动安装,作为可安装包(.vsix):

    • 安装包会在创建一个基于官方 Dynamics 365 Business Central 镜像的 Docker 沙箱时分发。

    • 从 Dynamics 365 Business Central 本地 DVD 下载。

要直接开始使用 AL 语言,只需通过以下简单步骤从市场中下载它:

  1. 启动 Visual Studio Code。

  2. 点击扩展视图栏。

  3. 在搜索框中输入 Dynamics 365 Business Central。

  4. 选择 AL 语言。

  5. 点击安装,并在安装完成后按要求重新加载 Visual Studio Code。它会显示以下 AL 语言扩展:

AL 语言的版本号,也称为开发版本,会显示在标题旁边。在前面的截图中,AL 语言开发版本(或运行时)是4.0.182565

开发版本非常重要,因为新语言功能和增强功能通常不会回移植到旧版本中,因此它们可能已经过时,并且与更新的 Dynamics 365 Business Central 平台更新不兼容。

AL 开发者应始终选择最新的 AL 语言开发版本,以便受益于最新的增强功能和稳定性特性。

AL 语言的开发模型涉及创建、编辑和组织具有典型 .al 扩展名的平面文本文件。简而言之:AL 语言开发只是基于文件夹和文件的。

值得一提的是,Visual Studio Code 中的术语将根文件夹称为工作区。AL 语言根文件夹代表扩展的源代码容器。因此,AL 语言根文件夹也被称为 Visual Studio Code 开发工作区。

创建任何类型的扩展时,工作区由以下项目组成:

  • launch.json 文件

  • app.json 文件

  • 符号文件

  • .al 对象文件(如表、页面、报告和代码单元)

  • 补充文件(如 WebService.xml 文件、.bmp 格式的扩展徽标文件和 permissions.xml 文件)

本书及后续章节将更深入分析 AL 语言对象和补充文件。现在我们将重点讨论应用程序开发的核心部分:launch.jsonapp.json 和符号文件。

launch.json

该文件存储在扩展工作区的名为 .vscode 的子文件夹中,主要确定下载和上传 AL 语言命令的具体参数设置。

下表显示了下载和上传 AL 命令:

下载命令 上传命令
AL: 下载符号 AL: 发布 (F5)
AL: 下载源代码 (F7) AL: 发布并在设计器中打开 (F6)
AL: 无调试发布 (Ctrl + F5)
AL: 快速应用发布 (Alt + F5)
AL: 快速应用发布无调试 (Ctrl + Alt + F5)

它还仅用于建立连接,如 AL:调试无发布(Ctrl + Shift + F5)命令,或启动特定的调试功能,如 AL:打开事件记录器。事件记录器功能将在第九章《调试》中进行介绍。

launch.json 文件是一个 JSON 数组,可能包含不同的 JSON 值,每个值代表一组针对不同部署目标的属性:本地部署或 SaaS。属性可能是必需的,也可能是可选的,具体取决于目标部署。

下表显示了 launch.json 属性:

属性 必需 部署类型 描述
Name 所有 在调试器窗口中显示的名称,用于识别启动参数集。默认值:发布到您的服务器(本地部署),发布到微软云(SaaS)。
Type 所有 常量值:al
Request 所有 常量值:launch
startupObjectType 所有 发布后运行的对象类型:表或页面。
startupObjectId 所有 StartupObjectType 一起使用。定义要运行的对象 ID。
tenant 所有 AAD 租户(SaaS)或租户名称(具有多租户的本地部署)用于连接、提取符号和/或发布应用程序包。
sandbox 在线 在为同一 AAD 租户创建多个在线沙盒时,指定沙盒名称。
breakOnError 所有 指定当发生错误时调试器是否应停止。默认值:true
breakOnErrorWrite 所有 指定调试器是否应在记录更改(插入、修改和删除)时停止。默认值:false
schemaUpdateMode 所有 确定数据同步模式。Synchronize:这是默认值。如果该扩展已经部署了数据,则会保留现有数据并且不被删除。扩展的元数据将与现有的元数据进行同步(如果存在)。Recreate:清除之前的元数据(通常是表和表扩展),并从头开始使用新的元数据。ForceSync:强制同步架构。由于可能导致数据丢失,应极其小心使用此选项。
DependencyPublishingOption 所有 此参数在 Dynamics 365 Business Central 2019 年秋季更新中引入。适用于多个依赖应用从同一根文件夹加载的复杂环境。可能的值如下:Default:启用所有依赖应用的重建和发布。Ignore:不进行依赖发布。应谨慎使用此选项,因为它有可能破坏现有的相互依赖的解决方案。Strict:如果有任何已安装的扩展依赖于启动文件夹,发布将失败。
Server 本地部署 服务器名称。默认值:http://localhost
serverInstance 本地部署 Dynamics 365 Business Central 服务器服务名称。
authentication 本地部署 身份验证类型:Windows 或用户密码。撰写时,AAD 不支持本地部署,并且它是在线部署的默认且唯一值。
Port 本地部署 Dynamics 365 Business Central 端口号。默认值:7049
applicationFamily AppSource 用于为 AppSource 开发嵌入式扩展。该标签用于 Microsoft 判断目标升级操作,如果在租户中部署了特定的 AppSource 扩展。
launchBrowser 所有

指定在发布扩展时是否启动浏览器。

|

enableLongRunningSqlStatements 所有

启用在调试时显示长时间运行的 T-SQL 语句。此功能计划支持本地部署和在线沙盒环境。

|

enableSqlInformationDebugger 所有 启用获取 T-SQL 查询信息的功能。此功能计划支持本地部署和在线沙盒环境。

如果你在 JSON 数组中设置了多个值,当执行上传或下载 AL 语言命令时,将提示你选择一个定义在 JSON 数组中的参数集名称。

以下截图是 launch.json 文件的示例:

这显示了带有两个参数设置值的文件。

app.json

通常存储在扩展工作区的根文件夹中,它代表了用 JSON 编写的应用清单。在 JSON 文件中,包含了引用基础和系统应用的参数,以及平台和运行时的定义。

在为 Dynamics 365 Business Central 开发时,必须充分理解这些术语。

系统和基础应用

在 2019 年秋季更新中,微软将所有遗留的 C/AL 代码转换为 AL 对象。目前,庞大的应用单体已经被拆分为两个应用:

  • 系统应用:大约包含 200 个对象。

  • 基础应用:根据本地化版本的不同,它包含 6,000 到 8,000 个对象。

要扩展这些应用,需要在 app.json 文件中将其作为依赖项引用,并通过 AL: Download symbols AL 语言命令将其符号拉取到本地或在线沙盒中。

当拉入 .alpackages 文件夹时,它们通常通过主版本号、次版本号、构建号和修订号来引用,并且这一点反映在下载的符号名称中(例如,Microsoft_System Application_15.0.36560.0Microsoft_Base Application_15.0.36626.36918)。

主要版本号通常对应于 Dynamics 365 Business Central 的主要更新发布。

2019 年 10 月(或 2019 年秋季)发布更新是主要版本 15。2020 年春季(或 2020 年 4 月)发布的更新将是主要版本 16,以此类推。

次版本号通常对应于次要更新。2019 年 11 月的更新 1 是次版本 15.1,2019 年 12 月的更新 2 应该是次版本 15.2,以此类推。

构建号是一个递增的数字,每当有更改提交到与功能增强或修复相关的分支时,微软就会增加该数字。

在开发扩展时,必须了解 app.json 文件中 dependency 参数定义的所需系统和应用对象级别的最低要求。

平台

平台表示 Dynamics 365 Business Central 平台组件(客户端、服务器、Web 服务器等)的最终编译结果。

它以与应用程序相同的标记显示。应用程序和平台构建通常有不同的构建号,因为平台代码更改和应用程序代码更改遵循不同的编译路径,最终会合并在一起。

在针对平台开发时,必须了解文件和 API 的最低要求,以便能够利用它们暴露的特性、属性和功能,避免应用程序出现不可预测的行为。

运行时

运行时表示 Dynamics 365 Business Central AL 语言扩展文件的最终编译结果。

该标记法更简洁,由主要版本号、次要版本号和构建版本组成。例如,2018 年春季更新(或 2018 年 4 月更新)命名为主版本 1,而 2018 年秋季更新(或 2018 年 10 月更新)是版本 2,以此类推。当前针对 Dynamics 365 Business Central 2019 年秋季更新的主要版本是版本 4。

在开发扩展时,您可以在 app.json 文件中定义应用程序的目标运行时版本。这将启用或禁用不同的功能集,这些功能集不能成为目标平台部署的一部分,AL 语言扩展的运行时将对此进行检测。

以下表格显示了 app.json 属性:

属性 必需 描述
Id 全局唯一标识符GUID
Name 扩展名称。
Publisher 发布者名称。
Version 扩展包的版本(例如,1.0.0.0)。
Brief 否(对于 AppSource 是是) 扩展的简短描述。
Description 否(对于 AppSource 是是) 扩展的长篇详细描述。
privacyStatement 否(对于 AppSource 是是) 隐私声明的 URL。
EULA 否(对于 AppSource 是是) 应用的许可条款和条件的 URL。
Help 否(对于 AppSource 是是) 应用帮助台支持的 URL。
url 否(对于 AppSource 是是) 扩展包主页的 URL。
Logo 否(对于 AppSource 是是) 从扩展根目录到应用 logo 的相对路径或完整路径
Dependencies 其他扩展的依赖项列表。从 2019 年秋季更新开始,必须至少引用系统应用程序和基础应用程序扩展。
Screenshots 应用截图的相对路径或绝对路径。
Platform 支持的最低平台版本(例如,15.0.0.0)。
idRanges 应用对象 ID 范围或对象 ID 范围的数组。
showMyCode 在调试时启用查看扩展源代码和/或从扩展管理页面下载源代码。默认值:false
Target 默认值:Cloud。与 Cloud 相同字体的扩展。它是 Target 选项的两个可能值之一(新版本为 Cloud,旧版本为 Extension)。这两个值是 Dynamics 365 Business Central SaaS 所允许的唯一值。如果需要将扩展定向到本地部署,请将该值设置为 OnPremInternal
helpBaseUrl 扩展的在线帮助的 URL。
contextSensitiveHelpUrl 否(对于 AppSource 是是) 针对 AppSource 扩展的上下文敏感帮助的 URL。
supportedLocales 应用支持的本地语言的逗号分隔列表。
features 可由编译器启用的预览版可选特性。例如,TranslationFile。将此参数标记添加到特性中时,会在扩展文件夹中生成一个名为Translations的目录,并且会生成一个包含所有扩展对象中使用的标签的.xlf翻译文件。
Runtime 扩展所针对的最低运行时版本。

以下截图是一个app.json文件的示例:

有了这些信息后,我们应该能够掌握应用程序配置文件,并根据运行时版本对其进行调整。在下一部分,我们将介绍符号并解释它们在扩展开发中的至关重要性。

理解符号

就像在所有其他语言中一样,符号表示对标准对象、属性和函数集合的引用。它们本身就是一个特殊的扩展文件,采用典型的.app命名规则,用于在编译时保持对象引用的一致性,同时还填充有效的 IntelliSense 条目。

符号通常以 JSON 格式存储在数据库中的 BLOB 字段中,每个对象记录都有一个。值得一提的是,在多租户环境中,Object Metadata表是应用程序数据库的一部分,因此在客户租户数据库中不会存储系统符号或元数据,只有数据。

在 Dynamics 365 Business Central 中,符号已经预先加载到应用程序数据库中,并且可以将它们分为两类:

  • 标准符号

  • 扩展符号

在 2019 年秋季更新之前,标准应用程序符号都是通过微软使用 CSIDE 开发环境对标准遗留对象进行特殊编译生成的。对于本地版本,情况也是如此:符号是异步生成的,或者可以通过 PowerShell 脚本作为常规扩展导入。

标准应用程序符号存储在Object Metadata表中的 Symbol Reference BLOB 字段中。

通过阅读以下官方参考文档,可以了解更多关于此主题的信息:

docs.microsoft.com/it-it/dynamics365/business-central/dev-itpro/developer/devenv-running-cside-and-al-side-by-side

下表展示了标准符号。在 2019 年秋季更新之前,了解这些符号及其重要性对成功编译和部署任何类型的扩展至关重要:

应用程序 包含 CSIDE 对象设计器中描述的所有应用程序对象的符号,除了 2000000004 到 2000000199 ID 范围内的系统表和标准测试工具对象。在本地版本或 Docker 沙盒中,如果你正在修改标准遗留对象,你必须选择通过 CSIDE 开发环境(重新)生成符号,正如以下博客文章中所述:在现代开发环境中使用 Microsoft Dynamics NAV 2018 生成符号。在升级后的本地版本中,来自早期版本的符号必须从标准本地数据库(或产品 DVD)中提取,导入到升级数据库中,并重新生成,正如以下博客文章中所述:在新建或升级数据库中导入符号与 Microsoft Dynamics NAV 2018
系统 包含 2000000004 到 2000000199 ID 范围内的系统表符号,也包括虚拟表定义。系统和虚拟表结构无法通过扩展进行修改。系统和虚拟表符号无法重新生成。因此,如果你正在开发扩展,它们永远不应该在 CSIDE 开发环境中进行任何更改。
测试 包含应用程序测试工具对象的符号。标准应用程序测试工具对象的符号无法重新生成。开发人员应创建自己的测试对象。因此,在现代开发环境中进行 SaaS 部署时,它们永远不应在 CSIDE 开发环境中进行任何更改。

每当你扩展一个应用程序时,你总是需要确保适当的符号到位。你可以通过两种方式实现这一点:

  • 连接到沙盒环境,运行命令面板(Ctrl + Shift + P),然后输入并选择AL: Download Symbols

  • 从其他地方(例如产品 DVD,用于本地部署)下载所需的符号,并将其存储在定义的符号存储目录中。

对于本地部署,您将在 Dynamics 365 Business Central 2019 年春季版产品 DVD 中找到 System.appTest.app 符号,路径为:\ModernDev\program files\Microsoft Dynamics NAV\140\AL Development Environment。在 Dynamics 365 Business Central 2019 年秋季版 DVD 中,您只会在以下路径找到 System.app\ModernDev\program files\Microsoft Dynamics NAV\150\AL Development Environment。自 2019 年秋季更新以来,AL 语言运行时不再自动下载应用程序和测试符号,且这些符号不再需要存储在数据库内,因为所有属于应用程序的对象,包括 Test Toolkit 中的对象,已经转化为 AL 对象。这些 AL 对象现在是标准扩展包的一部分。扩展包本身就包含符号。

如果您有一个多用户环境,且开发人员在同一个暂存租户上工作,您可以考虑通过命令面板下载符号一次,然后为所有用户设置一个公共的符号存储路径。这样可以避免每次都下载相同的符号集,从而提高开发生产力。

默认的符号存储路径可以通过以下快捷键之一进行更改:

  • 从菜单栏中,选择“文件”(Alt + F)| “首选项”(P)| “设置”(S),然后选择 AL 语言设置。

  • 使用设置快捷键(Ctrl),然后选择 AL 语言设置。

要更改的参数是 Al: Package Cache Path,其默认值设置为相对路径 ./.alpackages

或者,您可以运行命令面板(Ctrl + Shift + P),输入并选择“Preferences: Configure language specific settings...”,然后选择 AL。settings.json 文件将打开,您可以在其中添加或更改 al.packageCachePath 参数的值。下图显示了 AL 设置符号路径在更改为存储位置时的情况:

本章后续将讨论其他 AL 语言配置设置。

结合系统应用扩展、基础应用扩展和标准符号,您的扩展可能还依赖于其他自定义或第三方扩展。然后,这些扩展应该会发出符号,您应该能够在调用 AL:从命令面板下载符号时从应用程序数据库中下载这些符号。

扩展符号存储在 NAV App 表的 Symbols BLOB 字段中。

要指定您的扩展依赖于另一个扩展,您必须在 app.json 文件中填充相关的 JSON 数组参数。下面是一个依赖于两个其他应用的扩展在 app.json 文件中的参数示例:

  "dependencies": [
      {
          "appId":  "63ca2fa4-4f03-4f2b-a480-172fef340d3f",
          "publisher":  "Microsoft",
          "name":  "System Application",
          "version":  "15.0.0.0"
      },
      {
         "appId":  "437dbf0e-84ff-417a-965d-ed2bb9650972",
         "publisher":  "Microsoft",
         "name":  "Base Application",
         "version":  "15.0.0.0"
      },
      {
        "appId": "99ddd910-3aa8-4c3e-936c-be20edeaf777",
           "name": "Preferred Wine Basic"
            "publisher": "Tacconi Inc."
             "version": "1.0.0.0"
      },  
     {
        "appId": "77ddd910-3aa8-4c3e-936c-be20edeaf888",
        "name": "Preferred Wine Tools",
        "publisher": "Tacconi Inc.",
        "version": "2.1.0.0"
     }
   ],

如果你已经从 Cloud Ready Software 安装了 CRS AL Language Extension 工具箱(marketplace.visualstudio.com/items?itemName=waldo.crs-al-language-extension),你可以输入 tdependency 来启用代码片段,轻松编辑该参数的每个 JSON 数组元素。这将加快你的编码速度,防止语法错误。在本章最后一节中,我们将讨论标准和自定义代码片段的功能。

依赖扩展的版本参数表示编译器接受符号的最低要求。换句话说,低于报告版本的依赖扩展符号不被认为是有效的,无法下载或编译。

符号内部

符号是多个文件的压缩(.zip)操作的结果,这些文件由 AL Language 扩展使用。为了展示其内部原理,只需使用最常见的解压工具(例如 7-zip)在将 .app 包重命名为 .navx 扩展名后提取其内容。

以下表格显示了基础应用程序扩展的标准符号组件(文件和目录):

文件名 描述
[Content_Types.xml] 指定包的内容:XML 和 JSON 文件。
MediaIdListing.xml 指定扩展标志文件名及其 ID。
navigation.xml 包含部门菜单的条目。
NavxManifest.xml 它将报告标准符号或扩展的清单。基础应用程序符号的最相关参数如下:- version:标识 JSON 文件的应用程序版本(如 15.0.36626.36675)- platform:目标推荐的与这些符号兼容的主要平台版本(如 15.0.0.0)- runtime:建议用于这些符号的运行时版本(如 4.0)系统符号通常只指定版本和运行时。
SymbolReference.json 包含所有 AL 对象的 JSON 格式引用。这些 JSON 文件被 AL Language 扩展广泛使用,用于在编译/构建应用程序包时保持引用完整性,并启用所有与 IntelliSense 相关的功能。基本上,它的结构是一个数组,包含有效 AL 对象参数的列表,如下所示:"Tables": [],`` "Codeunits": [],`` "Pages": [],`` "PageExtensions": []对于这些对象元素,每个都有指定的字段、属性、函数等。

符号 JSON 文件不能被篡改/更改以手动生成或修改符号文件。

接下来,我们还将看看不同目录的作用:

目录名称 内容描述
addin 控制插件定义。
layout RDL 和 DOCX 报告布局。
logo 扩展标志。
ProfileSymbolReferences 配置文件符号和相关页面自定义的符号。
src AL 文件。其内容通常用于在调试时显示代码。
Translations XLIFF 格式的翻译文件。

符号是扩展验证机制的核心,正如之前的表格所示,它们还执行代码(如果在扩展的app.json文件中将showmycode参数设置为 true 的话)。

基于 AL 符号,你可以在 Visual Studio Code 市场中找到非常有用的扩展,这些扩展专门针对 AL 开发环境。

最常用的配置项如下:

  • Marton Sagi 的 AL 对象设计器

  • Andrzej Zwierzchowski 的 AZ AL 开发工具/AL 代码大纲

两者都非常易于使用,并且对检查符号及其内容非常有用。

在了解了符号之后,我们已经完成了构建应用所需的主要内容的概述。接下来,让我们看看 AL 语言扩展配置,了解如何配置它们以创建更高效的开发环境。

理解 AL 语言扩展配置

可以通过快捷键 Ctrl+ 来轻松查看常规设置和每个工作区设置。会弹出一个直观的菜单,选择“扩展 | AL 语言扩展配置”,即可列出一系列配置参数。以下截图显示了 AL 语言扩展配置参数:

基本上,这些配置值保存在一个名为settings.json的文件中。

以下是常见配置项的描述和值:

  • 对于以下路径参数:

    • "al.packageCachePath": "./.alpackages":可以将默认值更改为本地文件夹或多开发者环境下的共享文件夹。它表示存储和查找符号的路径。

    • "al.assemblyProbingPaths": ["./.netpackages"]:这个参数在编译扩展时至关重要,特别是当存在外部程序集引用时。它的数据类型是 JSON 数组,因此开发者需要指定一个以逗号分隔的路径列表,表示程序集存储的位置。

    • "al.ruleSetPath": null:如果开发者希望提供自定义的标准代码分析器规则覆盖,可以使用此设置。它将在第九章 调试中进行详细讨论。

  • 对于以下代码分析器参数:

    • "al.enableCodeAnalysis": false:此设置用于启用代码分析,具体内容将在第九章 调试中详细讨论。在大型项目中,若有数千个对象,建议关闭此功能,以避免在编码或编译过程中出现性能问题。

    • "al.codeAnalyzers": []:这是代码分析器的类型。其详细内容将在第九章 调试中进一步探讨。

    • "al.enableCodeActions": false:启用代码操作,如自动将多个if语句转换为CASE语句或拼写检查。默认情况下该功能为禁用状态。

    • "al.backgroundCodeAnalysis": true:默认启用。在大型项目中,这可能会对性能造成严重影响,建议在此类场景中关闭此功能。

  • 对于以下编译参数:

    • "al.compilationOptions": {"generateReportLayout": true, "parallel": true}:用于指定在编译时是否生成报告布局(如果报告布局不存在),以及是否对包进行串行或并行构建。

    • "al.incrementalBuild": false:在复杂的扩展开发环境中,如果从根文件夹加载多个扩展文件夹,则此参数指定是否从引用的项目中进行引用解析,而不是从存储在包缓存路径中的符号中进行解析。将此参数切换为 true 会在此类场景中提高性能。

  • 对于以下服务日志参数:

    • "al.editorServicesLogLevel": null:这对于调试编译报告中未处理的错误或崩溃的情况非常有用。日志可能包含错误,甚至详细描述发生在后台的过程。将会在第九章中更深入地讨论,调试部分。

    • "al.editorServicesPath": "bin/":如果启用了服务日志,则决定日志路径。

  • 对于以下浏览器参数:

    • "al.browser": "Edge":选择首选浏览器,从 Visual Studio Code 启动 Dynamics 365 Business Central 应用程序。选项包括 SystemDefault、Edge、Chrome 或 Firefox。如果安装了多个浏览器,则此选项非常有用。

    • "al.incognito": false:选择以正常会话启动浏览器,该会话存储现有的凭证,或使用私人/隐身浏览。

在探索了开发扩展所需的核心设置之后,让我们分析一下 AL 语言提供的最佳代码编辑功能之一:代码片段。

精通 AL 语言代码片段

安装 AL 语言扩展后,Visual Studio Code 中提供了 AL 语言标准代码片段。这些片段会在你在代码编辑器中输入时触发,你可以通过方框前缀符号识别它们。

通常,它们以字母t开头,后跟一个有意义的名称,用以描述代码片段的内容,例如ttabletpage。悬浮提示显示代码片段的预览。

以下截图展示了一个 if-then-else 条件语句的标准代码片段:

请注意,如果代码片段包含变量名或代码标识符,它们可能会被高亮显示,提示你应该为其指定不同的名称,并且它们充当某种占位符。当你重命名一个被高亮的标识符时,所有该标识符的出现位置也会被重命名,这使得代码片段的使用非常灵活。这不仅会减少编码时间,避免写或复制粘贴重复的语句,还能使用开发者可能忽略的复杂结构语法。

可以直接从 Visual Studio 市场下载由其他开发者制作的代码片段,形式为扩展。通常,许多扩展程序会扩展 AL 语言的支持,并附带一系列自己的代码片段。

一个典型的例子是免费的 CRS AL 语言扩展。

除了几个非常有用的开发者工具外,这个扩展还实现了一组与现有标准片段集成并丰富它们的 AL 代码片段。目前,它实现了 68 个额外的 AL 代码片段,并且随着每次扩展更新,列表还在不断增长。

以下截图显示了如果你在 .al 文件中输入 CRS 时,所有可用的额外代码片段:

另一个在编码时搜索代码片段的方法是运行命令面板 (Ctrl + Shift + P),然后输入 snippet 或 insert snippet 来调出一个下拉列表,显示可用的 AL 代码片段。

以下截图显示了来自命令面板的 AL 代码片段下拉列表:

如果你仍然没有在市场中找到对你有用的代码片段,使用 Visual Studio Code 你也可以手动从零开始添加新的代码片段。要做到这一点,你需要点击菜单栏并进入 文件 (Alt + F) | 首选项 (P) | 用户代码片段 (S + S).

快捷键序列 Alt + FPSSEnter 可以直接将你带到那里,而无需使用鼠标。

然后,你可以选择是为所有语言创建一个全局代码片段,还是为当前工作区创建一个本地代码片段,或者为特定目标语言创建一个片段。在这个例子中,我们将选择从语言列表中选择 al (AL),创建一个新的代码片段以供 AL 语言文件使用。

以下截图显示了创建特定代码片段时的可用选项:

如果你启用了面包屑功能,可能已经注意到一个特定的配置文件处于编辑模式,用于自定义 AL 语言代码片段。通常,这个文件叫做 al.json,并存储在以下位置:

C:\Users\<username>\AppData\Roaming\Code\User\snippets

每个代码片段由一个独特的名称定义,并由三个元素组成:

  • 前缀:用于在编辑器中搜索并触发代码片段

  • 主体:粘贴到编辑器中的部分

  • 描述:对代码片段功能的详细描述

在代码块内,你可以使用特定的语法来启用占位符:

  • $1$2$3$n 用于通过按下 Tab 键来移动代码片段中的光标位置。

  • $0 用作最终的光标位置。

  • ${1:labelX}${2:labelY}${3:labelZ} 用作占位符。具有相同 ID 的占位符会相互关联,从而启用多个光标功能。

现在,我们将通过一个简单的示例进行演示。

假设你想在对象顶部添加一个标准的代码头块,就像在旧版设计器(CSIDE 开发环境)中一样,并且你需要一个智能的方式,在每个对象上快速且重复地实现这一功能。

最简单的解决方案是创建一个特定的自定义代码片段,在每次新建对象文件时调用,如下所示:

  1. 将以下代码添加到 al.json 文件中并保存:
{
     "Create standard comment block": {
        "prefix": "tcomment (Custom)",
       "body": [
  "//",
  "//  ${1:YY.MM.DD} Initialization",
  "//  ${1:YY.MM.DD} ${2:Modification Description}",
  "//"
 ],
 "description": "Standard header comment block"
}
}

你可以通过简单地进入菜单栏并选择 文件(Alt + F)| 自动保存(U)来启用惊人的自动保存功能。自动保存菜单项旁会出现一个勾选标记。另一种方法是运行命令面板(Ctrl + Shift + P),然后输入 File: Toggle Auto Save(或者输入其中一部分并从下拉列表中选择该条目)。

  1. 在你的扩展中创建一个新文件,并将其保存为 .al 扩展名(例如,MyCodeunit.al)。光标应该会自动定位到文件的第一行和第一列。

  2. 开始输入 tcomment,IntelliSense 将检测到你自定义代码片段的存在。选择它。

  3. 光标将定位到第一个占位符元素。只需输入当前日期(格式为 YY.MM.DD),然后按 Tab。你可能会注意到,由于两个占位符共享相同的 ID,它们会一起编辑,从而启用多个光标功能。

  4. 现在,是时候写下与对象描述相关的有用信息,说明它的用途。

以下截图展示了带有多个光标的自定义注释块代码片段的实际应用:

这些代码片段使理解 Visual Studio Code 变得更加简单。试试看,掌握它们吧!

总结

Visual Studio Code 是一个以代码为中心的工具,开箱即用支持多种语言,提供诸如语法着色、分隔符匹配、代码块折叠、多光标、代码片段、IntelliSense 等编程功能,还有更多功能。

通过安装 AL 语言扩展,这个现代化的开发环境已完全设置好,成为初学者和有经验开发者的应用沙盒。在本章中,我们释放了一些技巧和窍门,使你能够熟练掌握开发人员日常工作,快速为 Dynamics 365 Business Central 创建现代应用。

接着,我们开始学习这个现代开发环境所提供的强大编码功能。所有这些学习结束后,是时候在本书中看到 AL 语言的实际应用了。但是,在进入结构化和高级扩展开发之前,了解如何实现和维护一个沙箱/预发布环境是很重要的。这正是我们在下一章要做的内容。

第三章:在线和基于容器的沙盒

Dynamics 365 Business Central 允许我们为开发和测试目的设置沙盒。对于这些沙盒,有两种通用选项:在线沙盒(基于 SaaS)和基于 Docker 的沙盒(自部署)。

在线沙盒创建非常简单,因为它们作为服务运行,与 Dynamics 365 Business Central 的生产环境相同。唯一的要求是已有的生产租户。

基于 Docker 的沙盒基于 Docker 容器,可以在 Azure 或本地运行。

本章我们将覆盖以下主题:

  • 创建在线沙盒

  • Docker 镜像和容器的基础知识,以及如何使用它们

  • 设置本地 Docker 环境

  • 掌握 navcontainerhelper PowerShell 工具

  • 为你的开发目的选择合适的 Docker 镜像

  • 创建你自己的 Docker 镜像

创建在线沙盒

在订阅试用租户或直接购买 Dynamics 365 Business Central 时,你也可以创建在线沙盒环境。

你可以通过两种方式创建在线沙盒:通过生产环境客户端和/或通过 Dynamics 365 Business Central 管理中心。

从生产环境创建时,搜索 (*Alt *+ Q) sandbox,然后选择沙盒环境(预览版):

然后你将被提示创建一个新的沙盒环境,或者打开或重置现有的沙盒。如果没有沙盒且你选择打开或重置,将创建一个新的沙盒。使用生产环境客户端创建的在线沙盒具有以下属性:

  • 它们默认命名为 sandbox,并且也能在 Dynamics 365 Business Central 管理中心中看到。

  • 它们不包含任何客户数据,仅包含来自标准演示/评估 Cronus 公司的数据。

在订阅在线沙盒租户时,仔细阅读微软免责声明非常重要,值得一提的是,此功能仍标记为预览版:

你还可以从 Dynamics 365 Business Central 管理中心创建最多三个沙盒环境。使用适当的凭证,通过支持的浏览器登录 https:\businesscentral.dynamics.com\GUID\Admin,其中 GUID 是你环境的租户 ID。

以下是管理中心中的样子:

点击 新建 按钮,然后为新的在线沙盒指定一个有效的名称。勾选复制生产数据的选项:

稍等片刻,你的沙盒将会启动并运行,带有生产数据的副本,准备好进行测试和开发任务。

在线沙盒的优缺点

根据微软的免责声明,在线沙盒仍然是预览功能,我们建议仅将其用于演示目的或临时开发活动。在线沙盒具有以下特点:

  • 它们的支持优先级不同于生产环境。

  • 租户是创建在 Azure SQL 数据库的 S 等级池中,这些池的性能不如生产环境中的 P 等级池。

  • 它们仍然被标记为预览版,因此经常会发生变化。

  • 在升级过程中,目前所有每个租户的扩展都将被卸载,您/您的 CSP 合作伙伴需要重新安装它们。

您可以通过阅读以下文章了解更多背景信息:

对于专业开发,基于容器的沙盒更为合适,我们鼓励您在开发 AppSource 或每个租户扩展时,在公司中使用 Docker 容器沙盒。

从在线生产环境中,您也可以搜索(Alt + Q)沙盒并点击沙盒环境(容器),轻松设置离线本地或 Azure 托管的 Docker 容器沙盒。

您可以通过demiliani.com/2018/03/29/d365bc-container-sandbox-environment/了解更多信息。

让我们进入下一部分,学习如何使用基于 Docker 的环境。

引入 Docker

如果选择使用自部署的沙盒,它们将基于 Docker。Docker 是领先的跨平台软件容器环境。由于本书是关于 Dynamics 365 Business Central 的,本节将简要介绍 Docker,但如果您想了解更多,有关 Docker(docs.docker.com/)和微软(docs.microsoft.com/en-us/virtualization/windowscontainers/about/index)的官方文档中有很多优秀的学习资源,都是非常好的起点。

Dynamics 365 Business Central 在 Windows 上运行,因此当您查找 Docker 文档时,请确保它是针对 Windows 的。虽然大部分 Docker 是平台无关的,但仍有一些特定平台的部分内容。

为了跟随接下来的内容,你需要了解以下 Docker 基础知识:

  • Docker 镜像就像一个预构建的模板,包含运行所需的最少操作系统二进制文件、库和应用程序二进制文件。镜像可以通过名称来识别,例如 Business Central / Sandbox。可以通过标签指定确切的镜像版本。例如,1910-cu1-de 表示 2019 年 10 月发布1910)、CU 1cu1)、德语版本de)。

  • Docker 容器是镜像的一个实例,具有不可变的基础(镜像中的文件)以及在其上进行的更改。容器不是 虚拟机VM)。它没有 GUI 或任何你可以通过 远程桌面协议RDP)连接的内容。

  • Docker 主机是一个运行容器的(物理或虚拟)机器。

  • Docker 仓库是你和其他人可以上传(推送)和下载(拉取)镜像的地方。具体来说,镜像可以从属于该注册表的仓库中下载。

所有 Dynamics 365 Business Central Docker 镜像都可以通过 Microsoft 容器注册表获得,你可以在 mcr.microsoft.com 的 business central 仓库中找到它。它将被称为 sandboxon-prem,因此,从 Dynamics 365 Business Central 本地镜像拉取的典型地址是 mcr.microsoft.com/businesscentral/onprem:1810-cu5-de

对于新版本和即将发布版本的预览,还有一个特殊的仓库 bcinsider.azurecr.io,但你需要凭证才能访问它。Microsoft 通过 Ready to Go! 程序提供这些凭证,你可以在协作平台上找到它们 (docs.microsoft.com/en-us/collaborate/)。

用于创建和运行 Dynamics 365 Business Central 镜像的脚本是开源的,可以在 github.com/Microsoft/nav-docker 上找到。

还值得注意的是,尽管这些镜像被称为 business central,但它们还包含一个 SQL Server 数据库,一个用于 Web 客户端 的 IIS,以及默认的文件共享功能。

使用 Docker 时的一些基础机制

在接下来的章节中,我们将使用几个涵盖大多数实际场景的机制。让我们逐一了解它们。

环境变量

环境变量是一种在 Docker 容器启动时进行参数化的方式。Dynamics 365 Business Central 镜像理解许多环境变量,例如用于设置认证类型、用户名和密码,或设置你希望将 Business Central 服务层连接到的 SQL 服务器和数据库。环境变量通过 -e 参数设置。如果你需要设置多个环境变量,可以使用多个 -e 参数:

-e auth=Windows -e databaseserver=sql -e databasename=cronus

没有关于所有环境变量的列表,你可以使用这些环境变量来根据你的需求配置 Dynamics 365 Business Central 容器,但用于设置这些变量的脚本是一个不错的起点,并且这些变量的名称可以让你大致了解它们的作用。你可以在 github.com/Microsoft/nav-docker/blob/master/generic/Run/SetupVariables.ps1 找到这个脚本。

使用卷,你可以将 Docker 主机上的文件夹映射到容器中,例如,为容器提供访问运行解决方案所需的二进制文件或其他文件的权限。

如果你没有使用卷,并且删除了一个容器,那么你对容器内文件系统所做的所有更改将会丢失,因为它们会与容器一起被删除。

卷通过 -v 参数进行设置,后面跟着主机上的路径,一个冒号,以及容器内部的路径。如果你想将主机上的 c:\data\containers 文件夹映射到容器内的 c:\temp,你可以使用以下命令:

-v c:\data\containers:c:\temp

网络和端口

Docker 让你可以通过不同的方式将容器连接到网络。如果你没有做其他配置,它会使用一个所谓的 NAT 网络。这意味着你的容器将获得一个仅在 Docker 主机上可知的 IP 地址,这也使得容器在主机上是可访问的。作为替代方案,你可以创建一个所谓的透明网络,这意味着容器将共享主机的网络连接,并且会尝试使用 DHCP 动态 IP 分配或你配置的静态 IP 地址来获取自己的 IP 地址。如果你有一个名为 transpNet 的透明网络,你可以通过以下命令告诉 Docker 使用它:

--network transpNet

Dynamics 365 Business Central 镜像使用所有标准端口,因此你有 7045-7049 用于 Business Central 服务层服务,443 用于 HTTPS,80 用于 HTTP 的 Web Client,以及 1443 用于 SQL。它们还通过端口 8080 共享一些文件,这个端口被映射到 IIS 后端共享。

如果你不想使用透明网络,但仍然希望将端口暴露到 Docker 主机之外,你可以使用一种叫做端口映射的机制,使用 -p 参数。通过这个参数,你可以告诉 Docker 将容器的某个端口映射到主机上的相同端口或不同端口。如果你想将基于 HTTPS 的 Web Client(监听在端口 443 上)暴露到主机的端口 4443,你通常会使用以下命令:

-p 4443:443

由于 Dynamics 365 Business Central 在知道它监听哪些端口时会运行得更好,最佳方法是包含 -e WebClientPort=4443,这样会让 Web Client 监听该端口而不是标准端口 443。因此,你的端口映射参数将是 -p 4443:4443

Docker 在 Dynamics 365 Business Central 沙盒中的特别有用场景

有一些场景下,使用 Docker 容器非常有意义,可以帮助解决常见的问题。请注意,在撰写本文时,Dynamics 365 Business Central 在 Docker 容器中运行尚不支持生产环境,因此你只能将其用于开发和测试。本文将覆盖的场景如下:

  • 本地可用场景:你希望在本地沙盒或自己的虚拟机上运行沙盒。

  • 集中式可用环境:沙盒提供于一个中央环境中,并由某种操作团队进行管理。

  • 托管在 Azure 虚拟机上的容器:如果你不想或者不能在自己的数据中心拥有虚拟机,你可以使用 Azure 的基础设施即服务IaaS)来托管你的沙盒。

  • Azure 容器实例中的无服务器环境:如果你只想运行容器而不关心 Docker 本身,你也可以使用 Azure 容器实例的平台即服务PaaS)选项。

值得一提的是,还有其他选项,比如Azure Kubernetes 服务AKS)或其他云提供商的产品,但由于 Dynamics 365 Business Central 倾向于与更多微软导向的客户一起使用,而 Kubernetes 最近才开始支持 Windows 容器,因此这里不会讨论这些选项。

Docker 对于持续集成/持续交付CI/CD)流水线中的自动化构建也非常有用。这个主题将在第十一章,与 Business Central 的源代码管理和 DevOps中深入讨论。

使用纯 Docker 命令的本地可用环境

如果你想让使用者完全控制 Dynamics 365 Business Central 沙盒或需要离线使用它们,那么将沙盒放在本地是有意义的。然而,这会要求使用者至少理解 Docker 的基础知识,虽然这并不复杂,但可能不符合你的需求。

你的第一个容器

运行 Dynamics 365 Business Central 沙盒的第一步是安装 Docker。这个过程很简单,并且有相关文档可供参考:docs.docker.com/install/windows/docker-ee/。完成安装后,就可以开始运行你的第一个容器。微软提供了一个非常有用的 PowerShell 模块navcontainerhelper,它简化了容器创建过程。然而,为了帮助你理解底层机制以及 Dynamics 365 Business Central 镜像的工作原理,最好通过几个简单的 Docker 示例来了解。

运行 Dynamics 365 Business Central 沙盒的最基本方式是使用以下命令:

docker run -e accept_eula=y mcr.microsoft.com/businesscentral/sandbox

这只在所谓的进程隔离模式下有效,这允许容器根据需要使用主机资源(如内存)。根据你的配置以及你是否运行的是旧版 Windows 10 或 Docker,你可能会收到一条错误信息,表示容器内存不足。在这种情况下,添加-m 3G作为参数,这将允许容器保留 3 GB 的内存。

有关进程隔离的更多信息,请参阅 docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container

请注意,默认情况下,这将基于 Windows Server 2016 拉取镜像。如果你想运行基于 Windows Server 2019 的更小、更快速的镜像,你需要添加一个称为标签的参数;在这种情况下,这个标签将是ltsc2019。更多细节可以在选择合适的镜像部分找到:

docker run -e accept_eula=y mcr.microsoft.com/businesscentral/sandbox:ltsc2019

这指示 Docker 使用指定的镜像(即mcr.microsoft.com/businesscentral/sandbox)运行一个容器。运行容器意味着 Docker 会检查该镜像是否已经被下载(拉取),如果没有,它将拉取镜像。如果已经存在,或者 Docker 已经为你下载了镜像,它将为该镜像创建并启动一个容器。你将看到从主进程输出的内容,显示在你运行该命令的控制台窗口中。

使用-e参数时,你让 Docker 知道你希望在容器内设置accept_eula=y环境参数,这意味着你接受最终用户许可协议EULA)。每当你运行 Dynamics 365 Business Central 沙箱时,这都是必要的。

以下是docker run命令的输出,其中镜像本地不可用:

docker run -e accept_eula=y -m 3G mcr.microsoft.com/businesscentral/sandbox

Unable to find image 'mcr.microsoft.com/businesscentral/sandbox:latest' locally

latest: Pulling from businesscentral/sandbox

3889bb8d808b: Already exists

…

6d7321cdab15: Already exists
5c2abed3c0c2: Already exists
0bc14e36adef: Pull complete
4fd56667f5dc: Pull complete
…
4dd9d6309b80: Pull complete
Digest: sha256:ca99037c70e1eedf21e8472a5d46efeb148dd46a7b16bdf2ddad864e2e4cb97c
Status: Downloaded newer image for mcr.microsoft.com/businesscentral/sandbox:latest
Initializing...
Starting Container
Hostname is 12cec5da3d89
PublicDnsName is 12cec5da3d89
Using NavUserPassword Authentication
Starting Local SQL Server
Starting Internet Information Server
Creating Self Signed Certificate
Self Signed Certificate Thumbprint 857B5A19C68D44BE3368CB96E2AFF6F540C0D957
Modifying Service Tier Config File with Instance Specific Settings
Starting Service Tier
Registering event sources
Creating DotNetCore Web Server Instance
Enabling Financials User Experience
Creating http download site
Setting SA Password and enabling SA
Creating admin as SQL User and add to sysadmin
Creating SUPER user
Container IP Address: 172.20.200.44
Container Hostname  : 12cec5da3d89
Container Dns Name  : 12cec5da3d89
Web Client          : https://12cec5da3d89/BC/
Admin Username      : admin
Admin Password      : Fuwu1800
Dev. Server         : https://12cec5da3d89
Dev. ServerInstance : BC
Files:
http://12cec5da3d89:8080/al-4.0.192371.vsix
http://12cec5da3d89:8080/certificate.cer
Initialization took 164 seconds
Ready for connections!

如你所见,日志输出会告诉你在哪里可以访问容器的Web Client和开发服务器服务(用于与 Visual Studio Code 的 AL Language 扩展进行连接)(暂时使用 IP 地址,稍后会详细说明)以及使用的用户名和密码。

拉取新镜像版本

请注意,默认情况下,Docker 不会检查镜像是否有新版本并提示你下载。如果你想确保你有当前版本,可以运行以下命令,它将始终检查是否有更新的镜像:

docker pull mcr.microsoft.com/businesscentral/sandbox

如果有新版本可用,Docker 将会下载它,并输出类似于你之前看到的内容。如果没有新版本可用,你将看到以下信息:

docker pull mcr.microsoft.com/businesscentral/sandbox
Using default tag: latest
latest: Pulling from businesscentral/sandbox
Digest: sha256:ca99037c70e1eedf21e8472a5d46efeb148dd46a7b16bdf2ddad864e2e4cb97c
Status: Image is up to date for mcr.microsoft.com/businesscentral/sandbox:latest

我们还可以使用更多环境参数、卷和端口映射,来展示一个更高级的示例,同时使用我们之前介绍的机制:

docker run -e accept_eula=y -e usessl=n -v "c:\dev\addins:c:\program files\Microsoft\Dynamics Business Central\150\Service Tier\Add-ins\mine" -p 80:80 –name mycontainer mcr.microsoft.com/businesscentral/sandbox

这实现了以下功能:

  • 它接受 EULA,并通过两个-e参数告诉镜像不要为Web Client使用 SSL。

  • 它使用-v参数将本地文件夹c:\dev\addins映射到 Business Central 服务层Add-Ins文件夹中的mine子文件夹。

  • 它使端口80在你的笔记本电脑上作为端口80可用,使用-p参数。你也可以使用-p 8080:80,这会将你笔记本电脑上的端口8080映射到容器上的端口80

  • 它将容器命名为mycontainer,这样你可以轻松地通过--name参数来引用它。

既然我们已经了解了如何运行本地 Docker 环境,接下来让我们看看如何连接到现有的 SQL Server 实例。

连接到现有的 SQL Server

你可以指示容器连接到现有的 SQL Server 和数据库,而不是使用容器内部的 SQL Server,这样容器内部的 SQL Server 也不会启动。如果你想将多个实例连接到同一个数据库,这是必要的,但如果你想在同一主机上运行多个容器,这也很有意义。否则,你将每个容器都运行一个 SQL Server,这将需要大量资源。

在本节中,我假设在你的本地环境中运行着一个名为sqlserver的 SQL Server(无论是否在容器中),并且 Business Central 容器能够访问它。我有一个名为sqluser的 SQL 用户,密码为1SuperSecretPwd!,该用户具有访问该服务器上名为FinancialsW1的 Business Central 数据库的必要权限。对于这种情况,docker run命令将如下所示:

docker run -e accept_eula=y -e databaseServer=sqlserver -e databaseUsername=sqluser -e "databasePassword=1SuperSecretPwd!" -e databasename=FinancialsW1 mcr.microsoft.com/businesscentral/sandbox

我们得到以下输出:

Initializing...
Starting Container
Hostname is c9658bdbe7f0
PublicDnsName is c9658bdbe7f0
Using NavUserPassword Authentication
Starting Internet Information Server
Import Encryption Key
Creating Self Signed Certificate
Self Signed Certificate Thumbprint 87B2FC05A437AFE41966A3E99EFC75A2C2CAD537
Modifying Service Tier Config File with Instance Specific Settings
Starting Service Tier
Creating DotNetCore Web Server Instance
Enabling Financials User Experience
Creating http download site
Creating Windows user admin
Container IP Address: 172.26.151.47
Container Hostname  : c9658bdbe7f0
Container Dns Name  : c9658bdbe7f0
Web Client          : https://c9658bdbe7f0/BC/
Dev. Server         : https://c9658bdbe7f0
Dev. ServerInstance : BC
Files:
http://c9658bdbe7f0:8080/al-4.0.192371.vsix
http://c9658bdbe7f0:8080/certificate.cer

以下是最终输出:

Initialization took 116 seconds
Ready for connections!

请注意,在这种情况下,容器不会在数据库中创建任何用户,因为它假设如果你使用的是现有数据库,那么你也会在该数据库中有现有的用户。同时,请注意,容器将把它自己的加密密钥导入到数据库中,这个密钥用于加密密码。因此,如果你想以这种方式连接多个容器到同一个数据库,你需要确保加密密钥是共享的。

使用 Docker cmdlet 处理正在运行的容器

如果你需要获取 PowerShell 会话进入正在运行的容器,你有两个选项:

  • 首先,你可以使用Enter-PSSession,就像连接到另一台计算机一样,不过你需要给它容器的完整 ID,而不是计算机名。最简单的方式是使用一个子命令查询 Docker 以获取该容器。进入mycontainer容器的 PS 会话将如下所示:
Enter-PSSession -containerid (docker --no-trunc -qf "name=mycontainer")

上述语句在以管理员身份运行时有效。

  • 第二个选项是执行容器中的powershell命令,并指示 Docker 为其打开一个交互式终端。对于mycontainer容器,这将如下所示:
docker exec -ti mycontainer powershell

使用这两个命令,你将在容器内启动一个 PowerShell 会话。如果你想运行 Business Central cmdlet,最简单的方式是调用 c:\run\prompt.ps1,它会导入所有开发和管理员 cmdlet。

要查看当前正在运行的所有容器,你可以输入并执行 docker ps,它会显示以下输出:

docker ps
CONTAINER ID        IMAGE                                        COMMAND                  CREATED             STATUS                       PORTS                                                NAMES
12cec5da3d89        mcr.microsoft.com/businesscentral/sandbox    "powershell -Comma..."   22 minutes ago      Up 21 minutes (healthy)      80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   determined_shockley
9518e7e456de        mcr.microsoft.com/dynamicsnav:2017-cu22-de   "powershell -Comma..."   42 minutes ago      Up 41 minutes (healthy)      80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   testcont
95959a311e54        mcr.microsoft.com/dynamicsnav:2017-cu22-de   "powershell -Comma..."   About an hour ago   Up About an hour (healthy)   80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   RKOS-18111
13057bf415cc        mcr.microsoft.com/dynamicsnav:2017-cu22-de   "powershell -Comma..."   22 hours ago        Up 22 hours (healthy)        80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   VOEKOM
0e7970ce5318        mcr.microsoft.com/dynamicsnav:2017-cu22-de   "powershell -Comma..."   29 hours ago        Up 29 hours (healthy)        80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   Buchau
6c1c44f170bb        mcr.microsoft.com/dynamicsnav:2017-cu22-at   "powershell -Comma..."   46 hours ago        Up 46 hours (healthy)        80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   syAToekom

要查看所有当前存在的容器,你可以使用 docker ps -a,它还包括已退出和/或已停止的容器(输出还会显示一个状态为 Exited 的容器):

docker ps -a
CONTAINER ID        IMAGE                                        COMMAND                  CREATED             STATUS                              PORTS                                                NAMES
12cec5da3d89        mcr.microsoft.com/businesscentral/sandbox    "powershell -Comma..."   24 minutes ago      Exited (1073807364) 9 seconds ago                                                        determined_shockley
9518e7e456de        mcr.microsoft.com/dynamicsnav:2017-cu22-de   "powershell -Comma..."   43 minutes ago      Up 43 minutes (healthy)             80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   testcont
95959a311e54        mcr.microsoft.com/dynamicsnav:2017-cu22-de   "powershell -Comma..."   About an hour ago   Up About an hour (healthy)          80/tcp, 443/tcp, 1433/tcp, 7045-7049/tcp, 8080/tcp   RKOS-18111

停止和启动容器和使用 docker stopdocker start 一样简单。如果你想删除一个容器,你需要先停止(docker stop)然后删除它(docker rm),或者使用 docker rm-f 参数强制删除容器,即使它仍在运行。

请注意,删除容器意味着该容器内的所有文件将丢失,无法恢复。

如果命令执行成功,它只会返回你指定的容器名称或 ID:

docker rm -f 12

请注意,你可以通过容器的名称或 ID 来定位容器。你不需要指定完整的 ID,只需要提供 ID 的前几个字符,以便 Docker 唯一地识别容器。同时,注意 docker ps 会为每个容器显示一个截断的 ID,你可以使用 docker ps --no-trunc 来获取完整的 ID。

使用 navcontainerhelper 创建本地可用的环境

为了让用户更容易采用和使用 Dynamics 365 Business Central Docker 镜像,微软创建了一个名为 navcontainerhelper 的 PowerShell 模块。它使用与纯 Docker 命令相同的镜像和命令,且在许多地方,你可以看到 cmdlet 是如何转化为 Docker 命令的。它还包含了一些非常有价值的 PowerShell 脚本集合,帮助完成 Dynamics 365 Business Central 中常见的开发、构建、测试和发布任务。

安装 navcontainerhelper 并保持其更新

要使用 navcontainerhelper 模块,你需要从 PowerShell Gallery 安装它,如下所示:

install-module navcontainerhelper -force

如果有新版本的 navcontainerhelper(在写这篇文档时,至少每隔几周就会有一次更新),并且包含新的有用功能和 bug 修复,只需运行以下命令:

update-module navcontainerhelper -force

你的第一个容器

为了跟随上一节的相同示例,你将学习如何使用 navcontainerhelper 运行你的第一个容器。不过,navcontainerhelper 需要你给它一个名称,并做出关于身份验证的明确选择。如果你没有提供任何其他参数,它会假设你想使用 Windows 身份验证,并要求输入密码,以便它可以在容器内创建一个具有相同用户名和密码的用户。这也启用了 单点登录 (SSO) 功能。考虑以下命令:

New-NavContainer -accept_eula -imageName mcr.microsoft.com/businesscentral/sandbox mycontainer

NavContainerHelper is version 0.6.4.16
NavContainerHelper is running as administrator
Host is Microsoft Windows Server 2019 Datacenter - ltsc2019
Docker Client Version is 19.03.2
Docker Server Version is 19.03.2
Pulling image mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
latest-ltsc2019: Pulling from businesscentral/sandbox
3889bb8d808b: Already exists
e0718b11f512: Pulling fs layer
…
76a160cd3c52: Pull complete
Digest: sha256:3eb2e9d87102c135c1b0c004523abbbe7bff53fc98fe5527a4e85e9ff198d1fd
Status: Downloaded newer image for mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
Using image mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
Creating Container mycontainer
Version: 15.0.36626.37711-w1
Style: sandbox
Platform: 15.0.37582.0
Generic Tag: 0.0.9.95
Container OS Version: 10.0.17763.737 (ltsc2019)
Host OS Version: 10.0.17763.678 (ltsc2019)
Using locale en-US
Using process isolation
Disabling the standard eventlog dump to container log every 2 seconds (use -dumpEventLog to enable)
Files in C:\ProgramData\NavContainerHelper\Extensions\mycontainer\my:
- AdditionalOutput.ps1
- MainLoop.ps1
Creating container mycontainer from image mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
db420a5aec3da0b94b2a466432c160f3a28bd7da5ed83d5ea683ee5e7bd330ff
Waiting for container mycontainer to be ready
Initializing...
Starting Container
Hostname is mycontainer
PublicDnsName is mycontainer
Using Windows Authentication
Starting Local SQL Server
Starting Internet Information Server
Modifying Service Tier Config File with Instance Specific Settings
Starting Service Tier
Registering event sources
Creating DotNetCore Web Server Instance
Enabling Financials User Experience
Creating http download site
Creating Windows user tobias.fenster
Setting SA Password and enabling SA
Creating SUPER user
Container IP Address: 172.23.237.104
Container Hostname  : mycontainer
Container Dns Name  : mycontainer
Web Client          : http://mycontainer/BC/
Dev. Server         : http://mycontainer
Dev. ServerInstance : BC 
Files:
http://mycontainer:8080/al-4.0.192371.vsix

Initialization took 311 seconds
Ready for connections!
Reading CustomSettings.config from mycontainer
Creating Desktop Shortcuts for mycontainer
Container mycontainer successfully created

正如你从最后几行看到的,navcontainerhelper甚至会创建方便的桌面快捷方式。它们允许你通过简单的双击访问Web Client,或在容器内打开 PowerShell 或命令提示符。这些快捷方式实际上使用了你在前几节中看到的相同的docker exec命令。

New-NavContainer有一个很长的参数列表,这些参数要么仅仅是环境参数的映射,要么是非常有用的小功能。举个例子,如果你指定了-updateHosts,那么navcontainerhelper会将容器的名称和 IP 地址添加到你笔记本电脑上的 hosts 文件中。这意味着你不需要使用 IP 地址——你可以使用容器名称作为地址!当然,也有其他方法可以做到这一点,但没有像在启动命令中指定一个简单的参数那么方便。

拉取新镜像版本

拉取新镜像在navcontainerhelper中的工作方式与普通 Docker 完全相同;没有特别的命令。然而,你可以为New-NavContainer命令指定-alwaysPull参数,正如名称所示——每次运行容器之前,都会尝试拉取新的镜像版本。

navcontainerhelper实际上会自动添加更多方便的优化。如果你没有在标签中指定ltsc2016ltsc2019,它会自动确定最佳的容器平台并使用它。它还会自动判断是否可以使用进程隔离,并在必要时将内存限制设置为 4GB。

使用更多环境参数、卷和端口映射

如前所述,New-NavContainer提供了许多仅设置环境参数的选项。在我们之前使用docker run的示例中,执行了以下操作:

docker run -e accept_eula=y -e usessl=n -v "c:\dev\addins:c:\program files\microsoft\Dynamics NAV\150\Service Tier\Add-ins\mine" -p 80:80 –name mycontainer mcr.microsoft.com/businesscentral/sandbox

要在navcontainerhelper中实现相同的功能,我们会使用以下命令:

New-NavContainer -accept_eula -PublishPorts 80 -additionalParameters @('--volume c:\dev\addins:c:\program files\microsoft\Dynamics NAV\150\Service Tier\Add-ins\mine') -containerName mycontainer -imageName mcr.microsoft.com/businesscentral/sandbox

让我们来比较一下这些功能:

  • 接受 EULA(最终用户许可协议)是通过-accept_eula完成的,且不使用 SSL 是navcontainerhelper的默认设置。

  • 本地文件夹与容器中Add-ins文件夹的映射是通过-additionalParameters参数来实现的。这是navcontainerhelper中用于指定任何你可能需要的docker run参数的机制,这些参数尚未被涵盖。

  • 端口映射是通过-PublishPorts参数来实现的。

  • 命名容器是通过-containerName参数来完成的。

再次强调,navcontainerhelper会自动添加更多内容:c:\programdata\navcontainerhelper始终会与容器共享,并作为容器内的同一文件夹。此外,每个容器都有自己在c:\programdata\navcontainerhelper\extensions\<containername>中的文件夹,所有本地与该容器相关的内容都会放在这里。

连接到现有的 SQL Server

navcontainerhelper使得这个任务比直接使用 Docker 稍微方便一些。你需要指定相同的参数,但你可以提供一个凭证对象,而不是将用户名和密码明文写入。提醒一下,下面是docker run命令的样子:

docker run -e accept_eula=y -e databaseServer=sqlserver -e databaseUsername=sqluser -e "databasePassword=1SuperSecretPwd!" -e databasename=FinancialsW1 mcr.microsoft.com/businesscentral/sandbox

以下是使用navcontainerhelper的相同命令。它将打开一个凭证输入对话框,在那里你可以输入 SQL 用户名和密码。但与直接将密码明文写入环境变量不同,navcontainerhelper确保密码得到安全处理,如下所示:

New-NavContainer -accept_eula -databaseServer sqlserver -databaseCredential (Get-Credential) -databaseName FinancialsW1 -imageName mcr.microsoft.com/businesscentral/sandbox:latest -containerName mycontainer -auth NavUserPassword
cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:
Credential

NavContainerHelper is version 0.6.4.16
NavContainerHelper is running as administrator
Host is Microsoft Windows Server 2019 Datacenter - ltsc2019
Docker Client Version is 19.03.2
Docker Server Version is 19.03.2
Pulling image mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
latest-ltsc2019: Pulling from businesscentral/sandbox
3889bb8d808b: Already exists
e0718b11f512: Pulling fs layer
…
76a160cd3c52: Pull complete
Digest: sha256:3eb2e9d87102c135c1b0c004523abbbe7bff53fc98fe5527a4e85e9ff198d1fd
Status: Downloaded newer image for mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
Using image mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
Creating Container mycontainer
Version: 15.0.36626.37711-w1
Style: sandbox
Platform: 15.0.37582.0
Generic Tag: 0.0.9.95
Container OS Version: 10.0.17763.737 (ltsc2019)
Host OS Version: 10.0.17763.678 (ltsc2019)
Using locale en-US
Using process isolation
Disabling the standard eventlog dump to container log every 2 seconds (use -dumpEventLog to enable)
Files in C:\ProgramData\NavContainerHelper\Extensions\mycontainer\my:
- AdditionalOutput.ps1
- MainLoop.ps1
Creating container mycontainer from image mcr.microsoft.com/businesscentral/sandbox:latest-ltsc2019
db420a5aec3da0b94b2a466432c160f3a28bd7da5ed83d5ea683ee5e7bd330ff
Waiting for container mycontainer to be ready
Initializing...
Starting Container
Hostname is mycontainer
PublicDnsName is mycontainer
Using NavUserPassword Authentication
Starting Internet Information Server
Import Encryption Key
Creating Self Signed Certificate
Self Signed Certificate Thumbprint 583BDDBAB357DB4B4AB722284629195E27328B7E
Modifying Service Tier Config File with Instance Specific Settings
Starting Service Tier
Creating DotNetCore Web Server Instance
Enabling Financials User Experience
Creating http download site
Creating Windows user admin
Setting SA Password and enabling SA
Creating SUPER user
Container IP Address: 172.23.237.104
Container Hostname : mycontainer
Container Dns Name : mycontainer
Web Client : http://mycontainer/BC/
Dev. Server : http://mycontainer
Dev. ServerInstance : BC

Files:
http://mycontainer:8080/al-4.0.192371.vsix

Initialization took 99 seconds
Ready for connections!
Reading CustomSettings.config from mycontainer
Creating Desktop Shortcuts for mycontainer
Container mycontainer successfully created

请注意,使用docker run时默认的身份验证机制是NavUserPassword,而navcontainerhelper的默认身份验证机制是 Windows,因此为了实现相同的效果,我们也需要指定这一点。

使用 NavContainerHelper 管理你的运行中的容器

navcontainerhelper中没有专门的命令让你查看正在运行的容器,因此你只需要使用前面章节中介绍的相同docker ps命令。也有启动和停止容器的命令(分别是Start-NavContainerStop-NavContainer),但它们只是docker startdocker stop的简单封装,没有额外的好处。

移除容器的操作是通过Remove-NavContainer完成的,它做的事情稍微多一些:它会清理快捷方式和特定于容器的文件夹,如果你指定了-updatehosts,它还会移除 hosts 文件中的相关条目。

还有一个命令可以让你进入容器会话,叫做Enter-NavContainer。这会给你一个 PowerShell 会话,进入容器内部,另外还可以调用c:\run\prompt.ps1,这会立刻提供所有开发和管理的 cmdlet,并且给你一个格式良好的提示,让你始终知道自己在哪里。考虑以下命令:

Enter-NavContainer mycontainer[MYCONTAINER]: PS C:\Run> Get-NAVServerInstance
ServerInstance : MicrosoftDynamicsNavServer$BC
DisplayName    : Dynamics 365 Business Central Server [BC]
State          : Running
ServiceAccount : NT AUTHORITY\SYSTEM
Version        : 15.0.37582.0
Default : True

本地环境的集中式可用性

到目前为止,我们只涵盖了本地运行的容器,但根据你的情况,你可能希望设置一个或多个中央虚拟机,这些虚拟机由运维团队而非开发人员或顾问来管理。在这种情况下,之前解释过的相同选项仍然适用,但你需要考虑一些额外的主题。你还需要决定是否希望提供一个完全自助服务的环境,允许开发人员或顾问自行配置新的沙盒,或者希望运维团队来处理创建和删除沙盒的工作。

如果你的运维团队负责这些操作,那么你唯一需要关注的就是网络配置。你有以下两种选择,可以使你的容器端口在 Docker 主机外部可用:

  • 端口映射让你可以将容器端口映射到主机端口。然而,随着时间的推移,这会变得繁琐,因为你需要为每个容器找到可用的端口,并告诉你的用户应该使用哪些端口。

  • 透明网络使容器能够获得自己的 IP 地址(静态或 DHCP)并保持标准端口。此方法的维护工作量要少得多。我们现在将更详细地讨论这个问题。

所以,创建透明网络非常简单。在最简单的情况下,你只需要编写以下命令来创建一个名为 transpNet 的透明网络:

docker network create -d transparent transpNet

你也可以设置子网或 IP 范围,但这超出了本书的范围。请查看 Docker 的在线文档或运行 docker network create --help 以了解更多信息。告诉容器使用透明网络也很容易,因为有一个名为 --network 的参数。在运行纯 Docker 时,使用如下命令:

docker run -e accept_eula=y --network transpNet mcr.microsoft.com/businesscentral/sandbox

navcontainerhelper 在写作时 New-NavContainer 没有特定的网络参数,但你可以使用 -additionalParametersparameter 来代替:

New-NavContainer -accept_eula -additionalParameters @('--network transpNet') -containerName mycontainer -imageName mcr.microsoft.com/businesscentral/sandbox

这样,容器将获得自己的 IP 地址,并且你可能也能通过名称从 Docker 主机外部访问它。请注意,这很大程度上取决于主机所处网络的设置。它类似于将一个新虚拟机添加到网络中,依赖于网络管理员设置的安全机制,这可能在没有进一步设置的情况下无法正常工作。

如果你想要为开发人员或顾问提供一个可以创建自己环境的环境,你需要解决两个问题:

  • 你希望如何处理容器操作(创建、启动、停止和删除操作)? 你可以允许用户通过 RDP 或 PowerShell 访问主机,但这会使权限管理变得复杂。你还可以使用如 Portainer(portainer.io)这样的工具,这是一个处理容器的 GUI。在这里,用户可以管理他们的容器,并且你可以例如创建具有预定义值的容器参数模板。然而,请注意,在这种情况下,你将无法使用 navcontainerhelper,因为这意味着需要在主机上运行 PowerShell 脚本。另一种选择是你自己创建某种前端应用程序,但这当然需要一些时间。

  • 用户如何访问容器中的文件系统? 这取决于你对 Dynamics 365 Business Central 的使用。通常需要访问文件系统。如果你已经运行了全云解决方案,这无论如何都不可行,但如果你仍有本地客户,这可能是一个问题。最简单的解决方案是始终将一个卷映射到你的容器中,例如 c:\shared,并允许用户访问主机上的文件夹。

托管在 Azure 虚拟机上的容器

如果你想在 Azure 虚拟机中运行沙盒,你面临的挑战几乎与在本地虚拟机中使用集中式沙盒相同。不过,你可以走捷径:微软提供了预安装了 Docker 的标准虚拟机镜像,比如带有容器的 Windows Server 2019 Datacenter,这样你就不必担心这一点了。

如果你想使用 navcontainerhelper,你需要先安装它,然后你就可以开始使用了。一个更快捷的方法是使用微软提供的快速启动模板之一,例如aka.ms/getbc。这将创建一个 Azure 虚拟机,安装 Docker 和 navcontainerhelper,拉取最新的 Dynamics 365 Business Central 镜像(默认为本地版镜像),并为你启动它。除此之外,你还会得到一个漂亮的日志,显示所有正在进行的“魔法”进展。之后,你可以使用该虚拟机根据需要创建更多沙盒。

然而,在 Azure 虚拟机中有一个问题,使得处理它们变得更加复杂:你无法使用透明网络。这是有多个原因的,而且很可能在近期不会改变,所以你必须考虑其他解决方案。一种解决方案是端口映射,正如我们之前提到的,但这也需要相当多的维护。最简单的方法是使用反向代理,例如 nginx 或 Traefik,但这也超出了本书的范围。

你可以在www.axians-infoma.de/techblog/running-multiple-nav-bc-containers-on-an-azure-vm/找到快速入门介绍。

选择正确的镜像

现在你已经知道如何创建和运行沙盒,唯一的问题是你想使用哪个版本;做出这个决定可能相当复杂。关于如何做出有效决定的基本知识将在本节中总结。

官方的公开发布版本始终可以在注册表(mcr.microsoft.com)中找到,以下是相关的仓库和镜像名称:

  • mcr.microsoft.com/businesscentral/sandbox:Dynamics 365 Business Central SaaS 版本的沙盒镜像

  • mcr.microsoft.com/businesscentral/onprem:Dynamics 365 Business Central 本地版

  • mcr.microsoft.com/dynamicsnav:旧版 Dynamics NAV 产品,从 Dynamics NAV 2016 开始

未发布版本的预览版可以在 bcinsider.azurecr.io 注册表中找到,以下是相关的仓库和镜像名称:

  • bcinsider.azurecr.io/bcsandbox:Dynamics 365 Business Central SaaS 版本下一个次要版本的预览版

  • bcinsider.azurecr.io/bconprem:Dynamics 365 Business Central 本地版下一个次要版本的预览版

  • bcinsider.azurecr.io/bcsandbox-master:Dynamics 365 Business Central SaaS 版本下一个重大版本的预览版

  • bcinsider.azurecr.io/bconprem-master:下一次主要发布的 Dynamics 365 Business Central 本地版预览

请注意,再次提醒,你需要bcinsider.azurecr.io的登录凭据,可以通过在微软的协作平台上注册“Ready to Go!”程序后获取。

当你决定使用哪张镜像时,还需要决定使用哪个标签,代表微软的某个特定版本。所有镜像都允许你指定语言(gbdedk等)和基础操作系统(Windows Server 2016 的ltsc2016和 Windows Server 2019 的ltsc2019)。发布的本地版本也允许你使用与传统安装相同的命名约定,通过参考它们的累积更新名称来使用。

考虑以下示例,以便了解语法:

  • Dynamics 365 Business Central 2018 秋季版(1810),CU 11,德语版,基于 Windows Server 2016:
mcr.microsoft.com/businesscentral/onprem:1810-cu11-de-ltsc2016
  • Dynamics 365 Business Central 2019 春季版(1904),CU 5,丹麦版,基于 Windows Server 2019:
mcr.microsoft.com/businesscentral/onprem:1904-cu5-dk-ltsc2019
  • Dynamics 365 Business Central 2019 秋季版(1910),CU 1,澳大利亚版,基于 Windows Server 2019:
mcr.microsoft.com/businesscentral/onprem:1910-cu1-au-ltsc2019
  • Dynamics NAV 2017,CU 28,英国版,基于 Windows Server 2019:
mcr.microsoft.com/businesscentral/dynamicsnav:2017-cu28-gb-ltsc2019

已发布的 SaaS 版本允许你指定更新版本,而不是累积更新:

  • Dynamics 365 Business Central SaaS 更新 25,西班牙版,基于 Windows Server 2016:
mcr.microsoft.com/businesscentral/sandbox:update25-es-ltsc2016

预览版只允许你指定语言和 Windows Server 版本:

  • 下一次小版本的 SaaS 版 Dynamics 365 Business Central 的最新预览,德语版,基于 Windows Server 2019:bcinsider.azurecr.io/bcsandbox:de-ltsc2019

  • 下一次主要发布的 Dynamics 365 Business Central 本地版预览,丹麦版,基于 Windows Server 2016:bcinsider.azurecr.io/bconprem-master:dk-ltsc2016

如果你没有指定所有内容,会有默认值,因此mcr.microsoft.com/businesscentral/onprem将给你最新的累积更新版本,基于 Windows Server 2016 的最新 Dynamics 365 Business Central 本地版本 W1 版。为了避免意外情况,通常最好尽可能详细地指定标签。

修改标准镜像中的脚本

如你所见,Dynamics 365 Business Central 的容器镜像有很多配置选项,允许你改变许多行为。然而,如果你遇到需要运行不同容器配置的情况,镜像还有一个王牌:你可以覆盖容器中的任何脚本。

这个机制通过将一个与想要覆盖的脚本同名的脚本放入容器中的 c:\run\my 文件夹来工作。最简单的方法是通过卷来实现。假设你有一个文件夹,比如 c:\bc-override,其中有一个 AdditionalSetup.ps1 文件,你可以这样做:

docker run -e accept_eula=y -v c:\bc-override:c:\run\my mcr.microsoft.com/businesscentral/onprem

当容器执行到调用 AdditionalSetup.ps1 的位置时,它会检查在 c:\run\my 中是否存在该文件,如果存在,则调用它。如果不存在,它会调用标准脚本,该脚本存储在 c:\run 中。这个脚本可能如下所示:

Write-Host "----- Hello from the override script --------------------"

如果是这样,当你启动容器时,你将看到以下输出:

Initializing...
Starting Container
Hostname is 4e58d9587fb0
PublicDnsName is 4e58d9587fb0
Using NavUserPassword Authentication
Starting Local SQL Server
Starting Internet Information Server
Creating Self Signed Certificate
Self Signed Certificate Thumbprint 1462D57EE355D19018232160C396159313A20893
Modifying Service Tier Config File with Instance Specific Settings
Starting Service Tier
Creating DotNetCore Web Server Instance
Enabling Financials User Experience
Creating http download site
Creating Windows user admin
Setting SA Password and enabling SA
Creating admin as SQL User and add to sysadmin
Creating SUPER user
----- Hello from the override script --------------------
Container IP Address: 172.26.148.48
Container Hostname  : 4e58d9587fb0
Container Dns Name  : 4e58d9587fb0
Web Client          : https://4e58d9587fb0/BC/
Admin Username      : admin
Admin Password      : Rohy1060
Dev. Server         : https://4e58d9587fb0
Dev. ServerInstance : BC
Files:
http://4e58d9587fb0:8080/al-4.0.192371.vsix
http://4e58d9587fb0:8080/certificate.cer
Initialization took 117 seconds
Ready for connections!

使用 navcontainerhelper 工具,你可以像这样调用它:

New-NavContainer -accept_eula -imageName mcr.microsoft.com/businesscentral/sandbox -myScripts @("c:\bc-override\AdditionalSetup.ps1") mycontainer

或者,由于 navcontainerhelper 提供了一种更方便的方法,你可以直接将你的 PowerShell 脚本代码内联添加:

New-NavContainer -accept_eula -imageName mcr.microsoft.com/businesscentral/sandbox -myscripts @( @{ "AdditionalSetup.ps1" = "Write-Host ‘----- Hello from the override script --------------------'" } ) mycontainer

借助这个强大的功能,你可以更改容器中的每个脚本以适应你的需求。为了了解可以调整的内容,请查看 github.com/Microsoft/nav-docker/tree/master/generic/Run

创建你自己的镜像

通过我们刚才看到的机制,我们可以更改容器中的所有内容。但是,如果你想确保你的同事、合作伙伴或客户获得完全相同的修改,而不必得到 my-scripts 覆盖正确?或者,如果你需要将某些 DLL 或其他文件放入容器中,并希望将它们作为你自己的镜像的一部分交付呢?答案是构建你自己的镜像,幸运的是,这也非常简单。

Docker 有一个分层概念,使得镜像变成了一个层的堆叠。你需要做的就是将自己的层放在标准镜像之上。你可以通过使用 Dockerfile 来实现这一点,Dockerfile 需要引用你想要扩展的标准镜像,然后再执行你想要的操作。假设你想将存储在 c:\bc\dlls 中的某些 DLL 文件放入镜像的 Add-ins 文件夹,并将你自己位于 c:\bc\override 文件夹中的 AdditionalSetup.ps1 脚本放入镜像的 c:\run 文件夹中。你的 Dockerfile 将如下所示,存储在 c:\bc 文件夹中:

FROM mcr.microsoft.com/businesscentral/sandbox
COPY ["./dlls/*", "c:/Program Files/Microsoft Dynamics NAV/150/Service/Add-ins/"]
COPY ./override/AdditionalSetup.ps1 c:/run/

要构建这个镜像,你需要在 c:\bc 中运行 docker build 命令,并使用 -t 参数为你的镜像命名:

docker build -t myimage

结果将类似于以下内容:

Sending build context to Docker daemon 7.168kB
Step 1/3 : FROM mcr.microsoft.com/businesscentral/sandbox
 ---> 20f72db6c9a9
Step 2/3 : COPY ./dlls/* c:/Program Files/Microsoft Dynamics NAV/150/Service/Add-ins/
---> f202642914a9
Step 3/3 : COPY ./override/AdditionalSetup.ps1 c:/run/
 ---> 1164c1273517
Removing intermediate container e9070e72cbaa
Successfully built 1164c1273517
Successfully tagged myimage:latest

配置完成后,你可以像使用其他镜像一样使用你的镜像:

docker run -e accept_eula=y myimage

与他人共享此镜像的方式是通过 Docker 仓库。对于基于官方 Microsoft 镜像构建的镜像,这将通过私有仓库来完成。如何设置私有仓库超出了本书的范围,但如果你想了解更多,可以参考 docs.docker.com/registry/ 作为一个好的起点。

总结

在本章中,我们了解了在线和基于 Docker 的沙盒的基础知识,以及如何最优化地使用它们。我们强调了掌握 Docker 的基础知识及如何从在线仓库中的 Dynamics 365 Business Central 镜像创建基本容器的必要性。

借助本章中获得的技能,你应该能够创建自己的容器,选择合适的镜像,并在处理自定义沙盒环境时熟悉navcontainerhelper工具。

你还应该能够掌握 Dockerfile 的格式,并通过覆盖脚本和创建自己的 Dynamics 365 Business Central 沙盒基线,对标准镜像进行深入修改。

在下一章中,我们将通过学习 AL 语言的基础知识,开始我们的开发之旅。

第二部分:为 Dynamics 365 Business Central 开发扩展

在本节中,我们将为您提供一个关于如何使用新的扩展模型为 Dynamics 365 Business Central 开发定制化解决方案的完整概述。

本节包括以下章节:

  • 第四章,扩展开发基础

  • 第五章,为 Dynamics 365 Business Central 开发定制化解决方案

  • 第六章,高级 AL 开发

  • 第七章,使用 AL 开发报表

第四章:扩展开发基础

在上一章中,我们概述了新的现代开发环境,并且学习了如何使用 AL 语言扩展和现代开发环境启动一个新的 Dynamics 365 Business Central 扩展项目。

在本章中,我们将详细探讨新扩展开发模型中的对象,并讲解如何使用 AL 创建新对象、扩展标准对象,以及如何处理 AL 扩展项目。更具体地说,我们将涵盖以下主题:

  • 扩展开发基础

  • AL 对象的概述

  • 如何在扩展项目中创建基本对象

  • 处理 AL 项目的最佳实践

  • AL 对象指南

在本章结束时,你将了解不同的 AL 对象类型,如何创建和使用它们,并且(更广泛地说)你将准备好开始一个 Dynamics 365 Business Central 扩展项目,使用 AL 语言扩展和现代开发环境(Visual Studio Code)。

技术要求

为了跟随本章内容并实验 AL 语言中的基本对象创建,你将需要以下内容:

  • Microsoft Dynamics 365 Business Central 沙箱环境(本地安装在 Docker 容器中或在线环境)

  • Visual Studio Code

  • AL 语言扩展,可以从 Visual Studio Code 市场安装

关于扩展的基本概念

正如你已经知道的,在 Microsoft Dynamics 365 Business Central SaaS 中,你无法访问数据库或标准的基础代码(这在本地版本中有所不同,在本地版本中你仍然可以访问基础代码,并且修改该核心是你的责任)。在 SaaS 世界中,你无法更改数据库架构,也不能修改标准的业务逻辑。

在以前版本的 Microsoft Dynamics ERP 中,我们一直在讨论代码修改。在 SaaS 世界中,我们必须开始思考一个新概念:代码扩展。要自定义 Dynamics 365 Business Central,你必须创建扩展

扩展(根据微软的指南)被定义为一种可安装的功能,其构建方式不会直接改变源资源,并且以预配置包的形式分发

扩展通过使用事件与标准基础代码进行交互。以下图示展示了在 Dynamics 365 Business Central 扩展中的不同层次之间如何进行事件交互:

事件本质上是一个由代码触发的函数,当业务流程中发生某些事情时该函数会被调用。这个函数通常定义为事件发布者函数。它仅包含一个签名,并不会执行任何代码。包含事件发布者函数的对象被定义为发布者

在 Dynamics 365 Business Central 中,事件根据以下类型进行分类:

  • 数据库事件:这些是在对表对象进行数据库操作(如插入、修改、删除和重命名)时由系统自动触发的事件。

  • 页面事件:这些是在页面对象中执行操作时由系统自动触发的事件。

  • 业务事件:这些是由 C/AL 代码触发的自定义事件。业务事件定义了一个正式契约,并隐含承诺在未来的应用程序版本中不进行更改。

  • 集成事件:这些是由 C/AL 代码触发的自定义事件。它们与业务事件类似,但在应用程序的未来版本中,它们的签名可能会发生变化。

  • 全局事件:这些是由应用程序触发的系统事件。

当事件被发布并由代码触发时,它将可供应用程序订阅。订阅者是一个代码功能,它监听并处理已发布的事件。它订阅一个特定的事件发布器功能,并通过向其中添加自定义业务逻辑来处理该事件。当应用程序触发事件时,订阅者功能会自动被调用,并执行其代码。

请记住,你可以有多个订阅者订阅同一个事件发布器功能。在这种情况下,订阅者执行的顺序是无法确定的(是随机的),因此在设计代码时要小心事件链的顺序。

事件确保你可以与标准业务流程交互或修改其行为,而无需更改基础代码。

Dynamics 365 Business Central 在其标准代码中暴露了大量事件,并且每月都会添加新的事件。你可以通过以下链接请求新的事件:github.com/Microsoft/ALAppExtensions/issues。要获得 Dynamics 365 Business Central 中已发布事件的完整概览,建议你查看以下 GitHub 仓库:github.com/waldo1001/blog.CALAnalysis/tree/master/Published%20Events

在本节中,我们学习了事件是每个 AL 扩展的基本构建块。在下一节中,我们将概述可用的 AL 对象,并学习如何使用 AL 语言扩展创建它们。

理解 AL 语言的基础

Dynamics 365 Business Central 的扩展是使用AL 语言编写的。使用 AL 语言,你可以创建新对象、扩展标准对象,并为你的应用程序创建自定义业务逻辑。

你通过使用 Visual Studio Code 作为开发环境,并使用 AL 语言扩展(如我们在第二章中描述的,掌握现代开发环境)来为 Dynamics 365 Business Central 创建扩展。安装后,你将获得完整的 AL 项目开发支持。

所有 Dynamics 365 Business Central 功能都作为对象进行编码(新对象或标准对象的扩展),这些对象在.al文件中定义。一个.al文件可以定义多个对象(尽管我们不推荐这样做)。

扩展程序随后被编译为.app包文件,这个文件就是你在最终环境中发布的扩展。

截至目前,AL 语言扩展对于 Visual Studio Code 提供了以下对象:

  • 表对象

  • 表扩展对象

  • 页面对象

  • 页面扩展对象

  • 代码单元对象

  • 报表对象

  • 枚举对象

  • XMLport 对象

  • 查询对象

  • 控件附加组件(JavaScript)

  • 配置文件和页面自定义

我们将在接下来的章节中详细查看主要对象。其中一些对象(如报告、页面自定义和附加组件)将在后续章节中介绍。

AL 语言扩展包含了许多用于定义对象和处理语言任务的代码片段。主要的标准代码片段如下:

  • 对象

    • tpagecust: 标准页面的新自定义

    • tpageext: 标准页面的新扩展

    • ttableext: 标准表的新扩展

    • tquery: 新查询

    • treport: 新报告

    • txmlport: 新的 xmlport

    • tpage: 在这里,我们可以选择是获取一个新的列表还是一个新的卡片

    • tcodeunit: 新代码单元

  • 代码

    • tcaseelse: 带有 else 的 Case 语句

    • tcaseof: 没有 else 的 Case 语句

    • tfor: For 语句

    • tforeach: Foreach 语句

    • tif: 带有 begin 和 end 的 If 语句

    • tifelse: 带有 begin 和 end else 的 If 语句

    • tisempty: 带有 begin 和 end 的 Isempty 语句

    • tisemptyelse: 带有 begin end else 的 Isempty 语句

    • trepeat: 带有 begin 和 end 子句的重复循环

    • twhile: While 语句

    • twith: With 语句

  • 配置文件

    • tprofile: 允许我们创建一个带有页面自定义的新的配置文件
  • 事件

    • teventbus: 允许我们创建一个业务事件

    • teventint: 允许我们创建一个集成事件

    • teventsub: 允许我们创建一个订阅者事件

  • 字段和键

    • tfield: 新字段,无类型(我们需要手动填写一个类型)。

    • tfieldbiginteger: 大整数类型。

    • tfieldboolean: 布尔字段。

    • tfieldblob: Blob 字段。

    • tfieldcode: 代码字段。你只需要设置字段的长度。

    • tfielddate: 日期字段。

    • tfielddateformula: 日期公式字段。

    • tfielddatetime: 日期时间字段。

    • tfielddecimal: 十进制字段。

    • tfieldduration: 持续时间字段。

    • tfieldguid: GUID 字段。

    • tfieldoption: 选项字段。在这种情况下,OptionMember属性会自动添加。

    • tfieldrecorid: RecordID 字段。

    • tfieldtext:文本字段。你只需要输入字段的长度。

    • tfieldtime:时间字段。

    • tkey:向表中添加一个新键。

  • 页面上的字段和操作

    • tfieldpage:向页面添加一个字段。

    • taction:向页面添加一个操作。

  • 触发器

    • ttrigger:创建触发器定义。

    • tprocedure:创建过程定义。

在 Visual Studio Code 中安装 AL 语言扩展后,你可以通过转到“视图” | “命令面板”并选择 AL:Go!来启动一个新的 AL 项目。

Visual Studio Code 会要求你提供一个文件夹,以便它可以在其中创建项目,然后要求你选择目标平台(Dynamics 365 Business Central 版本)。选择 4.0 Business Central 2019 版本第二波发布:

现在,Visual Studio Code 将为你配置项目。它会创建launch.json文件,以便你可以连接到开发环境,以及带有扩展清单文件的app.json文件(如第二章,掌握现代开发环境所述)。

现在,你可以开始定义组成你解决方案的对象。

表定义

使用 AL 扩展,你没有图形化工具来设计表(就像我们以前在 CSIDE 中做的那样);相反,你需要使用代码来创建表。

可以通过使用ttable代码段创建一个表定义:

table id MyTable
{    
    DataClassification = ToBeClassified;

    fields
    {
        field(1;MyField; Integer)
        {
            DataClassification = ToBeClassified;          
        }
    }   

    keys
    {
        key(PK; MyField)
        {
            Clustered = true;
        }
    }

    var
        myInt: Integer;

    trigger OnInsert()
    begin

    end;

    trigger OnModify()
    begin

    end;

    trigger OnDelete()
    begin

    end;

    trigger OnRename()
    begin

    end;

}

要定义一个表,你需要指定一个ID(必须在应用程序中唯一)和一个名称(也必须是唯一的)。然后,你可以设置表的属性(使用Ctrl + 空格键来发现所有可用属性):

一个表对象具有以下主要属性:

  • Caption:标识表在用户界面中显示的字符串。

  • DataCaptionFields:设置在显示此表内容的页面中,位于标题左侧的字段。

  • DataPerCompany:设置一个值,指示表数据是适用于数据库中的所有公司,还是仅适用于当前公司(当默认值为true时,数据仅适用于当前公司)。

  • DrillDownPageID:设置用作下钻的页面 ID。

  • LookupPageID:设置用作查找的页面 ID。

  • LinkedObject:仅适用于本地部署;它指定一个指向 SQL Server 对象的链接。

  • Permissions:设置对象是否具有执行某些操作所需的额外权限,这些操作适用于一个或多个表。

  • TableType:指定表的类型(普通、CRM、ExternalSQL、Exchange 或 MicrosoftGraph)。

  • ExternalName:当你在TableType属性中指定 CRM 或 ExternalSQL 时,此属性出现,并指定外部数据库中原始表的名称。

  • ExternalSchema:当你在TableType属性中指定 CRM 或 ExternalSQL 时,此属性出现,并指定外部数据库中数据库架构的名称。

  • ReplicateData: 指定表是否必须复制到云服务(默认值为 true)。

  • Extensible: 设置对象是否可以扩展。

表对象包含一组字段。表的字段可以通过使用 tfield 代码段创建:

field(id; MyField; Blob)
{
    DataClassification = ToBeClassified;
    FieldPropertyName = FieldPropertyValue;
}

字段由 ID(必须在声明表及其所有扩展中唯一)、名称(也必须在声明表及其所有扩展中唯一)和 类型(字段的数据类型)定义。

建议始终设置 Caption 属性(对于表和字段),并将 DataClassification 属性(用于定义 GDPR 法规的数据敏感性)设置为除 ToBeClassified 以外的值。字段可以具有其特定的属性,您可以根据需要进行设置(如以下屏幕截图所示的可选属性):

表还包含一组 。您可以使用 tkey 代码段定义键:

key(MyKey; MyField)
{

}

表的键由 名称 和组成键的 字段(以逗号分隔的表字段列表)定义。如果表的主键是该键,则键可以将 Clustered 属性设置为 true。聚集索引是一种特殊类型的索引,它重新排序表中记录的物理存储方式,因此一个表只能有一个聚集索引。

表还可以具有触发器(OnInsertOnModifyOnDeleteOnRename),在表内部,您可以定义自己的方法。

页面对象定义

页面对象是您在 Dynamics 365 Business Central 中为用户提供的用户界面。您可以使用 tpage 代码段在 AL 中定义页面对象:

前三个选项允许您创建以下页面类型:

  • Card 页面

  • API 页面

  • List 页面

Card 页面(第一个选项)定义如下:

page Id MyPage
{
    PageType = Card;
    ApplicationArea = All;
    UsageCategory = Administration;
    SourceTable = TableName;

    layout
    {
        area(Content)
        {
            group(GroupName)
            {
                field(Name; NameSource)
                {
                    ApplicationArea = All;                   
                }
            }
        }
    }

    actions
    {
        area(Processing)
        {
            action(ActionName)
            {
                ApplicationArea = All;           
                trigger OnAction()
                begin

                end;
            }
        }
    }

    var
        myInt: Integer;
}

Card 页面通过其 ID名称 来标识(这两个值在应用程序内部必须唯一)。页面也有自己的属性。需要定义的主要内容如下:

  • PageType: 标识页面的类型。

  • SourceTable: 设置此页面的基础表。

  • SourceTableView: 设置您希望使用的键、排序顺序和过滤器,以确定呈现给用户的源表的视图。

  • ApplicationArea: 设置页面在 Business Central 应用程序中的可见性。标准值包括 All、Basic、Suite 和 Advanced。

  • UsageCategory: 设置网页客户端中搜索页面的部门列。

  • Extensible: 设置对象是否可以扩展。

页面有一个 layout(定义页面在 UI 中的外观)和一个 actions 部分(定义可用的菜单项,以便在页面内添加代码操作)。在布局内,您有一个内容区域,其中包含一组组,且每个组可以包含一个或多个页面字段。您可以使用 tpagefield 代码段在页面组中添加字段:

field(MyField; FieldSource)
{
    ApplicationArea = All
    FieldPropertyName = FieldPropertyValue;
}

页面上的字段通过名称(页面中的字段关键字)和字段源(页面字段的源表达式,对应底层表中定义的物理字段)来定义。

字段可以具有自己的属性,并且必须设置ApplicationArea

List页面(第三种选项)定义如下:

page Id PageName
{
    PageType = List;
    ApplicationArea = All;
    SourceTable = TableName;

    layout
    {
        area(Content)
        {
            repeater(Group)
            {
                field(Name; NameSource)
                {
                    ApplicationArea = All;

                }
            }
        }

        area(Factboxes)
        {

        }
    }

    actions
    {
        area(Processing)
        {
            action(ActionName)
            {
                ApplicationArea = All;

                trigger OnAction();
                begin

                end;
            }
        }
    }
}

List页面的PageType属性设置为List,并且layout部分有Content区域和FactBox区域。Content区域包含一个repeater组,显示你希望在该列表上展示的所有字段。之后,你可以设置actions部分。

如果页面包含repeater控件(例如,List页面),你可以定义适用于整个页面或单个记录的操作。为此,操作有一个名为Scope的属性,可以定义为页面(操作位于页面级别)或repeater(操作位于记录级别)。

表扩展定义

正如我们之前提到的,在 Dynamics 365 Business Central 中,你不能修改现有表;相反,你需要创建一个表扩展。

可以使用ttableext片段定义表扩展:

tableextension Id MyExtension extends MyTargetTable
{
    fields
    {
        // Add changes to table fields here
    }

    var
        myInt: Integer;
}

tableextension对象通过ID名称(必须是唯一的)以及必须扩展(或修改)的表来定义。然后,在字段组内部,你可以添加新字段或更改现有字段的属性。

以下代码是对标准Customer表的扩展示例,添加了一些新字段并更改了现有字段的属性:

tableextension 50100 CustomerExtSD extends Customer
{
    fields
    {
        field(50100; PacktEnabledSD; Boolean)
        {
            DataClassification = CustomerContent;
            Caption = 'Packt Subscription Enabled';
        }
        field(50101; PacktCodeSD; Code[20])
        {
            DataClassification = CustomerContent;
            Caption = 'Packt Subscription Code';
        }

        modify("Net Change")
        {
            BlankZero = true;
        }
    }   
}

tableextension对象中,你还可以通过添加keys组来向扩展的表添加新键,就像在表定义中一样。例如,在我们之前的tableextension对象中,我们添加了两个新字段,并且我们还希望在这些字段上创建一个二级键。我们可以创建一个key组,定义键名和键字段:

tableextension 50100 CustomerExtSD extends Customer
{
    fields
    {
        field(50100; PacktEnabledSD; Boolean)
        {
            DataClassification = CustomerContent;
            Caption = 'Packt Subscription Enabled';
        }
        field(50101; PacktCodeSD; Code[20])
        {
            DataClassification = CustomerContent;
            Caption = 'Packt Subscription Code';
        }
        modify("Net Change")
        {
            BlankZero = true;
        }
    }

    keys
    {
       key(PacktKey; PacktCodeSD,PacktEnabledSD)
       {

       }
    }
}

你不能基于新字段或标准字段创建键,也不能在扩展的表中修改现有键。

在这里,我们在Customer表中定义了一个名为PacktKey的二级键,它由两个自定义字段(PacktCodeSDPacktEnabledSD)组成。定义二级键对于提高某些计算、排序记录和报告的性能非常有用。

页面扩展定义

与表一样,在 Dynamics 365 Business Central 中,你不能直接修改现有页面;相反,你需要创建一个页面扩展(使用 AL 中的pageextension对象)。

可以使用tpageext片段定义pageextension对象:

pageextension Id MyExtension extends MyTargetPage
{
    layout
    {
        // Add changes to page layout here
    }

    actions
    {
        // Add changes to page actions here
    }

    var
        myInt: Integer;
}

一个pageextension对象由ID名称(必须唯一)以及必须扩展的页面定义。一个pageextension对象包含一个layout块(你可以在其中添加对标准页面布局的更改,如添加新字段或新部分,或更改标准字段)和一个actions块(你可以在其中添加新的操作)。

以下是一个pageextension对象的示例,其中我们向Customer Card页面添加了一个新字段(该字段被添加到General选项卡的末尾),并修改了现有字段(Name字段)的Style属性:

pageextension 50100 CustomerCardExtSD extends "Customer Card"
{
    layout
    {
        addlast(General)
        {
            field(PacktEnabledSD; PacktEnabledSD)
            {
                ApplicationArea = All;               
            }
        }

        modify(Name)
        {
            Style = Strong;
        }
    }       
}

如你所见,我们已向页面添加了一个字段,并修改了Name字段的Style属性,使其显示为粗体。请记住,并非所有可用的字段属性都可以通过pageextension对象进行修改。

代码单元定义

代码单元是 AL 代码的容器,这些代码可以通过直接执行代码单元(使用OnRun触发器)或通过调用代码单元中定义的函数来触发。

我们可以通过使用tcodeunit片段在 AL 中定义代码单元:

codeunit Id MyCodeunit
{
    trigger OnRun()
    begin

    end;

    var
        myInt: Integer;
}

代码单元由ID名称(在你的应用程序中必须唯一)定义。默认情况下,代码单元框架只包含OnRun触发器的定义,在此触发器中,你可以编写希望在调用Codeunit.RUN方法时执行的代码。

代码单元有其自己的属性,你可以进行设置:

在代码单元中,你可以定义可以是本地或全局(即公开暴露给实例化该代码单元的对象)的过程(函数)。

你可以通过使用tprocedure片段来定义一个过程:

local procedure MyProcedure()
   var
        myInt: Integer;
    begin

    end;

默认情况下,这段代码创建了一个没有参数且没有返回值的本地过程。通过移除local关键字,你可以将作用域从本地(默认值,意味着它仅在声明该过程的对象内部可见)更改为全局(使其在对象外部也可见)。

例如,这是一个带有参数和返回值的全局过程:

procedure CheckIfPacktCustomerIsEnabled(CustomerNo: Code[20]): Boolean
    var
     //Local variables here 
    begin
      //Method code here 
    end;

一个代码单元可以定义多个过程(本地或全局)。

事件定义

如前所述,事件是开发 Dynamics 365 Business Central 扩展时的基础构件。在处理事件时,我们有两个主要实体:事件发布者和事件订阅者

一个事件发布者(由应用程序引发的事件)可以通过使用teventbus(用于业务事件)或teventint(用于集成事件)片段在 AL 中定义:

一个业务事件具有以下模式:

[BusinessEvent(IncludeSender)]
    local procedure MyProcedure()
    begin

    end;

在这里,IncludeSender是一个布尔值,指定包含事件发布者方法的对象中定义的全局方法是否对订阅此事件的事件订阅者方法可见(当全局方法必须可见时为true,默认为false表示不可见)。

IncludeSender参数设置为true时,订阅此发布事件的事件订阅者方法的签名将自动包含一个VAR参数(引用值),用于发布事件对象。

集成事件具有以下结构:

[IntegrationEvent(IncludeSender,GlobalVarAccess)]
    local procedure MyProcedure()
    begin

    end;

在这里,IncludeSender布尔参数与我们之前描述的意义相同。

GlobalVarAccess是一个布尔参数,指定是否可以访问包含事件发布者方法的对象中定义的全局变量,供订阅此发布事件的事件订阅者方法使用(当需要暴露时为true,默认值为false表示不可访问)。

GlobalVarAccess参数设置为true时,所有订阅此事件的事件订阅者方法都将能够访问事件发布者方法所在对象中的全局变量。你必须手动将变量参数添加到事件订阅者方法,并且需要使用与事件发布者对象中的变量声明匹配的名称和类型。

在事件发布者(你之前定义的方法)发布事件后,你需要在代码中适当的位置触发该事件(事件订阅者在应用程序代码中触发事件之前不会响应该事件)。

作为示例,以下是一个包含公共方法的代码单元,该方法触发业务事件和集成事件:

codeunit 50100 MyCodeunit
{
    procedure CheckIfPacktCustomerIsEnabled(CustomerNo: Code[20]): Boolean
    begin
        //Raising a business event
        MyBusinessEvent('XXX');

        //Other code here...

        //Raising an integration event
        MyIntegrationEvent('YYY'); 
    end;

    [BusinessEvent(true)]
    local procedure MyBusinessEvent(ID: Code[20])
    begin
    end;

    [IntegrationEvent(true,true)]
    local procedure MyIntegrationEvent(ID: Code[20])
    begin
    end;      

    //Global variables
    var
        myInt: Integer;
        Customer: record Customer;
}

事件订阅者(处理应用程序中已触发事件的函数)可以使用teventsub代码片段声明:

[EventSubscriber(ObjectType::ObjectType, ObjectID, 'OnSomeEvent', 'ElementName', SkipOnMissingLicense, SkipOnMissingPermission)]
local procedure MyProcedure()    
begin

end;

从上述代码中,我们可以看到以下内容:

  • ObjectType是一个枚举,标识发布事件以供订阅的对象类型(包含事件发布者方法的对象),或触发事件以供订阅的对象类型。

  • ObjectId是一个整数值,指定发布事件以供订阅的对象的 ID(声明时不要使用 ID,而是使用ObjectType::Name语法)。

  • OnSomeEvent是一个文本参数,指定由ObjectId参数标识的对象中发布事件的方法的名称。

  • ElementName是一个文本参数,用于数据库触发事件。它指定触发事件相关的表字段。

  • SkipOnMissingLicense 是一个布尔参数,用于指定当运行当前会话的用户帐户的 Dynamics 365 Business Central 许可证未包含对包含订阅者方法的对象的权限时,事件订阅者方法会发生什么(true 表示方法调用必须被忽略,false 表示必须抛出错误并停止代码执行)。

  • SkipOnMissingPermission 是一个布尔参数,用于指定当运行当前会话的用户帐户没有对包含事件订阅者方法的对象的权限时,订阅者方法会发生什么(true 表示方法调用必须被忽略,false(默认值)表示必须抛出错误并停止代码执行)。

作为一个示例,这是一个代码单元,包含我们在前面示例中定义的业务事件和集成事件的两个事件订阅者:

codeunit 50101 MySubscriberCodeunit
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, 'MyBusinessEvent', '', false, false)]
    local procedure MyBusinessEventSubscriber(ID: Code[20])
    begin

    end;

    [EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, 'MyIntegrationEvent', '', false, false)]
    local procedure MyIntegrationEventSubscriber(ID: Code[20])
    begin

    end;   
}

在定义事件订阅者时,如果你在事件参数上按 Ctrl + 空格键,你将看到事件可以与之交互的对象列表(由发布者暴露)。在我们的示例中,业务事件订阅者可以看到事件参数和发送者对象(因为我们已将事件发布者声明为IncludeSender设置为true),如下所示:

集成事件订阅者可以看到事件参数、发送者对象(因为我们已将事件发布者声明为IncludeSender设置为true),以及发送者对象的全局变量(因为我们已将事件发布者声明为GlobalVarAccess = true):

使用事件时,始终记住以下几点:

  • 当调用事件发布者方法的代码运行时,所有订阅该事件的事件订阅者方法都会被执行。

  • 如果有多个订阅者,订阅者方法将按随机顺序逐一运行(无法指定订阅者方法调用的顺序)。

  • 如果没有订阅发布事件的订阅者,那么调用事件发布者方法的代码行将被忽略,并且不会执行。

XMLport 定义

XMLport 是用于在外部源与 Dynamics 365 Business Central 之间导入和导出 XML 或基于文本的数据的对象。

可以使用 txmlport 代码片段在 AL 中定义一个 XMLport:

xmlport Id MyXmlport
{
    schema
    {
        textelement(NodeName1)
        {
            tableelement(NodeName2; SourceTableName)
            {
                fieldattribute(NodeName3; NodeName2.SourceFieldName)
                {

                }
            }
        }
    }

    requestpage
    {
        layout
        {
            area(content)
            {
                group(GroupName)
                {
                    field(Name; SourceExpression)
                    {

                    }
                }
            }
        }

        actions
        {
            area(processing)
            {
                action(ActionName)
                {

                }
            }
        }
    }

    var
        myInt: Integer;
}

作为一个示例,这是一个简单的 XMLport 定义,用于导入一些客户数据(No.Name 字段):

xmlport 50100 MyXmlportImportCustomer
{
    Direction = Import;
    schema
    {
        textelement(NodeName1)
        {
            tableelement(Customer; Customer)
            {
                fieldattribute(No; Customer."No.")
                {

                }
                fieldattribute(Name; Customer.Name)
                {

                }
            }
        }
    }  
}

xmlport 对象的 Direction 属性设置为 Import(仅用于将数据导入 Dynamics 365 Business Central),并从名为 Customer 的 XML 对象中读取 NoName 字段。

定义查询对象

query 对象允许你定义一个对象,可以通过应用过滤器和在表之间建立连接,从一个单独的表或多个表中检索数据。返回的结果是一个单一的数据集。

你可以通过使用tquery代码片段在 AL 中创建查询:

query Id MyQuery
{
    QueryType = Normal;

    elements
    {
        dataitem(DataItemName; SourceTableName)
        {
            column(ColumnName; SourceFieldName)
            {

            }
            filter(FilterName; SourceFieldName)
            {

            }
        }
    }

    var
        myInt: Integer;

    trigger OnBeforeOpen()
    begin

    end;
}

如你所见,query对象有一个elements部分,在该部分内,你定义了一个dataitem及其必须检索的column元素(要包含在结果数据集中的表字段)。

你还可以在dataitems之间创建链接,以从多个表中检索数据。

作为示例,以下是一个query对象,它已在 AL 中定义,用于检索客户列表及其销售和利润数据:

query 50100 "Customer Overview"
{
    Caption = 'Customer Overview';
    elements
    {
        dataitem(Customer; Customer)
        {
            column(Name; Name)
            {
            }
            column(No; "No.")
            {
            }
            column(Sales_LCY; "Sales (LCY)")
            {
            }
            column(Profit_LCY; "Profit (LCY)")
            {
            }
            column(Country_Region_Code; "Country/Region Code")
            {
            }
            column(City; City)
            {
            }          
            column(Salesperson_Code; "Salesperson Code")
            {
            }

            dataitem(Salesperson_Purchaser; "Salesperson/Purchaser")
            {
                DataItemLink = Code = Customer."Salesperson Code";
                column(SalesPersonName; Name)
                {
                }
                dataitem(Country_Region; "Country/Region")
                {
                    DataItemLink = Code = Customer."Country/Region Code";
                    column(CountryRegionName; Name)
                    {
                    }
                }
            }
        }
    }
}

查询循环遍历Customer表,然后(对于每个客户)从在DataItemLink属性中指定的其他表中检索数据。

查询对象在代码中非常有用和强大,用于检索记录。你可以通过查询对象解决的第一个基本问题是避免在从关联表(联接)中检索数据时使用嵌套循环。如果你有一个通过外键与Table1关联的Table2,那么你可以使用查询对象,避免通过循环遍历Table1并为每个记录去Table2检索相关数据,可以应用以下图示所描述的模式:

在这里,你可以定义一个查询,它返回两个表的完整过滤联接,然后你可以循环遍历由查询对象返回的记录集(这只需要一个循环)。

如果(例如)我们想在代码中使用之前定义的Customer Overview查询,那么我们需要在 AL 中这样做:

procedure UseCustomerOverviewQuery()
    var
        CustomerOverview: Query "Customer Overview";
    begin
        if not CustomerOverview.Open() then
            exit;
        while CustomerOverview.Read() do
        begin
            //Here we have all joined records to loop
        end;
    end;

在这里,我们通过调用Open方法执行查询对象,然后通过使用Read方法循环遍历返回的数据集。在循环内,你将获得查询返回的完整记录(主表和已联接的表),并且可以根据需要处理这些数据。

扩展选项 – 枚举

选项类型的字段在 Dynamics 365 Business Central 中用于定义一个提供固定且预定义值列表的字段。

当你定义一个选项字段时,你可以通过以下方式定义该字段的允许值:

field(5; LicenseType; Option)
{            
    OptionMembers = " ","Full","Limited";
    OptionCaption = ' ,Full,Limited';
    Caption = 'License Type';
    DataClassification = CustomerContent;
}

在前面的代码中,我们可以看到OptionMembers属性包含了字段的预定义值。在这里,许可证类型字段包含三个值(空白、完整、限制),其中空白(第一个值)是默认值。

但是,如果你想扩展这些选项,例如,添加一个新的许可证类型叫做Teams,该怎么办呢?这是不可能的!选项字段不能扩展。

为了创建一个可扩展的选项字段,AL 引入了enum对象。一个enum是由一组命名常量组成的类型,并且如果你将Extensible属性设置为true,它可以从其他扩展中进行扩展,如下所示:

enum 50100 LicenseType
{
  Extensible = true;
  value(0; None) { }
  value(1; Full) { }
  value(2; Limited) { }  
}

你可以按照以下方式定义一个字段,使其具有enum类型:

field(50100; LicenseType; enum LicenseType)     
{
    Caption = 'License Type';
    DataClassification = CustomerContent;
}

这使得你能够定义一个字段,它的行为与选项相同:当用户点击该字段时,Dynamics 365 Business Central 会展示一个可供选择的值列表。

要从另一个扩展中扩展 enum 字段并添加一个新的可能值 Team,你需要创建一个 enumextension 对象,具体如下:

enumextension 50110 LicenseTypeEnumExt extends LicenseType
{
  value(50110; Team)
  {
    Caption = 'Team License';
  }
}

之后,你的 License Type 字段将有一个新的选项值可以选择。

你还可以直接在 AL 代码中使用 enum 对象(作为变量):

var
    LicenseType: enum LicenseType;
begin
    case LicenseType of
        LicenseType::Full:
              //Write your code here…

你还可以扩展 enum 值的 TableRelation 属性。例如,假设你有以下表格:

table 50120 LicenseDetail
{
  fields
  {
     field(1; Id; Integer) { }
     field(2; LicenseType; enum LicenseType) { }
     field(3; LicenseDetail; Code[20])
     {
        TableRelation =
        if (LicenseType = const (Full)) FullLicenseTable
        else if (LicenseType = const (Limited)) LimitedLicenseTable;
    }
  }
}

在这个表中,我们有一个名为 LicenseType(这是一个 enum 类型)的字段,以及一个名为 LicenseDetail 的字段,它有一个 tablerelation 属性(指向 FullLicenseTableLimitedLicenseTable 表),其基于 enum 字段的值。

另一个应用程序可以同时扩展 enum 字段和表关系,这样它就可以处理新的扩展枚举。以下是一个示例:

enumextension 50110 LicenseTypeEnumExt extends LicenseType
{
  value(50110; Team)
  {
    Caption = 'Team License';
  }
}

tableextension 50110 LicenseDetailExt extends LicenseDetail
{
  fields
  {
    modify(LicenseDetail)
    {
      TableRelation = if (LicenseType = const (Team)) TeamLicenseTable;
    }
  }
}

在这里,新应用程序创建了 LicenseType 的枚举扩展(如我们之前所述),并创建了一个新的 tableextension 对象,在这个对象中,它通过添加一个新的关系来修改 LicenseDetail 字段的 TableRelation 属性,当 enum 的值为 Team 时,关联一个 TeamLicenseTable

合并后的 TableRelation 始终从上到下进行评估,因此第一个无条件的关系将优先。这意味着,如果原字段与表 A 有关系,你无法将其 TableRelation 从表 A 更改为表 B。

通过使用 enums,你可以扩展所有选项的值。如果你希望具有可扩展性,我们建议在你的扩展中使用这种新方法。

在这一部分,你已经全面了解了 AL 语言扩展中的可用对象。在下一部分,我们将学习一些创建和处理 AL 项目时的最佳实践。

创建一个配置文件对象

一个 profile 对象使你能够定义特定用户配置文件的用户体验(主页)。你可以通过使用 AL 语言扩展中的 tprofile 代码片段来创建一个 profile 对象。

一个配置文件对象的定义如下所示:

profile "SALES MANAGER"
{
  Caption = 'Sales Manager';
  ProfileDescription = 'Functionality for sales managers';
  RoleCenter = 9005;
  Enabled = false;
}

在这里,我们定义了一个名为 Sales Manager 的配置文件,它使用 RoleCenter 页面,ID = 9005(这是 Dynamics 365 Business Central 中的标准销售经理角色中心对象)。

要从你的扩展中部署一个 profile 对象,我建议在你的 AL 项目中创建一个 Profile 文件夹,并将所有定义配置文件的 .al 文件放在该文件夹内。

理解 AL 项目结构的最佳实践

正如我们之前提到的,AL 项目是基于文件的。你所有的 .al 文件都位于一个项目文件夹中。当你开始处理一个复杂项目时,最常遇到的问题就是如何组织项目。我们该如何组织对象和 .al 文件呢?

这个话题没有书面规则。我们真诚地建议避免将所有对象(.al 文件)放在项目根目录级别,如下图所示:

在这里,没有对对象进行任何组织,如果您有大量对象,您的对象列表将会增长很多,导致处理和查找文件时遇到困难。

组织项目的最常见方式是按对象类型组织文件,如下图所示:

这里,所有扩展的代码都在 SRC 文件夹内。然后,所有对象按类型进行组织,根据我们定义的对象(每种对象类型都有一个子文件夹)。这种组织方式使得查找对象更容易(只需进入相应的对象类型文件夹),但是这种项目结构有一个缺点:不容易识别出我们需要哪些对象来实现扩展项目中的特定业务功能。

我们建议先按功能组织项目树,再按对象类型组织,如下图所示:

这里,在 SRC 文件夹中,有两个子文件夹:Functionality1Functionality2。在这些文件夹中,对象按类型组织。这是我们推荐的工作方式,这种结构有助于我们按功能查找对象。

在接下来的章节中,我们将学习如何在 AL 中命名对象,以及如何使用对象范围。

命名规范和 AL 对象范围

在为 Dynamics 365 Business Central 创建扩展时,您需要为对象分配一个数字 ID。分配对象 ID 的规则如下:

范围 用途
0 – 49,999 Business Central 基础应用程序。合作伙伴不能使用此范围。
5,0000 – 99,999 每租户扩展(希望根据客户的个别需求定制已交付解决方案的经销商)。
80,000 – 99,999 需要在开发许可证中修改权限的扩展对象。
100,000 – 999,999 用于为特定国家或地区本地化 Dynamics 365 Business Central。合作伙伴不能使用此范围。
1,000,000 – 69,999,999 注册解决方案计划 (RSP) 范围。
70,000,000 – 74,999,999 Business Central SaaS 应用程序(AppSource)。

关于文件命名,每个 .al 文件名必须以相应的对象类型前缀和对象 ID 开头,并且只能使用字符 [A-Za-z0-9]。文件命名规则(对于 AppSource 是强制要求的)应该如下所示:

  • 完整对象:<ObjectNameExcludingPrefix>.<FullTypeName>.al

  • 扩展对象:<ObjectNameExcludingPrefix>.<FullTypeName>Ext.al

对于每种对象类型,您可以使用以下缩写(前缀):

对象类型 缩写(前缀)
页面 Page
页面扩展 PageExt
页面自定义 PageCust
代码单元 Codeunit
Table
表扩展 TableExt
XML 端口 Xmlport
报告 Report
查询 Query
枚举 Enum
枚举扩展 EnumExt

例如,以下是一些 AL 对象及其对应的文件名:

  • 表 50100 书籍 应命名为 Book.Table.al

  • 页面 50100 书籍卡片 应命名为 BookCard.Page.al

  • 代码单元 50110 书籍管理 应命名为 BookManagement.Codeunit.al

  • 页面扩展 50101 MyCustomerCardExt,扩展 客户卡片,应命名为 CustomerCard.PageExt.al

您还应该使用前缀/后缀来标识您的对象(由 Microsoft 为您保留,如下文所述)。这使您能够在扩展之间为对象命名一个唯一的方式,从而避免命名冲突。

使用前缀/后缀的规则如下:

  • 前缀/后缀必须至少为三个字符。

  • 对象/字段名称必须以前缀/后缀开始或结束。

  • 当您使用表扩展或页面扩展修改核心 Dynamics 365 对象时,前缀/后缀必须在控件/字段/操作/组级别定义。

  • 使用标题处理您想要在 UI 中显示的标签。

例如,如果您已保留 PACKT 前缀,并且想要创建一个名为 CustomerCategory 的字段,则可以使用以下有效字段名:

  • PACKTCustomerCategory

  • CustomerCategoryPACKT

  • CustomerCategory_PACKT

  • CustomerCategory PACKT

如果您想创建 客户类别 表,表对象的有效名称如下:

  • 表 70000000 PACKT 客户类别

  • 表 70000000 Customer Category PACKT

  • 表 70000000 Customer Category_PACKT

使用保留名称作为前缀或后缀完全由您选择。我们建议将其用作后缀,因为在 Visual Studio IntelliSense 中查找字段更自然(如果您在 UI 中看到的字段是客户类别,输入这些词会呈现实际字段名称及其后缀)。

这些指南对于 AppSource 是强制性的,但对于您的每租户扩展并非强制执行。我们的建议是始终遵循这些指南。

要为您的对象注册前缀/后缀,您需要发送电子邮件到 d365val@microsoft.com,并指定您希望为应用保留的名称。请记住,前缀/后缀应该是基于应用的,而不是基于公司的。

AL 编码指南工作中

在创建 AL 项目(和 .al 文件)时,请记住始终遵循这些主要指南。

.al 代码文件中,所有对象的结构必须遵循以下顺序:

  • 属性

  • 特定对象结构:

    • 表字段

    • 页面布局

    • 操作

  • 全局变量:

    • 标签(旧文本常量)

    • 全局变量

  • 方法

请记住,始终通过对象名称而非对象 ID 来引用 AL 对象。例如,以下是如何引用 Record 变量或 Page 变量:

Vendor: Record Vendor;
Page.RunModal(Page::"Customer Card", ...);

在事件订阅者对象中,应该这样引用发布者对象:

[EventSubscriber(ObjectType::Codeunit, Codeunit::MyCodeunit, 'MyIntegrationEvent', '', false, false)]
local procedure MyIntegrationEventSubscriber()
begin
end;

那么,让我们总结一下:

  • 格式化你的 AL 代码:注意缩进和空格(它保持代码的可读性)。你可以使用 Alt + Shift + F 来自动格式化代码。

  • 保持你的 .al 文件整洁:使用代码片段时,它们会自动创建带有方法、属性、变量、触发器或你可能未使用的部分的对象骨架。请删除所有未使用的代码。一个典型的例子是表格上的触发器定义(如果你没有处理它们,可以删除)或对象中的全局变量(如果不删除,它们会导致你的应用充满 myInt: integer 变量)。

  • 方法声明:尽量保持本地化。只有在需要将其暴露给其他对象时,才使用全局方法。

  • 使用事件触发业务逻辑,但 不要在这些触发器中编写代码:将大量代码放入触发事件中,就像把大量代码放入字段验证触发器中一样。应识别你的方法并从触发事件中调用它们。

对于复杂代码,你可以开始使用 通用方法 模式:

  • 在其类(表格)中声明每个方法。

  • 每个方法都是一个独立的代码单元(封装)。

  • 仅从其类(表格/代码单元)中调用方法。

  • 每个方法的代码单元只有一个全局函数。

  • 本地函数包括以下类别(按此顺序):

    1. 主函数(一个函数;以可读流程图形式呈现的方法头)

    2. 主要业务流程(多个函数)

    3. UI 包装器(两个函数)

    4. 业务扩展(一个或多个函数提供可扩展性)

    5. 事件包装器(两个函数)

这是根据此模式组织的一些 AL 代码示例:

你可以在 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/compliance/apptest-bestpracticesforalcode 上找到有关其他编码规则的更多信息。

遵守编码规则和指南对于提高代码可读性非常重要,而且这些规则中的许多是 AppSource 强制要求的。

总结

在本章中,我们探讨了使用 AL 语言进行扩展开发的基础知识,并概述了创建应用程序的主要对象(表格、页面、代码单元等)以及如何在 Visual Studio Code 中创建它们。接着,我们回顾了处理 AL 项目的最佳实践(项目组织、对象 ID、命名约定),并提供了编写更好代码的指南,重点关注我们扩展的可扩展性方面。

我们学习了如何创建对象,如何创建 AL 项目,如何处理其结构,以及如何遵守对象命名规范。

在下一章,我们将通过应用所有这些规则和最佳实践,为 Dynamics 365 Business Central 实现一个现实世界的扩展。

第五章:为 Dynamics 365 Business Central 开发定制化解决方案

在上一章中,我们学习了 Dynamics 365 Business Central 扩展开发的基本知识,并分析了创建扩展的所有构建模块,如事件和基本对象定义,以及如何扩展标准对象。

在本章中,我们将把所有这些概念结合起来,并为 Dynamics 365 Business Central 创建一个实际的扩展。这些扩展将按照AppSource的指导原则和最佳代码实践进行创建。

本章将涵盖以下主题:

  • 将业务案例转化为实际扩展

  • 理解依赖扩展

将业务案例转化为实际扩展

在本节中,让我们假设有一个拥有各种业务需求的 Dynamics 365 Business Central 客户。我们希望创建一个扩展来满足该客户的需求。

我们的客户是一家大型商业公司,已将 Dynamics 365 Business Central 作为公司 ERP,并且有多种业务需求,要求定制化标准功能以满足其需求。

业务需求如下:

  • 销售:这些要求包括以下内容:

    • 该公司希望根据自定义类别对客户进行分类,这些类别可以根据需要定义,并且将来可以更改。每个客户类别必须有其详细信息,这些信息可以用于某些业务流程。

    • 销售部门必须能够创建默认的客户类别,并将此默认值自动分配给客户。

    • 销售部门需要能够为客户类别创建赠品活动。赠品活动与特定的时间段和一组限定的商品相关。

    • 赠品活动可以在某一段时间内设置为非活动状态。

    • 当赠品活动处于激活状态时,销售订单经理必须能够在客户的销售订单上自动分配赠品(他们需要在销售订单文档上有一个按钮,分析订单内容,检查是否有活动促销,并相应地创建赠品行)。

    • 当销售操作员插入销售订单行时,如果客户订购的商品数量接近于一个正在进行的活动促销,他们应该被提醒。

    • 当销售订单过账时,生成的商品账单条目必须存储客户类别值(订单生成时的值),以供报告使用。

  • 供应商质量:这些要求包括以下内容:

    • 公司已实施质量管理流程(CSQ,国际商业质量认证机构),并需要根据其 CSQ 要求对供应商进行分类:

      • 与商品质量相关的评分(从 1 到 10)

      • 与按时交付相关的评分(从 1 到 10)

      • 与商品包装相关的评分(从 1 到 10)

      • 定价相关的评分(从 1 到 10)

    • 供应商质量卡还必须显示一些财务数据:

      • 本年度已开票 N

      • 本年度已开票 N-1

      • 开具 N-2 年度的发票

      • 此供应商的应付金额

      • 此供应商的应付金额(尚未到期)

    • 分配的分数根据算法确定供应商评分(一个数字值)。

    • 如果供应商不符合公司标准要求(供应商评分),采购部门不能发布采购订单。

    • 该应用的行为将来可能会扩展。

这些自定义将作为一个单一的扩展进行开发,使用每租户范围(50.000 – 99.999)。我们将使用 AppSource 规则,并使用 PKT 标签(已在 Microsoft 注册为我们的 AppSource 前缀/后缀)来定位所有对象。项目的 .al 文件将按照 AppSource 命名约定命名。

我们通过打开 Visual Studio Code 并创建一个新的扩展项目(查看 | 命令面板 | AL:GO!),选择 Wave 2 发布版本作为目标,开始我们的开发任务。

我们设置了扩展的清单文件(app.json),如下所示:

{
  "id": "dd03d28e-4dfe-48d9-9520-c875595362b6",
  "name": "PacktDemoExtension",
  "publisher": "SD",
  "brief": "Customer Category, Gift Campaigns and Vendor Quality Management",
  "description": "Customer Category, Gift Campaigns and Vendor Quality Management",
  "version": "1.0.0.0",
  "privacyStatement": "",
  "EULA": "",
  "help": "",
  "url": "http://www.demiliani.com",
  "logo": "./Logo/ExtLogo.png",
  "dependencies": [
    {
      "appId": "63ca2fa4-4f03-4f2b-a480-172fef340d3f",
      "publisher": "Microsoft",
      "name": "System Application",
      "version": "1.0.0.0"
    },
    {
      "appId": "437dbf0e-84ff-417a-965d-ed2bb9650972",
      "publisher": "Microsoft",
      "name": "Base Application",
      "version": "15.0.0.0"
    }
  ],
  "screenshots": [],
  "platform": "15.0.0.0",
  "features": [
    "TranslationFile"
  ],
  "idRanges": [
    {
      "from": 50100,
      "to": 50149
    }
  ],
  "contextSensitiveHelpUrl": "https://PacktDemoExtension.com/help/",
  "runtime": "4.0"
}

在这里,我们设置扩展的详细信息,例如名称、发布者、版本、描述、徽标图像路径、已批准的对象范围 ID(从 50100 到 50149)和支持的运行时版本。

我们还设置了以下选项:

"features": [
    "TranslationFile"
  ]

TranslationFile 功能意味着我们希望拥有一个 XLIFF 翻译文件,用于处理此扩展的多语言功能。

我们希望通过为功能和对象类型分别设置子文件夹来组织项目结构。我们的基本项目结构将如下所示:

在这里,我们有一个 Src 文件夹,里面有三个主要功能文件夹:

  • CustomerCategory:这是实现 Customer Category 需求的部分。

  • Gifts:这是实施赠品活动要求的部分。

  • VendorQuality:这是实施供应商质量要求的部分。

在这些文件夹内部,我们有按对象类型组织的子文件夹。

让我们开始处理这三个模块中的每一个。

客户类别实现

为了处理客户类别管理需求,我们需要执行以下操作:

  1. 定义 Customer Category 表。

  2. 创建将处理 Customer Category 实体的页面(ListCard 页面)。

  3. 向标准 Customer 表添加一个新的 Customer Category 字段。

  4. 将新字段添加到标准的 Customer Card 页面,并在 Customer 页面上添加一些操作来处理某些任务。

  5. 创建处理需求的业务逻辑。

在接下来的章节中,我们将详细查看各种对象的定义和实现。

表定义

通过使用 ttable 代码片段,我们定义 Customer Category 表如下:

table 50100 "Customer Category_PKT"
{
     DrillDownPageId = "Customer Category List_PKT";
     LookupPageId = "Customer Category List_PKT";
     Caption = 'Customer Category';

     fields
     {
         field(1; Code; Code[20])
         {
             DataClassification = CustomerContent;
             Caption = 'No.';
         }
         field(2; Description; Text[50])
         {
             DataClassification = CustomerContent;
             Caption = 'Description';
         }
         field(3; Default; Boolean)
         {
             DataClassification = CustomerContent;
             Caption = 'Default';
         }
         field(4; EnableNewsletter; Enum NewsletterType)
         {
             Caption = 'Enable Newsletter';
             DataClassification = CustomerContent;
         }
         field(5; FreeGiftsAvailable; Boolean)
         {
             DataClassification = CustomerContent;
             Caption = 'Free Gifts Available';
         }
         field(6; Blocked; Boolean)
         {
             DataClassification = CustomerContent;
             Caption = 'Blocked';
         }
         field(10; TotalCustomersForCategory; Integer)
         {
             FieldClass = FlowField;
             CalcFormula = count (Customer where ("Customer Category Code_PKT" = field (Code)));
             Caption = 'No. of associated customers';
         }
     }
     keys
     {
         key(PK; Code)
         {
             Clustered = true;
         }
         key(K2; Description)
         {
             Unique = true;
         }
     }

 procedure GetSalesAmount(): Decimal
 var
     CustomerCategoryMgt: Codeunit "Customer Category Mgt_PKT";
     begin
         exit(CustomerCategoryMgt.GetSalesAmount(Rec.Code));
     end;
}

对象的名称具有注册的 _PKT 后缀(确保在应用中唯一)。

在此表定义中,我们定义了以下字段:

  • Code:这是类别的代码(key 字段)。

  • Description:这是类别的描述。

  • Default:这是一个Boolean字段,用于设置默认类别。

  • FreeGiftsAvailable:这是一个Boolean字段,用于设置该类别是否可以与赠品活动一起使用。

  • Blocked:这是一个Boolean字段,用于设置类别为已阻止(不能使用)。

  • EnableNewsletter:这是一个选项字段,用于选择要为该类别发送的通讯类型(商业用途)。该字段是enum类型。正如上一章所述,enum类型允许我们拥有一个可扩展的选项字段。

  • TotalCustomersForCategory:这是一个计算字段(flowfield),用于自动计算与所选类别相关的客户数量。

该表的定义包含一个键部分,在这里我们定义了主键(No字段)和一个次键(Description字段)。这个次键被定义为设置了Unique属性为true,确保在该表中不能有相同值的记录:

key(K2; Description)
{
     Unique = true;
}

NewsletterType枚举定义如下:

enum 50100 NewsletterType
{
 Extensible = true;
 value(0; None)
 {
     Caption = 'None';
 }
 value(1; Full)
 {
     Caption = 'Full';
 }
 value(2; Limited)
 {
     Caption = 'Limited';
 }
}

作为一种通用编程规则,表格类似于类,在表的定义中,我们希望公开与该类相关的方法。这就是为什么我们在这里定义了一个名为GetSalesAmount(用于返回所选类别的总销售额)的方法。该方法的实现将在外部代码单元中(包含我们的业务逻辑)。

我们还为该扩展定义了一个设置表(Packt Extension Setup表,我们将在接下来的章节中使用),用于处理公司业务配置所需的所有可变参数。

该设置表定义如下:

table 50103 "Packt Extension Setup"
{
 DataClassification = CustomerContent; 
 Caption = 'Packt Extension Setup';

 fields
 {
     field(1; "Primary Key"; Code[10])
     {
         DataClassification = CustomerContent;
     }
     field(2; "Minimum Accepted Vendor Rate"; Decimal)
     {
         Caption = 'Minimum Accepted Vendor Rate for Purchases';
         DataClassification = CustomerContent;
     }
     field(3; "Gift Tolerance Qty"; Decimal)
     {
         Caption = 'Gift Tolerance Quantity for Sales';
         DataClassification = CustomerContent;
     }
 }
 keys
 {
     key(PK; "Primary Key")
     {
         Clustered = true;
     }
 }
}

为扩展创建一个专用的设置表是最佳实践,因为它允许你将设置集中在一个地方。如果可能,请避免将设置添加到不同的标准 Dynamics 365 Business Central 设置表中。

页面定义

为了处理Customer Category记录(插入、修改、删除和选择),我们需要有一个列表页面和一个卡片页面。

通过使用 tpage代码片段,我们定义了一个卡片页面(PageType = Card)和一个列表页面(PageType = List)。

列表页面(Customer Category List_PKT)有一个创建默认Customer Category记录的操作(它调用了一个在外部代码单元中定义的方法,因为我们不希望在页面上有业务逻辑)。

列表页面定义的代码如下:

page 50100 "Customer Category List_PKT"
{
 PageType = List;
 SourceTable = "Customer Category_PKT";
 UsageCategory = Lists;
 ApplicationArea = All;
 CardPageId = CustomerCategoryCard_PKT;
 Caption = 'Customer Category List'; 
 AdditionalSearchTerms = 'ranking, categorization';

 layout
 {
     area(content)
     {
         repeater(Group)
         {
             field(Code; Code)
             {
                 ApplicationArea = All;
             }
             field(Description; Description)
             {
                 ApplicationArea = All;
             }
             field(Default; Default)
             {
                 ApplicationArea = All;
             }
             field(TotalCustomersForCategory; TotalCustomersForCategory)
             {
                 ApplicationArea = All;
                 ToolTip = 'Total Customers for Category';
             }
         }
     }
 }
 actions
 {
     area(processing)
     {
         action("Create Default Category")
         {
             Image = CreateForm;
             Promoted = true;
             PromotedCategory = Process;
             PromotedIsBig = true;
             ApplicationArea = All;
             ToolTip = 'Create default category';
             Caption = 'Create default category';

             trigger OnAction();
             var
                 CustManagement: Codeunit "Customer Category Mgt_PKT";
             begin
                 CustManagement.CreateDefaultCategory();
             end;
          }
     }
   }
}

作为最佳实践,为了改善搜索体验并帮助用户通过 Dynamics 365 Business Central 的搜索功能轻松找到正确的页面,我们还定义了AdditionalSearchTerms属性。这些术语将与Caption页面属性一起用于通过搜索引擎查找页面。

CUSTOMER CATEGORY LIST 页面如下所示:

card页面(CustomerCategoryCard_PKT)有不同的组来分别在不同的FastTabs上显示数据。在OnAfterGetRecord触发器中,我们计算了该类别的总销售额,并将该值分配给一个全局小数字段(叫做TotalSalesAmount),然后将这个变量显示为页面字段。代码如下:

page 50101 CustomerCategoryCard_PKT
{
 PageType = Card;
 ApplicationArea = All;
 UsageCategory = Documents;
 SourceTable = "Customer Category_PKT";
 Caption = 'Customer Category Card';

 layout
 {
     area(Content)
     {
         group(General)
         {
             Caption = 'General';
             field(Code; Code)
             {
                 ApplicationArea = All;
             }
             field(Description; Description)
             {
                 ApplicationArea = All;
             }
             field(Default; Default)
             {
                 ApplicationArea = All;
             }
             field(EnableNewsletter; EnableNewsletter)
             {
                 ApplicationArea = All;
             }
             field(FreeGiftsAvailable; FreeGiftsAvailable)
             {
                 ApplicationArea = All;
             }
         }
         group(Administration)
         {
             Caption = 'Administration';
             field(Blocked; Blocked)
             {
                 ApplicationArea = All;
             }
         }
         group(Statistics)
         {
             Caption = 'Statistics';
             field(TotalCustomersForCategory; TotalCustomersForCategory)
             {
                 ApplicationArea = All;
                 Editable = false;
             }
             field(TotalSalesAmount; TotalSalesAmount)
             {
                 ApplicationArea = All;
                 Caption = 'Total Sales Order Amount';
                 Editable = false;
                 Style = Strong;
             }
         }
       }
     }
     var
         TotalSalesAmount: Decimal;
     trigger OnAfterGetRecord()
     begin
         TotalSalesAmount := Rec.GetSalesAmount();
     end;
}

...CUSTOMER CATEGORY CARD 页面看起来是这样的:

我们还创建了一个用于扩展设置的页面(叫做Packt Extension Setup),定义如下:

table 50103 "Packt Extension Setup"
{
 DataClassification = CustomerContent;
 Caption = 'Packt Extension Setup';

 fields
 {
     field(1; "Primary Key"; Code[10])
     {
         DataClassification = CustomerContent;
     }
     field(2; "Minimum Accepted Vendor Rate"; Decimal)
     {
         Caption = 'Minimum Accepted Vendor Rate for Purchases';
         DataClassification = CustomerContent;
     }
     field(3; "Gift Tolerance Qty"; Decimal)
     {
         Caption = 'Gift Tolerance Quantity for Sales';
         DataClassification = CustomerContent;
     }
 }

 keys
 {
     key(PK; "Primary Key")
     {
         Clustered = true;
     }
  }
}

这个页面看起来是这样的:

这将允许用户处理我们扩展的设置。

tableextension定义

我们需要在Customer表中创建一个新字段来处理Customer Category的分配,为了做到这一点,我们需要创建一个tableextension对象。这可以通过使用ttableext代码段在 AL 中完成。

Customer表的tableextension对象定义如下:

tableextension 50100 "CustomerTableExtensions_PKT" extends Customer //18 
{
 fields
 {
     field(50100; "Customer Category Code_PKT"; Code[20])
     {
         TableRelation = "Customer Category_PKT".No;
         Caption = 'Customer Category Code';
         DataClassification = CustomerContent;

         trigger OnValidate()
         var
             CustomerCategory: Record "Customer Category_PKT";
             ErrBlocked: Label 'This category is Blocked.';
         begin
             CustomerCategory.Get("Customer Category Code_PKT");
             if CustomerCategory.Blocked then
                 Error(ErrBlocked);
         end;
     }
 }

 keys
 {
     key(CustomerCategory; "Customer Category Code_PKT")
     {
     }
 }
}

在这里,我们还处理了该字段的OnValidate触发器,以避免插入被阻止的类别。

我们还在Customer表上基于这个新字段创建了一个新的辅助键:

keys
    {
        key(CustomerCategory; "Customer Category_PKT")
        {
        }
    }

其中一个需求是还需要将Customer Category Code字段添加到Item Ledger Entry表中(这个字段在过账时必须写入,以便于报告),因此我们还定义了以下tableextension对象:

tableextension 50101 "ItemLedgerEntryExtension_PKT" extends "Item Ledger Entry"
{
    fields
    {
        field(50100; "Customer Category Code_PKT"; Code[20])
        {
            TableRelation = "Customer Category_PKT".No;
            Caption = 'Customer Category';
            DataClassification = CustomerContent;
        }
    }

    keys
    {
        key(FK; "Customer Category Code_PKT")
        {
        }
    }
}

这个新的自定义字段将用于统计目的。

pageextension定义

这个新创建的Customer Category字段必须在Customer CardCustomer List页面上可见。

为了做到这一点,我们定义了两个pageextension对象(通过使用tpageext代码段)。以下是Customer Card页面的pageextension对象定义:

pageextension 50102 "CustomerCardExtension_PKT" extends "Customer Card"
{
     layout
     {
         addlast(General)
         {
             field("Customer Category PKT"; "Customer Category Code_PKT")
             {
                 ToolTip = 'Customer Category';
                 ApplicationArea = All;
             }
         }
     }

     actions
     {
         addlast("Functions")
         {
             action("Assign default category")
             {
                 Image = ChangeCustomer;
                 Promoted = true;
                 PromotedCategory = Process;
                 PromotedIsBig = true;
                 ApplicationArea = All;
                 Caption = 'Assign Default Category';
                 ToolTip = 'Assigns Default Category to the current Customer';

                trigger OnAction();
                 var
                     CustomerCategoryMgt: Codeunit "Customer Category Mgt_PKT";
                 begin
                     CustomerCategoryMgt.AssignDefaultCategory(Rec."No.");
                 end;
             }
         }
     }
}

这是Customer List页面的pageextension对象定义:

pageextension 50103 CustomerListExtension_PKT extends "Customer List"
{
     actions
     {
         addlast(Processing)
         {
             action("Assign Default Category")
             {
                 Image = ChangeCustomer;
                 Promoted = true;
                 PromotedCategory = Process;
                 PromotedIsBig = true;
                 ApplicationArea = All;
                 Caption = 'Assign Default Category to all Customers';
                 ToolTip = 'Assigns the Default Category to all Customers';

                 trigger OnAction();
                 var
                     CustomerCategoryMgt: Codeunit "Customer Category Mgt_PKT";
                 begin
                     CustomerCategoryMgt.AssignDefaultCategory();
                 end;
             }
         }
     }

     views
     {
         addlast
         {
             view(CustomersWithoutCategory)
             {
                 Caption = 'Customers without Category assigned';
                 Filters = where ("Customer Category Code_PKT" = filter (''));
             }
         }
     }
}

在这里,在Customer List页面上,我们添加了一个操作,将默认设置的类别分配给所有客户。在Customer Card上,相同的操作将在当前选定的记录上执行。

你可以看到这两个函数调用了一个外部代码单元中的方法,叫做AssignDefaultCategory。这个方法有两个实现(是重载的),我们将在本章后面进一步探讨。

标准的Customer List页面现在看起来像这样:

Customer Card看起来像这样:

在这里,我们添加了新创建的Customer Category字段和用于分配Customer Category的新操作。

代码单元定义

为了处理客户的Customer Category业务需求,所有必需的业务逻辑都在一个名为Customer Category Mgt_PKT的专用代码单元中定义。

代码单元定义如下:

codeunit 50100 "Customer Category Mgt_PKT"
{
 procedure CreateDefaultCategory()
 var
     CustomerCategory: Record "Customer Category_PKT";
 begin
     CustomerCategory.Code := 'DEFAULT';
     CustomerCategory.Description := 'Default Customer Category';
     CustomerCategory.Default := true;
     if CustomerCategory.Insert then;
 end;

 procedure AssignDefaultCategory(CustomerCode: Code[20])
 var
     Customer: Record Customer;
     CustomerCategory: Record "Customer Category_PKT";
 begin
     //Set default category for a Customer 
     Customer.Get(CustomerCode);
     CustomerCategory.SetRange(Default, true);
     if CustomerCategory.FindFirst() then begin
         Customer."Customer Category Code_PKT" := CustomerCategory.Code;
         Customer.Modify();
     end;
 end;

 procedure AssignDefaultCategory()
 var
     Customer: Record Customer;
     CustomerCategory: Record "Customer Category_PKT";
 begin
     //Set default category for ALL Customer 
     CustomerCategory.SetRange(Default, true);
     if CustomerCategory.FindFirst() then begin
         Customer.SetFilter("Customer Category Code_PKT", '%1', '');
         Customer.ModifyAll("Customer Category Code_PKT", CustomerCategory.Code, true);         
     end;
 end;

 //Returns the number of Customers without an assigned Customer Category
 procedure GetTotalCustomersWithoutCategory(): Integer
 var
     Customer: record Customer;
 begin
     Customer.SetRange("Customer Category Code_PKT", '');
     exit(customer.Count());
 end;

 procedure GetSalesAmount(CustomerCategoryCode: Code[20]): Decimal
 var
     SalesLine: Record "Sales Line";
     Customer: record Customer;
     TotalAmount: Decimal;
 begin
     Customer.SetCurrentKey("Customer Category Code_PKT");
     Customer.SetRange("Customer Category Code_PKT", CustomerCategoryCode);
     if Customer.FindSet() then
     repeat
         SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
         SalesLine.SetRange("Sell-to Customer No.", Customer."No.");
         if SalesLine.FindSet() then
         repeat
             TotalAmount += SalesLine."Line Amount";
         until SalesLine.Next() = 0;
     until Customer.Next() = 0;
     exit(TotalAmount);
 end;
}

这里,我们有以下函数:

  • CreateDefaultCategory:此方法在Customer Category表格中创建一个带有预定义代码且Default标志设置为true的条目。

  • AssignDefaultCategory:此方法将默认类别分配给客户。这里我们使用了重载(AL 中支持),并且有两个不同实现的相同函数(一个没有参数,一个带有Code[20]参数):

    • AssignDefaultCategory(CustomerCode: Code[20]):仅适用于当前客户

    • AssignDefaultCategory():适用于所有客户

  • GetTotalCustomersWithoutCategory:返回未分配类别的客户数量。

  • GetSalesAmount:返回所选Customer Category的销售订单总金额。

完成此操作后,我们将继续实现礼品促销的业务需求。

礼品促销实现

为了处理礼品促销要求,我们需要执行以下操作:

  • 定义Gift Campaign表格。该表格必须能够存储以下数据:
Customer Category Item Start Date End Date Minimum Quantity Ordered Gift Quantity
GOLD ITEM1 01/01/2019 30/03/2019 5 1
GOLD ITEM2 01/01/2019 30/03/2019 10 2
SILVER ITEM1 01/01/2019 30/03/2019 7 1
  • 创建处理礼品促销数据的页面(列表页面)。

  • 处理销售订单分配礼品的业务逻辑基于Customer Category和该类别的当前活动促销。这将通过外部代码单元完成。

  • Sales Order页面界面中添加一个新功能,以便销售操作员在销售订单完成时自动插入礼品行。

  • 当销售操作员在销售订单行中插入Quantity时,我们希望检查活动促销,并在订购数量接近活动促销时提醒用户。

表定义

通过使用ttable片段,我们定义Gift Campaign表格,如下所示:

table 50101 "GiftCampaign_PKT"
{
 DataClassification = CustomerContent;
 DrillDownPageId = "Gift Campaign List_PKT";
 LookupPageId = "Gift Campaign List_PKT";
 Caption = 'Gift Campaign';

 fields
 {
     field(1; CustomerCategoryCode; Code[20])
     {
         DataClassification = CustomerContent;
         TableRelation = "Customer Category_PKT";
         Caption = 'Customer Category Code';
         trigger OnValidate()
         var
             CustomerCategory: Record "Customer Category_PKT";
             ErrNoGifts: Label 'This category is not enabled for Gift Campaigns.';
             ErrBlocked: Label 'This category is blocked.';
         begin
             CustomerCategory.Get(CustomerCategoryCode);
             if CustomerCategory.Blocked then
                 Error(ErrBlocked);
             if not CustomerCategory.FreeGiftsAvailable then
                 Error(ErrNoGifts);
         end;
     }
     field(2; ItemNo; Code[20])
     {
         DataClassification = CustomerContent;
         TableRelation = Item;
         Caption = 'Item No.';
     }
     field(3; StartingDate; Date)
     {
         DataClassification = CustomerContent;
         Caption = 'Starting Date';
     }
     field(4; EndingDate; Date)
     {
         DataClassification = CustomerContent;
         Caption = 'Ending Date';
     }
     field(5; MinimumOrderQuantity; Decimal)
     {
         DataClassification = CustomerContent;
         Caption = 'Minimum Order Quantity';
     }
     field(6; GiftQuantity; Decimal)
     {
         DataClassification = CustomerContent;
         Caption = 'Free Gift Quantity';
     }
     field(7; Inactive; Boolean)
     {
         DataClassification = CustomerContent;
         Caption = 'Inactive';
     }
 }

 keys
 {
     key(PK; CustomerCategoryCode, ItemNo, StartingDate, EndingDate)
     {
         Clustered = true;
     }
 }
}

该表格的主键是一个复合键,定义如下:

keys
    {
        key(PK; CustomerCategoryCode, ItemNo, StartingDate, EndingDate)
        {
            Clustered = true;
        }
    }

在这里,我们处理了CustomerCategoryCode字段的OnValidate触发器,该触发器执行一些验证:

  • 如果所选Customer Category被阻止,则会抛出错误。

  • 如果所选Customer Category不适用于礼品促销(FreeGiftsAvailable = false),则会抛出错误。

页面定义

通过使用tpage片段,我们定义Gift Campaign List页面,如下所示:

page 50103 "Gift Campaign List_PKT"
{ 
     PageType = List;
     SourceTable = GiftCampaign_PKT;
     UsageCategory = Lists;
     Caption = 'Gift Campaigns';
     ApplicationArea = All;
     AdditionalSearchTerms = 'promotions, marketing';

     layout
     {
         area(content)
         {
             repeater(Group)
             {
                 field(CustomerCategoryCode; CustomerCategoryCode)
                 {
                     ApplicationArea = All;
                 }
                 field(ItemNo; ItemNo)
                 {
                     ApplicationArea = All;
                 }
                 field(StartingDate; StartingDate)
                 {
                     ApplicationArea = All;
                 }
                 field(EndingDate; EndingDate)
                 {
                     ApplicationArea = All;
                 }
                 field(MinimumOrderQuantity; MinimumOrderQuantity)
                 {
                     ApplicationArea = All;
                     Style = Strong;
                 }
                 field(GiftQuantity; GiftQuantity)
                 {
                     ApplicationArea = All;
                     Style = Strong;
                 }
                 field(Inactive; Inactive)
                 {
                     ApplicationArea = All;
                 }
             }
         }
     }

     views
     {
         view(ActiveCampaigns)
         {
             Caption = 'Active Gift Campaigns';
             Filters = where (Inactive = const (false));
         }
         view(InactiveCampaigns)
         {
             Caption = 'Inactive Gift Campaigns';
             Filters = where (Inactive = const (true));
         }
     }
}

发布后,页面显示如下:

为了在销售订单中处理礼品分配逻辑,我们通过创建一个pageextension对象,在Sales Order页面上添加了一个新操作,并通过此操作调用下一节中在代码单元中定义的AddGifts方法。

pageextension对象定义如下:

pageextension 50100 SalesOrderExt_PKT extends "Sales Order"
{
     actions
     {
         addlast(Processing)
         {
             action(AddFreeGifts)
             {
                 Caption = 'Add Free Gifts';
                 ToolTip = 'Adds Free Gifts to the current Sales Order based on active Campaigns';
                 ApplicationArea = All;
                 Image = Add;
                 Promoted = true;
                 PromotedCategory = Process;
                 PromotedIsBig = true;
                 trigger OnAction()
                 begin
                     GiftManagement.AddGifts(Rec);
                 end;
             }
         }
     }

     var
         GiftManagement: Codeunit GiftManagement_PKT;
}

带有新操作的Sales Order页面现在如下所示:

代码单元定义

所有处理这些需求的业务逻辑都在GiftManagement代码单元中定义,具体如下:

codeunit 50101 "GiftManagement_PKT"
{
 procedure AddGifts(var SalesHeader: Record "Sales Header")
 var
     SalesLine: record "Sales Line";
 Handled: Boolean;
 begin
     SalesLine.SetRange("Document Type", SalesHeader."Document Type");
     SalesLine.SetRange("Document No.", SalesHeader."No.");
     SalesLine.SetRange(Type, SalesLine.Type::Item);
     //We exclude the generated gifts lines in order to avoid loops
     SalesLine.SetFilter("Line Discount %", '<>100'); 
     if SalesLine.FindSet() then
     repeat
         //Integration event raised
         OnBeforeFreeGiftSalesLineAdded(SalesLine, Handled);
         AddFreeGiftSalesLine(SalesLine, Handled);
         //Integration Event raised
         OnAfterFreeGiftSalesLineAdded(SalesLine);
     until SalesLine.Next() = 0;
 end;

 local procedure AddFreeGiftSalesLine(var SalesLine: Record "Sales Line"; var Handled: Boolean)
 var
     GiftCampaign: Record GiftCampaign_PKT;
     SalesHeader: record "Sales Header";
     Customer: Record Customer;
     SalesLineGift: Record "Sales Line";
     LineNo: Integer;
 begin
     if Handled then
         exit;
     SalesHeader.Get(SalesLine."Document Type", SalesLine."Document No.");
     Customer.Get(SalesLine."Sell-to Customer No.");
     GiftCampaign.SetRange(CustomerCategoryCode, Customer."Customer Category Code_PKT");
     GiftCampaign.SetRange(ItemNo, SalesLine."No.");
     GiftCampaign.SetFilter(StartingDate, '<=%1', SalesHeader."Order Date");
     GiftCampaign.SetFilter(EndingDate, '>=%1', SalesHeader."Order Date");
     GiftCampaign.SetRange(Inactive, false);
     GiftCampaign.SetFilter(MinimumOrderQuantity, '<= %1', SalesLine.Quantity);
     if GiftCampaign.FindFirst() then begin
         //Active promo found. We need to insert a new Sales Line
         LineNo := GetLastSalesDocumentLineNo(SalesHeader);
         SalesLineGift.init;
         SalesLineGift.TransferFields(SalesLine);
         SalesLineGift."Line No." := LineNo + 10000;
         SalesLineGift.Validate(Quantity, GiftCampaign.GiftQuantity);
         SalesLineGift.Validate("Line Discount %", 100);
         if SalesLineGift.Insert() then;
     end;
 end;

 local procedure GetLastSalesDocumentLineNo(SalesHeader: Record "Sales Header"): Integer
 var 
     SalesLine: Record "Sales Line";
 begin
     SalesLine.SetRange("Document Type", SalesHeader."Document Type");
     SalesLine.SetRange("Document No.", SalesHeader."No.");
     if SalesLine.FindLast() then
         exit(SalesLine."Line No.")
     else
         exit(0);
 end;

 [EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterValidateEvent', 'Quantity', false, false)]
 local procedure CheckGiftEligibility(var Rec: Record "Sales Line")
 var
     GiftCampaign: Record GiftCampaign_PKT;
     Customer: Record Customer;
     SalesHeader: Record "Sales Header";
     Handled: Boolean;
 begin
     if (Rec.Type = Rec.Type::Item) and (Customer.Get(Rec."Sell-to Customer No.")) then begin
     SalesHeader.Get(Rec."Document Type", Rec."Document No.");
     GiftCampaign.SetRange(CustomerCategoryCode, Customer."Customer Category Code_PKT");
     GiftCampaign.SetRange(ItemNo, Rec."No.");
     GiftCampaign.SetFilter(StartingDate, '<=%1', SalesHeader."Order Date");
     GiftCampaign.SetFilter(EndingDate, '>=%1', SalesHeader."Order Date");
     GiftCampaign.SetRange(Inactive, false); GiftCampaign.SetFilter(MinimumOrderQuantity, '> %1',         Rec.Quantity);
     if GiftCampaign.FindFirst() then begin
         //Integration event raised
         OnBeforeFreeGiftAlert(Rec, Handled);
         DoGiftCheck(Rec, GiftCampaign, Handled);
         //Integration Event raised
         OnAfterFreeGiftAlert(Rec);
     end;
 end;
 end;

 local procedure DoGiftCheck(var SalesLine: Record "Sales Line"; var GiftCampaign: Record GiftCampaign_PKT; var Handled: Boolean)
 var
     PacktSetup: record "Packt Extension Setup";
     GiftAlert: Label 'Attention: there is an active promotion for item %1\. if you buy %2 you can have a gift of %3';
 begin
     if Handled then
         exit;
     PacktSetup.Get();
     if (SalesLine.Quantity < GiftCampaign.MinimumOrderQuantity) and (GiftCampaign.MinimumOrderQuantity - SalesLine.Quantity <= PacktSetup."Gift Tolerance Qty") then
         Message(GiftAlert, SalesLine."No.", Format(GiftCampaign.MinimumOrderQuantity), Format(GiftCampaign.GiftQuantity));
     end;

在这里,我们有一些程序、一些事件订阅者和一些事件发布者。主程序叫做AddGifts,它将礼品行(促销)添加到传入的销售订单中。它触发了一些集成事件,主代码由AddFreeGiftSalesLine程序处理。

此代码单元中定义的集成事件如下:

[IntegrationEvent(true, false)]
 local procedure OnBeforeFreeGiftSalesLineAdded(var Rec: Record "Sales Line"; var Handled: Boolean)
 begin
 end;

 [IntegrationEvent(true, false)]
 local procedure OnAfterFreeGiftSalesLineAdded(var Rec: Record "Sales Line")
 begin
 end;

 [IntegrationEvent(true, false)]
 local procedure OnBeforeFreeGiftAlert(var Rec: Record "Sales Line"; var Handled: Boolean)
 begin
 end;

 [IntegrationEvent(true, false)]
 local procedure OnAfterFreeGiftAlert(var Rec: Record "Sales Line")
 begin
 end;

 [EventSubscriber(ObjectType::Table, Database::"Item Ledger Entry", 'OnAfterInsertEvent', '', false, false)]
 local procedure OnAfterItemLedgerEntryInsert(var Rec: Record "Item Ledger Entry")
 var
     Customer: Record Customer;
 begin
     if rec."Entry Type" = rec."Entry Type"::Sale then begin
         if Customer.Get(Rec."Source No.") then begin
             Rec."Customer Category Code_PKT" := Customer."Customer Category Code_PKT";
             Rec.Modify();
          end;
     end;
 end;
}

在这里,我们实现了Handled模式(以保证可扩展性)。通过这种方式,依赖的扩展可以根据需要修改礼品分配逻辑,而无需修改主扩展的基础代码。

Handled模式的实现如下:

OnBeforeFreeGiftSalesLineAdded(SalesLine, Handled);
AddFreeGiftSalesLine(SalesLine, Handled);
OnAfterFreeGiftSalesLineAdded(SalesLine);

在这段代码中,我们有以下内容:

  • 我们有一个全局变量Handled,它的初始值为false

  • 我们通过传递我们正在处理的销售行和Handled变量来触发一个名为OnBeforeFreeGiftSalesLineAdded的集成事件。

  • 我们在一个名为AddFreeGiftSalesLine的程序中实现了业务逻辑。在这个程序中,如果事件被处理,我们就跳过标准逻辑:

if Handled then
   exit;
  • 在过程结束时,我们触发一个名为OnAfterFreeGiftSalesLineAdded的集成事件。

那么,为什么这个模式能保证可扩展性呢?这是因为在依赖的扩展中,你可以订阅OnBeforeFreeGiftSalesLineAdded事件,并将Handled变量设置为true,然后实现你自己的添加礼品的业务逻辑。然后,标准的业务逻辑(AddFreeGiftSalesLine)将被跳过。

在此之后,你可以订阅OnAfterFreeGiftSalesLineAdded事件,并实现必须在添加礼品过程后执行的其他自定义业务逻辑。我们将在本章的理解依赖扩展部分看到一个依赖扩展如何改变我们扩展的标准业务逻辑。

在这个代码单元中,我们还创建了一个名为CheckGiftEligibility的程序,它是Sales Line表的Quantity字段的OnAfterValidateEvent事件的事件订阅者。以下代码展示了这一点:

[EventSubscriber(ObjectType::Table, Database::"Sales Line", 'OnAfterValidateEvent', 'Quantity', false, false)]
    local procedure CheckGiftEligibility(var Rec: Record "Sales Line")

在这个函数中,我们处理了销售操作员在销售行中插入数量时必须触发的警告的业务逻辑。如你所见,在前面的代码中,我们再次实现了Handled模式以提供可扩展性。

在这个代码单元中,我们还处理了Item Ledger Entry表的OnAfterInsertEvent事件的事件订阅者,将Customer Category数据传递到Item Ledger Entry字段中(这是要求的功能之一):

[EventSubscriber(ObjectType::Table, Database::"Item Ledger Entry", 'OnAfterInsertEvent', '', false, false)]
    local procedure OnAfterItemLedgerEntryInsert(var Rec: Record "Item Ledger Entry")

当你从销售订单中触发“添加免费赠品”操作时,会发生什么?请参考以下截图:

我们可以看到事件被触发,AddGifts函数被执行,并且赠品促销(如果有的话)被插入到Sales Line表中(新的一行,LINE DISCOUNT %字段值设置为100)。

我们现在已经实现了管理客户赠品活动所需的所有业务需求。接下来,让我们进入供应商质量实现的详细内容。

供应商质量实现

为了处理供应商质量管理需求,我们需要执行以下操作:

  • 定义一个Vendor Quality表(与标准的Vendor表相关),该表将包含有关供应商的质量得分和质量相关的财务数据的详细信息。

  • 定义相关的卡片页面并将其附加到供应商卡片(这将是根据我们的需求用于供应商的质量详细卡片)。

  • 向标准的Vendor card页面添加一个新动作,以打开Vendor Quality card

  • 定义一个处理所有与此实现相关的业务逻辑的代码单元

在接下来的部分中,我们将详细查看各种对象的实现。

表定义

通过使用ttable代码片段,我们定义了Vendor Quality表,具体如下:

table 50102 "Vendor Quality_PKT"
{
 Caption = 'Vendor Quality';
 DataClassification = CustomerContent;

 fields
 {
     field(1; "Vendor No."; Code[20])
     {
         Caption = 'Vendor No.';
         DataClassification = CustomerContent;
         TableRelation = Vendor;
     }
     field(2; "Vendor Name"; Text[50])
     {
         Caption = 'Vendor Name';
         FieldClass = FlowField;
         CalcFormula = lookup (Vendor.Name where ("No." = field ("Vendor No.")));
     }
     field(3; "Vendor Activity Description"; Text[250])
     {
         Caption = 'Vendor Activity Description';
         DataClassification = CustomerContent;
     }
     field(4; ScoreItemQuality; Integer)
     {
         Caption = 'Item Quality Score';
         DataClassification = CustomerContent;
         MinValue = 1;
         MaxValue = 10;
         trigger OnValidate()
         begin
             UpdateVendorRate();
         end;
     }
     field(5; ScoreDelivery; Integer)
     {
         Caption = 'Delivery On Time Score';
         DataClassification = CustomerContent;
         MinValue = 1;
         MaxValue = 10;
         trigger OnValidate()
         begin
             UpdateVendorRate();
         end;
     }
     field(6; ScorePackaging; Integer)
     {
         Caption = 'Packaging Score';
         DataClassification = CustomerContent;
         MinValue = 1;
         MaxValue = 10;
         trigger OnValidate()
         begin
             UpdateVendorRate();
         end;
         }
     field(7; ScorePricing; Integer)
     {
         Caption = 'Pricing Score';
         DataClassification = CustomerContent;
         MinValue = 1;
         MaxValue = 10;
         trigger OnValidate()
         begin
             UpdateVendorRate();
         end;
     }
     field(8; Rate; Decimal)
     {
         Caption = 'Vendor Rate';
         DataClassification = CustomerContent;
     }
     field(10; UpdateDate; DateTime)
     {
         Caption = 'Update Date';
         DataClassification = CustomerContent;
     }
     field(11; InvoicedYearN; Decimal)
     {
         Caption = 'Invoiced for current year (N)';
         DataClassification = CustomerContent;
     }
     field(12; InvoicedYearN1; Decimal)
     {
         Caption = 'Invoiced for year N-1';
         DataClassification = CustomerContent;
     }
     field(13; InvoicedYearN2; Decimal)
     {
         Caption = 'Invoiced for year N-2';
         DataClassification = CustomerContent;
     }
     field(14; DueAmount; Decimal)
     {
         Caption = 'Due Amount';
         DataClassification = CustomerContent;
     }
     field(15; AmountNotDue; Decimal)
     {
         Caption = 'Amount to pay (not due)';
         DataClassification = CustomerContent;
     }
 }

 keys
 {
     key(PK; "Vendor No.")
     {
         Clustered = true;
     }
 }

 trigger OnInsert()
 begin
     UpdateDate := CurrentDateTime();
 end;
 trigger OnModify()
 begin
     UpdateDate := CurrentDateTime();
 end;
 local procedure UpdateVendorRate()
 var
     VendorQualityMgt: Codeunit VendorQualityMgt_PKT;
 begin
     VendorQualityMgt.CalculateVendorRate(Rec);
 end;
}

在此表中,我们定义了所需的得分字段(评分)和所需的财务字段。对于评分率,我们处理了OnValidate触发器,以便在用户插入字段值时动态更新率的计算(这是通过调用在表中定义的UpdateVendorRate函数来完成的,该函数作为类方法定义,但在稍后会看到的外部代码单元中实现)。

我们还处理了表的OnInsertOnModify触发器,以保存记录的插入或修改日期(业务需求)。

页面定义

对于我们的业务需求,我们需要创建一个Vendor Quality card页面。我们通过使用tpage代码片段创建一个新的Card类型页面,具体如下:

page 50102 "Vendor Quality Card_PKT"
{
     PageType = Card;
     ApplicationArea = All;
     UsageCategory = Administration;
     SourceTable = "Vendor Quality_PKT";
     Caption = 'Vendor Quality Card';
     InsertAllowed = false;

     layout
    {
         area(Content)
         {
             group(General)
             {
                 Caption = 'General';
                 field("Vendor No."; "Vendor No.")
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
                 field("Vendor Name"; "Vendor Name")
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
                 field("Vendor Activity Description"; "Vendor Activity Description")
                 {
                     ApplicationArea = All;
                 }
                 field(Rate; Rate)
                 {
                     ApplicationArea = All;
                     Editable = false;
                     Style = Strong;
                 }
                 field(UpdateDate; UpdateDate)
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
             }
             group(Scoring)
             {
                 Caption = 'Score';
                 field(ScoreItemQuality; ScoreItemQuality)
                 {
                     ApplicationArea = All;
                 }
                 field(ScoreDelivery; ScoreDelivery)
                 {
                     ApplicationArea = All;
                 }
                 field(ScorePackaging; ScorePackaging)
                 {
                     ApplicationArea = All;
                 }
                 field(ScorePricing; ScorePricing)
                 {
                     ApplicationArea = All;
                 }
             }
             group(Financials)
             {
                 Caption = 'Financials';
                 field(InvoicedYearN; InvoicedYearN)
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
                 field(InvoicedYearN1; InvoicedYearN1)
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
                 field(InvoicedYearN2; InvoicedYearN2)
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
                 field(DueAmount; DueAmount)
                 {
                     ApplicationArea = All;
                     Editable = false;
                     Style = Attention;
                 }
                 field(AmountNotDue; AmountNotDue)
                 {
                     ApplicationArea = All;
                     Editable = false;
                 }
             }
         }
     }

     trigger OnOpenPage()
     begin
         if not Insert() then;
     end;

     trigger OnAfterGetRecord()
     var
         VendorQualityMgt: Codeunit VendorQualityMgt_PKT;
     begin
         VendorQualityMgt.UpdateVendorQualityStatistics(Rec);
     end;
}

该页面通过创建不同的组(UI 中的FastTabs)来设计:

  • General:包含所选供应商的一般质量分类,例如名称、活动描述和计算率

  • Scoring:包含质量得分(由公司质量经理分配)

  • Financials:包含质量要求所需的财务数据

此页面的InsertAllowed属性设置为true,因为当从Vendor card打开页面时,记录会自动插入(我们在这里处理OnOpenPage触发器),并且用户不能直接从此页面插入新记录。

我们还处理了OnAfterGetRecord页面触发器,从这里调用一个刷新财务统计信息的函数。

页面扩展定义

我们需要一个pageextension对象,以便从标准的Vendor Card页面打开之前创建的Vendor Quality card页面。通过使用tpageext代码片段,我们创建了以下对象:

pageextension 50101 VendorCardExt_PKT extends "Vendor Card"
{
    actions
    {
        addafter("Comments")
        {
            action(QualityClassification)
            {
                Caption = 'Quality Classification';
                ApplicationArea = All;
                Image = QualificationOverview;
                Promoted = true;
                PromotedCategory = Process;
                PromotedIsBig = true;
                RunObject = Page "Vendor Quality Card_PKT";
                RunPageLink = "Vendor No." = field ("No.");
            }
        }
    }
}

在这里,我们定义了一个QualityClassification操作,它会为所选的Vendor记录打开Vendor Quality card页面(通过使用RunPageLink属性)。

页面操作显示如下:

当触发该操作时,Vendor Quality Card页面会被打开,显示如下:

当质量经理输入分数值时,Vendor Rate值会自动计算。财务统计数据在打开页面时会自动计算(实时计算)。

代码单元定义

如同往常一样,我们在一个名为VendorQualityMgt的外部代码单元中定义了所有业务逻辑,使用了如下的tcodeunit片段:

codeunit 50102 VendorQualityMgt_PKT
{
    procedure CalculateVendorRate(var VendorQuality: Record "Vendor Quality_PKT")
    var
        Handled: Boolean;
    begin
        OnBeforeCalculateVendorRate(VendorQuality, Handled);
        //This is the company's criteria to assign the Vendor rate.
        VendorRateCalculation(VendorQuality, Handled);
        OnAfterCalculateVendorRate(VendorQuality);
    end;

    local procedure VendorRateCalculation(var VendorQuality: Record "Vendor Quality_PKT"; var         Handled: Boolean)
    begin
        if Handled then
            exit;
        VendorQuality.Rate := (VendorQuality.ScoreDelivery + VendorQuality.ScoreItemQuality +
        VendorQuality.ScorePackaging + VendorQuality.ScorePricing) / 4;
    end;

    procedure UpdateVendorQualityStatistics(var VendorQuality: Record "Vendor Quality_PKT")
    var
        Year: Integer;
        DW: Dialog;
        DialogMessage: Label 'Calculating Vendor statistics...';
    begin
        DW.OPEN(DialogMessage);
        Year := DATE2DMY(TODAY, 3);
        VendorQuality.InvoicedYearN := GetInvoicedAmount(VendorQuality."Vendor No.", DMY2DATE(1, 1,                 Year), TODAY);
        VendorQuality.InvoicedYearN1 := GetInvoicedAmount(VendorQuality."Vendor No.", DMY2DATE(1, 1,             Year - 1), DMY2DATE(31, 12, Year - 1));
        VendorQuality.InvoicedYearN2 := GetInvoicedAmount(VendorQuality."Vendor No.", DMY2DATE(1, 1,             Year - 2), DMY2DATE(31, 12, Year - 2));
        VendorQuality.DueAmount := GetDueAmount(VendorQuality."Vendor No.", TRUE);
        VendorQuality.AmountNotDue := GetDueAmount(VendorQuality."Vendor No.", FALSE);
        DW.CLOSE;
    end;

    local procedure GetInvoicedAmount(VendorNo: Code[20]; StartDate: Date; EndDate: Date): Decimal
    var
        VendorLedgerEntry: Record "Vendor Ledger Entry";
        Total: Decimal;
    begin
        VendorLedgerEntry.SETRANGE("Vendor No.", VendorNo);
        VendorLedgerEntry.SETFILTER("Document Date", '%1..%2', StartDate, EndDate);
        if VendorLedgerEntry.FINDSET then
        repeat
            Total += VendorLedgerEntry."Purchase (LCY)";
        until VendorLedgerEntry.NEXT = 0;
        exit(Total * (-1));
    end;

    local procedure GetDueAmount(VendorNo: Code[20]; Due: Boolean): Decimal
    var
        VendorLedgerEntry: Record "Vendor Ledger Entry";
        Total: Decimal;
    begin
        VendorLedgerEntry.SETRANGE("Vendor No.", VendorNo);
        VendorLedgerEntry.SETRANGE(Open, TRUE);
        if Due then
            VendorLedgerEntry.SETFILTER("Due Date", '< %1', TODAY)
        else
            VendorLedgerEntry.SETFILTER("Due Date", '> %1', TODAY);
        VendorLedgerEntry.SETAUTOCALCFIELDS(VendorLedgerEntry."Remaining Amt. (LCY)");
        if VendorLedgerEntry.FINDSET then
        repeat
            Total += VendorLedgerEntry."Remaining Amt. (LCY)";
        until VendorLedgerEntry.NEXT = 0;
        exit(Total * (-1));
    end;

}

在这里,我们定义了以下函数:

  • CalculateVendorRate:这是根据质量经理分配的质量分数来计算供应商费率的函数。我们希望这个函数是可扩展的,以便在未来需要时可以更改标准费率算法。为此,我们使用了 HANDLE 模式:

    • 我们使用当前的Vendor Quality记录和Handled布尔变量作为事件参数,触发OnBeforeCalculateVendorRate事件。

    • 我们通过检查Handled参数,在VendorRateCalculation函数中执行标准费率计算,并在需要跳过标准计算时退出该函数(通过设置Handled = true)。

    • 我们触发OnAfterCalculateVendorRate事件,用于处理计算后的操作或完全重新计算。

  • UpdateVendorQualityStatistics:此函数计算质量经理所需的财务统计数据:

    • GetInvoicedAmount:对于给定的Vendor No.字段和日期区间(开始/结束日期),通过检查Vendor Ledger Entry表中的Purchase (LCY)字段,计算发票金额。返回的结果是-1,因为我们需要绝对值。

    • GetDueAmount:对于给定的Vendor No.字段,它通过检查Vendor Ledger Entry表中的Remaining Amt. (LCY)字段,计算应付金额(Due参数设置为true)或待支付金额(Due参数设置为false)。返回的结果会乘以-1,因为我们需要绝对值。

代码单元的事件定义如下:

[IntegrationEvent(true, false)]
    local procedure OnBeforeCalculateVendorRate(var VendorQuality: Record "Vendor Quality_PKT"; var         Handled: Boolean)
    begin
    end;

    [IntegrationEvent(true, false)]
    local procedure OnAfterCalculateVendorRate(var VendorQuality: Record "Vendor Quality_PKT")
    begin
    end;

在这个代码单元中,我们还为 Microsoft 的Release Purchase Document代码单元中定义的OnBeforeManualReleasePurchaseDoc标准事件定义了一个事件订阅者(使用teventsub片段),定义如下:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Release Purchase Document", 'OnBeforeManualReleasePurchaseDoc', '', false, false)]

我们使用此事件,在订单释放阶段如果供应商未达到公司在扩展设置表中定义的费率标准(即最低可接受费率),则抛出错误。

事件订阅者的实现如下:

[EventSubscriber(ObjectType::Codeunit, Codeunit::"Release Purchase Document",         'OnBeforeManualReleasePurchaseDoc', '', false, false)]
    local procedure QualityCheckForReleasingPurchaseDoc(var PurchaseHeader: Record "Purchase                 Header")
    var
        VendorQuality: Record "Vendor Quality_PKT";
        PacktSetup: Record "Packt Extension Setup";
        ErrNoMinimumRate: Label 'Vendor %1 has a rate of %2 and it''s under the required minimum                 value (%3)';
    begin
        PacktSetup.Get();
        if VendorQuality.Get(PurchaseHeader."Buy-from Vendor No.") then begin
            if VendorQuality.Rate < PacktSetup."Minimum Accepted Vendor Rate" then
                Error(ErrNoMinimumRate, PurchaseHeader."Buy-from Vendor No.",
                Format(VendorQuality.Rate), Format(PacktSetup."Minimum Accepted Vendor Rate"));
        end;
    end;

现在,所有客户的业务需求都由我们的扩展处理。

在下一节中,我们将看到如何通过在 Dynamics 365 Business Central 中创建定制页面视图来增强客户的用户体验。

创建页面视图

自 2019 年秋季发布以来,您可以为列表页面创建定制视图。这些定制视图可以在 Dynamics 365 Business Central 用户界面的专用部分中使用,以立即对列表应用过滤器。

您可以使用tview片段在页面对象中创建视图定义。在先前创建的Gift Campaign List页面中,我们定义了以下view对象:

views
{
    view(ActiveCampaigns)
    {
        Caption = 'Active Gift Campaigns';
        Filters = where (Inactive = const (false));
    }
    view(InactiveCampaigns)
    {
        Caption = 'Inactive Gift Campaigns';
        Filters = where (Inactive = const (true));
    }
}

第一个视图(称为ActiveCampaigns)显示所有活动的礼品活动(Inactive字段设置为false),而第二个视图(称为InactiveCampaigns)显示所有非活动的礼品活动(Inactive字段设置为true)。

这些在 Dynamics 365 Business Central 用户界面中的视图看起来像这样:

如果选择活动礼品活动视图,则列表会相应地进行过滤(Inactive设置为false):

如果选择Inactive Gift Campaigns视图,则列表会根据Inactive设置为true自动进行过滤:

我们还在Customer List页面上添加了一个视图,显示所有没有关联类别的客户。

Customer List pageextension对象中的视图定义如下:

views
{
    addlast
    {
        view(CustomersWithoutCategory)
        {
            Caption = 'Customers without Category assigned';
            Filters = where ("Customer Category_PKT" = filter (''));
        }
    }
}

我们已将此新创建的视图放置为页面上可用视图的最后一个:

如前面的屏幕截图所示,此视图显示在应用程序的用户界面中,选择后会自动按照没有关联类别的所有客户进行过滤(因此Customer Category = Blank)。通过这种方式,我们的客户可以通过选择预定义视图(仅需点击)立即按需重新插入所需的过滤器。

接下来,让我们继续看看如何安装和升级代码单元。

安装和升级代码单元

创建扩展时,需要检查某些条件以确保安装成功,或者需要初始化某些设置表或预填充其他表。为此,您需要创建Install codeunit

扩展的安装逻辑必须在具有设置为InstallSubType属性的代码单元中编写。当以下条件为真时,将触发此逻辑:

  • 您正在第一次安装扩展。

  • 您已卸载扩展,然后再次安装它。

一个Install代码单元支持以下系统触发器:

  • OnInstallAppPerCompany(): 此触发器中的代码对 Dynamics 365 Business Central 数据库中的每个公司运行一次。

  • OnInstallAppPerDatabase(): 此触发器中的代码在整个安装过程中运行一次。

在升级扩展时,会发生相同的逻辑。如果你需要创建一个新的扩展版本(app.json文件中的版本号必须大于之前的版本号),且该版本涉及对旧版本数据的修改,你需要创建Upgrade codeunit

扩展的升级逻辑必须编写在SubType属性设置为Upgrade的 codeunit 中。

Upgrade codeunit 支持以下系统触发器:

  • OnCheckPreconditionsPerCompany():此触发器中的代码用于检查升级过程的前提条件。此代码会在数据库中的每个公司中运行一次。

  • OnCheckPreconditionsPerDatabase():此触发器中的代码用于检查升级过程的前提条件。此代码会在整个升级过程中运行一次。

  • OnUpgradePerCompany():此触发器中的代码包含升级逻辑。此代码会在数据库中的每个公司中运行一次。

  • OnUpgradePerDatabase():此触发器中的代码包含升级逻辑。此代码会在整个升级过程中运行一次。

  • OnValidateUpgradePerCompany():此触发器中的代码用于检查升级过程的结果。此代码会在数据库中的每个公司中运行一次。

  • OnValidateUpgradePerDatabase():此触发器中的代码用于检查升级过程的结果。此代码会在整个升级过程中运行一次。

对于我们的扩展,我们已创建如下的Install codeunit:

codeunit 50105 CustomerCategoryInstall_PKT
{
    Subtype = Install;
    trigger OnInstallAppPerCompany();
    var
        archivedVersion: Text;
        CustomerCategory: Record "Customer Category_PKT";
        PacktSetup: Record "Packt Extension Setup";
    begin
        archivedVersion := NavApp.GetArchiveVersion;
        if archivedVersion = '1.0.0.0' then begin
            NavApp.RestoreArchiveData(Database::"Customer Category_PKT");
            NavApp.RestoreArchiveData(Database::Customer);
            NavApp.RestoreArchiveData(Database::"Packt Extension Setup");
            NavApp.RestoreArchiveData(Database::GiftCampaign_PKT);
            NavApp.RestoreArchiveData(Database::"Vendor Quality_PKT");
            NavApp.DeleteArchiveData(Database::"Customer Category_PKT");
            NavApp.DeleteArchiveData(Database::Customer);
            NavApp.DeleteArchiveData(Database::"Packt Extension Setup");
            NavApp.DeleteArchiveData(Database::GiftCampaign_PKT);
            NavApp.DeleteArchiveData(Database::"Vendor Quality_PKT");
        end;
        if CustomerCategory.IsEmpty() then
            InsertDefaultCustomerCategory();
        if PacktSetup.IsEmpty() then
            InsertDefaultSetup();
    end;

    // Insert the GOLD, SILVER, BRONZE reward levels
    local procedure InsertDefaultCustomerCategory();
    begin
        InsertCustomerCategory('TOP', 'Top Customer', false);
        InsertCustomerCategory('MEDIUM', 'Standard Customer', true);
        InsertCustomerCategory('BAD', 'Bad Customer', false);
    end;

    // Create and insert a Customer Category record
    local procedure InsertCustomerCategory(ID: Code[30]; Description: Text[250]; Default: Boolean);
    var
        CustomerCategory: Record "Customer Category_PKT";
    begin
        CustomerCategory.Init();
        CustomerCategory.Code := ID;
        CustomerCategory.Description := Description;
        CustomerCategory.Default := Default;
        CustomerCategory.Insert();
    end;

    local procedure InsertDefaultSetup()
    var
        PacktSetup: Record "Packt Extension Setup";
    begin
        PacktSetup.Init();
        PacktSetup."Minimum Accepted Vendor Rate" := 6;
        PacktSetup."Gift Tolerance Qty" := 2;
        PacktSetup.Insert();
    end;
}

OnInstallAppPerCompany触发器中,我们检查是否存在已归档的扩展版本(如果有人卸载了扩展,可能会发生这种情况):

archivedVersion := NavApp.GetArchiveVersion;

如果是这样,我们将从NavApp系统表中恢复已归档的数据(这样用户就可以自动恢复其旧数据)。

如果没有恢复的内容,我们将初始化Customer Category表和扩展的设置表(Packt Extension Setup表)并填充默认数据。

有关InstallUpgrade codeunit 的更多信息,请参阅本书的第九章,《调试》一章,以及以下链接:

docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-extension-install-codedocs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-upgrading-extensions.

当从 Visual Studio Code 在 Dynamics 365 Business Central 中发布时,我们的扩展会显示为已安装状态,在 EXTENSION MANAGEMENT 页面上:

我们现在已经学习了在发布扩展到 Dynamics 365 Business Central 时,如何处理安装和升级操作。

在下一节中,我们将探讨依赖扩展的概念,并学习如何使用依赖项对我们先前部署的应用程序进行定制。

理解依赖扩展

在前面的章节中,我们已经开发了扩展并进行了部署。

现在想象一下,您已经将这个扩展部署到客户租户,并且客户要求您进行一些定制:

  • 他们希望向 Vendor Quality 表中添加Certification No.字段。

  • 他们希望更改赠品分配逻辑,总是分配固定的赠品数量为 2。

为了为客户创建定制内容,你永远不要直接修改标准扩展代码,而应创建一个新的扩展,这个新扩展将是依赖于基础扩展的。

为此,我们在 Visual Studio Code 中创建一个名为PacktDemoDependencyExtension的新扩展项目。这个新扩展必须依赖于我们之前创建的PacktDemoExtension,否则我们将无法看到在该扩展中定义的对象。

首先,我们需要获取基础扩展的appIdnamepublisherversion。然后,我们需要打开新扩展的app.json文件,进入dependencies块,并按如下方式插入依赖扩展的详细信息:

"dependencies": [
    {
      "appId": "63ca2fa4-4f03-4f2b-a480-172fef340d3f",
      "publisher": "Microsoft",
      "name": "System Application",
      "version": "1.0.0.0"
    },
    {
      "appId": "437dbf0e-84ff-417a-965d-ed2bb9650972",
      "publisher": "Microsoft",
      "name": "Base Application",
      "version": "15.0.0.0"
    },
    {
 "appId": "dd03d28e-4dfe-48d9-9520-c875595362b6", "name": "PacktDemoExtension", "publisher": "SD", "version": "1.0.0.0" }  ],

现在,如果我们下载符号(AL:Download Symbols),你将看到我们依赖扩展的符号已经下载到我们项目中的.alpackages文件夹中:

我们现在准备好创建新的扩展了。

要将Certification No.字段添加到Vendor Quality Card,我们需要做如下操作:

  • 通过添加一个新字段来扩展Vendor Quality表。

  • 扩展Vendor Quality Card页面,将新字段添加到 UI 中。

我们之所以能这么做,是因为我们已经下载了符号;否则,无法看到在另一个扩展中定义的对象。

扩展Vendor Quality表的tableextension对象代码定义如下:

tableextension 50120 VendorQualityExt_PKN extends "Vendor Quality_PKT"
{
    fields
    {
        field(50120; "Certification No."; Text[50])
        {
            Caption = 'Classification No.';
            DataClassification = CustomerContent;
        }
    }
}

扩展Vendor Quality Card页面的pageextension对象定义如下:

pageextension 50120 VendorQualityCardExt_PKN extends "Vendor Quality Card_PKT"
{
    layout
    {
        addlast(General)
        {
            field("Certification No."; "Certification No.")
            {
                ApplicationArea = All;
            }
        }
    }
}

第二个要求是定制基础扩展(PacktDemoExtension*)中定义的标准业务流程,如果有活动的促销活动,则在销售订单中创建赠品行(在标准业务流程中,赠品数量是Gift Campaign表中定义的数量)。

只能在基础扩展有事件可以订阅时这样做,因为你不能直接修改另一个扩展的代码。

为了处理可扩展性,我们在基础扩展中使用了Handled模式。在PacktDemoExtension扩展中,我们定义了如下的AddGifts过程:

procedure AddGifts(var SalesHeader: record "Sales Header")
    var
        SalesLine: record "Sales Line";
        Handled: Boolean;
    begin
        SalesLine.SetRange("Document Type", SalesHeader."Document Type");
        SalesLine.SetRange("Document No.", SalesHeader."No.");
        SalesLine.SetRange(Type, SalesLine.Type::Item);
        if SalesLine.FindSet() then
            repeat
                //Integration event raised
                OnBeforeFreeGiftSalesLineAdded(SalesLine, Handled);
                AddFreeGiftSalesLine(SalesLine, Handled);
                //Integration Event raised
                OnAfterFreeGiftSalesLineAdded(SalesLine);
            until SalesLine.Next() = 0;
    end;

为了跳过标准的业务流程(AddFreeGiftSalesLine)并添加一个新的自定义礼品分配过程,我们做了以下操作:

  1. 我们订阅了OnBeforeFreeGiftSalesLineAdded事件,并将Handled参数设置为true。这样可以确保跳过标准的业务逻辑,因为在AddFreeGiftSalesLine过程的第一行,我们使用了以下代码:
if Handled then
    exit;
  1. 我们从这个事件订阅器中调用了自定义的业务逻辑。

所有这些逻辑都在一个代码单元对象中定义,如下所示:

codeunit 50120 CustomGiftLogic_PKN
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::GiftManagement_PKT,         'OnBeforeFreeGiftSalesLineAdded', '', false, false)]
    local procedure HideDefaultBehaviour(var Rec: Record "Sales Line"; var Handled: Boolean)
    begin
        Handled := true;
        //Here we create a custom gift line with a fixed quantity 
        //(override of standard behavior)
        CreateCustomGiftLine(Rec);
    end;

    local procedure CreateCustomGiftLine(var SalesLine: Record "Sales Line")
    var
        SalesHeader: Record "Sales Header";
        SalesLineGift: Record "Sales Line";
        LineNo: Integer;
        FixedQty: Decimal;
    begin
        FixedQty := 2;
        SalesHeader.Get(SalesLine."Document Type", SalesLine."Document No.");
        LineNo := GetLastSalesDocumentLineNo(SalesHeader);
        SalesLineGift.init;
        SalesLineGift.TransferFields(SalesLine);
        SalesLineGift."Line No." := LineNo + 10000;
        SalesLineGift.Validate(Quantity, FixedQty);
        SalesLineGift.Validate("Line Discount %", 100);
        if SalesLineGift.Insert() then;
    end;

    local procedure GetLastSalesDocumentLineNo(SalesHeader: Record "Sales Header"): Integer
    var
        SalesLine: Record "Sales Line";
    begin
        SalesLine.SetRange("Document Type", SalesHeader."Document Type");
        SalesLine.SetRange("Document No.", SalesHeader."No.");
        if SalesLine.FindLast() then
            exit(SalesLine."Line No.")
        else
            exit(0);
    end;
}

发布后,我们现在安装了两个扩展(标准扩展和新的自定义扩展):

为了测试我们的自定义功能是否有效,我们创建了一个新的销售订单,订单中包含一个与礼品活动关联的商品,然后我们开始了礼品分配过程:

那么,现在会发生什么呢?OnBeforeFreeGiftSalesLineAdded事件被触发,我们跳过了标准事件(Handled = true),然后触发了我们的新自定义函数(CreateCustomGiftLine):

从前面的截图中可以看到,销售订单中插入了一条数量为 2、折扣为 100%的新礼品行。

我们已经自定义了礼品活动的业务逻辑,而无需修改基础代码(我们的基础扩展),而是通过创建一个新的依赖扩展来实现。这应该是使用 Dynamics 365 Business Central 时的强制模型。

总结

在本章中,我们看到了 Dynamics 365 Business Central 的一个实际扩展的实现。我们定义了解决方案的后端(表)并创建了页面(用户界面)以及根据业务初步需求所需的业务逻辑(代码单元和事件)。我们看到了如何通过使用Handled模式使我们的代码具有扩展性,以及如何创建安装和升级代码。

在本章的最后部分,我们创建了一个新的扩展,它修改了我们基础扩展的标准行为,并且我们探讨了扩展之间的依赖关系概念。

你还学会了如何通过对象和事件创建扩展,如何使用编码规则,以及如何在不修改应用程序基础代码的情况下进行自定义。

在下一章中,我们将看到如何处理一些使用 AL 和扩展模型的高级主题,例如文件、媒体、XML 和 JSON 对象、Web 服务和异步编程。

第六章:高级 AL 开发

在上一章中,我们为 Dynamics 365 Business Central 开发了一个完整的扩展,并且在开发过程中,我们研究了 AL 编程的多个方面。

本章我们将重点讲解在为 Dynamics 365 Business Central 开发实际解决方案时,需要管理的其他开发主题。这些主题非常重要且实用,特别是在改善用户体验以及需要处理来自 AL 的外部服务集成时。

本章将涵盖以下主题:

  • 理解不可变键

  • 使用 AL 处理文件

  • 处理 BLOBs

  • 处理 XMLports

  • 使用 AL 处理 XML 和 JSON 对象

  • 创建和扩展角色中心和标题

  • 从 AL 代码中消费 Web 服务和 API

  • 从 AL 代码中使用 Azure 函数

  • 使用隔离存储处理敏感数据

  • 为 Dynamics 365 Business Central 创建控制 add-ins

  • 处理通知

  • 页面后台任务和异步编程

理解不可变键

在 Dynamics 365 Business Central wave 2 版本发布后,所有表现在都有一个(唯一的)不可变键(一个 GUID 字段),可以用于集成场景以及替换旧的 RECORDID 属性。这个新字段叫做 SystemId,它是一个 GUID 数据类型的字段,用于指定表中记录的唯一、不可变(只读)标识符。

新的 SystemId 字段(在每个表对象中用字段号 2000000000 标识)具有以下特点:

  • 它为表中的每一条记录提供一个值。

  • 你可以在插入时指定一个值;否则,平台会自动分配一个值。

  • 一旦SystemId被设置,就无法更改。

  • SystemId字段中总是有一个唯一的次级键。

作为平台规则,不允许修改现有记录的 SystemIdINSERT 函数现在有了一个新的重载:

Record.Insert([RunTrigger: Boolean[, InsertWithSystemId: Boolean]])

SystemId 在插入新记录时可以手动指定,如以下示例所示:

myRec.SystemId := '{B6667654-F4B2-B945-8567-006DD6B6775E}';
myRec.Insert(true,true);

现在,你可以使用GetBySystemId函数通过其SystemId检索记录,如以下示例所示:

var
    Customer: Record Customer;
    Text000: Label 'Customer was found.';
    begin
    if Customer.GetBySystemId('{B6667654-F4B2-B945-8567-006DD6B6775E}') then
         Message(Text000);
end;

你也可以使用新的 SystemId 字段来设置表之间的关系,如以下代码所示:

field(1; EntryID; GUID)
{
    DataClassification = CustomerContent;
    TableRelation = Item.SystemId;
}

版本 15 之前的 Integration Record 表为记录提供了 GUID。在升级到 Dynamics 365 Business Central 版本 15 的过程中,将使用这些值初始化新的 SystemId 字段。未来,Integration Record 表将被声明为过时。SystemId 字段在 API 页面上也非常有用。

在下一节中,我们将看到如何在 软件即服务SaaS)环境中使用 AL 处理文件。

使用 AL 处理文件

在 Dynamics 365 Business Central 中处理文件是一个棘手的地方。在本地版本中,你可以完全访问本地资源和文件系统,而在 Dynamics 365 Business Central 的 SaaS 版本中,情况有所不同。在这里,你没有文件系统,也无法访问本地资源(所有的处理都在微软的数据中心进行)。

如果你创建一个函数,你声明一个File变量,然后调用一个常见的文件管理方法(例如Create,它用于创建并打开一个 ASCII 或二进制文件),然后 Visual Studio Code 会显示如下错误:

这个错误发生是因为你尝试创建的扩展默认针对的是 Dynamics 365 Business Central SaaS 环境(app.json文件中的"target": "Extension")。

如果你在app.json文件中添加"target": "Internal"(这样你声明你的扩展仅针对本地环境),那么错误就会消失,你可以使用经典的File对象方法,如下所示:

如果你使用File Management代码单元来处理文件,也会发生同样的情况。它的一些方法在 SaaS 扩展中无法使用:

要在云环境中处理文件,你需要使用StreamsInStreamOutStream对象)。

InStreamOutStream数据类型是用于从文件和 BLOB 中读取或写入的通用流对象。

有关这些对象的更多信息,请参考以下链接:

docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/instream/instream-data-type docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/outstream/outstream-data-type

要将文件从客户端计算机上传到服务器端流对象,你需要调用UploadIntoStream方法:

[Ok := ]  File.UploadIntoStream(DialogTitle: String, FromFolder: String, FromFilter: String, var FromFile: Text, var InStream: InStream)

该方法的参数如下:

  • DialogTitle(字符串):这是文件选择对话框中显示的标题栏文本。此参数不被 Web 客户端支持(标题由用户的浏览器确定)。

  • FromFolder(字符串):这是在文件选择对话框中显示的文件夹路径。这个文件夹是默认文件夹,但用户可以浏览到任何可用的位置。此参数不被 Web 客户端支持(默认情况下,浏览器使用上次访问的文件夹)。

  • FromFilter(字符串):这是可以上传到服务器的文件类型。在 Windows 客户端中,该类型会显示在上传对话框中,用户只能选择指定类型的文件。对于 Web 客户端,用户界面不支持此筛选器。用户可以尝试上传任何类型的文件,但如果文件类型不符合指定类型,则会出现错误。

  • FromFile(文本):这是上传到服务的默认文件。用户可以更改该文件。Web 客户端不支持此参数。

  • InStream:这是用于加载数据的InStream对象。

有关该方法的更多详细信息,请参见 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/file/file-uploadintostream-method

举个例子,这是一个从客户端加载图像文件的函数(使用InStream对象和UploadIntoStream方法将客户端文件加载到InStream对象中),并将其作为Item对象添加:

procedure ImportItemPicture(Item: Record Item)
    var
        FileInstream: InStream;
        FileName: Text;
    begin
        if UploadIntoStream('', '', '', FileName, FileInstream) then
        begin
            Clear(Item.Picture);
            Item.Picture.ImportStream(FileInstream,FileName);
            Item.Modify(true);
        end;
    end;

另一个例子,这是一个函数,它将包含Item详细信息的 CSV 文件读取到InStream对象中,将其内容加载到CSV Buffer表中,然后根据需要更新Item字段:

local procedure UploadCSV()
  var
        CSVInStream : InStream;
        UploadResult : Boolean;
        TempBlob : Codeunit "Temp Blob";
        DialogCaption : Text;
        CSVFileName : Text;
        CSVBuffer: Record "CSV Buffer";
        Item: Record Item;
    begin
        UploadResult := UploadIntoStream(DialogCaption,'','',CSVFileName,CSVInStream);
        CSVBuffer.DeleteAll;
        CSVBuffer.LoadDataFromStream(CSVInStream,';'); 
        if CSVBuffer.FindSet() then
        repeat
         if (CSVBuffer."Field No." = 1) then
            Item.Init();
            case CSVBuffer."Field No." of
                1: Item.Validate("No.",CSVBuffer.Value);
                2: Item.Validate(Description,CSVBuffer.Value);
                3: Item.Validate("Item Category Code",CSVBuffer.Value);
                4: if not Item.Insert() then Item.Modify();
           end;
        until CSVBuffer.Next()=0;
    end;

要将文件从服务器端(SaaS 环境)下载到客户端(用户机器),需要使用DownloadFromStream方法:

[Ok := ]  File.DownloadFromStream(InStream: InStream, DialogTitle: String, ToFolder: String, ToFilter: String, var ToFile: Text)

该方法的参数如下:

有关此方法的更多信息,请参阅docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/file/file-downloadfromstream-method

作为示例,这段代码将与Item卡片相关的图像导出为MediaSet类型。它使用DownloadFromStream将文件下载到客户端。这些图像从Tenant Media表中检索,并保存为由Item Number、图像索引和图像扩展名组成的文件名(GetImageExtension函数根据其Mime Type检索图像文件的扩展名):

procedure ExportItemPicture(Item: Record Item)
    var
        FileInStream: InStream;
        FileName: Text;
        i: Integer;
        TenantMedia: Record "Tenant Media";
        ErrMsg: Label 'No images stored for the selected item.';
    begin
        if Item.Picture.Count() = 0 then
            Error(ErrMsg);
        for i := 1 to Item.Picture.Count() do begin
            if TenantMedia.Get(Item.Picture.MediaId()) then begin
                TenantMedia.CalcFields(Content);
                if TenantMedia.Content.HasValue() then begin
                    FileName := Item."No." + '_' + Format(i) + GetImageExtension(TenantMedia);
                    TenantMedia.Content.CreateInStream(FileInStream);
                    DownloadFromStream(FileInStream, '', '', '', FileName);
                end;
            end;
        end;
    end;

    procedure GetImageExtension(var TenantMedia: record "Tenant Media"): Text   
    begin
        case TenantMedia."Mime Type" of
        'image/jpeg': exit('.jpg');
        'image/bmp': exit('.bmp');
        'image/png': exit('.png');
        'image/gif': exit('.gif');
        'image/tiff': exit('.tiff');
        'image/wmf': exit('.wmf');
        end
    end;

如果您需要从 Dynamics 365 Business Central 创建文件,您需要在服务器端使用System ApplicationOutStream对象中定义的新Temp Blob代码单元来创建。

这是一个 AL 函数的示例,该函数接收一个文件名作为输入,创建一个包含三行的文本文件,并将其下载到客户端:

procedure CreateTextFile(FileName: Text)
var
    InStr: InStream;
    OutStr: OutStream;
    TempBlob: Codeunit "Temp Blob";
    CR: char;
    LF: char;
begin
    CR := 13;
    LF := 10;
    TempBlob.CreateOutStream(OutStr);        
    OutStr.WriteText('First line'+ CR + LF);
    OutStr.WriteText('Second line'+ CR + LF);
    OutStr.WriteText('Third line'+ CR + LF);        
    TempBlob.CreateInStream(InStr);
    DownloadFromStream(InStr, '', '', '', FileName); 
end;

在 Dynamics 365 Business Central 中,您不能直接将文件保存到本地文件夹(本地计算机或网络文件夹)。要执行此操作,您需要使用其他方式,例如 Azure Functions(我们将在本章稍后部分讨论)。

随着 Dynamics 365 Business Central wave 2 版本的发布,旧的TempBlob表已被弃用,并被一些系统代码单元(Temp BlobPersistent BlobTemp Blob List)替代。

在本节中,您学习了如何使用流处理 Dynamics 365 Business Central 中的文件。在下一节中,我们将学习如何处理 Dynamics 365 Business Central 中文档和实体的附件。

处理附件

附件是您可以链接到 Dynamics 365 Business Central 中的实体或文档的文件。用于存储附件的两个主要表格如下:

  • 文档附件(ID = 1173

  • 附件(ID = 5062

要将附件存储到这些表中,并从这些表中下载附件,您需要使用前面提到的UploadIntoStreamDownloadFromStream方法,并通过使用Streams加载 BLOB 字段。

一个将文件上传到 Attachment 表的函数示例如下:

procedure UploadAttachment()
    var
        Attachment: Record Attachment;
        outStr: OutStream;
        inStr: InStream;
        tempfilename: text;
        FileMgt: Codeunit "File Management";
        DialogTitle: Label 'Please select a File...';
    begin
        if UploadIntoStream(DialogTitle, '', 'All Files (*.*)|*.*', tempfilename, inStr) then 
        begin
            Attachment.Init();
            Attachment.Insert(true);
            Attachment."Storage Type" := Attachment."Storage Type"::Embedded;
            Attachment."Storage Pointer" := '';
            Attachment."File Extension" := FileMgt.GetExtension(tempfilename);
            Attachment."Attachment File".CreateOutStream(outStr);
            CopyStream(outStr, inStr);
            Attachment.Modify(true);
        end;
    end;

一个从 Attachment 表下载文件的函数示例如下:

procedure OpenAttachment(AttachmentEntryNo: Integer)
    var
        Attachment: record Attachment;
        inStr: InStream;
        tempfilename: text;
        ErrorAttachment: Label 'File not available.';
    begin
        if Attachment.get(AttachmentEntryNo) then
            if Attachment."Attachment File".HasValue then begin
                Attachment.CalcFields("Attachment File");
                Attachment."Attachment File".CreateInStream(inStr);
                tempfilename := CreateGuid() + '.' + Attachment."File Extension";
                DOWNLOADFROMSTREAM(inStr, 'Save file', '', 'All Files (*.*)|*.*', 
                   tempfilename);
            end
            else
                Error(ErrorAttachment);
    end;

我们可以对 Document Attachment 表使用相同的过程;您只需要将对 attachment 记录的引用添加到文档本身。

我们已经看到了如何处理附件(这是一个可以添加到扩展中的有用功能)。在接下来的部分中,我们将看到如何从 BLOB 字段读取和写入数据。

读取和写入文本数据到 BLOB 字段

要从 BLOB 字段读取数据并写入文本数据,您需要使用前面描述的 InStreamsOutStreams 对象。

以下代码中的两个方法从自定义表中定义的 BLOB 字段读取和写入文本数据:

table 50120 MyBlobTable
{
    DataClassification = CustomerContent;   
    fields
    {
        field(1;ID; Integer)
        {
            DataClassification = CustomerContent;           
        }

        field(2; BlobField; Blob)
        {
            DataClassification = CustomerContent;
        }
    }

    keys
    {
        key(PK; ID)
        {
            Clustered = true;
        }
    }

    procedure SetBlobValue(value: Text)
    var
        outStr: OutStream;
    begin
        BlobField.CreateOutStream(outStr);
        outStr.WriteText(value);
    end;

    procedure GetBlobValue(value: Text)
    var
        inStr: InStream;
    begin
        CalcFields(BlobField);
        if BlobField.HasValue() then
        begin
            BlobField.CreateInStream(inStr);
            inStr.ReadText(value);
        end
        else
            value := 'No value on the BLOB field';
    end;   
}

在这里,我们定义了一个包含 BLOB 字段的表,并创建了两个方法用于读取和写入数据到该 BLOB 字段:

  • SetBlobValue 函数将数据(作为输入传递)写入 BLOB 字段。

  • GetBlobValue 函数从 BLOB 字段读取数据。

使用这两个方法,我们已经实现了读取和写入文本到 BLOB 字段的目标。在接下来的部分中,我们将看到如何从 AL 中使用 XMLport。

在 AL 代码中使用 XMLport

正如我们在 第二章中所说的,掌握现代开发环境XMLport 是用于在 Dynamics 365 Business Central 与外部数据源之间导入和导出数据的对象(这一操作通过 Direction 属性来管理,可以设置为 ImportExportBoth)。数据可以以 XML 或 CSV(文本)格式导入或导出(Format 属性可以设置为 XmlVariable TextFixed Text)。

XMLport 属性的详细信息请见 docs.microsoft.com/en-us/dynamics-nav/xmlport-properties XMLport 触发器的详细信息请见 docs.microsoft.com/en-us/dynamics-nav/xmlport-triggers

现在,考虑在 第四章中定义的示例 XMLport,扩展开发基础

xmlport 50100 MyXmlportImportCustomer
{
    Direction = Import;
    Format = VariableText;
    FieldSeparator = ';';
    RecordSeparator = '<LF>';
    schema
    {
        textelement(NodeName1)
        {
            tableelement(Customer; Customer)
            {
                fieldattribute(No; Customer."No.")
                {                   
                }
                fieldattribute(Name; Customer.Name)
                {
                }
                fieldattribute(Address;Customer.Address)
                {
                }
                fieldattribute(City;Customer.City)
                {
                }
                fieldattribute(Country;Customer."Country/Region Code")
                {
                    trigger OnAfterAssignField()                   
                    begin
                      //Executed after a field has been assigned a value and before it is validated and imported.   
                    end;
                }
            }
        }
    }
}

要在 Dynamics 365 Business Central 中执行 XMLport,您需要通过页面或代码单元对象运行它(不能直接运行)。在 Dynamics 365 Business Central Web 客户端中不支持 XMLport 请求页面(用于设置过滤器或插入参数)。

要在 Dynamics 365 Business Central 中执行 XMLport 从文件导入数据,您需要使用以下代码:

procedure RunXMLportImport()
    var
        FileInstream: InStream;
        FileName: Text;
    begin
        UploadIntoStream('','','',FileName,FileInstream);
        Xmlport.Import(Xmlport::MyXmlportImportCustomer,FileInStream);
        Message('Import Done successfully.');
    end;

在这里,文件被加载到一个 InStream 对象中,然后通过将 InStream 对象作为输入来执行 XMLport。

要在 Dynamics 365 Business Central 中执行 XMLport 导出数据到文件,您需要使用以下代码:

procedure RunXMLportExport()
    var
        TempBlob: Codeunit "Temp Blob";
        FileName: Text;
        FileOutStream: OutStream;
        FileInStream: InStream;
        outputFileName: Text;
    begin
        TempBlob.CREATEOUTSTREAM(FileOutStream);
        Xmlport.Export(Xmlport::MyXmlportImportCustomer, FileOutStream);
        TempBlob.CREATEINSTREAM(FileInStream);
        outputFileName := 'MyOutputFile.xml';
        DownloadFromStream(FileInStream,'','','',outputFileName); 
       //The output is saved in the default browser's Download folder
    end;

在这里,我们已经看到如何使用 AL 代码中的 XMLports 导入或导出数据。在下一部分,我们将看到如何在 Dynamics 365 Business Central 中创建和扩展角色中心。

创建和扩展角色中心

当用户登录 Dynamics 365 Business Central 时,他们会看到一个展示与其公司角色相关的信息和操作的页面。这个页面叫做角色中心,它是应用程序角色定制体验的一个核心部分。

Dynamics 365 Business Central 提供约 20 个开箱即用的角色中心(作为标准),你可以扩展和自定义,并且可以创建新的角色中心。

角色中心是一个PageType属性设置为RoleCenter的页面。页面结构如下:

在结构图中,部分如下:

  • 第一部分是导航菜单区域(一个或多个项目,点击时显示其他子菜单)。用于提供对该角色中心页面所分配角色的相关实体的访问。

  • 第二部分是导航栏区域,用于显示链接到其他页面的链接列表,这些页面将在内容区域中打开。通常用于添加链接到用户在其业务角色中最有用的实体。

  • 第三部分是操作区域,用于添加链接以执行此角色的最重要任务(指向页面、报告或代码单元的链接)。

  • 第四部分是标题区域,用于显示动态生成的关于业务的信息。我们将在本章的定制标题部分中看到更多关于此区域的细节。

  • 第五部分是宽数据提示区域,一组显示有关业务的数值信息的数据提示。此区域是在页面上使用cuegroup控件创建的,页面的PageType = CardPart,并将Layout属性设置为wide

  • 第六部分是数据提示区域,用于提供汇总业务数据的可视化表示(如关键绩效指标)。此部分是在页面上使用cuegroup控件并设置PageType = CardPart创建的。

  • 第七部分是操作提示区域,显示链接到一些业务任务的瓷砖。此区域是在页面上使用cuegroup控件创建的,页面的PageType = CardPart

  • 第八部分是图表区域,用于以图表形式显示信息(自定义业务图表控件或嵌入的 Power BI 报告)。

  • 第九部分是CardPart 或 ListPart 页面区域,用于以卡片或列表布局显示来自应用程序的数据。

  • 第十部分是*控制添加项**区域,用于通过基于 HTML 的控制添加项(用 JavaScript 编写)显示自定义内容。

角色中心页面可以通过以下代码在 AL 中创建:

page 50101 "My Role Center"
{
    PageType = RoleCenter;

    layout
    {
        area(rolecenter)
        {
            part(SalesPerformance; "Sales Performance")
            {
                ApplicationArea = All;
                Visible = true;
            }

            part(MyCustomers; "My Customers")
            {
                ApplicationArea = All;
                Visible = true;
            }

            part(News;"Headline RC Business Manager")
            {
                ApplicationArea = All;
                Visible = true;
            }
        }
    }   
}

在这里,我们创建了一个包含三个部分(子页面)的角色中心页面。

你可以通过创建pageextension对象来自定义现有的角色中心页面:

pageextension 50100 SalesManagerRoleCenterExt_SD extends "Sales Manager Role Center"
{
    layout
    {
        addlast(Content)
        {
            part(MyNews; MyRoleCenterHeadline)
            {
                ApplicationArea = All;
                Visible = true;
            }
        }
    }

    actions
    {   
         addlast(Sections)
         {
            group("My Customers")
            {               
                 action("Customer Ledger Entries")
                {
                    RunObject = page "Customer Ledger Entries";
                    ApplicationArea = All;
                }
            }
         }
    }         
}

在这里,我们通过向内容中添加一个新的自定义头条部分和一个新动作来打开 Customer Ledger Entries 页面,扩展了 Sales Manager Role Center 页面。

自定义或创建角色中心非常重要,因为这可以为您的用户提供更好的用户体验。

在接下来的部分中,我们将看到如何自定义角色中心的头条部分。

自定义头条

如前所述,头条是随着 Dynamics 365 Business Central 网页客户端一起引入的新部分,用于动态显示有关您业务的重要信息。请参阅以下截图:

这是 Dynamics 365 Business Central 角色定制用户体验的重要部分,建议使用并自定义它,以便为您的用户提供更好的体验。

头条本质上是一个页面,包含一个或多个字段(每个字段是一个头条行),并且 PageType 被设置为 HeadlinePart。此页面仅在角色中心页面内部可见。

Dynamics 365 Business Central 提供了九个标准头条:

  • 头条 RC 商务经理

  • 头条 RC 订单处理员

  • 头条 RC 会计

  • 头条 RC 项目经理

  • 头条 RC 关系管理

  • 头条 RC 管理员

  • 头条 RC 团队成员

  • 头条 RC 生产计划员

  • 头条 RC 服务调度员

您还可以使用 AL 和 Visual Studio Code 创建自己的头条。

可以在 AL 中定义一个头条页面,如下所示:

page 50100 "MyRoleCenterHeadline"
{
    PageType = HeadLinePart;
    layout
    {
        area(content)
        {
            field(Headline1; text001)
            {
                ApplicationArea = all;
            }

            field(Headline2; text002)
            {
                ApplicationArea = all;
                trigger OnDrillDown()
                var
                    DrillDownURL: Label 'http://www.demiliani.com';
                begin
                    Hyperlink(DrillDownURL)
                end;
            }

            field(Headline3; text003)
            {
                ApplicationArea = all;
            }

            field(Headline4; text004)
            {
                ApplicationArea = all;
                // Determines visibility while the page is open (custom criteria)
                Visible=showHeadline4;
            }       
        }
    }

    var
        text001: Label 'This is Headline 1';
        text002: Label 'This is Headline 2 (click for details)';
        text003: Label 'This is Headline 3';
        text004: Label 'This is Headline 4';
        showHeadline4: Boolean;

        trigger OnOpenPage()
        var
           myInt: Integer;
        begin
            showHeadline4 := true;
        end;
}

在这里,我们定义了一个头条页面,包含四个文本字段,并在 Dynamics 365 Business Central 界面中显示适当的文本。

在第二个头条中,我们处理了 OnDrillDown 事件,如果您点击本示例中的第二个头条,将会跳转到一个 URL。通过处理此事件,您可以拥有一个可点击的头条,显示业务详细信息(例如,它可以打开一个 Dynamics 365 Business Central 详细页面)。头条页面也可以被隐藏,且其可见性可以通过代码程序性地设置(如前一个示例中的头条 4)。

头条页面上显示的文本可以根据以下 Expression 属性进行格式化:

Expression 标签 描述
<qualifier></qualifier> 这指定了在头条上方显示的标题。如果没有该元素,默认将使用文本 HEADLINE
<payload></payload> 这指定了显示的头条文本。
<emphasize></emphasize> 该标签中的文本显示为最大尺寸。

要修改现有的头条,您需要创建一个 pageextension 对象并对其进行扩展。作为示例,我们在这里通过添加一个新的头条面板,并动态创建内容,来修改标准的 Headline RC Business Manager 页面:

pageextension 50101 MyNewBCHeadline extends "Headline RC Business Manager"
{
    layout
    {
        addafter(Control4)
        {
            field(newHeadlineText;newHeadlineText)
            {
                ApplicationArea = all;
            }
        }
    }

    var
        newHeadlineText: Text;

        trigger OnOpenPage()
        var
            HeadlineMgt : Codeunit "Headline Management";
        begin
            //Set Headline text           
            newHeadlineText := 'This is my new Business Central Headline for ' + HeadlineMgt.Emphasize('Packt Publishing');
        end;

}

在这里,我们添加了一个名为 newHeadlineText 的新字段,并且在头条页面的 OnOpenPage 触发器中填充该字段,显示我们希望呈现给用户的信息。

本节已解释了如何自定义角色中心页面的标题,以及如何向我们的用户展示相关的业务信息。

使用 AL 语言处理 XML 和 JSON 文件

AL 语言扩展原生支持处理 XML 和 JSON 文档。

XML 文档通过使用XmlDocument数据类型来表示,详细说明见docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/xmldocument/xmldocument-data-type

以下代码展示了如何导入 XML 文件并将其加载到XmlDocument对象中:

local procedure ImportXML()
    var
        TempBlob : Codeunit "Temp Blob";
        TargetXmlDoc : XmlDocument;
        XmlDec : XmlDeclaration;
        Instr: InStream;
        filename: Text;
    begin
        // Create the Xml Document
        TargetXmlDoc := XmlDocument.Create;
        xmlDec := xmlDeclaration.Create('1.0','UTF-8','');
        TargetXmlDoc.SetDeclaration(xmlDec);

        // Create an Instream object & upload the XML file into it              
        TempBlob.CreateInStream(Instr);
        filename := 'data.xml';       
        UploadIntoStream('Import XML','','',filename,Instr);

        // Read stream into new xml document       
        Xmldocument.ReadFrom(Instr, TargetXmlDoc);    
    end;

在这里,我们创建了一个带有 XML 声明的XmlDocument对象,然后创建了一个InStream对象来加载 XML 文件,并将InStream的内容读取到XmlDocument对象中。

如果你引用TargetXmlDoc对象,你将看到所有可用的处理和操作 XML 文件的方法:

要直接从 AL 代码创建 XML 文档,可以使用XmlDocumentXmlElement类:

local procedure XMLDocumentCreation()
    var
        xmldoc: XmlDocument;
        xmlDec: XmlDeclaration;
        node1: XmlElement;
        node2: XmlElement;
    begin
        xmldoc := XmlDocument.Create();
        xmlDec := xmlDeclaration.Create('1.0','UTF-8','');
        xmlDoc.SetDeclaration(xmlDec);
        node1:= XmlElement.Create('node1');
        xmldoc.Add(node1);
        node2 := XmlElement.Create('node2');
        node2.SetAttribute('ID','3');
        node1.Add(node2);
    end;

这段代码创建了一个 XML 文档,包含一个根节点(称为node1)和一个子节点(称为node2),该子节点有一个 ID 属性,其值为3<node2 ID="3">)。

JSON 文档的原生支持通过使用JsonObjectJsonArray数据类型提供。这些数据类型包含处理 JSON 文件(包括读写)以及操作 JSON 数据(令牌)的方法。

所有可用方法的详细说明可以在以下链接中找到:

docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/jsonobject/jsonobject-data-type docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/jsonarray/jsonarray-data-type

以下代码展示了如何创建销售订单文档的 JSON 表示形式:

procedure CreateJsonOrder(OrderNo: Code[20])
    var
        JsonObjectHeader: JsonObject;
        JsonObjectLines: JsonObject;
        JsonOrderArray: JsonArray;
        JsonArrayLines: JsonArray;
        SalesHeader: Record "Sales Header";
        SalesLines: Record "Sales Line";

    begin
        //Retrieves the Sales Header
        SalesHeader.Get(SalesHeader."Document Type"::Order,OrderNo);
        //Creates the JSON header details
        JsonObjectHeader.Add('sales_order_no', SalesHeader."No.");
        JsonObjectHeader.Add(' bill_to_customer_no', SalesHeader."Bill-to Customer No.");     
        JsonObjectHeader.Add('bill_to_name', SalesHeader."Bill-to Name");
        JsonObjectHeader.Add('order_date', SalesHeader."Order Date");
        JsonOrderArray.Add(JsonObjectHeader);

        //Retrieves the Sales Lines
        SalesLines.SetRange("Document Type", SalesLines."Document Type"::Order);
        SalesLines.SetRange("Document No.", SalesHeader."No.");
        if SalesLines.FindSet then
        // JsonObject Init
        JsonObjectLines.Add('line_no', '');
        JsonObjectLines.Add('item_no', '');
        JsonObjectLines.Add('description', '');
        JsonObjectLines.Add('location_code', '');
        JsonObjectLines.Add('quantity', '');
        repeat
            JsonObjectLines.Replace('line_no', SalesLines."Line No.");
            JsonObjectLines.Replace('item_no', SalesLines."No.");            
            JsonObjectLines.Replace('description', SalesLines.Description);
            JsonObjectLines.Replace('location_code', SalesLines."Location Code");
            JsonObjectLines.Replace('quantity', SalesLines.Quantity);
            JsonArrayLines.Add(JsonObjectLines);
        until SalesLines.Next() = 0;
        JsonOrderArray.Add(JsonArrayLines);
    end;

该过程接收订单号作为输入,检索Sales HeaderSales Line的详细信息,并创建一个 JSON 表示形式。最终结果如下:

[
    {
        "sales_order_no": "SO1900027",
        "bill_to_customer_no": "C001435",
        "bill_to_name": "Packt Publishing",
        "order_date": "2019-03-23"
    },
    [
        {
            "line_no": "10000",
            "item_no": "IT00256",
            "description": "Dynamics 365 Business Central Development Guide",
            "location_code": "MAIN",
            "quantity": 30
        },
        {
            "line_no": "20000",
            "item_no": "IT03465",
            "description": "Mastering Dynamics 365 Business Central",
            "location_code":"MAIN",                                 
            "quantity": 27
        }
    ]
]

显然,你也可以将 JSON 表示形式作为输入(例如,作为 API 调用的响应),并使用相同的数据类型进行处理。

在这里,我们已经看到了如何通过使用原生 JSON 类型在 AL 中处理 JSON 文档。

在下一部分,我们将看到一个完整的示例,展示如何调用 API,接收 JSON 响应,解析它,并将数据保存到 Dynamics 365 Business Central 实体中。

从 AL 中使用 Web 服务和 API

AL 的HttpClient对象提供了一个基类,用于处理来自 Web 资源(通过 URI 标识)的 HTTP 请求和响应。使用HttpClient类,你可以发送GETDELETEPOSTPUT HTTP 请求消息(HttpRequestMessage,包含HttpHeadersHttpContent),并接收一个HttpResponseMessage对象作为该请求的结果(包括状态码和响应数据)。

你可以在以下链接中找到所有公开方法的更多详细信息:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods-auto/httpclient/httpclient-data-type

例如,在以下代码中,我们创建了一个扩展,允许通过调用名为Fullcontact的服务(www.fullcontact.com)来获取客户的地址详细信息。当你注册免费账户时,Fullcontact会提供一个 API,并附带一个访问密钥,你可以用这个密钥通过提供客户姓名来检索客户详细信息。

我们需要的是:如果在客户卡片的Name字段中插入一个公司的域名(例如,packt.com),系统必须调用 API 并检索该域名对应的客户详细信息。

我们通过扩展客户卡片来创建一个pageextension对象,并在Name字段的OnAfterValidate触发器中调用一个自定义方法(名为LookupAddressInfo,定义在一个名为TranslationManagement的代码单元中)来处理数据检索逻辑:

pageextension 50100 CustomerCardExt extends "Customer Card"
{
    layout
    {
        modify(Name)
        {
            trigger OnAfterValidate()
            var
                TranslationManagement: Codeunit TranslationManagement;
            begin
                if Name.EndsWith('.com') then begin
                    if Confirm('Do you want to retrieve company details?', false) then
                        TranslationManagement.LookupAddressInfo(Name, Rec);
                end;
            end;
        }
    }
}

TranslationManagement代码单元定义如下:

codeunit 50100 TranslationManagement
{
    procedure LookupAddressInfo(Name: Text; var Customer: Record Customer)
    var
        Client: HttpClient;
        Content: HttpContent;
        ResponseMessage: HttpResponseMessage;
        Result: Text;
        JContent: JsonObject;
        JDetails: JsonObject;
        JLocations: JsonArray;
        JLocation: JsonObject;
        JPhones: JsonArray;
        JPhone: JsonObject;
    begin
        Content.WriteFrom('{domain":"' + Name + '"}');
        Client.DefaultRequestHeaders().Add('Authorization', 'Bearer <YOUR KEY>');
        Client.Post('https://api.fullcontact.com/v3/company.enrich', Content, ResponseMessage);
        if not ResponseMessage.IsSuccessStatusCode() then
            Error('Error connecting to the Web Service.');        
        ResponseMessage.Content().ReadAs(Result);

        if not JContent.ReadFrom(Result) then
            Error('Invalid response from Web Service');
        JDetails := GetTokenAsObject(JContent, 'details', 'Invalid response from Web Service');
        JLocations := GetTokenAsArray(JDetails, 'locations', 'No locations available');
        JLocation := GetArrayElementAsObject(JLocations, 0, 'Location not available');
        JPhones := GetTokenAsArray(JDetails, 'phones', '');
        JPhone := GetArrayElementAsObject(JPhones, 0, '');
        Customer.Name := GetTokenAsText(JContent, 'name', '');
        Customer.Address := GetTokenAsText(JLocation, 'addressLine1', '');
        Customer.City := GetTokenAsText(JLocation, 'city', '');
        Customer."Post Code" := GetTokenAsText(JLocation, 'postalCode', '');
        Customer."Country/Region Code" := GetTokenAsText(JLocation, 'countryCode', '');
        Customer.County := GetTokenAsText(JLocation, 'country', '');
        Customer."Phone No." := GetTokenAsText(JPhone, 'value', '');
    end;

    procedure GetTokenAsText(JsonObject: JsonObject; TokenKey: Text; Error: Text): Text;
    var
        JsonToken: JsonToken;
    begin
        if not JsonObject.Get(TokenKey, JsonToken) then begin
            if Error <> '' then
                Error(Error);
            exit('');
        end;
        exit(JsonToken.AsValue.AsText);
    end;

    procedure GetTokenAsObject(JsonObject: JsonObject; TokenKey: Text; Error: Text): JsonObject;
    var
        JsonToken: JsonToken;
    begin
        if not JsonObject.Get(TokenKey, JsonToken) then
            if Error <> '' then
                Error(Error);
        exit(JsonToken.AsObject());
    end;

    procedure GetTokenAsArray(JsonObject: JsonObject; TokenKey: Text; Error: Text): JsonArray;
    var
        JsonToken: JsonToken;
    begin
        if not JsonObject.Get(TokenKey, JsonToken) then
            if Error <> '' then
                Error(Error);
        exit(JsonToken.AsArray());
    end;

    procedure GetArrayElementAsObject(JsonArray: JsonArray; Index: Integer; Error: Text): JsonObject;
    var
        JsonToken: JsonToken;
    begin
        if not JsonArray.Get(Index, JsonToken) then
            if Error <> '' then
                Error(Error);
        exit(JsonToken.AsObject());
    end; 
}

在这里,LookupAddressInfo过程通过使用HttpClient对象调用Fullcontact API(并通过 API 注册时提供的密钥设置授权头),并发送POST请求到提供的 URL,将HttpContent传递给该请求(该内容按 API 规范的格式包含要检查的姓名)。

HTTP 请求返回一个HttpResponseMessage对象,该对象包含响应消息。如果 HTTP 响应成功,我们将读取 HTTP 响应消息的内容(该内容为一个 JSON 字符串)。然后,我们使用一些辅助方法(你可以在前面的代码中看到这些方法的定义)解析这个 JSON 字符串,这些方法允许我们读取 JSON 令牌并按指定格式获取它们的值(如果令牌数据不可用,则返回错误字符串或默认值)。

使用HttpClient类,你可以处理对你要调用的 Web 服务或 API 的身份验证。例如,以下是如何使用HttpClient进行基本身份验证的示例:

var
  RequestMessage : HttpRequestMessage;
  Headers : HttpHeaders;
  base64Convert: Codeunit "Base64 Convert";
  AuthenticationString: Text;
begin
  RequestMessage.GetHeaders(Headers);
  AuthenticationString := StrSubstNo('%1:%2',YOURUSERNAME,YOURPASSWORD); 
  Headers.Add('Authorization', StrSubstNo('Basic %1',base64Convert.ToBase64(AuthenticationString)));
end

在这一部分中,我们了解了如何从 AL 消费 Web 服务和 API,以及如何处理请求、响应和身份验证。在接下来的部分,我们将看到如何在安装扩展时自动将对象发布为 Dynamics 365 Business Central 中的 Web 服务。

从 AL 发布 Dynamics 365 Business Central 对象作为 Web 服务

在开发扩展时,你可能需要自动将 Dynamics 365 Business Central 对象发布为外部应用程序的 Web 服务实例。

本质上,有两种不同的方式来自动化这个过程:

  • 创建一个Install代码单元,并在此代码单元中通过 AL 代码在Tenant Web Service表中插入记录来创建 Web 服务实例。

  • 创建一个包含TenantWebService定义的 XML 文件。

使用第一种方法,你定义一个Install代码单元,并且(例如,在OnInstallAppPerCompany触发器中)你可以通过在Tenant Web Service表中创建一个新条目来创建 Web 服务定义,如下所示:

codeunit 50104 TestInstallCodeunit
{
    Subtype = Install;
    trigger OnInstallAppPerCompany()

    var
        TenantWebService: Record "Tenant Web Service";
    begin
        TenantWebService.Init();
        TenantWebService."Object Type" := TenantWebService."Object Type"::Page;
        TenantWebService."Object ID" := 26;  //Vendor Card
        TenantWebService."Service Name" := 'VendorCardWS';
        TenantWebService.Published := true;
        TenantWebService.Insert(true);
    end;
}

在这里,我们已经将Vendor Card页面作为 Web 服务发布,SERVICE NAME 为VendorCardWS。这个 Web 服务将在 Dynamics 365 Business Central 的Web Services页面中可见,如下所示:

使用第二种方法,你可以通过twebservices代码片段来创建一个TenantWebService XML 定义文件。以下截图展示了这一点:

使用 XML 定义文件,要将Vendor Card页面作为 Web 服务发布,你需要编写以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<ExportedData>
    <TenantWebServiceCollection>
        <TenantWebService>
            <ObjectType>Page</ObjectType>
            <ServiceName>VendorCardWS</ServiceName>
            <ObjectID>26</ObjectID>
            <Published>true</Published>
        </TenantWebService>
    </TenantWebServiceCollection>
</ExportedData>

这个 XML 定义文件将成为你 AL 项目扩展代码的一部分。

这里描述的两种方法在安装扩展时效果相同:你的 Web 服务将被发布并在 Dynamics 365 Business Central 的Web Services页面中可见。但当你卸载扩展时,会发生什么呢?

当你卸载扩展时,如果 Web 服务是使用 XML 定义文件发布的,它会自动从Tenant Web Service表中移除;而如果 Web 服务是通过 AL 代码直接发布的,则不会从该表中移除。如果你需要在安装阶段自动发布 Web 服务,建议使用TenantWebService XML 定义文件。

在接下来的部分,我们将看到如何使用 Azure Functions 在云端执行无服务器流程,并替代标准的.NET 代码。

使用 Azure Functions 替代.NET 代码

在 Dynamics 365 Business Central 中,.NET 程序集的使用(DotNet变量类型)仅支持本地部署的场景。在云端的 Dynamics 365 Business Central(SaaS)中,你不能使用 DotNet 对象(出于安全原因),而替代DotNet变量的官方方式是使用 HTTP 调用Azure 函数

Azure Functions是 Azure 平台提供的一种无服务器计算服务,允许你在云中运行代码,而无需管理基础设施。我们将在第十四章,使用 Azure Functions 进行监控、扩展和 CI/CD中更深入地讨论 Azure 函数,因此这里我们不会讨论如何从头创建一个 Azure 函数,而仅介绍如何从扩展的代码中调用它。

现在,假设有一个名为PostCodeValidator的 Azure 函数,它验证邮政编码。这个函数(属于HttpTrigger类型的函数)通过查询字符串接收一个邮政编码,验证它,然后函数返回一个包含布尔值的 JSON 响应,指示邮政编码是否有效。

可以通过以下 URL 使用查询字符串调用 Azure 函数:

http://postcodevalidator.azurewebsites.net/api/postcodevalidator?code=POSTCODE

这里,POSTCODE是要验证的邮政编码。

来自 Azure 函数的 JSON 响应消息如下:

{"Isvalid":false}

在这个示例中,我们希望在Customer表的Post Code字段的OnAfterValidate事件中调用这个函数:

从 AL 调用 Azure 函数的代码如下:

[EventSubscriber(ObjectType::Table, Database::Customer, 'OnAfterValidateEvent', 'Post Code', false, false)]
    local procedure ValidatePostCodeViaAzureFunction(var Rec: Record Customer)
    var
        Client: HttpClient;
        Response: HttpResponseMessage;
        json: Text;
        jsonObj: JsonObject;
        token: JsonToken;
        FunctionURL: Label 'http://postcodevalidator.azurewebsites.net/api/postcodevalidator?code=';
        InvalidResponseError: Label 'Invalid Response from Azure Function.';
        InvalidCodeError: Label 'Invalid Post Code. Please reinsert.';
        TokenNotFoundError: Label 'Token not found in Json.';

    begin
        client.Get(FunctionURL + rec."Post Code", Response);
        //Reads the response content from the Azure Function
        Response.Content().ReadAs(json);
        if not jsonObj.ReadFrom(json) then
            Error(InvalidResponseError);
        //Retrieves the JSon token from the response
        if not jsonObj.Get('IsValid',token) then
            Error(TokenNotFoundError);
        //Convert the Json token to a Boolean value. is TRUE the post code is valid.
        if not token.AsValue().AsBoolean() then
            Error(InvalidCodeError);
    end;

在这里,通过使用HttpClient对象,我们发送一个 HTTP GET请求到 Azure 函数的 URI,传递请求的代码函数的参数(即要验证的Post Code字段的值)。然后,我们读取由 Azure 函数返回的HttpResponseMessage内容(这是一个 JSON 对象),如下所示:

Response.Content().ReadAs(json);

这个方法从 HTTP 响应内容中加载json文本变量。之后,我们在 JSON 对象中查找IsValid标记:

jsonObj.Get('IsValid',token)

如果找到了该标记,它的值将被保存到token变量中。我们读取它的布尔值,如果是 false,则会抛出错误(InvalidCodeError)。

我们已经看到如何调用 Azure 函数,然后如何读取和处理它的响应。在第十三章,无服务器业务流程与 Business Central 和 Azure中,我们将更深入地讨论 Azure 函数。

在下一部分,我们将看到如何使用隔离存储功能来处理和保护来自 Dynamics 365 Business Central 扩展的敏感数据。

理解隔离存储

Isolated Storage是一种基于键值的存储,它提供了扩展之间的数据隔离。隔离存储可用于存储必须保留在扩展范围内的数据,这些数据可以通过 AL 代码访问。DataScope选项类型标识隔离存储中存储数据的范围。

DataScope是一个可选参数,默认值为Module。所有可能的值列在下表中:

Member 描述
Module 它表示记录在应用程序上下文中的范围内可用。
Company 它表示记录在应用程序上下文中的公司范围内可用。
User 表示该记录在应用程序上下文中对用户可用。
CompanyAndUser 表示该记录在应用程序上下文中对特定用户和公司可用。

要管理 Isolated Storage 中的数据,您可以使用以下方法:

方法 描述
[Ok := ] IsolatedStorage.**Set**(Key: String, Value: String, [DataScope: DataScope]) 该方法设置与指定键相关联的值。可选的DataScope参数是存储数据的范围。
[Ok := ] IsolatedStorage.**Get**(Key: String, [DataScope: DataScope], var Value: Text) 该方法获取与指定键相关联的值。可选的DataScope参数是要检索的数据范围。
HasValue := IsolatedStorage.**Contains**(Key: String, [DataScope: DataScope]) 该方法判断存储中是否包含与指定键关联的值。可选的DataScope参数是检查该键值是否存在的范围。
[Ok := ] IsolatedStorage.**Delete**(Key: String, [DataScope: DataScope]) 该方法删除 Isolated Storage 中指定键相关联的值。可选的DataScope参数是要删除的键值的范围。

Isolated Storage 适用于存储敏感数据、用户选项和许可证密钥。

让我们来看一个示例:

local procedure IsolatedStorageTest()
    var
        keyValue: Text;
    begin
        IsolatedStorage.Set('mykey','myvalue',DataScope::Company);
        if IsolatedStorage.Contains('mykey',DataScope::Company) then
        begin
            IsolatedStorage.Get('mykey',DataScope::Company,keyValue);
            Message('Key value retrieved is %1', keyValue);
        end;
        IsolatedStorage.Delete('mykey',DataScope::Company);
    end;

从前面的代码中,我们得到了以下内容:

  1. 在第一步,我们将一个名为mykey的键值对存储在 Isolated Storage 中,值为myvalue,并将DataScope设置为Company。该键在应用程序上下文中属于公司的范围,因此其他扩展无法访问此键。

  2. 在第二步,我们检查名为mykey的键是否已存储在 Isolated Storage 中,且DataScope设置为Company。如果找到匹配项(键和值的范围),则通过Get方法获取该键,并将值返回到keyValue文本变量中。

  3. 在最后一步,我们删除此DataScope的键。

如前所述,您也可以使用 Isolated Storage 保存许可证密钥或许可证详细信息。以下代码展示了如何将名为License的表中的记录导出为 JSON,然后如何加密该 JSON 值,最后如何将加密后的文本存储到 Isolated Storage 中:

local procedure StoreLicense()
    var
        StorageKey: Text;
        LicenseText: Text;
        EncryptManagement: Codeunit "Cryptography Management";
        License: Record License temporary;

    begin
        StorageKey := GetStorageKey();
        LicenseText := License.WriteLicenseToJson();
        if EncryptManagement.IsEncryptionEnabled() and EncryptManagement.IsEncryptionPossible() then
            LicenseText := EncryptManagement.Encrypt(LicenseText);
        if IsolatedStorage.Contains(StorageKey, DataScope::Module) then
            IsolatedStorage.Delete(StorageKey);
        IsolatedStorage.Set(StorageKey, LicenseText, DataScope::Module);
    end;

    local procedure GetStorageKey(): Text
    var
        //Returns a GUID
        StorageKeyTxt: Label 'dd03d28e-4acb-48d9-9520-c854495362b6', Locked = true;
    begin
        exit(StorageKeyTxt);
    end;

    local procedure ReadLicense()
    var
        StorageKey: Text;
        LicenseText: Text;
        EncryptManagement: Codeunit "Cryptography Management";
        License: Record License temporary;
    begin
        StorageKey := GetStorageKey();
        if IsolatedStorage.Contains(StorageKey, DataScope::Module) then
            IsolatedStorage.Get(StorageKey, DataScope::Module, LicenseText);
        if EncryptManagement.IsEncryptionEnabled() and EncryptManagement.IsEncryptionPossible() then
            LicenseText := EncryptManagement.Decrypt(LicenseText);
        License.ReadLicenseFromJson(LicenseText);
    end;

这里,License表被声明为临时表。这样,数据就被隔离到调用的代码单元中。

随着 Dynamics 365 Business Central wave 2 版本的发布,Isolated Storage 的密钥管理方式有所变化。具体细节如下:

  • 在 Dynamics 365 Business Central SaaS 版中,存储在 Isolated Storage 中的敏感数据始终是加密的。

  • 在 Dynamics 365 Business Central 本地版中,加密由最终用户控制(通过数据加密管理页面):

    • 如果启用了加密,存储在 Isolated Storage 中的密钥会自动加密。

    • 在加密关闭时插入的秘密,如果加密开启,则将保持未加密状态。

    • 如果关闭加密,秘密将被解密。

根据这些更改,如果您有一个适用于 Dynamics 365 Business Central SaaS 和本地部署的扩展,并且您使用隔离存储来存储秘密,则需要检查加密是否启用(对于 SaaS 总是启用)并相应地保存秘密。

因此,一个将许可证密钥保存到隔离存储并适用于 Dynamics 365 Business Central SaaS 和本地部署的函数将如下所示:

local procedure StoreLicense()
var
   licenseKeyValue: Text;
begin
   if not EncryptionEnabled() then
       IsolatedStorage.Set('LicenseKey',licenseKeyValue,DataScope::Module)
   else         
      IsolatedStorage.SetEncrypted('LicenseKey',licenseKeyValue,DataScope::Module)
end;

使用 SetEncrypted 方法,您现在可以通过加密自动保存秘密(无需再调用 Cryptography Management 代码单元)。

我们已经看到如何使用隔离存储在扩展中提高数据安全性。在下一部分,我们将看到如何创建控制插件。

使用控制插件

控制插件 对象是一种向 Dynamics 365 Business Central 客户端添加自定义功能(函数或 UI 自定义)的方式。控制插件可以与 Dynamics 365 Business Central 事件进行交互,并且可以为 AL 代码触发事件。

可以通过使用 tcontroladdin 代码片段在 AL 代码中定义一个控制插件(control add-in),该片段具有以下结构:

controladdin MyControlAddIn
{
    RequestedHeight = 300;
    MinimumHeight = 300;
    MaximumHeight = 300;
    RequestedWidth = 700;
    MinimumWidth = 700;
    MaximumWidth = 700;
    VerticalStretch = true;
    VerticalShrink = true;
    HorizontalStretch = true;
    HorizontalShrink = true;
    Scripts =
        'script1.js',
        'script2.js';
    StyleSheets =
        'style.css';
    StartupScript = 'startupScript.js';
    RecreateScript = 'recreateScript.js';
    RefreshScript = 'refreshScript.js';
    Images =
        'image1.png',
        'image2.png';

    event MyEvent()

    procedure MyProcedure()
}

如您从代码片段中看到的,当定义 controladdin 对象时,您需要设置 Scripts 属性,以包含您的控制插件的脚本(在 JavaScript 文件中)。这些脚本可以是本地的 .js 文件,也可以是通过 HTTP 或 HTTPS 引用的外部文件。

StartupScript 属性允许您调用必须在托管 controladdin 对象的页面加载时执行的脚本。您可以通过使用 StyleSheet 属性(允许您引用 CSS 文件)和 Images 属性(允许您将图像加载到插件中)来对 controladdin 对象进行样式设置。

在定义 Dynamics 365 Business Central 中 controladdin 对象的样式时,请始终参考以下链接中的 控制插件样式指南docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-control-addin-style

作为一个基本示例,这里我们创建一个将被放置在 Dynamics 365 Business Central 页面(具体来说是 Item Card)中的控制插件对象。

controladdin 对象由两个 JavaScript 文件组成:

  • Start.js,包含启动脚本,并在包含插件的 Dynamics 365 Business Central 对象启动时加载

  • Main.js,包含插件的业务逻辑

Start.js JavaScript 文件的定义如下:

init();
var controlAddin = document.getElementById('controlAddIn');
controlAddin.innerHTML = 'This is our D365BC control addin';
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod("ControlReady", []);

在这里,我们初始化控制插件对象,打印一些 HTML 文本到插件中,并使用 InvokeExtensibilityMethod 方法在包含插件的页面上调用 AL 触发器。

有关 InvokeExtensibilityMethod 方法的更多信息,请参阅 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/methods/devenv-invokeextensibility-method

Main.js JavaScript 文件定义如下:

function init()
{
    window.alert('INIT');
}

function HelloWorld()
{
    window.alert('HELLO WORLD FROM D365BC ADDIN');
}

现在,在 Visual Studio Code 中,我们创建 controladdin 对象,如下所示:

controladdin DemoD365BCAddin
{
    RequestedHeight = 300;
    MinimumHeight = 300;
    MaximumHeight = 300;
    RequestedWidth = 700;
    MinimumWidth = 250;
    MaximumWidth = 700;
    VerticalStretch = true;
    VerticalShrink = true;
    HorizontalStretch = true;
    HorizontalShrink = true;
    Scripts = 'Scripts/main.js';
    StyleSheets = 'CSS/stylesheet.css';
    StartupScript = 'Scripts/start.js';

    Images = 'Images/Avatar.png';
    event ControlReady()
    procedure HelloWorld()
}

在前面的代码中,我们可以看到以下内容:

  • VerticalStretch指定插件可以垂直放大。

  • VerticalShrink指定插件可以垂直缩小。

  • HorizontalStretch指定插件可以水平放大。

  • HorizontalShrink指定插件可以水平缩小。

  • MinimumHeight/MaximumHeight指定控件插件可以收缩或拉伸到的最小/最大高度。

  • MinimumWidth/MaximumWidth指定控件插件可以收缩或拉伸到的最小/最大宽度。

  • RequestedHeightRequestedWidth指定插件控件在页面中的高度和宽度。

controladdin 对象引用了前面描述的脚本,它还可以引用样式表(CSS)文件,在这里你可以处理插件的外观。在此示例中,我们使用了一个简单的样式表:

#controlAddIn
{
    width: 300px;
    margin-top: 25px;
    border: 2px;
    background-color: lightcoral;
    box-sizing: border-box;
    border: 2px solid red;
}

为了将 controladdin 对象添加到物料卡页面,我们创建了一个 pageextension 对象,并在此处添加了一个 usercontrol 字段,如下所示:

pageextension 50100 ItemCardExt extends "Item Card"
{
    layout
    {
        addlast(Item)
        {
            group(AddinGroup)
            {
                Caption = 'Control Add-in';
                usercontrol(DemoAddin; DemoD365BCAddin)
                {
                    ApplicationArea = All;
                    trigger ControlReady()
                    begin
                        CurrPage.DemoAddin.HelloWorld();
                    end;
                }
            }
        }
    }
}

usercontrol 字段触发 ControlReady 事件,从此事件中,我们调用 HelloWorld 方法(定义在 main.js 文件中)。

最终结果如下。当页面加载时,INIT 方法会被触发:

这是 Dynamics 365 Business Central 页面的 HTML,你可以在 div 元素中看到控件插件:

然后,插件触发 ControlReady 事件,我们的 JavaScript HelloWorld 函数被执行:

我们已经了解了如何创建视觉控件插件以及如何在 Dynamics 365 Business Central 页面中使用它们。在下一部分中,我们将看到如何创建和使用基于计时器的控件插件。

创建基于计时器的控件插件

控件插件也可以用于在 Dynamics 365 Business Central 页面中创建基于计时器的逻辑(称为 乒乓 逻辑),用于每 N 次执行一次业务逻辑。

在我们的 AL 项目中,我们这样定义 controladdin 对象:

controladdin D365BCPingPong
{
    Scripts = 'Scripts/pingpong.js';
    StartupScript = 'Scripts/start.js';
    HorizontalShrink = true;
    HorizontalStretch = true;
    MinimumHeight = 1;
    MinimumWidth = 1;
    RequestedHeight = 1;
    RequestedWidth = 1;
    VerticalShrink = true;
    VerticalStretch = true;

    procedure SetTimerInterval(milliSeconds: Integer);
    procedure StartTimer();
    procedure StopTimer();

    event ControlAddInReady();
    event PingPongError();
    event TimerElapsed();
}

start.js 文件包含插件的初始化:

$(document).ready(function()
{
    initializeControlAddIn('controlAddIn');
});

pingpong.js 文件包含我们的插件的 JavaScript 业务逻辑:

"use strict"
var timerInterval;
var timerObject;
function initializeControlAddIn(id) {
    var controlAddIn = document.getElementById(id);
    controlAddIn.innerHTML =
        '<div id="ping-pong">' +
        '</div>';
    pageLoaded();
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlAddInReady', null);
}

function pageLoaded() {
}

function SetTimerInterval(milliSeconds) {
    timerInterval = milliSeconds;
}

function StartTimer() {
    if (timerInterval == 0 || timerInterval == null) {
        Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('PingPongError', ['No timer interval was set.']);
        return;
    }
    timerObject = window.setInterval(ExecuteTimer, timerInterval);
}

function StopTimer() {
    clearInterval(timerObject);
}

function ExecuteTimer() {
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('TimerElapsed', null);
}

现在,我们可以将插件对象插入到 Dynamics 365 Business Central 页面(例如,物料卡)中,从这里我们可以调用插件方法,如下所示:

pageextension 50100 ItemCardExt extends "Item Card"
{
    layout
    {
        addlast(Item)
        {
            group(userControlTimer)
            {
                usercontrol(D365BCPingPong; D365BCPingPong)
                {
                    ApplicationArea = All;

                    trigger TimerElapsed()
                    begin
                        //Stops the timer when the timer has elapsed
                        CurrPage.D365BCPingPong.StopTimer();
                        //Here you can have your code that must be executed every tick
                        Message('Run your timer-based code here');
                        CurrPage.D365BCPingPong.StartTimer();
                    end;
                }
            }
        }
    }

    trigger OnAfterGetCurrRecord()
    begin
        //Sets a timer interval every 10 seconds
        CurrPage.D365BCPingPong.SetTimerInterval(10000);
        CurrPage.D365BCPingPong.StartTimer();
   end;
}

在这里,我们设置一个定时器间隔(例如,10 秒),然后启动定时器。当定时器的计时器滴答时间过去时,TimerElapsed 触发器会被调用,并执行你的自定义业务逻辑。当 TimerElapsed 触发器被触发时,重要的是停止定时器,以避免在消息仍然显示时触发新的事件(正如你所看到的,我们停止定时器,运行自定义代码,然后重新启动定时器)。

更多关于 控制插件 对象和属性的信息,请访问 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-control-addin-object

基于定时器的插件对于执行基于定时器的操作或刷新页面非常有用。

在下一部分中,我们将展示如何在 Dynamics 365 Business Central 中使用通知,以更好地处理标准 UI 中的错误和消息。

Dynamics 365 Business Central 中的通知

Dynamics 365 Business Central 允许你通过编程方式向用户在 Web 客户端界面中发送非侵入性的通知,以显示信息、消息或错误通知。这些通知是非模态的,因此不要求用户立即停止工作并执行一些操作。它们也可以在必要时被关闭。

通知将出现在用户当前工作页面顶部的 通知栏 中。应用程序可以向用户发送多个通知,这些通知会按时间顺序依次出现在通知栏中。它们将一直显示在通知栏中,直到用户采取行动、关闭它们或页面实例持续存在。

作为开发者,你可以通过使用 NotificationNotificationScope AL 对象以编程方式创建通知。

作为如何在 Dynamics 365 Business Central 中使用 Notifications 的示例,我们将在 采购订单 页面中创建一个通知,如果所选的 供应商 有余额到期,该通知将显示。

pageextension 对象定义如下:

pageextension 50100 PurchaseOrderExt extends "Purchase Order"
{
    trigger OnOpenPage()
    var
        Vendor: Record Vendor;
        VendorNotification: Notification;
        OpenVendor: Text;
        TextNotification: Label 'This Vendor has a Balance due. Please check before sending orders.';
        TextNotificationAction: Label 'Check balance due';
    begin
        Vendor.Get("Buy-from Vendor No.");
        Vendor.CalcFields("Balance Due");
        if Vendor."Balance Due" > 0 then begin
            VendorNotification.Message(TextNotification);
            VendorNotification.Scope := NotificationScope::LocalScope;
            VendorNotification.SetData('VendorNo', Vendor."No.");
           VendorNotification.AddAction(TextNotificationAction, Codeunit::ActionHandler, 'OpenVendor');
           VendorNotification.Send();
       end;
   end;
}

我们正在检查供应商是否有余额到期。如果有,则创建一个 Notification 对象。Notification 对象具有一个 Message 属性(定义将出现在 UI 中的通知内容)和 Scope 属性。现在,Scope 定义了消息将在哪里显示给用户,它可能是以下之一:

  • LocalScope(默认):通知将出现在用户当前的页面上。

  • GlobalScope(当前不支持):通知将无论用户在哪个页面上,都将显示。

在定义Notification对象时,我们使用SetData方法将数据属性值设置为通知(在此案例中是Vendor编号),并使用AddAction方法将操作添加到通知消息中(我们希望有一个操作能立即打开Vendor Card页面)。AddAction方法启动一个名为OpenVendor的方法,该方法在名为ActionHandler的代码单元中定义。

该代码单元对象定义如下:

codeunit 50100 ActionHandler
{  
    procedure OpenVendor(VendorNotification: Notification)
    var
        VendorCode: Text;
        Vendor: Record Vendor;
        VendorCard: Page "Vendor Card";
    begin
        VendorCode := VendorNotification.GetData('VendorNo');
        if Vendor.Get(VendorCode) then begin
            VendorCard.SetRecord(Vendor);
            VendorCard.Run();
        end;
    end;
}

在这里,当点击通知中的操作时,代码从Notification对象中获取VendorNo参数,检索Vendor记录,并通过传递获取的记录打开Vendor Card页面。

当你从 Dynamics 365 Business Central 打开采购订单,并且选择的供应商有未结余额时,你现在将看到以下通知:

如果你点击通知中的“检查余额到期”操作,选定供应商记录的Vendor Card页面将被打开,用户可以根据需要进行操作。

在为 Dynamics 365 Business Central 创建扩展时,通知极为重要,因为它们将允许你为用户提供更好的体验。

在下一部分中,我们将了解如何在 Dynamics 365 Business Central 中使用异步编程。

理解页面后台任务

Dynamics 365 Business Central 版本 15 引入了一项新功能,称为页面后台任务,用于处理异步编程。

页面后台任务允许你在页面上定义一个只读且长时间运行的过程,可以在后台线程中异步执行(与父会话隔离)。你可以启动任务并继续在页面上工作,而无需等待任务完成。以下图(由微软提供)显示了后台任务的流程:

页面后台任务具有以下属性:

  • 这是一个只读会话(不能写入或锁定数据库)。

  • 它可以被取消,并且具有默认和最大超时。

  • 它的生命周期由当前记录控制(当当前记录发生变化、页面关闭或会话结束时,它会被取消)。

  • 完成触发器在页面会话中触发(例如更新页面记录和刷新 UI)。

  • 它可以排队等待。

  • 传递给页面后台任务的参数以及返回的参数采用Dictionary<string,string>对象的形式。

  • 回调触发器不能在 UI 上执行操作,除了通知和控制更新。

  • 每个会话的后台任务数量有限(如果达到限制,任务将排队等待)。

创建页面后台任务的基本步骤如下:

  • 创建一个包含在后台执行的业务逻辑的代码单元。

  • 在必须启动任务的页面上,请执行以下操作:

    • 添加创建后台任务的代码(EnqueueBackgroundTask)。

    • 使用OnPageBackgroundTaskCompleted触发器来处理任务完成结果(在此处可以更新页面 UI)。

    • 你也可以使用OnPageBackgroundTaskError触发器来处理可能的任务错误。

下面是如何实现前述逻辑的示例。在客户卡片中,我们希望执行一个后台任务,计算所选客户的销售统计数据,而不阻塞 UI(在更复杂的场景中,可以想象从外部服务检索这些数据)。

我们的调用页面(客户卡片)将一个Dictionary<string,string>对象(键值对)传递给后台任务,键设置为CustomerNo,值设置为我们所选客户记录的No.字段。任务代码单元获取CustomerNo值,计算该客户的总销售额、销售的商品数量和已发货的商品数量,并返回一个Dictionary<string,string>对象,键设置为TotalSales,值为计算出的销售金额。

任务代码单元定义如下:

codeunit 50105 TaskCodeunit
{
    trigger OnRun()
    var
        Result: Dictionary of [Text, Text];
        CustomerNo: Code[20];
        CustomerSalesValue: Text;
        NoOfSalesValue: Text;
        NoOfItemsShippedValue: Text;
    begin
        CustomerNo := Page.GetBackgroundParameters().Get('CustomerNo');
        if CustomerNo = '' then
            Error('Invalid parameter CustomerNo');
        if CustomerNo <> '' then begin
            CustomerSalesValue := Format(GetCustomerSalesAmount(CustomerNo));
            NoOfSalesValue := Format(GetNoOfItemsSales(CustomerNo));
            NoOfItemsShippedValue := Format(GetNoOfItemsShipped(CustomerNo));
            //sleep for demo purposes
            Sleep((Random(5)) * 1000);
        end;
        Result.Add('TotalSales', CustomerSalesValue);
        Result.Add('NoOfSales', NoOfSalesValue);
        Result.Add('NoOfItemsShipped', NoOfItemsShippedValue);
        Page.SetBackgroundTaskResult(Result);
    end;

    local procedure GetCustomerSalesAmount(CustomerNo: Code[20]): Decimal
    var
        SalesLine: Record "Sales Line";
        amount: Decimal;
    begin
        SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
        SalesLine.SetRange("Sell-to Customer No.", CustomerNo);
        if SalesLine.FindSet() then
            repeat
                amount += SalesLine."Line Amount";
            until SalesLine.Next() = 0;
        exit(amount);
    end;

    local procedure GetNoOfItemsSales(CustomerNo: Code[20]): Decimal
    var
        SalesLine: Record "Sales Line";
        total: Decimal;
    begin
        SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
        SalesLine.SetRange("Sell-to Customer No.", CustomerNo);
        SalesLine.SetRange(Type, SalesLine.Type::Item);
        if SalesLine.FindSet() then
            repeat
                total += SalesLine.Quantity;
            until SalesLine.Next() = 0;
        exit(total);
    end;

    local procedure GetNoOfItemsShipped(CustomerNo: Code[20]): Decimal
    var
        SalesShiptmentLine: Record "Sales Shipment Line";
        total: Decimal;
    begin
        SalesShiptmentLine.SetRange("Sell-to Customer No.", CustomerNo);
        SalesShiptmentLine.SetRange(Type, SalesShiptmentLine.Type::Item);
        if SalesShiptmentLine.FindSet() then
        repeat
            total += SalesShiptmentLine.Quantity
        until SalesShiptmentLine.Next() = 0;
        exit(total);
    end;
}

接着,我们创建一个pageextension对象,扩展客户卡片,添加新的SalesAmountNoOfSalesNoOfItemsShipped字段(由后台任务计算),并添加代码来启动任务和读取结果。pageextension对象定义如下:

pageextension 50105 CustomerCardExt extends "Customer Card"
{
    layout
    {
        addlast(General)
        {
            field(SalesAmount; SalesAmount)
            {
                ApplicationArea = All;
                Caption = 'Sales Amount';
                Editable = false;
            }
            field(NoOfSales; NoOfSales)
            {
                ApplicationArea = All;
                Caption = 'No. of Sales';
                Editable = false;
            }
            field(NoOfItemsShipped; NoOfItemsShipped)
            {
                ApplicationArea = All;
                Caption = 'Total of Items Shipped';
                Editable = false;
            }
        }
    }
    var
        // Global variable used for the TaskID
        TaskSalesId: Integer;
        // Variables for the sales amount field (calculated from the background task) 
        SalesAmount: Decimal;
        NoOfSales: Decimal;
        NoOfItemsShipped: Decimal;

    trigger OnAfterGetCurrRecord()
    var
        TaskParameters: Dictionary of [Text, Text];
    begin
        TaskParameters.Add('CustomerNo', Rec."No.");
        CurrPage.EnqueueBackgroundTask(TaskSalesId, 50105, TaskParameters, 20000, PageBackgroundTaskErrorLevel::Warning);
    end;

    trigger OnPageBackgroundTaskCompleted(TaskId: Integer; Results: Dictionary of [Text, Text])
    var
        PBTNotification: Notification;
    begin
        if (TaskId = TaskSalesId) then begin
            Evaluate(SalesAmount, Results.Get('TotalSales'));
            Evaluate(NoOfSales, Results.Get('NoOfSales'));
            Evaluate(NoOfItemsShipped, Results.Get('NoOfItemsShipped'));
            PBTNotification.Message('Sales Statistics updated.');
            PBTNotification.Send();
        end;
    end;

    trigger OnPageBackgroundTaskError(TaskId: Integer; ErrorCode: Text; ErrorText: Text; ErrorCallStack: Text; var IsHandled: Boolean)
    var
        PBTErrorNotification: Notification;
    begin
        if (ErrorText = 'Invalid parameter CustomerNo') then begin
            IsHandled := true;
            PBTErrorNotification.Message('Something went wrong. Invalid parameter CustomerNo.');
            PBTErrorNotification.Send();
        end
        else
            if (ErrorText = 'Child Session task was terminated because of a timeout.') then begin
                IsHandled := true;
               PBTErrorNotification.Message('It took to long to get results. Try again.');
               PBTErrorNotification.Send();
            end
    end;
}

OnAfterGetCurrRecord触发器中,我们添加了启动后台任务所需的参数,并调用EnqueueBackgroundTask方法。EnqueueBackgroundTask方法创建并排队一个后台任务,该任务在页面会话的只读子会话中运行指定的代码单元(没有 UI)。如果任务成功完成,将调用OnPageBackgroundTaskCompleted触发器。如果发生错误,将调用OnPageBackgroundTaskError触发器。如果页面在任务完成前关闭,或任务中的页面记录 ID 发生变化,任务将被取消。

OnPageBackgroundTaskCompleted触发器中,我们从字典中获取TotalSales参数,并相应更新 UI(页面上的相应字段)。

我们已经了解了如何在 Dynamics 365 Business Central 页面中使用新的异步编程功能。这是一个重要特性,在许多场景中能提升应用的整体性能。

概要

本章中,我们讨论了许多高级话题,并看到了使用 AL 语言扩展实现特定任务的一些技巧,特别是如何处理文件和图片,使用 XMLports,创建和扩展角色中心与标题,处理 XML 和 JSON 序列化,使用 AL 代码调用 Web 服务和 API,以及如何从扩展发布 Web 服务。除此之外,我们还可以使用 Isolated Storage 存储扩展中的敏感数据,学习了如何创建控件插件,如何处理通知,以及如何在扩展中使用异步编程功能(页面后台任务)。

阅读完本章后,你现在能够进行高级自定义,以改善整体用户体验并处理不同的业务任务。

在下一章中,我们将了解如何为 Dynamics 365 Business Central 自定义、开发和发布报告。

第七章:使用 AL 进行报告开发

在本书中,我们介绍并分析了各种 AL 语言对象,并且展示了如何使用它们开发从简单到复杂的扩展。

在本章中,我们将深入研究一个特定对象,查看其属性、触发器和方法,并学习如何熟练使用它。这个对象就是报告对象

我们将提供有关使用哪些工具来设计和开发数据集和布局的概述,包括 Microsoft Word 用于 Word 布局,以及 Microsoft Report Builder 用于报告定义语言RDL)布局,并且会讨论与使用 Visual Studio Code 进行数据集开发以及 Word 和 RDL 布局相关的主要快捷键、技巧和窍门。

在本章中,我们旨在为您提供必要的信心,以便您能够开发 Dynamics 365 Business Central 报告扩展,解释如何使其性能达到最佳,并帮助您排查这一领域最常见的问题。

在本章中,我们将讨论以下主题:

  • AL 报告对象的结构

  • 用于 Word 和 RDL 布局的工具

  • 将现有的 C/AL 报告转换为 AL

  • 开发 RDL 或 Word 布局报告时的功能限制

  • 理解报告性能的考虑因素

AL 报告对象的结构

新报告的请求来自各个部门,并且以多种不同的形式呈现。大多数时候,用户心中已经有了他们希望数据呈现方式的构想。

然而,报告开发人员应始终牢记一些重要的要点。一切都与数据相关:

  • 检索:一个优秀的报告开发人员应具备良好的业务流程知识(数据是如何创建、修改和删除的)和数据拓扑知识(数据存储的位置)。数据可以从无法直接存储在 Dynamics 365 Business Central 表中的异构资源中检索。例如,您可能希望通过 HTTP 调用一个 Web 服务,收集数据库外部的一些数据,并将其存储在物理表或临时表中,然后再进行处理。

  • 处理:一些呈现的数据可能是数据聚合的结果,可能是从不同字段计算得到的,甚至可能是不同表格中值的连接结果。数据的检索和处理结果生成数据集。

  • 呈现:数据集从应用程序发送到报告查看器组件,报告查看器负责数据的呈现和显示。与数据集一起,一个报告定义文件(Report.rdlc)也会发送给报告查看器,以构建报告的内容。报告定义文件包含用于呈现报告的元数据结构和规则。尽管它的扩展名是(.rdlc),但这实际上是一个 XML 格式的平面文件。诸如报告生成器或 Visual Studio 之类的工具可以解析该 XML 文件,并以更易于人类阅读的方式创建报告结构的呈现。在此设计器中进行的每个操作都会导致编辑和更改 XML 文件。

使用 Dynamics 365 Business Central,还可以设计报告仅执行数据检索和处理,通常会在处理过程中提交对表的更改。没有数据呈现给用户,因此不需要布局,也不会创建数据集。

报告可以分为两大类:仅处理和基于数据集。

仅处理的报告没有任何布局。它们通常也没有在数据集中定义任何列,只用于处理数据。通常,使用代码单元对象也可以实现相同的结果,因为这些只是简单的代码库,并没有任何图形或用户界面UI)交互。举个简单的例子,你可以创建一个仅处理的报告,数据项会循环所有客户记录,并打印一个包含客户编号、姓名和电子邮件的扁平 JSON 文件。同样的结果也可以通过实现一个代码单元来完成,该代码单元包含一个声明为IF CustomerRec FINDSET THEN REPEAT UNTIL NEXT=0的循环。在这个循环中,可以编写一个包含完全相同信息的扁平 JSON 文件。

使用仅处理报告或代码单元的优缺点已经在这里列出:

仅处理报告 代码单元
易于实现 它更快。数据项循环结构是预定义的。 构建一个循环需要更多的开发活动。
灵活性 它仅限于数据项触发器。 它更灵活。
性能 它的性能较差。 它的性能更好。

一个报告对象具有以下树结构:

  • 请求页面

      • 分组

        • 字段
    • 操作

  • 数据集

    • 数据项

      • 属性

      • 触发器

        • 属性

        • 触发器

    • 标签

      • 属性
    • 布局

      • RDL

      • 词汇

安装标准的AL Language 扩展CRS AL Language 扩展工具后,我们可以使用treporttreport(CRS)代码片段来创建报告的原型,并检查与主内容区域相关的所有不同项:数据集和请求页面。布局仅是报告对象中对应输出(RDL 和/或 Word 文件)的引用,这些是通过其他工具而非 Visual Studio Code 创建的。我们将在本章稍后部分与它们一起工作。

在探索 AL 中报告对象的结构后,是时候了解用于 Word 和 RDL 布局的工具了。

用于 Word 和 RDL 布局的工具

Visual Studio Code 目前没有有效的扩展——尚未发布——来替代由 Dynamics 365 Business Central 开发团队支持的顶级 RDL 报告编辑器。该应用程序每六个月发布一次,始终保持最新状态,并且在撰写本文时,它部署了 Report Viewer 2017 和最新的基于 RDL 2016 模式的语法。

要开发 RDL 布局报告,你有两个选择:

要了解更多信息,请访问以下官方参考资料和这个有用的博客:

Word 布局功能是基于最新的 Aspose.Words (products.aspose.com/words) 组件构建的,并由后端服务器团队在应用程序中实现。设计和编辑必须使用支持 XML 映射的 Microsoft Word 版本。最低系统要求规定我们应使用Microsoft Word 2016或更高版本。

RDL 和 Word 布局功能

为了解释和展示 Dynamics 365 Business Central 支持的一些最重要的 RDL 和 Word 布局功能,我们将通过一个逐步示例进行讲解。

在第五章,为 Dynamics 365 Business Central 开发定制解决方案中,我们通过创建“客户类别”字段来扩展了项目账目表。现在是时候创建一个使用此扩展字段进行销售分析的报告了。请在扩展中创建一个名为.\Src\CustomerCategory\report的新目录。让我们学习如何实现。

第一部分 – 设计数据集

数据集通过指定数据项字段的列及其属性在报告中进行设计。它们从定性角度(数据以何种格式导出?)和定量角度(处理了多少行?)都有较大影响。这两者对报告性能都有明显的影响。

一个简短的示例是由十进制数据类型表示的。当在数据集中特定十进制数据类型时,始终会包括两个字段:十进制数据及其格式化方式。这意味着,在十进制字段的同时,你总是会有一个与之紧密绑定的重复文本变量。这将增加数据集的维度,从而影响报告性能。

什么是 Dynamics 365 Business Central 数据集的定义?这是我的定义:

“数据集就像是客户端内存中的一张表,其列由数据集部分中定义的所有列字段组成,而行则是所有有效的(未跳过的)记录,这些记录在 DataItems 中进行处理。”

让我们设计/创建我们的报告数据集:

  1. .\Src\CustomerCategory\report文件夹中创建一个名为Rep50111.ItemLedegerEntryAnalysis.al的新文件。

  2. 输入treporttreport(CRS)以启用报告片段。

  3. 添加以下数据项和列:

report 50111 "Item Ledger Entry Analysis"
{
    Caption ='Item Ledger Entry Analysis';
    UsageCategory=ReportsAndAnalysis;
    ApplicationArea = All;
dataset
{
    dataitem("Item Ledger Entry";"Item Ledger Entry")
    {
        column(ItemNo_ItemLedgerEntry;"Item Ledger Entry"."Item No.")
        {
            IncludeCaption = true;
        }
        column(PostingDate_ItemLedgerEntry;"Item Ledger Entry"."Posting Date")
        {
            IncludeCaption = true;
        }
        column(EntryType_ItemLedgerEntry;"Item Ledger Entry"."Entry Type")
        {
            IncludeCaption = true;
        }
        column(CustCatPKT_ItemLedgerEntry;"Item Ledger Entry"."Customer Category_PKT")
        {
            IncludeCaption = true;
        }
        column(DocumentNo_ItemLedgerEntry;"Item Ledger Entry"."Document No.")
        {
            IncludeCaption = true;
        }
        column(Description_ItemLedgerEntry;"Item Ledger Entry".Description)
        {
            IncludeCaption = true;
        }
        column(LocationCode_ItemLedgerEntry;"Item Ledger Entry"."Location Code")
        {
            IncludeCaption = true;
        }
        column(Quantity_ItemLedgerEntry;"Item Ledger Entry".Quantity)
        {
           IncludeCaption = true;
        }
        column(COMPANYNAME;CompanyName)
        {
        }
        column(includeLogo;includeLogo)
        {
        }
        column(CompanyInfo_Picture;CompanyInfo.Picture)
        {
        }
    }
}    

之后,我们添加标签:

labels
    {
        PageNo = 'Page';
        BCReportName ='Item Ledger Entry Analysis';
    }
    var
        CompanyInfo: Record "Company Information";
        includeLogo: Boolean;
}
  1. 按照原样构建并发布扩展,在你的在线或 Docker 容器沙箱中进行。

  2. 转到报告布局选择,并根据报告的 ID 或名称进行筛选。

  3. 选择“操作 | 自定义布局”。

  4. 单击“新建”,选择“插入 RDLC 布局”,然后单击“确定”。这将为报告添加一个空的 RDL 布局,并引用数据集结构。

  5. 选择“处理 | 导出布局”,并将Default.rdl报告保存到.\Src\CustomerCategory\report文件夹中。你可以将报告布局命名为Rep50111.ItemLedgerEntryAnalysis.rdl

  6. 将报告绑定到扩展中的 RDL 布局,并通过在 AL 报告对象中指定以下参数,使 RDL 成为默认布局:

report 50111 "Item Ledger Entry Analysis"
{
    DefaultLayout = RDLC;
    RDLCLayout = './Src/CustomerCategory/report/Report50111.ItemLedgerEntryAnalysis.rdl';
  Caption = 'Item Ledger Entry Analysis';
  1. 在 Visual Studio Code 中,右键单击 RDL 文件并选择“外部打开”。它将默认在你选择的程序中打开该.rdl文件。在此示例中,我们将使用 Microsoft SQL Server Report Builder 2016。

  2. 如果尚未启用,请确保在 Report Builder 2016 实例的“视图 | 显示/隐藏”功能区菜单中勾选了“报告数据”选项。在“报告数据”窗格中,展开参数数据集

你会注意到 Parameters 项是标签和字段标题(由数据集中的 IncludeCaption=true 属性指定)。DataSet_Results 显示了整个数据集的定义,并转置到 Report Builder IDE 中。以下是报告数据窗格的截图:

第二部分 – 创建简单的 RDL 布局

RDL 布局开发是 Dynamics 365 Business Central 提供的两种布局设计选项之一。与 Report Builder 相比,使用 Visual Studio 2017(加上 Microsoft RDLC Report Designer for Visual Studio 扩展)进行报告布局设计提供了更完整的开发体验。一个例子是“文档大纲”窗口,它显示了布局中控件的层次视图,并让你可以快速从一个控件跳转到另一个控件。

专门针对 RDL 布局开发,你可以从 SQL Server Reporting Services 的官方文档、官方课程资料或第三方书籍中找到非常详尽的资料。如果你希望彻底掌握 Dynamics 365 Business Central 的 RDL 布局,本部分包含了一些非常好的开发参考资料。即使它们大多来自较早版本的 Dynamics NAV 或 SQL Server Reporting Services,它们仍然包含了许多有用的提示,你应该在个人资料库中留一个位置给它们。

这里有一些 RDL 布局开发的参考资料:

Microsoft Dynamics NAV 2015 专业报告 Renders (Packt)
Microsoft Dynamics NAV 2009: 专业报告 Renders (Packt)
Microsoft Dynamics NAV 2009 INSIDE 报告 Gayer (Mbst)
专业的 Microsoft SQL Server 2008 报告服务 Misner (Microsoft)

第 2.1 部分 – 创建 RDL 报告头部

在这一部分,我们将一步步创建报告头部:

  1. 在 Report Builder 中,让我们设置报告属性:右键点击灰色开发区域的任意位置,选择“报告属性”:

  1. 修改“页面设置”参数,如下所示:

  1. 让我们添加一个页面头部:右键点击灰色开发区域的任意位置,选择“添加页面头部”。

  2. 右键点击页面头部的任意位置,选择“头部属性”。

  3. 按如下方式编辑页面头部属性:

  1. 点击正文部分并按如下方式修改正文大小属性:

    • 宽度:7.21205 英寸

    • 高度:1.93403 英寸

  2. 现在,让我们直接将一些报告项控件添加到头部。右键点击报告头部的任意位置,选择“插入 | 文本框”到页面头部。执行此操作六次。

  3. 修改文本框的以下属性:

名称 大小 位置 字体 填充 是否可增长 文本对齐 垂直对齐
txtReportName =Parameters!BCReportName.Value 7.5 cm; 0.423 cm 0 cm; 0.0005 cm Arial; 8 pt; Default; Bold; Default 0 pt; 0 pt; 0 pt; 0 pt False Default Middle
txtCompanyName =Fields!COMPANYNAME.Value 7.5 cm; 0.423 cm 0 cm; 0.45878 cm Arial; 7 pt; Default; Default; Default 0 pt; 0 pt; 0 pt; 0 pt False Default Middle
txtExecutionTime =Globals!ExecutionTime 3.15 cm; 0.423 cm 15 cm; 0.0005 cm Arial; 7 pt; Default; Default; Default 0 pt; 0 pt; 0 pt; 0 pt False Right Middle
txtPageNoLabel =Parameters!PageNo.Value 1.25271 cm; 0.423 cm 16.44729 cm; 0.4235 cm Arial; 7 pt; Default; Default; Default 0 pt; 0 pt; 0 pt; 0 pt False Left Middle
txtPageNumber =Globals!PageNumber 0.45 cm; 0.423 cm 17.7 cm; 0.45878 cm Arial; 7 pt; Default; Default; Default 0 pt; 0 pt; 0 pt; 0 pt False Right Middle
txtUserID =User!UserID 3.15 cm; 0.423 cm 14.8868 cm; 0.91298 cm Arial; 7 pt; Default; Default; Default 0 pt; 0 pt; 0 pt; 0 pt False Right Middle

这就是报表标题部分的样子:

第二部分 2.2 – 向 RDLC 报表主体添加表格控件

在本节中,我们将在报表主体中添加一个表格控件,以便以表格格式显示数据:

  1. 在报表主体中某个位置右键点击并选择插入。然后从菜单中选择“表格控件”。保持表格较小,因为我们将需要添加一些额外的列并手动调整宽度。

  2. 选择最后一列,右键点击并选择插入列,选择右侧:

  1. 重复步骤 2三次,以便总共添加七列。

  2. 通过更改以下表格属性保持表格对齐:

    • 位置:0.02584 in; 0.18403in

    • 尺寸:7.06693 in; 0.48958in

  3. 更改每列的列宽属性,从左到右如下:

1 2 3 4 5 6 7
宽度 (cm) 1,905 2,222 2,593 2,990 3,373 2,620 1,798

这应该是当前布局结果:

  1. 现在是时候适当设置表格属性,并将它们绑定到 Dynamics 365 Business Central 数据集。选择表格(点击表格后,会出现一个灰色的列/行区域,类似 Microsoft Excel 风格,只需点击该区域的左上角即可选择整个表格)。

  2. 在属性窗口中,点击属性页面按钮。

  3. 在常规选项卡中更改以下值,并点击确定:

    • 名称:tableItemLedgerEntry

    • 数据集名称:Dataset_Result

现在,表格已绑定到相应的数据集,并且具有自解释的名称。为 RDL 布局中的每个控件命名是非常有用的,因为你可以一眼看出控件的用途及其位置。现在,让我们将每个表格控件绑定到数据集的标题和字段值。

  1. 对表格的第一行(表头行)中的每个文本框,打开属性页面窗口并按如下方式更改前七个表头文本框的值属性:
txtItemNoCap =Parameters!ItemNo_ItemLedgerEntryCaption.Value
txtPostingDateCap =Parameters!PostingDate_ItemLedgerEntryCaption.Value
txtCustCatPKTCap =Parameters!CustCatPKT_ItemLedgerEntryCaption.Value
txtDocumentNoCap =Parameters!DocumentNo_ItemLedgerEntryCaption.Value
txtDescriptionCap =Parameters!Description_ItemLedgerEntryCaption.Value
txtLocationCodeCap =Parameters!LocationCode_ItemLedgerEntryCaption.Value
txtQuantityCap =Parameters!Quantity_ItemLedgerEntryCaption.Value
  1. 让我们将表格主体文本框控件绑定到数据集字段。对于表格第二行(表格主体)中的每个文本框,打开属性页面窗口并更改以下属性:
名称 格式
txtItemNo =Fields!ItemNo_ItemLedgerEntry.Value
txtPostingDate =Fields!PostingDate_ItemLedgerEntry.Value
txtCustCatPKT =Fields!CustCatPKT_ItemLedgerEntry.Value
txtDocumentNo =Fields!DocumentNo_ItemLedgerEntry.Value
txtDescription =Fields!Description_ItemLedgerEntry.Value
txtLocationCode =Fields!LocationCode_ItemLedgerEntry.Value
txtQuantity =Fields!Quantity_ItemLedgerEntry.Value =Fields!Quantity_ItemLedgerEntryFormat.Value
  1. 创建交替行颜色,使其更易于阅读。在表格详细信息中,选择之前表格中提到的七个文本框,并在每个文本框的相应属性中添加以下值:

    • 背景颜色: =iif(RowNumber(Nothing) mod 2, "AliceBlue", "White")

    • TextAlign: 右对齐

  2. 条件格式化txtQuantity文本框的颜色属性。选择txtQuantity并按如下方式更改颜色属性:

    • 颜色: =iif(Fields!Quantity_ItemLedgerEntry.Value <= 0,"Red","Black")
  3. 启用在每一页的开头显示表头的功能,以提高报告的可读性。点击列组中的小箭头并启用高级模式。

  4. 选择行组中的(Static)组:

更改(Static)组的属性如下:

  • 保持在一起: True

  • RepeatOnNewPage: True

第三部分 – 理解分组

在“分组”一词中,我们指的是基于一个或多个判别元素的聚合结果集的能力。分组通常用于显示每组的总计和/或聚合并计算总计(通常使用求和公式)。分组功能通常用于实现数据区域范围的控件中,如表格、矩阵、列表、图表和仪表。

在本节中,我们将为报告的表格控件创建组总计:

  1. 在“行组”部分,右键点击(详细信息)静态行组并选择添加组。选择父组...。

  2. 应该出现一个弹出窗口,要求您提供一个分组元素。选择 [ItemNo_ItemLedgerEntry] 字段值,并选择同时添加组头和组尾。然后点击确定:

  1. 选择我们刚刚创建的分组(默认情况下通常命名为 ItemNo_ItemLedgerEntry),右键点击,选择“组属性...”。

  2. 在“组属性”窗口中,将名称更改为 ItemNoGroup,然后点击确定。

  3. 选择 ItemNoGroup,右键点击,选择“添加组和子组...”。

  4. 弹出窗口出现,要求您提供一个分组元素。选择 CustCatPKT_ItemLedgerEntry,选择仅添加组头(不添加组尾),然后点击确定。

  5. 选择我们刚刚创建的分组,右键点击,选择“组属性...”。

  6. 在“组属性”窗口中,将名称更改为 CustCatPKTNoGroup。然后,转到高级选项卡并在递归父项框中设置 CustCatPKT_ItemLedgerEntry。点击确定确认:

  1. 所有这些操作应该默认创建了两个额外的不需要的列以显示分组元素。选择前两列,右键点击并选择删除列s

  2. 添加两个额外列的自动操作应该自动放大了报告正文:将其恢复到原始大小。设置以下值:

    • 大小: 7.21205 英寸; 1.93403 英寸
  3. 现在让我们添加组标题标签。选择 ItemNoGroup 头行中的第一个文本框,如下图所示:

修改其属性如下:

    • 名称: txtItemNoGroup

    • : =Fields!ItemNo_ItemLedgerEntry.Value

    • 背景颜色: LightBlue

  1. 选择 SourceNoGroup 头列中的第三个文本框,并修改其属性如下:

    • 名称: txtCustCatPKTGroup

    • : =Fields!CustCatPKT_ItemLedgerEntry.Value

    • 背景颜色: LightSteelBlue

  2. 选择底部右侧 ItemNoGroup 页脚行中的最后一个文本框,并修改其属性如下:

    • 名称: txtSumQuantity

    • : =Sum(Fields!Quantity_ItemLedgerEntry.Value)

    • 颜色: =iif(Sum(Fields!Quantity_ItemLedgerEntry.Value) <= 0,"红色","黑色")

    • 字体: Arial; 10pt; 默认; 粗体; 默认

    • 格式: `=Fields!Quantity_ItemLedgerEntryFormat.Value`

  3. 现在,对于所有分组行中的其余文本框(包括页头和页脚),请删除自动从详细信息组复制过来的BackgroundColor公式,=iif(RowNumber(Nothing) mod 2, "AliceBlue", "White")。当从属性窗口中删除公式时,BackgroundColor 将默认为无颜色。

  4. 我们几乎完成了分组工作。让我们为总计部分添加最后的修饰。在 ItemNoGroup 页脚行中选择第六个文本框,点击该文本框的属性页面弹出窗口,并更改边框属性,如下图所示:

  1. 对第七个文本框(txtSumQuantity)重复步骤 15

  2. 启用在呈现多页时显示表头的功能。点击列组中的小下拉箭头并启用高级模式。选择行组中的(静态)组,并更改静态成员的属性,如下所示:

    • KeepTogether: True

    • KeepWithGroupAfter

    • RepeatOnNewPageTrue

第四部分 – 构建一个简单的请求页面

如果报告对象已设置UseRequestPage = true;(默认值),那么将显示一个请求页面,让用户可以设置过滤器、收集用户信息,并填充影响报告处理和输出的 AL 变量或参数。

在请求页面中,您还可以添加动作以在运行报告之前执行一些额外的活动。典型示例包括运行一个页面的快捷方式以检查某些特定设置,或者在设置请求页面变量之前执行预处理任务的动作。

在 Visual Studio Code 中,添加(或如果您使用过treport片段,则更改)requestpage部分和OnPreReport()触发器部分。参考以下代码:

requestpage
{
    layout
    {
        area(content)
        {
            group(Options)
            {
                Caption = 'Options';
                field(includeLogo;includeLogo)
                {
                    Caption = 'Include company logo';
                }
            }
        }
    }
    actions
    {
    }
}
trigger OnPreReport()
begin
    if includeLogo then begin
    CompanyInfo.Get;  //Get Company Information record           
    CompanyInfo.CalcFields(Picture);  //Retrieve company logo
    end;
end;

第五部分 – 添加数据库图片

在本节中,我们将为报告添加在运行时显示图像的功能:

  1. 返回到报告生成器和 RDL 布局。在报告头部的中间,右键单击并选择“插入 | 图像”。

  2. “图像属性”弹出窗口将加载。在“常规”选项卡中,将名称更改为imgCompanyLogo,并输入下图所示的参数:

  1. 在“可见性”选项卡中,将显示选项更改为基于表达式显示或隐藏,并添加以下公式:
=iif(Fields!includeLogo.Value,iif(Len(Convert.ToString(Fields!CompanyInfo_Picture.Value))>0,False,True),True)

此表达式设置图像控件的可见性,如果用户选择包含公司徽标,并且从二进制格式转换为文本的结果返回大于 0 字节的值(简而言之,如果数据集中有图像数据)。

  1. 点击“确定”以确认您所做的修改。

  2. 设置图像控件的位置和大小如下:

    • 位置:3.29875 英寸;0 英寸

    • 大小:2.42708 英寸;0.56578 英寸

最终的报告布局应如下所示:

RDL 报告及其布局现已准备好进行部署。编译扩展(Ctrl + Shift + B)并将包部署到您的在线或容器化沙箱中(F5)。一旦网页客户端加载完成,只需搜索(Alt + Q)项目分类账条目分析报告,如果您希望在报告输出中包括公司徽标,可以填写请求页面:

如果点击预览,输出应如下所示:

第六部分 – 添加 Word 布局

在本节中,除了 RDL 布局,我们还将向报告中添加一个 Word 布局:

  1. 转到报告布局选择并筛选报告 ID 50111。

  2. 选择操作 | 自定义布局。

  3. 点击新建,选择插入 Word 布局,然后点击确定。这将向报告中添加一个空的 Word 布局,并将数据集结构作为 XML 映射进行引用。

  4. 选择处理 | 导出布局并将 Default.docx 报告保存到 .\Src\CustomerCategory\report 文件夹中。您可以将报告布局命名为 Rep50111.ItemLedgerEntryAnalysis.docx

  5. 将报告绑定到扩展中的 RDL 布局,改变默认布局为 Word,并通过在 AL 报告对象中指定以下报告属性来更改记录排序。考虑以下代码:

report 50111 "Item Ledger Entry Analysis"
{
DefaultLayout = Word;
RDLCLayout = './Src/CustomerCategory/report/Rep50111.ItemLedgerEntryAnalysis.rdl';
WordLayout = './Src/CustomerCategory/report/Rep50111.ItemLedgerEntryAnalysis.docx';
Caption = 'Item Ledger Entry Analysis';
UsageCategory = ReportsAndAnalysis;
ApplicationArea = All;
dataset
{
dataitem("Item Ledger Entry";"Item Ledger Entry")
{
 DataItemTableView=SORTING("Item No.") ORDER(Ascending);
  1. 在 Visual Studio Code 中,右键点击 DOCX 文件并选择外部打开。这将打开 Microsoft Word。

  2. 如果您还没有启用开发人员选项卡,请确保已启用。点击文件 | 选项 | 自定义功能区。在主选项卡中,勾选开发人员(自定义)功能区选项并点击确定:

  1. 返回 Word 布局,选择开发人员选项卡并点击 XML 映射窗格。

  2. 在自定义 XML 部分框中,从下拉菜单中选择最后一个条目,并展开 NavWorldReportXMLPart 根目录下的标签和 Item_Ledger_Entry 节点。

它应如下所示:

让我们为扩展添加一个 Word 布局列表报告:

  1. 在 XML 映射窗格中右键点击,选择 Item_Ledger_Entry 下拉菜单,并选择 CompanyInformation_Picture。右键点击它,选择插入内容控件 | 图片。这将为公司信息徽标在 Word 布局中添加占位符。

  2. 添加一行。在 XML 映射窗格中右键点击,选择标签,并选择 BCReportName右键点击它,选择插入内容控件 | 纯文本。

  3. 添加一行。在 XML 映射窗格中右键点击,选择 Item_Ledger_Entry 下拉菜单,并选择 COMPANYNAME。右键点击它,选择插入内容控件 | 纯文本。

  4. 添加一行。在 Word 功能区,点击插入 | 表格,并创建一个 7 行 2 列的表格。

  5. 在第一行,对于每个单元格,将光标放在 XML 映射窗格中,选择标签,右键点击每个标题元素,选择插入内容控件 | 纯文本。以下是列标题的列表:

    • ItemNo_ItemLedgerEntryCaption

    • PostingDate_ItemLedgerEntryCaption

    • CustCatPKT_ItemLedgerEntryCaption

    • DocumentNo_ItemLedgerEntryCaption

    • Description_ItemLedgerEntryCaption

    • LocationCode_ItemLedgerEntryCaption

    • Quantity_ItemLedgerEntryCaption

  6. 选择表格的第二行。在 XML 映射窗格中,选择 Item_Ledger_Entry 元素,右键单击它,然后选择“插入内容控件 | 重复”。这将使行元素在 Item Ledger Entry 数据集中的每个记录上重复。

  7. 在第二行的重复元素内,在 XML 映射窗格中的每个单元格中放置光标,展开“Item_Ledger_Entry”下拉菜单,右键单击字段元素,然后选择“插入内容控件 | 纯文本”。以下是列字段的列表:

    • ItemNo_ItemLedgerEntry

    • PostingDate_ItemLedgerEntry

    • CustCatPKT_ItemLedgerEntry

    • DocumentNo_ItemLedgerEntry

    • Description_ItemLedgerEntry

    • LocationCode_ItemLedgerEntry

    • Quantity_ItemLedgerEntry

结果如下:

保存并关闭 Word 文件。

现在,Word 布局已经准备好部署。构建扩展(Ctrl + Shift + B),并将包部署到在线或容器化沙箱中(F5)。当 Web 客户端加载后,搜索(Alt + Q)“报告布局选择”,筛选当前报告,并将所选布局更改为 Word(内置),如下所示:

在搜索“Item Ledger Entry Analysis”报告后,选择将公司 logo 包含在报告输出中,并将其打印到 Microsoft Word。以下是结果:

本节结束,我们从零开始创建并美化了一个报告。在下一节,我们将看看开发者最常见的任务:复制并重构一个现有的报告,以应对客户的功能需求。

将现有的 C/AL 报告转换为 AL

对现有报告进行小的修改是非常常见的任务。与创建新页面和代码单元一起,这可能是开发者最频繁且重复性最高的工作之一。

假设我们希望对标准销售订单报告进行以下更改:

  • 在销售订单头中显示客户类别字段

  • 在销售行中,对于 100%折扣的项目行,打印 GIFT

当前版本的 AL 语言扩展没有类似于ReportExtension对象的工件,无法用于修改或合并现有报告。因此,即使我们只需要对数据集和/或布局做很小的修改,也总是需要从头开始创建一个全新的报告。

完成此任务的最简单方法是复制一个现有的报告,并在将其从 C/AL 转换为 AL 后,赋予其不同的 ID。

任务的第一部分(复制现有报告)在 Dynamics 365 Business Central 2019 年 10 月更新中非常简单,因为所有遗留的 C/AL 报告都已转换为其等效的 AL 对象,并已包含在基础应用程序扩展中。

对于本地版,你可以在 DVD 安装文件夹的 \Applications\BaseApp\Source 目录中找到它们。只需解压名为 Base Application.Source.zip 的文件,并搜索你需要复制的标准报告,以及它的 .rdl.docx 文件。

如果你使用的是 Docker 容器化沙盒,可能会想到使用一个由 NavContainerHelper 库实现的强大 cmdlet,该 cmdlet 特别适用于 Dynamics 365 Business Central 2019 年 10 月更新:Create-AlProjectFolderFromBcContainer。这个 cmdlet 会将基础应用程序源代码解压到你选择的本地文件夹中。以下是一个非常简单的代码片段:

#Set local variables
#####################
$alFolder = 'C:\APP\BaseApp'
$existingContainerName = "BC15ITCU1"

#Extract Base Application into a folder of your choice
############################################################
Create-ALProjectFolderFromBcContainer -containerName $existingContainerName `
-useBaseLine `
-useBaseAppProperties

$alFolder 中,你可以选择所需的 AL 报告项,并重新编号和自定义它们。

在 2019 年 4 月或更早的 Dynamics 365 Business Central 更新中,微软提供了一个非常有用且强大的命令行工具 txt2al.exe,它帮助将 C/AL 对象转换为 AL 语法。但还有更多功能。

New-NavContainer cmdlet 是包含在 NavContainerHelper PowerShell 库中的,我们在第三章 在线和基于容器的沙盒中讨论过,它还实现了一个非常有用的开关(-includeAL),该开关可以从容器中提取所有 C/AL 对象的 TXT 格式,并使用 txt2.al.exe 逐个转换它们。

自从 Dynamics 365 Business Central 2019 年 10 月更新后,微软已停止支持 C/AL 和 CSIDE 开发环境,因此 txt2al.exe 仅存在于基于 Dynamics 365 Business Central 2019 年 4 月更新或更早版本的部署中。

这是一个非常有用的脚本,它将所有标准的基础 C/AL 对象转换为其 AL 对应项:

#Set local variables
#####################
$mylicense = 'C:\DOCKER\LICENSE\BC14.flf'
$imageName = "mcr.microsoft.com/businesscentral/sandbox:1904"
$containerName = "BC14W1-LATEST"
#Create a Docker sandbox container with converted AL objects
############################################################
New-NavContainer -accept_eula `
-imageName $imageName `
-containerName $containerName `
-licenseFile $mylicense `
-auth NavUserPassword `
-shortcuts None `
-includeAL

以下是 PowerShell 标准输出的一个片段,显示当启用 -includeAL 开关时发生的情况:

…
*Export Objects with filter 'Id=1..1999999999' (new syntax) to C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-newsyntax\objects.txt (container path)* *Split C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-newsyntax\objects.txt to C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-newsyntax (container paths)* *Converting objects file from OEM(437) to UTF8 before splitting* *Converting object files from UTF8 to OEM(437) after splitting* *Converting files in C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-newsyntax to .al files in C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-al with startId 50100 (container paths)* *Converting my delta files from OEM(437) to UTF8 before converting* *txt2al.exe --source="C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-newsyntax" --target="C:\ProgramData\NavContainerHelper\Extensions\Original-14.1.33107.0-W1-al" --rename --extensionStartId=50100 --dotNetAddInsPackage="C:\ProgramData\NavContainerHelper\Extensions\BC14W1-LATEST\coredotnetaddins.al"* *Converting my delta files from UTF8 to OEM(437) after converting* *Creating .net Assembly Reference Folder for VS Code* *Copying DLLs from C:\Windows\assembly to assemblyProbingPath* *Copying DLLs from C:\Program Files\Microsoft Dynamics NAV\140\Service to assemblyProbingPath* *Copying DLLs from C:\Program Files (x86)\Microsoft Dynamics NAV\140\RoleTailored Client to assemblyProbingPath* *Copying DLLs from C:\Program Files (x86)\Open XML SDK to assemblyProbingPath* …

输出文件位于 C:\ProgramData\NavContainerHelper\Extensions\Original-<ApplicationVersion>-<localization>-al 目录中。

在此目录中,我们需要查找已转换的订单确认的 AL 标准对象。只需在文件资源管理器的搜索框中输入 Standard Sales Order,即可找到三个可复制并重命名的对象:

  • Report 1305 - Standard Sales – Order Conf..al

  • Standard Sales – Order Conf..docx

  • `Standard Sales – Order Conf..rdlc`

以下截图显示了这一点:

将这些文件复制到 .\Src\CustomerCategory\report 目录,并按照以下方式重命名它们:

  • Rep50115.PacktSalesOrderConf.al

  • Rep50115.PacktSalesOrderConf.docx

  • Rep50115.PacktSalesOrderConf.rdl

下一步是重新编号 AL 报告,在允许的自定义范围内,以避免重复的对象 ID,然后更改其属性,使用适当的 .docx.rdl 文件。

在 Visual Studio Code 中编辑 Rep50115.PacktSalesOrderConf.al,并按以下代码更改名称和属性:

report 50115 "Packt Sales - Order Conf."
{
    WordLayout = './Src/CustomerCategory/report/Rep50115.PacktSalesOrderConf.docx';
    RDLCLayout = './Src/CustomerCategory/report/Rep50115.PacktSalesOrderConf.rdl';
    Caption = 'Packt Sales-Confirmation';
    UsageCategory=ReportsAndAnalysis;
    ApplicationArea=All;
    AdditionalSearchTerms='Packt Sales Order report';

    DefaultLayout = Word;
    PreviewMode = PrintLayout;
    WordMergeDataItem = Header;

现在,我们必须让应用程序理解,每次调用标准报告 1305 时,都应该用自定义报告 50115 替代。这可以通过订阅一个名为 OnAfterSubstituteReport 的特定事件轻松实现,该事件是通过 ReportManagement 代码单元发布的。

编辑 Cod50100.CustomerCategoryMgt_PKT.al 文件,该文件位于 .\Src\CustomerCategory\codeunit,并添加以下代码:

[EventSubscriber(ObjectType::Codeunit, Codeunit::ReportManagement, 'OnAfterSubstituteReport', '', false, false)]
local procedure OnAfterSubstituteReport(ReportId: Integer; var NewReportId: Integer)
begin
    if ReportId = Report::"Standard Sales - Order Conf." then
        NewReportId := Report::"Packt Sales - Order Conf.";
    end;

此时,如果你构建 (Ctrl + Shift + B) 并发布 (F5) 扩展,整个(代码和布局)标准销售订单报告将自动在后台被自定义报告替代。

现在,是时候对自定义销售订单报告代码和 Word 布局进行适当的修改,以便在文档头部打印客户类别字段,并且对于每个有 100% 折扣的项行打印 GIFT。

在 Visual Studio Code 中编辑 Rep50115.PacktSalesOrderConf.al,并在 Header 数据项的数据集部分添加以下列:

column(CustomerCategory_PKT;Cust."Customer Category_PKT")
{
}           
column(CustomerCategory_PKT_Lbl;Cust.FIELDCAPTION("Customer Category_PKT"))
{
}

为礼品描述添加标签:

GiftLbl: Label 'GIFT';

然后,更改 OnAfterGetRecord 触发器中与折扣百分比相关的 IF 条件语句,以便 Line 数据项使用:

if "Line Discount %" = 0 then
    LineDiscountPctText := ''
else
    LineDiscountPctText := StrSubstNo('%1%',-Round("Line Discount %",0.1));
Replace the preceding code with the following case statement:
case "Line Discount %" OF
    0    : LineDiscountPctText := '';
    100  : LineDiscountPctText := GiftLbl;
    ELSE
    LineDiscountPctText := StrSubstNo('%1%',-Round("Line Discount %",0.1));
END;

一切已经准备好,可以按预期工作了。我们只需在报告布局中显示客户类别列。

只需构建应用程序 (Ctrl + Shift + B) 并在在线沙盒中发布 (F5) 它。

转到报告布局选择,筛选报告 50115 的列表,然后点击自定义布局. 以下截图显示了筛选后的报告布局选择:

创建一个新布局并选择插入 Word 布局:

导出我们刚刚创建的自定义 Word 布局。在 Word 布局中,删除 CustomerAddress8 字段,并添加以下内容:

  • CustomerCategory_PKT_Lbl 作为纯文本

  • 一个空格,一个冒号(:),再加一个空格

  • CustomerCategory_PKT 作为纯文本

这是它的显示方式:

保存布局并将其导入回客户布局记录。

如果你打算将此版本作为标准扩展部署的一部分使用,你也可以在标准扩展中使用此 Word 布局,并用此布局替换原始报告 50115 的 .docx 文件。

运行任何包含礼品行和客户类别的销售订单,检查结果。它应该如下所示:

这部分内容是关于将 C/AL 报告转换为 AL 的最后一个环节。接下来,我们来学习 RDL 和 Word 报告的功能限制如何工作。

开发 RDL 或 Word 布局报告时的功能限制

基本上——而且历史上——专业的报告开发应该通过使用 Visual Studio 和已安装的 RDLC 报告扩展来开发 RDL 报告布局。Word 文档布局的限制比 RDL 更多,其主要优点是它非常流行,并且易于被高级用户采用。

在开发布局时,您可能会遇到的主要痛点通常与文档相关。最常见的问题如下:

  • 始终保留页眉和页脚空间:报告的页眉和页脚具有静态内容,并且已经设计为如果存在,它们将在每个页面上始终显示。然而,在 RDL 中,您可以在主体部分使用典型的 SetData 函数,在页眉中使用 GetData 函数。这个技巧的一个示例可以在标准对象 206 销售发票的 RDL 布局中找到。

  • 无法轻松模拟旧经典客户端报告中的 PlaceInBottom 属性:在开发文档时,您可能会被要求生成整个文档,但总计(如增值税、每组总计等)必须始终打印在最后一页的底部,并且总是在相同的位置。这个问题出现的原因是,Dynamics 365 Business Central 文档报告可以被视为一批多个文档和多个副本,而不是单一报告。这意味着重新编号页面的拆分必须针对每个文档编号和副本编号进行。一个例子是报告 205 销售订单

    在标准的文档报告中,总计通常不会放在页面底部;它们是在最后一条文档线之后打印的。这意味着它们可能打印在页面的任何位置,甚至可能出现在额外的页面上。没有可行的方法可以将文档批次及其总计始终打印在最后一页的底部。

  • 实现汇总总计的复杂性:通常,在文档中,您可能希望在页面底部实现运行总计,并在下一页的顶部报告这些总计,例如打印账簿或一些交易条目时的“待续”或“继续”标签。

    使用旧经典客户端报告设计器时,您可以通过添加 transheader / transfooter 来解决这个问题。随着 RDL 或 Word 布局报告的出现,这些元素不再存在。使用 Word 布局报告时,没有可行的解决方案。使用 RDL,您可能会实现运行总计,但仅限于页眉和页脚部分。这是一个旧的但有用的开发参考:blogs.msdn.microsoft.com/nav/2011/06/06/transfooter-and-transheader-functionality-in-rdlcssrs-reports-revisited/

仅考虑 Word 布局时,您可能经常遇到的设计限制如下:

  • 没有条件格式化:如果你需要设置控件的可见性、更改布局中的字段值或设置任何条件格式化,这是在 Word 中无法实现的。一个典型的例子是当你需要打印一个空白字符而不是零时。这必须在数据集中完成,且值必须已经以字符串形式(空白或数值)发送到文档。

  • 没有合计公式:没有等同于 RDL 的 =SUM 函数或类似的功能。值的计算必须通过 AL 代码完成,然后将结果添加到数据集中。

  • 在同一个表格中嵌套重复项是一个挑战:由于无法有条件地触发表格行的可见性,因此在表格中创建一个既美观又能展示不同数据项的布局是一个折中方案。一个典型的例子是在销售行下的评论行,或者在销售出货行下的额外描述/条形码行。

    这些可能通过在行重复项中添加一个嵌套的表格结构重复项来实现,这样可以映射额外的附加信息。嵌套表格结构可以自由定义,只要它横跨外部表格中合并的单元格集合。当你开发时,要注意,如果没有额外的附加信息,至少会包含一个空的嵌套结构实例。

最佳的解决方案是使用数据集中的缓冲区表,并创建与布局中需要打印的精确行结构相同的结构。

如果你遇到一个或多个这样的限制,那么可能最好的和最简单的解决方案是开发一个 RDL 布局报告。

理解报告的性能考虑因素

使用 Dynamics 365 Business Central 在线时,有一些性能考虑因素需要考虑。

目前,Word 和 RDL 内置布局在使用 SAVEASPDFSAVEAS 语句时,都在同一应用程序域进程中渲染。

由于 RDL 布局可能会启用一些外部代码构件,这些构件可能会影响同一应用程序域中的数据,因此决定以隔离模式运行每个自定义 RDL 报告布局。值得注意的是,如果你开发了一个报告并声明 DefaultLayout 为 RDL 和 RDLLayout 属性,这被视为内置布局,应在同一应用程序域中渲染。

无论是内置的还是自定义制作的,Word 布局都不会在隔离模式下运行。

启用自定义 RDL 布局的应用程序域隔离提供了一个更加安全和可靠的处理环境。然而,缺点是可能会显著增加渲染时间。

每当你为 Dynamics 365 Business Central 在线开发 RDL 报告时,必须在在线沙箱或使用 customsettings.config 文件服务器参数 ReportAppDomainIsolation 设置为 true 的 Docker 容器沙箱中,测试使用 SAVEASPDFSAVEAS 语句的性能。

其他适用于 Word 和 RDL 报表布局的性能考虑因素基于数据集优化。

这些是开发 AL 报表数据结构时需要牢记的公式。我们假设数据集是一个内存表(X 轴 = 列Y 轴 = 行):

Smaller Dataset = Better Performance
Smaller Dataset = Reduce X axis (columns) + Reduce Y axis (rows)
Better Performance = Optimize (reduce) the number of columns in Dataset + Optimize (reduce) the number of records processed in DataItems

你可以使用以下链接来优化标准报表或你自定义的报表:

总结

在本章中,我们学习了如何使用哪些工具来开发 AL 报表。我们了解了如何创建 RDL 和 Word 布局,以及支持的工具。我们对报表的创建和使用有了更深入的理解。我们还解释了如何使用txt2al.exe将 C/AL 报表转换为 AL 报表,并通过实际示例重构它以便在标准应用中重用。

最后,我们了解到在 AL 报表开发中存在一些报表限制,几种解决方法,以及一些性能考虑因素,这些都能帮助你成为 AL 报表开发的高手。

在下一章中,我们将学习如何为 Dynamics 365 Business Central 构建自动化测试,以检查应用程序的业务流程一致性,并提高我们开发解决方案的健壮性。

第三部分:调试、测试与发布管理(DevOps)

在本节中,我们将深入概述如何从 Visual Studio Code 调试扩展,以及如何处理安装和升级过程。接着,你将学习如何编写自动化测试,并了解如何在开发过程中处理源代码管理和 CI/CD。

本节包括以下章节:

  • 第八章,安装和升级扩展

  • 第九章,调试

  • 第十章,使用 AL 开发自动化测试

  • 第十一章,与 Business Central 的源代码管理和 DevOps

第八章:安装和升级扩展

获取扩展的安装状态是一个四步过程:发布、同步、升级(如果需要)、安装。在本章中,我们将详细讲解这些步骤。

安装一个简单的或甚至复杂的扩展,可能比其维护要简单得多,即使是最复杂的代码组件。扩展的维护是通过扩展的升级过程完成的。可能需要进行升级,原因包括引入新功能、修复漏洞,或在 SaaS 环境中,通常是由于基础应用的依赖关系发生了变化。

在本章中,我们将探讨基础扩展安装和复杂依赖关系升级,帮助 AL 开发人员更好地理解如何检查他们的SaaSified私有知识产权。

由于 Dynamics 365 Business Central 在线产品每月大约进行一次持续的升级,因此它是一个不断变化和演化的产品。因此,合作伙伴和客户应该准备好升级他们的扩展,以应对平台和应用程序的每月更新。

在本章中,你将学习如何执行以下操作:

  • 在在线沙箱和生产环境中部署扩展

  • 检查已发布、已同步和已安装扩展的状态

  • 处理安装代码单元

  • 通过升级代码单元处理破坏性更改

  • 定义应用程序依赖关系

  • 处理一个简单的升级场景

  • 升级带有依赖关系的扩展

部署扩展

就像在任何其他编程语言中一样,术语非常重要。明确理解和区分扩展的不同部署阶段和状态,对于针对适当的故障排除非常关键,必要时进行相应的处理。

根据开发生命周期和部署情况,扩展分为两类:

  • 每租户扩展(PTE):这类似于老式的定制开发,为每个客户量身定制。开发通常在包含生产配置和数据副本的沙箱环境中进行。CSP 合作伙伴和/或其经销商与客户共同管理开发和部署生命周期。

    尽管从历史上看,这是本地部署 ERP 开发中最常见的场景,但我们鼓励合作伙伴创建自己的标准扩展,通过 Dynamics 365 Business Central Marketplace 进行部署和/或销售。

  • AppSource 扩展:这些扩展发布在 AppSource 上,其目的是供任何/所有租户在特定系统上使用,通过官方的 Dynamics Marketplace 获取。AppSource 扩展在被批准并作为全球(全球)可用于生产租户部署之前,必须经过严格的技术和市场验证流程。

    与 PTE 不同,AppSource 认证的扩展已经由 Microsoft 发布在每个应用程序数据库中,随时可以按需为每个租户安装。这使得扩展的部署过程更快速、更可靠、更专业。

PTE 可以通过两种方式进行部署:

  • 自动

  • 手动

让我们在这里看看这两种部署方式。

自动

在 Visual Studio Code 中,你需要设置launch.json来针对特定的目标沙盒租户环境("tenant": "")工作,并提供适当的凭证来建立连接。

由于你可能在同一环境中有多个沙盒,你还可以指定你希望连接的租户,以便下载符号和/或发布扩展("sandboxName": "")。

在在线生产环境中这是不可能的,只能在沙盒中进行。这仅适用于 PTE。通过这种方式部署的 PTE 通常被称为开发者扩展,因为它们只能针对开发者沙盒进行部署。

默认情况下,部署是通过对现有架构应用同步来执行的("schemaUpdateMode": "Synchronize")。因此,默认选项有助于保留数据,以防开发者做出的一些更改不涉及破坏性更改。然而,也可以完全清理任何先前的扩展部署,并从头开始进行部署("schemaUpdateMode": "Recreate"),甚至强制同步("schemaUpdateMode": "ForceSync"),以保证快速部署并进一步测试扩展。

ForceSync 必须小心使用,并应尽可能避免。即使它加快了部署和测试速度,它也无法在需要升级代码单元作业的生产环境中工作。有时候,懒惰的开发者可能会使用 ForceSync,并忘记在生产环境中处理适当的升级和同步。

手动

典型的生产部署是手动进行的。从 2019 年 4 月的更新开始,在在线沙盒中也可以进行部署,具体如下:

  1. 连接到你的生产或沙盒租户。

  2. 搜索(Alt + Q)进入扩展管理页面。

  3. 在扩展管理页面中,点击管理操作组(如果需要,可以将其固定),选择上传扩展,然后选择你开发的扩展(.app)。下图显示了上传和部署页面:

在截图中,我们可以看到,Deploy to 参数非常重要,因为它触发平台何时需要部署扩展。

通过指定当前版本,部署会立即执行,扩展会尝试进行同步。开发人员可以检查任务是否成功完成,或者在部署状态页面中查看是否有失败的错误。

选择下一个次要版本或下一个主要版本将推迟部署,直到下一个次要或主要更新发生时。

若想了解更多有关此主题的信息,请访问 demiliani.com/2019/04/29/dynamics-365-business-central-and-per-tenant-extensions-check-page-control-names-between-platform-upgrades/

一些部署提示

开发人员必须牢牢记住,Dynamics 365 Business Central 是一个多租户环境,声明了以下范式:

  • 应用程序和数据是解耦的,并存储在不同的数据库中。

  • 单个应用数据库,与应用服务绑定,可以为数百个租户(客户数据数据库)提供服务。这是多租户一对多概念的支柱。

  • 在应用程序中,数据库存储为扩展清单(存储扩展定义的记录,定义以app.json文件中的形式出现)。

  • 在与应用数据库绑定的服务中挂载并同步租户,会将这些扩展暴露给租户。

  • 租户扩展随后可以由用户选择进行安装。

每当发生次要更新(通常为每月更新)或主要更新(通常为每六个月更新)时,扩展将会被卸载。随后,租户将从旧的应用服务中卸载,并挂载到与新应用版本绑定的另一个应用服务中。

即使新的应用服务尚未发布 PTE,租户结构及其数据也会完全保留。

每当此操作发生时,开发人员只需要重新发布并安装扩展,以重新同步所有内容。正如前面提到的,这适用于所有在线沙箱。

部署在生产环境中的 PTE 具有更广泛的作用域,扩展会自动移植到新的应用服务中。在这种情况下,如果选择将扩展与下一个次要/主要版本一起部署,当升级发生时,将会触发安装新扩展。

若想深入了解此主题,请访问 demiliani.com/2019/01/24/dynamics-365-business-central-tenant-upgrade-extensions-disappeared-in-sandbox-environment/

PTE 必须保持在整个生态系统中的唯一性,开发人员不应违反这一原则。PTE 或任何扩展的一般唯一性由以下值的组合定义,在app.json文件中:

  • 包 ID:每次扩展构建时分配给.app 文件的新 GUID(Ctrl + Shift + B)。

  • 应用 ID:定义扩展的唯一 GUID。

  • 名称

  • 发布者

  • 版本:以x.x.x.x的形式表示。

只要更改其中一个值,该扩展就被视为新扩展。如果开发人员考虑重用 PTE 用于另一个客户租户,则必须处理此独特性模式。

尝试在另一个租户中部署完全相同的 PTE,甚至在重新构建包后再次部署,可能会导致失败,并显示类似于此处所示的错误:

在这种情况下,可能只需增加应用程序版本(bump)即可使部署顺利进行。

如果这发生在 PTE 从未安装过的租户中,则根本原因可能是在同一应用程序服务中存在重复项。为解决此问题,开发人员必须更改应用程序 ID 和名称,并重新部署以成功安装 PTE。

深入了解此问题,请访问 demiliani.com/2019/03/14/dynamics-365-business-central-online-sandbox-makes-you-crazy-maybe-remember-these-points/

部署扩展时可能会出现另外两个错误消息。这些是:

  • 由于租户 <Tenant Id> 已经使用其它版本,因此无法安装扩展 <name> by <publisher>

  • 应用程序扩展与应用程序 ID '<Extension Id>' 已配置为全局租户使用。

第一个错误是因为尝试发布具有与系统中另一个 Per tenant 扩展相同的应用程序 ID 和版本参数,但可能内容不同。解决此错误的最简单方法是增加扩展的版本。

第二个错误是因为尝试使用与分配给 AppSource 扩展或标准 Microsoft 拥有的扩展相同的 ID 来上传 Per tenant 扩展。在这种情况下,解决方案非常简单:更改扩展 ID 并重新发布。

在下一节中,我们将看到在部署扩展时在幕后发生了什么。

部署在幕后进行

在在线沙箱中部署任何类型的扩展意味着以下情况:

  • 该扩展发布在特定的应用程序服务中,具有特定的应用程序版本。

  • 相同的扩展已在沙箱租户中同步(Azure SQL 数据库结构与对象元数据定义相匹配)并安装。

扩展部署可以总结为四个阶段:

  • 发布: 扩展已上传到应用程序数据库中,在发布过程中将其挂载到参考租户。在租户数据库中的表的物理结构没有任何更改。

    发布意味着声明对象内容(元数据)和需要特定租户按需应用的数据库结构更改。这些内容和相关更改由 AL 对象(例如表、页面和页面扩展)定义。

  • 同步:发布的内容可能会直接传输到租户(例如页面或代码单元),或者它们可能需要对租户数据结构进行额外的操作(例如表或表扩展)。同步过程中的最重要步骤是对基础数据库进行更改,通常是根据 AL 对象定义的表结构。这一步是创建表或在租户数据库中添加或修改新字段的步骤。

  • 数据升级:如果应用程序版本发生更改,则在同步后必须进行数据升级。在运行数据升级时,应用程序将搜索升级代码单元并执行其中的代码。

    当你需要处理破坏性更改时(例如更改字段的数据类型),或者当你正在增强已经部署的现有扩展功能时,通常需要进行数据升级。

  • 安装:当元数据更改和数据升级成功完成后,租户中一切就绪,用户可以提供所有功能。此最后操作将扩展的状态更改为“已安装”,应用程序已经扩展并准备好与新功能一起使用。

为了在原型现实开发场景中演示这些阶段,让我们创建一个简单的扩展,并使用 Docker 容器环境仔细检查背后的过程。

在我们编写本文时,Dynamics 365 Business Central 2019 春季版是最新的可用版本,因此,以下内容基于该主要版本。

我们有一个 PowerShell 脚本,将执行以下活动:

  • 安装或更新NavContainerHelper库到最新版本。

  • 提示你提供一个用于容器内部的用户名和密码。

  • 使用最新版本的 Dynamics 365 Business Central 生成一个 Docker 容器化沙盒并更新。

  • 在桌面上创建一个与容器同名的文件夹,并将所有由New-NAVContainer cmdlet 创建的相关快捷方式移动到该文件夹中。

脚本如下所示:

#Set local variables
####################
$imageName = "mcr.microsoft.com/businesscentral/sandbox:1904" 
$containerName = "BC14MTW1" 
$createDirectory = $true #move shortcuts into a directory
$checkHelper = $true #install navcontainerhelper
#Install or update NavContainerHelper
#####################################
Clear-Host
if ($checkHelper)
{
Write-Host 'Installing navcontainerhelper module, please wait...'
install-module navcontainerhelper -force
      Write-Host 'Checking navcontainerhelper module updates, please wait...'
   update-module navcontainerhelper -force
   Get-InstalledModule navcontainerhelper | Format-List -Property name, version   
}
#Create a new container  
#######################
New-NavContainer -accept_eula `
           -containerName $containerName `
           -useBestContainerOS `
           -imageName $imageName `
           -auth NavUserPassword `
           -alwaysPull `
           -updateHosts `
           -licenseFile $mylicense `
           -assignPremiumPlan `
           -doNotExportObjectsToText `
           -multitenant `
           -includeCSide
#Create a desktop directory and move all the shortcuts  
######################################################
if ($createDirectory)
{
    $desktop = [System.Environment]::GetFolderPath('Desktop')
    New-Item -Path $desktop -Name $containerName -ItemType 'directory' -Force
    Get-ChildItem $desktop -Filter "$containerName*" -File | Move-Item -Destination "$desktop\$containerName"
    $code = @'
[System.Runtime.InteropServices.DllImport("Shell32.dll")]
private static extern int SHChangeNotify(int eventId, int flags, IntPtr item1, IntPtr item2);
public static void Refresh()  {
 SHChangeNotify(0x8000000, 0x1000, IntPtr.Zero, IntPtr.Zero);   
}
'@
    Add-Type -MemberDefinition $code -Namespace WinAPI -Name Explorer
    [WinAPI.Explorer]::Refresh()
}

运行脚本后,桌面上应该会出现一个名为BC14MTW1的新目录,里面有六个快捷方式,具体如下:

通过运行 SQL Server Management Studio(SSMS - 参见 docs.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-2017),我们可以使用 SQL Server 身份验证连接到容器中的 BC14MTW1\SQLEXPRESS 服务器。以下截图显示了 BC14MTW1\SQLEXPRESS 实例中的数据库列表:

值得注意的是,在多租户 Docker 容器化环境中有三个分配的数据库:

  • CRONUS:这是一个应用程序数据库,包含所有管理应用程序对象所需的系统表(如对象和对象元数据),这些对象是通过客户租户共享的。

  • default 和 tenant:default 是挂载到 CRONUS 应用程序上的沙盒,tenant 只是 default 的一个卸载副本。

在这个环境中,我们将重点关注以下部署:

  • 主要扩展,其中包含一个新表和一个表扩展

  • 依赖于主要扩展的第二个扩展

  • 包含破坏性更改(影响架构同步的更改)的主要扩展的新版本

  • 这是第二个扩展的新版本,用于应对主要扩展的破坏性更改。

理解如何执行上述部署对于掌握简单和复杂的扩展部署至关重要。

要从这个多租户、内部部署的、容器化的 Docker 环境中下载符号,我们将使用以下 launch.json 文件参数:

{
 "version": "0.2.0",
 "configurations": [
 {
 "type": "al",
 "request": "launch",
 "name": "Your own server",
 "server": "http://BC14MTW1",
 "serverInstance": "NAV",
 "authentication": "UserPassword",
 "tenant": "default"
 }
 ]
}

使用这个非常简单的脚本,我们创建了自己的 Docker 容器化多租户环境,以模拟一个沙盒 SaaS 部署。我们现在准备好开始我们的扩展部署之旅,并分析数据库级别和应用程序中的发生情况。

部署主要扩展

为了简化,我们将创建一个全新的表(Tab50105.NewTable.al),并在其中添加一些字段:

table 50105 "NewTable"
{
    DataClassification = ToBeClassified;
   fields
     {
        field(1;"Entry No."; Integer)
        {
          DataClassification = ToBeClassified;
        }
        field(2;"Description"; Text [30] )
        {
           DataClassification = ToBeClassified;
        }
        field(3; "Posting Date"; Date) 
        {
           DataClassification = ToBeClassified;  
        }
        field(4; "Open"; Boolean)
        {
          DataClassification = ToBeClassified;  
        }
     }
      keys
      {
         key(PK; "Entry No.")
          { 
             Clustered = true;
          }
      }
}

然后,我们创建一个表扩展(Tab-Ext50105.NewTableExtension.al),它扩展了标准的 Item 表,并添加了一个名为 Catalogue No***.*** 的新字段:

tableextension 50105 "New Table Extension" extends Item
{
   fields
      {
       field(50105;"Catalogue No.";Integer)
          {
             DataClassification = ToBeClassified;  
          }
      }
}

这将是扩展的第一个版本,清单文件(app.json)中包含以下参数,这些参数定义了扩展:

  "id": "15ecd2e5-b7a8-4612-ae6f-d722af29c0c0",
  "name": "MainExtension",
  "publisher": "DTacconi Inc.",
  "version": "1.0.0.0",

通常(但不是强制的),扩展使用一个特殊的代码单元,称为安装代码单元。安装代码单元通过 Subtype = Install 定义,并且每次安装扩展时都会触发它的执行。它的主要目的是在安装过程中配置扩展,通过在设置表中创建记录或用默认值填充表来实现。

在下面的代码示例中,我们将设计一个安装代码单元(Cod50100.MainExtensionInstall.al):

  1. 我们首先在新表中创建一条记录:
codeunit 50100 "MainExtensionInstall"
{
    Subtype = Install;

    trigger OnInstallAppPerCompany();
    var
        NewTable : Record NewTable;
    begin       
        if NewTable.IsEmpty() then
          InsertDefaultValues();
    end;

    local procedure InsertDefaultValues();
    begin
        InsertValue(1,'Activity Start',TODAY,false);
        InsertValue(2,'First Activity',TODAY,false);
        InsertValue(3,'Second Activity',TODAY,false);
    end;

    local procedure InsertValue(EntryNo : Integer; Desc : Text[30]; PostingDate : 
    Date; isOpen : Boolean);
    var
        NewTable : Record NewTable;
    begin
        NewTable.Init();
        NewTable."Entry No." := EntryNo;
        NewTable.Description := Desc;
        NewTable."Posting Date" := PostingDate;
        NewTable.Open := isOpen;
        NewTable.Insert();
    end;
}

由于你可以随意安装和卸载扩展,无论多少次,关键是检查NewTable.IsEmpty()是否仅在第一次安装时执行此操作。该代码片段将确保仅在需要时填充表格中的一些默认值。

卸载一个扩展时,使用 Dynamics 365 Business Central SaaS 将始终在保留数据的情况下进行。数据不会被清除,而只是变得不可见,当扩展卸载时,数据不会被删除。重新安装扩展时,旧的保存数据将会恢复。云计算范式是在所有方面保持保守,任何数据都不会在后台被删除。

  1. 在构建扩展(Ctrl + Shift + B)之后,我们准备好使用以下 PowerShell 命令序列部署应用文件,这些命令包含在NavContainerHelper PowerShell 库中:

    • Publish-BCContainerApp

    • Sync-BCContainerApp

    • Start-BCContainerAppDataUpgrade

    • Install-BCContainerApp

  2. 每执行一个 PowerShell 命令,我们将检查与扩展部署、同步和升级机制相关的系统表中的内容。我们将通过以下简单的 T-SQL 脚本来执行,方法是将DECLARE部分中的[Name][Version Major]替换为适当的扩展名称和主版本号:

-- Application database
USE "CRONUS"
GO

DECLARE @PackageID uniqueidentifier
SELECT @PackageID = NavApp.[Package ID]
FROM [CRONUS].[dbo].[NAV App] NavApp
WHERE (([Name] = 'MainExtension') and ([Version Major] = 1))

SELECT * FROM [NAV App] WHERE [Package ID] = @PackageID
SELECT * FROM [NAV App Dependencies] WHERE [Package ID] = @PackageID
SELECT * FROM [NAV App Object Metadata] WHERE [App Package ID] = @PackageID
SELECT * FROM [NAV App Object Prerequisites] WHERE [Package ID] = @PackageID 
SELECT * FROM [NAV App Publish Reference] WHERE [App Package ID] = @PackageID
SELECT * FROM [NAV App Resource] WHERE [Package ID] = @PackageID
SELECT * FROM [NAV App Tenant App] WHERE [App Package ID] = @PackageID

-- Tenant database
USE "default"
GO

DECLARE @AppID uniqueidentifier
DECLARE @PackageID uniqueidentifier
SELECT @AppID = NavApp.ID, @PackageID = NavApp.[Package ID]
FROM [CRONUS].[dbo].[NAV App] NavApp
WHERE (([Name] = 'MainExtension') and ([Version Major] = 1))

SELECT * FROM [$ndo$navappschemasnapshot] WHERE appid = @AppID
SELECT * FROM [$ndo$navappschematracking] WHERE appid = @AppID
SELECT * FROM [$ndo$navappuninstalledapp] WHERE appid = @AppID
SELECT * FROM [NAV App Data Archive] WHERE [App ID] = @AppID
SELECT * FROM [NAV App Installed App] WHERE ([App ID] = @AppID) and ([Package ID] = @PackageID)
SELECT * FROM [NAV App Published App] WHERE ([App ID] = @AppID) and ([Package ID] = @PackageID)
SELECT * FROM [NAV App Setting] WHERE [App ID] = @AppID
SELECT * FROM [NAV App Tenant Add-In] WHERE [App ID] = @AppID
  1. 接下来,你将在以下代码中看到,发布操作将在[NAV App]表中声明扩展清单,并在应用程序数据库(称为CRONUS)中声明其包 ID 和应用程序 ID:
Publish-BCContainerApp -containerName 'BC14MTW1' `
    -appFile 'C:\TEMP\UPGRADE\MainExtension\DTacconi Inc._MainExtension_1.0.0.0.app' `
    -skipVerification

Publish-BCContainerApp脚本的源代码可以在此找到:github.com/Microsoft/navcontainerhelper/blob/master/AppHandling/Publish-NavContainerApp.ps1

然后,扩展中包含的对象会被提取并填充到应用程序数据库中的[NAV App Object Metadata]表中。

一个元数据记录也会在[NAV App Resource]表中创建,并包含权限对象。

在此阶段,租户数据库(名为default)中不会执行任何操作。以下截图显示了主扩展发布后相关查询结果的片段:

  1. 在发布主扩展之后,我们需要同步其内容,并根据需要将元数据更改应用到数据库结构中。请参考以下代码:
Sync-BCContainerApp -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'MainExtension' `
    -Mode Add

Sync.BCContainerApp脚本的源代码可以在此找到:github.com/microsoft/navcontainerhelper/blob/master/AppHandling/Sync-NavContainerApp.ps1

此操作将同步应用数据库(在此示例中为 CRONUS)中主扩展的扩展元数据内容到特定挂载的租户(在此示例中为默认租户)。实际上,它将在租户[$ndo$navappschemasnapshot]表中为每个影响数据库层级架构更改的对象(如表和/或表扩展)创建记录。

  1. [$ndo$navappschematracking]表中还会创建一条记录,用于将快照表中的对象与扩展 ID、名称、发布者和版本相关联。以下截图显示了主应用程序同步后相关查询结果片段:

当扩展同步完成后,下一步是执行数据升级,如果需要的话。请考虑以下代码:

Start-BCContainerAppDataUpgrade -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'MainExtension' `
    -appVersion '1.0.0.0'

Start-BCContainerAppDataUpgrade脚本的源代码可以在这里找到:github.com/microsoft/navcontainerhelper/blob/master/AppHandling/Start-NavContainerAppDataUpgrade.ps1

  1. 如果你现在运行数据升级,这不会有任何效果,因为没有先前的扩展可供升级。PowerShell cmdlet 将返回如下所示的错误:
"Cannot upgrade the extension 'MainExtension by DTacconi Inc. 1.0.0.0' because no previous version was found."
  1. 最后一步是安装扩展:
Install-BCContainerApp -containerName 'BC14MTW1' `
-tenant 'default' `
-appName 'MainExtension' `
-appVersion '1.0.0.0'

Install-BCContainerApp脚本的源代码可以在这里找到:github.com/microsoft/navcontainerhelper/blob/master/AppHandling/Install-NavContainerApp.ps1

在安装任务期间,会在应用数据库[NAV App Tenant App]表中插入一条记录,以报告并链接租户 ID(在此示例中为default)和应用包 ID。在租户数据库中也会反映这一点,其中在[NAV App Installed App]表中插入一条记录,报告包 ID 和应用 ID。

在此阶段,应用数据库和租户数据库之间的同步机制已完成,元数据结构的更改也已应用到 SQL Server 数据库结构中。

在此示例中,你将会在租户数据库中找到以下内容:

  • 一个名为$item$<appID>的新表,其中包含新的字段Catalogue No.

  • 一个名为$NewTable$<appID>的新表,包含为该表在 AL 表对象中定义的所有相关字段。

以下截图展示了在安装过程中创建的两个新表的概览:

安装过程还执行了安装代码单元中的代码,在此示例中,它将New Table填充了三条记录。你可以通过运行以下代码轻松检查表中的内容:

SELECT * FROM [My Company$NewTable$15ecd2e5-b7a8-4612-ae6f-d722af29c0c0]

结果输出如下:

现在主扩展已经安装,让我们继续在示例中创建并部署另一个依赖于主扩展声明的对象的扩展。

部署依赖扩展

对主扩展的依赖关系在app.json文件中声明:

"id": "6d527590-711f-410c-b233-d267d192b13b",
"name": "SecondExtension",
"publisher": "DTacconi Inc.",
"version": "1.0.0.0",
"dependencies": [
    {
    "appId": "15ecd2e5-b7a8-4612-ae6f-d722af29c0c0",
    "name": "MainExtension",
    "publisher": "DTacconi Inc.",
    "version": "1.0.0.0"
    }
],

在前面的代码中,我们可以看到依赖项必须定义四个参数:应用程序 ID、名称、发布者和版本。如果我们想定义一个独特的扩展目标,这些参数是必须的。

一旦在app.json文件中定义了依赖关系,就必须从租户下载适当的符号。要执行此操作,只需运行命令面板(Ctrl + Shift + P),然后选择 AL:下载符号。

为了简化,我们将创建第二个扩展,如下所示:

  1. 第二个扩展将仅包含一个页面扩展对象,该对象基于与主扩展一起实现的表扩展字段(Pag-Ext50115.NewTablePageExtension.al)。因此,第二个扩展必须声明对主扩展的依赖关系。请参考以下代码:
pageextension 50115 "New Table Page Extension" extends "Item Card"
{
    layout
    {
        addafter(Description)
        {
            field("Catalogue No.";"Catalogue No.")
            {
                ApplicationArea = All;
            }
        }
    }   
}
  1. 现在我们准备好使用以下 PowerShell 代码片段发布第二个扩展:
Publish-BCContainerApp -containerName 'BC14MTW1' `
    -appFile 'C:\TEMP\UPGRADE\SecondExtension\DTacconi Inc._SecondExtension_1.0.0.0.app' `
    -skipVerification
  1. 与主扩展类似,发布操作将在[NAV App]表中声明扩展清单,且扩展中包含的对象将提取到应用程序数据库中的[NAV App Object Metadata]表。

  2. [NAV App Resource]表中也会创建一条类型为元数据的记录。

  3. 相比于先前的扩展,显著的变化是在[NAV App Dependencies]表中新增了一条记录,将第二个扩展包 ID 与主扩展的应用程序 ID、名称、发布者和版本关联起来。以下截图显示了相关的查询结果片段,依赖的应用程序发布后:

不执行任何操作,因此租户数据库中没有任何变化。

  1. 发布后,我们需要同步第二个扩展:
Sync-BCContainerApp -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'SecondExtension' `
    -Mode Add

此操作将同步第二个扩展在应用程序数据库中的元数据内容,与特定挂载的租户进行同步。在这种情况下,由于扩展中只有一个页面对象且没有其他内容,因此在租户数据库中的 NAV 应用架构快照表中不会创建记录。

然而,将会在[$ndo$navappschematracking]表中创建一条记录,用于将快照表中的扩展对象与扩展 ID、名称、发布者和版本关联起来,无论是否有需要跟踪的对象。以下截图显示了主应用程序同步后相关查询结果的片段:

  1. 在同步第二个扩展后,我们可以检查是否有需要升级的数据。然后,我们可以使用以下 PowerShell 脚本:
Start-BCContainerAppDataUpgrade -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'SecondExtension' `
    -appVersion '1.0.0.0'

与主扩展一样,将会抛出一个错误,表示没有内容可升级。

  1. 最后一步是安装第二个扩展:
Install-BCContainerApp -containerName 'BC14MTW1' `
-tenant 'default' `
-appName 'SecondExtension' `
-appVersion '1.0.0.0'

在这种情况下,它将是一个超级快速的任务,因为我们没有任何会导致架构更改的对象(我们在第二个扩展中只有一个页面扩展)。

一条记录会插入到应用程序数据库的[NAV App Tenant App]表中,用于报告并链接租户 ID 和应用包 ID,同样的记录会插入到租户数据库的[NAV App Installed App]表中,报告包 ID 和应用 ID。

一切准备就绪,可以在我们的解决方案中工作(即,两个相互依赖的扩展的组合),在BC14MTW1文件夹中,只需运行BC14MTW1 Web Client快捷方式。

在提供访问凭证并创建 30 天的试用版之后,前往项目列表,创建三个名为 ITEM1、ITEM2 和 ITEM3 的项目,或者您喜欢的任何名称,并分别将它们的 Catalogue No.赋值为111222333。以下截图显示了项目卡片:

这将确保我们现在有一些数据可以进行升级,如下一节所要求的。

部署主扩展的新版本

为了简化起见,我们将创建主扩展的第二个版本,其中一个字段,Catalogue No.,将其数据类型从整数更改为text 30。这是一个数据类型转换,构成数据架构中的破坏性更改。然后,第二个版本的扩展必须处理以下内容:

  • 增加(提高)扩展版本:按如下方式更改app.json文件中的版本字段:
     "version": "2.0.0.0",
tableextension 50105 "New Table Extension" extends Item
{
    fields
    {
        field(50105;"Catalogue No.";Integer)
        {
            DataClassification = ToBeClassified; 
            ObsoleteState = Removed;
        }
        field(50106;NewCatalogueNo;Text[30])
        {
            CaptionML=ENU='Catalogue No. 2';
            DataClassification = ToBeClassified; 
        }
    }
}
  • 安装代码单元的更改:如果在初始安装阶段有一些代码引用了Catalogue No.并预填充值,这些需要根据新的数据类型在安装代码单元中进行更改。我们的示例中没有这种情况,但在实际场景中可能会遇到。

  • 升级代码单元:表或表扩展对象中声明的代码更改可能涉及数据处理。现在需要一个全新的代码单元,并将Subtype属性设置为Upgrade,以处理从旧字段到新字段的数据迁移。

要了解更多关于升级代码单元的信息,请查看在线文档:docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-upgrading-extensions

在这个代码单元内的特定函数中,可以编写升级代码,检查是否已安装特定版本的扩展,并根据此信息执行可操作的任务。所有这些信息都可以通过在升级代码单元中使用NavAppModuleInfo数据类型的组合来获取。

这将使得升级代码单元变得非常强大和灵活。

在我们的案例中,我们将检索当前安装版本的信息(NavApp.GetCurrentModuleInfo(Module)),并将Catalogue No.字段的现有整数值转换为以C为前缀的文本。请参考以下代码:

codeunit 50105 "Upgrade Catalogue No."
{
    Subtype = Upgrade;

    trigger OnUpgradePerCompany();
    var
        ItemRec : Record Item;
        Module : ModuleInfo;
    begin

        NavApp.GetCurrentModuleInfo(Module);

        if (Module.DataVersion.Major = 1) then begin
            ItemRec.Reset();
            IF ItemRec.FindSet(true,false) then repeat
              if (ItemRec."Catalogue No." > 0) THEN begin
                ItemRec.NewCatalogueNo := 'C' +
                  FORMAT(ItemRec."Catalogue No.");
                ItemRec.Modify(true);
              end;
            until ItemRec.Next() = 0;           
        end;
    end;
}

我们现在能够发布主扩展的新版本:

Publish-BCContainerApp -containerName 'BC14MTW1' `
-appFile 'C:\TEMP\UPGRADE\MainExtensionV2\DTacconi Inc._MainExtension_2.0.0.0.app' `
-skipVerification

版本 2 的扩展清单将加载到[NAV App]表中,扩展中包含的对象将提取到应用程序数据库中的[NAV App Object Metadata]表中。同时,在[NAV App Resource]表中也会创建一个元数据记录。以下截图显示了在发布新版本主扩展后相关查询结果的片段:

不会对租户数据库执行任何操作。

让我们使用以下 PowerShell 代码同步主扩展的新版本:

Sync-BCContainerApp -containerName 'BC14MTW1' `
-tenant 'default' `
-appName 'MainExtension' `
-appVersion '2.0.0.0' `
-Mode Add

对于每个影响数据库级别架构更改的对象,都会在租户的[$ndo$navappschemasnapshot]表中创建记录。

[$ndo$navappschematracking]表中创建的记录已被更新,以关联快照表中的对象与扩展 ID、名称、发布者和新版本。该记录将version字段更新为 2.0.0.0,并将baselineversion字段更改为1.0.0.0

这是应用元数据更改的关键阶段。我们可以通过 SSMS 运行一个简单的查询,如下所示:

SELECT * FROM [default].[dbo].[My Company$Item$15ecd2e5-b7a8-4612-ae6f-d722af29c0c0]

上述查询将显示新的NewCatalogueNo字段已经创建。以下截图显示了查询在扩展项表中的结果:

值得注意的是,用户仍然能够无缝地继续工作,且不会遇到任何问题,可以继续给旧的Catalogue No.字段赋值。

部署主扩展 2.x 版本的下一步是执行数据升级:

Start-BCContainerAppDataUpgrade -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'MainExtension' `
    -appVersion '2.0.0.0'

上一步在数据库层级同步了元数据结构,并创建了新字段,同时保持旧字段的值不变。

正是在这一步,所有的升级操作发生,数据被迁移到新字段中。再次运行以下查询:

SELECT * FROM [default].[dbo].[My Company$Item$15ecd2e5-b7a8-4612-ae6f-d722af29c0c0]

你会注意到,NewCatalogueNo中的值已根据升级的代码单元代码进行了更新。以下截图展示了在扩展的商品表中运行查询的结果:

应用数据库中[NAV App Tenant App]表格中记录了租户 ID 和应用包 ID 的关联,这些记录也会更新为版本 2 的包 ID。这一点也会通过更新租户数据库中[NAV App Installed App]表格中的相关记录,反映出版本 2 的新包 ID。

在这个阶段,依赖扩展会被破坏,因为它绑定到了一个实际上已被标记为删除的字段;因此,与旧的Catalogue No.字段相关的页面文本框控件将不再在客户端显示。下图展示了主扩展升级到新版本后,Item 卡片的一部分:

最后一步是检查在使用以下 PowerShell 语句安装扩展时是否有变化:

Install-BCContainerApp -containerName 'BC14MTW1' `
-tenant 'default' `
-appName 'MainExtension' `
-appVersion '2.0.0.0'

在这种情况下,没有执行任何操作,因为扩展已经安装,只是升级到了另一个版本。

然后,在运行 PowerShell cmdlet 时,我们应该收到以下消息:

"WARNING: Cannot install extension MainExtension by DTacconi Inc. 2.0.0.0 for the tenant default because it is already installed."

接下来,我们来看一下如何部署到新的独立版本。

部署依赖扩展的新版本

在对主扩展进行数据升级到新版本后,我们看到依赖扩展已经被破坏。在这个阶段,至关重要的是让用户从客户端读取或更新新的NewCatalogueNo.字段。

首先,我们必须将依赖扩展的app.json版本提升到 2.0.0.0,并更新对主扩展适当版本的依赖。请参阅以下更新的代码片段:

"version": "2.0.0.0",
"dependencies": [
    {
        "appId": "15ecd2e5-b7a8-4612-ae6f-d722af29c0c0",
        "name": "MainExtension",
        "publisher": "DTacconi Inc.",
        "version": "2.0.0.0"
    }
],

更新app.json文件后,我们必须从我们的多租户环境中下载新的符号,以确保它们适当设置。下载符号后,.alpackages目录中的内容结果如下截图所示。这展示了构建依赖扩展新版本所需的符号列表:

更新后的页面扩展对象将会正确地引用新的NewCatalogueNo字段:

pageextension 50115 "New Table Page Extension" extends "Item Card"
{
    layout
    {
        addafter(Description)
        {
            field(NewCatalogueNo;NewCatalogueNo)
            {
                ApplicationArea = All;
            }
        }
    }   
}

然后,我们可以使用以下脚本发布第二个扩展的新版本:

Publish-BCContainerApp -containerName 'BC14MTW1' `
-appFile 'C:\TEMP\UPGRADE\DependentExtensionV2\DTacconi Inc._SecondExtension_2.0.0.0.app' `
-skipVerification

与主扩展版本 2 一样,清单将加载到[NAV App]表中,扩展中包含的对象会被提取到应用程序数据库中的[NAV App Object Metadata]表中。同时,在[NAV App Resource]表中也会创建一个元数据记录,并且在[NAV App Dependency]表中也会创建一个记录,反映第二个扩展版本 2 对主扩展版本 2 的依赖关系。

在租户数据库中没有执行任何操作。

然后,我们应该使用以下 PowerShell 脚本在租户中同步应用程序:

Sync-BCContainerApp -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'SecondExtension' `
    -appVersion '2.0.0.0' `
    -Mode Add

一旦运行同步,原始记录(在[$ndo$navappschematracking]表中创建的)将被更新,以便将快照表中的对象与扩展 ID、名称、发布者和新版本相关联。

依赖扩展的version字段将为2.0.0.0,而baselineversion字段保持为1.0.0.0。以下截图显示了在同步了新版本的依赖扩展后查询的结果:

如果我们现在停止,我们将处于一个混合的半部署状态,其中新版本的模式同步已经启用,但已安装的版本仍然是旧版本。

现在是时候开始为第二个扩展进行数据升级了。请看下面的简单脚本:

Start-BCContainerAppDataUpgrade -containerName 'BC14MTW1' `
    -tenant 'default' `
    -appName 'SecondExtension' `
    -appVersion '2.0.0.0'

应用程序数据库中[NAV App Tenant App]表中的记录,该记录将租户 ID 和应用程序包 ID 关联起来,将被更新为新版本的包 ID。租户数据库中也发生了同样的事情,通过更新[NAV App Installed App]表中的相关记录为新版本的包 ID。

此时,依赖扩展是一致的,新的页面扩展用于读取和更新NewCatalogueNo字段。以下截图显示了在升级到新版本的依赖扩展后的商品卡:

仅为了在四个部署阶段中保持一致,我们还可以执行安装语句:

Install-BCContainerApp -containerName 'BC14MTW1' `
-tenant 'default' `
-appName 'SecondExtension' `
-appVersion '2.0.0.0'

说这很简单,我们会收到以下信息:

"WARNING: Cannot install extension SecondExtension by DTacconi Inc. 2.0.0.0 for the tenant default because it is already installed."

这就结束了我们简单扩展的生命周期,该扩展涵盖了复杂的升级场景。剩下的部分作为练习交给你,手动在在线 Dynamics 365 Business Central 生产环境中部署所有四个扩展,并查看是否有任何不同。

最终,你可以过滤出已经部署到线上扩展,并且你可能会注意到,你会在扩展管理列表中找到它们:两个已安装(版本 2)和两个未安装(版本 1)。以下截图解释了这一情况:

处理安装和升级操作是你需要处理应用程序的两个重要步骤。特别是如果你想在不丢失数据的情况下升级扩展,升级是必须的。

总结

在本章中,我们详细了解了针对 Dynamics 365 Business Central 沙箱或生产环境部署扩展的各种选项。我们还详细介绍了扩展部署的四个阶段:发布、同步、数据升级和安装。

本章结束了开发部分,现在你已经准备好开始在实际项目中使用扩展(你知道如何创建扩展、如何部署它们以及如何扩展它们)。

在下一章,我们将开始一个全新的部分,讨论调试和测试扩展。最后一章将涉及源代码管理及其生命周期。

第九章:调试

Dynamics 365 Business Central AL 语言扩展提供了一个调试器,帮助开发者检查、修正或修改代码,以确保自定义扩展能够成功构建、顺利部署并按预期运行。

追踪潜在逻辑错误的另一种方式是编写测试单元代码,但这将是另一个章节的内容。现在,我们将看到如何轻松调试扩展并为报告创建测试。

本章将涵盖以下主题:

  • 以调试模式运行 AL 语言扩展

  • 定义特定的不可调试函数或变量

  • 掌握调试器和代码编辑器问题(调试调试器

  • 与代码分析器一起工作

  • 使用事件记录器跟踪事件可用性信息

以调试模式运行

调试的基本概念是 断点,它是你可以在语句上设置的标记。当程序执行到该语句时,调试器会被触发并暂停执行(从技术上讲,它会中断),直到指示继续执行。如果没有断点,只要调试器处于活动状态,代码就会正常运行。

调试器仅在遇到错误时,或在 launch.json 文件中指示在记录更改时停止代码执行。

开发者还可以使用调试器来查找潜在的逻辑错误,因为调试器使他们能够一次执行一条 AL 代码语句,同时检查每个运行步骤中变量的内容。通过这种方式,开发者可以在设计应用扩展时检查并匹配预期的结果。

您可以通过三种方式从 Visual Studio Code 运行调试器:

  • 点击调试 | 开始调试。

  • 按下 F5 快捷键。

  • 转到 DEBUG 视图(Ctrl + Shift + D),并按下顶部栏中的绿色右箭头。顶部栏还会显示 launch.json 文件中指定的调试会话名称。如果点击齿轮图标,将打开该文件。最右边的图标启用并显示调试控制台,通常会显示上下文调试信息。以下截图展示了调试器顶部栏:

这些操作将导致构建你的扩展(相当于 Ctrl + Shift + B),如果这还没有完成,然后将扩展发布到目标在线沙箱租户中。

自从 Dynamics 365 Business Central 2019 春季版发布以来,现在可以在不需要反复构建和发布扩展的情况下运行调试会话。这有助于减少调试周期并提高开发生产力。要尝试此功能,只需在 Visual Studio Code 中按 Ctrl + Shift + F5,或者运行命令面板(Ctrl + P),并搜索 AL: Debug without publishing

launch.json 文件包含一些影响调试行为及其目标的元素。以下是它们及其作用的列表:

  • BreakOnError:指定当遇到错误时,调试器是否应停止。

  • BreakOnRecordWrite:指定调试器是否应在记录更改时停止(通常是记录创建或更新)。

  • Tenant:指定Azure Active DirectoryAAD)租户,用于创建调试会话。

  • SandboxName:自 2019 年 4 月更新以来,可以拥有多个沙箱租户。此参数允许开发人员指定要连接调试会话的沙箱名称。

此外,app.json文件包含一个对于调试器作用于特定扩展代码至关重要的参数:ShowMyCode

如果你发布并调试该扩展,且没有显式设置此值,它将作为隐式设置为true进行工作。

然而,如果需要调试来自其他扩展的代码(以及不同的 Visual Studio Code 会话),由于ShowMyCode的默认值是false,必须明确声明该参数并将其设置为true

在处理ShowMyCode参数时需要小心,因为它不仅允许你调试代码,还使用户能够下载扩展的源代码。此参数启用或禁用客户端扩展管理菜单中的下载源代码操作。

Visual Studio Code 调试器区域

DEBUG 视图提供了多个部分和输出窗口,用于逐步检查当前执行的内容、变量分配状态以及代码流程。此外,自 Dynamics 365 Business Central 2019 春季版发布以来,还可以通过收集最长运行的数据库查询来获得代码性能的某些见解。

请参考以下截图:

调试器基本上分为四个区域:侧边栏、工具栏、编辑器和输出窗口。编辑器窗口会高亮显示当前代码停止的位置,通常用黄色标记。调试控制台则显示调试信息,位于输出窗口部分。

接下来我们将分别查看这些区域。

调试器侧边栏

侧边栏默认启用,位于调试器的左侧。可以与编辑器交换位置(右键点击其中一个区域并选择“移动侧边栏到右侧”),隐藏它(Ctrl + B),或者仅隐藏某些区域(右键点击其中一个区域并取消勾选需要隐藏的区域)。

侧边栏分为四个区域,用于提供与当前代码流相关的信息。我们将在这里逐一介绍它们。

变量

VARIABLES 部分提供了全局和局部变量分配的概览:

在本地变量(Locals)部分,还可以检查与代码执行相关的性能计数器:

特别地,可以测量以下内容:

  • 当前 SQL 延迟(毫秒):当调试器命中断点时,Dynamics 365 Business Central Server 服务将向 Azure SQL 数据库发送一个探测 SQL 语句,并跟踪接收答复的时间。此信息对了解租户的沙箱节点是否具有健康的延迟或是否存在基础设施问题非常有帮助。

  • SQL 执行次数:自调试器启动以来会话中执行的 SQL 语句的总次数。

  • SQL 行读取数量:自调试器启动以来读取的数据库行的总数。

  • 前 10 个长时间运行的查询:展开“最近执行的 SQL 语句”部分,你可以观察到最多 10 条 SQL Server 语句条目(编号从 0 到 9)。这些语句表示自会话启动以来至第一次命中断点的 10 个执行时长最久的查询。这些由以下元素定义:

    • 语句:已执行的 T-SQL 语句。

    • 执行时间(UTC):定义 SQL 语句执行时间的时间戳。

    • 持续时间(毫秒):SQL 语句的总执行时间。若在开发扩展时缺少索引,这一点非常有用。

    • 读取的行数:显示 SQL 语句读取的近似行数。在开发扩展时查找缺失的过滤器时,这可能非常有用。

观察

观察部分用于在调试时监视特定感兴趣的变量。可以在变量窗口中或在调试时在代码编辑器中右键单击要观察的变量名称,这将显示被观察变量的值。在此窗口中,还可以将要监视的变量名称插入到调试时的观察列表中。

调用堆栈

变量值和表达式评估相对于选定的堆栈帧。这将报告一个按执行顺序降序排列的对象级联/堆栈。

断点

这显示了可启用、禁用或随意重新应用的可用断点列表。可以通过单击左侧边距或按下F9在选定行中切换断点。显示在编辑器边距中的断点显示为红色实心圆。禁用的断点为灰色实心圆。

无法分配给调试器会话中的任何代码的断点显示为灰色空心圆。

调试器工具栏

工具栏包含暂停、停止、重启或控制调试过程的命令。以下截图展示了调试器工具栏:

可执行的操作如下:

  • 继续 (F5)。

  • 暂停 (F6)。

  • 重启 (Shift + F11):由绿色圆形箭头表示。

  • 停止(Shift + F5):由红色方块表示:调试器工具栏命令允许您继续(F5)进程,直到结束。通过这种方式,开发人员可以继续其迭代过程并在不启动新的 Web 客户端调试会话的情况下重新开始操作。该过程还可以被暂停(F6)——调试会话仍然存在;重新启动(Shift + F11)——它将创建一个新的调试会话;或彻底停止(Shift + F5)——调试会话被关闭。

  • 步过(F10):所有语句逐个执行。如果使用此命令,当遇到函数调用时,函数将被执行而不进入该函数的调试步骤。如果在其被指示跳过的函数中有断点,调试器仍会在该断点处中断。

  • 步入(F11):所有语句逐个执行。如果使用此命令,当遇到函数调用时,调试器将逐步执行该函数的所有指令。

  • 跳出(Shift + F11):它会跳过当前函数并进入下一个函数。

附加模式调试

随着 Dynamics 365 Business Central 2019 年 10 月更新,引入了调试功能,不仅可以通过启动新的调试会话进行调试,还可以通过将调试器附加到应用程序创建的下一个新会话来进行调试。

该功能当前有一些限制,以下表格说明了其支持场景:

部署类型 Web 客户端 Web 服务 后台会话
本地部署 支持 支持 支持
在线沙箱 不支持 支持 不支持

要启用附加过程,必须在扩展的 launch.json 文件中添加一个新的配置参数。

需要指定的关键参数如下:

  • "request": "attach":在典型的调试场景中,此参数默认值为 launch

  • "breakOnNext" : "WebServiceClient":在在线沙箱中,唯一允许的选项是 "WebServiceClient",而在本地和基于 Docker 的沙箱中,也可以将调试器附加到 "WebClient""Background" 会话。

要了解更多关于如何附加调试器的信息,请访问 demiliani.com/2019/10/25/dynamics-365-business-central-debugging-the-base-application/

不可调试项

通常,开发人员希望在每个扩展代码行上都能进行完整的调试体验。在某些情况下,某些特定的变量或函数不应显示其当前值。这些情况通常与存储私人信息的变量或返回私密值(如用户密码或许可证检查)的函数相关。

在开发扩展时,有一个特殊的属性可以与函数和/或变量一起使用,阻止它们被处理(调试器无法逐步进入它们)或在调试器中不可见(变量和/或函数输出值不显示)。在函数或变量声明之前编写[NonDebuggable]语句意味着它们不可检查,并且无法在其上设置断点。

在第七章中创建的报告 50111 项总账分录分析AL 报告开发,在OnPreReport触发器中的第一条语句if includeLogo then begin处添加一个断点,如下所示:

然后,再次发布扩展(F5)。

当客户端加载时,搜索packt report,当项总账分录分析报告记录显示时,点击它,选择在请求页面中包含徽标,然后点击预览。

调试器将在刚刚添加的OnPreReport断点处准确停止。

现在,按F11两次,将代码执行移到运行公司信息表中的Get语句:

如果你在调试器活动窗格中展开变量部分,你可能会注意到可以展开公司信息(命名为CompanyInfo)记录,并且可以看到它的所有值:

停止调试器(Shift + F5),并在公司信息全局变量之前添加非调试属性,如下所示:

...
  var
        [NonDebuggable]
        CompanyInfo: Record "Company Information";
        includeLogo: Boolean;
...

再次发布扩展(F5)。

当客户端加载时,搜索packt report并执行与之前相同的操作来预览报告:调试器会再次在相同位置停止。

F11两次,将代码执行移到运行公司信息表中的Get语句,并检索记录数据。现在,如果你展开变量部分,你可能会注意到公司信息记录甚至没有显示:

在代码编辑器中,将鼠标悬停在任何CompanyInfo语句上,将会显示<Out of Scope>消息,这是由于运行时操作中存在[NonDebuggable]属性。

精通调试器问题

在一些实际应用场景中,可能会出现某些原因导致调试器无法启动,并且会在输出窗口中报告未处理的错误消息;或者你可能只是需要跟踪调试器服务进程。换句话说,你可能需要调试调试器。毕竟,调试器也是另一种软件工具。

若要获得更多的调试信息和详细诊断,有一个未记录的功能,你需要通过在settings.json文件中输入特定的参数来启用它:

"al.editorServicesLogLevel": "Debug".

一旦启用,你需要重新启动 Visual Studio Code,以便使更改在整个应用程序中生效。

此参数将指示 AL 语言扩展在以下目录C:\Users\<USER>\.vscode\extensions\ms-dynamics-smb.al-3.0.121490\bin中为代码编辑器(EditorServices.log)和调试器(DebuggerServices.log)创建详细的日志活动。

ms-dynamics-smb.al-3.0.121490表示在当前 Visual Studio Code 会话中注册的 AL 语言扩展的名称和版本。

以下是调试器服务的活动日志片段,显示其处理过程:

...
04/19/2019 16:53:15 [/6] Process:
launch 04/19/2019 16:53:24 [/14] Process:
setBreakpoints 04/19/2019 16:53:24 [/14] Parsing Report 50111 "Item Ledger Entry Analysis". 04/19/2019 16:53:25 [/14] Parsing Codeunit 50100 "Customer Category Mgt_PKT". 04/19/2019 16:53:25 [/14] Parsing Page 50100 "Customer Category List_PKT". ...

在下一部分,我们将看到如何在使用 AL 进行开发时使用代码分析器。

了解代码分析器

AL 语言的主动调试体验通过代码分析器得到了极大的增强。代码分析器是标准 AL 语言扩展的一部分,是一组应用于扩展开发的上下文规则。这些规则在开发扩展时可以生成错误或警告。

代码分析器可以随时在每个工作区和全局范围内启用或禁用。

要启用代码分析器,请执行以下步骤:

转到文件|首选项|设置(工作区设置)|扩展|AL 语言扩展,并选择编辑settings.json文件。

您也可以选择通过选择用户设置来编辑settings.json文件。然而,由于您可能会在同一环境中开发每租户扩展和 AppSource 应用程序,因此在每个工作区启用这些功能比在每个用户设置中启用更为合理。

settings.json文件中,可以指定以下参数:

al.enableCodeAnalysis (default: false)

将此参数更改为true时,将启用在 JSON 数组参数al.codeAnalyzers中指定的分析器。如果没有指定分析器,或者没有al.codeAnalyzers条目,则假定启用所有分析器。

al.codeAnalyzers[]

al.codeAnalyzers[]参数表示一个代码分析器数组。目前,支持的值及其官方链接,按 ID 排序如下:

al.ruleSetPath

这是一个规则集文件的路径,文件中包含通过标准代码分析器提供的规则变更。

规则集文件采用 JSON 表示法,并引用了一个在标准 AL 语言扩展中实现的现有规则集项 ID。这个文件通常用于重新定义规则在特定扩展项目或工作区中的重要性。

如果我们在所创建的示例扩展项目中实现代码分析器,它将帮助我们获取更多关于代码风格的信息,以及是否有改进之处。让我们通过更改workspace设置中的settings.json文件来启用相关的分析器:

{
    "al.enableCodeAnalysis": true,
    "al.codeAnalyzers": [
        "${CodeCop}",
        "${PerTenantExtensionCop}",
        "${UICop}"
    ]
}

在“问题”窗口中,现在可能会有许多包含错误、警告和信息的记录。如果只考虑与Report 50111 ItemLedgerEntryAnalysis.al相关的记录,应该会有一个错误、两个警告和一条有用的信息。请看下面的截图:

看看错误信息,问题很明显:includeLogo列定义没有定义其ApplicationArea属性。因此,它在应用程序中将不可见,因为ApplicationArea属性必须显式声明。

只需点击“问题”窗口中的错误行,即标记为 ID AL(PTE0008)的那一行。此操作会将光标聚焦在代码编辑器中的includeLogo字段定义上。

为请求页面中的includelogo列添加ApplicationArea属性,如下所示:

field(includeLogo;includeLogo)
{
Caption = 'Include company logo';
ApplicationArea = All;
}

你可能会注意到,“问题”窗口中的错误突然消失了。而且,报告在“问题”窗口中的对象堆栈中向下移动了:

这种情况发生是因为问题记录堆栈按优先级降序排列,因此包含错误的Table 50103 Packt Extension Setup被移到对象列表的顶部,首先进行审核,而警告和信息则排在其后。

规则的重要性值可以通过创建一个 JSON 文件来随意更改,该文件包含需要更改的规则 ID,以及根据公司开发规则如何设置它们。

在扩展的主文件夹中创建一个名为.ruleset的目录,并创建一个名为demo.ruleset.json的文件:

打开demo.ruleset.json,并调用truleset标准代码片段,编写以下内容:

{
    "name": "PacktDemoExtensionRuleSet",
    "description": "Rule Set for Packt Demo Extension (PTE)",
    "rules": [
        {
            "id": "AA0008",
            "action": "Hidden",
            "justification": "Open and Close parenthesis warning is kept hidden"
        }
    ]
}

这样,我们希望指示 AL 语言代码分析器避免为 ID 为AA008的规则在问题窗口中添加警告记录。逐字翻译,该规则是"函数调用应该有括号,即使没有参数。"

使其生效的最后一步是将.alRuleSetPath参数指向新创建的文件:

{
  "al.enableCodeAnalysis": true,
    "al.codeAnalyzers": [
        "${CodeCop}",
        "${PerTenantExtensionCop}",
       "${UICop}"
    ],
   "al.ruleSetPath": "./.ruleset/demo.ruleset.json"
}

当你为规则集文件分配路径时,建议保存所有文件并关闭并重新打开 Visual Studio Code,以确保没有权限错误,并且当前进程能够访问规则集文件。

一旦规则集文件到位,PROBLEMS 窗口中不应再出现与打开和关闭括号相关的警告,问题中显示的记录数应该会减少。可以通过以下方式查看:

仍然有 19 个元素需要评估,以便符合 AL 最佳编码实践。此阶段的要点是,他们应该在自己的公司中充分利用这些规则,并讨论需要提升、保持现状或完全关闭的规则。

启用代码分析器时要小心,因为它们可能会增加开发机器的内存消耗(RAM)。

我们在这里展示了如何通过激活 AL 代码分析器来提高代码质量。在下一部分,我们将看到如何在使用 Dynamics 365 Business Central 开发扩展时使用事件记录器

理解事件记录器

我们都知道,Dynamics 365 Business Central 的在线开发只能通过扩展进行。访问代码库的可扩展性通过订阅标准事件发布者来保证。

考虑到应用程序中有数千个标准事件发布者,并且随着每次在线更新,这个数字还在增加,找到一个合适的地方来挂接标准发布者有时就像是试图在大海捞针一样困难。

查找适当的订阅入口点的推荐方法是使用事件记录器。

如果您不了解标准对象是什么,或者没有访问第三方源代码的权限(以查看发布者定义,假设您订阅的代码是第三方扩展或私有知识产权的一部分),那么这个应用功能是必须的。

有两种方式启用此功能:

  • 从 Visual Studio Code:打开定义了合适沙盒连接的扩展源代码项目,在launch.json文件中。运行命令面板(Ctrl + Shift + P),然后选择 AL: 打开事件记录器。

  • 连接到您的生产或沙盒租户,并搜索 Event Recorder:事件记录器页面提供了一个非常简单的操作菜单,名为 记录事件,具有“开始”和“停止”按钮。

只需按下“开始”,事件记录器将被激活并准备跟踪代码处理流程。

需要明确理解的是,事件记录器会捕获代码执行的所有内容;因此,建议您采取以下其中一种措施:

  • 在一个浏览器标签页中转到事件记录器页面(我们称之为 TAB 1),然后创建一个新标签页(TAB 2),在其中浏览到您希望开始记录事件的页面。然后,在 TAB 1 中启动事件记录器,并开始在 TAB 2 中执行所需的操作,以跟踪业务流程。当完成时,返回 TAB 1 停止事件记录器。

  • 浏览到您希望开始记录事件的页面(TAB 1),然后在新标签页(TAB 2)中转到事件记录器页面并启动它。返回 TAB 1,执行需要跟踪流程的操作,完成后,在 TAB 2 停止事件记录。

页面将刷新并按代码执行顺序显示插入临时表中的记录,如下所示:

事件记录器正在运行

由于这些记录存储在临时表中,它们在内存中是易失的,并且不会存储在数据库中。现在,您已经拥有了在业务过程中触发的所有事件的完整列表,从这里您可以找到适合自定义的正确入口点。

摘要

在本章中,我们已经学习了如何运行调试器并掌握其界面。我们还看到了如何固定不可调试的函数和变量,以避免在需要时显示私人数据。我们还检查了许多有用的标准功能,这些功能让我们的调试和开发工作变得更轻松:代码分析器和事件记录器。

现在,您已经准备好调试扩展、检查事件并分析您的 AL 代码。

在本章中,我们还展示了如何在检查代码流程时捕获运行时错误。在下一章中,我们将掌握如何通过开发自动化测试来检测应用代码中的逻辑问题(漏洞)。

第十章:使用 AL 进行自动化测试开发

在上一章中,我们学习了如何使用 Visual Studio Code 调试 AL 扩展。

在本章中,我们将讨论如何为 AL 扩展编写自动化测试。我们需要这样做,才能拥有现代的开发生命周期,如果你想在 AppSource 上发布扩展,这也是强制要求的。

使用在第五章中开发的演示扩展,为 Dynamics 365 Business Central 开发定制解决方案,我们将讨论以下主题:

  • 使用验收测试驱动开发模式设计测试

  • 设置测试扩展

  • 测试代码背后的技术

  • 实现测试代码

测试自动化和测试设计原则

应用程序测试并不是火箭科学,自动化应用程序测试也不是。它只是另一种可以学习的技能。然而,从开发人员的角度来看,你需要转变思维方式,编写与通常不同目的的代码。大家都知道,开发人员不应该测试自己的代码,因为他们无论有意还是无意,都知道如何使用软件并规避问题。他们编写代码是为了让某个功能工作。

然而,测试并不是关于如何做成,而是关于如何让它崩溃。但这一知识适用于手动的探索性测试,其中测试是根据知识和经验来执行的,而非脚本。而自动化测试是脚本。

要将这些脚本编写成自动化测试,我们需要开发人员。往往,进行应用程序编码的开发人员也是编写自动化测试的开发人员。

为了让开发人员能够编写自动化测试代码,他们需要提供明确定义的脚本。如果没有设计,就没有测试。这是我们在本章中的方法——我们将首先设计测试,然后展示如何编写它们。

使用 ATDD 设计测试

在他的书《Microsoft Dynamics 365 Business Central 中的自动化测试》中,Luc van Vugt 深入探讨了如何设计和实现你的测试。基于所谓的验收测试驱动开发ATDD)方法论,他展示了如何像编写测试设计一样编写需求,使用 ATDD 模式。这种模式引入了五个标签:

  • FEATURE: 定义测试或测试用例集正在测试的功能。

  • SCENARIO: 定义单个测试的测试场景。

  • GIVEN: 定义所需的数据设置;当数据设置较为复杂时,一个测试用例可以包含多个 GIVEN 标签。

  • WHEN: 定义被测试的动作;每个测试用例应该只有一个 WHEN 标签。

  • THEN: 定义该动作的结果,或者更具体地说,定义结果的验证。如果有多个结果,可能需要多个 THEN 标签。

以下是我们客户类别功能的 ATDD 场景示例:

  • [FEATURE] 客户类别

  • [SCENARIO #0002] 将被阻止的客户类别分配给客户

  • [给定] 一个被阻止的客户类别

  • [给定] 一个客户

  • [当] 设置客户的客户类别

  • [然后] 抛出被阻止类别错误

你可以在以下网址获取 Luc 的书:

www.packtpub.com/automated-testing-in-microsoft-dynamics-365-business-central

在学习如何设计之后,我们现在来看一下如何准备环境。

准备环境

为了开始在你的 AL 扩展上编写自动化测试,你需要将 Microsoft Test Framework 导入到你的 Dynamics 365 Business Central 环境中。如果你使用的是 Dynamics 365 Business Central 本地版(独立安装),可以从产品 DVD 中导入。如果你使用的是基于 Docker 的开发沙盒,可以通过将 -includeTestToolkit 开关参数添加到 New-BcContainer cmdlet 中,使用 navcontainerhelper 模块自动导入测试工具包。

如果你已经有一个运行中的 Docker 容器,并且正在使用 Dynamics 365 Business Central,你可以通过以下 cmdlet 导入测试工具包:

Import-TestToolkitToBcContainer -containerName d365bcdev
Generate-SymbolsInNavContainer -containerName d365bcdev

测试工具包测试库包括以下五个应用(包含在 C:\Applications folder 目录下的最新 Docker 镜像中):

  • Microsoft_Any.app

  • Microsoft_Library Assert.app

  • Microsoft_System Application Test Library.app

  • Microsoft_Tests-TestLibraries.app

  • Microsoft_Test Runner.app

现在你的环境已经包含了编写和执行自动化测试所需的一切。在接下来的章节中,我们将看到如何为你的扩展设置测试。

为扩展设置测试开发环境

如果我们采用最为严格的扩展要求(即微软认为在批准你的扩展发布到 AppSource 时是强制性的要求),应用和测试代码应该放在不同的扩展中。因此,测试扩展应依赖于应用扩展。

然而,这种分离可能会限制应用和测试代码的并行开发,因为对应用扩展的任何更改都会导致它的重新部署。这也可能导致测试扩展的更新和重新部署。

不知不觉中,你会不断地在管理扩展之间切换,从而降低开发团队的生产力。在开发过程中,最好的做法是将应用和测试代码放在同一个扩展中。一旦准备好,你可以通过自动化构建脚本或特定的合并策略拆分代码并创建两个必需的扩展。

如果你的扩展不打算发布到 AppSource,我仍然强烈建议你不要在应用扩展中发布测试代码,以防止在生产环境中运行自动化测试。

在我们演示扩展的具体案例中,由于应用代码已经完成,我们可以在一个独立的依赖扩展中设置我们的测试。在接下来的章节中,我们将看到如何在实践中操作。

设置我们的 Visual Studio Code 测试项目

要为我们的测试自动化设置一个新项目,请执行我们在第五章中进行的操作,为 Dynamics 365 Business Central 开发定制化解决方案,当时我们启动了演示扩展。确保我们的新测试项目的 app.json 文件已按如下所示更新:

{
  "id": "7737ab78-c872-4bca-b9f8-2de788818c21",
  "name": "TestPacktDemoExtension",
  "publisher": "fluxxus.nl",
  "brief": "Tests for Customer Category, Gift Campaigns and Vendor Quality Management",
  "description": "Tests for Customer Category, Gift Campaigns and Vendor Quality Management",
  "version": "1.0.0.0",
  "privacyStatement": "",
  "EULA": "",
  "help": "https://www.packtpub.com/business/automated-testing-microsoft-dynamics-365-business-central",
  "url": "http://www.fluxxus.nl",
  "logo": "./Logo/ExtLogo.png",
  "dependencies": [
    {
      "appId": "63ca2fa4-4f03-4f2b-a480-172fef340d3f",
      "publisher": "Microsoft",
      "name": "System Application",
      "version": "1.0.0.0"
    },
    {
      "appId": "437dbf0e-84ff-417a-965d-ed2bb9650972",
      "publisher": "Microsoft",
      "name": "Base Application",
      "version": "15.0.0.0"
    },
    {
      "appId": "dd03d28e-4dfe-48d9-9520-c875595362b6",
      "name": "PacktDemoExtension",
      "publisher": "SD",
      "version": "1.0.0.0"
    },
    {
      "appId": "dd0be2ea-f733-4d65-bb34-a28f4624fb14",
      "publisher": "Microsoft",
      "name": "Library Assert",
      "version": "15.0.36560.0"
    },
    {
      "appId": "e7320ebb-08b3-4406-b1ec-b4927d3e280b",
      "publisher": "Microsoft",
      "name": "Any",
      "version": "15.0.36560.0"
    },
    {
      "appId": "9856ae4f-d1a7-46ef-89bb-6ef056398228",
      "publisher": "Microsoft",
      "name": "System Application Test Library",
      "version": "15.0.36560.0"
    },
    {
      "appId": "5d86850b-0d76-4eca-bd7b-951ad998e997",
      "publisher": "Microsoft",
       "name": "Tests-TestLibraries",
       "version": "15.0.36560.0"
    }
  ],
  "screenshots": [],
  "platform": "15.0.0.0",
  "idRanges": [
  {
    "from": 60100,
    "to": 60150
  }],
  "runtime":"4.0",
  "showMyCode": true
}

如你所见,我们已经将扩展中的依赖项添加到测试中,并且加入了所有需要用于测试的测试工具库应用。

了解测试代码背后的技术

在我们开始编写测试之前,我们需要了解一些关于测试代码背后技术的内容,也就是所谓的可测试性框架

自 NAV 2009 Service Pack 1 起,微软允许平台通过测试函数测试代码单元中构建测试套件。当执行测试代码单元时,平台会执行以下操作:

  • 运行 OnRun 触发器和每个测试函数,按从上到下的顺序,在测试代码单元中依次执行

  • 记录每个测试函数的结果

这就是 Luc van Vugt 所称的可测试性框架的第一支柱。我们的第一个测试示例将实现这一点。

第二个支柱允许你创建所谓的正向-负向,或者异常路径测试,在这些测试中,我们测试导致失败的情况。为了实现这一点,我们使用 AL asserterror 关键字,该关键字应放在调用语句前,以捕获错误并使测试通过:

asserterror <calling statement>

我们的第二个场景,之前的示例中已使用,将利用这个可测试性功能。

在我们代码的各个部分,我们会与用户互动,询问他们确认某个操作,或者仅仅是显示一条消息。在自动化测试时,我们需要能够处理这些用户交互。

为此,第三个支柱,即用户界面UI处理函数应运而生。处理函数是只能在测试代码单元中创建的一种特殊类型的函数,旨在处理测试代码中存在的用户界面交互。它们使我们能够完全自动化测试,而无需真实用户的交互。我们的第三个测试示例将展示如何实现这一点。

第四个支柱测试运行器。这是一个特定的代码单元,能够执行以下操作:

  • 运行存储在多个代码单元中的测试,控制它们的执行,并收集和保护结果

  • 在隔离的环境中运行,以确保写入事务最终不会改变我们运行测试时所使用的数据库,并确保每次重新运行测试时都使用相同的初始数据设置

在我们将要构建的测试中,我们将利用存储在 Dynamics 365 Business Central 中的标准测试运行器。

向平台添加可测试性框架的初始触发点是为了避免通过 UI 测试业务逻辑。因此,启用可测试性框架时是无头的,从而可以更快速地测试业务逻辑,但无法测试 UI。

继续前进时,显而易见,单独的无头测试排除了太多内容。我们如何测试通常存在于页面上的业务逻辑,例如产品配置器,在其中选项会根据用户输入的值显示或隐藏?因此,后来,微软为可测试性框架添加了第五个支柱测试页面

测试页面是页面的逻辑表示,并严格在内存中处理,不显示 UI。它添加了允许你编写用户行为的功能,包括访问页面及其子部分、读取和更改数据、执行操作等。第四个测试示例(在UI 处理器 – 测试示例 4部分)将包含一个测试页面。那么,让我们看看如何进行测试。

了解更多关于可测试性框架五个支柱的详细信息,请参阅 Luc 的书:www.packtpub.com/business/automated-testing-microsoft-dynamics-365-business-central

设计我们的测试场景

正如我们之前提到的,我们将在一个测试示例中演示可测试性框架的五个支柱中的四个:

  • 测试代码单元和测试函数

  • asserterror

  • 测试页面

  • UI 处理器

我们需要设计每个场景,以便能够高效有效地编写测试。这是我们在下一节中首先要做的。

在他的书中,Luc 详细展示了如何从需求到自动化测试,或者说他所称的从客户需求到测试自动化

测试代码单元和测试函数 – 测试示例 1

由于测试代码单元和测试函数是 AL 中测试编码的基础,我们可以以任何场景作为例子。但为了简单起见,我们从演示扩展的基本需求开始:公司希望根据客户可以随时间定义的自定义类别来对客户进行分类,这些类别将来可能会发生变化。

通过添加一个名为客户类别的新表格,并在客户表中添加一个名为客户类别代码的新字段,已实现此功能。

在测试这个需求的基本部分时,第一个测试场景将如下:

  • [功能] 客户类别

  • [场景 #0001] 为客户分配非阻塞客户类别

  • [给定] 一个未被阻塞的客户类别

  • [给定] 一个客户

  • [当] 设置客户类别到客户

  • [然后] 客户的客户类别代码字段已填充

由于每个场景应该是自解释的,我们不会详细讨论每个标签。

asserterror – 测试示例 2

我们可以通过前面讨论的场景,轻松地演示 asserterror 关键字的使用:

  • [功能] 客户类别

  • [场景 #0002] 将阻止的客户类别分配给客户

  • [给定] 一个被阻止的客户类别

  • [给定] 一个客户

  • [当] 在客户上设置客户类别

  • [然后] 抛出阻止类别错误

这测试了与需求中定义的客户分类功能相同的功能。然而,尽管需求中没有提到应用于客户类别的阻止模式,但它已在扩展中实现,当将阻止的客户类别分配给客户时会抛出错误,因此需要进行测试。

测试页面 – 测试示例 3

根据第五章中描述的业务需求,为 Dynamics 365 Business Central 开发定制解决方案,用户必须是“能够创建一个默认客户类别,并自动将此默认值分配给客户。”

这完美地展示了使用以下场景的测试页面:

  • [功能] 客户类别 UI

  • [场景 #0007] 从客户卡分配默认类别给客户

  • [给定] 一个非阻止的默认客户类别

  • [给定] 客户的客户类别不等于默认客户类别

  • [当] 在客户卡上选择分配默认类别操作

  • [然后] 客户拥有默认客户类别

应该注意的是,我们在 [功能] 标签中使用了UI,这表示该功能通过使用 UI 进行测试。理想情况下,测试自动化是指创建所谓的无头测试;即不使用 UI 的测试,因为 UI 测试比无头测试和非 UI 测试慢 5 到 10 倍。Luc 在他的书中通过以下截图展示了这一点,比较了类似的无头测试和 UI 测试:

UI 测试的平均执行时长为 1.35 秒,而无头测试的平均执行时长几乎快七倍:0.20 秒。

UI 处理程序 – 测试示例 4

你可能已经在 GitHub 上检查了我们演示扩展的所有应用程序代码,因此你可能已经看到一些由代码触发的 UI 元素需要处理程序函数。实际上,在 50101 GiftManagement_PKT 代码单元的 DoGiftCheck 函数中只找到一个:

if (SalesLine.Quantity < GiftCampaign.MinimumOrderQuantity) and
    (GiftCampaign.MinimumOrderQuantity - SalesLine.Quantity <=
        PacktSetup."Gift Tolerance Qty")
then
    Message(
        GiftAlert, SalesLine."No.",
        Format(GiftCampaign.MinimumOrderQuantity),
        Format(GiftCampaign.GiftQuantity));

触发它并不像其他示例那么简单,因为需要满足很多条件。因此,它作为第四个也是最后一个示例出现。

如你所见,场景确实要广泛一些:

  • [功能] 赠品

  • [场景 #0010] 分配销售行的数量以触发活动促销消息

  • [给定] 配置了赠品容忍数量的 Packt 设置

  • [给定] 非阻止客户类别的客户,并且有可用的赠品

  • [给定] 商品

  • [给定] 设置了最小订单数量的商品和客户类别赠品活动

  • [给定] 带有商品项目行的客户销售发票

  • [When] 设置发票行上的数量小于最小订单数量,并且在赠品容差数量范围内。

  • [Then] 显示活跃的促销信息。

这里,我们已经设计好了所需的测试场景。接下来的部分,我们将看到如何有效地实现这些场景。

实现我们的测试场景

给定一个 ATDD 场景,我们可以通过以下四个步骤有效地实现测试代码:

  1. 创建一个基于[FEATURE]标签命名的测试代码单元。

  2. 将需求嵌入到一个基于[SCENARIO]标签命名的测试函数中。

  3. 基于[GIVEN][WHEN][THEN]标签编写测试故事。

  4. 构建真正的代码。

测试代码单元和测试函数 - 测试示例 1

让我们根据以下 ATDD 场景,按四步法进行第一个测试示例:

  • [FEATURE] 客户类别

  • [SCENARIO #0001] 将非阻止客户类别分配给客户

  • [GIVEN] 一个非阻止的客户类别

  • [GIVEN] 一个客户

  • [WHEN] 设置客户类别到客户

  • [THEN] 客户已填充客户类别代码字段。

创建一个测试代码单元

使用[FEATURE]标签并应用我们扩展的唯一后缀是我们代码单元的基本结构,结构如下所示:

codeunit 60100 "Customer Category PKT"
{
    // [FEATURE] Customer Category
    SubType = Test;
}

如你所见,测试代码单元是通过将其SubType设置为Test来定义的。

嵌入需求

现在,我们创建一个基于 SCENARIO 描述命名的测试函数,并将场景(GIVEN-WHEN-THEN部分)嵌入到该函数中。我称这种嵌入为green,因为它是被注释掉的GIVEN-WHEN-THEN语句,在你开始编写black部分(.al测试代码)之前。

看看代码单元现在变成了什么样子:

codeunit 60100 "Customer Category PKT"
{
    // [FEATURE] Customer Category
    SubType = Test;
    [Test]
    procedure AssignNonBlockedCustomerCategoryToCustomer()
    // [FEATURE] Customer Category
    begin
        // [SCENARIO #0001] Assign non-blocked customer category
         //                  to customer
        // [GIVEN] A non-blocked customer category
        // [GIVEN] A customer
        // [WHEN] Set customer category on customer
        // [THEN] Customer has customer category code field
         //        populated
    end;
}

测试函数由[Test]标签标识。如果忘记给一个函数添加此标签,它将变成普通函数。

编写测试故事

编写第一部分black内容时,实际上是在编写伪英语,定义测试需要达到的目标。这使得任何非技术同行都能轻松阅读测试,如果需要他们的支持,他们读取测试的门槛就远低于如果代码是技术性代码时的难度。更强的论点可能是,代码将嵌入到可重用的辅助函数中。

所以,开始吧;让我们编写black部分:

codeunit 60100 "Customer Category PKT"
{
    // [FEATURE] Customer Category
    SubType = Test;
    [Test]
    procedure AssignNonBlockedCustomerCategoryToCustomer()
    begin
        // [SCENARIO #0001] Assign non-blocked customer category
         //                  to customer
        // [GIVEN] A non-blocked customer category
        CreateNonBlockedCustomerCategory();
        // [GIVEN] A customer
        CreateCustomer();
        // [WHEN] Set customer category on customer
        SetCustomerCategoryOnCustomer();
        // [THEN] Customer category on customer
        VerifyCustomerCategoryOnCustomer();
    end;
}

这个story设置了四个辅助函数,内容将在下一步中构建。请注意,这些辅助函数的名称与它们所属标签的描述有多么接近,而且还没有定义任何参数或返回值。

构建真正的代码

编写测试故事让我们了解到我们需要四个辅助函数,如下所示:

  • CreateNonBlockedCustomerCategory

  • CreateCustomer

  • SetCustomerCategoryOnCustomer

  • VerifyCustomerCategoryOnCustomer

让我们构建并讨论它们。

CreateNonBlockedCustomerCategory

CreateNonBlockedCustomerCategory是一个多用途的可重用辅助函数,用于创建一个伪随机的Customer Category记录。在稍后的阶段,我们可以将其提升为库代码单元。其实现如下:

local procedure CreateNonBlockedCustomerCategory(): Code[20]
var
    CustomerCategory: Record "Customer Category_PKT";
begin
    with CustomerCategory do begin
        Init();
        Validate(
            Code,
            LibraryUtility.GenerateRandomCode(FIELDNO(Code),
            Database::"Customer Category_PKT"));
        Validate(Description, Code);
        Insert();
        exit(Code);
    end;
end;

为了填充主键字段,我们使用标准测试库LibraryUtility代码单元(131000)中的GenerateRandomCode函数。LibraryUtility变量像 Microsoft 在他们的测试代码单元中那样被全局声明,使其在其他辅助函数中可重用。

我们可以从前面的代码中观察到以下几点:

  • 伪随机意味着每当我们的测试在相同的上下文中执行时,GenerateRandomCode函数将产生相同的值,从而有助于测试的可重复性。

  • Description字段由与Code字段相同的值填充,因为Description的具体值没有意义,这样做是最有效的。

  • 在辅助函数中使用with-do结构可以方便地将代码用于类似的目的,只需要改变记录变量(以及它引用的表),就能将代码应用到其他表上。

CreateCustomer

使用标准库LibrarySales代码单元(130509)中的CreateCustomer函数,我们的CreateCustomer创建一个可用的客户记录,使得这个辅助函数变得非常简便。看看以下代码:

local procedure CreateCustomer(var Customer: record Customer)
begin
    LibrarySales.CreateCustomer(Customer);
end;

LibraryUtility变量一样,我们将全局声明LibrarySales变量。

你可能会问,为什么我们创建一个只有一行语句的辅助函数。正如我们之前提到的,使用辅助函数可以让测试对非技术同事更具可读性,并且使其可重用。我们没有提到的一点是,它还使得代码更易于维护和扩展。如果我们需要更新Library - Sales代码单元中CreateCustomer函数创建的客户记录,我们只需将其添加到本地的CreateCustomer函数中。

SetCustomerCategoryOnCustomer

看一下SetLookupValueOnCustomer的实现:

local procedure SetCustomerCategoryOnCustomer(
         var Customer: record Customer;
         CustomerCategoryCode: Code[10])
begin
    with Customer do begin
        Validate(
            "Customer Category Code_PKT",
            CustomerCategoryCode);
        Modify();
    end;
end;

在这里调用Validate是至关重要的。SetLookupValueOnCustomer不仅仅是将一个值分配给Customer Category Code_PKT字段,还要确保它与Customer Category表中现有的值进行验证。请注意,Customer Category Code_PKT字段的OnValidate触发器没有代码。

VerifyCustomerCategoryOnCustomer

每个测试都需要验证其结果。直白地说,没有验证的测试就不是测试。对于当前的测试,我们需要验证分配给客户记录中Customer Category Code_PKT字段的客户类别代码确实是Customer Category表中创建的值。因此,我们从数据库中检索记录,并验证Customer Category Code_PKT字段的内容如下:

local procedure VerifyCustomerCategoryOnCustomer(
        CustomerNo: Code[20]; CustomerCategoryCode: Code[20])
var
    Customer: Record Customer;
    FieldOnTableTxt: Label '%1 on %2';
begin
    with Customer do begin
        Get(CustomerNo);
        Assert.AreEqual(
            CustomerCategoryCode,
            "Customer Category Code_PKT",
            StrSubstNo(
                FieldOnTableTxt,
                FieldCaption("Customer Category Code_PKT"),
                TableCaption())
        );
    end;
end;

为了验证预期值(第一个参数)和实际值(第二个参数)是否相等,我们使用标准库 Assert 代码单元(130000)中的 AreEqual 函数。当然,我们也可以使用错误系统函数构建自己的验证逻辑,而 AreEqual 实际上就是这样做的。请看以下代码:

[External] procedure AreEqual(Expected: Variant;
     Actual: Variant;Msg: Text)
 begin
     if not Equal(Expected,Actual) then
         Error(
             AreEqualFailedMsg,
             Expected,
             TypeNameOf(Expected),
             Actual,
             TypeNameOf(Actual),
             Msg)
 end;

通过使用 AreEqual 函数,我们确保在预期值和实际值不相等时,能够得到一个标准化的错误信息。随着时间的推移,当你查看任何失败测试的错误时,由于你的验证辅助函数使用了 Assert 库,你将能够轻松识别发生的错误类型。

完成的测试函数如下所示,已经准备好执行:

[Test]
procedure AssignNonBlockedCustomerCategoryToCustomer()
// [FEATURE] Customer Category
var
    Customer: Record Customer;
    CustomerCategoryCode: Code[20];
begin
    // [SCENARIO #0001] Assign non-blocked customer category to
    //                  customer
    // [GIVEN] A non-blocked customer category
    CustomerCategoryCode := CreateNonBlockedCustomerCategory();
    // [GIVEN] A customer
    CreateCustomer(Customer);
    // [WHEN] Set customer category on customer
    SetCustomerCategoryOnCustomer(Customer, CustomerCategoryCode);
    // [THEN] Customer has customer category code field populated
    VerifyCustomerCategoryOnCustomer(
        Customer."No.",
        CustomerCategoryCode);
end;

请注意,已经添加到测试代码单元和函数中的变量和参数。

前往本书的 GitHub 仓库查看完整的测试代码单元实现:github.com/PacktPublishing/Mastering-Microsoft-Dynamics-365-Business-Central

运行测试

正如人们常说的,“实践出真知”,让我们运行我们的测试。最简单且最具指导性的方法是通过应用程序中的测试工具。你可以通过 Dynamics 365 Business Central 中的“告诉我...”功能轻松访问测试工具:

在一个干净的数据库中,或者至少在没有使用过测试工具的数据库或公司中,测试工具的界面如下所示,显示为一个名为 DEFAULT 的套件,其中没有任何记录:

要将我们的测试添加到测试套件中,请按照以下步骤操作:

  1. 选择获取测试代码单元操作。

  2. 在弹出的对话框中,你有两个选项:

    1. 选择测试代码单元:这将打开一个页面,列出数据库中所有存在的测试代码单元,你可以从中选择特定的测试代码单元;选择后点击确定,这些代码单元将被添加到套件中。

    2. 所有测试代码单元:这将把数据库中所有现有的测试代码单元添加到测试套件中。

让我们选择第一个选项,选择测试代码单元。这将打开 CAL TEST GET CODEUNITS 页面。页面显示了我们刚刚创建的测试代码单元以及数据库中存在的一堆测试,主要是由于标准扩展的存在:

  1. 选择测试代码单元并点击确定。现在,套件中会显示每个测试代码单元,在“行类型”列中显示Codeunit,并且与该行(并且缩进)关联的所有测试函数将在“行类型”列中显示为Function

  2. 要运行测试,在打开的对话框中选择运行操作,并选中活动代码单元和所有选项。由于在DEFAULT测试套件中只有一个代码单元,因此选择哪个选项无关紧要,所以点击确定。现在,我们的测试代码单元将会运行,每个测试都会产生成功结果:

如果我们选择了活动代码单元选项,那么只有选定的代码单元会被执行。

对于每个失败,"第一个错误"字段将显示导致失败的错误。如您所见,第一个错误是FlowField。如果进一步查看,会打开 CAL 测试结果窗口,显示特定测试的整个测试运行历史。

通过点击运行来运行测试,将调用标准测试运行器代码单元,CAL 测试运行器(130400),并确保从测试工具运行的测试会在隔离状态下执行,每个测试函数的结果将被记录。

asserterror – 测试示例 2

如前所述,我们将使用asserterror来说明场景#0002:

  • [功能] 客户类别

  • [场景#0002] 将被阻止的客户类别分配给客户

  • [给定] 一个被阻止的客户类别

  • [给定] 一个客户

  • [当] 设置客户的客户类别

  • [然后] 抛出阻止类别错误

创建测试代码单元

与测试示例 1 共享相同的[功能]标签值,我们的新测试用例也将共享相同的测试代码单元,即60100 Customer Category PKT

嵌入需求

根据之前的要求,我们需要在60100代码单元中创建以下新的测试函数:

procedure AssignBlockedCustomerCategoryToCustomer()
// [FEATURE] Customer Category
begin
    // [SCENARIO #0002] Assign blocked customer category to
    //                  customer
    // [GIVEN] A blocked customer category
    // [GIVEN] A customer
    // [WHEN] Set customer category on customer
    // [THEN] Blocked category error thrown
end;

编写测试用例

基于测试示例 1,编写测试用例并不是一个困难的练习。请查看以下代码:

procedure AssignBlockedCustomerCategoryToCustomer()
// [FEATURE] Customer Category
var
    Customer: Record Customer;
    CustomerCategoryCode: Code[20];
begin
    // [SCENARIO #0002] Assign blocked customer category to
    //                  customer
    // [GIVEN] A blocked customer category
    CustomerCategoryCode := CreateBlockedCustomerCategory();
    // [GIVEN] A customer
    CreateCustomer(Customer);
    // [WHEN] Set customer category on customer
    asserterror SetCustomerCategoryOnCustomer(
                    Customer,
                    CustomerCategoryCode);
    // [THEN] Blocked category error thrown
    VerifyBlockedCategoryErrorThrown();
end;

首先,请注意asserterror是如何应用的——在调用SetCustomerCategoryOnCustomer辅助函数之前。这确保了平台期望SetCustomerCategoryOnCustomer抛出一个错误。asserterror使得测试可以继续执行下一个语句,并且不会检查错误。因此,我们需要验证预期的错误是否确实发生。如果在asserterror之后没有验证特定错误,任何错误都会使测试通过。

接下来,请注意,基于测试示例 1,所需的变量已经提供。

构造真实的代码

如果我们重用测试示例 1 中的CreateCustomerSetCustomerCategoryOnCustomer函数,我们只需要创建两个新的辅助函数:

  • CreateBlockedCustomerCategory

  • VerifyBlockedCategoryErrorThrown

接下来,我们将深入了解它们俩。

CreateBlockedCustomerCategory

目标是CreateBlockedCustomerCategory与测试示例 1 中的CreateNonBlockedCustomerCategory辅助函数非常相似,因此其构造也非常简单,如下所示:

local procedure CreateBlockedCustomerCategory(): Code[20]
var
    CustomerCategory: Record "Customer Category_PKT";
begin
    with CustomerCategory do begin
           Get(CreateNonBlockedCustomerCategory());
           Blocked := true;
           Modify();
           exit(Code);
    end;
end;

VerifyBlockedCategoryErrorThrown

之前提到过,当 asserterror 使测试继续执行到下一个语句时,它不会检查错误。因此,这正是此辅助函数需要做的事情,如下代码所示:

local procedure VerifyBlockedCategoryErrorThrown()
var
    CategoryIsBlockedTxt: Label 'This category is blocked.';
begin
    Assert.ExpectedError(CategoryIsBlockedTxt);
end;

运行测试

让我们重新部署扩展并通过选择 操作 | 函数 | 获取测试方法 将第二个测试添加到测试工具中。获取测试方法会通过将代码单元中的所有当前测试函数作为行添加到测试工具中来更新所选的测试代码单元。请注意,RESULT 列将被清除。现在,运行测试代码单元并查看两个测试是否都成功。

请看下一个截图,显示了测试结果:

运行测试代码单元将显示两个测试都已成功执行。

测试测试

我们如何验证成功是真正的成功?我们可以通过简单的方式做到这一点——通过为测试案例的验证函数提供一个不同的期望值。所以我们来做:

Assert.ExpectedError('Testing the test.');

运行第二个测试时,当前将出现失败,并显示以下错误文本:

*Assert.ExpectedError failed. Expected: Testing the test. Actual: This category is blocked.*

实际错误确实是应该发生的错误。完成此步骤后,我们继续测试示例 3。

测试页面 – 测试示例 3

我们将在下一个场景中展示如何使用测试页面:

  • [功能] 客户类别

  • [场景 #0007] 从客户卡片为客户分配默认类别

  • [假设] 非阻止的默认客户类别

  • [假设] 客户的客户类别与默认客户类别不相等

  • [当] 在客户卡片上选择分配默认类别操作

  • [假设] 客户有默认客户类别

现在你应该知道四步法中的第一步,让我们加快速度,一次性创建嵌入编写

创建测试代码单元

与测试示例 1 和 2 共享相同的 [功能] 标签值,我们的新测试案例将共享相同的测试代码单元,即 60100 Customer Category PKT

嵌入和编写

在已经存在的 60100 Customer Category PKT 代码单元中,我们嵌入了需求并编写了测试故事,这引导我们到了以下的测试功能:

[Test]
procedure AssignDefaultCategoryToCustomerFromCustomerCard()
// [FEATURE] Customer Category UI
var
    Customer: Record Customer;
    CustomerCategoryCode: Code[20];

begin
    // [SCENARIO #0007] Assign default category to customer from
    //                  customer card
    // [GIVEN] A non-blocked default customer category
    CustomerCategoryCode :=
        CreateNonBlockedDefaultCustomerCategory();
    // [GIVEN] A customer with customer category not equal to
    //         default customer category
    CreateCustomerWithCustomerCategoryNotEqualToDefault(Customer);
    // [WHEN] Select "Assign Default Category" action on customer
    //        card
    SelectAssignDefaultCategoryActionOnCustomerCard(
        Customer."No.");
    // [THEN] Customer has default customer category
    VerifyCustomerHasDefaultCustomerCategory(
        Customer."No.",
 CustomerCategoryCode);
end;

构建实际代码

为了使 #0007 场景正常工作,我们需要创建以下四个辅助函数:

  • CreateNonBlockedDefaultCustomerCategory

  • CreateCustomerWithCustomerCategoryNotEqualToDefault

  • SelectAssignDefaultCategoryActionOnCustomerCard

  • VerifyCustomerHasDefaultCustomerCategory

但是,正如你将看到的,并且在编写更多测试时你也会体会到,这些辅助函数大部分都可以通过利用之前开发的辅助函数轻松构建。

CreateNonBlockedDefaultCustomerCategory

CreateNonBlockedDefaultCustomerCategory 类似于为测试示例 2 创建的 CreateBlockedCustomerCategory 辅助函数。我们可以使用那里使用的相同方法。看看以下代码:

local procedure
    CreateNonBlockedDefaultCustomerCategory(): Code[20]
var
    CustomerCategory: Record "Customer Category_PKT";
begin
    with CustomerCategory do begin
           SetRange(Default, true);
           if not FindFirst() then begin
            Get(CreateNonBlockedCustomerCategory());
            Default := true;
            Modify();
        end;
        exit(Code);
    end;
end;

请注意,已添加FindFirst构造,以确保只会添加一个默认客户类别。

CreateCustomerWithCustomerCategoryNotEqualToDefault

调用CreateCustomer助手函数即可,因为创建的客户记录将会有一个空的Customer Category Code字段。这使得构建这个助手函数变得非常简单,正如你在以下代码中看到的:

local procedure
    CreateCustomerWithCustomerCategoryNotEqualToDefault(
         var Customer: Record Customer)
begin
    CreateCustomer(Customer);
end;

SelectAssignDefaultCategoryActionOnCustomerCard

使用这个助手函数,我们触及了这个测试示例的核心——利用测试页面实现#0007,即测试用户能否将默认客户类别分配给特定客户。以下是这个助手函数的样子:

local procedure
    SelectAssignDefaultCategoryActionOnCustomerCard(
        CustomerNo: Code[20])
var
    CustomerCard: TestPage "Customer Card";
begin
    CustomerCard.OpenView();
    CustomerCard.GoToKey(CustomerNo);
    CustomerCard."Assign default category".Invoke();
end;

请注意,我没有使用with-do结构,以明确展示该函数中的三个语句是引用了你只能在测试页面对象上找到的方法,而不是普通页面上的方法:

  • OpenView:以查看模式打开测试页面

  • GoToKey:在数据集中查找由指定值标识的行

  • Invoke:在测试页面上调用一个动作

要查看所有测试页面方法的完整列表,请查看以下网址:

你可以在 Luc 的书中找到更多关于测试页面的详细信息:www.packtpub.com/business/automated-testing-microsoft-dynamics-365-business-central

VerifyCustomerHasDefaultCustomerCategory

由于默认客户类别的代码存储在测试示例 4 中的本地CustomerCategoryCode变量中,验证CustomerCategoryCode字段是否确实已经填充了默认客户类别,只需调用已存在的VerifyCustomerCategoryOnCustomer助手函数,正如以下代码所示:

local procedure
    VerifyCustomerHasDefaultCustomerCategory(
        CustomerNo: Code[20];
        DefaultCustomerCategoryCode: Code[20])
begin
    VerifyCustomerCategoryOnCustomer(
        CustomerNo,
        DefaultCustomerCategoryCode)
end;

运行测试

运行这两个测试代码单元会显示所有测试都已成功执行。

客户类别功能的更多示例

在本书的 GitHub 仓库中,你将找到一些关于客户类别功能的额外测试场景。去那里学习它们,看看如何重用各种辅助函数,这表明构建一个更大的测试套件通常涉及到现有元素的重用。你可能会想知道为什么测试示例没有连续编号。检查 GitHub 上的其他场景,你会明白为什么。

UI 处理器 – 测试示例 4

为了向你展示如何实现UI handler函数,我们将尝试以下场景:

  • [Feature] 礼品

  • [Scenario #0010] 在销售行上分配数量以触发激活的促销消息

  • [Given] 设置了礼品容差数量的 Packt 设置

  • [Given] 具有免费礼品可用的非阻塞客户类别的客户

  • [Given] 商品

  • [Given] 为商品和客户类别设置了最小订购数量的礼品活动

  • [Given] 客户的销售发票,包含商品的行

  • [When] 设置发票行上的数量小于最小订购数量,并且在礼品容差数量范围内

  • [Then] 显示激活的促销消息

由于这是一个相当广泛的场景,你还将探索并学习一个相对复杂的测试代码示例。

创建一个测试代码单元

通常,一个测试代码单元可以看作是一个测试套件,用于测试某个功能。由于此功能Gifts与前面测试示例中处理的功能不同,我们应该将此测试包含在一个新的测试代码单元中,如下所示:

codeunit 60101 "Gifts PKT"
{
    // [FEATURE] Gifts
    SubType = Test;
}

嵌入和编写

嵌入和编写现在将导致一个具有以下伪代码的测试函数:

codeunit 60101 "Gifts PKT"
{
    // [FEATURE] Gifts
    SubType = Test;

    [Test]
    procedure AssignQuantityOnSalesLineToTriggerActive
            PromotionMessage()
    // [FEATURE] Gifts
    begin
        // [SCENARIO #0010] Assign quantity on sales line
        //[GIVEN] Packt setup with "Gift Tolerance Qty" set
        CreatePacktSetupWithGiftToleranceQty();
        // [GIVEN] Customer with non-blocked customer category
        //         with "Free Gifts Available"
        CreateCustomerWithNonBlockedCustomerCategoryWith
            FreeGiftsAvailable();
        // [GIVEN] Item
        CreateItem();
        // [GIVEN] Gift campaign for item and customer category
        //          with "Minimum Order Quantity" set
        CreateGiftCampaignForItemAndCustomerCategory
            WithMinimumOrderQuantity();
        // [GIVEN] Sales invoice for customer with line for item
        CreateSalesInvoiceForCustomerWithLineForItem();
        // [WHEN] Set quantity on invoice line smaller than
        //        "Minimum Order Quantity" and within
        //        "Gift Tolerance Qty"
        SetQuantityOnInvoiceLineSmallerThanMinimumOrderQuantity
            AndWithinGiftToleranceQty();
        // [THEN] Active promotion message is displayed
        VerifyActivePromotionMessageIsDisplayed(); 
 end;
}

这是描述分配礼品过程的伪代码。在下一节中,我们将看到实际的实现。

请注意,由于函数名称较长,部分函数名称已被拆分并分布在两行。

构建真实代码

基于书面故事(在前一节中),我们现在需要创建以下七个辅助函数,以便有效地实现真实的测试代码:

  • CreatePacktSetupWithGiftToleranceQty

  • CreateCustomerWithNonBlockedCustomerCategoryWith

    FreeGiftsAvailable

  • `` `CreateItem` ``

  • CreateGiftCampaignForItemAndCustomerCategory

    `` `WithMinimumOrderQuantity` ``

  • `CreateSalesInvoiceForCustomerWithLineForItem`

  • AssignQuantityOnInvoiceLineSmallerThanMinimumOrderQuantity

    AndWithinGiftToleranceQty

  • VerifyActivePromotionMessageIsDisplayed

让我们跳过真实代码,看看几个具体细节。

CreatePacktSetupWithGiftToleranceQty

CreatePacktSetupWithGiftToleranceQty 辅助函数的主要目的是在扩展设置中设置GiftToleranceQty,其代码如下:

local procedure
    CreatePacktSetupWithGiftToleranceQty(
        GiftToleranceQtySet: Decimal)
var
    PacktExtensionSetup: Record "Packt Extension Setup";
begin
    with PacktExtensionSetup do begin
        if not Get() then
            Insert();
        Validate("Gift Tolerance Qty", GiftToleranceQtySet);
        Modify();
    end;
end;

CreateCustomerWithNonBlockedCustomerCategoryWithFreeGiftsAvailable 辅助函数

CreateCustomerWithNonBlockedCustomerCategoryWithFreeGiftsAvailable 是一个辅助函数,用于创建一个具有非阻塞客户类别设置的客户,其实现如下:

local procedure
    CreateCustomerWithNonBlockedCustomerCategory
        WithFreeGiftsAvailable(var Customer: record Customer)
begin
    LibrarySales.CreateCustomer(Customer);
    with Customer do begin
        Validate(
            "Customer Category Code_PKT",
            CreateNonBlockedCustomerCategory
                WithFreeGiftsAvailable());
        Modify();
    end;
end;

请注意,像我们之前的辅助函数CreateCustomer(参见测试示例 1 和 2)一样,这个测试函数也使用了标准库LibrarySales代码单元(130509)中的标准CreateCustomer函数。

CreateItem

CreateItem是一个类似于CreateCustomer函数的构造,它利用标准库LibraryInventory代码单元(132201)中的CreateItem函数。实际上,它只是对它的封装,如以下代码所示:

local procedure CreateItem(var Item: Record Item)
begin
    LibraryInventory.CreateItem(Item);
end;

CreateGiftCampaignForItemAndCustomerCategoryWithMinimumOrderQuantity

CreateGiftCampaignForItemAndCustomerCategoryWithMinimumOrderQuantity必须创建一个赠品活动记录,结合刚刚创建的项目和与新创建的客户关联的客户类别,并定义活动有效和激活的时间段。看看这些函数的样子:

local procedure CreateGiftCampaignForItemAndCustomerCategoryWith
    MinimumOrderQuantity(
        NewItemNo: Code[20]; NewCustomerCategoryCode: code[20];
        NewMinimumOrderQuantity: Decimal; NewGiftQuantity: Decimal)
var
    GiftCampaign: Record GiftCampaign_PKT;
begin
    with GiftCampaign do begin
        Init();
        Validate(CustomerCategoryCode, NewCustomerCategoryCode);
        Validate(ItemNo, NewItemNo);
        Validate(MinimumOrderQuantity, NewMinimumOrderQuantity);
        Validate(EndingDate, DMY2Date(31, 12, 9999));
        Validate(GiftQuantity, NewGiftQuantity);
        Insert();
    end;
end;

CreateSalesInvoiceForCustomerWithLineForItem

当提供我们项目和客户的编号时,CreateSalesInvoiceForCustomerWithLineForItem辅助函数必须创建一个新的销售发票,其中包含一行,并利用标准库Library - Sales代码单元(130509)中的CreateSalesDocumentWithItem辅助函数。以下是它的实现方式:

local procedure CreateSalesInvoiceForCustomerWithLineForItem(
        CustomerNo: Code[20]; ItemNo: Code[20]): Code[20]
var
    SalesHeader: Record "Sales Header";
    SalesLine: Record "Sales Line";
begin
    with SalesHeader do begin
        LibrarySales.CreateSalesDocumentWithItem(
            SalesHeader,
            SalesLine,
            "Document Type"::Invoice,
            CustomerNo,
            ItemNo,
            0,
            '',
            0D);
        exit("No.");
    end;
end;

请注意,行上尚未设置数量,正如在调用CreateSalesDocumentWithItem时的第六个参数所示。CreateSalesDocumentWithItem的最后两个参数表示一个未定义的位置和发货日期。

SetQuantityOnInvoiceLineSmallerThanMinimumOrderQuantityAndWithinGiftToleranceQty

这个辅助函数的主要目的是设置和验证销售发票行上的数量,以便当数量小于赠品活动中的最小订购数量,且一切都在设置中定义的赠品容差范围内时,调用Validate将触发消息。

看看以下代码:

local procedure
    SetQuantityOnInvoiceLineSmallerThanMinimumOrderQuantity
        AndWithinGiftToleranceQty(
            SalesInvoiceNo: Code[20]; NewQuantity: Decimal)
var
    SalesLine: Record "Sales Line";
begin
    with SalesLine do begin
        SetRange("Document Type", "Document Type"::Invoice);
        SetRange("Document No.", SalesInvoiceNo);
        if FindFirst() then begin
            Validate(Quantity, NewQuantity);
            Modify();
        end;
    end;
end;

在编写[THEN]部分之前,做一个非常有用的练习就是运行测试。在这个测试的情况下,它将展示一些非常相关的内容。但在我们进行之前,我们需要更新测试函数中的最后细节,因为我们在调用辅助函数时没有指定各种参数。因此,以下是新的测试代码单元的样子:

codeunit 60101 "Gifts PKT"
{
    // [FEATURE] Gifts
    SubType = Test;

    [Test]
    procedure AssignQuantityOnSalesLineToTriggerActive
            PromotionMessage()
    // [FEATURE] Gifts
    var
        Customer: Record Customer;
        Item: Record Item;
        SalesInvoiceNo: Code[20];
    begin
        // [SCENARIO #0010] Assign quantity on sales line
        // [GIVEN] Packt setup with "Gift Tolerance Qty" set
        CreatePacktSetupWithGiftToleranceQty(6);
        // [GIVEN] Customer with non-blocked customer category
        //         with "Free Gifts Available"
        CreateCustomerWithNonBlockedCustomerCategoryWith
            FreeGiftsAvailable(Customer);
        // [GIVEN] Item
        CreateItem(Item);
        // [GIVEN] Gift campaign for item and customer category
        /          with "Minimum Order Quantity" set
        CreateGiftCampaignForItemAndCustomerCategory
            WithMinimumOrderQuantity (
                Item."No.", Customer."Customer Category Code_PKT",
                10, 3);
        // [GIVEN] Sales invoice for customer with line for item
        SalesInvoiceNo :=
            CreateSalesInvoiceForCustomerWithLineForItem(
                Customer."No.", Item."No.");
        // [WHEN] Set quantity on invoice line smaller than
        //        "Minimum Order Quantity" and within
        //        "Gift Tolerance Qty"
        SetQuantityOnInvoiceLineSmallerThanMinimumOrderQuantity
            AndWithinGiftToleranceQty(SalesInvoiceNo, 5);
        // [THEN] Active promotion message is displayed
        //VerifyActivePromotionMessageIsDisplayed();
    end;
}

运行测试

所以,让我们运行测试,尽管它还没有准备好。正如截图所示,存在一个未处理的 UI元素,消息

如果我们仔细阅读错误消息,我们会看到这是我们想要触发的消息:

*Attention: there is an active promotion for item GL00000001\. if you buy 10 you can have a gift of 3*

这向我们展示了两件事:

  • 我们已经成功触发了消息。

  • 我们需要实现一个所谓的MessageHandler函数。

以下是最简单的MessageHandler函数的样子:

[MessageHandler]
procedure MessageHandler(Msg: Text[1024])
begin
end;

这将处理消息;也就是说,它将模拟用户按下消息对话框中的确定按钮。请注意,这个函数的名称不一定需要是MessageHandler

拥有一个MessageHandler函数是不够的。它还需要绑定到触发消息的测试函数。这是通过以下方式设置测试函数的HandlerFunctions属性来完成的:

    [Test]
    [HandlerFunctions('MessageHandler')]
    procedure AssignQuantityOnSalesLineToTriggerActive
            PromotionMessage()
    // [FEATURE] Gifts

再次运行测试表明现在测试是成功的,但请记住:没有验证的测试就不是测试MessageHandler函数可能会被任何消息触发。我们需要处理的最后一个辅助函数VerifyActivePromotionMessageIsDisplayed,需要验证我们的测试确实触发了正确的消息。

VerifyActivePromotionMessageIsDisplayed

让我们留点东西给你去发现吧。去 GitHub 看一下这是如何实现的。

在本节中,我们学习了如何根据业务场景为扩展创建自动化测试。自动化测试在 Dynamics 365 Business Central 中绝对是必不可少的(它对于 AppSource 应用是强制要求的,但总的来说它也是一个最佳实践)。

总结

在这一章,我们讨论了如何在 Dynamics 365 Business Central 中创建自动化测试的基础知识。

我们使用了 ATDD 测试用例模式来设计每个测试,然后在我们的四步流程中将其作为基础结构:创建测试代码单元、将客户选择嵌入到测试中、编写测试故事,最后构建你的实际代码。现在你应该能根据业务需求轻松编写扩展测试了。

在下一章,我们将探索在开发 Dynamics 365 Business Central 解决方案时需要掌握的另一个重要方面:源代码管理和 DevOps 实践。

第十一章:与 Business Central 的源代码管理和 DevOps

在没有使用源代码管理的情况下开发应用程序,就像开车时没有系安全带。创建一个必须支持多年的应用,并且需要根据新的需求进行扩展和修改,而不知道是谁写了哪些代码以及为何这样写,就像在没有地图和指南针的情况下进入荒野。也许你能勉强度过一天,但再也没有人和你在一起。

通过使用 Visual Studio Code 来开发我们的应用程序,我们拥有所需的所有工具。工具包括Git用于源代码管理,Azure DevOps用于管理应用开发,这将加强开发与运维之间的合作。

在本章中,我们将讨论以下主题:

  • 理解 Azure DevOps 及其提供的功能

  • 在 Azure DevOps 中管理任务、冲刺和看板

  • 为你的代码创建一个代码库

  • 管理仓库

  • 分支策略

  • 分支策略

  • 理解 Git 合并策略

  • 使用 Visual Studio Code 探索 Git

  • 理解 Azure DevOps 管道

  • 理解 YAML 管道

理解 Azure DevOps 及其提供的功能

也许你听过像Team Foundation ServerTeam Foundation ServiceTFSVisual Studio Team Services这样的术语——这些现在统称为Azure DevOps。在 Azure DevOps 中,你可以找到开发团队所需的一切,例如:

  • Azure Pipelines:它为任何语言、平台和云提供 CI/CD。它看起来像这样:

  • Azure Boards:这是你可以通过使用看板、待办事项、团队仪表板和报告来跟踪项目活动的区域。它看起来像这样:

  • Azure Artifacts:这是一个用于保存和分发包的工具。它看起来像这样:

  • Azure Repos:这提供云托管的私有和公共 Git 仓库。它看起来像这样:

  • Azure Test Plans:这提供用于计划和探索性测试的工具。它看起来像这样:

当然,你不需要使用所有可用的工具。你可以从 Azure Repos 开始,然后添加 Azure Pipelines 并将所有这些连接到 Azure Boards。

由于你可以免费获得五个用户,因此使用这些工具没有任何费用。如果你的开发人员有 MSDN 订阅,他们已经包括了 Azure DevOps 的许可证。如果这对你来说还不够,你可以购买额外的许可证(每个用户每月从$6 起—请参见azure.microsoft.com/en-us/pricing/details/devops/)。

如果你的公司域连接到Azure Active DirectoryAAD),你可以通过这些账户和组来管理 Azure DevOps 的访问。这意味着你的用户无需额外的账户即可处理所有相关事务。

创建 Azure DevOps 账户和项目

Azure DevOps中,你可以拥有多个账户。在每个账户下,你可以有多个项目。当你使用 Azure DevOps 创建一个新项目时,生成的项目 URL 会如下所示:

dev.azure.com/myaccount/myproject

让我们从创建一个新的 Azure DevOps 账户开始。你可以选择使用 Microsoft 账户访问 Azure DevOps,或使用公司的 AAD 账户。

如果你使用个人账户创建 Azure DevOps 账户,之后可以将所有权转移到公司账户。

创建新的 Azure DevOps 账户,请按照以下步骤进行:

  1. 访问 go.microsoft.com/fwlink/?LinkId=307137 并使用你的 Microsoft 或 AAD 账户登录。

  2. 阅读并接受《服务条款》、《隐私声明》和《行为准则》。点击继续。

  3. 如果你已经使用你的账户在 Azure DevOps 上工作,你可以通过点击“新建组织”按钮来创建一个新的组织。

  4. 输入你的组织名称并选择托管项目的区域。

  5. 现在,你可以创建你的第一个项目。选择该项目是公开的(任何人都可以访问)还是私有的(仅限你授权的用户访问)。

  6. 你现在是新 Azure DevOps 账户和项目的拥有者,恭喜!以下截图展示了这一点:

如果你拥有多个 Azure DevOps 账户,你可以在它们之间自由切换,并在其中创建项目,前提是你拥有相应的权限。

默认情况下,新的项目将使用Git 仓库和敏捷工作项流程模板。如果你更倾向于将产品待办事项视为用户故事,将障碍视为问题(以及其他一些差异,详见docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/choose-process),你可以将流程更改为Scrum

你可以为每个产品/客户创建一个项目,或者你也可以将所有内容放在一个项目中,并使用其他工具按产品/客户将内容分组。这取决于是否有不同的团队在各自的项目上工作,或者你是否在项目之间共享资源。

我建议从一个项目开始,该项目包含一个待办事项列表(队列),用来为团队优先安排工作。如果你有两个队列或两个独立的团队,可以使用不同的项目。

在 Azure DevOps 中管理任务、冲刺和看板

Azure DevOps 是管理 Dynamics 365 Business Central 项目的一个重要工具,从项目的早期阶段开始。通过使用 Boards 功能,你可以开始集中管理项目的任务、功能、缺陷和一般活动。

在你的 Azure DevOps 项目中,如果你点击 Boards,你会看到以下选项:

以下是可用选项的简要描述(我们稍后会详细查看):

  • Work Items:在这里,你可以管理你的活动列表(分配给你或你跟踪的活动,或你团队的活动)。

  • Boards:在这里,你可以访问你的看板视图。

  • Backlogs:在这里,你可以访问你的产品待办事项,它是一个项目团队计划开发和交付的工作项列表。

  • Sprints:在这里,你可以管理项目的迭代(在 Scrum 方法论中,冲刺通常定义为一个不超过三周的时间段,其中任务被分组并必须完成)。

  • Queries:这是一个可以设置查询来查找和列出工作项的区域。

作为项目经理,你可以做的第一件事是选择 Backlogs,并为你的项目创建一个产品待办事项(产品待办事项对应你的项目计划——你的团队计划交付的路线图)。

在这里,你可以创建阶段和工作项(任务、缺陷等),并将任务分配给团队的用户。下图显示了这一点:

在下图中,我们定义了一些类型为产品待办事项(Product Backlog Item)的工作项。这些工作项对应一组活动。在每个产品待办事项下,我们有相应的任务。每个任务都有自己的状态(待办、进行中或完成),描述,以及一个优先级:

在 Backlog 页面右侧,你有一个 Sprint 面板。

根据 Scrum 方法论,团队以固定的时间间隔计划和跟踪工作,这个时间间隔被称为 冲刺节奏。你可以定义冲刺,以对应你的团队在项目中使用的节奏。

在 Azure DevOps 中,你可以选择一个 冲刺,定义开始和结束日期,然后通过拖放活动将来自 Backlog 的活动分配到特定的冲刺(例如,在上图中,我将“客户类别开发”活动分配到了 Sprint 2)。

在为你的项目安排活动和冲刺后,还有一些其他有趣的视图可供使用。如果你点击 Work Items,你可以查看工作项的状态(例如,分配给你的工作项、所有工作项和最近创建的工作项):

如果你点击 Boards,你可以看到项目的看板(以卡片形式按状态排序的任务视图;你可以拖放任务来更改其状态):

如果你选择冲刺(Sprints),你可以查看项目中定义的每个冲刺(迭代路径)的详细信息。在这里,你可以查看任务板(Taskboard)视图,并查看冲刺待办事项和容量:

在这里,你可以监控每个冲刺的进度以及与每个任务相关的活动。

在查询页面,你可以定义自定义查询来检索工作项。在这里,我定义了一个查询,用于立即查看我项目中已声明完成的所有任务:

执行时,查询返回所需的结果(可以以不同的格式查看):

另一个有趣的功能是所谓的 Delivery Plans。Delivery Plans 将工作项以卡片形式展示,并配有时间线或日历视图。它非常有用,可以查看团队活动的预期发布或交付日期。

Delivery Plans 不是一个标准功能,要获取它,你需要从市场下载并安装 Azure DevOps 扩展(点击页面右上角的袋子图标)。扩展如下所示:

安装完成后,你会在左侧看到一个名为 Plans 的新菜单,点击它,你可以在时间线上查看你的项目交付计划:

最酷的部分是,所有这些项目管理功能都可以在一个工具中使用,并且与日常使用的开发工具(例如 Visual Studio Code)完全集成。在 Visual Studio Code 中,你可以例如提交代码并将该提交关联到分配给你的任务。通过这种方式,你可以拥有完整的产品生命周期,项目经理可以查看为开发或解决特定任务或问题所做的代码修改。

例如,如果我们选择已完成的客户类别开发任务,并点击链接,我们可以看到任务的所有详细信息。特别是,通过历史(History)链接,我们可以查看任务的整个历史:

如果我们点击链接,我们可以看到与该任务相关的所有提交。在这里,我们可以看到,对于此任务,我们有三个提交和一个拉取请求:

如果你选择特定的提交,你可以查看代码修改的详细信息:

如你所见,你可以从单一门户和统一界面中完全控制项目的各个方面。

创建代码库

仓库的目的是为了将你的源代码保存在安全、可靠并且在需要时可以访问的地方。Azure DevOps 提供了无限制的仓库。拥有这样一个安全的存储空间,并且不限空间,免费提供,这对于当笔记本被盗或硬件损坏时,可以大大缓解你的焦虑。而且你可以随时随地访问它;不需要 VPN。此外,你还可以轻松地将更改与需求(工作项)连接起来,了解某些操作的原因以及是谁完成的。

创建新仓库,按照以下步骤操作:

  1. 转到 Azure DevOps 的 Repos 部分。

  2. 展开顶部的仓库选择(你可以管理现有仓库,导入仓库或创建新仓库)。

  3. 选择新仓库。

  4. 输入名称。

  5. 点击创建。

不久后,你的新仓库就可以使用了,主页面会提供你所需要的所有信息,帮助你将代码填充到仓库中。以下截图展示了这一点:

在我们刚创建的仓库主页面上,你可以找到以下功能的链接:

  • 克隆到你的计算机:你可以使用项目的 URL 和 Git 来克隆仓库,或者点击 VS Code 中的 Clone 按钮来打开 Visual Studio Code(或其他支持的开发工具),选择目标文件夹,然后让 Visual Studio Code 将仓库克隆到本地磁盘。然后,你可以根据需要将源代码填充进去。

  • 从命令行推送现有仓库:复制命令,转到你的本地仓库并运行这些命令,你的本地仓库将与这个新的 Azure DevOps 仓库连接,并将推送到其中。

  • 导入仓库:如果你在其他地方有一个 Git 仓库,想要将其导入,只需输入 URL,当前状态将被导入。

  • 使用 READMEgitignore 初始化:如果你愿意,可以只创建一个包含项目描述的 README 文件,或者仅创建一个 .gitignore 文件,稍后再向仓库中添加其他内容。

接下来,我们来看一下如何管理仓库。

管理仓库

在你创建的每个仓库中,你可以设置多个设置。我们将介绍其中最有趣的设置。你可以通过点击项目设置 | Repos | 仓库来管理仓库:

在仓库的设置页面中,我们可以管理主要设置,我们将在接下来的章节中查看这些设置。

安全

你可以为所有仓库分配组并设置权限,也可以根据需要为每个仓库、每个分支或标签进行细化设置。我建议检查哪些用户可以执行以下操作:

  • 删除仓库

  • 强制推送(这可能会重写仓库中的历史记录)

  • 创建仓库

  • 跳过策略

如果你设置了正确的配置,可以防止代码丢失。

选项

在所有 Git 仓库和每个单独的仓库中,你可以设置以下内容:

  • 跨平台兼容性:这确保设置能够防止因文件/文件夹名称仅因大小写不同而导致的问题。Git 是区分大小写的,允许开发者将 File.txtfile.txt 作为两个独立的文件添加。但 Windows 和 iOS 不区分大小写,因此在这方面会出现问题。最佳实践是命名保持一致,避免创建此类文件。启用仓库中的选项将强制开发者保持名称唯一。因为即使标签和分支在 Git 内部也是文件,所以标签和分支名称也可能存在冲突。

  • Fork:如果您不希望允许用户从仓库创建 Fork,您可以禁用此功能。Fork 是仓库的副本,并保持与原始仓库的连接。开发者可以使用 Fork 创建拉取请求,将更改从一个仓库传输到另一个仓库。Git 是一个分布式系统,一个仓库可以存在于多个地方(多个服务器或同一服务器上的多个仓库——都可以)。

  • 工作项管理(每个仓库):保持此设置开启,以便将提交与现有工作项连接。通过这种方式,您将获得每个需求所做的工作的信息,甚至可以知道哪些更改是每个构建或发布版本的一部分。这有助于为每个版本创建变更日志。

  • 代码搜索分支(每个仓库):您可以为每个仓库选择最多五个分支进行代码搜索索引。这些分支中的文件将可以通过 Azure DevOps 的搜索功能进行搜索。只需输入您想搜索的文本,Azure DevOps 将在所有仓库中快速找到它。这是一个非常方便的工具。

  • 分支策略(每个分支):请参见Branching policies部分。

我们将在下一节中查看这些分支策略。

分支策略

为了保持应用程序的质量,您可以定义一些必须满足的政策,才能将开发者的更改合并到选定的分支中。

通常,政策是在master分支上定义的,但它也可以是您希望保持健康的任何分支。如果您为某个分支定义了策略,则不能直接将更改推送到该分支,只能通过 Pull Request(PR)进行。有关更多详细信息,请参见Pull request部分。通过这种方式,每个更改都会被检查和测试,如果不符合政策,该更改将无法进入分支。您可以在策略中定义以下内容:

  • 最少审阅者数量:这是指必须有多少位审阅者批准 PR 才能被接受。

  • 检查关联的工作项:这强制开发者将 PR 与工作项关联,以建立需求和更改之间的链接。

  • 检查评论是否解决:如果审阅者写了评论,则必须在接受 PR 之前解决这些评论。

  • 强制合并策略:你可以禁止快速前进合并(虽然失去了一些细节,但得到了简化),或者强制使用压缩合并,这将把开发者的所有提交合并为目标分支上的一个新提交。

  • 构建验证:你可以定义一个构建管道,用来构建和测试更改。如果构建成功,PR 可以被接受。构建失败可能会阻止 PR 被接受(可选行为)。通过这种方式,你可以保持分支的健康状态。

  • 自动包括代码审查员:这定义了将默认作为审查员的用户组或用户。审查员可以根据特定的更改来选择,例如,当脚本文件或应用程序的设置发生更改时,负责人将被自动添加为审查员。

一些开发者倾向于将分支策略视为增加的障碍,但这是一个很好的机会,可以通过代码审查提升团队成长和质量。它为团队提供了相互学习的机会,并帮助彼此传授新知识。额外的代码审查人员总是有益的。

分支策略

我们已经为分支设置了策略,以便进行规则和技术检查,但问题是,如何在 Git 中使用分支来支持你的工作?你应该使用哪种策略?什么时候应该创建新分支?什么时候应该合并分支?

你可以使用许多策略,而且没有万能的解决方案。适合你团队的最佳策略可能对另一个团队来说并不合适,或者你可能会为你的 AppSource 应用程序使用一种策略,为你的 PerTenant 应用程序使用另一种策略。

在你决定采用哪种方式之前,考虑一下KISS 原则Keep It Simple, Stupid)。

在本章的所有示例中,我们将主分支视为最稳定的分支,它代表着已发布到生产环境的应用程序。你可以决定将该分支命名为其他名称,这不会影响策略本身。你只需要定义名称并确保全团队保持一致。接下来,我们将讨论各类分支。

仅有一个主分支

拥有一个分支是你可以使用的最简单策略。如果只有一个开发者在开发应用程序,就不需要创建分支。即使有更多的开发者,他们也可以继续在一个分支上工作,每次出现冲突时将更改合并到该分支中。但由于未完成的更改可能被推送到分支中,这会使产品很难保持稳定:

为了稳定应用程序,你需要暂停开发。不过,当你看到需要时,仍然可以稍后将策略更改为其他方案。

功能/开发分支

为了将一个功能或者一个开发者的变更隔离开来,你可以为每个功能或者开发者创建一个分支。这样,开发者们就可以在各自的分支上进行工作,直到完成工作并将变更集成回主分支之前,不会与其他人的工作产生冲突。

当功能完成并集成回主分支后,功能分支可以被删除。如果分支是按开发者分的,那么可以预期分支将长时间存在。从长远来看,这可能会成为一个问题。以下图表描述了这一点:

每个开发者使用一个分支会将不同功能的不同变更混合在一起,以后选择性地释放选定的功能可能会成为问题。

使用功能分支可以让你有可能仅发布选定的功能或在流程后期取消功能开发而无需付出成本。

发布分支

下一个策略是为你正在准备的每个发布创建一个分支。它使你能够在发布之前稳定产品并将其与正在进行的开发隔离开来:

正如我们在前面的图表中所看到的,它非常适合适用于 AppSource 的应用程序,因为你可以根据 AppSource 验证程序修复问题,而应用程序不会受到在此期间进行的新开发的影响。修复可以随时集成回主分支。

其他策略

即使为了服务创建一个新的分支(例如,通过另一个团队完成已发布版本的支持并长期支持时,例如创建服务包),或者为了热修复而创建热修复分支时,也可以创建一个新分支并将其与发布和开发分离开来。

如你所见,分支的唯一目的是为了将其变更与其他为不同目的进行的变更隔离开来,以防需要长时间保持隔离状态。

每种策略都可以与其他策略结合使用,这样你可以根据自己的需求创建自己的策略。其中一种组合被命名为Git flow,我们将在接下来探讨它。

Git flow

Git flow 是一种工作流程,结合了特性分支和 bug 和发布分支。它被广泛使用,你甚至可以找到支持此流程的工具,通过自动化不同部分来支持它。在 Git flow 中,主分支代表已发布的版本,即其中的每个提交代表产品的一个发布版本。

第二个分支是开发分支,用作从中创建每个功能分支的集成分支。

当准备发布新版本时,首先从开发分支创建一个新的发布分支。在发布分支上进行稳定性和微调,直到版本准备好发布。发布通过将发布分支与主分支合并来完成。

如果发布的版本有 bug,可以从主分支创建一个新的 bug 修复分支。所有修复都在这个分支上进行,然后将其合并回主分支(创建一个新的修复版本的应用)并合并到开发分支(以便在下一个发布版本中保留 bug 修复)。

以下图示展示了这一点:

这种流程适用于开发 AppSource 应用,因为发布可以被隔离,并且你可以轻松地支持多个版本的应用。

GitHub flow

GitHub flow 是一种用于 GitHub 上开发的工作流。它基于两个规则:

  • 主分支中的一切都可以随时发布。

  • 发布可以随时进行,甚至一天多次。

它本质上是特性分支。修复 bug 的方式与开发其他任何特性相同:

正如我们在前面的图示中看到的,它需要产品的自动化发布,因此可能仅适用于 Business Central 中的 PerTenant 或 OnPrem 应用。AppSource 的发布周期较长,因此此流程不适合它。它甚至假设只有一个版本的产品会发布,但对于 AppSource 来说,这并不真实。

分支考虑因素

在公司中可以使用多种分支策略,因为每种策略适用于不同的情况。但不要忘记 KISS(保持简单!)。采用复杂的策略而对团队没有任何帮助,只会导致捷径和团队不遵守规则。采用单一分支也是一种策略。可以从一个分支开始,根据需要再添加其他分支。随着团队的成长,策略也可以不断发展。

对于 AppSource 开发,你可以使用任何策略,但最适合的策略是 Git flow,因为它允许你分离每个发布并支持多个版本。不要忘记,客户租户上的应用只有在进行重大版本发布或因出现关键 bug 时(这是在合作伙伴请求之后)才会自动更新。这意味着多个版本可以一起在云中共存。

对于 PerTenant 应用开发,因为在这种情况下,你只会向客户租户发布一个版本的产品,所以你可以使用任何策略,包括 GitHub flow。

理解 Git 合并策略

我们不会深入探讨 git merge 命令的所有可能性,但我们会解释一些与 Git 和合并相关的术语。

快进合并

当你在 Git 中合并两个分支,其中一个分支是另一个分支提交的子集时,结果将是快进合并,此时根本不会进行合并。请参考以下图示:

分支将重置到一个新的位置。

Squash 提交

使用 squash commit 可以帮助你保持分支的简洁。当你想要将一个分支合并到另一个分支时,使用 squash commit 可以将分支中的提交合并成一个新的提交,并附上新的提交信息,并将这个新的提交连接到目标分支。下图展示了这一过程:

分支之间不会有真正的合并。你只需丢弃旧分支,因为所有的更改现在都已经提交到目标分支。你会失去一些细节,但获得了简洁性。这取决于你的优先级是什么。

Rebase

你可以使用rebase命令代替merge。顾名思义,你将分支从树上剪切下来,并将其重新基于另一个提交。通过这种方式,你可以在不进行合并的情况下将更改基于新版本。下图展示了这一过程:

所有从原始基点到分支头(分支的起始点)之间的提交都会被获取并重新应用到新的基点上。同样,你获得了简洁性,但失去了现实,因为你在影响历史。

Git 合并的考虑事项

当你需要将更改合并回目标分支时,你可以选择使用快速前进合并、常规合并或 rebase,或者使用 squash 提交。通过结合这些技巧,你可以在 Git 中保持简单的历史记录,但可能会丢失一些必要的细节。但再次强调,不要害怕做出选择。只需从最简单的方式开始,如果你觉得这会有所帮助,可以以后再修改规则。

接下来,我们继续看看如何在 Visual Studio Code 中使用 Git。

使用 Visual Studio Code 探索 Git

Git 是 Visual Studio Code 的默认版本控制系统,你可以通过 Visual Studio Code 的 GUI 执行基本的 Git 操作(如 push、pull、fetch 和 clone)。你还可以通过安装扩展来丰富集成体验。

我推荐这些扩展:

  • Azure Repos:连接到 Azure DevOps 仓库,包括工作项和构建流水线

  • GitLens:为 Git 历史记录添加不同的视图,如 blame 功能

让我们来看看 Git 能为我们提供什么。

Visual Studio Code 中的 Git GUI

Visual Studio Code 提供了一个与 Git 和版本控制管理系统(SCM)完全集成的体验。以下是 Visual Studio Code 的界面:

截图左侧的数字表示以下详细信息:

  1. 当前分支:点击此处,你可以创建一个新分支或切换到另一个现有分支。

  2. 仓库状态:我们与远程仓库保持同步。如果不同步,你可以看到当前分支的进出提交数量。点击它们时,你将执行同步操作,即从远程仓库拉取和合并。

  3. 在 Azure DevOps 中的项目名称(Azure Repo 扩展):点击此项将打开 Azure DevOps 门户。

  4. 拉取请求的数量(Azure Repo 扩展):点击此项可以选择并浏览拉取请求。

  5. 上次构建状态(Azure Repo 扩展):点击此项可以查看仓库的最后一次构建。

  6. 工作项的数量(Azure Repo 扩展):点击此项可以浏览工作项并在 Web 门户中打开它们。

  7. 源代码控制活动栏:你可以看到更改文件的数量。你可以切换到源代码控制活动,在哪里你可以提交这些更改。

  8. 提交消息文本框:在提交更改之前,输入提交信息。

  9. 更改列表:你可以选择你希望撤销或暂存以便提交的更改。如果你双击此项,你可以打开差异窗口,查看当前状态与最后一次提交状态之间的差异。

  10. 源代码控制菜单:你将在这里找到更多关于源代码控制的命令。

完成这些之后,我们来探索 Git/Visual Studio Code 的工作流。

Git 工作流

当你在 Visual Studio Code 和 Git 中工作时,以下是你常见的工作流:

  1. 第一步是将你想要工作的仓库获取到本地系统中。

  2. 如果你想处理现有的代码,你需要获取远程仓库的 URL。然后,你可以使用 Git: Clone 命令,输入 URL,并选择仓库克隆到的文件夹(它会被克隆到一个以仓库名命名的子文件夹中)。

  3. 如果你正在创建一个新的应用程序,你可以先创建文件夹,打开它并在 Visual Studio Code 中创建基本结构(使用 AL: GO! 或其他命令),然后使用 Git: Initialize Repository 将文件夹变成一个 Git 仓库。之后,你可以通过命令提示符将本地仓库连接到新的远程仓库。请参见 如何为你的代码创建仓库 部分。

  4. 检出现有分支或创建一个你想进行开发的新分支。你可以通过点击底部的分支按钮来完成此操作。别忘了检查你正在工作的是正确的分支。

  5. 在你进行了一些更改后,转到源代码管理活动栏(按 Ctrl + Shift + G 打开 Git),写下有意义的消息(例如 My first commit),然后提交更改(点击消息上的勾号或按 Ctrl + Enter)。如果你还没有暂存一些更改(即在更改列表中选择已更改的文件并将其移动到暂存区),Visual Studio Code 会询问你是否想提交所有更改。我建议你逐一查看更改并手动检查和暂存它们,因为修复已经提交的内容并不简单或方便。通过暂存更改,你可以选择所有修改中的一部分进行提交。你甚至可以在行级别上进行暂存/取消暂存,方法是打开差异窗口,右键点击行并选择“暂存/取消暂存选中的范围”。通过这种方式,你可以将更改拆分为独立的提交,例如,如果它们与不同的需求相关。

  6. 如果你想撤销更改,只需点击已更改行中的“放弃更改”。

  7. 提交更改后,如果你想让其他人能够使用这些更改,你可以点击窗口底部的同步按钮,它将把提交推送到远程仓库(如果有更改,它还会从远程仓库拉取更改)。这就是按钮的样子:

  1. 如果你想将更改合并到开发分支(或任何你不负责的其他分支),请在 Azure DevOps 门户中创建一个拉取请求。你可以通过点击状态栏中的“拉取请求”按钮(在Visual Studio Code Git 图形界面部分中的第 4 项),然后选择“浏览你的拉取请求”选项进入。如果你需要修复某些问题,例如在拉取请求期间发生冲突,只需进行修改、提交并推送,拉取请求将会自动更新为新的提交。

  2. 如果所有内容都在远程仓库中,并且更改已经合并,你可以在不再需要该文件夹时直接删除磁盘上的该文件夹。

制定关于提交信息格式的规则是一种良好的实践,以确保公司内部的一致性。掌握写好提交信息的技能应该是每个开发者持续改进的一部分。你可以通过写 #1234 来引用工作项,其中 1234 是工作项的 ID。你可以在网上找到一些示例和规则,了解如何写出好的 Git 提交信息。以下是一个示例:

Fix error "Value is incorrect" in Sales posting
Error text was changed to give more context to user and
in some cases, solved by finding correct value automatically.
Fix bug #1234
Related to #1258

你可以通过点击“创建新分支”直接从 Azure DevOps 中的工作项创建一个新分支:

这样,分支将与工作项关联,大家都知道更改是在哪里开发的。当你使用特性分支策略时,这种做法是非常好的。

合并

在某些情况下,你需要将远程仓库中的更改与本地更改合并。在这种情况下,同步仓库后,你将在源代码控制部分看到一个名为 MERGE CHANGES 的新部分(在正常开发工作中会有 CHANGES 和 STAGED CHANGES 部分)。

当你点击每一行/文件时,Visual Studio Code 会打开一个编辑窗口,显示更改,你可以接受这些更改或手动修正它们。所有冲突解决后,将更改暂存并提交为新的合并提交,并将更改同步(推送)到远程仓库。

学习了 Git 之后,让我们来看一下 Azure DevOps Pipelines 是如何工作的。

理解 Azure DevOps Pipelines

由于业务案例应用程序的生命周期较短,并且你发布新版本的频率应该比过去(AL 时代之前)要高得多,你不能手动构建、测试和部署应用程序。

为了自动化生命周期的这一部分,你可以使用 Azure DevOps Pipelines,它将为你构建、测试和部署。你将生成的源代码作为输入提供给管道,而在管道的另一端,你将获得一个经过测试的应用程序,甚至可以自动交付或部署。现在,管道有两种类型:

  • 构建管道:输入是源代码,输出是应用程序和其他工件。

  • 发布管道:输入是构建管道生成的输出,输出是经过测试的应用程序,已交付或部署到选定位置。

计划是将会有一个多阶段管道,覆盖整个过程。请参考以下图表:

在构建阶段,你将与源代码进行工作,生成产品/应用并进行测试。通常,运行应用程序的测试不需要将应用程序部署到某个地方。对于 Dynamics 365 Business Central,它有所不同,你需要将应用部署到服务器上才能进行测试——是否使用容器并不重要。

在发布管道中,你尝试将应用程序交付或部署到不同的环境中(当前版本、下一个版本、不同的本地化、新环境,或者是包含先前版本的环境以测试数据升级等),在该环境中进行测试,并执行交付/部署应用程序所需的其他步骤。这使得你可以随时以尽可能少的人工干预交付或部署应用程序。

你在提交或拉取请求中添加的与工作项相关的所有内容都会通过管道传输,并且在每次发布时,你都能看到与该发布相关的所有工作项。这帮助你识别和描述属于应用特定版本的更改,并且这个列表可以随应用程序一起自动交付。

代理

你创建的管道必须在某个地方执行。执行由名为代理的应用程序完成。

你可以使用托管代理,这些代理由微软在 Azure 上维护,并运行在不同操作系统上,安装了不同的附加软件(如 macOS、Ubuntu 和带有 Visual Studio 2019 的 Windows 2019)。对于这些托管代理,你可以在 Azure DevOps 组织中使用一些免费的分钟数(你可以在“组织设置”的“计费”部分查看使用的分钟数)。但是,由于不能安装附加软件,使用这些托管代理是有限制的。

如果你愿意,也可以使用自己的代理。这意味着你可以在自己的服务器上安装并配置一个小应用程序,该应用程序通过 RESTful API 连接到 Azure DevOps,并在你的服务器上执行管道中的任务。如何安装代理的详细信息,在点击“下载代理”按钮时,会在设置中的“代理池”部分显示,如下所示:

关于如何配置代理并获取认证代理所需的访问令牌的详细信息,可以在下载代理页面点击“详细说明”找到 (go.microsoft.com/fwlink/?LinkID=825113)。不要忘记以管理员身份运行代理,这样才能在执行 Dynamics 365 Business Central 任务时做必要的操作。

用于执行管道的代理由代理的能力(你可以在“代理池”部分设置)和所需的代理任务能力(你可以稍后在管道定义中的代理任务上设置)决定。这意味着,如果有多个具有相同能力的代理可用,每次运行管道时都可以由不同的代理处理。接下来,让我们看看如何创建构建管道。

创建构建管道

要创建你的第一个构建管道,请打开 Azure DevOps 门户中的管道部分,然后点击“新建管道”:

现在,你可以选择要构建的代码源。首先,我们将使用经典编辑器,这意味着我们将手动创建管道,只是为了查看管道设置的不同部分。稍后,我们将使用Azure Repos Git从 YAML 文件一步创建整个管道。

点击“使用经典编辑器”后,你可以选择代码的来源(选择 Azure Repos Git | 团队项目,然后选择源代码的存储库和该存储库的分支)。选择正确的值后点击“继续”。

因为没有预定义的 Dynamics 365 Business Central 管道模板,我们需要从空工作开始:

现在您已经进入了管道编辑器,您可以添加管道代理作业,这些作业代表了您需要执行的构建和测试应用程序的管道步骤。在右侧,您可以看到您在左侧选择的实际步骤的参数。

在参数中,您可以使用变量,这些变量可以在“变量”标签中定义,也可以由系统定义。您可以在 Azure DevOps 文档中找到有关变量的详细解释。

要在任务的参数中使用变量,请使用 $(variablename) 语法。在 PowerShell 脚本中使用时,请使用 $env:VARIABLENAME 语法。所有可作为环境变量访问的变量都是大写的,且点被替换为下划线。

注意查看“查看 YAML”按钮。当我们研究 YAML 构建管道时,它会很有用。

在为 Business Central 构建应用程序时,您通常会使用可以运行现有脚本(例如,如果它们是源代码的一部分)或运行内联定义脚本的 PowerShell 任务。

在定义了所有所需任务后,您可以为新的构建管道定义触发器。您可以选择以下几种方式:

  • 持续集成:每当新的提交被推送到服务器时,这将会运行。检查分支过滤器,只为您希望节省计算时间的分支运行构建。您甚至可以为仓库中的路径指定过滤器,只有当路径发生变化时,才会触发管道(例如,当readme.md被修改时不触发)。

  • 定时:管道将在设定的时间自动触发。您可以选择仅在上次有变化时触发它。

  • 构建完成:当另一个管道完成时,会触发该管道。这在您有应用程序之间的依赖关系时非常有用,可以在依赖项构建完成后,触发应用程序的构建。

当一切准备就绪后,保存管道并尝试运行它。在大多数情况下,您需要多次运行和修改,才能获得第一次成功的构建。

常见的构建任务包括以下步骤:

  1. 准备构建环境(安装脚本、下载工具、创建 Docker 容器等)。

  2. 编译应用程序(下载符号,使用 ALC.exe 编译应用程序等)。

  3. 安装应用程序(将其发布并安装到 Docker 容器中)。

  4. 运行应用程序的测试并下载结果。

  5. 发布测试结果(即使测试失败,也应该执行此操作)。

  6. 发布构件(将应用程序推送到 Azure DevOps 商店或共享文件夹)。

  7. 清理环境(例如,删除 Docker 容器;无论管道中是否存在失败的步骤,都应该进行此操作)。

变量组和安全文件

在创建管道时,您主要需要定义在构建之间共享的值(例如用户名、密码、密钥等)。为此,您可以创建变量组。要定义新的变量组,请打开库:

在创建变量组时,定义名称和描述。为了更好的安全性,你可以选择将变量存储在 Azure 密钥库中,或者仅创建变量名称/值对,并在密码字段上使用锁图标来隐藏值。此外,你还可以为每个变量组定义安全性。

要能够使用变量组中的变量,你需要将该变量组链接到你的流水线。只需打开编辑器,转到“变量”标签,选择变量组,然后使用“链接变量组”按钮。变量组链接后,你就可以在任务中使用这些变量。

如果你在构建流水线中需要使用证书或其他文件,你可以将其存储为库中的安全文件。你可以通过流水线中的“下载安全文件”任务来下载这个文件。这样,用户不需要访问该文件,并且该文件也不需要能够访问网络资源。该过程将从 Azure DevOps 存储中下载该文件,在那里它是被保护的。

接下来,我们来看看什么是 YAML 流水线。

理解 YAML 流水线

在上一节中,我们使用经典编辑器创建了一个流水线,向你展示流水线的不同部分,并让你对流水线有一个初步了解。但在编辑器中创建流水线并不方便,而且你无法对定义进行版本控制。这就是YAML 流水线的存在意义。它们与经典流水线有相同的属性和部分,但它们是通过 YAML 文件定义的,作为源代码的一部分。这意味着你可以将流水线定义为代码(你可以直接编写流水线代码),并且可以使用你正在使用的所有工具来处理代码。

首先,下面是一些关于YAML的信息。YAML 是一种类似于 XML 和 JSON 的文件语法,但它主要是为了让人类阅读(而 XML 和 JSON 是为了计算机读取)。这意味着 YAML 的语法非常易于理解。它不像 XML 和 JSON 那样使用人工标记来传达意义,而是使用缩进和一些符号,比如-来表示列表项,:用来分隔名称和值:

如果你查看示例,你应该能够识别出属性(格式为name: value)、包含属性的对象(例如前面截图中的customer)以及带有对象的列表(例如前面截图中的items)。

通过使用 YAML,你可以定义流水线编辑器中看到的所有部分:

  • 带有参数和属性的作业和任务

  • 变量,包括变量组

  • 触发器

如果你在编辑器中创建了某些流水线,你可以使用编辑器中的“查看 YAML”按钮来查看定义相同内容的 YAML 文件。这样,你就可以开始创建你的 YAML 流水线。只需在你的项目中创建一个azure-pipelines.yaml文件,将流水线描述写入其中,然后将该文件提交到你的代码仓库中。

当您想要更改管道中的内容时,只需更改 YAML 文件,提交并推送。管道将自动更改。

创建 YAML 管道

若要基于 YAML 文件创建管道,请转到 Pipelines | Builds 部分并执行以下操作:

  1. 点击“New Pipeline”。

  2. 选择 Azure Repos Git(YAML)。

  3. 选择仓库。

  4. Azure DevOps 将自动检测仓库中的 YAML 管道文件并打开它。

  5. 点击“Run”。

完成了。您的新管道已创建!是不是很简单?

YAML 管道模板

为了使其更加通用,您可以在 YAML 管道中使用模板。这意味着您将每个任务的 YAML 文件定义作为单独的文件存储在仓库中,并且可以从 YAML 管道中引用这些模板。这些定义与所有在其 YAML 管道中使用它们的应用程序共享,如果您需要修复某些内容,您只需在一个地方进行修复。当然,这样做的结果是,您可能会从一个地方影响到所有管道。请注意!

这是创建管道的方法:

  1. 为您的模板创建一个新的仓库。

  2. 将包含任务定义的 YAML 文件放入其中:

  1. 将仓库引用添加到 YAML 管道文件中:

引用参数如下:

    • Repository:在 YAML 管道文件中使用的名称

    • Type:源仓库类型

    • Name:仓库名称

    • Ref:要使用的模板版本的分支或引用

    • Endpoint:在 Azure DevOps Service connections 部分中定义的端点名称

  1. 在 Azure DevOps Service connections 中添加与您的仓库同名的服务连接:

  1. 更改 YAML 文件以将这些模板作为步骤引用:

在 YAML 文件中,我们有这些引用:

  • Template@MSDYN365BC_Yaml仓库中的文件路径和名称

  • Parameters:任务的参数值

所有特定于仓库的设置应保留在您的 YAML 管道文件中。所有共享的内容,如步骤定义,应放在模板仓库中。

您可以在github.com/kine/MSDYN365BC_Yaml找到模板和管道文件的示例。

要使用此模板交换新应用程序,您可以将应用程序克隆为模板,网址为github.com/kine/MSDyn365BC_AppTemplate

仅供将来参考,作为 Dynamics 365 Business Central 的通用 YAML 管道,您还可以参考以下 YAML 定义:

variables:
 build.clean: all
 platform: x64

trigger: none

steps:
- task: PowerShell@2
  displayName: 'Install NAVContainerHelper module'
  inputs:
    targetType: filePath
    filePath: 'BuildScripts\InstallNAVContainerHelper.ps1'

- task: PowerShell@2
  displayName: 'Create a Docker Container for the build'
  inputs:
     targetType: filePath
     filePath: 'BuildScripts\CreateDockerContainer.ps1'
     arguments: '-credential ([PSCredential]::new("$(DockerContainerUsername)", (ConvertTo-SecureString -String "$(DockerContainerPassword)" -AsPlainText -Force)))'

- task: PowerShell@2
  displayName: 'Copy Files to Docker Container'
  inputs:
    targetType: filePath
    filePath: 'BuildScripts\CopyFilesToDockerContainer.ps1'

- task: PowerShell@2
  displayName: 'Compile extension stored in the repository'
  inputs:
    targetType: filePath
    filePath: 'BuildScripts\CompileApp.ps1'
    arguments: '-Credential ([PSCredential]::new("$(DockerContainerUsername)", (ConvertTo-SecureString -String "$(DockerContainerPassword)" -AsPlainText -Force))) -BuildFolder "$(Build.Repository.LocalPath)" -BuildArtifactFolder "$(Build.ArtifactStagingDirectory)"'
 failOnStderr: true

- task: PowerShell@2
  displayName: 'Publish extension'
  inputs:
     targetType: filePath
     filePath: 'BuildScripts\PublishApp.ps1'
     arguments: '-Credential ([PSCredential]::new("$(DockerContainerUsername)", (ConvertTo-SecureString -String "$(DockerContainerPassword)" -AsPlainText -Force))) -BuildArtifactFolder "$(Build.ArtifactStagingDirectory)"'
    failOnStderr: true

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifacts'
  inputs:
     PathtoPublish: '$(Build.ArtifactStagingDirectory)'
     ArtifactName: FinalApp

该管道模型使用一组 PowerShell 脚本,您可以将其存储在名为BuildScripts的文件夹中。它可以与您的扩展文件一起存储,如下图所示:

如果成功执行,此管道将发布你的 Dynamics 365 Business Central 扩展的最终 .app 文件作为工件(管道输出),你可以从构建摘要页面下载。

发布管道

构建管道完成后,你可以使用发布管道来交付或部署构建工件,或者执行你想做的其他操作。要创建一个新的发布管道,请进入发布部分并点击新建发布管道。由于没有 Business Central 发布管道的模板,请从空作业开始。

每个发布管道的创建包括以下内容:

  • 工件:这可以是构建管道的输出,Azure DevOps Git 仓库,GitHub 仓库,TFVC 仓库,Azure 工件,Azure 容器,Docker Hub 仓库,或 Jenkins 作业。

  • 阶段:每个阶段是一个独立的过程,可以在不同的代理上执行,并且可以由不同的事件触发。

  • 变量:这些与构建管道中的变量相同。

对于每个工件,你可以定义触发器来启动管道。可以是每次工件更新时(持续部署),或者按照给定的时间表(例如每晚发布)。

在每个阶段,你可以设置预部署条件和后部署条件:

  • 预部署条件:这些包括以下内容:

    • 发布后:当选定的工件被部署或按照给定的时间表时,会触发此操作。

    • 阶段结束后:当另一个阶段完成时会触发此操作。

    • 仅手动:必须有人在门户中触发部署。

    • 预部署审批:选定的用户必须批准部署到此阶段。

    • 闸门:这些是可以根据某些条件批准部署的自动化过程(例如,在发布到上一个阶段后没有错误时)。

  • 后部署条件:这些包括以下内容:

    • 后部署审批:选定的用户必须批准发布阶段成功,并且发布可以继续。

    • 闸门:自动化过程可以批准阶段发布。

    • 自动重新部署触发器:你可以在需要时触发重新部署;例如,在阶段失败后,你可以重新部署上次成功的部署。这对于恢复到最后一个已知的工作版本非常有用。

以下是 Dynamics 365 Business Central 的发布管道示例:

每个阶段将应用程序(并运行测试)部署到 Business Central 沙盒的不同版本(当前版本、未来版本和主版本)。如果一切正常,应用程序将部署到 QA 环境进行用户测试。如果测试成功,应用程序将由证书签名并存储在服务器上以供以后使用(发送到 AppSource)或部署到目标环境(每租户应用)。这就是 YAML 管道的意义所在。

概述

在本章中,我们了解了 Azure DevOps 的概念以及它所提供的功能,然后在 Azure DevOps 上创建了我们的账户。我们查看了如何使用 Azure DevOps 来管理和规划我们的工作。我们为我们的代码创建了一个仓库,并学习了可以设置哪些内容来支持我们的开发周期。

分支策略部分,我们学习了如何在项目中使用分支来保持开发的稳定性和可追溯性。在Git 合并策略在 Visual Studio Code 中使用 Git部分,我们查看了一些与 Git 源代码控制相关的特定方面,并了解了如何通过 Visual Studio Code 使用 Git SCM 来确保我们的代码安全。

我们学习了 Azure DevOps Pipelines,如何使用它们以及如何通过经典设计器创建它们。在上一节中,我们查看了 YAML 文件,并了解了如何使用它们将管道定义为我们代码的一部分。

在下一章中,我们将深入探讨 Dynamics 365 Business Central APIs,并探索如何创建新的 API 以及如何使用现有的 API 来执行集成操作。

第四部分:与 Dynamics 365 Business Central 的高级集成

本节将介绍 Dynamics 365 Business Central API 框架。我们将描述如何通过使用 Dynamics 365 Business Central 和不同的 Azure 服务来创建真实的业务流程。我们还将涵盖 Dynamics 365 Business Central 与 Dynamics 365 Power Platform 结合使用的方式。最后,我们将使用 Power Platform 和 Dynamics 365 Business Central 构建一个真实世界的应用程序。

本节包括以下章节:

  • 第十二章,Dynamics 365 Business Central APIs

  • 第十三章,使用 Business Central 和 Azure 实现无服务器业务流程

  • 第十四章,使用 Azure Functions 进行监控、扩展和 CI/CD

  • 第十五章,Business Central 与 Power Platform 集成

第十二章:Dynamics 365 Business Central API

在上一章中,我们学习了如何在 Dynamics 365 Business Central 项目中使用 DevOps 技术,并重点讨论了源代码管理和 CI/CD 管道等方面。

在本章中,我们将学习如何通过使用平台暴露的 RESTful API,将 Dynamics 365 Business Central 与外部应用集成,重点将放在以下主题:

  • 比较 OData 和 RESTful API

  • 使用 Dynamics 365 Business Central 标准

  • 为新实体和现有实体创建自定义 API

  • 创建使用 Dynamics 365 Business Central API 的应用程序

  • 使用绑定动作

  • 使用 Dynamics 365 Business Central Webhook

  • 在 Microsoft Graph 自动化 API 中使用 Dynamics 365 Business Central API

到本章结束时,你将能够为 Dynamics 365 Business Central 创建 RESTful API,并使用它们与外部应用集成。

比较 Dynamics 365 Business Central 中的 OData 和 API

每个能够发出 HTTP 调用的客户端都可以使用 RESTful API。通过使用 HTTP 协议的 GET、POST、PATCH 和 DELETE 动词,实体可以创建、读取、更新和删除CRUD)。为了与 Dynamics 365 Business Central 进行集成,OData 和 RESTful API 是推荐的工具。

开放数据协议OData)是一个 Web 协议,允许你使用 URI 进行资源标识,通过 HTTP 调用对表格数据执行 CRUD 操作。在 Dynamics 365 Business Central 中将对象暴露为 OData 非常简单:打开 Web 服务页面,插入一个新记录,选择你想要暴露的页面类型,然后点击发布

Dynamics 365 Business Central 会自动为发布的实体分配 OData 和 OData V4 URL,然后你可以通过执行 HTTP REST 调用(GET、POST、PUT、DELETE 和 PATCH)到提供的端点(如下面的截图所示),将此发布的实体(我们的页面)作为 Web 服务使用:

当调用 OData 端点时,你可以应用过滤器、使用分组、使用流过滤器,并通过使用绑定动作调用业务逻辑(我们将在本章稍后的使用绑定动作部分进行讨论)。

Dynamics 365 Business Central 中的 API 在后台使用相同的 OData 栈,但在我们谈论集成时,它们有三个主要优势:

  • 它们有版本控制(在进行服务集成时,这是最重要的事情之一,因为你需要一个稳定的合同)。

  • 它们支持 Webhook(你可以发布你的 API 页面,然后调用/api/microsoft/runtime/beta/webhookSupportedResources来验证该实体是否支持 Webhook)。

  • 它们有命名空间,因此你可以根据作用域或功能领域来隔离和分组你的 API:{{shortUrl}}/api/APIPublisher/APIGroup/v1.0/mycustomers('01121212')

固定合同是微软阻止扩展标准 API 页面的主要原因。如果您尝试扩展标准 API 页面(例如 Customer Entity 页面),Visual Studio Code 会抛出错误:

欲了解如何使用 RESTful API 的更多信息,我推荐以下链接:www.odata.org/getting-started/basic-tutorial/

现在我们已经解释了 OData Web 服务和 RESTful API 之间的主要区别,在接下来的章节中,我们将看到如何在应用程序中使用 Dynamics 365 Business Central API。

使用 Dynamics 365 Business Central 标准 API

Dynamics 365 Business Central 平台将一些标准实体暴露为 RESTful API。暴露的实体总结如下表:

Dynamics 365 Business Central API 端点具有以下格式:

端点 URL 部分 描述
https://api.businesscentral.dynamics.com Dynamics 365 Business Central 基础 URL(标准 API 和自定义 API 相同)
/v2.0 API 版本
/your tenant domain Dynamics 365 Business Central 租户的域名或 ID
/environment name 环境名称(生产、沙盒等)。可以从 Dynamics 365 Business Central 管理门户中检索
/api 固定值
/beta  表示正在使用的 API 版本

截至撰写本文时,Dynamics 365 Business Central API 正在使用端点版本 2.0,API version_number = 1.0

在使用基本身份验证时需要租户 ID。操作方法如下:

GET https://api.businesscentral.dynamics.com/v2.0/{tenant Id}/{environment name}/api/v1.0/$metadata

如果您使用 OAuth 认证,则不需要租户 ID:

GET https://api.businesscentral.dynamics.com/v2.0/{environment name}/api/v1.0/$metadata

Dynamics 365 Business Central API 的版本 1.0 仅支持生产和主要沙盒环境。如果您需要在与默认沙盒环境(即 Sandbox)不同的沙盒环境或不同的生产环境中使用 API,则需要使用版本 2.0 的 API,如下所示的端点:

https://api.businesscentral.dynamics.com/v2.0/{tenant Id}/OtherSandboxName/api/

使用 API 时,首先要做的是使用特定的公司 ID。要检索您在 Dynamics 365 Business Central 租户中可用的公司列表,您需要向 /companies API 端点发送 HTTP GET 请求。以下是此 API 调用的示例:

GET https://api.businesscentral.dynamics.com/v2.0/<tenantID>/production/api/beta/companies
Content-Type: application/x-www-form-urlencoded
Authorization: Basic sdemiliani <YourWebServiceAccessKey>

这是我们收到的响应:

如果我们想要检索特定公司(例如 Cronus IT)的 Customer 记录列表,我们需要向以下 API 端点发送 HTTP GET 请求:

GET https://api.businesscentral.dynamics.com/v2.0/<tenantID>/production/api/beta/companies(80d28ea6-02a3-4ec3-98f7-936c2000c7b3)/customers
Content-Type: application/x-www-form-urlencoded
Authorization: Basic sdemiliani <YourWebServiceAccessKey>

这是我们从中收到的响应:

您在调用 API 时还可以应用过滤器。例如,在这里,我们检索所有 Item 记录,其中 unitPrice 大于 100:

GET https://api.businesscentral.dynamics.com/v2.0/<tenantID>/production/api/beta/companies(80d28ea6-02a3-4ec3-98f7-936c2000c7b3)/items?$filter=unitPrice%20gt%20100
Content-Type: application/x-www-form-urlencoded
Authorization: Basic sdemiliani <YourWebServiceAccessKey>

这是响应:

Dynamics 365 Business Central 标准 API 还支持expand等功能,在一次调用中,您可以扩展实体之间的关系,并检索主实体及其相关实体。例如,要在一次 HTTP 调用中检索销售发票及其所有销售发票行记录,您可以执行对以下 API 端点的 HTTP GET 调用:

GET https://api.businesscentral.dynamics.com/v2.0/<tenantID>/production/api/beta/companies(80d28ea6-02a3-4ec3-98f7-936c2000c7b3)/salesInvoices(034a122b-962b-4007-b3d1-00718c2f21ff)?$expand=salesInvoiceLines
Content-Type: application/x-www-form-urlencoded
Authorization: Basic sdemiliani <YourWebServiceAccessKey>

结果是,您将得到一个包含销售发票头信息及其相关销售发票行详细信息的单一 JSON 响应对象。以下是头部对象:

此外,这里是相关行的详细信息:

现在,您可以解析这个 JSON 对象,并根据需要使用其数据。

在下一节中,我们将看到如何为 Dynamics 365 Business Central 中新增的自定义实体创建 API 页面,以及如何为现有实体创建新的 API 页面。

在 Dynamics 365 Business Central 中创建自定义 API

通过 Dynamics 365 Business Central 扩展,您可以创建自定义实体,并且可以将自定义实体作为 RESTful API 对外暴露。

要在 Dynamics 365 Business Central 中创建一个新的 API,您需要定义一个新的Page对象,PageType = API。为此,您可以使用 tpage代码段,然后选择类型为 API 的页面,如下所示:

创建 API 页面时,请记住以下几点:

  • 字段必须具有符合 REST-API 规范的名称格式(仅限字母数字字符,且不能包含空格或特殊字符(camelCase))。

  • 您应使用实体的 ID(SystemId)。

  • 当您通过 API 插入、修改或删除实体时,底层表的触发器不会执行。您需要通过在页面级别处理相应的触发器来调用表的触发器。

  • 在 API 页面中的OnModify触发器中,您需要处理重命名记录的可能性(通过记录 ID 进行 API 调用可能会触发主键重命名)。

在这里,我们将看到两种主要情况:

  • 如何为自定义实体实现 API(假设有一个扩展将Car实体添加到 Dynamics 365 Business Central 中,以便在 ERP 中管理汽车详细信息)

  • 如何为现有实体实现新 API

我们将在以下章节中详细讨论每种情况。

为自定义实体实现新的 API

在此示例中,我们将在 Dynamics 365 Business Central 中创建一个新的实体来处理Cars的详细信息,并且该实体还将作为 API 对外暴露,供外部应用程序使用:

  1. 为了实现这一点,我们首先创建一个新的Car表,如下所示:
table 50111 Car
{
    DataClassification = CustomerContent;
    Caption = 'Car';
    LookupPageId = "Car List";
    DrillDownPageId = "Car List";
    fields
    {
        field(1; ModelNo; Code[20])
        {
            Caption = 'Model No.';
            DataClassification = CustomerContent;
        }
        field(2; Description; Text[100])
        {
            Caption = 'Description';
            DataClassification = CustomerContent;
        }
        field(3; Brand; Code[20])
        {
            Caption = 'Brand';
            DataClassification = CustomerContent;
        }
        field(4; Power; Integer)
        {
            Caption = 'Power (CV)';
            DataClassification = CustomerContent;
        }
        field(5; "Engine Type"; Enum EngineType)
        {
            Caption = 'Engine Type';
            DataClassification = CustomerContent;
        }
        field(10; ID; Guid)
        {
            Caption = 'ID';
            DataClassification = CustomerContent;
        }
    }

    keys
    {
        key(PK; ModelNo)
        {
            Clustered = true;
        }
    }

    trigger OnInsert()
    begin
        ID := CreateGuid();
    end;
}

Car表包含所需的字段,并且它有一个ID字段,定义为Guid,该字段会在OnInsert触发器中自动分配。

  1. Engine Type字段是Enum EngineType类型,enum定义如下:
enum 50111 EngineType
{
    Extensible = true;
    value(0; Petrol)
    {
        Caption = 'Petrol';
    }
    value(1; Diesel)
    {
        Caption = 'Diesel';
    }
    value(2; Electric)
    {
        Caption = 'Electric';
    }
    value(3; Hybrid)
    {
        Caption = 'Hybrid';
    }
}
  1. 我们还创建了一个 Car List 页面(标准列表页面),用于在 Dynamics 365 Business Central 中管理 Car 数据。Car List 页面定义如下:
page 50112 "Car List"
{   
    PageType = List;
    SourceTable = Car;
    Caption = 'Car List';
    ApplicationArea = All;
    UsageCategory = Lists;

    layout
    {
        area(content)
        {
            repeater(General)
            {
                field(ModelNo;ModelNo)
                {
                    ApplicationArea = All;
                }
                field(Description;Description)
                {
                    ApplicationArea = All;
                }
                field(Brand;Brand)
                {
                    ApplicationArea = All;
                }
                field("Engine Type";"Engine Type")
                {
                    ApplicationArea = All;
                }
                field(Power;Power)
                {
                    ApplicationArea = All;
                }
            }
        }
    }   
}
  1. 现在,我们需要创建 API 页面(通过使用 tpage 代码片段并选择 API 类型的页面)。CarAPI 页面定义如下:
page 50111 CarAPI
{
    PageType = API;
    Caption = 'CarAPI';
    APIPublisher = 'sd';
    APIGroup = 'custom';
    APIVersion = 'v1.0';
    EntityName = 'car';
    EntitySetName = 'cars';
    SourceTable = Car;
    DelayedInsert = true;
    ODataKeyFields = ID;

    layout
    {
        area(Content)
        {
            repeater(GroupName)
            {
                field(id; ID)
                {
                    Caption = 'id', Locked = true;
                }
                field(modelno; ModelNo)
                {
                    Caption = 'modelNo', Locked = true;
                }
                field(description; Description)
                {
                    Caption = 'description', Locked = true;
                }
                field(brand; Brand)
                {
                    Caption = 'brand', Locked = true;
                }
                field(engineType; "Engine Type")
                {
                    Caption = 'engineType', Locked = true;
                }
                field(power; Power)
                {
                    Caption = 'power', Locked = true;
                }
            }
        }
    }

    trigger OnInsertRecord(BelowxRec: Boolean): Boolean
    begin
        Insert(true);
        Modify(true);
        exit(false);
    end;

    trigger OnModifyRecord(): Boolean
    var
        Car: Record Car;
    begin
        Car.SetRange(ID, ID);
        Car.FindFirst();
        if ModelNo <> Car.ModelNo then begin
            Car.TransferFields(Rec, false);
            Car.Rename(ModelNo);
            TransferFields(Car);
        end;
    end;

    trigger OnDeleteRecord(): Boolean
    begin
        Delete(true);
    end;
}

此页面暴露了我们希望在 API 中展示的字段,按照 OData 规范应用命名规则。

然后我们处理 OnInsertRecordOnModifyRecordOnDeleteRecord 页面触发器,以调用表的触发器并处理记录重命名。

  1. 现在,在 Visual Studio Code 中按 F5 并发布你的扩展。当发布完成后,搜索 Car List,然后插入一些示例 Car 记录,例如以下内容:

  1. 现在,我们可以测试我们的自定义 API。当它在 SaaS 租户中发布时,自定义 API 端点的格式如下:
{BaseURL}/v2.0/<your tenant id>/<environment name>/api/<api publisher>/<api group>/<api version>

如果你是在基于 Docker 的沙盒环境中进行测试(例如,我在这里使用的是 Azure 虚拟机),API 端点如下所示:

{BaseServerUrl:ODATA_Port}/{ServerInstance}/api//<api publisher>/<api group>/<api version>

你可以通过以下 URL 检查已发布 API 的元数据(这里,d365bcita0918vm 是托管容器的 Azure 虚拟机的名称):

https://d365bcita0918vm.westeurope.cloudapp.azure.com:7048/BC/api/sd/custom/v1.0/$metadata

API 按公司调用。要获取数据库中公司的列表,你必须发送一个 GET 请求到以下 URL:

{baseUrl}/{D365BCInstance}/api/sd/custom/v1.0/companies

要获取所选公司车辆的列表,你需要发送一个 GET 请求到以下 URL(通过传递前面调用中检索到的公司 ID):

{baseUrl}/{D365BCInstance}/api/sd/custom/v1.0/companies({id})/cars

在我们的环境中,URL 如下所示:

GET https://d365bcita0918vm.westeurope.cloudapp.azure.com:7048/BC/api/sd/custom/v1.0/companies(ecdc7cd0-ab75-4d40-8d0e-80d2471c4378)/cars

这是我们收到的响应:

如你所见,我们有已插入记录的 JSON 表示,每个字段(JSON token)都具有我们在 API 定义中分配的名称。

要通过我们之前发布的自定义 cars API 插入一条新的 Car 记录,你需要发送一个 POST 请求到以下 URL,并将 JSON 记录作为请求体传递:

{baseUrl}/{D365BCInstance}/api/sd/custom/v1.0/companies({id})/cars

这是我们发送的 HTTP 请求:

返回的响应如下所示:

我们收到 HTTP/1.1 201 Created 响应,并且 Car 记录的 JSON 详情被添加到 Dynamics 365 Business Central 中。

如果你查看 Dynamics 365 Business Central 中的 Car List,你可以看到新记录已被创建:

发送 POST 请求时,记得正确设置请求的内容类型为 application/json. 否则,你可能会在响应消息中收到一个相当困惑的错误,如 {"error":{"code":"BadRequest","message":"Cannot create an instance of an interface."}}

要检索特定汽车记录的详细信息,只需发送一个 GET 请求到以下 URL:

{baseUrl}/{D365BCInstance}/api/sd/custom/v1.0/companies({id})/cars({id})

这是通过传递要检索的汽车记录的 GUID 来完成的。

在我们的示例中,如果我们想要检索 Mercedes 记录的详细信息,我们必须向以下 URL 发送 HTTP GET 请求:

https://d365bcita0918vm.westeurope.cloudapp.azure.com:7048/BC/api/sd/custom/v1.0/companies(ecdc7cd0-ab75-4d40-8d0e-80d2471c4378)/cars(0237f4af-3422-41b3-94aa-81196346460e)

这是我们收到的响应:

正如你所看到的,我们已经检索到了 Car 记录的 JSON 表示。

为现有实体实现一个新的 API

正如我们在 比较 Dynamics 365 Business Central 中的 OData 和 API 部分中讨论的,你不能扩展现有的标准 Dynamics 365 Business Central API 页面。如果需要检索标准 Dynamics 365 Business Central 实体的新字段,你需要在你的命名空间中创建一个新的 API 页面。

例如,在这里,我正在创建一个简单的新 API,用于检索客户的详细信息,这些信息在标准的 Customer API 中并未原生暴露。API 页面定义如下:

page 50115 MyCustomerAPI
{
    PageType = API;
    Caption = 'customer';
    APIPublisher = 'SD';
    APIVersion = 'v1.0';
    APIGroup = 'customapi';
    EntityName = 'customer';
    EntitySetName = 'customers';
    SourceTable = Customer;
    DelayedInsert = true;
    ODataKeyFields = SystemId;
    //URL: https://api.businesscentral.dynamics.com/v2.0/TENANTID/sandbox/api/SD/customapi/v1.0/customers

    layout
    {
        area(Content)
        {
            repeater(GroupName)
            {
                field(no; "No.")
                {
                    Caption = 'no', Locked = true;
                }
                field(name; Name)
                {
                    Caption = 'name', Locked = true;
                }
                field(Id; SystemId)
                {
                    Caption = 'Id', Locked = true;
                }
                field(balanceDue; "Balance Due")
                {
                    Caption = 'balanceDue', Locked = true;
                }
                field(creditLimit; "Credit Limit (LCY)")
                {
                    Caption = 'creditLimit', Locked = true;
                }
                field(currencyCode; "Currency Code")
                {
                    Caption = 'currencyCode', Locked = true;
                }
                field(email; "E-Mail")
                {
                    Caption = 'email', Locked = true;
                }
                field(fiscalCode; "Fiscal Code")
                {
                    Caption = 'fiscalCode', Locked = true;
                }
                field(balance; "Balance (LCY)")
                {
                    Caption = 'balance', Locked = true;
                }
                field(countryRegionCode; "Country/Region Code")
                {
                    Caption = 'countryRegionCode', Locked = true;
                }
                field(netChange; "Net Change")
                {
                    Caption = 'netChange', Locked = true;
                }
                field(noOfOrders; "No. of Orders")
                {
                    Caption = 'noOfOrders', Locked = true;
                }
                field(noOfReturnOrders; "No. of Return Orders")
                {
                    Caption = 'noOfReturnOrders', Locked = true;
                }
                field(phoneNo; "Phone No.")
                {
                    Caption = 'phoneNo', Locked = true;
                }
                field(salesLCY; "Sales (LCY)")
                {
                    Caption = 'salesLCY', Locked = true;
                }
                field(shippedNotInvoiced; "Shipped Not Invoiced")
                {
                    Caption = 'shippedNotInvoiced', Locked = true;
                }
            }
        }
    }   
}

当我们将其发布到 Dynamics 365 Business Central 租户时,可以通过以下端点访问此 API:

{baseUrl}/{D365BCInstance}/api/sd/customapi/v1.0/companies({id})/customers

如果我们向此端点发送 HTTP GET 请求以检索 Customer 记录,我们将获得以下 API 响应:

正如你所看到的,自定义 API 显示了我们已添加到 API 页面中的所有 Customer 字段(包括 NormalFlowfields 字段)。

在下一节中,我们将看到如何从外部应用程序使用 Dynamics 365 Business Central API。

创建一个使用 Dynamics 365 Business Central API 的应用程序

正如我们在本章中提到的,API 对于将外部应用程序与 Dynamics 365 Business Central 集成非常有用(它们允许我们使用简单的 HTTP 调用来管理 ERP 实体和业务逻辑)。例如,在这里,我们将创建一个 C# .NET 应用程序,用于在 Dynamics 365 Business Central SaaS 租户中创建 Customer 记录。

这个场景非常适用于实现自定义数据加载程序。通过使用 API,你可以创建非常强大的数据传输例程,避免使用标准工具,如配置包,从而加载大量数据。

该应用程序是一个 .NET 控制台应用程序,执行以下操作:

  • 使用基本身份验证(用户名和 Web 服务访问密钥)连接到 Dynamics 365 Business Central 租户

  • 读取此租户中的公司并检索公司 ID

  • 创建一个 JSON 对象,表示一个 Customer 记录

  • 通过向 Customer API 端点发送 POST 请求,传递要创建的 Customer 记录的 JSON 数据

本书的完整源代码可以在该书的 GitHub 仓库中找到。

该应用程序的主要功能定义如下:

static HttpClient client = new HttpClient();
static string baseURL, user, key;
static string workingCompanyID;

static void Main(string[] args)
{
   GetSettingsParameters();
   RunAsync().GetAwaiter().GetResult();
}

static async Task RunAsync()
{
   client.BaseAddress = new Uri(baseURL);
   client.DefaultRequestHeaders.Accept.Clear();
   client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
   string userAndPasswordToken =
        Convert.ToBase64String(Encoding.UTF8.GetBytes(user + ":" + key));
   client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
        $"Basic {userAndPasswordToken}");
   try
   {               
      //Reads D365BC tenant companies
      await GetCompanies(baseURL);
      //Creates a D365BC customer
      await CreateCustomer(baseURL, workingCompanyID);
   }
   catch (Exception e)
   {
      Console.WriteLine(e.Message);
   }
   Console.ReadLine();
}

Main 方法中,我们通过调用 GetSettingsParameters 函数读取应用程序设置参数,然后异步启动读取公司(GetCompanies)和通过 API 创建 Customer 记录(CreateCustomer)的任务。

GetSettingsParameters 函数定义了使用 Dynamics 365 Business Central API 所需的必填参数(例如租户 ID、API URL、用户和访问密钥),并按如下方式定义:

static void GetSettingsParameters()
{
   string tenantID = "<YourTenantIDHere>";
   baseURL = "https://api.businesscentral.dynamics.com/v2.0/" + tenantID +    
      "/production/api/beta";
   user = "<YourUsernameHere>";
   key = "<YourUserWebServiceAccessKeyHere>";
}

我们在 GetCompaniesCreateCustomer 方法中调用了 Dynamics 365 Business Central 的 API。

GetCompanies 方法中,我们向以下端点发送一个 HTTP GET 请求:

https://api.businesscentral.dynamics.com/v2.0/{tenantID}/production/api/{APIversion}/companies

然后,我们检索指定租户中的公司列表。响应是 JSON 格式的,因此我们需要解析它(我们正在检索 idname 字段)。相关代码如下:

static async Task GetCompanies(string baseURL)
{
   HttpResponseMessage response = await client.GetAsync(baseURL + "/companies");
   JObject companies = JsonConvert.DeserializeObject<JObject>   
         (response.Content.ReadAsStringAsync().Result);
   JObject o = JObject.Parse(companies.ToString());
   foreach (JToken jt in o.Children())
   {
      JProperty jProperty = jt.ToObject<JProperty>();
      string propertyName = jProperty.Name;
      if (propertyName == "value")
      {
         foreach (JToken jt1 in jProperty.Children())
         {
            JArray array = new JArray(jt1.Children());
            for (int i = 0; i < array.Count; i++)
            {
               string companyID = array[i].Value<string>("id");
               string companyName = array[i].Value<string>("name");
               Console.WriteLine("Company ID: {0}, Name: {1}", companyID, companyName);
               if (companyName == "CRONUS IT")
               {
                  workingCompanyID = companyID;
               }
            }
         }
      }
   }
}

在这里,我们希望与特定公司合作,因此我们将所需的公司 ID 保存到一个名为 workingCompanyID 的全局变量中,以便在整个应用程序中使用该公司 ID。

CreateCustomer 方法中,我们向以下 API 端点发送一个 POST 请求:

https://api.businesscentral.dynamics.com/v2.0/{tenantID}/production/api/{APIversion}/customers

这是通过在请求体中传递一个 JSON 对象来完成的。这个对象是一个表示 Customer 记录的 JSON(请求的内容类型必须是 application/json)。然后,读取 API 响应。

CreateCustomer 方法的代码如下:

static async Task CreateCustomer(string baseURL, string companyID)
{                       
   JObject customer = new JObject(           
      new JProperty("displayName", "Stefano Demiliani API"),
      new JProperty("type", "Company"),
      new JProperty("email", "demiliani@outlook.com"),
      new JProperty("website", "www.demiliani.com"),
      new JProperty("taxLiable", false),
      new JProperty("currencyId", "00000000-0000-0000-0000-000000000000"),
      new JProperty("currencyCode", "EUR"),
      new JProperty("blocked", " "),
      new JProperty("balance", 0),
      new JProperty("overdueAmount", 0),
      new JProperty("totalSalesExcludingTax", 0),
      new JProperty("address",
         new JObject(
            new JProperty("street", "Viale Kennedy 87"),
            new JProperty("city", "Borgomanero"),
            new JProperty("state", "Italy"),
            new JProperty("countryLetterCode", "IT"),
            new JProperty("postalCode", "IT-28021")
            )
       )
   );
   HttpContent httpContent = new StringContent(customer.ToString(), Encoding.UTF8,    
       "application/json");
   HttpResponseMessage response = await client.PostAsync(baseURL + 
       "/companies("+companyID+")/customers", httpContent);
   if (response.Content != null)
   {
      var responseContent = await response.Content.ReadAsStringAsync();
      Console.WriteLine("Response: " + responseContent);
   }
}

在这里,我们创建一个表示 Customer 实体的 JSON 对象,异步发送该 JSON 对象作为 HTTP POST 请求的正文到 Dynamics 365 Business Central /customers API,并读取 API 响应。当此操作被调用时,Customer 记录将在 Dynamics 365 Business Central 中创建。

请记住,Dynamics 365 Business Central 限制您在一定时间窗口内可以执行的 API 调用数量。如果外部服务对租户发起了过多请求,可能会收到 HTTP 429 错误(请求过多):

{
   "error":
   {
     "code": "Application_TooManyRequests",
     "message": "Too many requests reached. Actual (101). Maximum (100)."
   }
}

这样做的主要目的是避免例如拒绝服务DoS)攻击和租户资源不足等问题。

实际允许的每分钟最大请求数如下:

  • 沙盒环境:OData 每分钟 300 次请求(每秒 5 次请求),SOAP 每分钟 300 次请求。

  • 生产环境:OData 每分钟 600 次请求(每秒 10 次请求),SOAP 每分钟 600 次请求。

为避免这种情况,您应该处理如何向 Dynamics 365 Business Central API 端点发出请求,如果收到此错误,您应采取类似重试策略等措施,来处理外部应用中的 API 调用。

这是一个如何在自定义应用中使用 Dynamics 365 Business Central API 的示例。所提供的示例使用了 .NET 和 C#,但您可以在任何支持 HTTP 调用的平台和语言中使用这些 API。

使用绑定操作

我们可以使用绑定操作,不仅通过 RESTful API 执行 CRUD 操作,还可以调用应用中定义的标准业务逻辑(包括自定义和标准代码)。

绑定操作可以在 OData V4 端点中使用(如在demiliani.com/2019/06/12/dynamics-365-business-central-using-odata-v4-bound-actions/中所述)以及标准的 Dynamics 365 Business Central API 中。

假设你有一个代码单元(在这里描述的示例中,它被称为CustomerWSManagement),该代码单元定义了一些业务逻辑(函数集合),用于操作Customer实体,并且你想从 API 调用其中的一些方法。我们的代码单元有两个业务函数:

  • CloneCustomer:这将基于现有的客户记录创建一个新客户。

  • GetSalesAmount:这会返回给定客户的总销售金额。

CustomerWSManagement代码单元的代码定义如下:

codeunit 50102 CustomerWSManagement
{
   procedure CloneCustomer(CustomerNo: Code[20])
   var
      Customer: Record Customer;
      NewCustomer: Record Customer;
   begin
      Customer.Get(CustomerNo);
      NewCustomer.Init();
      NewCustomer.TransferFields(Customer, false);
      NewCustomer.Name := 'CUSTOMER BOUND ACTION';
      NewCustomer.Insert(true);
   end;

   procedure GetSalesAmount(CustomerNo: Code[20]): Decimal
   var
      SalesLine: Record "Sales Line";
      total: Decimal;
   begin
      SalesLine.SetRange("Document Type", SalesLine."Document Type"::Order);
      SalesLine.SetRange("Sell-to Customer No.", CustomerNo);
      SalesLine.SetFilter(Type, '<>%1', SalesLine.Type::" ");
      if SalesLine.FindSet() then
      repeat
         total += SalesLine."Line Amount";
      until SalesLine.Next() = 0;
      exit(total);
   end;
}

要使用 OData V4 绑定操作,你需要在页面中声明一个函数,并且这个函数必须具有[ServiceEnabled]属性。

如果在pageextension对象中声明了一个[ServiceEnabled]函数,并且尝试访问 OData 端点的元数据(baseurl/ODataV4/$metadata),你将看不到已发布的操作。

要发布与Customer实体相关联的操作,你需要创建一个新的页面,如下所示,然后将其发布为 Web 服务:

page 50102 "My Customer Card"
{
   PageType = Card;
   ApplicationArea = All;
   UsageCategory = Administration;
   SourceTable = Customer;
   ODataKeyFields = "No.";
   layout
   {
      area(Content)
      {
         group(GroupName)
         {
            field(Id; Id)
            {
               ApplicationArea = All;
            }
            field("No."; "No.")
            {
               ApplicationArea = All;
            }
            field(Name; Name)
            {
               ApplicationArea = All;
            }
         }
      }
   }
}

在这里,ODataKeyFields属性指定了在调用 OData 端点时作为键使用的字段(我想要的是Customer记录的No.字段)。

在这个页面中,我声明了两个过程来调用我们 AL 代码单元中定义的两个方法:

[ServiceEnabled]
procedure CloneCustomer(var actionContext: WebServiceActionContext)
var
   CustomerWSMgt: Codeunit CustomerWSManagement;
begin
   CustomerWSMgt.CloneCustomer(Rec."No.");
   actionContext.SetObjectType(ObjectType::Page);
   actionContext.SetObjectId(Page::"My Customer Card");
   actionContext.AddEntityKey(Rec.FIELDNO("No."), Rec."No.");
   //Set the result code to inform the caller that the record is created
   actionContext.SetResultCode(WebServiceActionResultCode::Created);
end;

[ServiceEnabled]
procedure GetSalesAmount(CustomerNo: Code[20]): Decimal
var
   actionContext: WebServiceActionContext;
   CustomerWSMgt: Codeunit CustomerWSManagement;
   total: Decimal;
begin
   actionContext.SetObjectType(ObjectType::Page);
   actionContext.SetObjectId(Page::"My Customer Card");
   actionContext.AddEntityKey(Rec.FIELDNO("No."), rec."No.");
   total := CustomerWSMgt.GetSalesAmount(CustomerNo);
   //Set the result code to inform the caller that the result is retrieved
   actionContext.SetResultCode(WebServiceActionResultCode::Get);
   exit(total);
end;

从前面的代码中,我们可以看到以下内容:

  • CloneCustomer是一个无参数调用的过程。它获取调用的上下文,并调用我们代码单元中定义的CloneCustomer方法。

  • GetSalesAmount是一个过程,它接受一个代码参数,调用我们代码单元中定义的GetSalesAmount过程,并将结果作为响应返回。

当我们将MyCustomerCard页面发布为 Web 服务(在这里称为MyCustomerCardWS)时,这些过程定义会发生什么?

如果我们访问 OData V4 元数据端点,现在可以看到操作已经发布:

现在,我们可以尝试通过 OData 调用绑定的操作。作为第一步,我们想要调用CloneCustomer函数。为此,我们需要向以下端点发送 HTTP POST 请求:

https://yourbaseurl/ODataV4/Company('CRONUS%20IT')/MyCustomerCardWS('10000')/NAV.CloneCustomer

以下是我们在调用后获得的输出:

我们代码单元中的代码被调用,并且创建了一个Customer记录(即通过编号为"No." = 10000的客户进行克隆):

我们要调用的第二个函数(GetSalesAmount)需要一个Code[20]参数作为输入(这不是严格要求的,仅仅是为了展示如何将参数传递给绑定操作)。我们需要向以下端点发送 POST 请求:

https://yourbaseurl/ODataV4/Company('CRONUS%20IT')/MyCustomerCardWS('10000')/NAV.GetSalesAmount

如我们所见,这是通过传递包含所需参数的 JSON 正文来完成的。

发送的 POST 请求如下:

POST https://d365bcita0918vm.westeurope.cloudapp.azure.com:7048/NAV/ODataV4/Company('CRONUS%20IT')/MyCustomerCardWS('10000')/NAV.GetSalesAmount
Content-Type: application/json
Authorization: Basic admin Z1JkubB/3epQOtfnBph04rcNgyFpaEuB9OVTnrd0VPs=

{
 "customerno": "10000"
}

发送 POST 请求后收到的响应如下:

正如您所见,响应是一个 JSON 对象,其值为给定客户的总销售金额(通过调用我们的代码单元方法获取)。

要在 JSON 对象中传递的参数名称必须与 OData 元数据匹配,而不是您函数的参数。

在下一节中,我们将探讨 Dynamics 365 Business Central 中 Webhooks 的概念,并探讨如何订阅从 Dynamics 365 Business Central 实体发送的通知。

使用 Dynamics 365 Business Central Webhooks

Webhooks 是创建事件驱动服务集成的一种方法:客户端不需轮询其他系统以检查实体是否有更改,而是订阅将从源系统推送到其的事件。Dynamics 365 Business Central 支持 Webhooks,因此客户端可以订阅 Webhook 通知(事件),并将自动接收到 Dynamics 365 Business Central 实体更改的通知。

要使用 Dynamics 365 Business Central 的 Webhooks,我们需要执行以下步骤:

  1. 订阅者必须通过向 subscription API 发送 POST 请求并在请求主体中传递通知 URL 来向 Dynamics 365 Business Central 注册 Webhook 订阅。端点 URL 如下:
https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/subscriptions

建立订阅的请求主体如下(这里,我们以 customers 实体为例):

{
  "notificationUrl": "YourAplicationUrl",
  "resource": "https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/companies(COMPANYID)/customers",
  "clientState": "SomeSharedSecretForTheNotificationUrl"
}
  1. Dynamics 365 Business Central 向订阅者返回验证令牌。

  2. 订阅者需要在响应主体中返回验证令牌,并提供状态码 200(这是强制性的握手阶段)。

  3. 如果 Dynamics 365 Business Central 在响应主体中收到验证令牌,则注册订阅并将通知发送到通知 URL。

订阅建立后,订阅者将收到有关已订阅实体每次更新的通知。如果在此之前不续订,Webhook 订阅将在 3 天后过期。

要续订 Webhook 订阅,订阅者必须向订阅端点发送 PATCH 请求(此请求也需要握手阶段)。用于续订 Webhook 订阅的请求端点如下:

https://api.businesscentral.dynamics.com/v2.0/TENANTID/production/api/v1.0/subscriptions('SUBSCRIPTIONID')

要更新 Webhook 订阅,您需要在 PATCH 请求头中作为 If-Match 块传递之前建立的订阅的 @odata.etag 标签。

当订阅建立时收到的 HTTP 响应如下:

如果尝试再次向已建立活动订阅的同一端点发出订阅请求,将收到以下错误:

当建立订阅时,订阅者可以在 Dynamics 365 Business Central 中的订阅实体被修改时接收通知。这是发送给订阅者的通知示例(该通知是一个包含所有修改实体的 JSON 对象):

{
  "value": [
    {
      "subscriptionId": "customers",
      "clientState": "someClientState",
      "expirationDateTime": "2019-07-20T07:52:31Z",
      "resource": "api/beta/companies(80d28ea6-02a3-4ec3-98f7-
                   936c2000c7b3)/customers(26814998-936a-401c-81c1-0e848a64971d)",
      "changeType": "updated",
      "lastModifiedDateTime": "2019-07-19T12:54:20.467Z"
    },
    {
      "subscriptionId": "webhookCustomersId",
      "clientState": "someClientState",
      "expirationDateTime": "2019-07-20T07:52:31Z",
      "resource": "api/beta/companies(80d28ea6-02a3-4ec3-98f7-
                   936c2000c7b3)/customers(130bbd17-dbb9-4790-9b12-2b0e9c9d22c3)",
      "changeType": "created",
      "lastModifiedDateTime": "2019-07-19T12:54:26.057Z"
    }
  ]
}

Webhooks 也可用于我们的扩展中的自定义对象。具有PageType = API的页面将暴露具有以下限制的 Webhooks(这些限制也适用于标准 API 页面):

  • 页面不能使用复合键。

  • 页面不能使用临时表或系统表作为SourceTable

可以通过向/subscriptions({id})端点发送 DELETE 请求来删除 Webhook 的订阅。此外,要删除订阅,您需要发送包含@odata.etagIf-Match头信息的请求。

有关 Dynamics 365 Business Central Webhooks 的更多信息,我建议查看这篇文章:

demiliani.com/2019/12/10/webhooks-with-dynamics-365-business-central/

在本节中,您了解了 Dynamics 365 Business Central 中 Webhook 的工作原理。在下一节中,我们将展示如何使用 Microsoft Graph API 与 Dynamics 365 Business Central 配合使用。

在 Microsoft Graph 中使用 Dynamics 365 Business Central API

Microsoft Graph (graph.microsoft.io/) 是一个有趣的平台,提供了一个独特的网关,用于跨多个 Microsoft 服务的 RESTful API。现在,Dynamics 365 Business Central 是 Microsoft Graph 中可用的端点之一。

要在 Graph 中使用 Dynamics 365 Business Central,您首先需要更改 Dynamics 365 Business Central 用户在 Graph 中的权限,然后启用Financials.ReadWrite.All权限范围。您可以通过使用 Graph Explorer 工具来完成:

设置权限后,您可以开始使用在 Graph 中可用的 Dynamics 365 Business Central API(实际上,您需要使用 BETA API 端点)。

例如,要检索您 Dynamics 365 Business Central 租户中的可用公司,您需要向https://graph.microsoft.com/beta/financials/companies发送 HTTP GET 请求,如下所示:

您可以解析此 JSON 响应并检索公司 ID,之后将在所有后续 API 调用中使用此 ID。

要检索某个公司Customer记录的列表,您需要向以下 URL 发送 HTTP GET 请求(并传递该公司的 ID):

https://graph.microsoft.com/beta/financials/companies('80d28ea6-02a3-4ec3-98f7-936c2000c7b3')/customers

作为响应,您将收到包含所有客户列表的 JSON 数据:

要按降序posting date检索某个公司的一般账簿条目,可以向以下 URL 发送 HTTP GET 请求:

https://graph.microsoft.com/beta/financials/companies('80d28ea6-02a3-4ec3-98f7-936c2000c7b3')/generalLedgerEntries?$orderby=postingDate desc

这是从 API 接收到的响应:

例如,要获取某个Currency(例如 USD)的详细信息,你需要向以下 URL 发送 HTTP GET 请求:

https://graph.microsoft.com/beta/financials/companies('80d28ea6-02a3-4ec3-98f7-936c2000c7b3')/currencies?$filter=code eq 'USD'

获取的响应将如下所示:

从这个响应中,我们可以获取货币的 ID,因为我们可以稍后使用它,通过 Graph API 在 Dynamics 365 Business Central 中创建新的Customer记录。

要在公司中创建一个Customer记录,并将Currency Code设置为 USD,你需要向以下端点发送 HTTP POST 请求,并将Content-type设置为application/json

https://graph.microsoft.com/beta/financials/companies('80d28ea6-02a3-4ec3-98f7-936c2000c7b3')/customers

该 POST 请求的请求体必须是包含我们想要创建的客户详细信息的 JSON 内容,如下所示:

{
 "displayName": "Graph Customer",
 "type": "Company",
 "address": {
 "street": "V.le Kennedy 8",
 "city": "Novara",
 "state": "IT",
 "countryLetterCode": "IT",
 "postalCode": "28021"
 },
 "phoneNumber": "",
 "email": "graph@packtpub.com",
 "website": "",
 "currencyId": "12902bb7-4938-41b9-8617-33492bcac8b3",
 "currencyCode": "USD",
 "blocked": " ",
 "overdueAmount": 0
}

作为响应,我们将得到一些包含已创建的Customer记录的 JSON 数据:

如果你现在打开 Dynamics 365 Business Central,你会看到新创建的Customer记录:

在 Graph 中可用的 Dynamics 365 Business Central API 列在docs.microsoft.com/en-us/graph/api/resources/dynamics-graph-reference?view=graph-rest-beta

目前将其视为 Beta 版本,因为它们将在未来得到改进。

我们已经介绍了如何使用 Graph API 与 Dynamics 365 Business Central 进行交互。在接下来的章节中,我们将概述自动化 API。

Dynamics 365 Business Central 中的自动化 API

Dynamics 365 Business Central 还暴露了用于自动化租户相关任务的 API,例如以下内容:

  • 创建公司

  • 管理用户、组和权限

  • 处理扩展(租户扩展的安装/卸载)

  • 导入和应用配置包

自动化 API 位于/microsoft/automation命名空间下。例如,要在 Dynamics 365 Business Central 租户中创建公司,你可以向以下端点发送 HTTP POST 请求:

POST https://api.businesscentral.dynamics.com/v2.0/api/microsoft/automation/{apiVersion}/companies({companyId})/automationCompanies
Authorization: Bearer {token}
Content-type: application/json
{
    "name": "PACKT PUB",
    "displayName": "PACKT Publishing",
    "evaluationCompany": false,
    "businessProfileId": ""
}

要获取你租户中的用户信息,你需要向以下端点发送 GET 请求:

GET https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/beta/companies({id})/users

当你获取到用户的详细信息后,要通过自动化 API 为用户分配权限集,你需要向以下端点发送 POST 请求:

POST https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/{apiVersion}/companies({companyId})//users({userSecurityId})/userGroupMembers
Authorization: Bearer {token}

{ 
  "code": "D365 EXT. ACCOUNTANT",
  "companyName" :"CRONUS IT"
}

要修改 Dynamics 365 Business Central 用户的详细信息,你需要向以下端点发送 HTTP PATCH 请求:

PATCH https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/beta/companies({id})/users({userSecurityId})
Content-type: application/json
If-Match:*
{
 "state": "Enabled",
 "expiryDate": "2021-01-01T21:00:53.444Z"
}

要获取已安装在租户上的扩展列表,你可以向以下端点发送 GET 请求:

GET https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/{apiVersion}/companies({{companyid}})/extensions

要处理扩展的安装和卸载,你可以向以下绑定的操作发送 POST 请求:

  • Microsoft.NAV.install

  • Microsoft.NAV.uninstall

例如,要卸载一个之前安装的扩展,你可以向以下端点发送 POST 请求:

POST https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/{apiVersion}/companies({companyId})//extensions({extensionId})/Microsoft.NAV.uninstall
Authorization: Bearer {token}

AppSource 扩展必须先在租户上安装,然后你可以通过自动化 API 安装/卸载它们。

如果您有一个每租户扩展,可以通过向以下端点发送 PATCH 请求将其上传并安装到 SaaS 租户上:

PATCH https://api.businesscentral.dynamics.com/v1.0/api/microsoft/automation/beta/companies({companyId})/extensionUpload(0)/content
Authorization : Bearer {token}
Content-type : application/octet-stream
If-Match:-*

在这里,请求体内容必须包含要上传到租户的 .app 包文件(二进制)。使用自动化 APIs 进行身份验证时,必须使用 OAuth 2.0 授权(Bearer Token)。

关于 Dynamics 365 Business Central APIs 的更多信息,请访问docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/administration/itpro-introduction-to-automation-apis

如果需要激活 CI/CD 流水线并且需要为租户提供初始数据,自动化 APIs 就显得极其重要和强大。

概要

在本章中,我们概述了如何使用 OData 栈(特别是 RESTful APIs)与 Dynamics 365 Business Central 进行集成。我们看到了如何使用标准 APIs,创建自定义 APIs,创建使用 Dynamics 365 Business Central APIs 的应用程序,以及如何使用 Webhooks 和 Graph APIs 等高级概念。然后,我们概述了自动化 APIs。

本章结束时,您已经全面了解了如何暴露 Dynamics 365 Business Central 的业务逻辑和实体,并如何通过使用 REST HTTP 调用处理与外部应用程序的集成。APIs 是 Dynamics 365 Business Central 集成的未来,您已经学会了如何在您的应用程序和扩展中使用它们。

在下一章中,我们将看到如何在 Dynamics 365 Business Central 扩展中使用 Azure Functions 和其他无服务器服务。

第十三章:使用 Business Central 和 Azure 实现无服务器业务流程

在第十二章《Dynamics 365 Business Central APIs》中,我们回顾了由 Dynamics 365 Business Central 提供的各种 API 的概述。我们学习了如何在应用程序中使用这些 API,以及如何创建自定义 API。

本章将介绍一个在云环境中架构业务应用时出现的重要概念:无服务器处理。正如你所知,在 SaaS 环境中,你不能使用所有本地环境中可用的功能(如文件和 .NET DLLs)。在云环境中,你需要重新思考这些功能,使用云基础设施提供的服务。

本章将学习以下内容:

  • Azure 平台提供的无服务器功能概览

  • 使用 Azure Functions 与 Dynamics 365 Business Central 集成

  • Dynamics 365 Business Central 在现实应用中的无服务器处理场景

到本章结束时,你将清楚地理解如何通过使用 Azure Functions 在 Dynamics 365 Business Central 中实现无服务器流程。

技术要求

为了跟上本章内容,你需要准备以下内容:

  • 一个有效的 Azure 订阅(可以是付费订阅,或者是可以免费激活的试用订阅,访问 azure.microsoft.com/free/

  • Visual Studio 或 Visual Studio Code

  • Visual Studio 或 Visual Studio Code 的 Azure Functions 扩展

Microsoft Azure 无服务器服务概览

无服务器技术在云计算世界中至关重要。它们使你能够专注于业务应用和代码,而不必处理资源的配置和管理、扩展以及更广泛地说,处理运行应用所需的基础设施。

Azure 提供了一整套托管的无服务器服务,你可以将其作为构建应用程序的基础,这些服务包括计算资源、数据库、存储、编排、监控、智能和分析。

Azure 无服务器服务可以分为以下几类:

  • 计算:

    • 无服务器函数(Azure Functions)

    • 无服务器应用环境(Azure 应用服务)

    • 无服务器 Kubernetes(Azure Kubernetes 服务)

  • 存储:

    • Azure 无服务器存储(Blob 存储)
  • 数据库

  • 工作流和集成:

    • Azure 逻辑应用

    • Azure API 管理

    • Azure 事件网格

  • 监控:

    • Azure Monitor
  • 分析:

    • Azure 流分析
  • 人工智能和机器学习:

    • Azure 认知服务

    • Azure 机器学习服务

    • Azure Bot 服务

  • DevOps:

    • Azure DevOps

处理服务所需的基础设施由 Microsoft 在全球数据中心全面管理。使用 Azure 无服务器处理时,你将享有以下三个主要优势:

  • 您可以根据需要扩展服务:您可以扩展服务的实例数量(增加或减少实例),或者您可以扩展服务的资源(增加或减少资源)。

  • 按需付费:您只需为代码运行时或执行代码时所使用的资源付费。

  • 集成安全性和监控:由 Azure 平台管理的功能。

有关 Azure 提供的无服务器服务的更多信息,请访问 azure.microsoft.com/en-us/solutions/serverless/

现在我们已经了解了 Microsoft Azure 无服务器服务的概述,接下来让我们看看 Azure Functions 的概览。

获取 Azure Functions 概览

Azure Functions 是 Azure 提供的一项服务,提供函数即服务。您可以编写代码(使用不同的编程语言),无需担心基础设施问题,您的代码将在云端执行。使用 Azure Functions,您可以按需执行代码(在请求函数后),按计划执行,或自动响应不同的事件。

您可以通过 Azure 门户直接编写 Azure 函数,或者在本地开发机器上进行开发。您还可以在将其部署到云端之前,本地调试和测试 Azure 函数。

Azure Functions 的以下是主要功能:

  • 使用您最熟悉的编程语言进行开发,或将现有代码重用到云端。

  • 集成安全性;您可以指定所需的安全设置,平台将自动处理。

  • 可扩展性管理;您可以根据负载和使用情况选择服务等级。

  • 按需付费定价模型

在撰写本文时,以下 Azure 函数类型可用:

  • HTTPTrigger:这是经典的函数类型,您的代码执行是通过 HTTP 请求触发的。

  • TimerTrigger:您的代码将在预定的时间表上执行。

  • BlobTrigger:当一个 Blob 被添加到 Azure Blob 存储容器时,您的代码将被执行。

  • QueueTrigger:当消息到达 Azure 存储队列时,您的代码将被执行。

  • EventGridTrigger:当事件被传递到 Azure Event Grid 中的订阅时,您的代码将被执行(基于事件的架构)。

  • EventHubTrigger:当事件被传递到 Azure Event Hub 时,您的代码将被执行(通常用于物联网场景)。

  • ServiceBusQueueTrigger:当消息到达 Azure 服务总线队列时,您的代码将被执行。

  • ServiceBusTopicTrigger:当订阅主题的消息到达 Azure 服务总线时,您的代码将被执行。

  • CosmosDBTrigger:当文档被添加或更新到存储在 Azure Cosmos DB 中的文档集合时,您的代码将被执行。

Azure Functions 提供以下定价计划:

  • Consumption plan:您为代码在云端执行的时间付费。

  • 应用服务计划:你为托管计划付费,就像正常的 Web 应用一样。你可以在同一个应用服务计划上运行不同的函数。

正如我们之前提到的,直接使用 .NET DLLs(AL 中的 .NET 变量)在 SaaS 环境中不可用。Azure Functions 是在 SaaS 环境中将 .NET 代码与 Dynamics 365 Business Central 结合使用的推荐方式。

在接下来的章节中,我们将查看一个验证邮箱地址的 Azure 函数的实现。我们将使用这个函数来验证 Dynamics 365 Business Central 中客户记录关联的邮箱地址。这个函数将使用 Visual Studio 开发,然后使用 Visual Studio Code 开发。

使用 Visual Studio 开发 Azure 函数

要使用 Visual Studio 创建 Azure 函数,你需要安装 Azure SDK 工具。这些工具可以在安装 Visual Studio 时直接安装,也可以稍后通过访问 azure.microsoft.com/en-us/downloads/ 安装。

现在,按照以下步骤学习如何开发 Azure 函数:

  1. 使用 Visual Studio(我使用的是 2019 版本),创建一个新项目并选择 Azure Functions 模板:

  1. 为你的项目选择一个名称(在这里,我使用 EmailValidator),选择一个保存项目文件的位置,并点击创建:

  1. 接下来,你需要选择你的 Azure 函数的运行时版本:

    • Azure Functions v2 (.NET Standard):基于 .NET Core(跨平台),这是新的可用运行时。

    • Azure Functions v1 (.NET Framework):基于 .NET Framework,仅支持在 Azure 门户或 Windows 计算机上进行开发和托管。

在这里,我选择了 Azure Functions v2 (.NET Core)。

  1. 然后,你需要选择 Azure 函数类型(我选择了 Http 触发器,因为我希望创建一个可以通过 HTTP 调用的函数),为了简单起见,我选择了匿名作为我们函数的访问权限(无需身份验证,任何人都可以使用;我们将在本章的 管理 Azure 函数密钥 部分讨论这一点)。

  2. 现在,点击 OK 来创建解决方案。这是你在 Visual Studio 中看到的项目树:

在这里,你会看到以下文件:

  • host.json:这个文件包含影响项目中所有函数的全局配置选项。在我们的项目中,我们有运行时版本(2.0)。

  • local.settings.json:这个文件包含你的项目设置。

  • EmailValidator.cs:这是你函数的源代码(C#)。

我们函数的实现非常简单。它接收一个 email 参数作为输入(通过 GET 或 POST 请求),验证邮箱地址,然后返回一个 JSON 响应,说明该地址是否有效(这是一个名为 EmailValidationResult 的自定义对象)。

函数代码定义如下:

[FunctionName("EmailValidator")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
     ILogger log)
{
     log.LogInformation("C# HTTP trigger function processed a request.");
     string email = req.Query["email"];
     string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
     dynamic data = JsonConvert.DeserializeObject(requestBody);
     email = email ?? data?.email;

     //Validating the email address
     EmailValidationResult jsonResponse = new EmailValidationResult();
     jsonResponse.Email = email;
     jsonResponse.Valid = IsEmailValid(email);
     string json = JsonConvert.SerializeObject(jsonResponse);

     return email != null
           ? (ActionResult)new OkObjectResult($"{json}")
           : new BadRequestObjectResult("Please pass a email parameter on the query string or in the request body");
}

这个函数从输入中获取 email 参数,并调用 IsEmailValid 函数。

这个函数通过使用 System.Net.Mail.MailAddress 类来验证电子邮件地址,如下所示:

static bool IsEmailValid(string emailaddress)
{
    try
    {
        System.Net.Mail.MailAddress m = new System.Net.Mail.MailAddress(emailaddress);
        return true;
    }
    catch (FormatException)
    {
        return false;
    }
}

在电子邮件验证后,函数会创建一个 EmailValidationResult 对象,并用响应值进行序列化,最后返回 JSON 响应。这个 EmailValidationResult 对象定义如下:

public class EmailValidationResult
{
    public string Email { get; set; }
    public bool Valid { get; set; }
}

现在我们已经测试了函数,是时候将其本地发布了。

本地测试 Azure 函数

Visual Studio 提供了一个模拟器,我们可以使用它来测试和调试我们的 Azure 函数,直到它部署到 Azure。如果你运行这个项目,模拟器会启动并为你提供一个本地 URL 用于测试你的函数:

我们可以通过打开浏览器并调用一个 URL 来测试我们的函数,例如 localhost:7071/api/EmailValidator?email=masteringd365bc@packt.com

当被调用时,模拟器会显示请求详情:

我们可以在浏览器中看到 JSON 响应。之前的调用返回以下响应对象:

{"Email":"masteringd365bc@packt.com","Valid":true}

如果我们用一个无效的电子邮件地址调用这个函数,比如 http://localhost:7071/api/EmailValidator?email=masteringd365bc,我们会得到以下响应对象:

{"Email":"masteringd365bc","Valid":false}

我们的函数运行正常。

现在我们已经测试了 Azure 函数,准备将其部署。

部署函数到 Azure

现在,我们需要将函数部署到 Azure。我们可以直接从 Visual Studio 进行部署,步骤如下:

  1. 右键点击项目并选择 发布...:

  1. 我们需要选择一个 Azure 应用服务来部署函数。为此,我们可以选择一个现有的,或者创建一个新的。这里,我为这个函数创建了一个新的 Azure 应用服务实例:

点击发布。

  1. 然后,我们选择订阅、资源组(创建一个新组或使用现有的)、托管计划以及用于部署的存储账户:

现在,点击 创建 – 我们的函数(以及所有相关资源)将被部署到 Azure。

  1. 现在,我们的函数已经发布到 Azure 数据中心,并且我们有了一个公共 URL,以便使用,如下所示的截图所示:

在我的示例中,公共 URL 为 emailvalidator20190603055323.azurewebsites.net(你可以自定义它)。

要测试你的函数,使用以下 URL: https://emailvalidator20190603055323.azurewebsites.net/**api/EmailValidator?email=masteringd365bc****@packt.com****。

现在,函数已在 Azure 云中运行,你可以使用它了。

还有一种替代的 Azure 函数开发方式,对于任何 Dynamics 365 Business Central 开发者来说,这非常重要。我们将在下一节中查看这个方法。

使用 Visual Studio Code 开发 Azure 函数

要开始使用 Visual Studio Code 开发 Azure 函数,您需要安装以下扩展:

您还需要安装 Azure Functions Core Tools,这是一个工具集,允许您在本地机器上开发和测试您的函数。

您需要安装支持您正在使用的 Azure Functions 运行时的版本。您可以在github.com/Azure/azure-functions-core-tools安装此版本。

要使用 Visual Studio Code 开发 Azure 函数,请按照以下步骤操作:

  1. 您需要做的第一件事是通过 Visual Studio Code 命令面板中的 Azure: Sign In 命令登录到您的 Azure 订阅:

输入凭据后,您将在 Visual Studio Code 的底部栏看到与您的订阅关联的帐户。

  1. 现在,在 Visual Studio Code 侧边栏中,选择 Azure Functions 扩展并点击“创建新项目”按钮:

选择一个文件夹,将您的 Azure Functions 项目放在其中。

  1. 然后,系统会提示您选择用于开发函数的语言。在支持的语言列表中,我选择了 C#,如图所示:

  1. 接下来,您需要为您的 Azure 函数选择以下任一运行时版本:

    • Azure Functions v2(.NET Standard):基于 .NET Core(跨平台),这是新的可用运行时。

    • Azure Functions v1(.NET Framework):基于 .NET Framework,仅支持在 Azure 门户或 Windows 计算机上进行开发和托管。

在这里,我选择了 Azure Functions v2(.NET Standard):

  1. 现在,为您的 Azure Functions 项目设置一个名称(在这里,我将其命名为EmailValidatorCore):

  1. 命名项目后,提供一个命名空间:

  1. 现在,您需要选择函数的认证类型。为了简便起见,我选择了匿名(每个人都可以调用我们的函数):

  1. 现在,选择打开将创建的项目的位置(当前窗口、新窗口和添加到工作区是可用的选项)。

Visual Studio Code 将开始下载你的 Azure Functions 项目所需的包,下载完成后,将创建一组文件。以下是你的 Azure 函数的模板:

在这里,你会看到以下文件:

  • host.json:此文件包含影响项目中所有函数的全局配置选项。在我们的项目中,我们有运行时版本(2.0)。

  • local.settings.json:此文件包含你的项目设置。

  • EmailValidatorCore.cs:这是你的函数源代码(C#)。

请注意,你的代码中可能会出现一些临时错误(缺少引用)。这种情况发生在 Visual Studio Code 需要下载所有 .NET Core 包时。为了获取正确的引用,你需要从命令面板执行 .NET: Restore Project 命令,具体操作如下:

选择推荐的选项并点击确定,如下所示:

现在你的项目引用已修复,你可以开始编写函数代码了。

如同之前的示例一样,我们要创建一个用于验证电子邮件地址的函数,并且可以重用相同的 C# 代码。这里,你已经了解了如何通过直接使用 Visual Studio Code 来开始创建一个 Azure Functions 项目。

本地测试你的 Azure 函数

为了在本地测试你的函数,你需要安装 Azure Functions Core Tools。你可以在命令提示符(或 Visual Studio Code 终端)中使用以下命令安装:

npm i -g azure-functions-core-tools --unsafe-perm true

要在 Visual Studio Code 中使用 npm,你需要在机器上安装 Node.js。你可以从 nodejs.org/en/ 安装它。

当你运行 npm 命令时,某些包会被下载并安装到你的本地机器上:

安装工具后,你需要重启 Visual Studio Code 以使其生效。

要开始在本地测试你的函数,你可以直接在 Visual Studio Code 中按 F5 键。此时本地的 Azure Function Host 环境将启动,Visual Studio Code 会提供一个本地 URL,供你调用你的函数:

现在,我们可以通过浏览器中的 URL 传递电子邮件参数来测试该函数(就像我们在 将函数部署到 Azure 部分所做的那样)。

同时,我们可以通过在 Visual Studio Code 中设置断点、逐步调试、检查变量和输出等,直接调试代码,如下图所示:

现在,你已经在本地环境中调试并测试了你的 Azure 函数。在下一部分,我们将学习如何将函数发布到 Azure 云端。

将你的函数发布到 Azure

要将你的 Azure 函数部署到 Azure,按照以下简单步骤进行:

  1. 点击 Visual Studio Code 侧边栏中的 Azure Functions 图标,然后点击名为“部署到 Function App”的蓝色箭头图标,如下所示:

  1. Visual Studio Code 将要求您从帐户的可用订阅列表中选择一个 Azure 订阅作为部署函数的目标位置,如下所示:

  1. 现在,选择在 Azure 中创建新的 Function App 并为其指定一个全球唯一的名称:

我称之为SDEmailValidatorCore*。

  1. 从这里,您可以创建一个新的资源组和一个新的存储帐户。选择您希望部署函数的区域。然后,资源部署将开始。

  2. 部署过程完成后,Visual Studio Code 将在右下角显示确认消息。你可以在左侧的订阅树视图中看到已部署的功能:

现在,您的 Azure 函数已在 Azure 数据中心运行,您可以开始在 AL 代码中使用它。我们将在下一节中更详细地讨论这一点。

从 AL 调用 Azure 函数

现在我们已将函数部署到 Azure,可以在扩展中的 AL 代码中使用它。

正如我们在第六章《高级 AL 开发》中的从 AL 调用 Web 服务和 API一节所解释的那样,我们可以通过使用HttpClient数据类型来调用 Azure 函数,该数据类型提供了一种发送 HTTP 请求和接收来自通过 URI 标识的资源的 HTTP 响应的方式。

为了测试我们的 Azure 函数,我们将创建一个简单的扩展(一个使用AL:Go!的新项目),它允许我们验证与客户记录关联的电子邮件地址。我们的CustomerEmailValidation扩展由一个代码单元对象组成,我们在其中定义了一个事件订阅者来监听Customer表中电子邮件字段的OnAfterValidate事件。在这个EventSubscriber过程(名为ValidateCustomerEmail)中,我们做了以下操作:

  • 使用HttpClient对象调用 Azure 函数。

  • 使用HttpResponse对象读取 Azure 函数的响应。

  • 解析 JSON 响应以提取Valid标记。

  • 如果Valid = false,则抛出错误。

这段代码如下所示:

codeunit 50100 EmailValidation_PKT
{
    [EventSubscriber(ObjectType::table, Database::Customer, 'OnAfterValidateEvent', 'E-Mail', false, false)]
    local procedure ValidateCustomerEmail(var Rec: Record Customer)
    var
        httpClient: HttpClient;
        httpResponse: HttpResponseMessage;
        jsonText: Text;
        jsonObj: JsonObject;
        funcUrl: Label 'https://sdemailvalidatorcore.azurewebsites.net/api/emailvalidatorcore?email=';
        InvalidEmailError: Label 'Invalid email address.';
        InvalidJonError: Label 'Invalid JSON response.';
        validationResult: Boolean;
    begin
        if rec."E-Mail" <> '' then begin
            httpClient.Get(funcUrl + rec."E-Mail", httpResponse);
            httpResponse.Content().ReadAs(jsonText);
            //Response JSON format: {"Email":"test@packt.com","Valid":true}
            if not jsonObj.ReadFrom(jsonText) then
                Error(InvalidJonError);
            //Read the Valid token from the response
            validationResult := GetJsonToken(jsonObj, 'Valid').AsValue().AsBoolean();
           if not validationResult then
                Error(InvalidEmailError);
        end;
    end;

    local procedure GetJsonToken(jsonObject: JsonObject; token: Text) jsonToken: JsonToken
    var
        TokenNotFoundErr: Label 'Token %1 not found.';
    begin
        if not jsonObject.Get(token, jsonToken) then
            Error(TokenNotFoundErr, token);
    end;
}

按下F5并部署你的扩展。

如果打开客户卡,转到地址和联系方式标签页,并在电子邮件字段中插入一个值,您将收到以下消息:

这是您第一次这样做时发生的,因为默认情况下,沙箱环境会阻止外部 HTTP 调用。请选择“允许一次”并点击“确定”。

然后,我们的 Azure 函数被调用,JSON 响应被检索并解析,验证过程开始。如果你插入一个有效的电子邮件地址,值会正确插入到电子邮件字段中;否则,你将收到一个错误,如下图所示:

为了自动允许沙盒环境中的外部调用,在 NAV App Settings 表中,有一个名为 Allow HttpClient Requests 的布尔字段,当用户选择其中一个始终选项(始终允许或始终阻止)时,会在此表中插入一条记录,并将字段设置为 truefalse

你还可以通过 AL 代码以编程方式控制此设置。在我们的扩展中,我们添加了以下过程:

local procedure EnableExternalCallsInSandbox()
    var
        NAVAppSetting: Record "NAV App Setting";
        EnvironmentInformation: Codeunit "Environment Information";
        ModInfo: ModuleInfo;
    begin
        NavApp.GetCurrentModuleInfo(ModInfo);
        if EnvironmentInformation.IsSandbox() then begin
            NAVAppSetting."App ID" := ModInfo.Id();
            NAVAppSetting."Allow HttpClient Requests" := true;
            if not NAVAppSetting.Insert() then
                NAVAppSetting.Modify();
        end;
    end;

上述代码检索当前扩展的信息(ModuleInfo),然后检查扩展是否在沙盒环境中运行。如果是,它将 Allow HttpClient Requests 字段设置为 true

在我们之前的事件订阅者(ValidateCustomerEmail 过程)中,当需要时,我们通过调用 EnableExternalCallsInSandbox 过程来启用外部调用,如下所示:

begin
    if rec."E-Mail" <> '' then begin
        EnableExternalCallsInSandbox();
        httpClient.Get(funcUrl + rec."E-Mail", httpResponse);

这样可以避免在每次外部 HTTP 调用时看到确认请求。

在下一节中,我们将学习如何使用 Azure Functions 和 Azure Blob 存储来处理云中的文件。

与 Azure Blob 存储交互以处理云中的文件

我们在 Dynamics 365 Business Central 的云环境中遇到的主要问题之一与文件保存有关。正如我们之前提到的,在云环境中,你没有文件系统,无法访问本地资源,如网络共享或本地磁盘。

如果你想在 Dynamics 365 Business Central SaaS 中保存文件,解决方案是调用 Azure 函数并将文件存储到基于云的存储中。你可以创建一个将文件保存到 Azure Blob 存储的函数,之后你可以将 Azure 存储共享为网络驱动器。这是我们将在本节中介绍的场景。

创建 Azure Blob 存储帐户

实现我们解决方案的第一步是需要在 Azure 上拥有一个存储帐户,并且我们需要在该存储帐户中创建一个 Blob 容器。

要手动创建一个 Azure 存储帐户(在本例中为 d365bcfilestorage),请按照以下步骤操作:

  1. 在 Azure 门户中选择存储帐户,点击创建,并按照屏幕上的指示操作。要在此存储帐户中创建 Blob 容器,请从服务部分选择 Blobs:

  1. 然后,点击容器,给它命名(在这里,我选择了 d365bcfiles),选择公共访问级别(默认情况下,容器数据对帐户所有者是私密的),然后点击确定:

现在,Blob 容器已经在你的 Azure 存储帐户中创建。

访问 Azure 存储账户的连接字符串(必须在 Azure 函数中使用)可以通过选择存储账户并点击“访问密钥”来获取:

在这里,我们将把连接字符串嵌入到函数项目中,但在生产环境中,您可以将其存储在 Azure Key Vault 中,并从中检索以提高安全性。

在我们的 Blob 容器中,我手动上传了一个文件(PNG 图片)用于存储文件:

现在,我们已经在 Azure 存储上创建了一个 Blob 容器,以便托管我们的文件。在接下来的部分,我们将创建一个 Azure 函数,用于保存和检索这个 Blob 存储中的文件。

使用 Visual Studio 创建 Azure 函数

打开 Visual Studio,选择 HttpTrigger 模板并创建一个新的 Azure 函数项目。将函数命名为 SaaSFileMgt

在我们的项目中,我们想要创建以下几个函数:

  • UploadFile:此函数将通过 HTTP POST 请求接收一个包含二进制数据(文件内容)和一些元数据(文件详细信息)的对象。它会将文件存储在 Azure Blob 存储账户的容器中。

  • DownloadFile:此函数将接收一个包含要下载文件的 URI 和其详细信息的对象(通过 HTTP POST 请求),并返回存储在 Azure Blob 存储容器中的文件的二进制数据。

  • ListFiles:此函数将通过 HTTP GET 请求检索存储在 Azure Blob 存储容器中的所有文件的 URI 列表。这些文件将在接下来的部分中解释。

上传/下载文件的函数必须只支持 HTTP POST 方法,因此,在 HttpTrigger 定义模板中,我们已经移除了 get 参数。这些函数的签名如下:

[FunctionName("UploadFile")]
public static async Task<IActionResult> Upload(
   [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger    
   log)

[FunctionName("DownloadFile")]
public static async Task<IActionResult> Download(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger 
    log)

列出 Blob 存储中文件的函数必须只支持 GET 方法,函数签名如下:

[FunctionName("ListFiles")]
public static async Task<IActionResult> Dir(
    [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
 ILogger log)

让我们逐一探索这些函数。

UploadFile 函数

UploadFile 函数通过 HTTP POST 请求接收一个 JSON 对象,具体如下:

{
    "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAACVCAYAAAC3i3MLAA",
    "fileName": "MyFile.png",
    "fileType": "image/png",
    "fileExt": "png"
}

UploadFile 函数解析请求体中的 JSON,然后调用 UploadBlobAsync 函数。在此函数中,我们将文件上传到 Azure Blob 存储容器,并返回上传文件的 URI。

UploadFile 函数的代码如下:

[FunctionName("UploadFile")]
public static async Task<IActionResult> Upload(                
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
    {
        log.LogInformation("C# HTTP trigger function processed a request.");
        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        string base64String = data.base64;
        string fileName = data.fileName;
        string fileType = data.fileType;
        string fileExt = data.fileExt;
        Uri uri = await UploadBlobAsync(base64String, fileName, fileType,     
                    fileExt);           
        return fileName != null
            ? (ActionResult)new OkObjectResult($"File {fileName} stored. URI = {uri}")
            : new BadRequestObjectResult("Error on input parameter (object)");
}

UploadBlobAsync 是一个将文件上传到 Azure 存储账户中的 d365bcfiles 容器的函数。其代码如下:

public static async Task<Uri> UploadBlobAsync(string base64String, string fileName, string fileType, string fileExtension)
{
    string contentType = fileType;           
    byte[] fileBytes = Convert.FromBase64String(base64String);
    CloudStorageAccount storageAccount =     
        CloudStorageAccount.Parse(BLOBStorageConnectionString);
    CloudBlobClient client = storageAccount.CreateCloudBlobClient();
    CloudBlobContainer container = client.GetContainerReference("d365bcfiles");
    await container.CreateIfNotExistsAsync(
        BlobContainerPublicAccessType.Blob,
        new BlobRequestOptions(),
        new OperationContext());
    CloudBlockBlob blob = container.GetBlockBlobReference(fileName);
    blob.Properties.ContentType = contentType;
    using (Stream stream = new MemoryStream(fileBytes, 0, fileBytes.Length))
    {
        await blob.UploadFromStreamAsync(stream).ConfigureAwait(false);
    }
    return blob.Uri;
}

DownloadFile 函数

DownloadFile 函数通过 HTTP POST 请求接收一个 JSON 对象,具体如下:

{
    "url": "https://d365bcfilestorage.blob.core.windows.net/d365bcfiles/MasteringD365BC.png",
    "fileType": "image/png",
    "fileName": "MasteringD365BC.png"
}

该函数从请求体中检索文件的详细信息,然后调用 DownloadBlobAsync 函数。接着,它返回下载文件的内容(Base64 编码 字符串):

[FunctionName("DownloadFile")]
public static async Task<IActionResult> Download(
    [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");
    try
    {
        string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        string url = data.url;
        string contentType = data.fileType;
        string fileName = data.fileName;
        byte[] x = await DownloadBlobAsync(url, fileName);
        //Returns the Base64 string of the retrieved file
        return (ActionResult)new OkObjectResult($"{Convert.ToBase64String(x)}");            
    }
    catch(Exception ex)
    {
        log.LogInformation("Bad input request: " + ex.Message);
        return new BadRequestObjectResult("Error on input parameter (object): " + 
            ex.Message);
    }
}

DownloadBlobAsync 是连接到 Azure 存储 Blob 容器、检查文件并(如果文件存在)返回该文件的字节数组(流)的函数:

public static async Task<byte[]> DownloadBlobAsync(string url, string fileName)
{
    CloudStorageAccount storageAccount =
        CloudStorageAccount.Parse(BLOBStorageConnectionString);
    CloudBlobClient client = storageAccount.CreateCloudBlobClient();
    CloudBlobContainer container = client.GetContainerReference("d365bcfiles"); 
    CloudBlockBlob blob = container.GetBlockBlobReference(fileName);
    await blob.FetchAttributesAsync();
    long fileByteLength = blob.Properties.Length;
    byte[] fileContent = new byte[fileByteLength];
    for (int i = 0; i < fileByteLength; i++)
    {
        fileContent[i] = 0x20;
    }
    await blob.DownloadToByteArrayAsync(fileContent, 0);
    return fileContent;
}

ListFiles 函数

ListFiles 函数通过 HTTP GET 请求(无参数)调用。它调用 ListBlobAsync 函数,然后返回存储在 Blob 容器中的文件 URI 列表(JSON 格式)。

它的代码定义如下:

[FunctionName("ListFiles")]
        public static async Task<IActionResult> Dir(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            dynamic data = JsonConvert.DeserializeObject(requestBody);
            var URIfileList = await ListBlobAsync();
            string json = JsonConvert.SerializeObject(URIfileList);
            return URIfileList != null
                ? (ActionResult)new OkObjectResult($"{json}")
               : new BadRequestObjectResult("Bad request.");
       }

ListBlobAsync 函数连接到 Azure 存储容器,并使用 ListBlobsSegmentedAsync 方法检索存储在其中的 Blob 文件列表。

此方法通过使用 BlobContinuationToken 分段返回文件列表。当此令牌为 null 时,所有文件都会被检索:

public static async Task<List<Uri>> ListBlobAsync()
{
    CloudStorageAccount storageAccount = 
        CloudStorageAccount.Parse(BLOBStorageConnectionString);
    CloudBlobClient client = storageAccount.CreateCloudBlobClient();
    CloudBlobContainer container = client.GetContainerReference("d365bcfiles");
    List<Uri> URIFileList = new List<Uri>();
    BlobContinuationToken blobContinuationToken = null;
    do
    {
        var resultSegment = await container.ListBlobsSegmentedAsync(prefix: null,
                                           useFlatBlobListing: true,
                                           blobListingDetails: BlobListingDetails.None,
                                           maxResults: null,
                                           currentToken: blobContinuationToken,
                                           options: null,
                                           operationContext: null);
        // Get the value of the continuation token returned by the listing call.
        blobContinuationToken = resultSegment.ContinuationToken;
        foreach (IListBlobItem item in resultSegment.Results)
        {
            URIFileList.Add(item.Uri);
        }
    } while (blobContinuationToken != null); //Loop while the continuation token is not null.

    return URIFileList;
}

现在,我们已经创建了 Azure 函数,是时候部署它们了。在下一部分,我们将学习如何进行部署。

部署 Azure 函数

我们的 Azure 函数可以直接从 Visual Studio 部署到我们的 Azure 订阅。要将这些函数部署到 Azure,你需要创建一个新的 Azure App Service 实例,如下所示:

之后,将你的函数发布到这个 Azure App Service:

如果你进入 Azure 门户,你将看到这些函数已经发布,并且你现在有一个公共 URL 来测试它们:

现在,函数运行在 Azure 数据中心。在下一部分,我们将学习如何管理已部署函数的访问密钥(授权)。

管理 Azure Functions 密钥

Azure Functions 使用授权密钥来保护对 HTTP 触发的函数的访问。当你部署一个函数时,可以在以下授权级别之间进行选择:

  • 匿名:无需访问密钥。

  • 函数:访问该函数需要特定的访问密钥。

  • 管理员:需要主机密钥。

你可以通过从 Azure 门户选择你的函数并点击“管理”来直接管理这些访问密钥:

如果你想以编程方式管理密钥,还可以使用密钥管理 API(github.com/Azure/azure-functions-host/wiki/Key-management-API)。

关于如何管理 Azure Functions 授权密钥的更多信息可以在这里找到:docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook#authorization-keys

在本节中,你已经学会了如何处理 Azure 函数的访问密钥。在下一部分,我们将学习如何测试我们之前部署的 Azure 函数(从 Blob 存储上传和下载文件)。

测试 Azure 函数

在我们的场景中,Azure 函数已使用 Function 授权级别部署,因此我们需要通过传递一个 code 参数和从门户中获取的函数密钥来调用所需的函数。例如,要测试 ListFiles 函数,我们需要调用以下 URL:saasfilemgt.azurewebsites.net/api/ListFiles?code=FUNCTIONKEY

这是我们收到的响应(我们的 Blob 文件的 URI 列表):

要测试 DownloadFile 函数,我们需要通过传递以下参数的 JSON 对象(用于标识要检索的文件)发送 POST 请求到函数的 URL。

{
    "url": "https://d365bcfilestorage.blob.core.windows.net/d365bcfiles/MasteringD365BC.png",
    "fileType": "image/png",
    "fileName": "MasteringD365BC.png"
}

从 Visual Studio Code 启动 HTTP 请求,我们得到以下响应:

如您所见,该函数从 Azure Blob 存储下载请求的文件(函数返回 Base64 字符串)。

在下一节中,我们将学习如何从 Dynamics 365 Business Central 的 AL 扩展中调用我们的 Azure 函数。

编写 Dynamics 365 Business Central 扩展

我们想在此创建的扩展是一个简单的应用程序,用于在 项目列表 页面上添加两个操作,用于上传和下载文件到/从 Azure Blob 存储。

此 AL 扩展将定义两个对象:

  • codeunit 对象具有调用处理 Azure Blob 存储文件的 Azure 函数的逻辑

  • 一个 pageextension 对象,为项目列表页面添加操作,并调用我们代码单元中定义的相关过程。

让我们更详细地查看这些内容。

Codeunit 定义

代码单元称为 SaaSFileMgt,包含两个过程:

  • UploadFile:此函数将处理上传到 Azure Blob 存储的文件。

  • DownloadFile:此函数将处理从 Azure Blob 存储下载文件。

在代码单元中,我们有两个全局变量,均包含要调用的 Azure 函数的 URL:

var
        BaseUrlUploadFunction: Label 'https://saasfilemgt.azurewebsites.net/api/UploadFile?code=YOURFUNCTIONKEY';
        BaseUrlDownloadFunction: Label 
                'https://saasfilemgt.azurewebsites.net/api/DownloadFile?code=YOURFUNCTIONKEY';

这里,YOURFUNCTIONKEY 是我们用于访问 Azure 函数的密钥(从 Azure 门户中选择函数并单击管理获取)。

UploadFile 过程定义如下:

    procedure UploadFile()
    var
      fileMgt: Codeunit "File Management";
      selectedFile: Text;
      httpClient: HttpClient;
      httpContent: HttpContent;
      jsonBody: text;
      httpResponse: HttpResponseMessage;
      httpHeader: HttpHeaders;
      fileName: Text;
      fileExt: Text;
      base64Convert: Codeunit "Base64 Convert";
      instr: InStream;
    begin
        UploadIntoStream('Select a file to upload','','',selectedFile,instr);
        fileName := delchr(fileMgt.GetFileName(selectedFile), '=', '.' + 
                fileMgt.GetExtension(selectedFile));
        fileExt := fileMgt.GetExtension(selectedFile);
        jsonBody := ' {"base64":"' + tempblob.ToBase64String() +
                '","fileName":"' + fileName + '.' + fileExt +
                '","fileType":"' + GetMimeType(selectedFile) + '", "fileExt":"' + 
                 fileMgt.GetExtension(selectedFile) + '"}';
        httpContent.WriteFrom(jsonBody);
        httpContent.GetHeaders(httpHeader);
        httpHeader.Remove('Content-Type');
        httpHeader.Add('Content-Type', 'application/json');
        httpClient.Post(BaseUrlUploadFunction, httpContent, httpResponse);
        //Here we should read the response to retrieve the URI
        message('File uploaded.');
    end;

从前述代码可以看到以下内容:

  1. 我们要求上传文件。

  2. 我们将文件读入 Stream 对象中。

  3. 我们检索与文件相关的一些参数(名称和扩展名)。

  4. 我们按照函数请求创建 JSON 消息(如前所述)。

  5. 然后,我们通过使用 HttpClient 对象向我们的 Azure 函数发送 HTTP POST 请求,将 JSON 放在主体中。

DownloadFile 过程定义如下:

    procedure DownloadFile(fileName: Text; blobUrl: Text)
    var
        tempblob: Codeunit "Temp Blob";
        httpClient: HttpClient;
        httpContent: HttpContent;
        jsonBody: text;
        httpResponse: HttpResponseMessage;
        httpHeader: HttpHeaders;
        base64: Text;
        fileType: Text;
        fileStream: InStream;
        base64Convert: Codeunit "Base64 Convert";
        outstr: OutStream;
    begin
        fileType := GetMimeType(fileName);
        jsonBody := ' {"url":"' + blobUrl + '","fileName":"' + fileName + '", "fileType":"' + 
                    fileType + '"}';
        httpContent.WriteFrom(jsonBody);
        httpContent.GetHeaders(httpHeader);
        httpHeader.Remove('Content-Type');
        httpHeader.Add('Content-Type', 'application/json');
        httpClient.Post(BaseUrlDownloadFunction, httpContent, httpResponse);
        httpResponse.Content.ReadAs(base64);
        base64 := DelChr(base64, '=', '"');
        base64Convert.FromBase64(base64);
        tempblob.CreateOutStream(outstr);
        outstr.WriteText(base64);
        tempblob.CreateInStream(fileStream);
        DownloadFromStream(fileStream, 'Download file from Azure Storage', '', '', fileName);
    end;

此过程接收要检索的文件名称作为输入。其工作方式如下:

  1. 它调用一个自定义函数(GetMimeType),返回此文件的内容类型,然后我们为请求组合 JSON 消息。

  2. 然后,我们发送一个 HTTP POST 请求到我们的 Azure 函数(通过使用HttpClient对象),并将 JSON 放入请求体中。

  3. 我们读取HttpResponse对象(获取文件的 Base64 编码),并通过使用InStream对象并调用DownloadFromStream方法将其下载到客户端:

GetMimeType工具的定义如下:

local procedure GetMimeType(selectedFile: Text): Text
var
    fileMgt: Codeunit "File Management";
    mimeType: Text;
begin
    case lowercase(fileMgt.GetExtension(selectedFile)) of
        'pdf':
            mimeType := 'application/pdf';
        'txt':
            mimeType := 'text/plain';
        'csv':
            mimeType := 'text/csv';
        'png':
            mimeType := 'image/png';
        'jpg':
            mimeType := 'image/jpg';
        'bmp':
            mimeType := 'image/bmp';
        'gif':
            mimeType := 'image/gif';
        else
            Error('File Format not supported!');
    end;
    EXIT(mimeType);
end;

这个操作只是接收一个文件名作为输入,获取文件扩展名,然后返回与文件关联的 MIME 类型。

pageextension定义

pageextension对象通过添加我们之前描述的两个操作来扩展 Item List 页面。该对象的定义如下:

pageextension 50103 ItemListExt extends "Item List"
{
    actions
    {
        addlast(Creation)
        {
            Action(Upload)
            {
                ApplicationArea = All;
                Caption = 'Upload file to Azure Blob Storage';
                Image = Add;
                Promoted = true;
                trigger OnAction();
                var
                    SaaSFileMgt: Codeunit SaaSFileMgt;
                begin
                    SaaSFileMgt.UploadFile();
                end;
            }

            Action(Download)
            {
                ApplicationArea = All;
                Caption = 'Download file from Azure Blob Storage';
                Image = MoveDown;
                Promoted = true;
                trigger OnAction();
                var
                    SaaSFileMgt: Codeunit SaaSFileMgt;
                begin
                    SaaSFileMgt.DownloadFile('TEST.txt', 'https://d365bcfilestorage.blob.core.windows.net/d365bcfiles/TEST.txt');
                end;
            }
        }
    }
}

这两个操作简单地通过传递所需的参数调用我们代码单元中定义的方法。

在下一节中,我们将测试集成的解决方案(由 Dynamics 365 Business Central 调用的 Azure 函数)。

测试我们的应用程序

现在,是时候测试我们的应用程序并查看它是如何工作的了。发布后,我们的扩展将在Item List页面上添加两个文件上传/下载的功能:

如果你点击“上传文件到 Azure Blob 存储”操作,你可以从本地计算机选择一个文件:

这个文件将通过 HTTP POST 请求上传到我们的 Azure Blob Storage 容器。我们可以调试httpResponse对象,看到它返回HttpStatusCode = 200 (成功)

我们将收到一条消息,告诉我们文件已成功上传到 Azure Blob Storage,如下所示:

如果我们检查 Azure 存储账户中的 blob 容器,我们会看到文件已经正确上传到 blob 存储中:

当你点击“从 Azure Blob Storage 下载文件”操作时,将执行一个 HTTP POST 请求到DownloadFile Azure 函数(通过传递 JSON 请求体,如我们之前所描述的),并且httpResponse对象返回HttpStatusCode = 200 (成功)

httpResponse对象是获取的文件的 Base64 编码。在这里,Base64 字符串被解码,文件通过使用InStream下载到客户端:

如你所见,文件是从流中获取的,浏览器提示用户将其下载到本地。

现在,你可以在云环境中处理文件(上传和下载)。Azure 还允许我们将 Blob 存储映射到我们的本地网络,从而实现一个对最终用户完全透明的无服务器文件系统。

总结

在本章中,我们了解了什么是 Azure 函数,以及如何在我们的 Dynamics 365 Business Central 扩展中使用它们,以便在云环境中执行.NET 代码,并实现无服务器的流程。

我们学习了如何使用 Visual Studio 和 Visual Studio Code 创建一个简单的 Azure 函数,以及如何将 Azure Functions 与其他 Azure 服务一起使用(特别是如何结合 Azure Functions 和 Azure Blob Storage,在 Dynamics 365 Business Central 中实现云端文件系统)。

阅读本章后,你应该理解如何开发、部署和使用 Azure 函数,在完全无服务器的方式下在云中实现业务任务。在现代基于云的 ERP 中,这是一个非常重要的功能,需要掌握以扩展平台功能并融入其他云服务。

在下一章,我们将学习如何在云中监控和扩展我们的函数,并通过使用 Azure DevOps,将 DevOps 技术(持续集成和持续部署)应用到我们的 Azure Functions 项目中。

第十四章:监视、扩展和 CI/CD 与 Azure 函数

在上一章中,我们学习了如何使用 Visual Studio 和 Visual Studio Code 创建 Azure 函数,如何在 Azure 上发布函数,以及如何从 Dynamics 365 Business Central 扩展中调用 Azure 函数。

我们还学习了如何在 Azure 上使用 Azure 函数来实现无服务器流程,并通过实际示例了解了在 SaaS 环境中处理文件的.NET 代码的执行。理解这些主题非常重要,以便您能够在云环境中执行自定义代码,并以无服务器方式与外部服务交互。

在本章中,我们将探讨与 Azure 函数相关的其他重要方面,包括以下内容:

  • 监视发布在 Azure 云中的函数

  • 扩展 Azure 函数

  • 将 DevOps 和 CI/CD 应用于 Azure 函数

到本章结束时,您将对 Azure 函数服务有一个完整深入的概述,并且您将对在 Microsoft Dynamics 365 Business Central 项目中使用此服务执行复杂代码和无服务器任务充满信心。

技术要求

要遵循本章内容,您需要具备以下条件:

监视 Azure 函数

在 Azure 上管理无服务器服务时,监视 Azure 函数是一项重要任务。如果您希望拥有始终正常工作的可靠服务,您需要制定一个策略来检查函数的传入调用、错误、特定函数的可伸缩性需求等。

从 Azure 门户中选择您的 Azure 函数。通过单击监视,您将能够查看已记录的请求(成功和错误)。以下屏幕截图显示了这一点:

如果您选择单个请求,您可以查看其调用详细信息:

对于更高级的日志记录,您可以单击在应用洞察中运行(必须从门户激活):

在这里,您还可以在函数的遥测中执行自定义查询。例如,这是一个关于遥测日志的查询,显示过去 20 分钟内每个工作角色(函数)的请求次数:

requests
| where timestamp > ago(20m)
| summarize count() by cloud_RoleInstance, bin(timestamp, 1m)
| render timechart

以下是此内容的输出:

下面是一个在应用洞察中执行的有用查询,用于检索函数中的错误:

traces 
| where customDimensions.LogLevel == "Error"

当配置了应用程序洞察(Application Insights)时,你将拥有一个漂亮的面板,能够立即查看函数中的所有事件(失败的请求、服务器响应时间、服务器请求次数、可用性等):

应用程序洞察是一个重要的检查部分,如果你希望发现故障并确定函数是否表现良好或需要扩展,可以查看此功能。以下截图展示了这一点:

有关如何高效使用应用程序洞察的更多信息,请访问docs.microsoft.com/en-us/azure/azure-functions/functions-monitoring

正如你所看到的,应用程序洞察是一个极其强大的工具,能帮助你监控实时服务并管理函数日志。我建议你始终启用它,以便将 Azure 上的无服务器平台保持在可控状态。接下来,我们来了解 Azure Functions 的工作原理。

扩展 Azure Functions

在生产环境中使用 Azure Functions 时,扩展性是一个重要的检查方面,因为它允许你优化服务,避免瓶颈或资源短缺。

Azure Functions 在两个不同的计划上执行:

  • 消费计划:你为所消耗的资源付费。应用根据功率需求自动扩展或缩减。计费依据是执行次数、执行时间和应用使用的内存。

  • 高级计划:这与消费计划非常相似(根据请求的功率自动处理扩展)。你的计费取决于每秒核心数和每秒使用的内存 GB 总量,涵盖了所有实例。高级计划增加了以下功能:

    • 始终预热实例以避免冷启动

    • VNet 连接

    • 无限执行时长

    • 高级实例大小(一个核心、两个核心和四个核心实例)

    • 可预测的定价选项

    • 高密度应用分配,适用于包含多个函数应用的计划

你可以通过 Azure 门户中的概述选项卡查看函数使用的托管计划:

你的 Azure 函数还可以在与其他云应用程序(如 App Service 应用)相同的专用环境中运行。这被称为应用服务计划,你为同一应用服务计划中的所有函数支付相同的配额。当你使用应用服务计划时,可以手动扩展(例如通过添加更多虚拟机实例)或自动扩展(通过启用自动扩展)。

有关此计划的更多信息,请参见docs.microsoft.com/en-us/azure/app-service/overview-hosting-plans

现在,您清楚地了解了在将函数部署到 Azure 时可用的扩展和相关计费选项。根据您的业务需求,您应选择您希望使用的计划。

Azure Functions 和 DevOps

在本节中,我们将讨论 DevOps 技术,特别是如何使用 Azure DevOps 为您的 Azure Functions 创建 CI/CD 过程。

首先,为了实现源代码管理,您的函数代码必须托管在 Git 仓库中。这里,我们使用 Azure DevOps 作为我们的 CI/CD 过程的仓库:

  1. 首先,在 Azure DevOps 中创建一个新项目。此项目将包含您的源代码的 Git 仓库。点击左侧的Repos菜单,您将看到此仓库的 URL。使用以下命令将其克隆到您的本地文件夹:
Git clone https://demiliani@dev.azure.com/demiliani/AzureFunctionDevOps/_git/AzureFunctionDevOps
  1. 在这个本地仓库中,我已将我们之前使用 Visual Studio 开发并提交的 EmailValidator Azure 函数的所有项目文件放置其中。然后,我们将所有文件推送到我们的在线 Azure DevOps 仓库,如下图所示:

我们的代码现在已在 Azure DevOps 上。

  1. 现在,我们想为我们的 Azure Functions 项目创建一个构建管道。从 Azure DevOps 项目页面,选择 Pipelines | Builds 并点击新建管道(New Pipeline)。在“您的代码在哪里?”页面,点击使用经典编辑器(Use the classic editor)以在没有 YAML 的情况下创建管道:

  1. 现在,您需要选择代码将托管在哪里。选择 Azure Repos Git,并从列表中选择一个仓库(您应该只为所选项目拥有一个仓库)以及一个分支:

  1. 点击继续(Continue)。在选择模板页面上,您需要为构建管道选择一个模板。向下滚动列表,选择适用于 .NET 的 Azure Functions,然后点击应用(Apply):

您的构建管道将根据您选择的模板创建。

  1. 点击保存并排队(Save & queue):

  1. 现在,会出现一个新的保存构建管道和排队窗口。在这里,再次点击保存并排队(Save & queue):

  1. 现在,您的构建管道已排队。在屏幕顶部,您应该看到类似以下的消息:

  1. 如果点击构建号,您将看到已执行的构建步骤:

构建过程正在进行,而且好的一点是,您还会收到一封电子邮件,通知您构建结果。

我们手动触发了构建,但一个优秀的开发者通常是个懒开发者,因此我们希望在每次提交到主分支时自动触发构建。让我们来看一下如何做到这一点:

  1. 要定义构建触发器,选择构建管道并点击编辑(Edit):

  1. 在这里,选择触发器并点击启用持续集成:

  1. 点击“保存并排队”并选择“保存”选项。

现在,当您向代码库推送新的代码修改时,构建会自动触发。

如果我们希望在每次提交时自动部署 Azure 上的函数项目,我们必须创建一个发布流水线。让我们来学习如何做到这一点:

  1. 要在 Azure DevOps 上创建发布流水线,请选择“流水线”|“发布”,然后点击“新建流水线”。接着,选择“Azure 应用服务部署”并点击“应用”:

  1. 在下一屏幕中,点击左侧“工件”部分的“添加”:

  1. 在“添加工件”页面上,选择“构建”作为源类型。然后,选择您的构建流水线并点击“添加”:

  1. 现在,点击“阶段 1”上的红色感叹号查看构建任务:

在这里,您需要选择一个 Azure 订阅,并将应用类型设置为 Windows 上的“函数应用”。

  1. 要使用 Azure 订阅,您需要在做出选择后点击“授权”按钮(如果弹出窗口拦截器已开启,需要禁用它):

  1. 当您的订阅已被授权后,选择应用服务名称:

  1. 然后,点击“保存”。这样,您的发布流水线将被保存并准备好:

  1. 要在 Azure DevOps 上手动创建一个新发布,请选择“流水线”|“发布”,然后点击“创建发布”。在“创建新发布”页面上,不要更改任何内容,只需点击“创建”:

  1. 新的发布将会被排队:

  1. 如果您点击发布名称,您将被重定向到发布进度页面:

  1. 如果您的发布流水线成功,您的函数将被自动部署到 Azure 应用服务:

  1. 最后,为了完全启用持续部署流程,我们需要再次编辑发布流水线,并点击持续部署图标:

  1. 在这里,我们需要启用“持续部署触发器”选项:

就这样!我们已经为我们的 Azure 函数设置了 CI/CD 流程。

使用此过程,并通过 Azure DevOps,您可以自动构建并发布(部署)一个 Azure 函数到您的云环境中,整个过程都在一个托管平台上完成。

概要

在本章中,我们学习了如何监视 Azure 函数,如何监控传入的调用和日志,以及如何使用 Application Insights 进行高级分析。

总的来说,我们学习了如何为我们将在云端部署的功能选择最佳的服务计划,接着我们又学习了如何实施 CI/CD 技术,以创建一个智能的云端功能部署流程。

现在,您已经清楚地了解了可用于在 Azure 上部署和监控服务的选项,并且学会了如何为您的业务案例选择最佳的部署模型。您还学会了如何在开发流水线中激活 CI/CD 流程,从而在 Azure 上构建和部署功能。

在下一章中,我们将讨论如何将 Dynamics 365 Business Central 与Microsoft Power Platform集成,特别是如何使用FlowPowerApps与 Dynamics 365 Business Central 配合,实施“零编码”业务流程。

第十五章:Business Central 与 Power Platform 的集成

在上一章中,我们看到如何使用 Azure 服务结合 Dynamics 365 Business Central 来实现现代无服务器业务流程,并且我们对如何从 AL 中使用 Azure Functions 进行了广泛的概述。

在本章中,我们将介绍 Dynamics 365 系列中的一个重要部分:Microsoft Power Platform。我们将涵盖以下主题:

  • Microsoft Power Platform 介绍

  • Microsoft Flow 和 PowerApps 概述

  • 使用 Flow 和 PowerApps 与 Dynamics 365 Business Central 生态系统集成来实施现实场景的实现

本章结束时,您将对 Power Platform 有一个完整的概览,并能够以低代码方式使用 Flow 和 PowerApps 来实现与 Dynamics 365 Business Central 的自定义解决方案和工作流。

技术要求

为了跟随本章中的示例,您需要以下内容:

  • 具有 Flow 和 PowerApps 访问权限的 Office 365 订阅

  • 一个活跃的 Dynamics 365 Business Central 在线租户

Power Platform 介绍

Power PlatformFlowPowerAppsPowerBI 等应用程序的功能结合成一个统一的商业应用平台。Satya Nadella 在他的演讲中多次直接解释了这个平台在 Microsoft 生态系统中的重要性。在现代商业应用程序的实现中,我们需要认真考虑 Power Platform。

该平台采用 分析、行动和自动化 范式。Power Platform 的每个组件都建立在 应用程序的通用数据服务(一个允许用户快速集成程序、构建新的自定义应用程序并创建自动化工作流的平台)之上,并且该平台将数据汇聚到一个叫做 通用数据模型 的结构中。

该平台的架构如以下图所示:

FlowPowerApps 是基于低代码开发的平台,由 Microsoft 制造,为我们所说的 强大用户 提供强大的工具——这些用户通常没有技术背景,但完全参与并深入了解业务,他们熟知所有的业务规则、流程例外和其他业务方面,能够从最终用户的角度了解系统可以做什么。

所以,我们已经解释了 Power Platform 是什么以及它的构建块是什么。由于这些工具已经可以使用,为什么不将其与 Dynamics 365 Business Central 结合起来,为那些强大用户提供更多的能力,来创建应用程序、集成和业务逻辑工作流,并为我们的企业构建一个完全集成和管理的平台呢?

在接下来的部分中,我们将回顾 Flow 和 PowerApps 的概述,并展示一些涉及 Dynamics 365 Business Central 与 Power Platform 联动的真实世界解决方案。

了解 Flow

Flow 是一个平台,旨在通过连接器和触发器提供创建自动化工作流的方式,并将 Microsoft 和非 Microsoft 技术集成在一起。这些工具对最终用户开放,使他们可以以可视化的方式构建业务流程。更多信息请参见 us.flow.microsoft.com/en-us/

Flow 拥有超过 180 个不同的连接器,您可以通过操作和触发器使用它们来构建自动化工作流。这些连接器之一是 Dynamics 365 Business Central 连接器,它是从 Flow 与 Dynamics 365 Business Central 交互的标准连接器。该连接器可通过 us.flow.microsoft.com/en-us/connectors/?filter=business+central&category=all 获取。

使用 Flow 的最大优势之一是,它为我们提供了一种可视化的方式来构建自动化工作流,使我们能够将 Dynamics 环境中的数据与外部平台交互、创建审批流程、发送通知操作等应用程序,且操作简便。

如下图所示,Microsoft 还为我们提供了许多现有模板,帮助我们快速创建第一个 Flow 工作流:

更多信息请参见 us.flow.microsoft.com/en-us/templates/

通过按 business central 筛选模板,您可以看到所有可用的模板(您也可以提交您为技术社区创建的模板),如下所示:

通过这些模板,您可以玩转与其他平台集成的工作流,并且可以轻松地开始使用一个可以根据您的需求进行更改的模板。

这些模板始终由社区和 Flow 团队持续改进和开发。

开始使用模板创建工作流,了解其工作原理,然后通过添加其他操作和集成来根据您的需求进行更改。

如您所见,使用 Flow,您可以立即在现有的 Dynamics 365 Business Central 解决方案中使用大量现成的任务(工作流),而无需编写一行代码。只需使用连接器并通过几次点击即可启动工作流。

在下一部分,我们将概览 PowerApps,看看它在实现您的解决方案时能提供什么。

了解 PowerApps

PowerApps 是一个平台,旨在创建可在手机、平板电脑和网页浏览器上使用的商业应用程序,所有这些都使用非常简便的用户界面。它还通过连接器将这些应用程序与 Microsoft 和非 Microsoft 平台集成。

这些应用的分发、权限、许可和身份验证基于 Microsoft 365 许可证,因此它已经与您的 Microsoft 365 环境完全集成。

关于 PowerApps 及其许可的更多信息,请访问powerapps.microsoft.com/

PowerApps Studio (web.powerapps.com)中,你可以轻松创建应用:

由于 PowerApps 也使用与 Flow 相同的连接器概念,我们还可以创建与 Dynamics 365 Business Central 环境中的数据连接并交互的应用,从而为我们提供了一种方式,创建可以在浏览器和移动设备上运行的业务应用,并且能够在 Dynamics 365 Business Central 中执行业务逻辑。

在接下来的部分中,我们将学习如何在实际场景中使用 Flow 和 PowerApps 与 Dynamics 365 Business Central 结合,并实现一个现代化的业务解决方案。

与 Power Platform 的集成场景

现在我们已经概览了FlowPowerApps,并且知道它们可以让我们与 Dynamics 365 Business Central 结合,构建强大的集成解决方案,从而提供更多的控制、集成和扩展性,接下来让我们讨论并学习如何将这些技术结合起来,充分发挥环境的潜力。

以下场景仅为一些帮助你头脑风暴的想法,并展示了你可以使用的一些功能。然而,这些平台非常灵活,可以适应不同的使用场景,甚至需要一本完整的书籍才能涵盖所有内容,因此请记得利用这些场景为你的日常需求提取一些想法。

场景 1 – 创建人力资源招聘/入职流程

在这个场景中,我们将创建一个招聘/入职流程,便于人力资源部门将表单发送给新员工,让他们填写。此数据经过审批流程后,将自动注册到 Dynamics 365 Business Central。

使用的技术: Microsoft Forms、Flow、Teams 和 Dynamics 365 Business Central。

首先,让我们在 Microsoft Forms 上创建表单。目标是为人力资源经理提供一种方式,生成一个外部表单,通过链接发送给新员工,员工可以在上面填写他们的数据,从而避免了需要员工亲自填写表单的繁琐程序。

创建此表单的步骤如下:

  1. 使用你的 Office 365 帐户打开 Forms,forms.microsoft.com,然后点击“新建表单”。

  2. 添加表单的名称和描述。

  3. 现在,让我们在表单中添加一些字段,考虑到我们在 Dynamics 365 Business Central 中需要/想要的所有字段。要创建一个新字段,只需点击“+ 添加新字段”按钮:

  1. 然后,选择要创建的字段类型(在这里,我选择了“选项”):

  1. 然后,选择标签:

让我们创建以下字段:

    • 名字 (文本)

    • 姓氏 (文本)

    • 电子邮件 (文本)

    • 地址 (文本)

    • 地址城市 (文本)

    • 地址状态 (文本)

    • 地址邮政编码 (文本)

    • 电话号码 (文本)

    • 生日 (日期)

  1. 最终的表单将如下所示:

在 Forms 上创建表单后,我们可以创建我们的 Flow 工作流:

  1. 打开 Microsoft Flow 网站(flow.microsoft.com),使用您的 Office 365 账户登录,然后进入我的流程(My Flows)。

  2. 点击“+ 新建 | 从空白创建”:

  1. 让我们寻找更多的触发器。点击“搜索数百个连接器和触发器”。

  2. 搜索Microsoft Forms,然后选择当提交新响应时触发器:

  1. 在触发器操作中,从“表单 Id”字段中选择你的表单(新的入职流程):

  1. 现在,让我们添加一个新步骤。点击“+ 新步骤”:

  1. 选择“内置”然后选择应用于每个

我们将选择当提交新响应时触发器中的表单响应作为此“应用于每个”操作的输出。基本上,对于每个提交的响应(在此情况下,总是一个响应,这是 Flow 执行方式),我们将在工作流执行时做一些操作:

  1. 现在,我们可以创建一个审批流程,在响应提交到 Dynamics 365 Business Central 之前进行验证。点击“添加一个操作”:

  1. 搜索Microsoft Forms,并选择获取响应详情

  1. 在“表单 Id”字段中选择我们创建的表单,并将“响应 Id”添加为响应 Id。此操作将填写表单上的所有字段:

  1. 现在,点击“添加一个操作”添加一个新步骤:

  1. 搜索Approvals,然后选择开始并等待审批

  1. 审批类型将是“批准/拒绝 - 第一个回应者”(如果你愿意,可以更改为其他选项):

  1. 你可以个性化审批信息,发送给负责做出审批决策的人力资源人员:

在“分配给”字段中,我使用了固定的人员,但如果你愿意,你可以与其他应用程序创建集成,自动设置审批人。

  1. 配置完批准后,我们需要处理批准人完成审批流程时的情况。在此示例中,如果得到批准,我们将在 Dynamics 365 Business Central 中创建该条目。如果被拒绝,我们将向员工发送电子邮件要求重新填写表单。为此,添加一个动作:

  1. 然后,选择条件:

  1. 条件必须使用来自响应的值(这是当审批流程处理多个审批时的响应集合):

  1. 我们需要检查该响应的值是否等于“批准”:

  1. 如果条件为真,我们将在 Dynamics 365 Business Central 中创建该条目,并将消息发布到我们的入职 Teams 频道。在 "如果是" 框中,我们添加一个动作:

  1. 搜索 Dynamics 365 Business Central 然后选择创建项:

  1. 选择你的公司名称。在 "表格名称" 下,你可以看到我们可以使用 Flow 交互的所有表格。对于此场景,我们将使用员工表:

  1. 选择员工表后,我们将能够看到所有可用的字段(所有可以通过 Flow 发送信息的字段):

  1. 让我们开始从表单响应中提取数据,并将其添加到 Dynamics 365 Business Central 动作的正确位置:

  1. 现在,我们将在 Teams 频道中创建一条信息,通知人力资源团队,新的入职流程已提交并获得批准,且该条目已在 Dynamics 365 Business Central 中创建。点击添加一个动作:

  1. 搜索 Microsoft Teams 然后点击 "以 Flow 机器人身份在频道中发布消息"。这将自动以 Flow 机器人用户身份发布到 Teams 频道:

  1. 选择你要发布的团队和频道:

  1. 在 "消息" 下,你可以添加要发布到 Teams 的文本:

  1. 完成!现在,让我们添加一个动作,当审批流程被拒绝时。点击 "如果不是",然后点击添加一个动作:

  1. 之后,搜索 Office 365 Outlook 然后选择发送电子邮件:

在 "收件人" 字段中,添加表单上的电子邮件(记住,我们是要给员工发送电子邮件,要求他们重新填写表单):

你可以更改 "主题" 和 "正文" 使其包含你想要的内容:

你也可以通过将“Is HTML”选项设置为“是”来更改配置,使该电子邮件能够处理 HTML 格式。

现在,我们已经有了最终的工作流,如下图所示。要保存它,请在顶部栏中为此工作流命名,并点击保存如下所示:

现在,让我们试一下:

  1. 打开我们以预览模式创建的表单,填写内容并点击提交:

提交后,这是你将获得的响应:

  1. 访问人力资源帐户(你选择负责审批表单的帐户)以检查是否已收到审批电子邮件:

如果你点击批准,工作流将继续在 Dynamics 365 Business Central 中创建该员工,并将表单中的所有数据添加进去,同时会发布到 Teams 上:

  1. 如果你可以访问你的 Dynamics 365 Business Central 租户,你将能够看到已创建的员工记录:

  1. 然后,如果你访问你的 Teams 渠道,你将能够看到审批通知已经发布,如下所示:

  1. 如果你点击拒绝,系统将向员工发送电子邮件,要求他们重新填写表单:

这是发送给员工的电子邮件:

这样,我们就有了一个完全自动化的员工入职流程工作流,它可以与招聘流程连接。它可以在 ERP 系统中注册新员工,通知 IT 经理等。

创建 Dynamics 365 Business Central 中表格项的相同方法也可以用于销售订单信息、采购、税务和其他你在 Dynamics 365 Business Central 中拥有的数据类型。

在这一节中,你已经看到通过使用 Flow 与 Dynamics 365 Business Central,创建自定义工作流是多么简单。我们在没有编写一行代码的情况下成功完成了这个场景。

在接下来的章节中,我们将学习如何使用 Flow 在 Dynamics 365 Business Central ERP 中实现销售订单审批工作流。

场景 2 – 创建一个简单的销售订单审批工作流

在这个场景中,我们将在 Dynamics 365 Business Central 中使用 Microsoft 提供的模板创建一个简单的销售订单审批工作流来处理我们的销售订单审批。

使用的技术: Flow 和 Dynamics 365 Business Central。

要开始创建自定义审批工作流,你可以直接从 ERP 端开始,方法如下:

  1. 在 Dynamics 365 Business Central 上,进入销售菜单并点击销售订单。现在,点击新建,在新订单页面上点击请求审批,然后点击创建工作流:

  1. 会向你展示流程模板,你可以选择最适合你流程的模板。我们选择名为“请求审批”的工作流模板,用于 Dynamics 365 Business Central 销售订单:

  1. 你可以更改工作流,使其完全符合你的流程需求,包括与销售订单相关的信息、审批流程中的审批人,以及工作流将采取的路径(取决于回复):

  1. 在这里,我们可以选择请求的详细信息(请参见以下截图中的字段):

  1. 然后,我们可以选择行动(如果批准)并保存工作流:

  1. 现在,回到销售订单界面。我们选择一个现有的销售订单,选择请求审批,然后点击发送审批请求(Dynamics 365 Business Central 会检查是否已经配置了工作流,并在用户点击发送审批请求时使用它):

  1. 工作流已经启动,审批人应该会收到通知以批准/拒绝该销售订单:

最终结果是,我们通过几个简单的步骤创建了一个工作流,允许通过电子邮件直接审批 Dynamics 365 Business Central 文档(如销售订单)。如前所述,我们可以通过几次点击更改整个审批流程的行为,并使这个工作流按照最适合我们业务需求的方式运作。

这是创建销售订单工作流的一种简单方法,也可以轻松地应用于 Dynamics 365 Business Central 上的其他数据类型。

场景 3 – 创建一个简单的应用来列出所有客户和销售报价

在这个场景中,我们将在 PowerApps 上创建一个应用,它将连接到我们的 Dynamics 365 Business Central 环境,列出所有客户,并为选定的客户列出他们的销售报价。这个应用可以在浏览器或移动设备上使用。

使用的技术:PowerApps 和 Dynamics 365 Business Central。

要开始创建我们的应用,执行以下步骤:

  1. 使用你的 Office 365 账户打开 PowerApps Studio,访问 web.powerapps.com。在 PowerApps 中有不同的应用构建方式;我们将使用Canvas应用从空白模板开始构建一个适用于手机布局的应用。点击从空白开始的 Canvas 应用:

  1. 给你的应用起个名字,选择你想要的格式(在此示例中,我们将使用“电话格式”),然后点击“创建”:

一旦应用创建完成,你将被重定向到 PowerApps Studio,在这里我们将开始构建应用的组件。

我们需要通过使用现有连接器在 PowerApps 中将我们的应用与 Dynamics 365 Business Central 连接。这里的连接器概念与 Flow 中的相同。Microsoft 在所有 Power Platform 应用中都使用相同的概念。

  1. 在顶部导航菜单中,选择“视图”,然后点击“数据源”:

  1. 现在,添加数据源。如果你已经在任何其他 Power Platform 应用中与 Dynamics 365 Business Central(或其他平台)创建了连接,你可以直接从出现的列表中选择该连接。

  2. 如果你之前没有在任何其他 Power Platform 应用中与 Dynamics 365 Business Central(或其他平台)创建连接,点击+ 新建连接,搜索Business Central,然后选择 Business Central 连接器,如下所示:

  1. 现在,点击“创建”。通过创建连接(或选择现有连接)与 Dynamics 365 Business Central 连接,你需要选择要使用的数据集:

  1. 现在,我们可以选择要在应用中使用的 Dynamics 365 Business Central 中的哪个表格。在我们的示例中,我们将选择客户表和销售报价表,然后点击“连接”:

连接已完成。现在,我们可以开始向应用中添加控件,以使用这些连接。

  1. 在顶部导航菜单中,进入“插入”选项卡,在“控件”中选择“下拉控件”:

  1. 默认情况下,这个下拉菜单的数据源将是一个示例,但我们可以通过进入右侧面板,进入“项目”属性,然后选择来自 Dynamics 365 Business Central 的客户表来更改它:

  1. 在“值”属性中,我们可以选择从客户表中显示给最终用户的值;我们选择displayName

  1. 此时,我们将添加一个新控件,以显示所有针对选定客户的销售订单,这样每次我们在下拉菜单中更改客户时,它都会加载该客户的所有销售订单。在“插入”选项卡中,点击“画廊”并选择“垂直画廊”:

选择画廊后,你需要为该画廊选择数据源。这与我们之前为下拉菜单介绍的概念几乎相同。

  1. Items属性中,选择salesOrder数据源:

  1. 现在,我们可以选择画廊将使用的布局以及显示在哪些字段。在Layout属性中,选择名为“标题、副标题和正文”的模板:

  1. 在数据部分,选择number作为Title2属性。在Subtitle2属性中,选择status

同样,在 Body1 属性中,选择totalAmountExcludingTax

现在,我们需要确保应用筛选器,以便只显示从下拉框中选择的客户的销售订单。默认情况下,当我们选择数据源时,数据将被检索而不应用任何筛选。

要做到这一点,请参考以下截图:

点击画廊。在控制属性面板中,选择Items属性。在函数条中,我们需要使用名为Filter()的命令来筛选数据源中的数据。

你可以在docs.microsoft.com/en-us/powerapps/maker/canvas-apps/functions/function-filter-lookup找到更详细的信息。

让我们使用以下Filter公式:

Filter(salesOrders, Text(customerId) = Text(Dropdown1.Selected.id))

过滤器公式如下所示:

让我们看看这个公式到底是怎么回事。

Filter函数要求数据源作为第一个参数,在这种情况下,数据源将是 Dynamics 365 Business Central 中的salesOrder表。

Filter函数的第二个参数是逻辑测试。在这种情况下,我们将检查customerId是否等于从salesOrder表中选择的Dropdown1中的item,即我们在 Dynamics 365 Business Central 的客户表中选择的项目。

在这种情况下,我们使用Text()函数将两个参数转换为文本,以便进行比较。

现在,每当用户更改下拉框时,筛选器将会被应用,销售订单将按下拉框中选择的值进行筛选。

现在,我们只需要保存应用程序。为此,请按照以下步骤操作:

  1. 在顶部导航菜单中,进入文件,输入你的应用程序名称,改变图标(如果需要),然后点击保存:

  1. 现在,返回到应用程序并运行我们的应用程序。要启动应用程序,请点击右上角的播放图标:

  1. 从下拉框中选择其他客户,以检查筛选器是否已应用:

这样,我们就有了一个与 Dynamics 365 Business Central 集成的应用程序,可以根据我们业务的其他流程和需求进行更改。

使用 PowerApps,我们迅速实现了一个可以在浏览器和移动设备上运行的应用程序,能够与整个组织共享,并且可以由我们的 IT 管理员进行安全管理。

摘要

在本章中,我们概览了 Microsoft Power Platform,特别关注了两个重要的应用程序:Microsoft Flow 和 Microsoft PowerApps。

我们看到了通过将 Microsoft Dynamics 365 Business Central 与 FlowPowerApps 集成,实现三个真实世界业务场景的应用,且这一切都在零编码的情况下完成。这就是这个商业平台的强大之处!

我们还学习了如何利用整个 Dynamics 365 平台,扩展我们的 ERP 解决方案并创建现代化的业务应用程序。

在下一章中,我们将探讨另一个有趣的集成场景:如何将 Dynamics 365 Business Central 与机器学习结合使用。

第五部分:将解决方案迁移到新扩展模型

本节将介绍将机器学习功能集成到 Dynamics 365 Business Central 解决方案中的方法,如何将现有的 ISV 解决方案(使用旧的 C/AL 代码编写)迁移到新扩展模型的过程,涉及的架构方面,以及如何正确开始这一过程的建议。我们还将介绍一套著名的 AL 开发者第三方工具集,这些工具对开发 Dynamics 365 Business Central 应用非常有用。

本节包含以下章节:

  • 第十六章,将机器学习集成到 Dynamics 365 Business Central 中

  • 第十七章,将现有的 ISV 解决方案迁移到新扩展模型

  • 第十八章,AL 开发者的实用工具与高效工具

第十六章:将机器学习集成到 Dynamics 365 Business Central 中

在上一章中,我们概述了 Microsoft Power Platform,并且我们看到了如何结合 Flow 和 PowerApps 使用 Dynamics 365 Business Central 来解决无需编码的业务任务。

在本章中,我们将讨论一个近年来逐渐兴起的话题:机器学习ML)与 Dynamics 365 Business Central。2019 年是人工智能AI)的年份。你到处都能听到关于人工智能的讨论。世界告诉我们:如果你想保持领先,应用人工智能。但是,什么是人工智能?它与经典编程有何不同?幕后发生了什么?

本章的目标不是让你成为一名真正的数据科学家或机器学习大师,而是让你清晰理解人工智能的基础知识,并获得如何将人工智能嵌入 Dynamics 365 Business Central 项目中的一些经验。

本章将涉及以下主题:

  • 机器学习是什么以及其主要过程概述

  • Dynamics 365 Business Central 机器学习框架概述

  • 在你的 Dynamics 365 Business Central 应用程序中使用机器学习

  • 了解预测 API

什么是人工智能(AI)和机器学习(ML)?

人工智能包含了人类智慧特征的任务,例如语言和语音理解、物体和声音识别以及规划。之所以使用任务这个词,是因为从技术上讲,你可以通过两种不同的方法来完成这些任务:经典编程和机器学习。

在 1990 年初,一些公司推出了光学字符识别OCR)软件。他们投资了数百万美元,雇用了数百名开发人员编写代码以识别手写文本。这是使用简单工具(如if-else)的经典编程方法。该方法有效,但结果相当糟糕。准确率低,错误率高。

为什么这种方法失败了?因为对人类大脑来说自然的事情对于编程来说非常困难。

那么,我们如何以另一种方式解决这个任务呢?答案是机器学习!

"机器学习是机器(计算机)展示未被明确编程的行为的过程"

– 阿瑟·塞缪尔,1959 年

或者换句话说:计算机从数据中学习以执行预测分析。

正如你所看到的,创建机器学习功能时,我们需要的不是编写代码,而是数据。

所以,让我们在接下来的章节中详细讲解这个过程。

机器学习过程概述

根据其定义,机器学习(ML)是人工智能(AI)的一种应用,它赋予系统从经验中自动学习和改进的能力,而不是通过显式编程来实现。

这是经典的机器学习过程。当你尝试根据先前的经验预测答案时:

让我们详细了解前面的图示:

  1. 定义问题:机器学习不是一个魔法盒子。要获得答案,你应该知道问题,并构建一个能回答这个确切问题的模型。

记住:正确的问题是获得正确答案的关键!

  1. 查找数据:你需要找到能回答你问题的数据。数据在机器学习术语中通常被称为 数据集,它应当是相关的、完整的、准确的并且足够充分。在本章节中,我们将使用 Dynamics 365 Business Central 作为与业务相关的数据存储。

  2. 准备数据:为了使机器学习训练过程可行,你应该将所有表格合并成一个表格(数据集)。你需要定义你希望预测的字段(标签)以及哪些字段会影响预测(特征)。

  3. 训练机器学习模型:在这个步骤中,你通过将训练数据集应用到机器学习算法中来创建机器学习模型。

  4. 测试训练好的机器学习模型:为了了解预测质量并计算机器学习模型的准确度,你还需要一个 测试数据集。测试数据集应该与训练数据集不同,但具有相同的结构。

例如,以下是我们的训练数据集:

日期 销售金额
01.11.17 100
18.11.17 150
08.12.17 250
17.12.17 260

这是我们用来创建 F(day) = "销售金额" 机器学习模型的内容,以下是我们的测试数据集:

日期 销售金额
05.11.17 140
15.12.17 240

我们从测试数据集中提取特征,并将它们应用到训练好的机器学习模型中,以预测标签:

日期 销售金额 预测销售金额
05.11.17 140 137
15.12.17 240 245

然后,我们将销售金额与预测销售金额进行比较,得出模型的准确性:

日期 销售金额 预测销售金额 差异,%
05.11.17 140 137 2.1%
15.12.17 240 245 2.1%

因此,我们的模型准确度为 97.9%。

  1. 将机器学习模型发布为 web 服务:当你对模型的准确度感到满意时,可以将模型发布为 web 服务,并可以从任何地方使用它来预测未来的新增特征。如果我调用发布的模型来预测未来日期,结果会显示未来日期的预测值,如下表所示:
日期 预测销售金额
06.11.18 115
16.12.18 255

使用经过良好训练的机器学习模型,你可以轻松地使用数据进行预测。

接下来,让我们看看 Business Central 机器学习框架是如何工作的。

理解 Business Central 机器学习框架

从头开始构建自己的(自定义)机器学习模型可能会比较复杂。通常需要在 Python、R 或者像 Azure ML Studio 这样的服务中具有一定的经验。如果你不想在这方面投资,但仍然希望利用 AI 和你的数据,你可以使用 Dynamics 365 Business Central 机器学习框架

从技术上讲,它可以分为四个不同的框架:

  • 时间序列 API

  • 机器学习预测 API

  • 自定义 Azure ML API

  • 自定义视觉 API

每种框架都旨在完成各自的任务,并使用不同的算法:

  • 例如,通过 时间序列 API,你可以使用回归算法的强大功能,仅凭过去的日期和数字来预测数字(如销售额和数量)。

  • 使用 ML 预测 API,你可以预测类别,例如是/否或颜色。

  • 自定义 Azure ML API 允许你连接到你在 Azure ML Studio 中构建的自定义模型。

  • 自定义视觉 API 允许你连接到你在 自定义视觉 中构建的自定义模型。

在接下来的部分,我们将重点关注 时间序列ML 预测 API,因为它们是最简单的,不需要 ML 经验,但依然强大。

时间序列 API

在这个例子中,假设你是一个餐厅的老板,你想预测客户在接下来 7 天内将从菜单中点多少个菜品。

你有以下销售历史记录:

在这个数据集中,你有 38,325 行和 11 列。销售历史记录从 2015 年 1 月 10 日到 2018 年 12 月 9 日。数据集可以在 dkatsonpublicdatasource.blob.core.windows.net/machinelearning/AML-restaurant-sales-by-menu-item.csv 找到。

你可以使用 AL 语言代码调用 时间序列 API,以获得关于 订单 的预测。然后,我们可以在显示给最终用户之前,程序化地检查预测的质量。让我们看看如何操作。

步骤 1 – 下载数据集到 Dynamics 365 Business Central

执行以下步骤将数据集下载到 Business Central 中:

  1. 在 Visual Studio Code 中创建一个新的 AL 项目,并克隆这个 GitHub 仓库:github.com/dkatson/BC-ML-Framework。你将获得一个新的 RestSalesEntry 表(以及一个页面),在其中保存该数据集,并获得一个新的代码单元 RefreshRestSales,它将从外部源上传该数据集:

  1. 当你发布这个应用程序时,你将看到这个页面:

  1. 点击“刷新餐厅销售”按钮,你将看到该数据集出现在 Dynamics 365 Business Central 中:

现在,你已经有了数据,但要进行预测,你需要将一个 ML 模型发布为 web 服务,通过该服务你可以发送数据并返回预测结果。所以,为此我们将进入第二步。

步骤 2 – 从公共模板发布模型为 web 服务

只需几次点击,你就可以从公共模板创建一个模型,并发布一个仅满足你需求的端点。请在你喜欢的浏览器中输入以下网址:

gallery.cortanaintelligence.com/Experiment/Forecasting-Model-for-Microsoft-Dynamics-365-for-Financials-1

这是一个由 Microsoft ERP 团队准备的公开可用模型,旨在进行时间序列预测:

现在,按照以下步骤操作:

  1. 点击在 Studio 中打开按钮。

  2. 选择免费工作区标准工作区

  3. 登录到你的 Microsoft 账户。

  4. 点击确定按钮将实验从画廊复制。

  5. 保持区域工作区字段的默认值,除非你是 Azure ML 的高级用户。

  6. 点击实验画布底部的运行按钮。

  7. 点击实验画布底部的部署 Web 服务 | 部署 Web 服务(经典版)按钮。

系统将 Azure ML 实验部署为 Web 服务,并提供可以被广泛设备和平台(包括 Dynamics 365 Business Central)消费的 RESTful API:

部署完成后,将打开 Web 服务仪表盘。

在这里,你可以看到 API 密钥和两个可用的 API:请求/响应和批量执行。当前与 Dynamics 365 Business Central 一起发布的时间序列 API 仅支持请求/响应 API。在 API 帮助页面底部,你可以找到输入输出定义和代码示例。然而,对于这个示例,你只需要请求 URI。

现在,按照以下步骤操作:

  1. 复制并粘贴 API 密钥到文本文件中保存,尽管你以后也可以再次访问它。

  2. 点击请求/响应链接打开 API 帮助页面:

  1. 之后,复制并粘贴请求 URI 到文本文件中保存,尽管你以后也可以访问它:

就这样。这个 ML 模型可以预测任何数据,包括我们示例中的订单,现在已发布,你可以通过 Dynamics 365 Business Central 时间序列 API 调用它。

第 3 步 – 将数据从 Business Central 发送到 ML 端点以获取预测

下一步是将数据发送到端点以接收预测。让我们看看这是如何发生的:

  1. 在 Visual Studio Code 中打开你的 AL 项目,该项目是从 github.com/dkatson/BC-ML-Framework 克隆的,然后切换到Time-Series-API分支。在这里,按Ctrl + Shift + P 并输入checkout

  1. 现在,选择一个分支:

这是餐厅示例扩展的升级版。

这个版本与之前的版本的区别如下:

  • 安装应用时,演示数据(演示项和销售历史)会自动加载。

  • 在物料卡中会出现一个新操作——Update Rest. Forecast

让我们看看它是如何工作的。

有一个新的代码单元,名为 Calculate Rest. Forecast,其主要函数是 CalculateRestForecast,并且有两个附加函数,getMLUrigetMLKey将你的 URI 和密钥(从 第 2 步 中复制)插入到这些函数中:

让我们调查一下 CalculateRestForecast 函数的变量:

从前面的截图中,我们可以理解以下内容:

  • TimeSeriesMgt 是一个时间序列库,它准备数据,将其提交给 Azure ML,并获取预测结果。

  • RestSalesEntry 是我们的历史数据集,我们将用它来准备数据,并将其发送到 Azure ML 网络服务以获取预测结果。

  • TempTimeSeriesForecast 是我们将发送到 Azure ML 网络服务以获取预测结果的准备数据。

  • TimeSeriesModel 是回归算法的名称(或组合),它将在 Azure ML 网络服务中应用于准备好的数据,以获取预测结果。

  • TempTimeSeriesBuffer 是我们从 Azure ML 网络服务获得的预测结果。

让我们调查一下 CalculateRestForecast 函数的业务逻辑。整个任务可以通过 Time Series Management 变量中的四个函数来完成。在这里,Initialize 用于建立连接:

现在,准备你将用于预测的历史数据。在我们的案例中,我们将按项预测 orders,这意味着一次调用 ML 网络服务将返回一个项的 orders 预测。因此,将销售历史数据集按单个项进行过滤是有意义的:

PrepareData 将任何表格数据转换为可提交的数据集。指定源表格和用于分组的字段。请记住,时间序列 API 需要将日期作为第二个分组字段。选择一个标签——你希望预测的字段。它应该包含数值——小数或整数。

在我们的案例中,我们将指定 RestSalesEntry 中的 date 字段作为日期,menu_item_id 作为分组字段,orders 作为预测字段。

此外,我们指定 Period type 等于 day(因为我们有每一天的历史交易数据),预测的 start day 将是当前的 work date,用于计算 orders 预测的历史条目数量将是我们所拥有的所有条目:

历史周期的数量告诉系统应该从过去获取多少个周期,从预测日期开始。这意味着,如果你的历史数据集存在空缺,它也将包含在计算中。

在我们的例子中,我们的历史数据截止于 2018-12-09,且我们希望从 2019-01-05 开始进行预报。我们有约 6 个月的缺失数据。这意味着PrepareData函数将用零填充这段缺失数据,因此会排除掉 6 个月的历史数据,从最开始(2015-01-10)算起:

你可以通过调整ObservationPeriod参数来避免这种情况。

一旦数据准备好,你可以在将数据发送到 Azure 机器学习服务之前读取并修改它。例如,这里我们的RestSalesEntry数据集中有一段缺失的时期。在PrepareData阶段,会生成系统默认的零(0)条目。如果我们直接发送这个数据集,机器学习服务会认为这段时期没有销售,这将极大影响未来的销售预测。为避免这种情况,我们需要从准备好的数据集中排除零条目:

Forecast函数将最终准备好的数据集发送到 Azure ML Web 服务,后者根据指定的参数计算预报并返回预报结果。让我们来调查一下输入参数:

  • ForecastingPeriods:这是你希望获得的未来预报天数/月数/年数——它对应于你在PrepareData函数中指定的特定时间段类型。

  • ConfidenceLevel:这是预报结果的最低概率。如果指定为 0,则会使用 80%的概率,或者你可以指定一个确切的百分比。你可以尝试不同的值,看看它如何变化。我们不建议使用高于 95 的值。

  • TimeSeriesModel 这是 Azure ML Web 服务使用的统计算法,用于根据你发送的历史数据集来创建预报。它可以是 ARIMA、ETS 或 STL,也可以是 ETS + ARIMA、ETS + STL,或三者的组合。

在我们的例子中,我们将计算未来 7 天的orders预报,最低概率为 80%,并将应用所有统计算法计算平均结果:

GetForecast将预报结果填充到TempTimeSeriesForecast表中,之后你可以在任何地方使用这些结果:

调查结果最简单的方式是创建一个包含TempTimeSeriesForecast表的列表页面:

让我们发布这个应用并调查结果。进入米布丁项目,字段 No. 2 为 34,点击操作 | 项目 | 餐厅 | 更新餐厅预报

结果将存储在临时表中,并显示在屏幕上:

增量约为 140%,这相当大。原因在于我们是从 2019 年 4 月计算的预测,但我们过去的最后一个数据条目来自 2018 年 9 月,如下所示:

如果我们将工作日期更改为 2018 年 9 月 24 日(即我们最后一次实际预测后的第二天),并运行预测,我们会看到增量减少到 65%:

但增量百分比仍然相当大。为什么?那是因为预测中使用的特征过少。

时间序列预测仅使用两个特征,item no.date。但通常需要使用更多影响预测的特征。

在了解时间序列 API 框架如何工作的基础上,接下来让我们探索其预测 API。

理解 ML 预测 API

在上一节中,我们基于时间序列 API 训练了一个机器学习模型。由于仅有两个特征, resulting 模型的准确率较差。使用ML 预测 API,您可以设置任意数量的特征。这种方法为您提供了更多的灵活性和实验机会,让您可以通过改变特征和生成新特征来提高模型质量。

同时,ML 预测 API 允许您直接从 AL 训练一个自定义的 ML 模型。

如果您正在构建行业解决方案,可以将 train-ml-model 功能直接添加到您的 Dynamics 365 Business Central 应用中。

该 API 可在代码单元 2003 中找到,名为 ML 预测管理。让我们来看一下它是如何工作的。

在 Visual Studio Code 中,打开您从 github.com/dkatson/BC-ML-Framework 克隆的项目,并切换到 Train-ML-Model-From-AL-API 分支。这是之前餐厅示例扩展的升级版。

这与之前的不同之处如下:

  • 我们不使用时间序列 API。

  • 我们训练了一个具有八个特征的机器学习模型,直接从 AL 中获得订单预测。

步骤 1 – 从公共模板发布一个通用预测模型为 Web 服务

作为从 Business Central 进程进行训练的前提,您仍然需要将通用预测 ML Web 服务发布到您的 Azure 订阅中。

访问 gallery.azure.ai/Experiment/Prediction-Experiment-for-Dynamics-365-Business-Central 在您喜欢的浏览器中。

这是微软 ERP 团队准备的一个公开可用的模型,专门为 ML 预测管理使用而设计:

要发布它,请按照以下步骤操作:

  1. 在 Azure ML Studio 中打开此实验。

  2. 运行它,然后将其作为 Web 服务部署。

  3. 在 Web 服务仪表板中,复制 API 密钥。

步骤 2 – 从 AL 训练机器学习模型

在你之前克隆的 Visual Studio Code 项目中,有一张名为 Rest. ML Forecast Setup 的表格,包含两个额外的函数:getMLUrigetMLKey.

将你的 URI 和密钥(从前一步复制)插入这些函数:

打开 Train Rest. Forecast ML 代码单元,找到 Train() 函数. 我们来看看它是如何工作的。定义的变量如下:

从上面的截图中,我们观察到以下内容:

  • ML Prediction Management:这是主要的代码单元,包含用于训练、保存和使用机器学习模型的功能。

  • MyModel:这是训练后的模型,采用编码文本格式。

  • MyModelQuality:这是训练模型的质量。

  • Setup:这是存储训练模型的表格。

  • RestSalesEntry:这是用于训练机器学习模型的历史数据。

如下所示,指定与你发布的预测机器学习 web 服务的连接:

现在,为训练过程准备历史数据。你可以过滤数据或添加新的列。

这里需要注意的要点如下:

  • 训练数据集不应有空字段。如果有空字段,请用任何值填充它们,例如 0NA

  • 如果你有 Date 字段,请将其拆分为两个字段(至少):日和月。通常,预测依赖于这些字段,而非日期本身。此 API 不支持日期格式。

请参考以下截图:

为模型指定特征。在这里,你指定影响预测的字段:

然后,我们指定标签。在这里,你指定你要预测的字段:

接下来,我们将训练模型。在这里,你将请求发送到 web 服务,并使用历史数据进行训练。作为结果,你将获得一个 Base64 文本格式的训练模型和模型质量:

保存你的训练模型。它将在后续的预测过程中使用:

第 3 步 – 使用训练好的模型进行预测

打开 Calculate Rest. Forecast ML 代码单元,找到 Predict() 函数。让我们看看它是如何工作的:

  1. 检查是否已有训练好的模型:

  1. 指定与你发布的预测机器学习 web 服务的连接:

  1. 生成一个包含数据(特征)的临时表,用于获取 订单(预测)。该表的结构应与训练过程中使用的表相同,否则 Prediction web 服务将无法正常工作。

  2. 然后,将此表传递到 ML 网络服务输入。重要的是要理解,预测将针对传递表的每条记录(行)进行计算:

  1. 指定表中将成为特征的字段。按照训练模型时的相同顺序列出它们。否则,Prediction网络服务将无法正常工作:

  1. 指定从传递表中将成为标签的字段。使用训练模型时使用的相同字段。这里只能提及一个字段:

ML 预测 API 可以使用分类或回归算法进行预测。您无法控制。如果标签字段的类型为整数或小数,则将应用回归树算法annova。否则,它将使用分类算法。

如果使用分类算法预测值,那么还可以指定一个字段来保存预测的置信百分比。这对于回归算法不支持。

使用Predict函数预测值并传递训练好的 ML 模型:

保存预测结果:

现在您有预测数据要检查。

第 4 步 - 深入了解 ML 的工作原理

当您从 ML 网络服务获取预测时,了解模型为何给出某些结果或作出这些决策总是很有趣。由于 ML 预测 API 使用基于树的 ML 算法,我们可以看到决策树:

重要的是要理解,决策树是训练模型的产物,而不是训练模型所做出的预测的产物。

打开 Train Rest. Forecast ML 代码单元并找到DownloatPlotOfTheModel函数。让我们看看它是如何工作的:

  1. 连接到您发布的 ML 网络服务。

  2. 使用PlotModel函数获取带有决策树的.pdf文件。您将以 Base64 文本格式获得它。

  3. 然后,使用DownloadPlot函数将.pdf文件保存在本地。如果不需要保存它,只需跳过这行代码:

您将拥有您的 ML 模型的绘图。

第 5 步 - 发布和运行预测

当您从 Item 卡发布和运行预测时,您将得到此屏幕截图:

如果您要将预测结果与先前的模型进行比较,您会注意到该模型比定制的 ML 模型结果差,但比时间序列 API 结果好:

本节介绍了 ML 预测的工作原理及执行它所涉及的步骤。

摘要

在本章中,我们回顾了与 Dynamics 365 Business Central 一起使用的 ML API。我在下表中对三种 ML API 进行了对比分析:

时间序列 API ML 预测 API 自定义 Azure ML API
所需 ML 经验
ML 模型 微软 微软 定制
数据准备水平
最大特征数 2 无限 无限
训练服务 - Business Central Azure ML Studio
训练模型存储 - Business Central Azure ML Studio
ML 模型质量
ML 模型使用水平 一般 行业 公司

此比较基于本章提供的示例。使用此表格作为指南,帮助你选择在应用程序中应用 AI 的最佳方式。

如我们在这里所学,构建定制的 ML 模型是一门艺术,需要创造力、时间和一定的数学技能。

在下一章,我们将探讨架构概述,并了解将现有 ISV 解决方案迁移到扩展中的最佳实践。

第十七章:将现有的 ISV 解决方案迁移到新的扩展模型

本章我们将重点介绍现有的 ISV 解决方案(主要基于 C/AL 语言)。我们还将探讨在将这些解决方案迁移到 Dynamics 365 Business Central 及新扩展编程范式时应采纳的技巧、窍门和最佳实践。

本章我们将涵盖的主题如下:

  • 将 C/AL 解决方案迁移到基于扩展的架构的最佳实践

  • 将现有 C/AL 代码转换为 AL

  • 在 SaaS 环境中重新设计解决方案时需要检查和记住的事项

到本章结束时,你将更好地理解将现有 C/AL 解决方案迁移到 AL 所需的步骤,转换单体 C/AL 解决方案为扩展时的架构选择(这将影响你的最终应用程序以及如何销售它),以及在迁移过程中可以帮助你的工具(代码转换工具)。

准备从 C/AL 迁移到 AL 和扩展的过渡

毋庸置疑,Dynamics NAV 是国际市场上拥有最活跃的合作伙伴和用户社区之一。如果你是想实施 Dynamics NAV 的客户,实际上很容易找到符合并量身定制的解决方案。为此,只需搜索通过合作伙伴或 独立软件供应商ISVs)多年来开发的多个附加组件。

所有这些解决方案都是使用 C/AL 语言编写的。通常,它们包含以下内容:

  • 新对象(为满足客户业务需求而创建的对象)

  • 修改过的标准对象(从标准应用基础代码中修改过的对象,以满足客户需求)

这些解决方案始终是单体解决方案(所有内容都打包成一个数据库内的单一代码库,其中一个对象可以引用解决方案中的所有其他对象)。

在 Dynamics 365 Business Central 中,CSIDE 开发环境和 C/AL 语言仅适用于版本 14.x,并且仅适用于本地部署环境。从版本 15(wave 2)开始,Microsoft 已移除这些开发工具,因此现有解决方案必须迁移到 AL 并转换为新的扩展模型。

将现有的 C/AL 基础解决方案迁移到 AL 扩展并不仅仅是一个简单的 代码转换 过程;通常,它需要重新设计并重新思考整个应用程序(这始终是 Microsoft 推荐的方法)。

在计划将现有 C/AL 解决方案迁移到 AL 扩展时,从技术角度来看,有三个主要方面需要考虑:

  • 我应该编写多少个扩展才能最好地拆分 C/AL 解决方案?

  • 如何重用我现有的 C/AL 代码?

  • 针对基于 SaaS 的解决方案,哪些是允许的,哪些是不允许的?

在接下来的章节中,我们将学习这些方面如何影响从现有解决方案到新编程模型和新平台的过渡。

规划扩展的数量

正如我们在第五章中提到的,为 Dynamics 365 Business Central 开发定制解决方案,扩展 A 不能引用扩展 B 所暴露的对象和方法。只有当扩展 A 明确声明依赖于扩展 B 时,这才是可能的。

在将现有解决方案迁移到扩展时,您有两个主要选择:

  • 创建一个单一的整体扩展

  • 创建 N 个依赖扩展

让我们更详细地探索这些概念。

单一的整体扩展是一个更简单的选择,因为开发人员不需要考虑独立模块。相反,他们只需在一个巨大的 AL 扩展项目中创建所有对象和业务逻辑。最终,将会生成一个 .app 文件,该文件执行以下操作:

  • 添加新对象

  • 扩展标准对象

  • 添加新的业务逻辑

  • 触发事件

  • 订阅由标准业务逻辑触发的事件

下图展示了这个解决方案:

在这种情况下,开发人员无需考虑依赖关系(所有代码都在一个对象中)。然而,这种解决方案的缺点是,即使是扩展的小更新(比如增加一点代码变化)也需要撤销发布并重新发布整个应用程序。这意味着我们在 Dynamics 365 Business Central 中更新扩展:撤销发布旧版本并发布新版本。

其次,这不是一个可以拆分并作为模块销售的解决方案。然而,如果你希望拥有一个模块化的解决方案,将解决方案拆分为 N 个独立的扩展是一个不错的选择。在将现有的 C/AL 解决方案迁移到 N 个独立的扩展时,通常会有以下情况:

  • 独立或独立运行的扩展(不依赖于其他模块的模块)

  • 依赖扩展(需要其他模块的依赖的模块)

该解决方案的示意图如下:

这里,扩展 A 是一个独立扩展(它只能引用基础模块中的对象)。扩展 C 依赖于扩展 A,而扩展 B 依赖于 A 和 C。

依赖关系有一些优点,例如:

  • 它们帮助我们构建更复杂的部署场景

  • 它们提高了代码和业务逻辑的可重用性(避免冗余)

  • 它们增加了维护成本

  • 它们增强了部署的灵活性

依赖关系的缺点如下:

  • 当你发布扩展时,必须先发布没有任何依赖关系的扩展(所谓的主扩展或父扩展)。例如,如果你尝试先发布带有依赖关系的扩展(子扩展),它将抛出一个错误,指出数据库中不存在对象引用。

  • 当你删除扩展时,必须先删除依赖的扩展(例如子扩展),然后再删除父扩展。

因此,我们可能会想知道哪种选择更好。

这是每个微软合作伙伴目前都在思考的问题。关于这个话题没有固定的规则,你想要实现的对象模块化目标以及你的市场营销策略,很大程度上取决于你现有的解决方案。

一个最佳实践和建议是不要跳过或避免依赖关系。将现有的 C/AL 解决方案迁移到 N 个不同的依赖扩展中是一个不错的选择,因为它保证了模块化和灵活性,但也建议不要创建过多的微型扩展。开发人员应该思考宏功能,并尝试将其隔离到可以在客户需要时安装的特色模块中。

在这个过程中需要记住的一点是,始终添加事件,以便让其他人能够钩取你现有的代码库。

只有通过引发事件(集成业务事件),你才能拥有一个能够与系统中已安装的其他扩展进行交互的解决方案,并且该解决方案也可以被第三方(其他微软合作伙伴)扩展。

在接下来的章节中,我们将学习如何加速将现有基于 C/AL 的解决方案转换为 AL 的过程(在可能的情况下!)。

将现有解决方案转换为 AL

许多实际使用 Dynamics NAV 的微软合作伙伴多年来开发了大量的自定义解决方案(附加组件、客户解决方案等)。为了准备迎接新的 Dynamics 365 Business Central 平台,这些解决方案必须迁移到 AL 代码。

如果你有现有的代码库或现有的解决方案,关于将该解决方案迁移到扩展世界,首先可以做的事情是尝试将你的 C/AL 对象转换为 AL。

请记住,转换并不总是最好的做法,它只是一个起点(你可以照原样转换所有新的对象,但你应该注意并重构你现有解决方案中修改过的标准对象,这些对象是你现有解决方案中一定会有的)。

那么,你如何将 C/AL 解决方案转换为 AL 呢?

正如我们在第七章中提到的,使用 AL 进行报表开发,Dynamics 365 Business Central 本地版和 Docker 镜像自带一个工具,可以轻松帮助将 C/AL 对象转换为 AL 对象:Txt2AL.exe

使用此工具,你可以指定一系列任何类型的 C/AL 对象,将它们导出为 TXT 格式,并自动转换为 AL 格式。

为了熟练使用此工具,你应该执行以下步骤:

  1. 使用以下命令,将你新数据库中的所有基础对象导出为 TXT 文件(这里称为MyBaseline.txt):
finsql.exe Command=ExportToNewSyntax, File=MyBaseline.txt, Database="<databasename>", ServerName=<servername> ,Filter=Type=table;ID=<tableID>

ExportToNewSyntax 命令的详细信息请参见:docs.microsoft.com/en-us/dynamics-nav/exporttonewsyntax

  1. 将你的 C/AL 解决方案导入到新创建的数据库中,编译对象,并使用前面的语法将所有新建和/或修改的对象导出到 TXT 文件中(例如,命名为MyCustomObjects.txt)。

  2. 执行Set-ObjectPropertiesFromMenuSuite cmdlet,以便从 MenuSuite 信息转换为生成的 AL 文件中的页面和报表(记住:MenuSuite 对象在 Dynamics 365 Business Central 中不可用)。

  3. 执行Compare-NAVApplicationObject cmdlet,以比较基础对象和修改后的对象,并创建包含它们之间差异的.DELTA文件:

Compare-NAVApplicationObject -OriginalPath "C:\MyBaseline.txt " -ModifiedPath "C:\ MyCustomObjects.txt " -ExportToNewSyntax
  1. 执行Txt2AL.exe工具,使用以下语法:
txt2al –source=<DELTAFilePath> --target=<ALOutputFilesPath> --rename --type --extensionStartId --injectDotNetAddIns --dotNetAddInsPackage --dotNetTypePrefix --translationFormat –addLegacyTranslationInfo

以下表格包含了所有Txt2AL.exe命令参数的描述(其中一些是可选的)。我们来看看每个参数是如何工作的:

参数名称 描述
--source=Path 包含.delta文件的文件夹路径。此参数是必填的。
--target=Path 包含生成的.AL文件的文件夹路径。此参数是必填的。
--rename 如果使用此选项,输出文件将自动重命名为.txt对象。
--type=ObjectType 要转换的对象类型。允许的值包括 Codeunit、Table、Page、Report、Query 和 XmlPort。
--extensionStartId 这个选项允许你定义生成的扩展对象的起始 ID(默认值是 70,000,000)。每生成一个对象,该 ID 会递增 1。
--injectDotNetAddIns 这个选项会在生成的.NET 包中添加标准.NET 插件的定义(这些插件是一组嵌入到平台中的插件)。
--dotNetAddInsPackage=Path 这个选项指定包含.NET 类型声明的 AL 文件的路径,这些类型声明应该包含在转换生成的.NET 包定义中。
--dotNetTypePrefix 这个选项允许你为转换过程中创建的所有.NET 类型别名定义一个前缀。
--translationFormat=ObjectType 这个选项允许你指定翻译文件的格式。允许的值包括 Xliff 和 Lcg。
--addLegacyTranslationInfo 这个选项允许你向翻译文件添加信息。在转换过程中,应用程序中所有CaptionML属性的 XLIFF 文件都会被提取。如果设置了此选项,生成的 XLIFF 文件中将添加一个注释,指定翻译项在 C/SIDE 中的 ID。这充当了一个映射,允许你将现有的翻译资源转换到你的应用程序中。

现在我们已经解释了将代码从 C/AL 转换为 AL 的工具(Txt2AL),那么我们如何以半自动化的方式将现有的 C/AL 解决方案迁移到基于扩展的架构(AL 语言)中呢?

第一部并且良好的做法是,将现有的 C/AL 解决方案迁移到支持 CSIDE 开发环境和 C/AL 语言的最后一个累积更新CU)的 Dynamics 365 Business Central 版本:Dynamics 365 Business Central 2019 年春季更新(平台 14.x)。

为了展示半自动化过程,我们将使用带有NavContainerHelper PowerShell 库的 Docker 容器,库可在github.com/microsoft/navcontainerhelper获取。

创建一个新的 Docker 容器,使用你的 C/AL 解决方案所基于的 Dynamics NAV 版本(在此示例中,我们将使用 Dynamics NAV 2018 CU 16),并将自定义或修改过的 TXT 对象导入此容器。脚本可能如下所示:

# Environment Settings
$auth = "NavUserPassword"
$credential = New-Object pscredential 'admin', (ConvertTo-SecureString -String 'P@ssword1' -AsPlainText -Force)
$licenseFile = "C:\temp\license.flf"
$demoSolutionPath = "C:\ProgramData\NavContainerHelper\MyNAVSolution.txt"
*New-NavContainer -accept_eula `
* *-imageName "mcr.microsoft.com/dynamicsnav:2018-cu16" `* *-containerName "nav2018" `* *-licenseFile $licenseFile `* *-auth $auth `* *-Credential $Credential `* *-updateHosts `* *-includeCSide* *# Import and compile objects* *Import-ObjectsToNavContainer -containerName "nav2018" -objectsFile $demoSolutionPath* *Compile-ObjectsInNavContainer -containerName "nav2018" -filter "Modified=Yes"*

现在,运行以下命令:

Export-ModifiedObjectsAsDeltas -containerName "nav2018" -openFolder

这将打开一个本地文件夹(通常是C:\ProgramData\NavContainerHelper\Extensions\nav2018\delta),该文件夹包含所有对基础代码的修改(称为deltas)。特别是,运行上述命令后,你将在这个文件夹中找到两种类型的文件:

  • .TXT文件是你的新对象(你可以直接使用它们)。

  • .DELTA文件是修改后的对象。

你应该始终检查.DELTA文件,因为它们可能包含一些在 AL 中不再支持的自定义代码修改。一个不支持的自定义示例可能是之前直接插入到标准表触发器中的代码,或者写在标准过账程序中的代码。这些代码必须移动并封装在由 Dynamics 365 Business Central 平台本地触发的事件订阅者中(正如我们在第五章中解释的,为 Dynamics 365 Business Central 开发定制解决方案)。

在进行这个(强制性)代码重构之后,我们需要创建一个 Dynamics 365 Business Central 容器(在这里称为d365bc),并且我们需要导入对象的增量。

脚本如下:

*# Environment Settings* *$imageName = "mcr.microsoft.com/businesscentral/onprem:ltsc2019"**$auth = "NavUserPassword"* *$credential = New-Object pscredential 'admin', (ConvertTo-SecureString -String 'P@ssword1' -AsPlainText -Force)* *$licenseFile = "C:\temp\license.flf"* *# Create Dynamics 365 Business Central container* *New-NavContainer -accept_eula `
* *-imageName $imageName `* *-containerName "d365bc" `* *-licenseFile "C:\temp\license.flf" `* *-auth $auth `* *-Credential $Credential `* *-updateHosts `* *-includeCSide**# Import and compile Delta files* *Import-DeltasToNavContainer -containerName "d365bc" -deltaFolder "C:\ProgramData\NavContainerHelper\Extensions\nav2018\delta"* *Compile-ObjectsInNavContainer -containerName "d365bc" -filter "Modified=Yes"*

现在,你有了一个包含 C/AL 解决方案的容器,在其中你可以测试你的代码,并且(最终)反复重构它。之前创建的 NAV 容器现在可以通过执行以下命令从系统中移除:

Remove-NavContainer -containerName nav2018

现在,我们准备好开始处理我们的基于 AL 的新解决方案。

在为 Dynamics 365 Business Central 开发(或迁移现有解决方案)应用程序时,我们现在有两种可能的情况:

  • C/AL 到 AL 的转换(不需要对标准基础对象进行修改,以支持产品的 SaaS 版本)

  • C/AL 到 AL 的代码自定义(基础 AL 对象将被更改;这仅适用于本地环境)

在接下来的章节中,我们将学习如何在这两种 C/AL 到 AL 的情况下进行操作。

C/AL 到 AL 转换

在这里,我们需要创建一个包含我们 AL 解决方案的 Dynamics 365 Business Central 开发容器。这个容器不再支持 C/AL。创建这个容器的脚本如下:

*# Environment Settings* *$imageName = "mcr.microsoft.com/businesscentral/onprem-ltsc2019"* *$auth = "NavUserPassword"* *$credential = New-Object pscredential 'admin', (ConvertTo-SecureString -String 'P@ssword1' -AsPlainText -Force)* *$licenseFile = "C:\temp\license.flf"
**# Create Business Central container* *New-NavContainer -accept_eula `
* *-imageName $imageName `* *-containerName "d365bcdev" `* *-licenseFile $licenseFile `* *-auth $auth `* *-Credential $Credential `* *-updateHosts*

现在,你已经有了一个可用的 Dynamics 365 Business Central 容器,不再包含我们将在接下来的步骤中使用的 C/AL 工具。

现在,打开 Visual Studio Code,创建一个新的 AL 项目(CTRL + Shift + P,然后选择AL:GO!),给它命名(这里命名为MyALSolution),并修改你的解决方案中的launch.json文件,以便连接到这个容器(这里容器名为d365bcdev)。

在 PowerShell 中执行以下命令:

Convert-ModifiedObjectsToAl -containerName "d365bc" -sqlCredential $credential -alProjectFolder "C:\Packt\MyALSolution"

NavContainerHelper模块有一个名为Convert-ModifiedObjectsToAl的功能,允许你从选定的容器中导出所有修改过的对象(如果需要,你也可以应用对象过滤器),然后对结果文件运行Convert-Txt2Al命令。执行该命令后,你将得到一个文件夹(由-alProjectFolder参数指定),其中包含许多在从 C/AL 基础解决方案转换过程中生成的.al文件。

输出结果不会始终达到 100%的完美;你需要做一些重构,并且需要为你的对象添加ApplicationAreaUsageCategory属性,但主要工作已经完成。现在,你可以编译你的 AL 解决方案并将其部署到你的d365bcdev容器中。因此,你的解决方案是一个 100%的 AL 扩展,运行在 Dynamics 365 Business Central 上。

C/AL 到 AL 代码自定义

在将 C/AL 解决方案转换为 AL 时,你可能还会遇到一些必须严格修改标准 AL 代码的情况(我们建议尽量避免这样做,因为如果修改了微软的基础代码,你的解决方案将无法迁移到 Dynamics 365 Business Central 的 SaaS 环境中)。让我们开始吧:

  1. 如果你遇到这种情况,可以通过-includeAL选项创建一个 Dynamics 365 Business Central 开发容器(d365bcdev):
*# Environment Settings* *$imageName = "mcr.microsoft.com/businesscentral/onprem-ltsc2019"* *$auth = "NavUserPassword"* *$credential = New-Object pscredential 'admin', (ConvertTo-SecureString -String 'P@ssword1' -AsPlainText -Force)* *$licenseFile = "C:\temp\license.flf"
**# Create Business Central container* *New-NavContainer -accept_eula `
* *-imageName $imageName `* *-containerName "d365bcdev" `* *-licenseFile $licenseFile `* *-auth $auth `* *-Credential $Credential `* *-updateHosts `*                 *-includeAL*

执行该命令时,你将会在一个新的文件夹中找到包含 AL 对象基线的文件夹,文件夹名称为Original-<version>-<country>-al(例如,C:\ProgramData\NavContainerHelper\Extensions\Original-14.0.29537.0-W1-al)。

  1. 现在,你可以使用从上一步中获取的所有基础 AL 对象创建一个新的 AL 项目。你可以通过执行以下脚本自动完成这一步:
Create-AlProjectFolderFromNavContainer -containerName "d365bcdev" -alProjectFolder "C:\ProgramData\NavContainerHelper\AL\MyALSolution" -useBaseLine -addGIT

在前面的代码中,我们使用了以下两个参数:

    • -useBaseline选项用于将.AL基础文件复制到我们的 AL 解决方案项目中。

    • -addGit选项会在该文件夹创建一个离线 Git 仓库,并提交所有对象(你需要先安装 Git)。

  1. 现在,你可以使用 Visual Studio Code 打开这个文件夹,并在不发布的情况下编译解决方案(或者使用Ctrl + Shift + B快捷键)。这个编译过程可能需要几分钟。你也可以通过执行以下命令在不打开 Visual Studio Code 的情况下编译解决方案:
Compile-AppInNavContainer -containerName "d365bcdev" -credential $credential -appProjectFolder "C:\ProgramData\NavContainerHelper\AL\MyALSolution"

在编译过程中,你可能会看到一些弃用警告。编译后,你需要将这些修改提交到本地 Git 仓库。

现在你拥有一个完整的 AL 应用(包含所有标准 AL 对象)。

在下一步中,你需要将容器数据库中的 C/AL 对象替换为这个新编译的 AL 应用。为此,执行以下命令:

Publish-NewApplicationToNavContainer -containerName "d365bcdev" -appDotNetPackagesFolder "C:\ProgramData\NavContainerHelper\AL\MyALSolution\.netpackages" -appFile "C:\ProgramData\NavContainerHelper\AL\MyALSolution\output\SD_myalapp_1.0.0.0.app" -credential $credential -useCleanDatabase

Publish-NewApplicationToNavContainer 是一个 cmdlet,卸载数据库中的所有应用,移除所有 C/AL 对象,并使用容器的开发端点发布新的 .app 文件。我们使用 -useCleanDatabase 标志来移除 C/AL 对象并卸载现有的应用。

现在你已经有了运行完整 AL 基础应用的 Docker 容器,你需要导入你的 AL 自定义解决方案(扩展)。为此,执行以下命令:

Convert-ModifiedObjectsToAl -containerName "d365bc" -sqlCredential $credential -doNotUseDeltas -alProjectFolder "C:\Packt\MyALSolution" -alFilePattern "*.al,*.xlf"

这将在你之前导入自定义 C/AL 解决方案的容器上运行(这里称为d365bc)。现在,转换将作用于所有对象(完整的数据库,报表布局文件除外)。

完成此步骤后,你将拥有一个完整的基础应用,里面包含你的自定义对象和修改的标准 .AL 对象。你现在可以编译这些对象并将它们部署到你的 Dynamics 365 Business Central 容器中进行测试。你现在拥有一个代码定制的 AL 解决方案(再次建议如果可能的话避免这样做)。

这是如果你希望开始代码转换所需的步骤。通常,请记住始终以 SaaS 环境作为参考和目标点。

从 Dynamics 365 Business Central 版本 14 升级到版本 15

微软推荐的迁移路径是从已迁移到版本 14 的解决方案开始(迁移到版本 14 和 AL 是第一步),将你的解决方案迁移到新的重构版 Dynamics 365 Business Central 版本 15。

微软的官方迁移路径如下图所示:

更多信息可以参考 docs.microsoft.com/en-us/dynamics365/business-central/dev-itpro/upgrade/upgrade-overview-v15

要从版本 14 数据库开始进行技术升级到版本 15,你可以执行以下 PowerShell 命令:

Invoke-NAVApplicationDatabaseConversion -DatabaseServer <database server name>\<database server instance> -DatabaseName "<database name>"

转换将更新数据库的系统表到新的模式(数据结构),并提供最新的平台功能和性能提升。

对于迁移到版本 15,详细步骤可以参考以下官方 Microsoft 页面:

在下一节中,我们将探讨在为新的 Dynamics 365 Business Central 平台架构解决方案时需要考虑的另一个重要方面:如何处理客户定制需求。

处理客户特定的个性化需求

你已经付出了很大的努力,现在你的解决方案已经完成了从旧的 C/AL 到新扩展架构的迁移。现在,通常的商业场景是你将解决方案卖给客户:他们希望对你的解决方案做一些特定的定制,以满足他们的特定业务需求。在这里,我们立刻遇到一个问题:你如何处理客户的定制需求?

扩展模型有一些规则,你需要绝对避免以下图示所代表的情况:

在前面的图示中,我们可以看到 EXT BASE 是标准解决方案,它的基础代码为每个购买该解决方案的客户所修改。

你不需要为每个客户直接定制你的扩展代码。分叉(Forking)你的解决方案基础代码绝对是一个坏习惯(它违反了扩展的原则;也就是说,基础代码永远不应被修改)。

你需要做的事情如以下图所示:

在这里,你的扩展基础代码(在前面的图示中称为 EXT BASE)对于每个客户都是相同的。为了处理每个客户的定制需求,你需要为每个客户创建一个新的扩展(在前面的图示中称为 CUSTOM EXT),该扩展将会依赖于你的基础扩展(它将是标准层之上的新一层)。这是最佳实践,也是扩展模型的要求:你不修改基础代码,而是扩展基础代码。那么,除了这些,我们还需要记住其他哪些扩展方案呢?让我们一起来看看。

其他需要记住的事项

在将解决方案迁移到扩展时,还有其他需要记住的事项,以及你需要处理或重新考虑的方面。在接下来的章节中,你将看到一些最常见事项的总结。

处理 MenuSuite

在 Dynamics NAV 中,通过将页面和报告添加到 MenuSuite 对象(一个定义应用程序功能菜单的标准对象)中,Web 客户端可以对其进行搜索。而在 Dynamics 365 Business Central 中,MenuSuite 对象不再受支持,页面和报告可以通过设置 UsageCategoryApplicationArea 属性来进行搜索和显示。

如果您从 C/AL 转换对象,则需要在转换的对象上设置这些属性。您可以使用名为TransitionMenuSuiteObjectsForSearch.psm1的 PowerShell 模块自动设置对象上的这些属性,该模块可以在 Dynamics 365 Business Central DVD 映像中找到。

您可以在 PowerShell 中导入此模块,如下所示:

Import-Module -Name c:\dvd\WindowsPowerShellScripts\WebSearch\TransitionMenuSuiteObjectsForSearch.psm1

然后,执行以下命令:

Set-ObjectPropertiesFromMenuSuite -RoleTailoredClientFolder "C:\Program Files (x86)\Microsoft Dynamics NAV\140\RoleTailored Client" -DataBaseName "YourDatabase" -OutPutFolder "C:\temp"

现在,在所有转换的对象上设置了UsageCategoryApplicationArea

.NET 变量和插件

如果您的现有代码使用.NET 变量,则这些对象不支持在 SaaS 环境中使用。如果要在 SaaS 上使用.NET,您需要将 DLL(或您的.NET 代码)包装到 Azure 函数中,并从 AL 代码中调用该函数。第六章中描述的高级 AL 开发和第十三章中描述的Serverless Business Processes with Business Central and Azure展示了如何处理这些情况。

如果你的扩展目标是在本地环境中(在app.json文件中Target = Internal),那么你可以在 AL 代码中使用.NET 程序集(但这些代码永远不能移到 SaaS 环境中)。关于如何在本地环境中使用.NET 变量的更多信息,请参见docs.microsoft.com/zh-cn/dynamics365/business-central/dev-itpro/developer/devenv-get-started-call-dotnet-from-aldemiliani.com/2019/06/04/dynamics-365-business-central-using-dotnet-assemblies-on-a-docker-container-sandbox/

如果您的解决方案使用.NET 可视化插件,可能会遇到另一个问题,比如以下的销售订单页面:

下面是我们实施的自定义销售订单页面,它使用了在 C/AL 中声明的.NET Windows Presentation FoundationWPF)插件:

要将此解决方案移植到 Dynamics 365 Business Central,您需要将可视化插件重做为 JavaScript 插件,如第六章中描述的高级 AL 开发

文件管理

在 Dynamics 365 Business Central 中,仅支持本地环境中的文件处理。如果您的目标是 SaaS 环境,您应该通过使用(如第六章中描述的高级 AL 开发处理文件部分)或通过使用Azure Functions进行文件存储(在第十三章中描述的Serverless Business Processes with Business Central and Azure中提供了一个完整的解决方案)来处理文件。

打印

在 SaaS 环境中,无法进行直接打印(将文档直接发送到本地网络上的打印机)。一个可能的解决方案在这里描述:demiliani.com/2019/01/29/dynamics-365-business-central-and-direct-printing/

微软也在努力支持在不久的将来在 SaaS 环境中进行直接打印。wave 2 发布将包含一个名为 OnDocumentReady 的新报告事件,该事件暴露了文档的数据流和上下文。文档随后可以由能够处理打印的扩展程序拾取。

在下一节中,我们将学习 Dynamics 365 Business Central wave 2 发布架构如何在不久的将来影响你扩展的开发。

Dynamics 365 Business Central wave 2 发布变更

Dynamics 365 Business Central Wave 2 发布(平台 15)仅支持 AL 和 Web 客户端。你将不再找到对 C/AL 和 CSIDE 的支持。相反,如果你访问 Dynamics 365 Business Central 平台 15 中的 扩展管理 页面,你将看到以下两个微软扩展:

  • 基础应用程序(版本 15.0..0):此扩展包含所有已迁移到 AL 的业务逻辑。

  • 系统应用程序(版本 15.0..0):此扩展处理系统层。

除了简化整个代码库外,这种新结构的主要好处是,你可以摆脱代码定制,开始基于 Dynamics 365 Business Central 平台进行垂直或水平解决方案的开发。你可以设置一个预备环境,并通过以下两个官方文章来练习新版本中将正式引入的破坏性变更:

所有你为 Dynamics 365 Business Central 创建的新扩展必须依赖这些微软应用。在 Visual Studio Code 中,当你启动一个新的扩展项目时,你需要将以下依赖项添加到扩展的 app.json 文件中:

如果你在 Visual Studio Code 中选择 4.0 作为目标平台,这些依赖项将自动添加。

之后,你可以从你的环境中下载符号并开始编码:

这是为 SaaS 和本地环境开发扩展的推荐方式。

在 Dynamics 365 Business Central 的第二波发布中,微软允许你修改基础代码(现在,所有代码已完全转换为 AL),本节开头的链接解释了如何将.al 文件提取到本地文件夹,并开始在这些文件上工作,以创建你自己的自定义BaseApp

作为一个例子,这里我直接通过添加一个自定义函数来修改标准的Sales-Post代码单元:

更多关于如何修改Base Application的信息可以在这里找到:demiliani.com/2019/09/24/dynamics-365-business-central-wave-2-customizing-the-base-application/

正如我们之前提到的,许多社区专家的来源中提到,仅仅因为你可以,并不意味着你应该。修改基础代码实际上是被允许的,以帮助合作伙伴尽快将解决方案迁移到 AL 和新平台,但从长远来看,本地部署将遵循云规则,因此,微软的基础代码修改在未来可能会变得更加严格。

在新平台上,你还可以在System Application本身之上构建扩展。只需移除BaseApp的依赖,下载一些符号,就可以开始了。如下面的截图所示,现在,你只下载了两个应用包(没有 BaseApp):

现在,你可以开始编写仅依赖于System Application的扩展。

System Application实际上是一个正在进行的工作,未来可能会有所变化(将添加新的模块)。最新版本始终可以在 GitHub 上找到:github.com/Microsoft/alappextensions

如你在本节中看到的,从 Dynamics 365 Business Central 15.x 平台开始,所有的基础代码都已经迁移到 AL,你需要使用 AL 和 Visual Studio Code 来创建扩展,从 Microsoft 的基础和系统应用开始。

总结

在本章中,我们研究了将现有的单体 C/AL 解决方案迁移到新的扩展架构和 AL 语言的最佳实践。我们还看到了架构你的解决方案的最佳实践,比如如何以半自动化的方式将现有的 C/AL 代码转换为 AL,以及在迁移现有解决方案的过程中如何处理常见问题。

在本章的末尾,你学习了如何迁移到新的 Dynamics 365 Business Central 平台,以及在开始新解决方案的项目时应采用的最佳实践。现在,你已经清楚地了解了迁移现有代码到 AL 的工具,并知道了开始这一迁移活动所需的步骤。

在下一章,我们将学习第三方工具如何帮助我们使用 AL 和扩展,也将帮助我们将现有解决方案迁移到新的 Dynamics 365 Business Central 架构。

第十八章:对 AL 开发者有用且高效的工具

在上一章中,我们提供了将现有 ISV 解决方案迁移到基于扩展的全新 Dynamics 365 Business Central 开发模型的一些指导和最佳实践。

在使用扩展和 Visual Studio Code 时,拥有正确的工具可以节省大量的时间和精力。在本章中,我们将概述一些你可以在日常开发中使用的第三方开发工具,帮助你在 AL 开发中更加高效。我们将重点介绍微软 Dynamics ERP 世界中著名人物 Waldo 开发的工具。

本章将涵盖以下内容:

  • Waldo 是谁?

  • 使用哪些工具

Waldo 是谁?

Waldo 的真实姓名是 Eric Wauters,他是 iFacto Business SolutionsCloud Ready Software 的创始合伙人之一。凭借 18 年的技术经验,他每天都为开发团队提供灵感。作为开发经理,他持续推动 iFacto 和 CRS 的技术准备工作。

除此之外,Eric 还非常活跃于 Microsoft Dynamics 365 Business Central 社区,他在其中尝试解决技术问题,并与其他 Dynamics 爱好者分享知识。相信很多人都读过 Eric 的帖子,他通常都会署名 waldo

许多人使用并且还为他在 MiBuSo、GitHub、PowerShell Gallery 和 Visual Studio Marketplace 上免费分享的工具做出了贡献。

他凭借卓越的成绩,每年自 2007 年以来都获得了 微软最有价值专家 (MVP) 奖项。

了解了 Waldo 后,在下一部分中,我们将概述他为 AL 开发者提供的大多数工具。

使用哪些工具

多年来,Waldo 创建了许多工具。Waldo 第一个上线的工具是在 2004 年发布的 WaldoNavPad,它是一个帮助在 Microsoft Dynamics NAV 中处理大文本的工具。它帮助将代码拆分成更小的部分,因为当时我们在一个字段中只能输入最多 250 个字符。

该工具在 MiBuSo 上已被下载超过 11,000 次。由于它的受欢迎程度,Waldo 将工具更新到了一个适用于 RTC 和作为 AL 扩展的版本,并在功能上进行了扩展,使其在 Business Central 内部拥有了一个 HTML 编辑器。

紧随其后的是一些较小的工具,它们被添加到了 MiBuSo 的下载列表中,你可以在 mibuso.com/downloads/results?keywords=waldo 找到这些工具。

自 2013 年微软发布越来越多的 PowerShell 构建模块以来,Waldo 决定深入研究,帮助社区的采用。这导致了一些非常广泛的辅助功能库,这些库经过分类并发布在 PowerShell Gallery 上。只需搜索waldo (www.powershellgallery.com/packages?q=waldo),你将找到六个 PowerShell 模块:

  • Ready.Software.SQL:这些包括一些帮助你与 SQL Server(和 Business Central)一起工作的功能,例如备份和恢复。

  • Ready.Software.PowerShell:这是一个非常小的函数集合,用于一些 PowerShell 相关的挑战。

  • Ready.Software.Windows:这些包括与 Windows 相关的功能,如压缩和解压缩文件。

  • Ready.Software.NAV:这个模块包含了大多数功能,全部与 NAV(Business Central)相关:

    • 与对象的工作(升级、版本列表、语言等)

    • 与服务器的工作(例如权限和公司)

  • RemoteNAVDockerHostHelper:这是一个帮助你在DockerHost不在本地 PC 上的时候使用DockerHost的模块(因此Remote出现在RemoteNAVDockerHost中)。

  • NavContainerHelperExtension:这只是 Waldo 为了与 Docker 一起工作而创建的一组功能。类似于NavContainerHelper,这些功能在当时创建时并不属于那个模块。

所有这些功能在 Waldo 作为开发人员的生活中都有一定的目的。他使用这些模块的每个脚本都在线上 GitHub 上: github.com/waldo1001/Cloud.Ready.Software.PowerShell。你可以在这里找到所有模块和他将这些模块应用到的脚本。

许多这些 PowerShell 脚本的创建是为了帮助开发 V1 扩展。然而,当这些扩展被终止(为了更好的发展)时,一个新的工具出现了:Visual Studio Code,在这里我们可以开发我们称之为扩展 V2 的东西。在 Waldo 看来,这个工具在以下方面需要一些帮助:

  • 自动命名文件

  • 运行对象

  • 代码片段

所以,Waldo 开始为 Visual Studio Code 构建一个扩展,帮助 AL 开发人员更加高效地完成工作。CRS AL 语言扩展应运而生:marketplace.visualstudio.com/items?itemName=waldo.crs-al-language-extension

这只是对 Waldo 工具的一个简短介绍,以及他是如何构建这些工具的。以下是一些你可以找到他工具的资源:

在这一章中,我们将讨论他的一些工具,重点是让你作为 AL 开发者的工作变得更加轻松。

AL 扩展包

Waldo 曾经构建的最小工具是 Visual Studio Code | 扩展包。事实上,它是 Waldo 在日常开发任务中使用并重视的所有 Visual Studio Code 扩展的集合。

你可以在 Marketplace 中找到这个扩展包,名称为 AL 扩展包。这里是直接链接:

marketplace.visualstudio.com/items?itemName=waldo.al-extension-pack

这是主页的样子:

只需安装此扩展,它将自动安装包中的所有扩展,当 Waldo 添加新的扩展时,它也会自动安装到你的系统中。

如果你想拥有一个功能完备的 Visual Studio Code 环境,我们强烈推荐安装一个类似的包:

你可以通过以下链接找到这个工具:

marketplace.visualstudio.com/items?itemName=StefanoDemiliani.sd-extpack-d365bc

CRS AL 语言扩展

Waldo 为社区编写的一个稍大的扩展是CRS AL 语言扩展

可以通过以下链接找到此扩展:

marketplace.visualstudio.com/items?itemName=waldo.crs-al-language-extension

许多人使用它的主要原因是它管理文件名约定:开发人员不再需要担心如何命名文件——这个扩展可以自动处理这个问题。但它还做了很多其他事情。

让我们来概览一下它的功能。

运行对象

我们都知道,我们可以通过修改launch.json文件中的一些设置,在发布应用时运行表或页面,但这并不方便。

从某种意义上讲,我们需要能够做到以下几点:

  • 运行 Windows、Web、平板或手机客户端中的任何对象。

  • 直接运行一些工具,例如以下内容:

    • 测试工具

    • 事件订阅者

    • 数据库锁定页面

  • 运行当前在客户端中打开的对象。

launch.json方式运行对象在这里帮不上忙。

CRS AL 语言扩展带有这些新命令,可以在命令面板中找到:

  • CRS: 运行对象(Web 客户端)。

  • CRS: 运行对象(平板客户端)。

  • CRS: 运行对象(手机客户端)。

  • CRS: 运行对象(Windows 客户端)。

  • CRS:运行当前对象(Web 客户端)(Ctrl + Shift + R)——此命令会运行打开文件中的对象(扩展需要先发布)。你也可以从状态栏(在 Web 客户端中运行)和资源管理器的上下文菜单运行此命令。

  • CRS:在 Web 客户端中运行 CAL 测试工具。

  • CRS:在 Web 客户端中运行事件订阅页面。

  • CRS:在 Web 客户端中运行数据库锁页面。

好的一点是,它会在 launch.json 中找到设置并使用这些设置来运行实际的对象。

重命名/重新组织文件

如前所述,这是该工具最广泛使用的功能:

  • 重命名 是仅重命名文件。

  • 重新组织 是重命名文件 并且 将其放入与对象类型匹配的子文件夹中。

本质上,这些命令被归纳为四个命令,可以通过 Visual Studio Code 命令面板再次访问:

  • CRS:重命名 - 当前文件。

  • CRS:重命名 - 所有文件。

  • CRS:重新组织 - 当前文件。

  • CRS:CRS:重新组织 - 所有文件——请注意,重新组织 将会把测试代码单元移动到测试文件夹。

还有一个设置,可以在保存 .al 文件时自动启动文件重命名/重新组织:

"CRS.OnSaveAlFileAction": "Rename"

重命名/重新组织的另一个有趣功能是能够更改文件名模式:

"CRS.FileNamePattern": "<ObjectNameShort>.<ObjectTypeShort><ObjectId>.al",
 "CRS.FileNamePatternExtensions": "<ObjectNameShort>.<ObjectTypeShort><BaseId>-Ext<ObjectId>.al",
 "CRS.FileNamePatternPageCustomizations": "<ObjectNameShort>.<ObjectTypeShort><BaseId>-PageCust.al"

这是所有可用工具设置的概览:

  • CRS.nstfolder:这是 NST 的文件夹。

  • CRS.WebServerInstancePort:这是 Web 客户端的端口号。

  • CRS.WinServer:这是 Windows 客户端连接的服务器。

  • CRS.WinServerInstance:这是 Windows 客户端连接的服务器实例。

  • CRS.WinServerInstancePort:这是 Windows 客户端连接的服务器实例的端口号。

  • CRS.PublicWebBaseUrl:如果需要运行来自 VS Code 的对象,可以通过此设置覆盖 Launch.json 设置。

  • CRS.ExtensionObjectNamePattern:这是对象名称的模式;如果设置了(默认未设置),它将为扩展对象执行自动命名:

    • <Prefix>

    • <Suffix>

    • <ObjectType>

    • <ObjectTypeShort>:对象类型的简短表示

    • <ObjectTypeShortUpper>:与 ObjectTypeShort 相同,但为大写字母

    • <ObjectId>

    • <BaseName>:移除奇怪的字符——不包括前缀或后缀

    • <BaseNameShort>:不包括前缀或后缀

    • <BaseId>:如果你希望此功能正常工作,你需要在基本名称后加上 Id 注释

  • CRS.FileNamePattern:这是非扩展对象文件名的模式。可以使用以下变量:

    • <Prefix>:仅单独的前缀

    • <Suffix>:仅单独的后缀

    • <ObjectType>

    • <ObjectTypeShort>:对象类型的简短表示

    • <ObjectTypeShortUpper>:与 ObjectTypeShort 相同,但为大写字母

    • <ObjectId>

    • <ObjectName>:移除奇怪的字符,包括前缀和后缀

    • <ObjectNameShort>

  • CRS.FileNamePatternExtensions:这是扩展对象文件名的模式。可以使用以下变量:

    • <Prefix>:仅单独的前缀

    • <Suffix>: 仅后缀部分

    • <ObjectType>

    • <ObjectTypeShort>: 对象类型的简短表示

    • <ObjectTypeShortUpper>: 与ObjectTypeShort相同,但为大写字母

    • <ObjectId>

    • <ObjectName>: 移除奇怪字符,包括前缀和后缀

    • <ObjectNameShort>

    • <BaseName>: 移除奇怪字符,但不包括前缀和后缀

    • <BaseNameShort>: 不包括前缀和后缀

    • <BaseId>: 如果希望此功能生效,您需要在基本名称后面加上Id注释,如本例所示:

tableextension 50100 "Just Some Table Extension" extends Customer //18
{
    fields
    {
        // Add changes to table fields here
        field(50100;"Just Some field";Code[10]){
            TableRelation="Just Some Table"."No.";
        }
    }   
}
  • CRS.FileNamePatternPageCustomizations: 这是页面自定义文件名的模式。可以使用以下变量:

    • <Prefix>: 仅前缀部分

    • <Suffix>: 仅后缀部分

    • <ObjectType>

    • <ObjectTypeShort>: 对象类型的简短表示

    • <ObjectTypeShortUpper>: 与ObjectTypeShort相同,但为大写字母

    • <ObjectName>: 移除奇怪字符——包括前缀和后缀

    • <ObjectNameShort>: 包含前缀和后缀

    • <BaseName>: 移除奇怪字符,不包括前缀和后缀

    • <BaseNameShort>: 不包括前缀和后缀

    • <BaseId>: 与之前的说明相同

  • CRS.ObjectNamePrefix: 使用重组/重命名命令时,此设置将确保对象名称(和文件名)有前缀:

    • 提示 1:将其用作工作区设置

    • 提示 2:如果希望前缀与后缀之间有空格,请使用结束空格

  • CRS.ObjectNameSuffix: 使用重组/重命名命令时,此设置将确保对象名称(和文件名)有后缀:

    • 提示 1:将其用作工作区设置

    • 提示 2:如果希望后缀与前缀之间有空格,请使用起始空格

  • CRS.RemovePrefixFromFilename: 使用重组/重命名命令时,此设置将移除文件名中的任何前缀(但保留在对象名称中)。提示:将其用作工作区设置。

  • CRS.RemoveSuffixFromFilename: 使用重组/重命名命令时,此设置将移除文件名中的任何后缀(但保留在对象名称中)。提示:将其用作工作区设置。

  • CRS.AlSubFolderName: 这是可变子文件夹名称。“None”表示您希望禁用将文件移动到子文件夹的命令。

  • CRS.OnSaveAlFileAction: 此操作将自动重命名/重组您正在编辑的文件。它也会考虑前缀和后缀。

  • DisableDefaultAlSnippets: 禁用随Microsoft.al-language扩展提供的默认片段。更改此设置后,您需要重启 Visual Studio Code 两次——第一次是禁用激活时的片段(此时片段仍然加载),第二次是彻底不再加载片段。

  • DisableCRSSnippets: 禁用随此扩展提供的 CRS 片段。更改此设置后,您需要重启 Visual Studio Code 两次——第一次是禁用激活时的片段(此时片段仍然加载),第二次是彻底不再加载片段。

  • RenameWithGit:使用git mv重命名文件。这样可以保留文件的历史记录,但会暂存重命名,你应该单独提交它。该功能仍处于预览模式,因此默认值为false

在 Google/Microsoft 文档上搜索

一个小的新增功能,但在编码时非常方便,能够通过命令面板中的两个新命令轻松查找文档:

  • CRS:搜索 Microsoft 文档

  • CRS:搜索 Google

它将获取选中的单词,并在 Google 或 Microsoft Docs 上搜索该单词,以 Business Central 作为主要话题。

代码片段

最后但绝对不容忽视的是,多个代码片段已包含在 CRS AL 语言扩展中。

首先,Microsoft 的代码片段已经得到了改进:

  • 删除了未使用的触发器

  • 改进了制表符停靠

  • 改进了无法编译的代码

  • 删除了默认全局变量

还有一些新的代码片段实现了某些默认设计模式:

  • tmynotifications (CRS):为你自己的通知实现我的通知功能

  • tassistedsetup (CRS):为你自己的向导实现辅助设置功能

  • tcodeunit (CRS 方法):用于实现默认封装方法设计模式的代码片段,默认实现OnBeforeOnAfter事件

探索所有代码片段并熟悉它们是件好事。

反馈

如果你有反馈,或者你想为这个项目做贡献,请不要犹豫,分叉或者在 GitHub 上的 CRS AL 语言扩展的代码库中创建问题,你可以在github.com/waldo1001/crs-al-language-extension找到它。

WaldoNavPad

WaldoNavPad的目标一直是能够轻松处理无限量的文本,通过将文本轻松分割成多个片段,以便将其保存在 Business Central(或 NAV)数据库中(不仅仅是将其作为 BLOB 保存,而是以文本形式保存),这样你仍然可以过滤和搜索文本的部分内容。

在此基础上,文本应该智能地分割,保留段落、换行符和完整的单词。这仍然尽可能地保留文本格式,使其在 NAV 所能使用的小字段长度中尽可能可读。

如何使其正常工作

WaldoNavPad的最新版本可以在 Waldo 的 GitHub 上找到:

github.com/waldo1001/Waldo.NAV.Pad

它可以轻松地从那里下载、分叉或克隆。

如果你这么做,你将拥有一个实现了WaldoNavPad的应用程序的 AL 代码:

这组文件旨在允许你复制、重新编号、重命名,并对自己的项目进行更多操作。它并不打算作为一个应用程序创建,也没有上传到 AppSource,因此你可以将它作为你自己应用程序的依赖项使用。

第一次运行应用程序

一旦进入 Visual Studio Code,应用程序按原样构建并发布。你只需创建 launch.json,下载符号,并立即构建应用程序。这将带你到客户列表页面,其中有以下两个新操作:

  • 打开 WaldoNAVPad 文本:一个常规页面,包含一个多行文本框来处理大文本

  • 打开 WaldoNAVPad HTML:一个基于 JavaScript 的 HTML 编辑器(基于 TinyMCE)

操作显示如下:

这是常规页面上的文本编辑器:

HTML 文本编辑器如下:

接下来,让我们看看文本的背景是如何显示的。

背景

当你深入查看代码时,你会看到应用程序由两部分组成:

  • NAVpad 处理:此子文件夹包含处理 NAVpad 的代码。其思路是你只使用 WaldoNAVPad 类代码单元,在其中你可以展示/保存/获取来自 NAVpad 的文本。为了保存文本,它将使用文本处理功能。

  • 文本处理:此子文件夹包含用于智能处理需要保存到数据库或从数据库加载的文本的代码。如果你想使用 NAVPad 文本表格,则无需使用这些方法。然而,如果你想将文本保存到自己的表格中,你可以直接使用 WaldoNAVPad 文本类代码单元中的函数,这些函数允许你获取文本并循环处理它,将其保存到你自己的表格中。

通过循环处理文本,系统会在空格或回车符处切割句子,尽量保留文本的格式。

在下面的示例中,你可以看到 NAVPad 处理程序通过循环文本来保存文本:

默认情况下,系统将文本保存到两个表格中:

  • WaldoNAVPad 二进制大对象存储:这是一个包含格式化 HTML 标签的表格,用于保留用户对文本所做的所有格式化。

  • WaldoNAVPad 文本存储:这是一个表格,其中所有 HTML 标签都被去除,以便能够在 Business Central 中体面地显示。

为了显示这些表格的内容,应用程序在 _JustForTexting 文件夹中有两个页面:

  • 页面 82,150 WaldoNAVPad 二进制大对象

  • 页面 82,149 WaldoNAVPad 文本

这是运行名为 WaldoNAVPad 文本的页面时,记录如何被保存的示例:

接下来,我们将看到如何实现这些操作。

实现逻辑

为了向你展示如何在自己的业务逻辑中实现这一点,应用程序有一个 _JustForTesting 子文件夹,包含页面 22(客户列表)的页面扩展,展示了如何在页面上简单地创建一个操作,并通过调用类函数实现 WaldoNAVPad

Initialize 函数将加载与当前记录关联的文本。ShowAndSaveTexts 方法将在用户选择查看时,显示文本(在这种情况下,是在 HTML 编辑器中)。

MostUselessAppEver

Waldo 有一个并非特别重要,但相当有趣的仓库,名为 最无用的应用程序。它是一个 AL 应用程序,旨在用于演示、原型设计、试验或测试——你可以任意定义。它包含了 AL 开发中许多不同部分和主题的尝试和演示。

以下是几个示例:

  • 翻译

  • 不同的 .NET 封装

  • 一些与 Visual Studio Code 配合使用的编辑技巧

  • 包含 SQL 文件以检查应用程序的表

  • 发布 Web 服务

  • 包含用于检查 Web 服务的 HTTP 文件

  • 函数重载

  • 租户管理代码单元

你可以在 Waldo 的 GitHub 上找到 MostUselessAppEvergithub.com/waldo1001/MostUselessAppEver

你可以简单地克隆仓库并开始使用。这里没有一块有用的业务逻辑,但它会向你展示一些关于 AL 开发的技巧。

PowerShell 工具

如前所述,Waldo 已经深入使用 PowerShell 进行开发。虽然重点放在 C/AL 上,包括合并、升级等,但在 AL 上,PowerShell 的使用需求目前还不大(暂时)。

但让我们指出他脚本中一些在 AL 开发中可能有用的地方。

GitHub

Waldo 的所有 PowerShell 模块和脚本都可以在 GitHub 上找到:github.com/waldo1001/Cloud.Ready.Software.PowerShell

你将看到两个文件夹:

  • PSModules:此文件夹包含所有模块的代码。这些模块也可以在 PowerShell Gallery 上找到:www.powershellgallery.com/packages?q=waldo

  • PSScripts:此文件夹包含大多数使这些模块功能得以实现的脚本。

Docker 脚本

Waldo 在多种方式中使用 Docker。首先,他在自己的笔记本电脑上运行了一个 Windows 2016 服务器虚拟机,并在其中安装了 Docker——因此,从某种程度上说,它是一个远程 Docker 主机。接着,他转向了在自己 PC 上使用 Docker,这使得开发体验有所简化。他管理 Docker 的所有脚本都位于 PSScripts/NAVDocker 文件夹中。

在这里,你会找到创建容器的脚本,也有一些在 Docker 容器中与应用程序交互的脚本,例如:

  • CleanApp:这将从 Docker 容器中移除所有应用程序。

  • InstallApp:这将使用 PowerShell 安装一个应用程序。

  • ExportObjectsAsAL:这将把对象导出为 AL 文件。它对于报告非常方便,可以轻松导出、重命名和替换。

发布者

在另一个代码库(blog.CALAnalysis)中,Waldo 记录了从某个版本的 NAV 或 Business Central 中的所有发布者,这将生成一个列出所有事件发布者及其调用位置的文件。

以下是发布者的示例:

这些是它们被调用的地方:

很多人会参考这个内容,试图确认他们即将使用的事件是否在预期的位置被调用。

ALOps

ALOps 是 Waldo 当前正在开发的工具。该工具的目标是为 Dynamics 365 Business Central 合作伙伴提供一种尽可能简单的方式,以便在 Azure DevOps 中设置构建和发布管道。

本质上,构建和发布管道是 持续集成/持续部署CI/CD)的重要组成部分,但这离 AL 开发者的日常知识相去甚远。ALOps 的作用就是弥合这座桥梁:即使知识有限,你也可以在几分钟内设置自己的构建管道。

DevOps 扩展

ALOps 是一个可在 Azure DevOps 市场上获取的 DevOps 扩展:

对于任何开源代码库,都是免费的。

该扩展实际上是一个结构化的 PowerShell 脚本集合。更棒的是,用户无需具备任何 PowerShell 知识即可设置最复杂的构建管道,包括应用签名、编译、测试等。

步骤

ALOps 用于设置管道,它包含我们所称的步骤。今天它包含的步骤如下:

  • 编译应用

  • 签名应用

  • 验证已签名的应用

  • 发布应用

  • 测试应用

  • 从环境中清除应用

  • 在不同环境之间复制应用

  • 导入 RapidStart 包

  • 导入许可证

  • 构建 Docker 容器

  • 等待 Docker 容器构建完成

  • 移除 Docker 容器

  • 导入 用于并行开发fob

  • 导出对象(txtfob

  • 编译(C/AL)

所有这些步骤都可以在 Docker 或非 Docker 环境中运行,视你需要而定。

GitHub 上的文档

Waldo 在 GitHub 上记录 ALOps,代码库也用于收集问题或其他反馈。你可以在 github.com/HodorNV/ALOps 找到它。

该代码库实际上只是描述 DevOps 扩展的一组文档。

应用模板

为了让应用开发者更容易设置构建和发布管道,已有一个不断发展的代码库,里面有模板应用和构建管道,应用开发者可以直接使用这些模板导入,这样他们就可以从一套现成的文件开始,包括一个可工作的构建管道。

所有代码库都托管在这个公共 DevOps 项目中:

dev.azure.com/HodorNV/ALOps%20Templates

使用 ALOps 的示例

我们之前提到的WaldoNavPad应用实际上已经在 Azure DevOps 中设置了一个有效的构建管道,即使WaldoNavPad的仓库是在 GitHub 上。Waldo 已在这个公共仓库中进行了设置:

dev.azure.com/msdyn365bc/WaldoGitHubBuilds/

只需导航到构建页面并点击其中一个构建,就可以查看详细信息:

对于此构建的设置,你需要在仓库中打开azure-pipelines.yml文件:

github.com/waldo1001/Waldo.NAV.Pad

它为你提供了一个可读的、即刻可用的构建管道,并且它是仓库的一部分。所有构建应用所需的设置和步骤都在那里:

name: $(Build.BuildId)
variables:
- group: 'ALOps Build Pipeline Variables'
- name: 'AppVersion'
 value: '1.0.[yyyyWW].*'
- name: 'dockerimage'
 value: 'mcr.microsoft.com/businesscentral/sandbox'

pool:
 name: WaldoHetzner

steps:
- checkout: self
 clean: true

- task: ALOpsDockerStart@1
  inputs:
    docker_image: $(dockerimage)
    docker_pull: true
    docker_login: 'Insider Docker Registry'

- task: ALOpsDockerWait@1
  inputs:
     search_string: 'Ready for connections!'

- task: ALOpsLicenseImport@1
  inputs:
    usedocker: true
    license_path: $(bc.license)

- task: ALOpsAppCompiler@1
  inputs:
    usedocker: true
    nav_app_version: $(AppVersion)
    failed_on_warnings: true

- task: ALOpsAppPublish@1
  inputs:
     usedocker: true
     nav_artifact_app_filter: '*.app' 
     skip_verification: true

- task: ALOpsDockerRemove@1
  enabled: true
  condition: always()
  inputs:
     docker_login: 'Insider Docker Registry'

- task: PublishBuildArtifacts@1
  enabled: false
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'Base'
    publishLocation: 'Container'

关于如何设置的更多信息,请查看市场上的应用或之前提到的 GitHub 仓库,它们可以为你提供所有需要的设置信息。

摘要

在本章中,我们看到了一些有趣的第三方工具,可以帮助你在为 Dynamics 365 Business Central 开发扩展时提高生产力。

这是本书的最后一章。在所有这些章节中,我们涵盖了掌握每个 Dynamics 365 Business Central 实施所需的所有主题,从基础到最复杂的内容。现在轮到你了:开始开发扩展,拥抱 SaaS,并将这些主题付诸实践。

posted @ 2025-06-27 17:08  绝不原创的飞龙  阅读(0)  评论(0)    收藏  举报