Dynamics 365 portals: Implement FullCalendar with Liquid

Using the techniques discussed in Using liquid to return JSON or XML we can easily build a services that work with various with JavaScript libraries. FullCalendar is one of the most popular libraries for displaying events in various views on the web and can easily be used in the portal and creating a JSON return template for the event data. With FullCalendar you can fully customize a calendar output in the portal to really get that user experience you might be looking for. In this post is a simple implementation of FullCalendar with a custom entity JSON return template that will help outline how you can use this library in your portal projects.

First we can start with creating the web template that will be the return JSON data. Within this template a fetchxml query that takes at least the parameters of start and end so that it will filter for the date range being displayed on the calendar by adding the necessary conditions in the fetch.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="100" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %}
page="1"
{% endif %}>
  <entity name="dpx_courseschedule">
    <attribute name="dpx_number" />
    <attribute name="createdon" />
    <attribute name="dpx_starttime" />
    <attribute name="dpx_endtime" />
    <attribute name="dpx_courselocationid" />
    <attribute name="dpx_courseid" />
    <attribute name="dpx_coursescheduleid" />
    <filter type="and">
      <condition attribute="statecode" operator="eq" value="0" />
      {% if request.params['location'] %}
        <condition attribute="dpx_courselocationid" operator="eq" value="{{ request.params['location'] | xml_escape }}" />
      {% endif %}
      {% if request.params['start'] %}
        <condition attribute="dpx_starttime" operator="ge" value="{{ request.params['start'] | xml_escape }}" />
      {% endif %}
      {% if request.params['end'] %}
        <condition attribute="dpx_endtime" operator="le" value="{{ request.params['end'] | xml_escape }}" />
      {% endif %}
    </filter>
    <link-entity name="dpx_instructor" from="dpx_instructorid" to="dpx_instructorid" visible="false" link-type="outer" alias="instructorlink">
      <attribute name="dpx_contactid" />
      <attribute name="dpx_number" />
    </link-entity>
    <link-entity name="dpx_course" from="dpx_courseid" to="dpx_courseid" alias="courselink">
      <attribute name="dpx_coursecategoryid" />
      {% if request.params['category'] %}
        <filter type="and">
          <condition attribute="dpx_coursecategoryid" operator="eq" uiname="Category 1" uitype="dpx_coursecategory" value="{{ request.params['category'] | xml_escape }}" />
        </filter>
      {% endif %}
    </link-entity>
  </entity>
</fetch>
{% endfetchxml %}[
  {% for item in feed.results.entities %}
    {
      "title": "{{ item.dpx_courseid.name }} - {{ item['instructorlink.dpx_contactid'].name }}",
      "start": "{{ item.dpx_starttime | date_to_iso8601 }}",
      "end": "{{ item.dpx_endtime | date_to_iso8601 }}"
    }{% unless forloop.last %},{% endunless %}
  {% endfor -%}
]

The JSON is formatted to follow the FullCalendar Event Object and return an Event Source Object. Providing the return format directly in the service allows us to now easily hook up this service to the FullCalendar event configuration. The sample above only includes a number of simple properties but you can add any number of properties from the Event Object and map them to data in the query results.

Ensure that you now give the return JSON template a URL using the instructions in Use liquid to return JSON or XML and the Mime Type is set to application/json.

Now create a new web template that is going to implement the FullCalendar library and consuming the event return JSON template.

The first thing that needs to be added is references to both the CSS as well as JavaScript for FullCalendar as well as a reference to Moment.js. FullCalendar utilizes Moment.js for all date functions and therefore the Moment.js library needs to be referenced prior to the FullCalendar JavaScript library. Below is a simple template that uses the CDN’s for FullCalendar as well as Moment.js.

<link href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.3.1/fullcalendar.min.css" rel="stylesheet" />

<div id='calendar'></div>

<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.3.1/fullcalendar.min.js"></script>

<script>
  $(document).ready(function() {
      $('#calendar').fullCalendar({
          // FullCalendar configuration properties
      });
  });
</script>

The JavaScript in the code above initializes FullCalendar on the HTML element with the id of calendar and takes a series of parameters. You can review all the parameters available for configuration on the FullCalendar documentation. There are configuration properties for just about every function and include event callbacks that can be configured for certain events which could be utilized to make further calls to return JSON based templates.

To display event data on the calendar there are a couple of parameters that can be used to inject event data. For the following configuration we will be using the previously created web template that returns the FullCalendar Event Object based on our custom entity, but you can retrieve the data in various ways with the options that FullCalendar provides including multiple event sources.

