Microsoft Power Pages - Importing JS Libraries

 INTRODUCTION

Microsoft Power Pages is a platform for developing web sites - from very simple... to surprisingly complex.  However, with additional complexity comes the need to leverage external JavaScript libraries.  We'll be taking a look at that in this brief article.

ASSUMPTIONS

This article will be most useful to you if you already have some working knowledge of

  • Power Pages platform
    • Creating a Site and Adding Pages/Components
  • HTML
    • Specifically, the Script tag
  • Portal Management
    • All of the information surrounding a Power Pages site is stored in a D365 environment and that data is accessible through the D365 Portals application
SCENARIO

We have a web page on our site that needs to include an HTML editor.  There are are many (paid and free) great editors out there and we'll be using the Jodit editor in this example.

HOW TO

To implement a Jodit editor on a web page, we need to import the Jodit CSS and JavaScript files.  Once we have those libraries, we can then properly surface a nice editor.

SOME CUSTOM HTML

The Jodit editor requires an HTML textarea control that it transforms into a nice HTML editor.  We started by adding a 1 column section to the Page as a place to put the textarea and then inserted the markup there.  Here's the HTML for the textarea, including the placeholder section added by the platform:

 <div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">  
  <div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">  
   <div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 310px; word-break: break-word;">  
    <div class="mb-3" id="body">  
     <label for="editor">Dashboard Message</label>  
     <textarea id="welcomeContent" style="width:100%;"></textarea>  
   </div>  
   </div>  
  </div>  
 </div>  

METHOD 1 - ADD FILES FROM PORTAL MANAGEMENT

In this approach, we'll link to the Portal Management application by clicking the ellipses on the Power Pages left nav menu.  This will open a new browser Tab.

There is a great write up on this approach, so I'm not going to recreate the wheel


Follow this process for the files needed (CSS/JS) for the library you need.

METHOD 2 - ADD FILES DYNAMICALLY FROM PAGE JAVASCRIPT

This approach is equally effective but has the advantage of being able to consume files directly from CDN or other global providers without having a local copy of the files.  If the library you need is not available from a CDN, the first method will need to be used.

For the Page in question, 
  • click the Edit code button to open Visual Studio Code for Web.
  • click the JavaScript file on the left explorer panel
    • this loads the file in the editor
  • The following code will dynamically load the Jodit JS/CSS from the CDN
  • This code will initialize the editor

   let welcomeEditor = null;  
   
   var joditCSS = document.createElement("link");  
   joditCSS.rel = "stylesheet";  
   joditCSS.type = "text/css";  
   joditCSS.href = "//cdnjs.cloudflare.com/ajax/libs/jodit/3.19.3/jodit.min.css";  
   document.getElementsByTagName("head")[0].appendChild(joditCSS)  
   
   let joditScript = document.createElement("script");  
   joditScript.type = "text/javascript";  
   joditScript.src = "//cdnjs.cloudflare.com/ajax/libs/jodit/3.19.3/jodit.min.js";  
   document.head.appendChild(joditScript);    
   
   document.addEventListener("DOMContentLoaded", function(){  
     console.log("initRegistrations started");  
     document.getElementById('email').addEventListener(  
       'change',   
       email_Change  
       );   
      
     document.querySelectorAll('.fldRequired').forEach(c => {  
       c.addEventListener('change', event => {  
         validateRequired(c);  
       });  
     });  
     
     setTimeout(() => {  
       document.querySelectorAll('.fldRequired').forEach(c => {  
         validateRequired(c);  
       });  
     }, 1000);  
   
       welcomeEditor = Jodit.make("#welcomeContent");  
   
   });  

THE RESULT

With all of this in place, you should get a page that renders the Jodit editor as expected





SUMMARY

Both methods effectively import the libraries you specify and the method you choose will likely be based on the answer to some basic questions
  • Is this library used by other pages?
    • Get a local copy and use Method 1
  • Is this library available from a CDN
    • Consider Method 2
  • Are the timing issues with the load of the files?
    • Use Method 1

Microsoft Power Pages - Roll Your Own UI

INTRODUCTION

As part of getting my feet wet with Power Pages, I'm creating a simple site that does most of things I'd expect a site might need to do.  The more I play with the platform, the more potential I see for creating sites that range in complexity from simple to sophisticated.  This is a GREAT feature - make simple things simple to do and make difficult things possible.  

In this post, I'm going a little deeper with a discussion of bypassing the platform's built-in integration with Dataverse to performing customizations that let you roll your own UI and talk directly with SQL Server...  no Dataverse required!

ASSUMPTIONS

This article will be most helpful if you have some familiarity with the following concepts

  • HTML
    • used to create our custom UI
  • CSS (just a little in-line styling)
    • used  to style the UI
  • JavaScript
    • used to manipulate the HTML DOM
    • and call our Power Automate Flow Endpoints
  • Power Automate Flows
    • to get data from the UI and send it to SQL Server
      • Email validation
      • New user registration creation
    • but you could use any API you like
  • Power Pages
    • we won't be explaining the basics of creating a site and adding Pages/Components
  • Bootstrap (ugh... 3.x)
    • used to style the pages
    • used to implement a Modal message box
SCENARIO

As shown in a previous post, we have a simple site that promotes a software product.  On the Home page, there is an immediate call to action: Register.  The Registration page 

  • Prompts for basic information
  • Validates a non-duplicated email address
    • using a Power Automate Api call
  • Creates the new User in the database
    • using a Power Automate Api call
      • Adds the user
      • Assigns appropriate roles
      • Sends a confirmation email with Quick Start attachment
  • Uses a Bootstrap Modal 
    • for Validation/Api messages
