D365 JavaScript Web Resource Library Usage

It’s fairly straight forward to find which Forms use a Web Resource with the ‘Show Dependencies’ option, but that will list Forms where the JavaScript library has been added to the form in the Form Libraries. This doesn’t give you any indication whether the functions in the JavaScript file have actually been used in the form.

Obviously, you can scroll through each Tab and Field in the Event Handlers to look for occurrences of an event, but if you are doing maintenance on a form you didn’t originally develop then it could take a while to scroll though all the possible events. The Event Libraries and Event Handlers are stored in the formjson column of the systemform entity so with a simple FetchXML query you can get a list of Forms that actually use functions from a particular library. The example below will return all Forms where the msdyn_/Utils/head.js Web Resource has been used.The key part is the Filter Condition that is looking for a LibraryName that matches our JavaScript library. The formjson content contains an array of EventHandlers that define the EventName, FunctionName and LibraryName.

<fetch>
  <entity name='systemform' >
    <attribute name='name' />
    <attribute name='formjson' />
    <attribute name='formactivationstate' />
    <attribute name='type' />
    <attribute name='objecttypecode' />
    <filter>
      <condition attribute='formjson' operator='like' 
         value='%&quot;LibraryName&quot;:&quot;msdyn_/Utils/head.js%' />
    </filter>
  </entity>
</fetch>

The next step is to figure out which EventHandlers, if any, use that library in those forms. To do that I created a simple LINQPad script to return a list of the functions in each form.

void Main()
{
	string url = "https://myorgname.crm6.dynamics.com"; 
	string username = "my.user@myorgname.onmicrosoft.com";
	string password = Util.GetPassword("d365-admin");
	
	CrmServiceClient conn = new CrmServiceClient($"Url={url};Username={username};Password={password}; AuthType=Office365");

	conn.OrganizationServiceProxy.Timeout = new System.TimeSpan(0, 3, 0);
	
	conn.OrganizationServiceProxy.EnableProxyTypes();
	
	IOrganizationService orgService = conn.OrganizationWebProxyClient != null ? (IOrganizationService)conn.OrganizationWebProxyClient : (IOrganizationService)conn.OrganizationServiceProxy;

	string pagingCookie = null;
	
	EntityCollection formsCol = null;
	
	List<Entity> forms = new List<Microsoft.Xrm.Sdk.Entity>();
	
	do
	{
		string fetchXML = $@"<fetch>
		  <entity name='systemform' >
		    <attribute name='name' />
		    <attribute name='formjson' />
		    <attribute name='formactivationstate' />
		    <attribute name='type' />
		    <attribute name='objecttypecode' />
		    <filter>
		      <condition attribute='formjson' operator='like' value='%&quot;LibraryName&quot;:&quot;msdyn_/Utils/head.js%' />
		    </filter>
		  </entity>
		</fetch>";

		formsCol = orgService.RetrieveMultiple(new FetchExpression(fetchXML));
		
		if (formsCol.Entities.Count > 0)
		{
			forms.AddRange(formsCol.Entities);	
		}
		pagingCookie = formsCol.PagingCookie;
	}
	while (formsCol.MoreRecords);

	forms.Select(e => new
	{
		Name = e.GetAttributeValue<string>("name"),
		FormJSON = e.GetAttributeValue<string>("formjson"),
		Functions = System.Text.RegularExpressions.Regex.Matches(e.GetAttributeValue<string>("formjson"), "(\"EventName\":(.*?)},)", RegexOptions.None).Cast<Match>().Select(m => m.Value).ToList()
	}).Dump();
}

Since the formjson content is just a string, I used a Regex pattern to find multiple occurrences of the Event Handlers for each form. The result is a list of the EventNames, FunctionNames and LibraryNames for each form that utilises the JavaScript library in the FetchXML statement.

Advertisements

Azure Web App – Read local XML file

I needed a way to get read access to a local XML file in the Azure Web App that is not a .config file. Using the standard naming format for the root folder I can read the XML data file into a DataSet.

DataSet dataSet = new DataSet();
var wscXML = string.Concat(Environment.GetEnvironmentVariable("HOME", EnvironmentVariableTarget.Process), @"\site\wwwroot\wsc.xml");
dataSet.ReadXml(wscXML);

Visual Studio & Node JS

When you install Visual Studio 2017 it very kindly gives you the option to install Node.js

The only problem with this is that if you are not using v15.3 (or greater) of VS2017 then working with Azure Functions can present a couple of unusal issues.

  • Not being able to install the Azure Client Tools.
  • My application not being able to read from the local.settings.json file using the WebConfigurationManager.

The problem turned out to be the version of Node.js that was installed with Visual Studio and that I needed to update it to a more recent version. After completing the updated installation I was able to install the Azure tools and successfully read values from my local.settings.json file in my local development environment.

SharePoint Client – Folder and Files

If you know the relative Url of the folder within SharePoint there is a straight forward way to access the files within that folder.

