在本章中,我们将介绍以下方法:
Dynamics365套件最大的优势之一是其基于web的前端。用户不需要在他们的机器上安装胖客户端就可以访问该平台;一个网络浏览器就足以访问它。大多数最终用户认为前端用户体验对他们的日常工作至关重要。Dynamics 365 Sales提供了广泛的可配置扩展,以增强前端用户体验。在上一章中,我们介绍了一些无代码扩展;然而,有时,客户的要求超出了Dynamics365提供的开箱即用功能。这就是JavaScript的用武之地。JavaScript是目前最流行的web前端脚本语言。它在行业中被广泛使用,因此,开发人员可以很容易地开始使用它。
你找到的大多数在线资源和培训材料通常只涵盖了实现扩展的最低限度。很少有人真正指导您如何使用现代行业标准来构建企业级客户端扩展。多年来,我继承了大量Dynamics CRM实现,但客户端扩展编写得很差,这使得它们很难维护和增强。
本章不仅介绍了一些利用JavaScript扩展功能的简单方法,还重点介绍了代码结构技术和调试功能,以帮助您实现可扩展的JavaScript,同时帮助您提高前端开发效率。
在第一个方法中,我们将设置我们的环境,并编写一个简单的JavaScript方法,当填充另一个字段时,使用上一章中创建的自定义毕业详细信息实体,自动生成具有当前日期的日期字段。确保实体有一个带有“主管”和“研究生开始日期”属性的表格,以便在其上应用我们的自定义。即使我们从第一章开始使用实体,自定义也可以应用于您选择的任何查找和日期字段。
除了通常访问Dynamics 365、正确的安全角色(系统自定义程序或更高版本)以及包含自定义的解决方案外,强烈建议开发人员使用成熟的集成开发环境(IDE)来开发JavaScript。您可以选择使用开箱即用的有限Dynamics 365 web资源编辑器;然而,使用诸如Visual Studio(。甚至免费的等效Visual Studio code)在编写代码时大大提高了您的生产力。这包括调试、单元测试以及将代码与源代码管理集成。
Visual Studio是市场上最成熟的IDE之一。它极大地简化了编写JavaScript代码的任务。此外,Dynamics365团队为Visual Studio实现了开发人员工具包附加组件,以帮助开发人员将Dynamics355与Visual Studio集成。该加载项可帮助您从Dynamics365检索自定义项并将其部署到Dynamics355。这种组合的唯一限制是缺乏Visual Studio内部的发布功能。
开发人员仍然需要转到Dynamics365手动发布他们的更改。有关开发人员工具包的详细信息,请参阅第4章“服务器端扩展”中的“使用Dynamics CRM开发人员工具包模板示例创建解决方案”。
另一个流行的选择是XrmToolBox。XrmToolBox是一个最初由微软MVP Tanguy Touzard发起的项目。它是一个可扩展的基于插件的Windows应用程序,用于管理、配置和自定义Dynamics365。Web资源管理器插件通过一些快捷方式帮助编写、部署和发布Web资源,以加快工作速度。开发人员很快就养成了按Ctrl+S和Ctrl+U保存、推送和发布自定义内容的习惯。此外,Web资源管理器允许将Web资源持久化到磁盘,使开发人员可以选择使用单独的编辑器(如Sublime或Visual Studio)来进行丰富的开发。
- var packtNs = packtNs || {};
- packtNs.common = packtNs.common || {};
- /** * A method that populates the post graduate start date
- * when the supervisor lookup is populated. * @returns {Void} */
- packtNs.common.populateWithTodaysDate = function()
- { if (Xrm.Page.getAttribute("packt_supervisor").getValue() !== null && Xrm.Page.getAttribute("packt_postgraduatestartdate").getValue() === null)
- {
- Xrm.Page.getAttribute("packt_postgraduatestartdate"). setValue(new Date());
- }
- }
在前三个步骤中,我们创建了JavaScript函数。请注意,我们是如何在文件开头引入名称空间的。在创建企业应用程序时,JavaScript命名空间是一种很好的做法。命名空间将确保具有相同函数名的不同库可以共存,并且您的代码调用正确的方法。此外,我们声明命名空间的方式(如下面的代码行所示)确保了如果命名空间已经在另一个文件中声明,则不会覆盖内容:
var packtNs = packtNs || {}
当您将大型JavaScript库重构为文件集合时,这种做法尤为重要。
请注意传统JsDoc注释:/***/。JsDoc是JavaScript的标准文档样式。当您开始键入/**时,Visual Studio会自动生成注释的骨架。在后续,我们将省略注释,使代码更加简洁。
在步骤5和步骤6中,我们将JavaScript库添加到表单中,以确保在加载表单时加载它。
在步骤7到步骤9中,我们手动将函数连接到毕业生开始日期字段的OnChange事件期间的触发器。
在步骤10中,我们保存并发布了更改。发布步骤对于在实例上反映您的更改非常重要。在按下“发布”按钮之前,您的更改将无法使用。
函数本身会查看硬编码字段Supervisor和Post Graduate Start Date的内容,如果Supervisor字段不为空且Post Graduated Start Date字段为空,则会将Post Graduation Start Date的值设置为今天的日期。
在后台,Dynamics365将您的web资源保存在其数据库中,并在加载表单时加载必要的资源,就好像它们是web应用程序内容的一部分一样。然后,当Supervisor字段的值更改时,它会调用populateWithTodaysDate函数。
作为第一个简单的JavaScript函数,它非常简单;但是,在构建企业应用程序时,尽量避免硬编码的值,并考虑通过使函数通用化来最大限度地重用它们。
Dynamics365客户端API是广泛而多样的。MSDN中的下图(Use the Xrm.Page object model | Microsoft Learn)突出显示高级名称空间:
有关前端脚本参考指南和客户端功能列表,请访问Form scripting quick reference | Microsoft Learn
在上一个方法中,我们编写了第一个JavaScript函数,并手动连接它。如果您正在走自定义开发的道路,请始终牢记未来的维护任务。与其通过用户界面手动连接事件,不如考虑以编程方式进行连接。这不仅将减少配置扩展的手动步骤,而且还将为您提供一个查看/维护所有事件连接的位置。
为了以编程方式连接您的事件,您需要一个要连接的候选函数(我们将使用上一个方法中的函数)、一个编写代码的IDE,当然,还需要访问具有系统自定义程序或更高安全角色的Dynamics 365实例。
- packtNs.graduateForm = packtNs.graduateForm || {};
- packtNs.graduateForm.loadEvent = function(){ Xrm.Page.getAttribute("packt_supervisor").addOnChange(populateWithTodaysDate);
- }
在步骤3中,我们扩展了JavaScript,将loadEvent函数作为手动连接表单加载事件的唯一方法。该函数以编程方式连接populateWithTodaysDate函数,以便在Supervisor字段的OnChange事件期间触发。
然后,我们在步骤6到步骤8中删除了现有的手动函数调用(如果我们之前已经连接了它),然后在步骤9和步骤10中引入loadEvent函数作为表单的OnLoad事件期间调用的唯一函数。
正如引言中所提到的,这种模式确保所有必要的设置都在一个位置完成,为您提供了一个查看所有有线事件的地方,以防您需要增强/维护扩展。此外,当您要设置大量事件时,这将简化连接。
OnChange事件并不是唯一可以连接的事件;您还可以使用addOnKeyPress事件以编程方式关联OnKeyPress事件,以便在用户向字段中键入时添加行为。表单级事件也可以使用addOnLoad和addOnSave进行连接。
每当您看到自己多次复制和粘贴代码时,请考虑将代码重构为可重用的函数。在本章的前两个示例中,我们构建了具有硬编码值的单用途函数。我们可以将代码重构为可重用函数的集合。
在这个方法中,我们将通过参数化我们的两个函数,将它们重构为通用函数。
根据前面的方法,您将需要一个IDE,并访问具有系统自定义程序或更高安全角色的Dynamics 365实例。
- packtNs.common.populateWithTodaysDate = function(attributeToMonitor, dateAttributeToChange) {
- if (Xrm.Page.getAttribute(attributeToMonitor).getValue() !== null && Xrm.Page.getAttribute(dateAttributeToChange).getValue() === null) { Xrm.Page.getAttribute(dateAttributeToChange).setValue(new Date());
- }
- }
-
- packtNs.common.wireOnChangeEvents = function(eventAttributeTuples){
- for (var i in eventAttributeTuples) {
- Xrm.Page.getAttribute(eventAttributeTuples[i].attribute) .addOnChange(eventAttributeTuples[i].function);
- }
- }
- packtNs.graduateForm.loadEvent = function(){
- packtNs.common.wireOnChangeEvents([
- {attribute: "packt_supervisor",
- function: packtNs.common.populateWithTodaysDate("packt_supervisor", "packt_postgraduatestartdate")
- }
- ]);
- }
在这个方法中,我们将现有的函数重构为更通用、更可重用的函数。
在步骤3中,我们删除了populateWithTodaysDate函数的硬编码属性名称,并将其替换为该函数的参数。
在步骤4中,我们创建了一个新函数,将参数化属性写入onChange事件并执行参数化函数。请注意函数是如何期望一个元素数组并在其中循环的。该函数还利用数组中每个对象的点表示法来检索属性名称和执行OnChange的函数。
在步骤5中,我们用对步骤4中新创建的函数的调用替换了loadEvent事件,并向它传递了一个JSON(JavaScript Object Notation)对象,该对象实际上构成了函数和要连接的每个元素的属性。
在步骤6中,我们保存并发布了更改。
结果是一组通用函数,丰富了您的公共库。只需插入需要监控和更改的属性的名称,就可以在多个解决方案中重用它们。
随着Dynamics平台多年来的发展,引入了新功能,以保持平台的现代化和最新。在旧版本中,以编程方式与DynamicsCRM通信的唯一方式是通过SOAP web服务。在后来的几年里,Microsoft Dynamics产品团队引入了OData端点,极大地简化和现代化了通信机制。
在这个方法中,我们将检索与联系人关联的活动列表,然后在任何活动打开并分配给当前登录用户时显示警告消息。
若要执行自定义,您需要访问Dynamics 365模块以及系统自定义程序或更高角色。还建议您选择IDE或JavaScript编辑器。要测试OData查询,请考虑使用Microsoft MVP Jason Lattimer编写的CRM REST Builder解决方案。另外,考虑一个REST查询工具来快速测试您的查询。Chrome的REST控制台插件是一个不错的插件。
此外,用户将需要对被查询的相关实体进行适当的访问。
- var packtNs = packtNs || {};
- packtNs.contact = packtNs.contact || {};
- packtNs.contact.checkActiveTasksAssignedToMe = function() {
- var uniqueNotificationId = "OutstandingTasks";
- Xrm.Page.ui.clearFormNotification(uniqueNotificationId);
- var contactId = Xrm.Page.data.entity.getId().replace(/[{}]/g,"");
- var currentUserId = Xrm.Page.context.getUserId().replace(/[{}]/g,"");
- var req = new XMLHttpRequest();
- var requestUrl = Xrm.Page.context.getClientUrl() + "/api/data/v9.2/tasks?$filter=_regardingobjectid_value eq " + contactId + " and _ownerid_value eq " + currentUserId + " and statecode eq 0";
- req.open("GET", requestUrl, true);
- req.setRequestHeader("OData-MaxVersion", "4.0");
- req.setRequestHeader("OData-Version", "4.0");
- req.setRequestHeader("Accept", "application/json");
- req.setRequestHeader("Prefer", "odata.include- annotations=\"OData.Community.Display.V1.FormattedValue\"");
- req.onreadystatechange = function () {
- if (this.readyState === 4) {
- req.onreadystatechange = null;
- if (this.status === 200) {
- var results = JSON.parse(this.response);
- if(results.value.length > 0){
- Xrm.Page.ui.setFormNotification("You have outstanding tasks assigned to you.", "WARNING", uniqueNotificationId) }
- }else {
- //.. handle error
- }
- }
-
- };
- req.send();
- }
上面的代码可能会导致500错误,最新的D365 CRM可以使用Xrm.WebApi发出请求 - var packtNs = packtNs || {};
- packtNs.contact = packtNs.contact || {};
- packtNs.contact.checkActiveTasksAssignedToMe = function () {
- var uniqueNotificationId = "OutstandingTasks";
- Xrm.Page.ui.clearFormNotification(uniqueNotificationId);
- var contactId = Xrm.Page.data.entity.getId().replace(/[{}]/g, "");
- var currentUserId = Xrm.Page.context.getUserId().replace(/[{}]/g, "");
- //var requestUrl = Xrm.Page.context.getClientUrl() + "/api/data/v9.2/tasks";
- Xrm.WebApi.retrieveMultipleRecords("task", "?$filter=_regardingobjectid_value eq '" + contactId + "' and _ownerid_value eq '" + currentUserId + "' and statecode eq 0").then(
- function success(results) {
- console.log(results);
- for (var i = 0; i < results.entities.length; i++) {
- var result = results.entities[i];
- // Columns
- var activityid = result["activityid"]; // Guid
- }
- },
- function (error) {
- console.log(error.message);
- }
- );
- }
为了从我们的函数调用Web API,我们首先准备了查询URL,使用XMLHttpRequest构建并执行我们的请求,然后如果结果符合特定的标准,则显示警告。最后几个步骤按照本章的第一个方法描述了手动连接事件。
在自定义JavaScript扩展的第3步中,我们构造了requestUrl参数,如下所示:
Xrm.Page.context.getClientUrl() + "/api/data/v8.2/tasks?$filter=_regardingobjectid_value eq " + contactId + " and _ownerid_value eq " + currentUserId + " and statecode eq 0";
getClientUrl()函数返回Dynamics 365实例的基本URL。然后,我们附加/api/data/v8.2,这是Web api端点的地址,如在“设置”|“自定义项”|“开发人员资源”|“实例Web api”下定义的:
当使用内部部署Dynamics 365实例运行查询时,URL构造将有所不同。强烈建议使用自己的方法重构URL构造。
请注意如何提取currentUserId和contactId作为变量,以便更好地理解流并用于调试目的。
URL看起来用于任务活动,并使用$filter关键字仅检索与当前记录相关的记录,该记录由statecode为0(Open)的当前用户所有。
此外,在自定义JavaScript扩展的第3步中,我们使用跨浏览器XMLHttpRequest(一种API,帮助在客户端和服务器之间进行通信)来调用Dynamics365 Web API端点。我们使用以下行打开了连接:
req.open("GET", requestUrl, true);
第一个参数是一个“动词”,定义我们正在执行GET请求,以使用构造的URL检索数据。末尾的布尔值确保异步执行请求。
异步调用是在客户端调用Web API端点以确保页面响应的推荐方法。
回调函数在代码后面的onreadystatechange元素中定义。
在设置请求时,我们确保请求OData v4.0,并接受JSON。
以下几行代码确保我们获得屏幕上显示的格式化值:
req.setRequestHeader("Prefer", "odata.include- annotations=\"OData.Community.Display.V1.FormattedValue\"");
最后,我们通过执行send()函数来发送请求。
在函数开始时,我们清除了与预定义的唯一通知相关联的所有通知。最后,我们检查JSON结果是否包含多个值。如果是,我们将使用唯一键和文本消息设置一个警告通知。
在步骤5到步骤9中,我们将扩展连接到联系人实体的OnLoad事件
ODataWebneneneba API不仅仅用于检索数据。事实上,微软产品团队的目标是让它与SOAPWeb服务不相上下。差异很小,每次发布都会进一步缩小差距。新的OData v4.0端点是使用Dynamics365后端构建自定义富应用程序的主要候选端点。
自2017年2月起,SOAP端点(也称为CRM 2011端点)已被弃用(Important changes coming in future releases of Microsoft Dynamics 365 | Microsoft Learn)。
如果您计划在Dynamics365外部,在单独的Web应用程序中使用Web API,请参阅第6章“增强代码”中的“使用跨来源资源共享与CRM Online”示例,说明如何设置跨来源资源分享(CORS)。
这个示例的目的是演示连接到OData端点的不同步骤。考虑到大多数代码都可以重用,它是重构到库中的一个很好的候选者。事实上,已经有一些库可以帮助您封装其中的一些逻辑,例如SDK。SDK示例中包含的REST.js库。
在上一个方法中,我们使用Web API来查询Dynamics 365实例中可用的数据。正如我们前面提到的,Web API几乎与SOAP端点不相上下。这包括查询Dynamics 365元数据;检索元数据是构建由Dynamics365配置驱动的自定义前端的关键。
在这个方法中,我们将介绍如何查询与联系人实体相关的元数据,以查看实体详细信息以及属性详细信息。
除了Dynamics 365访问权限外,您还需要对安全角色中“自定义”选项卡下的实体和字段权限进行“读取”访问。
[your organization URL]/api/data/v9.2/EntityDefinitions?$filter=SchemaName eq 'Contact'
[your organization URL]/api/data/v9.2/EntityDefinitions(608861bc-50a4-4c5f-a02c-21fe1943e2cf)?$select=LogicalName&$expand=Attributes($select=LogicalName;$filter=AttributeType eq Microsoft.Dynamics.CRM.AttributeTypeCode'Picklist')
Web API提供了一个RESTful服务,可以使用GET HTTP谓词和URL来查询该服务,以选择和过滤内容。在步骤1中,我们查询实体定义并在联系人时过滤红色。JSON结果包含与联系人实体关联的所有元数据。我们可以扩展URLt以包括更多的OData语句,例如$select关键字,以过滤返回的字段数。
第一个查询返回了一个元素列表,其中包括实体的MetadataId。然后,我们将ID输入到第二个查询中,以检索与联系人实体相关的属性。我们限制了返回到LogicalName的元素数量,如$select语句所示。$expand=Attributes语句类似于SQLjoin语句。它包括所有相关的属性。然后,我们进一步细化内容,使其仅包括每个属性的LogicalName,并过滤expand语句,使其只包括Picklist。
元数据服务引入了一种新的定制维度,它超越了基本的数据操作。它为用户界面定制以及一般处理打开了新的大门,在这些处理中,您正在处理的实体的模式在运行时之前是未知的。
除了我们在这个示例中描述的两个查询之外,您还可以查询特定属性的详细信息、关系、全局选项集等等。
有关更多详细信息,请访问以下MSDN文章Query metadata using the Web API | Microsoft Learn
Dynamics CRM 2011引入了Web资源。引入HTML和JavaScript web资源的原因之一是为了摆脱需要部署在Dynamics CRM服务器上的ASPX ISV页面。在云环境中无法执行操作。ISV页面被可嵌入表单iFrame中的自定义HTML用户界面(UI)所取代。
作为一名开发人员,您可以选择构建自己的UI库来检索内容并在自定义用户界面上显示,也可以利用行业标准框架,使用经过验证的设计模式来帮助生成干净、可维护的用户界面扩展。在这个食谱中,我们选择AngularJS作为一个流行的框架来构建一个自定义的web资源,以嵌入iFrame中。我们将重点讨论Angular版本1,因为版本2仍然很受欢迎。
此方法的自定义用户界面将利用上一个示例中讨论的元数据查询来构建一个页面,该页面显示与联系人实体相关联的选择列表的逻辑名称。
若要开发自定义UI,您需要对具有系统自定义程序或更高安全性角色的Dynamics 365实例进行常规访问。强烈建议使用IDE或您选择的现代文本编辑器来促进Angular页面的开发。
您还需要从以下位置下载最新的Angular 1,AngularJS — Superheroic JavaScript MVW Framework
在这个方法中,我们将使用Angular 1.5.0。
var app = angular.module("packtApp", []);
- app.controller('packtController', function ($scope, packtCrmService) {
- $scope.dataLoading = true;
- packtCrmService.loadAttributesFromCurrentEntity().then( function (result) {
- if (result.status == 200) {
- $scope.attributes = result.data;
- }else{
- alert("Error while loading attributes. " + result.status);
- } }).finally(function () { $scope.dataLoading = false; });
- });
- app.service('packtCrmService', function ($http, $location) {
- this.loadAttributesFromCurrentEntity = function () {
- var entityGuid = window.location.href.substring(window.location.href.indexOf("=")+1);
- var attributesUrl = window.location.origin + '/api/data/v9.2/EntityDefinitions(' + entityGuid + ')?$select=LogicalName&$expand=Attributes( $select=LogicalName;$filter=AttributeType eq Microsoft.Dynamics.CRM.AttributeTypeCode\'Picklist\')';
- var attributesRequest = { method: 'GET', url: attributesUrl, headers: { 'Prefer': 'odata.include- annotations="OData.Community.Display.V1.FormattedValue"' } }
- var attributesPromise = $http(attributesRequest).then( function (response) {
- return { "status": response.status, "data": response.data.Attributes };
- },function (response) { return { "status": response.status, "data": response.statusText}; } );return attributesPromise;
- }
- });
-
- <head>
- <script type="text/javascript" src="js/angular.min.js">script>
- <script type="text/javascript" src="js/packt.app.js">script>
- <script type="text/javascript" src="js/packt.controller.js">script>
- <script type="text/javascript" src="js/packt.service.js">script>
- <meta charset="utf-8"> <title>Attributes V1.14title> meta>
- head>
- <body>
- <div ng-app="packtApp" ng-controller="packtController">
- <div ng-if="dataLoading">Loading analysis...div>
- <ul ng-repeat="attribute in attributes" ng- if="dataLoading === false">
- <li>{{ attribute.LogicalName }}li>
- ul>
- div>
- body>
AngularJS应用程序分为五层:一个带有嵌入式指令的HTML用户界面,一个初始化Angular应用程序的应用程序JavaScript文件,一个检索数据的服务层,一个协调数据访问层和用户界面之间通信的控制器,最后是AngularJS库。
在步骤3中,我们首先添加AngularJS库,以确保浏览器的JavaScript引擎理解指令。
在步骤4中,我们定义了应用程序——一个名为packtApp的简单AngularJS应用程序。
在步骤5中,我们构建了具有一个内置功能的控制器。我们将$scope变量注入到控制器中,这将帮助我们从全局范围访问变量。我们还注入了packtCrmService,这是我们将在下一步中定义的服务层。然后将服务层的结果传递给可从HTMLView访问的$scope.attributes变量。控制器还将$scope.loading变量设置为true和false。这将在稍后的HTML绑定中用于显示和隐藏部分,因为请求是异步执行的。
在步骤6中,我们定义了服务层,该服务层调用上一个示例中的元数据查询并返回属性的LogicalName值。考虑到结果是JSON格式的,JavaScript引擎足够灵活,可以将其视为具有每个记录属性的数组。请注意,我们是如何从windows.location.href中检索值的。此属性在步骤9中定义,我们将实体GUID作为参数传递。我们可以使用Dynamics365元数据服务来查询实体的GUID,并使库完全通用,只需最少的设置。但是,为了减少客户端和服务器之间的聊天次数并提高性能,我们将其作为静态参数值传递。至于检索查询字符串值的方法,我们只是使用indexOf和子字符串函数。AngularJS通过启用HTML5模式提供了更好的替代方案。也就是说,为了简化代码,省略了这一点。
在步骤7中,我们构建了HTML文件。注意用于引用JavaScript库的相对路径,因为我们在web资源的名称中引入了一个使用斜杠(/)的层次结构。
使用分层web资源结构有助于使用web浏览器的开发工具调试代码,或使用其他编辑工具(如XrmToolBox的web资源)调试代码
在HTML文件中,我们使用dataLoading值来显示和隐藏异步调用检索数据时的相关部分。返回结果后,将显示列表
最后,在步骤9中,我们将HTMLweb资源插入联系人的表单中,并将实体元数据ID(在联系人实体的上一个示例中检索到)作为参数,以避免两次调用元数据服务。如果您在不同的实体上使用该应用程序,则必须使用上一个示例中的技术为您的实体检索正确的GUID。在查询量和提供量之间总是有一个折衷方案。这是性能和设置开销之间的平衡。
AngularJS是Dynamics365中可以使用的众多框架之一。还有许多其他框架也很适合与您的扩展集成。常见的有Knockout、React、TypeScript和jQuery。
微软最近宣布将在未来的Dynamics365版本中包含自定义控件功能。此功能将允许开发人员使用Microsoft定义的框架构建自己的控件。事实上,自定义控件在Dynamics CRM中已经存在一段时间了。它们是在DynamicsCRM2016中作为(不可自定义)丰富的移动控件引入的。
调试是开发人员在构建JavaScript扩展时需要掌握的基本技能之一。此示例涵盖了使用Microsoft Edge调试扩展所需的步骤。
为了做好准备,您需要一个调用自定义JavaScript函数的表单。您可以使用本章第一个方法中的毕业详细信息客户端自定义作为示例。确保可以从“销售”模块访问“毕业详细信息”实体。
您可以通过勾选实体配置常规选项卡中的Sales复选框来完成此操作。您还需要Edge浏览器(在Windows 10及以上版本上默认可用)。如果您更喜欢Internet Explorer,这两种浏览器的调试界面非常相似。
在步骤4中,我们搜索要调试的JavaScript。如果您正在手动查找自定义脚本,它们通常位于一个包含组织URL的文件夹下,然后是一个在花括号之间有数字的子文件夹,如以下屏幕截图所示:
在步骤5中,我们在希望调试器中断的行设置断点。
在步骤7中,我们检查了一个表达式的值。我们也可以通过单击值下的添加观察链接将其添加到观察列表中。对该表达式的任何更改都将在监视列表中突出显示。
控制台是开发人员最好的朋友。它可以通过实时测试代码来最大限度地提高开发速度,而不是每次更改脚本时都要经历更新/保存/推送/发布/刷新的循环。您还可以随时编辑JavaScript以解决问题,并在不刷新页面的情况下重新测试代码。此功能在edge中是实验性的,可能无法按预期工作。
在OnLoad事件期间执行脚本的情况下,请考虑使用Telerik的Fiddler拦截来自Dynamics 365的调用,用更新的版本替换JavaScript,直到您对结果感到满意,然后再返回常规频道。
最后,在步骤8和步骤9中,我们使用键盘快捷键跨过断点行或继续运行脚本。我们也可以使用F11进入函数,并使用Shift+F11退出函数。
此方法涵盖Edge的JavaScript调试功能的基础知识;浏览器的功能远远大于简单的调试。例如,“网络”选项卡显示有关加载每个文件所用时间的有用详细信息。以下屏幕截图显示了不同的缓存GET请求:
当对性能问题进行故障排除以确定问题的根本原因(网络、大文件或其他任何问题)时,这一点尤为重要。
性能选项卡是另一个很好的功能,它允许您记录会话的一部分,并显示浏览器性能的指标。仪表板不仅会显示网络值,还会显示脚本评估时间、DOM渲染时间等:
考虑到Dynamics365是跨浏览器兼容的,开发人员也可以使用Google Chrome调试他们的客户端代码。这个示例将涵盖与上一个类似的场景,只是调试将使用谷歌Chrome进行。
与前面的示例类似,您需要一个调用自定义JavaScript函数的表单。您可以使用本章中的第一个示例作为示例。当然,你还需要一个谷歌Chrome浏览器。
在步骤4中,我们搜索要调试的JavaScript,在步骤5中。我们将断点设置在希望调试器中断的行上。
在步骤7中,我们检查了一个表达式的值。我们也可以通过右键单击表达式并选择add to Watch将其添加到“监视”列表中。对该表达式的任何更改都将在“监视”列表中高亮显示。我们还可以将其添加到控制台中,以便在其上运行实时查询。
最后,在步骤8和步骤9中,我们使用键盘快捷键跨过断点行或继续运行脚本。我们也可以使用F11进入函数。
这个方法涵盖了Chrome的JavaScript调试功能的基础知识。浏览器的功能远远大于简单的调试。例如,“网络”选项卡显示逐文件的幕后下载时间,这对于识别性能问题非常有用。
在“网络”选项卡中,您还可以通过选择处于限制状态(高延迟、下载/上传速度慢)的配置文件来模拟慢速连接,如以下屏幕截图所示:
“时间轴”选项卡显示有关评估脚本和呈现页面的时间的进一步详细信息。如果您的页面有大量元素,并且您想了解哪些位对性能的影响最大,那么这一点尤其有用:
开发人员工具中的其他选项卡涵盖了更多功能,如DOM检查、安全性等等。
如今,自动化单元测试已经成为大多数项目的常态。单元测试不仅可以确保您的代码得到重复和彻底的测试,而且它还提供了一个安全的工具来防止未来可能破坏代码的更改。此外,单元测试通过构建松散耦合的模块来帮助您提高代码结构质量。
在这个方法中,我们将为Dynamics365JavaScript扩展编写一个VisualStudio单元测试。更具体地说,我们将对本章中构建的第一个JavaScript库进行单元测试。我们将检查条件是否正确,以及设置值是否在postgraduatestartdate属性上调用一次。假设单元测试将使用假Xrm从Visual Studio运行。页面库,我们不需要访问Dynamics 365。
与托管不同。NET代码单元测试,JavaScript单元测试需要一些准备才能启动和运行。您的设置有几个部分:您需要一个工具来将JavaScript单元测试与Visual Studio、JavaScript单元测试断言框架、Xrm.Page 库,还有一个在没有浏览器的情况下运行JavaScript的工具。您还需要Visual Studio 。
有一些选项可以将JavaScript单元测试集成到Visual Studio中。JetBrains和Chutzpah的ReSharper是最受欢迎的。考虑到ReSharper工具的丰富性,以及它与Visual Studio的无缝集成,我们将在本食谱中使用它。ReSharper不是一个免费的工具,但JetBrains确实为其产品提供了30天的试用期。ReSharper的常规版本应该足以进行此练习。
市场上有一些JavaScript断言框架。QUnit和Jasmine是与ReSharper和Chutzpah很好结合的热门歌曲之一。在这里,我们将重点关注QUnit。
有一些可用的mocking/stubbing框架,sinon.js(Sinon.JS - Standalone test fakes, spies, stubs and mocks for JavaScript. Works with any unit testing framework.)是他们中的一员。
甚至还有特定于Xrm的,例如Fake Xrm Page https://fakexrmpage.codeplex.com/
为了更好地理解我们是如何与单元测试中幕后发生的事情进行交互的,我们将构建自己的伪Xrm.Page库。
headless browser是可选的,但强烈建议您使用,尤其是在您采用连续集成/连续部署路线时。headless browser允许您在没有浏览器的情况下运行JavaScript。最流行的无头JavaScript执行工具之一是PhantomJS(PhantomJS - Scriptable Headless Browser)。PhantomJS基于Safari和Chrome中使用的WebKit引擎。
默认情况下,ReSharper在默认浏览器中执行JavaScript单元测试。要更改此行为,请导航到ReSharper | Options | JavaScript Tests | Run Tests With,并在Path to phantomjs executable选项中添加下载的phantomjs.exe文件的位置:
- var packtNs = packtNs || {};
- packtNs.common = packtNs.common || {};
- packtNs.common.populateWithTodaysDate = function() {
- if (Xrm.Page.getAttribute("packt_supervisor").getValue() !== null && Xrm.Page.getAttribute("packt_postgraduatestartdate"). getValue() === null) {
- Xrm.Page.getAttribute("packt_postgraduatestartdate"). setValue(new Date());
- }
- }
- var Xrm = {};
- Xrm.Page = {}
- var Attribute = function (attributeName) {
- this.attributeName = attributeName, this.setValueCounter = 0;
- this.getValue = function () {
- if (this.attributeName === ("packt_supervisor")) {
- return "Sample Value";
- }
- return null;
- },this.setValue = function (value) {
- this.setValueCounter += 1;
- },this.getCountFunctionCalls = function () {
- return this.setValueCounter;
- } }
- var pageAttributes = {}
- Xrm.Page.getAttribute = function (attributeName) {
- if (pageAttributes[attributeName]) {
- return pageAttributes[attributeName];
- }
- pageAttributes[attributeName] = new Attribute(attributeName);
- return pageAttributes[attributeName];
- }
- ///
- ///
- ///
- test("Xrm test populateWithTodaysDate", function () {
- // Act
- packtNs.common.populateWithTodaysDate();
- //Assert
- equal(Xrm.Page.getAttribute("packt_postgraduatestartdate"). getCountSetValueCalls(), 1, "PostGraduate attribute setValue called exactly once");
- });
在步骤1到步骤3中,我们设置了空白的ASP.NET解决方案。我们添加了JavaScript单元测试断言框架(QUnit.js)和我们正在进行单元测试的JavaScriptWebResource。请注意,如果您的web资源已经有了VisualStudio解决方案,则可以重用它;无需重新创建专门用于单元测试的新解决方案。
在步骤4中,我们创建了伪库。我们首先创建了Xrm和Xrm.Page名称空间,然后为每个getAttribute、setValue和getValue定义了存根。
getAttribute函数创建一个自定义对象,该对象具有setValue;getValue,一个新方法,getCountSetValueCalls;以及一些属性。请注意getAttribute如何将新创建的对象存储在数组中,以确保我们不会在每次调用具有相同属性名称的getAttribute函数时重新创建它。
getValue函数根据属性名称伪造结果,以模拟Dynamics365在特定场景中的实际操作。
setValue函数实际上用作mock,而不是stub。它所做的只是增加方法被调用的次数。
最后,getCountSetValueCalls函数是一个辅助函数,它返回调用setValue的次数。
在步骤5中,我们首先使用///
每个测试都从帮助ReSharper识别单元测试的功能测试开始。我们为我们的单元测试命名为Xrm测试populateWithTodaysDate,并向它传递一个回调函数,以便在populatewithTodaysDame测试下执行我们的函数。然后,我们断言setValue函数只被调用一次,并显示一个描述预期结果的文本,PostGraduate属性setValue只被调用了一次。
一旦在步骤6中执行,最终结果将如下所示:
这个方法只是触及了JavaScript单元测试的表面。它强调了可用于使其工作的不同组件,并演示了使用一些流行选项的工作组合。有很多可能的框架/库/工具组合可以利用单元测试的力量,尤其是在使用AngularJS、React或Knockout等现代JavaScript框架时。选择一个你最舒服的,试试看。此外,还可以考虑使用mocking/stubbing框架来简化设置。
减少执行操作的点击次数始终是任何客户的首要任务。实现此功能的一种方法是将按钮添加到Dynamics365功能区。在这里,我们将保持扩展的简单性,并添加一个标记为Packt的按钮来打开一个新的浏览器窗口,导航到Packt网站。
为了自定义Dynamics365功能区,您可以手动编辑XML,也可以使用功能区编辑工具。微软MVP Scott Durow在实现Ribbon Workbench方面做得非常好,以至于我很少使用其他任何东西来编辑Dynamics 365 Ribbon。为了获得Ribbon Workbench,您可以将其作为Microsoft MVP Tanguy Touzard的XrmToolBox的一部分获得,也可以从Develop 1 Ltd | Ribbon Workbench for Dynamics 365 & Dynamics CRM.编辑功能区XML需要系统自定义程序或更高级别的角色。
Ribbon Workbench是一个下载和修改负责呈现Dynamics365实体Ribbon的XML片段的工具。
我们首先在步骤5到步骤8中创建命令,在新窗口中导航到Packt网站。请注意,我们还可以调用JavaScriptFunction操作,它可以利用客户端脚本的功能。
在第9步到第11步中,我们创建了将执行命令的按钮,并给它一个特定的标签。我们在窗体功能区上添加了按钮。或者,也可以将其添加到实体功能区或子网格功能区中。
当我们在步骤12中单击“发布”时,ribbon工作台将更改上传回解决方案并发布。
通常,功能区按钮用于快速启动工作流、执行计算/转换、执行复杂的按需验证等。在将Dynamics 365与其他系统集成时,自定义功能区按钮还用于将用户重定向到外部位置。
尽管功能区编辑器是一个很好的工具,但有时可能会发生意外行为,这需要您查看底层XML以进行调试和手动编辑。如果您想了解如何手动编辑功能区,请参阅MSDN上的文章Customize commands and the ribbon | Microsoft Learn.