LET'S DO IT - QUICK POWER AUTOMATE FLOW OVERVIEW

The Email Validation Flow

This flow is simple enough, it receives a JSON payload with an "email" property for the value to be checked for duplicates.  It returns an empty array (if no duplicates are found) or an array of the duplicates.


The Create Registration Flow

This flow receives a JSON payload with the registration data and creates the new user record as well as appropriate roles for the user.  It then sends a confirmation email with a QuickStart.pdf attachment.


In the remainder of this article, we'll refer to these two flows as the API endpoints being called when we need to do some database interactions.

A PEEK AT A POWER PAGE

Every page that is added to a Power Pages site has 3 important files that define how the page looks and works:
  • HTML file
    • defines the layout and elements on the page
    • populated with HTML markup as you add elements (like text, images, etc) to the page
  • CSS file
    • defines the styling of the page and elements
    • empty by default
  • JavaScript file
    • defines behaviors
    • empty by default
You access these 3 files by clicking the Edit code button on the page Designer to open Visual Studio Code for Web in a new browser tab


It will look something like this.  When you select a file, as shown below, that file will be loaded into the VS Code editor for you to make any changes you like


If you've done any web development, your eyes should be getting wide at this point.  With access to these files, you can customize a site page to do lots of things that you couldn't otherwise accomplish.... like creating a custom Registration form that interacts with SQL Server through an API....  And that's exactly what we'll be showing next!

ADD A SECTION FOR THE FORM

The Registration page we started with a simple page that we laid out using the out-of-box tools - it's just a couple of text controls and an image.  We then used the Add Section feature to create a new section under the page header.

When you add a new section, you are prompted to select an element to put in the section - we didn't do this!  The result was a placeholder in the HTML markup that we used to contain our custom form.  In the HTML file, the section started with these tags:


This gives us a perfect place to put our HTML for the Registration fields and submit button.  After the custom HTML changes (shown below), it will look like this



THE CSS FILE

For this exercise we did mostly in-line styling; however, we did add the following to the empty CSS file:

 label {  
   text-transform: uppercase;  
   font-weight: 600;  
   font-size: 80%;  
 }  
   
 .fldRequired {  
   border-left: 5px solid red;  
 }  
   
 .fldRecommended {  
   border-left: 5px solid yellow;  
 }  
   
 .fldValidated {  
   border-left: 5px solid green;  
 }  
The label simply standardizes how the field labels look.  The 3 classes help with making required field validations visually effective.

THE HTML FILE

In the placeholder section mentioned above, we added the markup for the fields used to gather Registration information from the visitor and a DIV at the bottom for the Bootstrap modal.  The full HTML markup is shown here and the customized sections are commented:

 <div data-component-theme="portalThemeColor3" class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px;">  
  <div class="container" style="display: flex; flex-wrap: wrap;">  
   <div class="col-md-6 columnBlockLayout" style="flex-grow: 1; min-width: 310px; word-break: break-word; display: flex; flex-direction: column; margin: 25px 0px; padding: 16px;">  
    <div class="row sectionBlockLayout" style="display: flex; flex-wrap: wrap; padding: 8px; margin: 0px; min-height: 15px;"></div>  
    <div class="row sectionBlockLayout" style="display: flex; flex-wrap: wrap; padding: 8px; margin: 0px; min-height: 15px;"></div>  
    <h3 style="text-align: left;">Registration</h3>  
    <p style="text-align: left;">Want to see what iQueue can do for you?&nbsp; Take a few seconds to register and you'll get an email with login information and a Quick Start guide so that you can kick the tires.&nbsp; The "demo" site is fully functional and waiting for you - elevate your Process Management today!</p>  
   </div>  
   <div class="col-md-6 columnBlockLayout" style="flex-grow: 1; min-width: 310px; word-break: break-word; display: flex; flex-direction: column; margin: 25px 0px; padding: 16px;"><img src="/Registration/register.jpg" alt="" name="register.jpg" style="width: 100%; height: 295px; max-width: 100%; box-shadow: 0px 0px 6px rgb(0, 0, 0); border-radius: 5px;" /></div>  
  </div>  
 </div>  
 <div class="row sectionBlockLayout text-left" style="display: flex; flex-wrap: wrap; margin: 0px; min-height: auto; padding: 8px; background-image: linear-gradient(0deg, rgba(161, 159, 157, 0), rgba(161, 159, 157, 0)); border: 0px solid rgb(161, 159, 157); border-radius: 0px;">  
  <div class="container" style="padding: 0px; display: flex; flex-wrap: wrap;">  
   <!--the customized form section-->  
   <div class="col-md-12 columnBlockLayout" style="flex-grow: 1; display: flex; flex-direction: column; min-width: 310px; word-break: break-word; margin: 20px 0px; padding: 16px;">  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="firstName">First Name</label><input type="text" id="firstName" class="form-control fldRequired" /></div>  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="lastName">Last Name</label><input type="text" id="lastName" class="form-control fldRequired" /></div>  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="email">Email</label><input type="text" id="email" class="form-control fldRequired" /></div>  
    <div class="mb-3" style="margin-bottom: 15px;"><label for="company">Company</label><input type="text" id="company" class="form-control" /></div>  
    <div class="mb-3"><button onclick="submitRegistration()" class="btn btn-primary">Submit</button></div>  
   </div>  
   <!--end customized form section-->  
  </div>  
 </div>  
 <!--error modal-->  
 <div class="modal fade" id="errorModal" tabindex="-1" role="dialog">  
  <div class="modal-dialog" role="document">  
   <div class="modal-content">  
    <div class="modal-header">  
     <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>  
     <h4 class="modal-title" id="errorModalLabel">...</h4>  
    </div>  
    <div class="modal-body" id="errorModalBody">  
     ...  
    </div>  
    <div class="modal-footer">  
     <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>  
    </div>  
   </div><!-- /.modal-content -->  
  </div><!-- /.modal-dialog -->  
 </div><!-- /.modal -->  

