meocuteequas
Real-time Notifications in Power Apps with PCF and Azure SignalR

Real-time Notifications in Power Apps with PCF and Azure SignalR

Dec 22, 2024

In today's fast-paced digital world, real-time notifications are crucial for keeping users informed and engaged. This blog post will guide you through building a real-time notification feature in Power Apps using the Power Apps Control Framework (PCF) and Azure SignalR.

Prerequisites

  • Power Apps account with appropriate permissions.
  • Azure Subscription with an active SignalR and Azure Functions service.
  • Visual Studio Code or a similar code editor.
  • Basic understanding of JavaScript, TypeScript, and React.
  • Familiarity with Power Apps and its components.

1. Create an Azure SignalR Service

Log in to the Azure portal and create a new SignalR service.

Create Azure SignalR service
Create Azure SignalR service

For the demo, I'll use SignalR's free tier, which allows for 20 concurrent connections and 20,000 messages each day. On other purposes, please refer to the Azure SignalR Pricing

After the creation is complete, navigate to the newly created Azure SignalR resource, click Settings on the left hand side blade, then Keys tab, copy the connection string, and paste it in your convinence location for future use.

Create Azure SignalR keys
Create Azure SignalR keys

2. Create an Azure Functions Project

I want to make things as simple as possible, therefore in this blog, I will be using Azure Fuctions as a serverless backend service that allows Powerapps PCF Controls to interact with the SignalR service to establish the necessary connection and able to deliver messages through the service.

2.1 Project Setup

Create Azure Functions Project

Log in to the Azure portal and create a new Function App.

Select the Consumption hosting option, with this option you only need to pay for compute resources when your functions are running.

Create Azure Function App
Create Azure Function App

Select NodeJS for the Runtime stack and your preferred Region (tip: choose the Region closest to you; this will make the functions app more responsive and take less time to respond). Leave everything else as is and click Review + Create.

2.2 Negotiate Function

First create a Negotiate fuction with HTTP trigger, this function will interact with the SignalR service to obtain the necessary connection information (access token and andpoint URL)

Why Is It Necessary?

Azure SignalR Service requires secure connections. The negotiate process helps:

  • Offload the responsibility of generating tokens and connection URLs to the server.
  • Ensure security by providing tokens only to authenticated clients.

How it works?

Azure, Powerapps integration
Azure, Powerapps integration
  • Client Requests the Negotiate Endpoint
  • Server Generates Connection Information: Negotiate function generates the URL and access token using the SignalR SDK through SignalRConnectionInfo.
  • Client Uses Connection Info: The client (Powerapps/PCF control) receives the URL and token and uses them to establish a WebSocket connection with the SignalR service.

Implementation

  • Connect to SignalR Service: Use the SignalR client library to establish a connection with your Azure SignalR Service.
  • Negotiate Connection: Call the SignalR Service's negotiation endpoint to obtain the connection details.
  • Return Connection Information: Return the necessary information (e.g., access token, endpoint URL) to the calling client (your PCF control).

Code example (Typescript)

