Extending Dynamics Marketing Event Management Portals.

In this article, I would like to give you a technical overview of the Event Management Portal which is a part of Microsoft Dynamics Marketing offering. The event management portal is an application specially created for organizing and managing exhibitions, conferences as well as attendees, hotels and other related infrastructure. The goal of this article is to give you a good understanding of all the technical components of this solution. We will review the backend and frontend parts of the application, deployment options, as well as possible ways and methods which you can use to extend the application.  As a prerequisite, you should have a good understanding of Microsoft Portals, Typescript, and Angular. You can also use this article as a starting point and later improve your knowledge by reading more about MSPortal, Angular or Typescript.

Dear reader, please check Event management updates and obsolescence article to be updated on some significant changes in Event Management Portals extensibility options. Portal API is now deprecated, but you can still use Liquid script+Fetch XML+Json response for GET APIs extensibility.

1. Event management portal hosting options

Let’s first review the basic components of the portal and ways you can host it.

The Event Management portal consists of two parts:

Frontend (Angular)
REST API backend (MSPortal + Dynamics CE plugin)

The frontend part is a single page application created using the Angular framework and is fully customizable as source code is provided by Microsoft. According to Microsoft’s documentation backend is only customizable if hosted outside or using special entity.

I would recommend using self-hosted variant only if you need to build really complex backend with some custom authentication and integrations, but for the most cases, you can use out of the box extensibility options or liquid scripts.

More information about this topic you can find at Microsoft Docs: Build and host a custom event portal

2. Event management portal backend.

To better understand how the event management portal backend works internally, let’s review the API request path. So, for example, you’ve opened the event management portal and it requested some event related information from the backend. The picture below shows the API request path from angular frontend to MS Portal and back, let’s see how does it work.

When you are installing Dynamics Marketing application, the installation procedure deploys dedicated MS Portal and creates two different web sites: Event Portal and Marketing portal. Event portal hosts all web pages and process all requests (GET/POST) of the Event management portal.

At first Angular frontend generate request to get some data from the API, then according to the query path it goes to the appropriate web page which represents some API in our case, it is API Events Event. Each API has its own web page, but they all have the same page template which uses the same web template. The web template is using a LIQUID script which processes all API requests. Let’s look deeper into it and try to understand how does it work.

{% assign bypassTokenVerification = true %}
{% assign ssl = false %}
{% if 'on' == request.params["HTTPS"] %}
{% assign ssl = true %}
{% endif %}

{% assign tokenVerified = false %}
{% assign tokenSize = request.params['__RequestVerificationToken'] | size %}
{% if tokenSize == 0 %}
  {% assign tokenVerified = true %}
{% elsif request.params['__RequestVerificationToken'] == request.params["HTTP_VALIDATION"] %}
  {% assign tokenVerified = true %}
{% endif %}

{% if bypassTokenVerification or request.params["REQUEST_METHOD"] == "GET" or tokenVerified %}

{% assign path = request.path %}