When you break this down, you see that there are 2 divs
  • a Data Entry div
    • we use this to gather the data from the visitor and then pass it along to the API endpoints
  • a Bootstrap Modal div
    • for messaging
It looks like this when the custom HTML is added




THE JAVASCRIPT FILE

The following JavaScript code provides the functionality we're looking for.  There is nothing too fancy here but it does include some required field validation and the accompanying visual styling manipulations.  
   console.log("initRegistrations started");  
   document.getElementById('email').addEventListener(  
     'change',   
     email_Change  
     );   
    
   document.querySelectorAll('.fldRequired').forEach(c => {  
     c.addEventListener('change', event => {  
       validateRequired(c);  
     });  
   });  
   
   setTimeout(() => {  
     document.querySelectorAll('.fldRequired').forEach(c => {  
       validateRequired(c);  
     });  
   }, 1000);  
     
     
     
   function email_Change(){  
     //some code here to see if that email is in play already  
     console.log("email_Change executed");  
     let payload = {};  
     payload.email = document.getElementById('email').value;  
   
     if (payload.email.length === 0){return;}  
   
     let url = "https://prod-84.westus.logic.azure.com:443/workflows/abc/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=123";  
     executeApiRequest(payload, url,   
       function(retVal){  
         if (retVal.length > 0){  
           document.getElementById('email').value = "";  
           validateRequired(document.getElementById("email"));  
           popModal("Duplicate Email", "The email you entered is already in use - please enter a different email address.");  
         }  
       },   
       function(error){},   
       true);  
   
   }  
   
   function executeApiRequest (parameters, url, successCallback, errorCallback, async) {  
     let uri = url;  
     let req = new XMLHttpRequest();  
     req.open("POST", uri, async);  
     req.setRequestHeader("Accept", "application/json");  
     req.setRequestHeader("Content-Type", "application/json; charset=utf-8");  
     req.setRequestHeader("Cache-Control", "no-cache'");  
   
     if (async) {  
       req.onreadystatechange = function () {  
         if (this.readyState === 4 /* complete */) {  
           req.onreadystatechange = null;  
           if (this.status === 200) {  
             let data = JSON.parse(this.response, dateReviver);  
             successCallback(data);  
           }  
           else {  
             errorCallback(this.responseText);  
           }  
         }  
       };  
       parameters ? req.send(JSON.stringify(parameters)) : req.send();  
     }  
     else {  
       parameters ? req.send(JSON.stringify(parameters)) : req.send();  
       if (req.status === 200 || req.status === 204) {  
         if (req.status === 200) {  
           let data = JSON.parse(req.response, dateReviver);  
           successCallback(data);  
         }  
       }  
       else {  
         errorCallback(this.responseText);  
       }  
     }  
   }  
   
   function dateReviver (key, value) {  
     if (typeof value === 'string') {  
       let a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);  
       if (a) {  
         return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));  
       }  
     }  
     return value;  
   }  
   
   function validateRequired(c) {  
   
     if (c.value.length > 0) {  
       c.style.borderLeft = "5px solid green";  
     }  
     else {  
       c.style.borderLeft = "5px solid red";  
     }      
   }  
   
   function submitRegistration() {  
     console.log("submit button clicked");  
       
     let reg = {};  
     reg.FirstName = document.getElementById("firstName").value;  
     reg.LastName = document.getElementById("lastName").value;  
     reg.Email = document.getElementById("email").value;  
     reg.Company = document.getElementById("company").value;  
   
     let updateOk = true;  
   
     document.querySelectorAll(".fldRequired").forEach(c => {  
       if (window.getComputedStyle(c).borderLeft.indexOf("red") >= 0 ||  
         window.getComputedStyle(c).borderLeft.indexOf("255") >= 0) {  
         updateOk = false;  
       }  
     });  
   
     if (!updateOk){  
         
       popModal("Validation", "Fields marked in RED are required")  
       return;  
     }  
    
     let url = "https://prod-176.westus.logic.azure.com:443/workflows/abc/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=123";  
     //validations passed... create the registration record  
     executeApiRequest(reg, url,   
       function(retVal){  
         popModal("Success", "Registration Succeeded!<br/><br/>Be sure to check for the Confimration email.<br/>Stand by for automatic navigation to Home page.");  
         setTimeout(function(){  
           window.location.href = "https://iqueue.powerappsportals.com/";  
         }, 10000);  
       },   
       function(error){},   
       true);  
   
   }  
   
   function popModal(title, body){  
   
     document.getElementById("errorModalLabel").innerHTML = title;  
     document.getElementById("errorModalBody").innerHTML = body;  
     $('#errorModal').modal('show');  
   
   }  

  • When the Registration page loads
    • add an Event Listener to the email field so that we can fire the function to check for duplicates
    • add an Event Listener to all required fields so that we can provide the visual cues
    • do a validation to set the required field visual (the red left border)
  • email_Change
    • sends the email value entered by the user to the Email Validation Flow and processes the result
  • executeApiRequest
    • a reusable XmlHttpRequest function to call the API Flows
  • dateReviver
    • a help for executeApiRequest to generate friendly date formats
  • validateRequired
    • sets the left border for required fields
  • submitRegistration
    • gathers the Registration information and sends it to the Create Registraion Flow
  • popModal
    • populates and displays the notifications div

