CRMQuickDeploy now has the ability to deploy Content Snippets to PowerApps Portal from Visual Studio. Below are information on how to structure content snippets in your Visual Studio project.
Folder name
Like other supported Portal artefact types, content snippets need to be stored in a particular folder in your Visual Studio project. The folder name for content snippets is PortalContentSnippets. Create this folder at the root of your Visual Studio project.
Sub-folder for content snippet language (optional)
In CRM, content snippets can be associated with a language (as seen below).
In Visual Studio, you can specify this language for your content snippets by creating a sub-folder under the PortalContentSnippets folder, e.g. ‘English‘. The tool will look for a language matching the name of the sub-folder.
This is optional though since Content Snippet Language is optional in CRM. If your content snippets are placed directly under the PortalContentSnippets folder in Visual Studio, then the tool will not associate it with a language when deploying to CRM.
Naming your content snippets in Visual Studio
As with other supported artefact types, the tool uses the file name of your items in Visual Studio to locate a matching record in CRM in order to create or update it.
With content snippets, the tool supports both creating and updating from Visual Studio. Use the following file extensions:
.html: the Type field in CRM will be set to HTML.
.txt: the Type field in CRM will be set to Text.
Special name mapping for ‘/’ character
There are some OOTB content snippets that Portal specifically look for by name, e.g. ‘Head/Bottom‘. Since the ‘/’ character cannot be used in an item name in Visual Studio, the tool uses a special name mapping rule to get around this.
Any ‘.’ in the item name in Visual Studio, except for the one immediately before the extension, will be converted to ‘/’ when searching for the target record in CRM.
For example, to target the ‘Head/Bottom‘ content snippet, name the item in Visual Studio as ‘Head.Bottom.html‘.
Example
Here is what your Visual Studio project should look like:
Download CRMQuickDeploy
You can download this extension from the Marketplace.
Currently PowerApps Portal does not support rich text editor out-of-the-box (OOTB). While the backend CRM has an OOTB rich text control that can be used in CRM, this unfortunately currently does not work on Portal.
You can add rich text editor support to PowerApps Portal using a JavaScript library such as CKEditor. While there are a few popular libraries out there, CKEditor is the library that Microsoft uses underneath the hood for their OOTB rich text control in CRM. For this reason, I recommend that you use CKEditor on the Portal to ensure that rich text submitted from the Portal can be viewed seamlessly from the backend CRM and vice versa. This post provides the steps and code to implement CKEditor on the Portal, and to address a few technical issues that I have encountered.
It’s actually fairly easy to implement CKEditor on the Portal (assuming that you want to enable it on multi-lines text fields). There are however two issues that I have encountered:
Field validation no longer works
Save Changes Warning on advanced form (aka web form) no longer works
Field validation no longer works
Validation such as mandatory field no longer works properly. In the screenshot below for example, user cannot submit the form due to the mandatory field validation error, even though they have typed a value into the rich text editor for that field.
Save Changes Warning on advanced form no longer works
Advanced form (aka web form in the old terminology) has a feature that can be enabled to warn user about unsaved changes upon leaving the page.
This option no longer works for fields where the rich text editor is enabled. That is, if a user has made a change to a rich text enabled field, then they will not be warned about unsaved changes when they navigate away from the page.
Root cause
The root cause for the above issues is because CKEditor adds a separate HTML element to the form for each field that is rich text enabled (and hides the original field). When you type into the rich text editor, it is this HTML element that you are editing, and not the actual field underneath. The OOTB validation and unsaved changes warning however, operate on the actual fields (and not the HTML elements that CKEditor injected onto the form). While CKEditor has built-in smart to automatically copy content from its injected HTML elements to the actual fields upon form submission (therefore enabling the content to be saved to CRM without any work on your behalf), the timing of this does not appear to be right for the aforementioned OOTB features to work correctly.
Solution
To solve this issue, we will use CKEditor’s autosave feature. This feature allows us to keep the content of the actual fields in sync with the injected HTML elements as the user types into the rich text editor. As this will be done continuously on a near-real-time basis, this will enable the aforementioned OOTB features to work correctly.
OK! Show me the steps (and the code)!
Acquire a build of CKEditor with the Autosave plugin
The autosave feature is not included in any of the CKEditor packages by default, so we will need to generate a custom build of CKEditor. Go to https://ckeditor.com/ckeditor-5/online-builder/ and choose your preferred editor style. When you get to the “Choose editor plugins” step, make sure you click Add on Autosave.
Progress to the step where you can build and download the generated package, which is a zip file. Grab the ckeditor.js file in the build folder of the zip. This is the file that we will need to reference on our web page.
Prepare Portal artifacts in CRM
I won’t go into details here as these are general Portal management step, but at a high level you will need to:
Upload ckeditor.js as a web file to CRM
Configure your basic form (aka entity form) or advanced form
Configure your web page
Test your web page
Add code to web page to implement rich text editor
Add the following to your web page’s HTML. Review the inline comment.
<!-- Add reference to our build of CKEditor -->
<script src="/ckeditor.js"></script>
<!-- By default the height of the rich text editor will be pretty small. This CSS will increase its height to 300px. You may
want to move this to a global CSS file. -->
<style>
.ck-editor__editable_inline {
height: 300px;
}
</style>
<script>
/**
* This script initialises the rich text editor for the target field. You may want to move this code into the web page's JavaScript
* section, or to a dedicated JS web file.
* */
$(document).ready(function () {
/**
* It appears we need to specify the toolbar when using a custom build of CKEditor. See this post to learn how to discover all the
* component names that you can list on the toolbar: https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html#member-toolbar
* */
ClassicEditor.create(document.querySelector("#new_backgroundinformation"), {
toolbar: ["heading", "|", "bold", "italic", "numberedList", "bulletedList", "|", "indent", "outdent"],
autosave: {
save(editor) {
//This method must return a promise.
var $deferred = $.Deferred();
//Access the source field for the editor and copy content over to it.
var sourceElementId = editor.sourceElement.id;
$("#" + sourceElementId).val(editor.getData());
//This is an OOTB Portal function and marks the field as dirty.
setIsDirty(sourceElementId);
//We have done all we need to do so mark the promise as resolved before returning it.
$deferred.resolve();
return $deferred.promise();
}
}
});
});
</script>
You should now have CKEditor integrated nicely into your Portal. You should also configure the OOTB rich text control in the CRM backend so that the rich text submitted from Portal will be displayed nicely to your CRM users.
But wait…
There is actually one more issue with CKEditor on the Portal. OOTB, you can click on a validation error message (highlighted in the screenshot below), and the page will scroll to the offending field and that field will receive the focus.
With CKEditor enabled, it still scrolls to the field, but the field no longer receives the focus. This is because the actual field is hidden by CKEditor. I don’t have a solution for this yet, but it appears to be a minor issue.
Essentially the approach here is to use Liquid to output the content of the EntityImage field to a JavaScript variable, and then use JavaScript to convert that to base64-encoded image data and dynamically create an <img> tag on the page.
For some unknown peculiar reason however, Liquid will throw an error
Liquid error: Exception has been thrown by the target of an invocation.
While the above fix will technically work, it may not be acceptable from a security perspective (depending on your scenario/requirements). For example, you probably don’t want to grant Global Read to the Account entity.
I have found that you can get this to work by granting Global Read permission to the Image Descriptor entity instead, and not the parent entity of the EntityImage field. Image Descriptor is the entity that CRM uses to store image data behind the scene.
In Liquid, you would then query the Image Descriptor entity and retrieve the EntityImage data like so:
The above code outputs the image data for an Account record that is specified via the id query string parameter.
Note that the entity permission for Image Descriptor still needs to be at Global scope. This entity however is less likely to be exposed directly to Portal users (e.g. via a web page or entity list), and therefore this approach may be more acceptable than granting Global Read on Account (for example).
It is interesting to note however that Microsoft documentation states that:
CRMQuickDeploy now supports a wider range of connection options to CRM, including client ID/secret and MFA. Below are examples of connection string to use for each scenario.
This mode would be suitable if no one ever look at your screen while you code :).
Client ID and secret
Without embedding client secret: url=https://yourcrm.crm6.dynamics.com;authtype=ClientSecret;clientid=yourclientid
The tool will prompt you to enter a client secret on the first deployment.
With client secret embedded: url=https://yourcrm.crm6.dynamics.com;authtype=ClientSecret;clientid=yourclientid;clientsecret=yourclientsecret
This mode would be suitable if no one ever look at your screen while you code:).
Office 365 user without MFA
Without embedding password: url=https://yourcrm.crm6.dynamics.com;AuthType=OAuth;username=user@yourdomain.com;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97
The SDK’s login form will be shown to prompt you for the password. The AppId and RedirectUri values as seen here are provided by Microsoft and will work. However, you may want to create your own.
With password embedded: url=https://yourcrm.crm6.dynamics.com;AuthType=OAuth;username=user@yourdomain.com;password=userpassword;AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97
This mode would be suitable if no one ever look at your screen while you code :). The AppId and RedirectUri values as seen here are provided by Microsoft and will work. However, you may want to create your own.
Do not specify your password in the connection string. The SDK’s login form will be shown to prompt you for the password and the second-factor. The AppId and RedirectUri values as seen here are provided by Microsoft and will work. However, you may want to create your own.
This feature of CRMQuickDeploy is for xRM Portal CE only and does not work for D365 Portal (on cloud). The context of this post therefore is xRM Portal CE only.
When updating portal artefacts (such as web templates, web files, etc.), you need to refresh the portal cache for the changes to be picked up by the portal. During development, this means either triggering a request to cache.axd via a bookmarklet, or clicking the Clear Cache button on the /_services/about page of the portal.
While CRMQuickDeploy allows you to quickly deploy portal artefacts to CRM from Visual Studio, you still needed to manually trigger a portal cache refresh after each deployment. Not only this means extra work for you as the developer, it also has an impact on your productivity as the portal typically takes 3-8 seconds to recover after each cache refresh.
I am pleased to announce that a new feature has been added to CRMQuickDeploy to address these pain points. With CRMQuickDeploy 3.7, you can now specify the location of cache.axd for your portal as a solution property in Visual Studio. Each time a portal artefact is deployed, CRMQuickDeploy will automatically fire a request to the specified cache.axd to refresh the cache. Unlike the bookmarklet or the Clear Cache button however, which invalidate the entire portal cache, CRMQuickDeploy will invalidate only the specific CRM records that were updated by your deployment. This therefore significantly reduces the page load time in portal following a deployment.
Do note however that your browser may cache JS/CSS/HTML rendered by the portal, even if server-side cache has been refreshed by CRMQuickDeploy. You therefore may need to press Ctrl + F5 (as oppose to F5), or disable browser cache during development, to reload the page with all changes.
OK, so to make use of this feature, you will need to do two things:
Enable cache.axd for your targeted portal
Specify the cache.axd URL in Visual Studio
Enabling cache.axd for your portal
cache.axd is a special HTTP handler that can be used to invalidate server-side cache for your portal. This HTTP handler may not be enabled for your portal by default. To check if this handler is enabled, open the web.config for your portal and search for a line similar to this:
Important: Once enabled, this HTTP handler can be invoked by anyone that has access to your portal website, and can be used maliciously. You should enable this handler only in the development environment.
Specify cache.axd URL in Visual Studio
A new solution property, namely Portal Cache.axd Invalidation Handler URL, has been added to the solution node in Visual Studio. Specify the full URL to cache.axd for your portal website in this property, e.g. http://myportal/cache.axd if your website is at the root path, or http://myserver/portal/cache.axd if your website is not at the root path.
D365 Portal (on cloud) uses a completely different approach to invalidate portal cache. Unfortunately it does not seem to expose any hooks for us to be able to trigger the cache refresh programmatically. This means that unfortunately it is not possible to implement the same feature for D365 Portal at this stage.
User-specific options at the solution level are stored in the .suo file. Back when it was the norm to have your extension inherits from the Package class, and therefore loaded synchronously by Visual Studio, you can have these options loaded by overriding the OnLoadOptions method. This method is automatically called by the platform for each option key that you previously specified (via Package.AddOptionKey). The OnLoadOptions method would look something like the below.
Since synchronous loading of packages has been deprecated, your extension now should inherit from AsyncPackage (and have AllowsBackgroundLoading enabled and PackageAutoLoadFlags set to BackgroundLoad if applicable). When your package is loaded asynchronously in the background however, it may not have been loaded when the platform processes the .suo file, and therefore OnLoadOptions may not be called on your package, which results in user-specific options not being restored for the user.
A solution for this is to initiate the loading of solution user options in your own code (rather than relying on the platform). You can use the IVsSolutionPersistence.LoadPackageUserOpts method and pass in the option key to do this. Here are the steps:
Make your package class implements IVsPersistSolutionOpts.
public sealed class MyExtensionPackage : AsyncPackage, IVsPersistSolutionOpts
Add the following code to the package’s InitializeAsync method:
var solutionPersistenceService = GetService(typeof(IVsSolutionPersistence)) as IVsSolutionPersistence;
solutionPersistenceService.LoadPackageUserOpts(this, "MyOption");
solutionPersistenceService.LoadPackageUserOpts(this, "MyOption2");
//Other option keys
That’s it. Your OnLoadOptions method does not need to change.
Note that this will change the UX of your package slightly. When the user opens a solution in Visual Studio, they would be able to start using Visual Studio and work with the solution before your package finishes loading. This means that for a few seconds, the user options may not have been loaded, and this may confuse some users.
Recently a colleague taught me a trick that I did not know about Liquid in Dynamics 365 Portal: use the {%- -%} syntax (as oppose to {% %}) to keep the HTML output clean. I don’t think this is documented in Microsoft’s documentation, but it is described here: https://shopify.github.io/liquid/basics/whitespace/. I have tested this an it works on both OnPrem and OnCloud portals.
In most cases I think you’d want to use the {%- -%} syntax to keep your HTML clean and compact. To illustrate the point, let say I have the code below to list all the accounts, with a different CSS class for odd and even items.
Without the {%- -%} syntax, this produces the following HTML:
As you can see, there are multiple blank lines between each item. While this does not impact how the browser renders the page, it does make the page’s source messy and increase the page’s download size.
Here is what the output look like when the {%- -%} is used:
As you can see, this is a lot cleaner.
The Shopify documentation also mentioned the {{- -}} syntax (as oppose to {{ }}). I have not tried this, but if it does work, it probably would be something you’d want to use more often than not too.
In Dynamics 365 Portal you can write FetchXML query in Liquid using the fetchxml tag. As this query can be dynamically constructed, there may be scenarios where you will need to debug it. There’s an undocumented feature that allows you to view the actual XML that Portal executes against CRM.
Here’s an example. Let say I have a page that takes a query string and uses Fetch to list all accounts where account name starts with that string. I would like to view the actual FetchXML being executed in order to debug the page. I can do this by simply accessing the xml property of the query object. Below is the Liquid code for this.
Here is how the page renders:
As you can see, this can be a very useful tool for debugging. Note that in the Liquid code, the escape filter is used to escape the XML tags. Without this, the XML will still be output, but will not be displayed on the page when viewing in browser.
Since a couple of versions back, CRMQuickDeploy has a “link item” feature that allows you to share source code (Liquid/HTML, CSS and JavaScript) between multiple Portal artefacts (web pages, entity forms and web forms). When you deploy the source item however, the linked items are not automatically deployed and you had to trigger their deployment separately. If you forget to do this, then the linked artefacts would become out of sync.
I have released a new version of the extension that eliminates this issue. Now when you deploy an item, the extension automatically adds all linked items that reference that item to the deployment list. This means that you will no longer need to separately trigger deployment of linked items, and that your linked artefacts will automatically stay in sync.
In Dynamics 365 Portal, web templates are most often used to define reusable page layouts. Web templates however can also be used to create “functions” that “return” values, which you can then use to streamline your Liquid code.
I have put “functions” and “return” in quotes above because technically you can’t define functions with web templates. In fact, there are no ways to define a true function in Liquid that I am aware of! With some creative workaround however, you can come pretty close to achieving the same goals/benefits of functions with web templates. I will detail the approach in this post.
There are 3 main concerns that we need to address when it comes to function:
invoking the function,
input parameters, and
return values.
To invoke a web template from a web page, you can use the following liquid:
{% include “Name of the Web Template” %}
To pass input parameters to the web template, we can update the call above to be as follow:
{% include “Name of the Web Template” param1: value1, param2: value2 %}
Return values requires a bit of a workaround. Web templates predominantly render content and do not return values. However, any Liquid variables you create and assign value to in a web template are also available for use on the calling web page, i.e. outside of the web template. We can use this workaround to return values to the calling web page.
As an example, let say you want to list all the open Opportunities for a given Account on a page. On some other pages, you need to display them in a carousel. You therefore would like to encapsulate the retrieval of open Opportunities into a reusable function.
Create a web template function
First, create a web template, e.g. Get Open Opportunities For Account, with the following code:
This simple “function” expects an input parameter called accountId. It executes a Fetch query and returns the resulting records in a variable called opportunities.
Create the web page
Create a web page with the following code:
{% include "Get Open Opportunities for Account" accountId: request.params["accountid"] %}
{% for record in opportunities %}
{{record.name}}<br />
{% endfor %}
This simple web page invokes our function and pass in the account ID from the query string. It uses the returned result (in the opportunities variable) to render the list of Opportunities.
Other things to note and limitation
By creating multiple variables in your web template, you can return multiple values to the calling web page. In the example above for instance, you can create a separate return value to hold the total count of matching Opportunities.
In terms of limitation, the biggest I see is the readability of the code (the syntax isn’t the most natural for functions), and the discoverability of the input parameter(s) and return value(s). I’d suggest you workaround this by using the {% comment %} tag in your function web template.
One more thing…
Do a lot of coding with Dynamics 365 Portal? You should check out my Visual Studio extension CRMQuickDeploy. This extension allows you to deploy HTML, Liquid, CSS, and JavaScript to a wide range of Portal artefacts (e.g. web templates, web pages, web files, entity forms, etc.) seamlessly from Visual Studio. Not only this would allow you to leverage the powerful editing features of Visual Studio, but also to source control your code – and facilitate multiple developers working on the project concurrently.
So there you have it…
Function is not a native concept in Liquid, but with a simple workaround, you can still encapsulate your logic into reusable blocks and use them throughout your code-base as if they were functions.