<script>
  $(document).ready(function() {  
      $('#calendar').fullCalendar({
        events: {
          url: '/fullcalendar-json/',
          type: 'GET',
          error: function() {
            alert('there was an error while fetching events!');
          },
          color: 'yellow',
          textColor: 'black'
        }
      });
  });
</script>

Here we have replaced the blank configuration properties with the Events property which uses a url to retrieve event data. The URL is configured to our previous templates URL and the GET HTTP method is configured as the type. The event properties color and textColor are also configured for the default display of events on the calendar. After hooking up this new web template to a page template and web page you can view it in the portal and should have a result similar to the following:

Now we can take this further and enhance the display of events as well as add event filtering options. For my custom entities, I extended the Instructor with 2 additional fields, color and text color which I put in HEX color code values into for each record. I extended the fetchxml query to include these fields as well as the JSON output to add these fields as Event Object properties, below is the new JSON output including as well a URL that will link to the event details using a site marker.

{% fetchxml feed %}
<!-- fetchxml statement removed for length -->
{% endfetchxml %}[
  {% assign urlMarker = sitemarker['Event Details'] %}
  {% for item in feed.results.entities %}
    {
      "title": "{{ item.dpx_courseid.name }} - {{ item['instructorlink.dpx_contactid'].name }}",
      "start": "{{ item.dpx_starttime | date_to_iso8601 }}",
      "end": "{{ item.dpx_endtime | date_to_iso8601 }}",
      "color": "{{ item['instructorlink.dpx_colorcode'] }}",
      "textColor": "{{ item['instructorlink.dpx_textcolor'] }}",
      "url": "/{{ urlMarker.Url }}?id={{ item.id }}"
    }{% unless forloop.last %},{% endunless %}
  {% endfor -%}
]

In my FullCalendar liquid template I have add a fetchxml query so I can get a list of locations which are displayed in a drop down so users can then filter events in the calendar by this property.