OF INTEREST - BOOTSTAP INTEGRATION

Microsoft includes BOTH Bootstrap CSS and Bootstrap Bundle JS.... so you can do a lot of cool things in the UI natively.  However, this is a 3.x version of the Bootstrap stack and it definitely works a bit differently than 4.x and 5.x versions.

An example of a Modal Bootstrap message



WRAP-UP

Power Pages gives you the flexibility to keep things simple when they are simple.... or to go hog wild when you need elevated levels of complexity.  

Microsoft Power Pages - Calling a RESTful API

 INTRODUCTION

Microsoft has put a nice, new front end on their "Portals" technology... and they now call it "Power Pages".  This is really a great step forward because it abstracts much of the complexity that underlays the Portals platform.  The new UI gives you a "point-and-click" designer with (limited) access to properties of the elements you add to a page.  If you are a pro web designer, you'll find it limiting...  otherwise, you'll be amazed that you can create a nice web site without having to know much at all about web development (assuming your site doesn't have do much other than support site navigation).

WHAT WE WON'T COVER

It's not the purpose of this article to cover in detail the creation of a RESTful Api endpoint.  For the purposes of this article, we created a simple Power Automate triggered by an HTTP Request.  This process generates an Endpoint Url which we then used in our XmlHttpRequest.

WHAT WE ASSUME

We won't be talking about the basics of getting into Power Pages and creating a site and adding Pages or Components...  This article isn't for you if you haven't yet covered those basics.  Although we do discuss the Dataverse in this article, it would be helpful if you are already at least a little familiar with what it is and how to work with tables, fields, views and forms.

SOME OF THE NICE STUFF in POWER PAGES

  • Templates
    •  A variety...  just to get you up and running
  • Page Layout
    • Based on Bootstrap and very easy to work with
  • Elements
    • Every kind of thing you'd want to add to a page... including IFrames, Images, Videos, etc
  • Authentication
    • If you want restrict access to your site, Power Pages lets you easily implement one of any number of Authentication methods including:
      • Azure AD
      • Google
      • Facebook
      • Twitter
      • Ad Hoc (Forms based)
      • Microsoft Account
      • Etc
    • Or... no Authentication required
      • Which makes your site publicly available
  • Customization
    • Per Page, you can
      • add your own JavaScript
      • add your own CSS
      • customize the HTML
    • Per Site, you can
      • Associate your site with a custom Domain :)
  • Dataverse Integration
    • When you have to work with data, seamless integration with the Dataverse makes it pretty simple
These are just some of the highlights....  you can dig in yourself by starting a free trial of the platform: https://powerpages.microsoft.com

OUR SCENARIO

We have a page that asks a visitor to Register.  We want to make sure that they are entering a unique Email address based on Email addresses in the existing Registration table.  

OUR SIMPLE SITE 

We start our site with a simple Home page 


The Home page includes a "call to action" to register.  When a visitor clicks that button, the Registration page is opened


We're not asking for much here...  but we do want to make sure that the email address provided is unique within our Registration table in the Dataverse.  In this case, we'll check the email BEFORE the form data is submitted.

HOW-TO OVERVIEW

To accomplish our goal, we're going to need to add some custom JavaScript to our Registration page.  In the Designer, every page includes a link to "Edit code" near the top-right of the designer canvas


When you click the Edit code button, a new browser tab opens with Visual Studio Code for Web and any resources associated with the current page available to edit (html, js, css).



The html file will be populated with the markup for your page - which you can edit!  The js and css files will be empty - just waiting for your brilliant customizations.  The changes you make here will only apply to the specific Power Page loaded in the Designer when you clicked the Edit code button.

Now that we know how to add some custom JavaScript....  exactly what JavaScript to we need to add?

  • We need to be able to run a validation process when the value in the Email field changes
    • So we need to add an event listener to the Email field
    • Fortunately, a Power Page form based on a Dataverse table gives us easy accessibility to each field on the form 
  • That event listener, when triggered, will pass the Email value to a REST Api endpoint
    • This will be done with XmlHttpRequest in JavaScript
    • the Endpoint gets a list of any records in our Registration table and passes that back to the Power Page
  • The Power Page checks the value returned by the REST Api and responds accordingly

A WORD OR TWO ABOUT DATA

When we created the Registration form (see above), we designated a Dataverse table for the form to be based upon.  In fact, you can ONLY create a form based on a Dataverse table.  The Dataverse table and it's related Forms and Views come into play here.  When you create the form in the Designer, you have to specify the table and form from the Dataverse.  This way, Power Pages has somewhere to put the data AND a reference for what the form should look like.  All of this can be done from the Designer.  Just be aware... you don't get to lay out your form in the Designer.  You create the form layout as part of the Dataverse table definition... and Power Pages renders the form you designate.