import { app, input, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; const inputSignalR = input.generic({ type: "signalRConnectionInfo", name: "connectionInfo", hubName: "serverless", connectionStringSetting: "SIGNALR_CONNECTION_STRING", }); app.http("negotiate", { methods: ["GET", "POST"], authLevel: "anonymous", handler: negotiate, extraInputs: [inputSignalR], }); // Allow client to get access token export async function negotiate(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { try { return { body: JSON.stringify(context.extraInputs.get(inputSignalR)) }; } catch (error) { context.log(error); return { status: 500, jsonBody: error, }; } }

2.3 Broadcast Function

This function will broadcast the message to all connected clients

Implementation

  • Receive Message: Receive the message to be broadcast as input to the function.
  • Connect to SignalR Service: Establish a connection to your SignalR Service using the connection string.
  • Send Message: Invoke the desired method on the SignalR Hub to broadcast the message to all connected clients.

Code example (Typescript):

import { app, output, HttpRequest, HttpResponseInit, InvocationContext } from "@azure/functions"; const goingOutToSignalR = output.generic({ type: "signalR", name: "signalR", hubName: "serverless", connectionStringSetting: "SIGNALR_CONNECTION_STRING", }); app.http("broadcast", { methods: ["GET", "POST"], authLevel: "anonymous", handler: broadcast, extraOutputs: [goingOutToSignalR], }); // Broadcast message to all clients export async function broadcast(req: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { const message = req.query.get("message"); context.extraOutputs.set(goingOutToSignalR, { target: "newMessage", arguments: [message], }); return { status: 200, }; }

Following the complete implementation of the Negotiate and Broadcast functions, we have two more tasks to complete. As you are aware, Azure's default setting is to restrict external hosts from interacting with your backend; therefore, you need to add your Powerapps origin to the allow list. Simply leave it alone for the moment; we will add the origin later.

To add a new environment variable, select Environment Variables from the Function App settings on the left-hand blade and then click Add. The SignalR connection string that you previously copied should be the value of a new environment variable called SIGNALR_CONNECTION_STRING. After selecting Apply, click Save.

3. Develop, build and publish the PCF Control

You can create a new PCF control project using a PCF CLI or by manually setting up the project structure. For reference please follow this article from Microsoft on how to create and build a code component.

The next step after creating your PCF project is to examine the files and project file structure. First, let's examine the ControlManifest.Input.xml file. The metadata for the code component is contained in an XML file called the control manifest. Additionally, it describes how the code component should behave. The manifest file created in this tutorial is located in the MeosignalR subfolder. The ControlManifest.Input.xml file is predefined with certain properties, as you will see when you open it in Visual Studio Code.

The control node defines the namespace, version, and display name of the code component.

If you want to learn more about this manifest file, please follow this article from Microsoft.

3.1 Apply changes to the ControlManifest.Input.xml file.

Copy and paste this code under external-service-usage tag.

<property name="SignalRHubConnection" display-name-key="SignalR Hub Connection" description-key="Please enter your SignalR hub endpoint for example: https://example.com/hub" of-type="SingleLine.Text" usage="bound" required="true" /> <property name="MessagesOut" display-name-key="Broadcast messages" description-key="Messages from SignalR hub" of-type="SingleLine.Text" usage="bound" required="true" />

When you finish, it will look something like this.

PCF controlmanifest file
PCF controlmanifest file

3.2 Generate ManifestDesignTypes.d.ts file using the following command.

npm run refreshTypes

3.3 Implementing component logic.

The next step after implementing the manifest file is to implement the component logic using TypeScript. The component logic should be implemented inside the index.ts file. When you open the index.ts file in the Visual Studio Code, you'll notice that the four essential functions (init, updateView , getOutputs, and destroy) are predefined. Now, let's implement the logic for the code component.

  • Initial some variables

    private connection: HubConnection; private _context: ComponentFramework.Context<IInputs>;
  • Update the init function

    public init( context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement, ): void { const endpoint = context.parameters.SignalRHubConnection.raw || ""; if(!endpoint || endpoint === "val") { console.error("SignalR Hub Connection URL is required"); return; } this.connection = new HubConnectionBuilder().withUrl(endpoint) .withAutomaticReconnect() .build(); this.connection.on("newMessage", (message: string) => { console.log(message) context.parameters.MessagesOut.raw = message; notifyOutputChanged(); }); this.connection.start().catch(err => console.error(err.toString())); this._context = context; }
  • Edit the getOutputs function

    public getOutputs(): IOutputs { return { MessagesOut: this._context.parameters.MessagesOut.raw || "" }; }
  • Edit the destroy function

    public destroy(): void { this.connection.stop(); }
  • The complete index.ts file should look like this:

    import { HubConnection, HubConnectionBuilder } from "@microsoft/signalr"; import { IInputs, IOutputs } from "./generated/ManifestTypes"; export class MeosignalR implements ComponentFramework.StandardControl<IInputs, IOutputs> { private connection: HubConnection; private _context: ComponentFramework.Context<IInputs>; /** * Empty constructor. */ constructor() {} public init( context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement ): void { const endpoint = context.parameters.SignalRHubConnection.raw || ""; console.log(endpoint); if (!endpoint || endpoint === "val") { console.error("SignalR Hub Connection URL is required"); return; } this.connection = new HubConnectionBuilder().withUrl(endpoint).withAutomaticReconnect().build(); this.connection.on("newMessage", (message: string) => { console.log(message); context.parameters.MessagesOut.raw = message; notifyOutputChanged(); }); this.connection.start().catch((err) => console.error(err.toString())); this._context = context; } public updateView(context: ComponentFramework.Context<IInputs>): void { // Add code to update control view } public getOutputs(): IOutputs { return { MessagesOut: this._context.parameters.MessagesOut.raw || "", }; } public destroy(): void { this.connection.stop(); } }

3.4 Package and publish your code component.

To package a code component, please refer to this article from Microsoft.

After successfully build your code component, go to your Powerapp environment, select Solutions tab on the left hand blade, click on Import solution. Browse to your build folder and select the zip file that generated by the build process.

Powerapps import solution
Powerapps import solution

Note that, you need to enable Power Apps component framework for canvas apps before using your custom code control, otherwise you will not able to import and select your custom control from your Powerapps (canvas).

To enable Power Apps component framework for canvas apps, go to Powerapps Admin Platform, select your prefered environment, go to Settings > Product > Features, enable Power Apps component framework for canvas apps

Enable PCF
Enable PCF

4. Use the PCF Control in Your Power App

Your custom code component is now ready. Open the Canvas app, choose Insert > Import component > Code, then pick your custom code component—in my case, the MeosignalR component—and click Import.

Import PCF control into canvas app
Import PCF control into canvas app

Where is the SignalR Hub Connection field that needs to be filled in with the recently imported component?

To get the SignalR connection enpoint you need to go to the Azure Function App > select the Negotiate function you created earlier, copy the function URL. You can copy either _master URL or default URL, for me I just copy the default URL

Get SignalR Hub Connection Endpoint
Get SignalR Hub Connection Endpoint

Paste it in the SignalR Hub Connection field, remove the function name, which is /negotiate? path, the result will look like this: https://meosfunctions.azurewebsites.net/api

I'll make a very simple senerio just for the demo. I'll make a text label whose value comes from the MessageOut property of the MeosinalR control. If the MessageOut property returns an empty value, I'll give it a default message. How does it all work together, then? The MessageOut property will be updated if the custom code control MeosignalR receives any new messages from the Azure SignalR service. Any updated values will then be reflected in the label text value, allowing us to view all incoming messages.

Label Text
Label Text

Now that everything is set up, if you play the app at this moment, you will see a message stating that the connection has been blocked by the CORS policy. This is because we haven't configured the CORS policy for our Function App. To fix this, copy the origin from the message, navigate to the Function App's CORS section, paste it in the Allow origins list, make sure to check Enable Access-Control-Allow-Credentials, and click Save.

CORS error
CORS error
Azure Function App CORS
Azure Function App CORS

Go back to your Powerapps, refresh it, everything should working now.

5. Put everything on a test

Let's go to your Azure Function App's broadcast function and click Test/Run to show how a remote notification is sent to your app. To check if the Powerapps can receive a message from the SignalR service, I will make a GET request and specify a message=Hello+world. As you are aware, the broadcast function takes a message from the query parameter and sends it to all connected clients.

Powerapps, Azure Function test
Powerapps, Azure Function test

As you can see, the Default Message text has changed to Hello World, indicating that the application is now able to receive messages from Azure SignalR.

Building real-time notification features in Power Apps is something I hope this blog post will help you get started on! Please keep in mind that this guide is simplified. For comprehensive guidelines and best practices, consult the official documentation.

You should then modify the code to meet your exact requirements and fill in the necessary details. Sending and receiving messages in a real-world application will require much more complex handling; this could include Power Automate, a third-party API, etc.

Pattern 04

Lets work together on your next project

Collaboration is key! Lets join forces and combine our skills to tackle your next project with a powerful energy that guarantees success.