{% fetchxml locationfeed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
  <entity name="dpx_courselocation">
    <attribute name="dpx_name" />
  </entity>
</fetch>
{% endfetchxml %}

<p>
  <div class="input-group">
    <select id="course-location" class="form-control">
        <option value="" selected disabled>Select a location...</option>
      {% for item in locationfeed.results.entities %}
        <option value="{{ item.id }}">{{ item.dpx_name }}</option>
      {% endfor %}
    </select>
    <span class="input-group-btn">
      <button id="refresh-events" class="btn btn-primary">Refresh Events</button>
    </span>
    <span class="input-group-btn">
      <button id="clear-events" class="btn btn-danger">Reset/Clear</button>
    </span>
  </div>
</p>

Then the JavaScript I have added 2 events for the new refresh and clear buttons, as well I have added a data property to the events call that will inject the location value if it is selected.

$('#calendar').fullCalendar({
  events: {
    url: '/fullcalendar-json/',
    type: 'GET',
    data: function() {
      var locationId = $('#course-location option:selected').val();            
      if (locationId) {
        return {
          location: locationId
        };
      }            
      return null;
    },
    error: function() {
      alert('there was an error while fetching events!');
    },
    color: 'yellow',   // a non-ajax option
    textColor: 'black' // a non-ajax option
  }
});

$('#refresh-events').on('click', function(){
  $('#calendar').fullCalendar('refetchEvents');
});

$('#clear-events').on('click', function(){
  $('#course-location').prop('selectedIndex',0);
  $('#calendar').fullCalendar('refetchEvents');
});

The display and function of the calendar should now be enhanced with colors for each instructor and the ability to filter the course schedules by location!

Hopefully this has helped show an example of the power you can get out of liquid templates by creating your own JSON based service and utilizing a JavaScript library like FullCalendar.

If your interested in learning more about liquid techniques I will be giving a webinar with xRMVirtual on April 26th at 12pm EST – Advanced Liquid Templates for Dynamics 365 portals.

Dynamics 365 portals: Use liquid to return JSON or XML

For many business requirements to get a desired user experience you may push the limits that the entity list functionality provides. With liquid you can write your own service to return data in various formats using the web template Mime Type property. By returning your own data you can inject logic and specific formatting using liquid functionality, this will allow you to utilize new components or libraries to help you provide the specific experience your requirements demand. This post will look at how liquid can be used with web templates to return JSON or XML so that the data can be consumed and used to build a complex user experience.

With liquid there are many ways to query for data. There is the entities object which can be used to retrieve a single record by ID. To get lists of data you can use the entity list which behind is using one or many entity views and an entity view is a fetchxml query that defines the view. Entity list contains a feature, OData feed, that allows you to take an entity view and make it available as a service. The OData feed is a great way to get a RESTFul JSON return but it has many shortcomings. If your interested in trying out the OData feed functionality then check out the documentation still available on the Adxstudio Community site – Entity List OData Feeds.

If you want to directly write your own queries in web templates, perhaps dynamically constructing them, utilize entity permission relationship based data, then you can use the liquid fetchxml tag. Below is a little outline of the functions of this liquid object.

{% fetchxml my_query %}
  <fetch version="1.0" mapping="logical">
    <!-- Write FetchXML here, use Liquid in here if you want, to build XML dynamically. -->
  </fetch>
{% endfetchxml %}
 
{{ my_query.xml | escape }}
{{ my_query.results.total_record_count }}
{{ my_query.results.more_records }}
{{ my_query.results.paging_cookie | escape }}
{% for result in my_query.results.entities %}
  {{ result.id | escape }}
{% endfor %}

Reference: Adxstudio Community Forums

With Dynamics 365 portals entity permissions is required by default and does not need to be referenced in the liquid tag. This differs from Adxstudio Portals v7.x, so if you are getting blank results using the fetchxml liquid object then ensure to first validate your entity permissions.

The fetchxml liquid tag and web templates Mime Type functionality provide the ability to build a web template that returns custom JSON or XML objects. With this you can build endpoints that intake custom parameters, perform logic while constructing the query, logic in returning the results, formatting the results and doing related record queries, all the while adhering to the entity permissions in place for that entity.

Below is an example of a web template that queries a custom course schedule entity with joins to related entities, course and instructor. The liquid code looks for 2 parameters location and category and if they exist it adds the fetch conditions for those parameters.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="10" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %} page="1" {% endif %}>
  <entity name="dpx_courseschedule">
    <attribute name="dpx_number" />
    <attribute name="createdon" />
    <attribute name="dpx_starttime" />
    <attribute name="dpx_endtime" />
    <attribute name="dpx_courselocationid" />
    <attribute name="dpx_courseid" />
    <attribute name="dpx_accountid" />
    <attribute name="dpx_cost" />
    <attribute name="dpx_coursescheduleid" />
    <order attribute="createdon" descending="true" />
    <filter type="and">
      <condition attribute="statecode" operator="eq" value="0" />
      {% if request.params['location'] %}
        <condition attribute="dpx_courselocationid" operator="eq" value="{{ request.params['location'] | xml_escape }}" />
      {% endif %}
    </filter>
    <link-entity name="dpx_instructor" from="dpx_instructorid" to="dpx_instructorid" visible="false" link-type="outer" alias="instructorlink">
      <attribute name="dpx_contactid" />
      <attribute name="dpx_number" />
    </link-entity>
    <link-entity name="dpx_course" from="dpx_courseid" to="dpx_courseid" alias="courselink">
      <attribute name="dpx_level" />
      <attribute name="dpx_lengthunit" />
      <attribute name="dpx_length" />
      <attribute name="dpx_coursecategoryid" />
      {% if request.params['category'] %}
        <filter type="and">
          <condition attribute="dpx_coursecategoryid" operator="eq" value="{{ request.params['category'] | xml_escape }}" />
        </filter>
      {% endif %}
    </link-entity>
  </entity>
</fetch>
{% endfetchxml %}{
  "totalcount": {{ feed.results.total_record_count }},
  "morerecords": {{ feed.results.more_records }},
  "page": {{ request.params['page'] | default: 0 }},
  "results": [
    {% for item in feed.results.entities %}
      {
        "starttime": "{{ item.dpx_starttime | date_to_iso8601 }}",
        "endtime": "{{ item.dpx_endtime | date_to_iso8601 }}",
        "instructorname": "{{ item['instructorlink.dpx_contactid'].name }}",
        "courselevel": "{{ item['courselink.dpx_level'].label }}",
        "location": {
          "id" : "{{ item.dpx_courselocationid.id }}",
          "name": "{{ item.dpx_courselocationid.name }}"
        }
      }{% unless forloop.last %},{% endunless %}
    {% endfor -%}
  ]
}

The fetchxml result is then formatted into a JSON object using the forloop liquid object to iterate through each entity record. Linked entity attributes are easily accessed via the linked entity alias {{ entityRecord['alias.attribute'] }}. With the return being JSON you will want to set the Mime Type property to application/json.