The image above shows the Designer in Data mode with our Registration table/form selected.  As shown, this form is what is eventually rendered by the Registration Power Page.

THE CODE

With our Registration page selected in the Designer, we click the Edit code button to open Visual Studio Code for Web.  Click on the JS file to open the empty editor.

The first snippet we need is the Event Listener for the Email field.  In our case, the ID for the Email field is cr2d2_email (which is the logical name for this field in the Dataverse).  

   document.getElementById('cr2d2_email').addEventListener(  
     'change',   
     email_Change  
     );   
This is reasonably straight-forward.  We're hooking a call to a function called "email_Change" when the Email field value changes.

Now we need the "email_Change" function.  This function will get the value for Email entered by the visitor and then pass that to a function to make the REST Api call.

   function email_Change(){  
     let payload = {};  
     payload.email = document.getElementById('cr2d2_email').value;  
   
     if (payload.email.length === 0){return;}  
   
     executeApiRequest(payload,   
       function(retVal){  
         if (retVal.length > 0){  
           document.getElementById('cr2d2_email').value = "";  
           alert("That email is alreday in use.... please select another.");  
         }  
       },   
       function(error){},   
       true);  
   
   }  
As shown, we create a payload object and populate a property called "email" with the value from the form.  This is specific to the structure of our Api - the Api expects to receive this exact payload.  If Email is blank, we exit.

All good so far... but now we need a couple of functions to do the XmlHttpRequest part of the process.  As shown above, the function we need to create next is called "executeApiRequest".  That function will in turn call a helper function called "dateReviver" which helps to format dates in a friendly way.

   function executeApiRequest (parameters, successCallback, errorCallback, async) {  
     let uri = "https://prod-84.westus.logic.azure.com:443/workflows/xxxxxxxxxxxxxxxxxxxxx11111111abc999/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=uLAnPelNPd2RvVwCbri7j9ctQnwPO7ev54l6yU9UJ2M"; //path to api goes here  
     let req = new XMLHttpRequest();  
     req.open("POST", uri, async);  
     req.setRequestHeader("Accept", "application/json");  
     req.setRequestHeader("Content-Type", "application/json; charset=utf-8");  
     req.setRequestHeader("Cache-Control", "no-cache'");  
   
     if (async) {  
       req.onreadystatechange = function () {  
         if (this.readyState === 4 /* complete */) {  
           req.onreadystatechange = null;  
           if (this.status === 200) {  
             let data = JSON.parse(this.response, dateReviver);  
             successCallback(data);  
           }  
           else {  
             errorCallback(this.responseText);  
           }  
         }  
       };  
       parameters ? req.send(JSON.stringify(parameters)) : req.send();  
     }  
     else {  
       parameters ? req.send(JSON.stringify(parameters)) : req.send();  
       if (req.status === 200 || req.status === 204) {  
         if (req.status === 200) {  
           let data = JSON.parse(req.response, dateReviver);  
           successCallback(data);  
         }  
       }  
       else {  
         errorCallback(this.responseText);  
       }  
     }  
   }  
   
   function dateReviver (key, value) {  
     if (typeof value === 'string') {  
       let a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);  
       if (a) {  
         return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));  
       }  
     }  
     return value;  
   }  
We created a simple Power Automate triggered by HTTP Request.  The Uri that you see is generated by Power Automate when saved.  This "flow" accepts the payload (email address) and queries the Registration table for any rows with the same email...  then returns the results.  The Power Page evaluates the return of the Async call and then displays an Alert if any matching rows are returned.  When the Alert is dismissed, the Email field is cleared.

WRAP UP

When the code snippets have been added, save the changes by clicking the Visual Studio Code menu button (far upper left) and selecting Save.  When you close the Visual Studio Code tab, you are prompted by the Power Page Designer to Sync... do so.  You can now test your page!  

Squel - A JavaScript SQL API


 A project I'm currently working has a requirement to enable ad-hoc query capabilities.  This means I have to create a GUI that allows a user to declare the information they want and convert that declaration into something my data storage (SQL Server, in this case) understands.  GUI to SQL expression.

Although it can be a challenge to conceive and build a GUI - the more difficult part is to accurately convert the values in the GUI to a SQL statement.  In past projects, I have simply done a ton of string manipulations to get to the end result, and that works okay... until I want to add capabilities.... then the string refactoring begins.

Squel is a JavaScript API that removes almost all of the tedium from generating valid SQL.... including a "flavour" option to target MS Sql or MySql family databases.

Where to get Squel

https://hiddentao.github.io/squel/

NOTE

The author has archived the library.  You will have the full source code - but you should take care to make sure that your implementation works as desired.  If not, your only recourse is to tweak the library.

How I Implemented Squel

My first step was to create a set of Views in the database against which my Advanced Find tool will query.  This afforded me the opportunity to 

  • Align table fieldnames more closely with the application UI and leverage the user's familiarity with the UI to more intuitively go after data....  i.e. the names of things they see in the GUI match the names they are used to seeing in the application.  
  • Another big plus here is that I can hide fields from the user that they either don't need to have access to or should be secured.  
  • Finally, this seemed like a great way to resolve the foreign key values and include common data from related tables - this allows the users to not have to relate tables in the GUI quite so often.

The second step was to populate a table with "Joinable Fields" data.  This table helps simplify the process of creating queries with multiple tables by defining the fields in each table that are either the table primary key or foreign keys to other tables.  In the GUI, this results in vastly limiting the "from" and "to" fields required to join tables together - not perfect, but very helpful.