{% capture portalCGIRequest %}
{
"WebsiteId" : "{{ website.id | escape }}",
"PageId" : "{{ page.id | escape }}",
"UserId" : "{{ user.id | escape }}",
"Time" : "{{ now | date_to_iso8601 }}",
"Event" : "{{ request.params["event"] | url_encode | escape }}",
"Request" : {
"Path" : "{{ path }}",
"Query" : "{{ request.query  | replace: '&', '+' }}",
"Params" : "#{{ request.params["json"] }}#",
"Token" : "{{ request.params['__RequestVerificationToken'] | default: request.params['Dynamics365PortalAnalytics'] }}",
"Method" : "{{ request.params["REQUEST_METHOD"] }}",
"HeaderToken" : "{{ request.params["HTTP_VALIDATION"] }}",
"IsSSLOn" : "{{ ssl }}",
"ClientIP" : "{{ request.params["REMOTE_ADDR"] }}", 
"ClientHost" : "{{ request.params["REMOTE_ADDR"] }}",
"ExpectJson" : "true"
}
}
{% endcapture %}
{% fetchxml portalcgi %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="true" returntotalrecordcount="true">
    <entity name="msevtmgt_portalcgi">
        <filter>
            <condition attribute="msevtmgt_name" operator="eq" value="PortalCGI:{{ portalCGIRequest | xml_escape }}">
        </condition></filter>
    </entity>
</fetch>
{% endfetchxml %}
{% if portalcgi.results.entities.size &gt; 0 %}
  {{ portalcgi.results.entities[0].msevtmgt_value }}
{% endif %}

{% else %}
{
  "errorMessage": "Token not provided",
  "correlationId": 0
}
{% endif %}

So as you see the script is performing some verifications and then construct a structure called portalCGIRequest. This structure contains all the information which came from a frontend like WebsiteId, Request, Query, Method and so on. Let’s call it request structure. The next step is to execute the fetchXML to retrieve entity called msevtmgt_portalcgi with some condition which contains previously prepared request structure. Of course, there is no data in the system which can be returned according to this condition. The logic of this fetchxml is to execute a special plugin called  PortalCGIPlugin which works for message RetrieveMultiple and to transfer request structure to this plugin. This plugin will parse request structure, perform needed operations in CRM (create/update) and return the result back via the fetchxml response. The result is stored in the msevtmgt_value attribute. After this liquid script prepares json which will be returned to the frontend. Frontend process and render the result of the execution. So all API requests which are coming from the angular frontend are actually served by PortalCGIPlugin plugin.

As we now have a better understanding of how the system serves the requests we can summarize all the ways we can have to extend backend:

1. Fully custom backend. (time-consuming)

2.Via Website Entity Configuration. (following entities supported: Event, Session, Session track, Pass, Event registration, Session registration, Custom registration field, Registration response, Waitlist item, Speaker, Speaker Engagement, Sponsorship, Building, Room, Layout, Building). In case you want to create your custom entity this way would not work, otherwise, the system will return your custom data as a part of the standard API request.

3. Using Liquid scripts functionality of Microsoft Portals. (only to retrieve data)

4. Using Liquid scripts + custom plugin (to retrieve and create data. the same way Microsoft uses to serve event management requests)

3. Event management portal frontend.

The frontend part of event management portal is written using Angular framework and out of the box hosted by MS Portal. You can host it somewhere else if needed. Frontend application consists of five main files: main.js, polyfills.js, runtime.js, scripts.js, styles.css. All these files are hosted as WebFiles (attachments) in Event management web site. To summarize web files are used to host compiled Angular based application.

Let’s now talk about web pages. As you know to host a page using MS Portals you need to create web page configuration. Every page of event management portal has its representation in CRM as a web page. For example, for the Sessions page, we have “Event Details Session Section” web page and so on. However, each web page should have a page template as well as a web template, the same we have for every API. The content of the web template is also equal for all pages you have in Event Management portal. This web template is just requesting Angular application files which finally manage the application lifecycle. As Microsoft is using Angular to render frontend, there is no need to implement logic in MS Portal. MS Portal only hosts Angular runtime which is managing what to do when the user decided to navigate to another page or to submit something. That’s why in case you need to change some logic the only way is to update the source code of Angular application provided by Microsoft, compile it and upload compiled files in MS portal. But still it doesn’t mean that you can’t create your own pages which can be embedded into the application, I’m only talking about the logic which is implemented in Event Management frontend core. Here is how the general web template page looks like:

    <style>
        body { top: 0px !important; }
    </style>
  <meta charset="utf-8">
  <title>Event Management</title>
  <base href="/">
  <meta name="token" content="{{ request.params['__RequestVerificationToken'] }}">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="eventmanagementapp/styles.css">


  <app-root></app-root>
<script type="text/javascript" src="eventmanagementapp/runtime.js"></script>
<script type="text/javascript" src="eventmanagementapp/polyfills.js"></script>
<script type="text/javascript" src="eventmanagementapp/scripts.js"></script>
<script type="text/javascript" src="eventmanagementapp/main.js"></script>

Let’s now talk a bit more about local development environment preparation.

To get more information on how to setup environment please check following link: Build and host a custom event portal. Check Prerequisites section. Let’s assume that you’ve downloaded and extracted the portal frontend source code to the following folder: C:\EventManagement\Front\. To host the event management portal locally you need to setup NodeJs as well as Angular. You can easily find lots of articles regarding the configuration and installation of Angular, so I’m not going to cover it here. Once you’ve downloaded and installed everything we can move forward and configure the local environment.

Below part will partially repeat Microsoft documentation with my comments or remarks.

1. You need to bypass the anti-CSRF token for local development. To do that, you need to go to Dynamics 365 > Portals > Web Templates and open the PortalAPI web template and flip the flag bypassTokenVerification to true.

2. Microsoft documentation says that we also need to configure CORS, but I don’t use it as prefer to launch Chrome in DEV mode. To do this you can create a .bat file with the following content:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --user-data-dir="C:\DevChrome" --disable-web-security --remote-debugging-port=56850

This command will execute Chrome in DEV mode and allows you to easily debug Angular application without any issues. I also define remote debugging port because I prefer to use WebStorm as a development environment and this environment uses port 56850 for debugging data exchange with Chrome.

3. You need to create environment.ts file and configure connection information between your MS Portal and Event Management Portal. Please go to C:\EventManagement\Front\src\environments and create appropriate file. The content of the file should be like the following:

export const environment = {
  production: true,

  apiEndpoint: 'http://yourcrmserver.microsoftcrmportals.com/',

  localizationEndpoint: 'localization/',

  useRestStack: false,

  emApplicationtoken: '',

  useAadB2C: false,

  useMockData: false,

  aadB2CConfig: {
    authorityHost: '',
    tenant: '',
    clientID: '',
    signUpSignInPolicy: '',
    b2cScopes: [],
    redirectUri: ''
  }
};

3. Next step is to install all the dependencies required by the portal. To do this you need to execute the following command in the application directory: npm install.

4 In case you are going to use IE 11, you might also need to configure polyfills. Please find my configuration below, which perfectly works for IE 11. The file is located in C:\EventManagement\Front\src\polyfills.ts

/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10 and IE11 requires all of the following polyfills. **/
 import 'core-js/es6/symbol';
 import 'core-js/es6/object';
 import 'core-js/es6/function';
 import 'core-js/es6/parse-int';
 import 'core-js/es6/parse-float';
 import 'core-js/es6/number';
 import 'core-js/es6/math';
 import 'core-js/es6/string';
 import 'core-js/es6/date';
 import 'core-js/es6/array';
 import 'core-js/es6/regexp';
 import 'core-js/es6/map';
 import 'core-js/es6/weak-map';
 import 'core-js/es6/set';

/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install --save classlist.js`.

/** IE10 and IE11 requires the following for the Reflect API. */
// import 'core-js/es6/reflect';


/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect';


/**
 * Web Animations `@angular/platform-browser/animations`
 * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
 * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
 **/
// import 'web-animations-js';  // Run `npm install --save web-animations-js`.

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 */

 // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames

 /*
 * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
 * with the following flag, it will bypass `zone.js` patch for IE/Edge
 */
// (window as any).__Zone_enable_cross_context_check = true;

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import 'zone.js/dist/zone';  // Included with Angular CLI.



/***************************************************************************************************
 * APPLICATION IMPORTS
 */

5. Now you can execute ng serve to execute the application in development mode.

After executing this command, you should receive the following information:

compiled

Now if everything was properly configured you can access your event management portal using http://localhost:4200/.

4. Extending the Event management portal.

As we now have a better understanding of how the backend works and also properly configured the frontend part we can move forward and learn how to extend the portal. In this part, I will create some simple API using the standard way of configuration and also using Liquid script functionality.

The task is to show some text about the event directly on the main page (ex. Hello dear visitors!). Every time you switch between pages the text should still be shown. This text should be taken from the msevtmgt_expectedoutcome field of the event as this field is not provided by the API out of the box. Following image shows what we want to achieve:

eportal

Actually, this functionality is already in place as you can just fill out description field of the event and this information will be shown above the navigation menu, but my goal is to show you how to extend the portal so that later you can develop much more complex logic.

1. Extending existing API using WebSite entity configuration.

As you know Microsoft provides a standard way of backend API configuration. For this, you just need to configure which fields should be additionally provided via standard API using a special entity. In our case, I want to add a msevtmgt_expectedoutcome field to the standard event API.

This is standard event API request: https://myportal.microsoftcrmportals.com/api/events/event/?readableEventId=Contoso963106518

This is the standard response:

{  
   "customFields":{  

   },
   "allowAnonymousRegistrations":true,
   "allowCustomAgenda":false,
   "autoregisterWaitlistItems":true,
   "building":{  
      "customFields":{  

      },
      "accessibleToilets":false,
      "additionalFacilities":null,
      "addressComposite":"St.-Martin-Straße 106 München 81669 ",
      "addressLine1":"St.-Martin-Straße 106",
      "addressLine2":null,
      "addressLine3":null,
      "city":"München",
      "country":null,
      "description":null,
      "disabledAccess":false,
      "disabledParking":false,
      "email":null,
      "id":"5f057d93-e234-e911-a964-000d3aba03c4",
      "name":"Contoso office",
      "postalCode":"81669",
      "publicTelephoneAvailable":false,
      "stateProvince":null,
      "telephone1":null,
      "telephone2":null,
      "telephone3":null,
      "website":null,
      "wifiAvailable":false,
      "wifiPassword":null,
      "wifiSSID":null
   },
   "description":"hello world",
   "enableCaptcha":false,
   "endDate":"2019-03-24T19:00:00",
   "eventFormat":100000001,
   "eventId":"a200668c-27cd-4c14-b2cb-4c7ae93819df",
   "eventLanguage":null,
   "eventName":"Contoso",
   "eventType":100000001,
   "image":null,
   "maxCapacity":300,
   "publicEventUrl":"https:\/\/yoursite.microsoftcrmportals.com\/event\/?id=Contoso963106518",
   "readableEventId":"Contoso963106518",
   "room":{  
      "customFields":{  

      },
      "description":null,
      "disabledAccess":false,
      "id":"6f9f7999-e234-e911-a964-000d3aba03c4",
      "name":"Conference Center"
   },
   "showAutomaticRegistrationCheckbox":false,
   "showWaitlist":false,
   "startDate":"2019-03-24T08:47:00",
   "timeZone":95,
   "timeZoneName":null
}

As you see it has a special section called “customFields” which doesn’t contain information so far. Let’s add our field to the output.

On the picture below you can see how to configure WebSite entity configuration.

websiteentityconfig

Now the field is added to the API and we can switch to the Angular front-end to use it.

Let’s briefly review the project structure and how it is connected to the different parts of event management portal.

ProjectStructure

In general, every component in the application consists of three parts. TypeScript (Controller) , HTML (View), CSS (View). In case you want to add custom API, you also need to add model which will describe your data structure, and we will implement it in the proper part of this article. The main component of every event is event.component.ts. It also contains an HTML part called event.component.html. HTML part contains the template which uses data coming from the service calls done in ts part as well as a static navigation menu for the event.   TS file contains all service calls and business functions to get information about the event and sessions. Let’s switch into the code and open this file. Here is the path: C:\EventManagement\Front\src\app\components\event\event.component.ts. ngOnInit function of event.component.ts is executed every time the component is initialized, so every time you open new event in the browser.

    ngOnInit() {
        this.route.queryParamMap.subscribe(paramMap => {
            this.readableEventId = paramMap.get(
                EventQueryParameterNames.ReadableEventId
            );

            this.getEventAndSessionData().then(() =>
                this.setIsSessionCartAllowed()
            );
        });

        this.sessionCartService.isOpen = false;
    }

The function is quite simple and it just takes ReadableEventId from the query string and then performs some asynchronous API calls. As we have extended event API let’s check the function which calls this API. The function which is getting Sessions and Event information called getEventAndSessionData.

    private async getEventAndSessionData(): Promise {
        try {
            this.event = await this.activeEventService.getEvent(this.readableEventId).toPromise();

            this.sessions = await this.activeEventService.getSessions(this.readableEventId).toPromise();
        } catch (error) {
            return console.error(error);
        }
    }

As you see we are using event service and calling the getEvent function. Our class has a public member called “event” this member contains all the information about the event once it loaded. I evaluated this member in the debugger so you can check the structure of the event below.

eventeval.png

I’ve highlighted our custom field which we added. You can easily access information in this field by using the following expression this.event.customFields.msevtmgt_expectedoutcome.

As we know how to access our custom field and this field is a public member of our component we can just put this call into our HTML template, and it will render the value of the field.

Below you can find an updated version of event.component.html which uses our custom field.

To get data from our custom field we need to put following code snippet:

<div class="spinner-container" *ngif="isLoadingData">
    <app-spinner></app-spinner>
</div>

<div *ngif="error">
    <app-errormessage [servererrormessage]="error.message" [errormessagetranslationkey]="error.localizationKey">
    </app-errormessage>
</div>

<div *ngif="!isLoadingData &amp;&amp; !error">
    <div *ngif="!isLoadingData" class="container event-banner" [ngstyle]="{ 'background': 'no-repeat center url(' + getBannerImage() + ')', 'background-size': 'cover' }">
        <div class="row">
            <div class="col-md-12">
                <h1 class="section-landing-heading">{{ event.eventName }}</h1>
                <h3 class="event-date">{{ event.startDate | date:'medium' }}</h3>
                <h3 class="event-location">{{ event.building ? event.building.name : '' }}</h3>
            </div>
        </div>
        <div class="row register-container">
            <button *ngif="!isSessionCartAllowed" [routerlink]="['registration']" [queryparams]="{id: event.readableEventId}" class="btn btn-primary btn-lg">
                <span class="fa fa-calendar"></span> <span class="icon-padding" [apptranslate]="'RegisterNow'">Register Now</span>
            </button>
            <button *ngif="isSessionCartAllowed" (click)="registerForAllSessions()" class="btn btn-primary btn-lg">
                <span class="fa fa-calendar"></span> <span class="icon-padding" [apptranslate]="'RegisterNow'">Register Now</span>
            </button>
        </div>
    </div>

    <div class="container event-description-container mt-4">
            {{ event.description }}
    </div>

    <div [hidden]="!event" class="container event-data-container mt-4">
        <ul class="nav nav-tabs nav-justified">
            <li class="nav-item">
                <a [routerlink]="['/event', EventDetailSections.Sessions]" [queryparams]="{ id: readableEventId}" [apptranslate]="'Sessions'" routerlinkactive="active" class="nav-link">Sessions</a>
            </li>
            <li class="nav-item">
                <a [routerlink]="['/event', EventDetailSections.SessionTracks]" [queryparams]="{ id: readableEventId}" [apptranslate]="'SessionTracks'" routerlinkactive="active" class="nav-link">Session
                    Tracks</a>
            </li>
            <li class="nav-item">
                <a [routerlink]="['/event', EventDetailSections.Speakers]" [queryparams]="{id: readableEventId}" [apptranslate]="'Speakers'" routerlinkactive="active" class="nav-link">Speakers</a>
            </li>
            <li class="nav-item">
                <a [routerlink]="['/event', EventDetailSections.PassInformation]" [queryparams]="{id: readableEventId }" [apptranslate]="'PassInformation'" routerlinkactive="active" class="nav-link">Pass
                    Information</a>
            </li>
        </ul>
        <br><b>{{ this.event.customFields.msevtmgt_expectedoutcome }}</b>
        <div class="mt-4">
            <router-outlet></router-outlet>
        </div>
    </div>


<app-sponsors [readableeventid]="readableEventId"></app-sponsors>

<aside>
    <app-session-cart [readableeventid]="readableEventId"></app-session-cart>
</aside>

</div>

Once you’ve started your local version of the portal using “ng serve” the platform will automatically run recompilation procedure for all project related files in case of modification, so there is no need to restart the application.

2. Create your own API using the Liquid script.

In this part, we will implement the same logic we did in the previous part, but using our own API. To create and use API you need to implement the following steps:

1. Request needed data from the CRM system using some conditions. (In our case we need to get msevtmgt_expectedoutcome from the particular event).

2. Convert this result into JSON and return it back the request initializer.

3. Update frontend to call newly created API as well as process and render results.

First two steps we can easily implement using liquid scripts as we can process GET requests and also execute fetchxml requests with parameters from the query string. For the third step we, of course, will need to update Angular frontend.

Let’s start and create the Liquid script. At first, you need to create the following hierarchy of pages and templates in your Portal:

apihierarchy.png

As you see on the root level we have Event Portal, then existing API events web page to host our API under events path. Information that we are going to provide is a part of the event, so that let’s place our API as a child of event API.

The path to our API will be: https://yourporal.microsoftcrmportals.com/events/expectedoutcome

Here are the entities which I’ve created:

Web page:

webpage

Page template:

pagetemplate.png

Web template:

webtemplate.png

Web template should contain the following liquid script:

{% assign readableEventId = request.params["readableEventId"] | url_encode | escape %}
{% if request.params["readableEventId"] %}
    {% fetchxml crmRequest %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
    <entity name="msevtmgt_event">
     <attribute name="msevtmgt_eventid">
     <attribute name="msevtmgt_expectedoutcome">
     <filter type="and">
       <condition attribute="msevtmgt_readableeventid" operator="eq" value="{{ readableEventId | xml_escape }}">
     </condition></filter>
    </attribute></attribute></entity>
</fetch>
    {% endfetchxml %}
    {% assign entities = crmRequest.results.entities %}
    {% if entities.size &gt; 0 %}
      {
        "expectedoutcome":"{{entities[0].msevtmgt_expectedoutcome}}"
      }
    {% endif %}
{% endif %}

As you see the script is quite simple. We just check that we have needed parameter and execute fetchxml which is looking for our event and return needed attribute, then we go through the results, normally we should have only one, so we take element zero from the array and return it as JSON.

Our API request path: https://yourwebsite.microsoftcrmportals.com/api/events/expectedoutcome/?readableEventId=Contoso963106518

API response: { “expectedoutcome”:”hello my visitor” }

Now as we finished with API creation we can switch to the frontend part and implement logic to request the data.

At first, let’s create a simple model which can be used to store information. All models are located in C:\EventManagement\Front\src\app\models.

You can create a new file called ExpectedOutcome.ts.

Now let’s add our small model:

export interface Outcome {
   expectedoutcome: string;
}

Next part is to create an interface for our service call. FrontEnd needs interface because we might use different implementations of connections. As we are going to extend the list of interfaces for Event API, so let’s go to C:\EventManagement\Front\src\app\services and open event.service.ts.

We need to reference model and also add one more function template which we will implement and later call from our component. Please find the function below:

getExpectedOutcome(readableEventId: string): Observable;

So we are looking for a parameter readableEventId and return the Outcome.

Please find below the full version of the file.

import { Captcha } from './../models/Captcha';
import { FinalizeRegistrationRequest } from './../models/FinalizeRegistrationRequest';
import { RegistrationResult } from '../models/RegistrationResult';
import { Sponsorship } from '../models/Sponsorship';
import { Speaker } from '../models/Speaker';
import { SessionTrack } from '../models/SessionTrack';
import { Session } from '../models/Session';
import { Outcome } from '../models/ExpectedOutcome';
import { Pass } from '../models/Pass';
import { Event } from '../models/Event';
import { Observable } from 'rxjs';
import { RegistrationData } from '../models/RegistrationData';
import * as CustomRegistrationFieldModel from '../models/CustomRegistrationField';

export interface EventService {

    getExpectedOutcome(readableEventId: string): Observable;

    getPublishedEvents(): Observable<event[]>;

    getEvent(readableEventId: string): Observable;

    getPasses(readableEventId: string): Observable<pass[]>;

    getSessions(readableEventId: string): Observable<session[]>;

    getSessionTracks(readableEventId: string): Observable<sessiontrack[]>;

    getSpeakers(readableEventId: string): Observable<speaker[]>;

    getSponsors(readableEventId: string): Observable<sponsorship[]>;

    getCaptcha(readableEventId: string): Observable;

    getCustomRegistrationFields(readableEventId: string): Observable<customregistrationfieldmodel.customregistrationfield[]>;

    getEventRegistrationCount(readableEventId: string): Observable;

    registerToEvent(readableEventId: string, registrationData: RegistrationData): Observable;

    finalizeRegistration(readableEventId: string, requestData: FinalizeRegistrationRequest): Observable;

    registerToSession(readableEventId: string, sessionId: string): Observable;
}

</customregistrationfieldmodel.customregistrationfield[]></sponsorship[]></speaker[]></sessiontrack[]></session[]></pass[]></event[]>

Now we also need to implement our function in both versions of existing connectors. Connectors are located in folders “d365” and “rest”. Let’s start with “d365” as we are going to use this connector later. Open the event.d365.service.ts file. Here is the path: C:\EventManagement\Front\src\app\services\d365\event.d365.service.ts. This file contains implementations of all functions for events API.

Check the implementation of our function below:

public getExpectedOutcome(readableEventId: string): Observable {
return this.http.get(
`${environment.apiEndpoint}${EventD365Service.eventsEndpoint}/expectedoutcome/?readableEventId=${readableEventId}`
);
}

As you see it looks quite simple. Don’t forget to add a reference to our model. You also need to implement this function for Rest service otherwise it would not be possible to compile the app. You can just create blank function which throws some exception.

Once you’ve done with services, we can switch to our event.component.ts file and update it to request additional information from our API. Again we need to add a reference to our model and also add the public member to the component so that we can use it in our HTML part. Of course, we need now to call our function. Let’s do it directly from getEventAndSessionData().

Please find below how the function looks like:

private async getEventAndSessionData(): Promise {
try {
this.event = await this.activeEventService.getEvent(this.readableEventId).toPromise();
this.outcome = await this.eventService.getExpectedOutcome(this.readableEventId).toPromise();
this.sessions = await this.activeEventService.getSessions(this.readableEventId).toPromise();
} catch (error) {
return console.error(error);
}
}

You might ask why don’t we use activeEventService and why do we need it, well, this service also uses eventService internally but in addition, does some cashing. As I don’t want to make things complicated for now we will avoid using it.

Once you’ve done the app should successfully be recompiled and then you can check network requests and see that browser now uses our API to request some additional data.

chromedeb.png

The next step is just to add received outcome to the HTML, but this is quite simple. The only thing you need is to put {{outcome.expectedoutcome}} to the right place in HTML part of your component.

In case you want to use custom entities don’t forget to configure portal entity permissions and enable change tracking.

entitypermissions.png

changetrack.png

5. Deployment

To deploy your changes you can use the deployment script located in C:\EventManagement\Front\Scripts. Or compile everything manually using the following command ng build –prod –output-hashing none. This command will generate compiled files in C:\EventManagement\Front\dist\ClientApp. The only thing you need to do is to change the extension of JS files to Es, as you can’t add JS file as attachments and replace appropriate Web Files in your MS Portal.

6. Finish 🙂

I hope you got the idea of how to extend event management portals and can now provide estimations for such projects. It is not that simple to customize it, but if you understand the idea of how things work you can easily implement such projects and handle complex requirements from your customers. Happy implementations!

5 thoughts on “Extending Dynamics Marketing Event Management Portals.

  1. @Pavel: Could you guide to the Pt 4 which is not covered in this blog

    4. Using Liquid scripts + custom plugin (to retrieve and create data. the same way Microsoft uses to serve event management requests)

    What we are trying to achieve is – Update scenario for Registration (e.g. user registers for an event and 2 sessions, provided T-shirt size as S, Now he wants to update and register for existing session + update T-shirt size to XL).
    OOB the Portal creates a new Registration (as the POST does not accept a nullable Registration id parameter, which should ideally solve this problem)

    1. Hi!

      For your scenario, it is better to create some plugin that will detect the creation of msevtmgt_eventregistration and merge it with a previously existing record. You should do some research on this, but it should not be that complicated. When the system creates msevtmgt_eventregistration there should be a contact and event, so you can define if this person has already been registered and now you just receive an update of previously created record.

      Another way is to create an entirely custom registration procedure, as you mentioned type 4. You can implement it the same way as Microsoft did. Create a plugin that will be executed on some fetchxml requests. Create a Liquid script that will run this fetchxml request and transfer all parameters which came from the Angular framework. Call this page from Angular frontend (you need to change the standard registration procedure in Angular front-end and replace it by yours). I would say this one is quite complicated, and also Microsoft doesn’t recommend to use synchronous plugins to execute server side-code directly from the portal.

      Microsoft currently created Event API 2, which is external service, as I see.

Leave a Reply

Your email address will not be published. Required fields are marked *