Another example using Case (incident) where we also use a N:N relationship of all child cases in the custom data object returned with the referenced attribute to load the entity relationship.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="10" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %} page="1" {% endif %}>
  <entity name="incident">
    <attribute name="ticketnumber" />
    <attribute name="prioritycode" />
    <attribute name="title" />
    <attribute name="createdon" />
    <attribute name="customerid" />
    <attribute name="ownerid" />
    <attribute name="statecode" />
    <attribute name="incidentid" />
    <attribute name="caseorigincode" />
    <order attribute="title" descending="false" />
  </entity>
</fetch>
{% endfetchxml %}{
  "totalcount": {{ feed.results.total_record_count }},
  "morerecords": {{ feed.results.more_records }},
  "page": {{ request.params['page'] | default: 0 }},
  "results": [
    {% for item in feed.results.entities %}
      {
        "ticketnumber": "{{ item.ticketnumber }}",
        "title": "{{ item.title }}",
        "customer":  {
          "id" : "{{ item.customerid.id }}",
          "name": "{{ item.customerid.name }}"
        },
        "incident_parent_incident": [
            {% for parent in item.incident_parent_incident.referenced %}
              {
                "parentticketnumber": "{{ parent.ticketnumber }}"
              }{% unless forloop.last %},{% endunless %}
            {% endfor %}
          ]
      }{% unless forloop.last %},{% endunless %}
    {% endfor -%}
  ]
}

If you instead wanted to return XML then it is just a matter of updating the Mime Type of the web template to application/xml and the code to output XML instead of the JSON format. Below is a sample of the first example but returning XML.

<!--FETCHXML query -->
{% endfetchxml %}<?xml version="1.0" encoding="UTF-8" ?>
<fetchxmlquery>
  <totalcount>{{ feed.results.total_record_count }}</totalcount>
  <morerecords>{{ feed.results.more_records }}</morerecords>
  <page>{{ request.params['page'] | default: 0 }}</page>
  <results>
    {% for item in feed.results.entities %}
      <item>
        <starttime>{{ item.dpx_starttime | date_to_iso8601 }}</starttime>
        <endtime>{{ item.dpx_endtime | date_to_iso8601 }}</endtime>
        <instructorname>{{ item['instructorlink.dpx_contactid'].name }}</instructorname>
        <courselevel>{{ item['courselink.dpx_level'].label }}</courselevel>
        <location>
          <id>{{ item.dpx_courselocationid.id }}</id>
          <name>{{ item.dpx_courselocationid.name }}</name>
        </location>
      </item>
    {% endfor %}
  </results>
</fetchxmlquery>

Once you have setup your web template with your liquid logic and Mime Type you need to get a URL for it. Create a page template of the Type, Web Template, and the previous Web Template referenced. As well ensure that Use Website Header and Footer is unchecked so that all that is returned is the data formed by the web template.

Now using the portals front-end editor, create a new web page using the new page template that references the web template. This will give your web template a URL and you can now refer to this endpoint within other JavaScript on the site. Here is a small jQuery sample calling a JSON endpoint URL and logging the result to the browser console. You can also do testing of your endpoints with Postman.

<script>
  $(function(){
    $.ajax({
      method: "GET",
      url: "/cases-json/"
    })
    .done(function( msg ) {
      console.log(JSON.parse(msg));
    });
  });
</script>

Note for entity permissions to function beyond anonymous a cookie for authentication must be attached to the request. JavaScript on the site already making requests will include the necessary cookie by default.

If your interested in learning more about liquid techniques I will be giving a webinar with xRMVirtual on April 26th at 12pm EST – Advanced Liquid Templates for Dynamics 365 portals.

Dynamics 365 portals: Events and iCalendar Download with Liquid

A popular component of Adxstudio Portals was its event management system, which unfortunately has not made the transition to Dynamics 365 portals. There will be a new event management system coming in the future (as seen at eXtreme365 in Lisbon), which will come with a portal component that will list and allow registration to events. Until then you may still want to create some simple event functionality using the out of box configuration based components that allows you to list events and allow users to add an event to their own calendar. In this post we will use Liquid Templates to create your own display of events from a custom entity with an entity list and an iCalendar download (Add to Calendar button) so that users can add it to their own calendar.

First we are starting with an entity (this could be any entity that already exists or creating a new one) that includes the event information. All you need for an event is really a subject and a date. If you want to get a little more complete then we want a start date, end date, subject, description, location. It’s really up to you and the functionality or detail you want to contain in your event. I am starting with an entity called Group Meetings which includes the following fields:

To display the events on the portal you can use an entity list with a custom web template to create a custom output of the information instead of just a grid/table display. Here is the entity list web template used on xrmvirtual.com (I have removed the paging to keep the code length displayed here brief).

{% assign meetingDetails = sitemarkers["Meeting Details"] %}

{% entitylist id:page.adx_entitylist.id %}
  <div class="meeting-list-body">
    {% entityview id:params.view, search:params.search, order:params.order, page:params.page, pagesize:params.pagesize, metafilter:params.mf %}
      {% if entityview.records == empty %}
        <div class="alert alert-info">
            {% if entitylist.empty_list_text %}
              <p>{{ entitylist.empty_list_text | escape }}</p>
            {% else %}
              <p>No items matching your selected criteria were found.</p>
            {% endif %}
        </div>
      {% else %}
        {% for meeting in entityview.records %}
          <div class="media">
            <div class="media-left jumbotron-icon">
              <span class="fa fa-calendar fa-2"></span>
            </div>
            <div class="media-body">
              <h4 class="media-heading"><a href="{{ meetingDetails.url }}?id={{meeting.id}}">{{meeting.xv_name}}</a></h4>
              <p class="meeting-date">Speaker: <span class="meeting-speaker">{{meeting.xv_primaryspeakerid.name}}</span> | <time datetime="{{meeting.xv_starttime | date_to_iso8601}}"></time></p>
              <div class="meeting-abstract">
                <p>{{meeting.xv_abstract}}</p>
              </div>
              <div class="meeting-actions">
                {% if meeting.xv_recordingposted | false and meeting.xv_recordingurl %}
                  <a href="{{meeting.xv_recordingurl}}" target="_blank" class="btn btn-success btn-sm">
                    <span class="fa fa-video-camera"></span>&nbsp;
                    Download Recording
                  </a>
                {% endif %}
                {% if now < meeting.xv_starttime and meeting.xv_meetingurl %}
                  <a href="{{meeting.xv_meetingurl}}" target="_blank" class="btn btn-primary btn-sm">
                    <span class="fa fa-calendar"></span>&nbsp;
                    Join Meeting
                  </a>
                {% endif %}                
                {% if now < meeting.xv_starttime %}
                  {% assign iCal = sitemarkers["XRM iCal"] %}
                  <a href="{{iCal.Url}}?id={{meeting.id}}" target="_blank" class="btn btn-default btn-sm">
                    <span class="fa fa-calendar-plus-o"></span>&nbsp;
                    Add to Calendar
                  </a>
                {% endif %}
              </div>
            </div>
          </div>
          <hr/>
        {% endfor %}
      {% endif %}
    {% endentityview %}
  </div> 
{% endentitylist %}

If you review the code it is basically taking the entity list that the page references {% entitylist id:page.adx_entitylist.id %}, does a check to make sure the entity view is not empty {% if entityview.records == empty %}, if it is not then it iterates through the items with a for loop {% for meeting in entityview.records %}. Within the for loop we have the individual record with the {{meeting}} object, which we then format the attributes of it with some HTML.

As this entity list template is used for upcoming and past meetings there are also some checks so that we can conditional show certain elements on each meeting, one of those being if there is a meeting URL and we are before the start time then show the join meeting button {% if now < meeting.xv_starttime and meeting.xv_meetingurl %}.

When building an event display the highest requested feature is to provide a download or add to calendar button so that users can easily add the event to their own calendars. With liquid templates we can easily satisfy this requirement. Not well documented and really only 1 public example is the web templates MIME type. With the MIME type field on web template we can actually set a custom type that the browser will use to interpret the content it is trying to process. If you don't set a MIME type then this will default the MIME type to text/html. For browsers to detect the content as calendar data then we can set the MIME type to text/calendar and then within the web template define the standard iCalendar format.

Let's start off with creating a new Web Template called iCal Download Handler. At the bottom of the form fill in the MIME type with text/calendar so that when a browser accesses this content it tries to interpret it using the calendar format. Now for the contents of the web template we want to output the standard iCalendar format. You can read through the entire RFC (if you really want) for iCalendar here to understand all the formatting options. Alternatively you can review the Wikipedia iCalendar document, as well below is a simple single event example.