The third step was to create the Advanced Find GUI.  Because of the prior two steps, I'm able to effectively use information_schema to fetch and present the names of the views and fieldnames - which are now very friendly.  I chose to create a GUI similar to MS Access, with a glaring difference (which I'll save for a future update of the software).

The final step was to "scrape" the values from the GUI, convert that to a SQL expression and run it.  This is where Squel comes into play.... making it almost trivial.

The Advanced Find GUI

The essential elements for basic queries are all here
  • Choose the table/field you want to output
    • or not - maybe you just want to filter a field, but not display it
    • no limit on number of field selections
  • Alias any field
  • Multi-level sorting
  • Five filter "columns"
    • each column is internally AND with OR between columns
  • Join definitions between tables
    • with some auto-fill where joins are obvious
There are also some validation/convenience features
  • Data types are integrated so that we know when to enclose filter parameters in quotes and when not to
  • Duplicate field names are detected and automatically Aliased
  • Existence of appropriate Joins is validated

The Advanced Find Results

When the query is run, the SQL is generated and submitted to the database.  The results are returned to the client where the output table is displayed.  For now, I'm also showing the SQL statement that was generated and run.

I've tried every way I can think of do some SQL injection, but nothing destructive occurs at the database because of the way the Squel API parameterizes the building blocks.

The Generate SQL Code

Although there is a lot of parsing through the DOM here, focus on the lines that involve the "sql" object (which is an instance of the Squel API).

     //this will roll through the page and  
     //populate the squel object...  
     let selectedTables = iq.AdvancedFind.getSelectedTables();  
     if (selectedTables.length === 0) {  
       return;  
     }  
     let rows = iq.AdvancedFind.getBuilderRows();  
     //fire up the sqluel api object  
     let top = count == 0 ? 1000 : count;  
     let sql = squel.useFlavour("mssql");  
     sql = squel.useFlavour("mssql").select({ fieldAliasQuoteCharacter: "" }).top(top);  
     //add the fields  
     rows.forEach(row => {  
       if (row.TableName.length > 0) {  
         sql.field("qry_" + row.TableName + "." + row.FieldName, (row.FriendlyName.length > 0 ? "[" + row.FriendlyName + "]" : "") );  
       }  
     });  
     //add the from section/joins  
     if (selectedTables.length === 1) {  
       sql.from("qry_" + selectedTables[0].TableName);  
     } else {  
       let joinRows = iq.AdvancedFind.getJoinRows();  
       let isValid = true;  
       joinRows.forEach(row => {  
         if (row.FromTable.length === 0  
           || row.FromField.length === 0  
           || row.ToTable.length === 0  
           || row.ToField.length === 0) {  
           isValid = false;  
         }  
       });  
       if (!isValid) {  
         iq.Error.banner = "Oops!";  
         iq.Error.heading = "Missing Table Connections";  
         iq.Error.errorMsg = "When you select more than 1 table, you also have to provide the connection information for those tables.";  
         iq.Error.errorMsgPlain = "When you select more than 1 table, you also have to provide the connection information for those tables.";  
         iq.Error.popError(false);  
         return;  
       }  
       sql.from("qry_" + joinRows[0].FromTable);  
       joinRows.forEach(jr => {  
         if (jr.ToTable.length > 0) {  
           if (jr.JoinType.toLowerCase() === "inner") {  
             sql.join("qry_" + jr.ToTable, null, "qry_" + jr.FromTable + "." + jr.FromField + " = qry_" + jr.ToTable + "." + jr.ToField);  
           } else {  
             sql.left_join("qry_" + jr.ToTable, null, "qry_" + jr.FromTable + "." + jr.FromField + " = qry_" + jr.ToTable + "." + jr.ToField);  
           }  
         }  
       });  
     }  
     //where section  
     //we're going to build an expression for each column filter, or1, orX  
     let fullExpr = squel.expr();  
     let filterRows = rows.filter(row => row.Filter.length > 0);  
     let fExpr = squel.expr();  
     let or1Rows = rows.filter(row => row.Or1.length > 0);  
     let or1Expr = squel.expr();  
     let or2Rows = rows.filter(row => row.Or2.length > 0);  
     let or2Expr = squel.expr();  
     let or3Rows = rows.filter(row => row.Or3.length > 0);  
     let or3Expr = squel.expr();  
     let or4Rows = rows.filter(row => row.Or4.length > 0);  
     let or4Expr = squel.expr();  
     //populate the individual expressions and append to the fullExpr  
     if (filterRows.length > 0) {  
       filterRows.forEach(row => {  
         fExpr.and("(" + "qry_" + row.TableName + "." + row.FieldName + " " + row.Filter + ")");  
       });  
       fullExpr.and("(" + fExpr.toString() + ")");  
     }  
     if (or1Rows.length > 0) {  
       or1Rows.forEach(row => {  
         or1Expr.and("(" + "qry_" + row.TableName + "." + row.FieldName + " " + row.Or1 + ")");  
       });  
       fullExpr.or("(" + or1Expr.toString() + ")");  
     }  
     if (or2Rows.length > 0) {  
       or2Rows.forEach(row => {  
         or2Expr.and("(" + "qry_" + row.TableName + "." + row.FieldName + " " + row.Or2 + ")");  
       });  
       fullExpr.or("(" + or2Expr.toString() + ")");  
     }  
     if (or3Rows.length > 0) {  
       or3Rows.forEach(row => {  
         or3Expr.and("(" + "qry_" + row.TableName + "." + row.FieldName + " " + row.Or3 + ")");  
       });  
       fullExpr.or("(" + or3Expr.toString() + ")");  
     }  
     if (or4Rows.length > 0) {  
       or4Rows.forEach(row => {  
         or4Expr.and("(" + "qry_" + row.TableName + "." + row.FieldName + " " + row.Or4 + ")");  
       });  
       fullExpr.or("(" + or4Expr.toString() + ")");  
     }  
     sql.where(fullExpr.toString());  
     //SORT SECTION  
     let sortRows = rows.filter(row => row.SortDirection !== "Not Sorted");  
     sortRows.sort((rowA, rowB) => {  
       return rowA.SortOrder - rowB.SortOrder;  
     });  
     //process filtered/sorted resultSet  
     sortRows.forEach(row => {  
       if (row.SortDirection === "Ascending") {  
         sql.order("qry_" + row.TableName + "." + row.FieldName);  
       } else {  
         sql.order("qry_" + row.TableName + "." + row.FieldName, false);  
       }  
     });  
     return sql.toString();  