using Microsoft.SharePoint.Client;
...
using (ClientContext clientContext = new ClientContext(&amp;quot;https://something.sharepoint.com/sites/mysite/&amp;quot;))
{
    Folder folder = clientContext.Web.GetFolderByServerRelativeUrl(&amp;quot;/sites/mysite/root_folder/sub_folder&amp;quot;);

    // Load the Folder and Files into the ClientContext so we can access them
    clientContext.Load(folder);
    clientContext.Load(folder.Files);
    clientContext.ExecuteQuery();

    foreach (var item in folder.Files)
    {
        // Load each file into the ClientContext to access the file information
        context.Load(item);
        context.ExecuteQuery();
    }
}

Accessing secure web services – invalid certificate errors

Calling a secure web service from within an ASP.NET application was generating the following exception:

The remote certificate is invalid according to the validation procedure.

Digging further down into the exception provided a little more detail:

Could not establish trust relationship for the SSL/TLS secure channel with authority 'companyaddress.com'

But, this still didn’t give me too many clues as to the problem. I then came across this very useful MSDN blog post that gave me some very valuable steps to follow to resolve the issue. My first problem was that there were a couple of intermediate certificates involved that needed to be installed to the relevant locations on the server where my ASP.NET app was running; but this didn’t solve the problem so I set up the tracing file and ran the request again. This time the error was a bit more helpful:

Certificate name mismatch

It turns out that my problem was the endpoint address in my web.config file did not match the address in the certificate. My endpoint address was entered as companyaddress.com but the certificate was issued to www.companyaddress.com. The final part of the solution was to change the endpoint address in the web.config to match that in the certificate.

ASP.NET Web API, ELMAH and Folder Permissions

This post just covers a few useful things I have found while I have been developing an ASP.NET Web API that uses AttributeRouting for Web API and ELMAH for Web API.

I found AttributeRouting much easier to configure and administer than using MapHttpRoute in the WebApiConfig.cs file; especially using the RoutePrefix at the controller level. Running tests on the attributes using the Route.axd was also very straight forward. I also found Fiddler (v4.4.8.0) to be very helpful in throwing different unit tests at my newly created web API. This is simple and powerful configuration that works straight out of the box.

Using the Elmah.Contrib.WebApi took a little more configuring to get working but I found a good place to start, after you had installed the NuGet packge was on the sample web.config page. This provides detailed examples of the configuration settings needed for the different types of log stores as well the basic settings for the different flavours of IIS. There were a couple of additional tweaks I would recommend:

  1. Since I am not generating any View I removed the filters.Add(new HandleErrorAttribute()); in the FilterConfig. The HandleErrorAttribute to serve up a View called Error. Since this is a Web API project and I don’t have any Views then it wasn’t needed.
  2. Instead, there is a rather neat process for capturing unhandled errors that I found here.
  3. Finally, if you are using a simple XML file to store your errors you need to make sure that the security configuration is correct for the folder where the files will be generated and stored. For example, I had my files stored in the App_Data folder under the root of the web site on IIS 7.5. The application pool for this site was running under the ApplicationPoolIdentity account and so this account needed to have write permissions to the App_Data folder. Instructions for doing this can be found here under the Securing Resources section of the article.

 

Using HtmlHelper.GenerateLink

Since the documentation for HtmlHelper.GenearteLink is a bit limited I thought I would document some of my understanding of its usage. The application we are developing uses a static class, called HtmlExtensions, that contains a number of methods to build MvcHtmlString’s. One of those methods was used to generate an anchor tag containing a Controller, Action and Id; for example, Id. Using the MvCHtmlString.Create method the first version of this helper method was written as

return MvcHtmlString.Create(string.Format("<a class="my-link" href="Controller/Action/{0}">{0}</a>", myNumber));

This worked fine in the development environment (VS 2012 with IIS Express 8) but when it was deployed to a virtual directory on the Test server we found that the links would not work because the paths were not being resolved correctly. With experience comes knowledge and I found people who had identified similar problems but using the GenerateLink method; I worked on converting the code above to use that method.

public static MvcHtmlString MyLink(this HtmlHelper helper, string myNumber)
{
    string anchor = HtmlHelper.GenerateLink(helper.ViewContext.RequestContext, 
                        helper.RouteCollection, myNumber, null, "Action", "Controller", 
                        new System.Web.Routing.RouteValueDictionary() { {"Id", myNumber } }, 
                        new System.Collections.Generic.Dictionary<string, object>() { { "class", "my-link" } });
    return MvcHtmlString.Create(anchor);
}

The GenerateLink method listed above produces virtually the same return string value as the initial implementation but this time it takes into account the RequestContext to generate the correct href attribute for the anchor. The other parameters worth noting are the RouteValueDictionary and the htmlAttributes Dictionary. The RouteValueDictionary allows us to define the final part of the Route Url, which in our case was the Id – {controller}/{action}/{id}. The htmlAttributes Dictionary is just a simple Key/Value pair of additional attributes required by the anchor tag. For the code example above to work you would need to replace the “Action” and “Controller” parameters with an actual Action name and Controller name, but other than that this provides a more robust solution to the problem.