{% assign meeting = entities.xv_groupmeeting[request.params.id] %}
{% if meeting %}
BEGIN: VCALENDAR
VERSION:2.0
PRODID: -//xrmvirtual.com//NONSGML ical.net 2.1//EN
BEGIN:VEVENT
DTEND:{{meeting.xv_endtime | date_to_iso8601 | remove: '-'}}
DTSTAMP:{{meeting.xv_starttime | date_to_iso8601 | remove: '-'}}
DTSTART:{{meeting.xv_starttime | date_to_iso8601 | remove: '-'}}
SEQUENCE: 0
SUMMARY:{{meeting.xv_name}}
X-ALT-DESC;FMTTYPE=text/html:{{meeting.xv_abstract}} {% if meeting.xv_meetingurl %}<p><a href="{{meeting.xv_meetingurl}}">Join meeting...</a></p>{% endif %}
UID: {{meeting.id}}
END:VEVENT
END:VCALENDAR
{% endif %}

The first check in the template is that we assume the ID of the event we want to provide as a download is provided as a query string parameter called 'id'. We use this parameter and the liquid entities object to retrieve the group meeting record. If the meeting exists then we output the iCalendar format with attributes from the group meeting as values for the iCalendar attributes.

The date/time iCalendar attributes do expect date/times formatted in the ISO8601 format and does not included any dashes. Luckily there are liquid filters that can help us achieve this exact format. Taking the meeting start or end time which is stored as a CRM date/time object we can apply the date_to_iso8601 filter and then apply the string filter to pull out the dashes that are included in that format remove: '-'.

In the SUMMARY attribute just simply use the liquid to expose a CRM attribute from the meeting object, {{meeting.xv_name}}. For the description (abstract in XRM Virtual group meeting) because we are using the CK editor to provide rich text for the description contents this is stored as HTML we need to provide the X-ALT-DESC iCalendar attribute and tell it the format FMTTYPE=text/html so that the encoding is properly interpreted. To this we also add a simple conditional check to see if there is a URL and then include the HTML to generate that link as part of the description. Finally the UID attribute of iCalendar format we just make the CRM GUID of the record.

To make this web template accessible we need to give it a URL. First create a new Page Template with the type of Web Template. Ensure the set the Use Website Header and Footer is not selected and set the Web Template to your iCal Download Handler. It is important that the Use Website Header and Footer is turned off as this will ensure that when this template is rendered it only includes the content in the web template and none of the scaffolding of the portal (like the header and footer HTML). With your page template create a new Web Page that references the new Page Template.

Page Template:

For ease of access in web template I also created a Site Marker that refers to the web page, then used the following code in my event list template to get the URL and pass the ID of the record:

{% assign iCal = sitemarkers["XRM iCal"] %}
<a href="{{iCal.Url}}?id={{meeting.id}}" target="_blank" class="btn btn-default btn-sm">
  <span class="fa fa-calendar-plus-o"></span>&nbsp;
  Add to Calendar
</a>

From this we have taken a custom entity that has event type data and displayed it in a nicely presented format on the portal and included an add to calendar or download calendar item functionality so that users can include it in their own calendar. Now because of the various versions of iCalendar that vendors have implemented you may notice this iCalendar format does not work on every device, you can though create specific web/liquid templates for the various formats and provide links to each of them or you can look at JavaScript libraries that help provide this format. AddEvent is a common plugin that can be used freely for personal use and licensed for commercial sites that will provide an easy implementation in a web template (below is a sample) that will cover all iCalendar formats.

AddEvent Liquid Template Sample:

<span class="addtocalendar  atc-style-blue">
  <var class="atc_event">
    <var class="atc_date_start">{{ event.adoxio_startdate | date: 'yyyy-MM-dd hh:mm:ss' }}</var>
    <var class="atc_date_end">{{ event.adoxio_enddate | date: 'yyyy-MM-dd hh:mm:ss' }}</var>
    <var class="atc_timezone">America/Vancouver</var>
    <var class="atc_title">{{ event.adoxio_name }}</var>
    <var class="atc_description">{{ event.adoxio_name }}</var>
    <var class="atc_location">{{ event.adoxio_name }}</var>
    <var class="atc_organizer">City of Victoria</var>
    <var class="atc_organizer_email">info@adoxio.com</var>
  </var>
</span>

Determine your Dynamics 365 portal data center

When you deploy a portal for Dynamics 365 you always want it located as close to your Dynamics 365 instance physically so that the latency for all communications between the portal and Dynamics 365 is as low as possible. With Microsoft managing the portal provisioning they take care of this for you. You still may want to determine the location for hosting of other services that will communicate with your portal, like when your looking at deploying a companion web app for your portal it would be ideal if it is the same data center as your portal.