Summary

At first glance, it might seem like that's a lot of code to generate a SQL statement - but it's really nothing like having to do those string manipulations and contortions to end up with valid SQL.  





Sql Data Reader: Wish I knew this earlier!

 


Dealing with DataReader Null Values

Wish I'd known this years ago...  when parsing through a SQL DataReader, you can effectively use reader.IsDBNull(ordinal) to determine whether the value in the database is null.  What you do from there depends on your context.  The example below shows different ways to implement based on data type.

     private void CopyTopicMaterials(int fromTopicId, int toProjectId, int toTopicId, int modifiedBy)  
     {  
       SqlConnection dbConn = new SqlConnection(dbConnString);  
       string sql = "select * from tblTopicMaterials where TopicId = @TopicId";  
       SqlCommand cmd = new SqlCommand(sql, dbConn);  
       cmd.Parameters.AddWithValue("@TopicId", fromTopicId);  
       dbConn.Open();  
       SqlDataReader reader = cmd.ExecuteReader();  
       while (reader.Read())  
       {  
         TopicMaterial m = new TopicMaterial();  
         m.TopicId = toTopicId;  
         m.ProjectId = toProjectId;  
         m.Sequence = reader["Sequence"] as byte? ?? 0;  
         m.FindNumber = reader.IsDBNull(reader.GetOrdinal("FindNumber")) ? "" : reader["FindNumber"].ToString();  
         m.MaterialMasterId = reader["MaterialMasterId"] as int? ?? 0;  
         m.Quantity = reader.IsDBNull(reader.GetOrdinal("Quantity")) ? "" : reader["Quantity"].ToString();  
         m.MaterialUnitId = reader["MaterialUnitId"] as int? ?? 0;  
         m.Notes = reader.IsDBNull(reader.GetOrdinal("Notes")) ? "" : reader["Notes"].ToString();  
         m.ModifiedBy = modifiedBy;  
         m.DateModified = DateTime.UtcNow;  
         m.RecordStatus = reader.IsDBNull(reader.GetOrdinal("RecordStatus")) ? "" : reader["RecordStatus"].ToString();  
         CopyMaterial(m);  
       }  
       dbConn.Close();  
       reader.Close();  
     }  



Integrating Live Web (AspNet Signal-R) in a Web Site - Part 4 (the finale)

 

In This Post

This will be the final post of the AspNet SignalR series.  In this post, we will create a controller class that can be used to send messages from a REST WebApi to clients connected to our SignalR hub (via our default.html web page).

Looking Back

In the first three posts of this series (links below), we created an AspNet empty web site with support for SignalR and WebApi.  When the default.html page opens, the user is automatically joined to a Hub group called All.  The hub responds by returning a list of all users currently joined to the All group and the web page displays that list.  We can enter messages into a text box and see those messages displayed in a simple DIV - like a chat application.

Now it's time to look ahead....

Create a New Controller Class

Open your SignalRProjects solution in Visual Studio and right click the the Controllers folder under the MySignalRWebSite project (if you used different names, proceed accordingly).  In the right click context menu, select Add >> Web Api Controller Class (v 2.1).  Name the class SignalRController.cs.

Controller Class Code

Here is the code for the new controller class:

 using System;  
 using System.Collections.Generic;  
 using System.Web.Http;  
 using System.Configuration;  
 using Microsoft.AspNet.SignalR;  
 using Newtonsoft.Json;  
 namespace MySignalRWebSite.Controllers  
 {  
   public class SignalRController : ApiController  
   {  
     [HttpPost]  
     [AllowAnonymous]  
     public void PostHubMessage(HubMessage msg)   
     {  
       //this is the object the client web page is expecting  
       var sendMsg = new  
       {  
         groupName = msg.MessageToGroup,  
         userName = msg.MessageFrom,  
         message = msg.Message  
       };  
       //create the connection to the hub  
       IHubContext context = GlobalHost.ConnectionManager.GetHubContext<SignalRHub>();  
       //send the message by calling the client proxy method for all users connected to the group  
       context.Clients.Group(msg.MessageToGroup).processChatMessage(sendMsg);  
     }  
   }  
   public class HubMessage  
   {  
     public string Message { get; set; }   
     public string MessageFrom { get; set; }   
     public string MessageToGroup { get; set; }  
   }  
 }  