You can quickly determine your major data center region just by looking at the address to your Dynamics 365 instance. The following link has a listing of all current major regions – Discover the URL for your organization using the Organization Service.  However within these major regions are regional data centers. For instance, https://*.crm.dynamics.com (North America which should actually be called USA/Mexico now since there is a region for Canada) has 3 different regional data centers and they are even creating secondary data centers regionally.  Within North America (or USA/Mexico) you could be in West US, Central US, East US, or even East US2.  Within CRM3 (Canada) you could be in Canada Central or Canada East as your primary data center. You can see all the Azure datacenter locations in the following link – Azure Datacenter locations.

Previous ways to determine your data center have involved spinning up an Azure VM and moving it through regions while doing latency tests.  You can also use a IP locator service to help you determine provided the IP address you testing has the proper location data registered with it (this sometimes can give false information as IP addresses can be re-routed to different regions).  Another way which the guys over at Peak Engagement blogged about is using the debug information page which reveals information about your Dynamics instance including the server name, database server, database name and much more.  If you look at patterns you may notice that the names of the servers are based on those regional data centers.

The Dynamics 365 portal has something similar but you can even use this technique on any Dynamics 365 portal without even being the owner of that site.  On every request response to the portal if you inspect the headers you will notice 2 additional response header keys added specifically for the Dynamics 365 portal, x-ms-portal-app and x-ms-request-id. The x-ms-request-id is just a unique GUID for each request, likely exposed to help assist with debugging. The x-ms-portal-app is a value for the site itself, it remains constant through all requests. Taking a look at the value found, there is the site GUID which is the GUID of the Azure Web App and then appended to that is a data center code.

You can easily get to this yourself without even being logged into the portal. The request headers are available on all requests. To look at this value open your browsers developer tools (F12), then select the network tab, open the details of any request and view the response headers. Below is a screenshot of Chrome’s developer tools highlighted with the navigation tips.

Below is a list of the data center region codes found on Dynamics 365 portals from the portals I have checked thus far.

  • EUw – West Europe
  • EUn – North Europe
  • USw – West US
  • GCv – US Government Cloud Virginia
  • GCi – US Government Cloud Iowa
  • USe2 – East US2
  • CAc – Canada Central
  • AUse – Australia Southeast

This is not an exhaustive list, if you come across others then feel free to drop a note in the comments.

You can now use this information to help you when picking a hosting location for a companion app as this region should be in same as your Dynamics 365 instance so you can attempt to get the best performance between applications and the backend Dynamics 365 instance.

Note there are instances where the Dynamics 365 instance and the portal will not be located in the same data center. This is not often the case but instances of this have seen that East US can host Dynamics 365 and East US2 can host the portal, this type of setup might also occur in other locations.

Building a Dynamics 365 ASP.NET Web App with Adoxio Connect Framework

The primary purpose behind the Adoxio Connect Framework was to help facilitate an easy server-to-server connection for a Web App. By using the Adoxio Connect Framework it helps by providing an implementation of OrganizationWebProxyClient and Active Directory Authentication Library so that you can easily interact through the Dynamics 365 SDK within your web application. This guide will walk you through starting an ASP.NET MVC and Web API project with the Adoxio Connect Framework to build your own forms, and APIs. If you’re looking to build a companion app to the Dynamics 365 portal or any CMS that can access a Dynamics instance and interact with it, then this is the guide to help you get that process started. This will also include the option of using the XRM Tooling API so you can utilize the CrmServiceClient extensions in your web application.

For this guide I am using Visual Studio 2017, but it should also be the same with Visual Studio 2015 provided you have the latest updates.

To start we need to create an an ASP.NET Web Application and this should be done using .NET Framework 4.6.1.

After creating, you should be prompted to select the template for the project. Here its up to the requirements you have and your preferred application templates, all templates are supported to implement the Connect Framework. For this guide I will be selecting the MVC template but also enabling Web API.

When you create a new ASP.NET Web Application depending on your template and references selected it will download a certain set of NuGet packages. Optionally you can update all the packages to the latest version. To update all the packages and install the additional NuGet packages I suggest using the Package Manager Console (View > Other Windows > Package Manager Console).

To update all packages run the following command in the Package Manager Console:

Update-Package

Next we need to install OWIN host for IIS (System.Web) as well OWIN for ASP.NET Identity. We are going to be using the OWIN framework to add the CrmContext object from the Adoxio Connect Framework so that it will make the context easily available from all controllers, API controllers, classes, and other application components.

Run the following 2 package install commands to get the necessary OWIN NuGet packages:

Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.0.1
Install-Package Microsoft.AspNet.Identity.Owin