You'll notice above that I have, for clarity, deleted the out-of-box template functions from the new class - you don't need to do that if you don't want to, but they are not needed for our purposes.

In place of those methods, we have added the PostHubMessage method.  We have also added a simple data class (HubMessage) that defines the payload expected by the action whenever it is called.

There are 3 simple steps here:

  • Populate a new object from the incoming data
  • Connect to our SignalR Hub
  • Use the connection to call the client side processChatMessage proxy function
    • The client proxy function will call a local JavaScript function that will in turn display the message
Testing the Api

We're going to test our Api using Postman - a great tool for this purpose.  If you have other ways of testing, have at it!

If you don't have Postman and want to use it to test, download it from


Once downloaded and installed, running the test is easy.  First,  run the VS site in debug mode.  You may have to right click the default.html file and select Set Start Page from the menu.  The site will open a browser window with a URL address  something like 

https://localhost:12345/default.html

Next, open Postman and click File >> New Tab.  A new Tab will open looking like this


Change GET to POST. Copy the root of the URL from your browser and paste it into the Enter request URL textbox.


Add the following to the root URL you just copied

/api/SignalR/PostHubMessage

If you changed any of the naming suggested when we created the controller, you'll have to use your names here.

The last step before we test is to create a mock payload.  Immediately below the URL you just entered, you'll see a Body option - click that.  Click raw and select JSON from the select option that defaulted to Text.  Now we can compose the payload in JSON format like this
 {  
   "Message":"Test from the SignalR controller",  
   "MessageFrom": "SignalR Controller",  
   "MessageToGroup": "All"  
 }  

The screen should now look something like this



With your VS web site running, click the Send button.  If everything is set up correctly we should see the message from Postman work its way through the SignalR controller and on to the client!

If this didn't work, go back through the SignalR controller and make sure that the code matches, allowing for any naming differences you may have implemented.

Full Demo



Wrap Up

In this series, we covered a lot of ground but at a very simplified level.
  • Created a New Solution/Project in Visual Studio
    • AspNet Framework, Empty, WebApi
  • Downloaded the necessary NuGet packages to support desired functionality
  • Created C# classed for to support our SignalR hub
    • Startup.cs
    • SignalRHub.cs
  • Created an HTML page (default.html) with JavaScript to interact with the SignalR Hub
  • Created a C# SignalR controller (SignalRController.cs) to interact with connected users
  • Tested the API with Postman
At the end of this series, you should have a functioning SignalR-ready web site (however simple) that can allows communication between users and external clients through the Hub or the API.

Series Links




HTML Table with Fixed Headers using CSS

 



This is a pretty simple solution and probably doesn't cover all sorts of use cases - but I've found it effective for most of things I do.

First, I finally took the time to research this and figure out a model for my needs because I was about to gin up pagination for a table that might have anywhere from 0 to 50 rows.  After a deep sigh, I decided it would be more effective to just have a simple scrollable table.  At this point, let me just rant a little that a table with fixed column headings, a fixed footer and a scrollable body (rows) just SEEMS like an obvious thing.  No idea why the HTML table element doesn't support this natively.

The HTML Table

   <div class="row" style="border:1px solid lightgrey;padding-top:10px;max-height:500px">  
     <div class="col">  
       <div style="overflow-y:auto; max-height:350px;">  
         <table id="tblUserListing" style="width:100%;display:none;margin:auto;">  
           <thead>  
             <tr>  
               <th>First Name</th>  
               <th>Last Name</th>  
               <th>Login</th>  
               <th>Email</th>  
               <th>Last Login</th>  
               <th>Login Count</th>  
               <th>Status</th>  
               <th>&nbsp;</th>  
             </tr>  
           </thead>  
           <tbody id="tblUserListingBody" style="font-size:90%">  
           </tbody>  
         </table>  
       </div>  
       <div id="tblUserListingLoading" style="text-align:center;padding-top:20px;padding-bottom:20px">  
         <div class="spinner-border text-dark" role="status">  
           <div class="visually-hidden">loading.....</div>  
         </div>  
       </div>  
     </div>  
   </div>  

This table is in the context of a lot of other stuff going on with a page that is styled by Bootstrap.  As you can see, the table also doesn't have any rows in the tbody section - rows are added programmatically.  However....

The DIV enclosing the table needs to support vertical scrolling and height and/or max-height.  I chose a max-height so that any enclosing borders allow the table to assume its natural height up to a point... and then scroll.  When the table doesn't have enough rows to fill 350px, there is no scroll bar and a properly sized border. In my case, the border is actually on a DIV a couple levels up.

The CSS

     thead th {  
       font-weight:400;  
       padding-left:20px;  
       position:sticky;  
       top:0;  
       background-color:whitesmoke  
     }  

This will apply to any table with thead and th elements.  The critical pieces here are position: sticky; top: 0; background-color:whitesmoke.  This sticks the column headers to the top of the parent DIV.  The background-color is needed so that the background-color of the rows scrolling up don't bleed into the headers.

Notes

One of the great things about this approach (other than being dead simple), is that it allows the table to flow the column widths as needed based on the content of the td elements AND the headers and column data will stay aligned.

Microsoft Power Pages - Importing JS Libraries

  INTRODUCTION Microsoft Power Pages is a platform for developing web sites - from very simple... to surprisingly complex.  However, with ad...