Finally we will run the install package command for the Adoxio Connect Framework which will also install the Dynamics 365 SDK.

Install-Package Adoxio.Dynamics.Connect

With all the dependencies installed to the project the next step is to ensure you have done the necessary setup for server-to-server authentication. This includes creating an Azure AD application with the appropriate API access, and creating an application user with a security role in CRM. To learn more about the server-to-server authentication setup follow the MSDN documentation. Follow the same steps for multi-tenant for building this single tenant web application, other than you don’t need to worry about creating a process to deploy the application user as it will be a one time process per Dynamics instance. Use Multi-Tenant Server-to-server authentication

With the server-to-server settings created you will need to add the following settings to the web.config, completing the settings with your values (this are not working values only for sample purposes):

<appSettings>
  <add key="dyn:ClientId" value="1d8925fd-8cbe-4f07-a83f-f59f7b111350" />
  <add key="dyn:ClientSecret" value="ckrtN4TrckIAF1i5ccEcJw+C4/ESfcyjWGBRBI80a3A=" />
  <add key="dyn:Resource" value="https://connectexampleinstance.crm.dynamics.com" />
  <add key="dyn:TenantId" value="ae83bd39-7849-4089-3965-1e5749dc4dc2" />
</appSettings>

With these settings present you can use the default constructor of the CrmContext object, alternatively you can use the other constructors to have your settings loaded from another location like an Azure Key Vault. The Connect Framework also includes a setting manager if you want to save the application settings to a file on disk. The setting manager contains methods both to save and load the 4 application settings from a settings.json file in the App_Data folder.

However you choose to instantiate the CrmContext object we can use OWIN to make it easy to access this object throughout the application. By adding a OWIN Startup class to the application and loading the CrmContext into the OwinContext it becomes available everywhere in the application that the OwinContext can be accessed. To create an OWIN Startup class, right click in the solution explorer on your project name and select Add > OWIN Startup Class.

Within the startup class we want to ensure that the CrmContext object using the default constructor is going to load properly so there is a method in the SettingManager called InitAppSettings that will check it can properly load the settings first. This would allow you to insert other logic here if the settings don’t exist to potentially gather them from the user or throw/route the exception based on your desired application output. Below is a small sample of this check as well as if it succeeds how you can use the OWIN framework to register the CrmContext through dependency injection.

public void Configuration(IAppBuilder app)
{
    if (SettingManager.InitAppSettings())
    {
        app.CreatePerOwinContext<CrmContext>(CrmContext.Create);
    }
    else
    {
        throw new Exception("Adoxio Connect Framework app settings not found.");
    }
}

CreatePerOwinContext will register a callback of CrmContext.Create on the OwinContext and this callback called once per request so that you can access it easily throughout the application. To match the CrmContext constructors there are also CrmContext.Create methods to support you injecting server-to-server application settings easily in this callback format.

Once you have registered the CrmContext on the OwinContext you can easily access it in MVC controllers with the following code:

var context = Request.GetOwinContext().Get<CrmContext>();

For a Web API controller:

var context = HttpContext.Current.GetOwinContext().Get<CrmContext>();

Within both you will need to add using statements for Adoxio.Dynamics.Connect as well as Microsoft.AspNet.Identity.Owin. For the API controller to access HttpContext add a using statement for System.Web.

Both of these statements are using the Get method on the OwinContext, which is initiating the callback that was registered for type CrmContext and making the result available within your controller. Once you have this object returned it will already be set based on the logic within your OWIN startup class. If using the default constructor of CrmContext it will use the app settings to build the connection including the token so you can easily make requests using either the OrganizationWebProxyClient or an OrganizationServiceContext.

With this CrmContext now easily available you can now just build your ASP.NET Web Application as you would build any web application for your requirements. The CrmContext acts as your persistent application database context that is available on every request. To see a full MVC and API controller read examples check out the Git repo for Adoxio.Dynamics.Connect in the samples\Adoxio.Connect.WebApp folder.

Based on how you build applications with the Dynamics 365 SDK you may want to use the XRM Tooling API as well. You can easily add the CrmServiceClient as another type registered on the OwinContext or have this as your only type registered. The CrmServiceClient actual provides a constructor for OrganizationWebProxyClient and because the CrmContext object contains a property of this type it is very easy to use both together.

The rest of this guide is optional if you want to use the XRM Tooling API / CrmServiceClient Continue reading “Building a Dynamics 365 ASP.NET Web App with Adoxio Connect Framework”