<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[pragmatic development blog RSS feed]]></title><description><![CDATA[my theme]]></description><link>https://www.pragmatic-development.io</link><generator>GatsbyJS</generator><lastBuildDate>Fri, 05 Dec 2025 07:45:10 GMT</lastBuildDate><item><title><![CDATA[Offline-Funktion in Model-Driven Apps: Effizienz ohne permanente Internetverbindung]]></title><description><![CDATA[In der heutigen digitalisierten Welt sind Unternehmen auf leistungsfähige Anwendungen angewiesen, die auch unterwegs problemlos…]]></description><link>https://www.pragmatic-development.io/blog/offline-funktion-model-driven-apps/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/offline-funktion-model-driven-apps/</guid><pubDate>Tue, 20 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;In der heutigen digitalisierten Welt sind Unternehmen auf leistungsfähige Anwendungen angewiesen, die auch unterwegs problemlos funktionieren.
Die Microsoft Power Platform ermöglicht es Nutzern durch die Offline-Funktionalität von Model-driven Apps auch ohne permanente Internet-Verbindung produktiv zu bleiben.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Warum ist die Offline-Funktion wichtig?&lt;/h2&gt;
&lt;p&gt;Cloud-Apps werden in vielen Unternehmen für tägliche Arbeitsvorgänge oder zur Datenerfassung genutzt. Ohne eine stabile Verbindung ist die Nutzung von cloudbasierten Anwendungen herausfordernd. Schlimmstenfalls müssen Daten manuell erfasst und später in das System übertragen werden, was einen vermeidbaren zusätzlichen Aufwand und verlangsamte Prozesse bedeutet. &lt;/p&gt;
&lt;p&gt;Wie kann also Mitarbeitenden im Außendienst, in Lagerhallen oder an entfernten Standorten mit begrenztem Internetzugang die Nutzung ermöglicht werden? Die Offline-Funktion von Model-driven Apps stellt sicher, dass Nutzer weiterhin Daten erfassen, aktualisieren und bearbeiten können, ohne auf eine aktive Internetverbindung angewiesen zu sein.&lt;/p&gt;
&lt;h2&gt;Wie funktioniert die Offline-Fähigkeit in Model-driven Apps?&lt;/h2&gt;
&lt;p&gt;Microsoft Power Apps ermöglichen es, Model-driven Apps mit Offline-Funktionalität auszustatten.&lt;br/&gt;
Einige wesentliche Merkmale sind:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Automatische Synchronisation:&lt;/strong&gt; Sobald die App erkennt, dass eine Internetverbindung besteht, synchronisiert sie automatisch die lokal gespeicherten Daten mit der Cloud.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cachingsystem:&lt;/strong&gt; Häufig genutzte Daten können zwischengespeichert werden, sodass Nutzer diese auch ohne Verbindung abrufen können.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Formulare und Datensätze offline nutzen:&lt;/strong&gt; Nutzer können neue Datensätze anlegen und bestehende bearbeiten, während sie offline sind. Diese Änderungen werden später synchronisiert.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Benutzerfreundliche Einrichtung:&lt;/strong&gt; Administratoren können Offline-Profile definieren, um festzulegen, welche Entitäten und Daten für die Offline-Nutzung verfügbar sein sollen.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Vorteile der Offline-Nutzung&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Produktivität steigern:&lt;/strong&gt; Mitarbeiter können ohne Unterbrechung arbeiten, selbst wenn das Internet nicht verfügbar ist.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Datenverluste vermeiden:&lt;/strong&gt; Da Daten lokal gespeichert werden, gehen sie nicht verloren, wenn die Verbindung plötzlich abbricht.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimierte Performance:&lt;/strong&gt; Durch den Zugriff auf lokal gespeicherte Daten kann die App schneller reagieren.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Beispiel aus der Praxis&lt;/h2&gt;
&lt;p&gt;Energieberater sind häufig bei Kunden vor Ort tätig, um Gebäude auf ihre Energieeffizienz zu prüfen. In vielen Fällen gibt es keinen oder nur eingeschränkten Internetzugang, was die digitale Erfassung von Daten erschwert. Mit einer offline-fähigen Model-driven App können Energieberater auch ohne Internetverbindung alle relevanten Daten direkt vor Ort erfassen und bearbeiten.&lt;/p&gt;
&lt;p&gt;Vor dem Besuch synchronisiert der Energieberater die App mit der zentralen Datenbank. Dadurch stehen ihm bereits vorhandene Kundendaten, der Stand des Projekts oder projektbezogene Dokumente zur Verfügung.
Vor Ort können vorher festgelegte Datensätze vearbeitet und bearbeitet werden, z.B.:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Eingabe von Gebäudedaten (z. B. Baujahr, Heizsystem, Materialien)&lt;/li&gt;
&lt;li&gt;Dokumentation mit Fotos und Notizen&lt;/li&gt;
&lt;li&gt;Stand des Projekts verfolgen&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alle eingegebenen Daten werden lokal gespeichert und automatisch synchronisiert, sobald wieder eine Internetverbindung besteht.
Nach der Synchronisation stehen die Daten in der zentralen Datenbank zur Verfügung. Der Energieberater kann die Ergebnisse am PC weiterbearbeiten und Berichte erstellen.&lt;/p&gt;
&lt;h2&gt;Wie kann die Offline Funktion aktiviert werden?&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://make.powerapps.com/&quot; target=&quot;_blank&quot;&gt;Maker Portal der Power Platform&lt;/a&gt; öffnen &lt;/li&gt;
&lt;li&gt;Gewünschte Umgebung wählen&lt;/li&gt;
&lt;li&gt;Entsprechende Solution wählen&lt;/li&gt;
&lt;li&gt;Tabellen/Entitäten offline zur Verfügung stellen&lt;br/&gt;
a.	Jede Tabelle, die offline zur Verfügung stehen soll, muss für die Offline-Verfügbarkeit freigeschaltet werden.&lt;br/&gt;
b.	Tabelle wählen &gt; Eigenschaften der Tabelle öffnen &gt; &quot;Kann Offline geschaltet werden&quot;
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/offline-mda/Bild1_Offline_schalten.png&quot; alt=&quot;Offline-Verfügbarkeit freischalten&quot; title=&quot;Offline-Verfügbarkeit freischalten&quot;&gt;
&lt;br/&gt;&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;Die Model-driven App im Power Apps App Editor öffnen&lt;/li&gt;
&lt;li&gt;&quot;Einstellungen&quot; öffnen
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/offline-mda/Bild2_Einstellungen.png&quot; alt=&quot;Einstellungen&quot; title=&quot;Einstellungen&quot;&gt;
&lt;br/&gt;&lt;/p&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;Den Reiter &quot;Allgemein&quot; wählen und bis zu “Kann offline verwendet werden” scrollen. Den Schalter auf &quot;Ja&quot; setzen und “Neues Profil mit aktuellen App-Daten” wählen.
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/offline-mda/Bild3_Offline_verwenden.png&quot; alt=&quot;Offline verwenden&quot; title=&quot;Offline verwenden&quot;&gt;
&lt;br/&gt;&lt;/p&gt;
&lt;ol start=&quot;8&quot;&gt;
&lt;li&gt;Daraufhin öffnet sich ein Fenster. Hier können der Name des Offline-Profils eingegeben und die offline verfügbaren Tabellen hinzugefügt werden.
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/offline-mda/Bild4_Mobile_Offline_Profil.png&quot; alt=&quot;Mobile Offline Profil&quot; title=&quot;Mobile Offline Profil&quot;&gt;
&lt;br/&gt;&lt;/p&gt;
&lt;ol start=&quot;9&quot;&gt;
&lt;li&gt;Wenn eine weitere Tabelle hinzugefügt werden soll, wird &quot;+ Add Table&quot; gewählt und es können alle für den Offline-Modus verfügbaren Tabellen ausgewählt werden. Wurde eine Tabelle ausgewählt, erscheint ein weiteres Fenster. Hier kann eine weitere Auswahl getroffen werden, beisielsweise welche Zeilen offline angezeigt werden sollen oder wie oft sich diese Tabelle bei verfügbarer Internetverbindung synchroniseren soll.
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/offline-mda/Bild5_Tabelle_hinzuf%C3%BCgen.png&quot; alt=&quot;Tabelle hinzufügen&quot; title=&quot;Tabelle hinzufügen&quot;&gt;
&lt;br/&gt;&lt;/p&gt;
&lt;ol start=&quot;10&quot;&gt;
&lt;li&gt;Im Anschluss speichern und schließen. Daraufhin die App speichern und veröffentlichen. Nun kann die Power Apps App offline verwendet werden. &lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Hilfreiche Hinweise&lt;/h2&gt;
&lt;p&gt;Es gibt zwei Möglichkeiten, wie die Offline Funktion ausgeführt werden kann. Zum einen gibt es den &quot;Offline First Modus&quot; und zum anderen den &quot;Offline Classic Modus&quot;. Beim
Offline First Modus sind die Daten immer gleich, unabhängig von der Internetverbindung. Außerdem gibt es keinen Schalter in dem die Benutzer zwischen Offline- und Online-Modus wechseln können. Ein Benutzer vergisst also nie, seine Änderungen mit dem Server zu synchronisieren, da die App dies automatisch tut.&lt;/p&gt;
&lt;p&gt;Der Offline Classic Modus hingegen müssen die Nutzer manuell entscheiden, ob sie im Offline- oder Online-Modus arbeiten. Außerdem müssen Sie dort manuell die neuen Daten mit dem Server synchronisieren. Das ist sehr fehleranfällig.
Unsere Empfehlung ist daher der Offline First Modus, da dieser weniger fehleranfällig ist.
Um den Offline First Modus zu aktivieren sind folgende Schritte nötig: &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Im Power Apps App Editor die Einstellungen öffnen.&lt;/li&gt;
&lt;li&gt;Unter Funktionen zu &quot;Enable Offline Classic&quot; scrollen und dies auf &quot;Nein&quot; setzen
&lt;br/&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/offline-mda/Bild6_Offline_First_Modus.png&quot; alt=&quot;Offline First Modus&quot; title=&quot;Offline First Modus&quot;&gt;
&lt;br/&gt;&lt;/p&gt;
&lt;h2&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Die Offline-Funktion in Model-driven Apps ist ein bedeutender Schritt zur Steigerung der Effizienz und Flexibilität in Unternehmen. Sie ermöglicht es Nutzern, unabhängig von ihrer Internetverbindung produktiv zu bleiben und sicherzustellen, dass alle relevanten Daten nahtlos synchronisiert werden, sobald eine Verbindung wiederhergestellt ist.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Change Visibility of Attributes on Visible Sections Only]]></title><description><![CDATA[When changing the visibility of attributes in Dynamics 365 or Power Apps, you might have noticed that this change also impacts hidden…]]></description><link>https://www.pragmatic-development.io/blog/change-visibility-of-attributes-on-visible-sections-only/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/change-visibility-of-attributes-on-visible-sections-only/</guid><pubDate>Tue, 25 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;When changing the visibility of attributes in Dynamics 365 or Power Apps, you might have noticed that this change also impacts hidden sections, where the attribute is located, i.e. these sections are set to visible by default. While this of course makes sense, it might not always be what you intended, especially if you used attributes with multiple instances within a form. &lt;/p&gt;
&lt;p&gt;For instance, as you can see in the following illustration, the visibility of sections in our order form is determined by product selection (sand or bricks). The section “Product Details” is individual for each product, though in both cases it allows the user to choose a unit (kg or m&lt;sup&gt;3&lt;/sup&gt;). Upon selecting kg as “Unit (Classic)” in “Sand Details” the visibility is set for the attribute “Weight” (resp. “Volume” if you chose m&lt;sup&gt;3&lt;/sup&gt;), which then appears on our form.&lt;/p&gt;
&lt;div class=&quot;row justify-content-around&quot;&gt;
  &lt;div class=&quot;col-12 col-md-8&quot;&gt;
    &lt;img src=&quot;/images/blog/change-visibility-of-attributes/Visibility-Classic.gif&quot; alt=&quot;Example of a material order form with hidden sections appearing on attribute visibility change&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;But – lo and behold – what is this?! The section “Bricks Details” is also suddenly visible on our form. How did this happen?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;See, as “Brick Details” contains the attribute “Weight” as well the whole section is set to visible by default. Of course you could override this by using unique control names, like “weight_2” etc., and set the visibility individually for each item. While this could work for a while, it is not the safest option development-wise (think reusability and clean coding) and will definitely cause problems as soon as changes are made to the controls or if attributes are used more than once.&lt;/p&gt;
&lt;p&gt;Let me show you a better option.&lt;/p&gt;
&lt;p&gt;As stated, our aim is to apply attribute visibility only to controls on already visible sections. We can achieve this by checking in the event handler beforehand, whether the control we want to set to visible is located on a visible section. If the section is visible, the control will appear. Any other invisible sections, which contain controls of the same name, will remain hidden. This way, section visibility will not be implicitly affected. &lt;/p&gt;
&lt;p&gt;Let’s have a look at the code.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;typescript&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;static setVisibilityOfAttributeOnVisibleSectionsOnly(fCtx: Xrm.FormContext, attributeName: string, isVisible: boolean) {
    // Get the names of all controls on visible sections
    var controlNames: string[] = [];
    fCtx.ui.tabs.forEach(t =&amp;gt; t.sections.forEach(s =&amp;gt; {
        if (s.getVisible()) {
            s.controls.forEach(c =&amp;gt; controlNames.push(c.getName()));
        }
    }));

    // Change the visibility for the control of the attribute.
    fCtx.getAttribute(attributeName).controls.forEach(x =&amp;gt; {
        // But only if the current control is on a visible section
        if (controlNames.includes(x.getName())) {
            x.setVisible(isVisible);
        }
    });
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;br/&gt;
&lt;p&gt;And this is what it looks like in our app. Note that now, when you select kg as unit on “Sand Details”, the attribute “Weight” will show on this section – while our alternative section “Brick Details” will remain hidden.&lt;/p&gt;
&lt;div class=&quot;row justify-content-around&quot;&gt;
  &lt;div class=&quot;col-12 col-md-8&quot;&gt;
    &lt;img src=&quot;/images/blog/change-visibility-of-attributes/Visibility-Enhanced.gif &quot; alt=&quot;Example of a material order form with hidden sections remaining hidden on attribute visibility change&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;In a nutshell, if you aim to avoid problems in the long run, the proposed solution is a good way to go.  You can use (and reuse) attributes of the same name/type without worrying that it might impact the intended functionality. Using this function will enable you to build forms with multiple sections and still control exactly which of your data will be presented.&lt;/p&gt;
&lt;p&gt;Let me know, if you have already encountered this matter and if this solution helped you. &lt;/p&gt;</content:encoded></item><item><title><![CDATA[Automatisches Erstellen von Listen und Dokumenten-Bibliotheken in SharePoint mit Azure Automation]]></title><description><![CDATA[Azure Automation in der Kombination mit SharePoint bietet viele Möglichkeiten, um den unternehmerischen Alltag zu erleichtern. So können z.B…]]></description><link>https://www.pragmatic-development.io/blog/automatisches-erstellen-von-lists-und-dokumentenbibliotheken-in-sharepoint/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/automatisches-erstellen-von-lists-und-dokumentenbibliotheken-in-sharepoint/</guid><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Azure Automation in der Kombination mit SharePoint bietet viele Möglichkeiten, um den unternehmerischen Alltag zu erleichtern. So können z.B. Routineaufgaben automatisiert und manuelle Aufwände für das Erstellen von SharePoint-Seiten,
-Listen, -Dokumenten-Bibliotheken, -Ordnern etc. enorm reduziert werden. Dies führt nicht nur zu einer höheren Effizienz, sondern auch zu einer schnelleren Bereitstellung von Diensten und Ressourcen.&lt;/p&gt;
&lt;h3&gt;Anwendungsfälle von SharePoint und Azure Automation:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Genehmigungen (Approvals):&lt;/strong&gt; Azure Automation in der Kombination mit SharePoint kann verwendet werden, um bestimmte Prozesse mit Genehmigungen zu versehen. Bevor ein Prozess ausgelöst wird, muss eine ausgewählte Person
diesen Prozess bestätigen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatisches Erstellen von Berechtigungsstufen:&lt;/strong&gt; In Azure Automation wird ein Prozess ausgelöst, um einen Ordner zu einem Projekt im SharePoint anzulegen. Durch das Azure Automation-Skript werden die exakt vorgesehenen Berechtigungen auf diesen Ordner gesetzt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automatisches Erstellen von SharePoint-Seiten:&lt;/strong&gt; SharePoint-Seiten können automatisch erstellt werden, sobald ein vorher festgelegter Auslöser dafür bestimmt wurde.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Im Folgenden zeige ich dir, wie du Azure Automation einrichtest und die benötigten Module und Runbooks hinzufügst. Des weiteren sehen wir uns zwei mögliche
Anwendungsfälle von SharePoint-Automatisierungen mithilfe von Azure Automation im Detail an.&lt;/p&gt;
&lt;h3&gt;Inhalt:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#automation&quot;&gt;Einrichtung Azure Automation, Runbooks und Webhooks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#beispiel&quot;&gt;Anwendungsbeispiel: Verwendung in Power Automate zur automatischen Anlage von Dokumenten-Bibliotheken&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;br/&gt;
&lt;p&gt;&lt;a name=&quot;automation&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Azure Automation einrichten&lt;/h2&gt;
&lt;p&gt;Azure Automation ermöglicht eine automatisierte Erstellung von Ordnern. Zum Ausführen der notwendigen Befehle nutzt Azure Automation die PowerShell.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Erstellen eines Azure Automation Accounts:&lt;/strong&gt; &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/automation/automation-create-standalone-account?tabs=azureportal&quot; target=&quot;_blank&quot;&gt;Create a standalone Azure Automation account | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Module&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Damit die PowerShell Befehle ausgeführt werden können, müssen wir die benötigten Module in den Azure Automation Account laden. Diese sind unter &quot;Shared Resources&quot; &gt; &quot;Modules&quot; zu finden. Unter der Auswahl &quot;+Add Module&quot; &gt; &quot;Browse from gallery&quot; kann ein neues Modul gesucht und hinzugefügt werden.
&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/Module.png&quot; alt=&quot;Ansicht Module in Azure Automation&quot; title=&quot;Ansicht Module in Azure Automation&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Damit Dokumenten-Bibliotheken und Listen (sowie wenn nötig bestimmte Berechtigungen) erstellt werden können, müssen folgende Module in Azure Automation installiert werden:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PnP.PowerShell&lt;/li&gt;
&lt;li&gt;ExchangeOnlineManagement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Runbooks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Innerhalb eines Azure Automation Accounts können &quot;Runbooks&quot; hinzugefügt werden. Dort werden die PowerShell Befehle ausgeführt (mit Managed Identity).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/Runbooks.png&quot; alt=&quot;Ansicht Runbooks in Azure Automation&quot; title=&quot;Ansicht Runbooks in Azure Automation&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Managed Identity&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Damit die Befehle mit den installierten Modulen durch eine Managed Identity ausgeführt werden können, müssen diese im Azure Automation Account zugeordnet werden. Dazu wird die Object ID des Azure Automation Accounts benötigt. Diese ist unter &quot;Account Setting&quot; &gt; &quot;Identity&quot; zu finden. Der Status muss auf &quot;On&quot; stehen. Die Object (principal) ID ist ebenfalls dort zu finden.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/ObjectID.png&quot; alt=&quot;Aktivieren der Managed Identity in Azure Automation&quot; title=&quot;Aktivieren der Managed Identity in Azure Automation&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Um die Module für den Azure Automation Account zu registrieren, müssen das PowerShell Terminal geöffnet und folgende Befehle ausgeführt werden:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PnP.PowerShell:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;PnP.PowerShell ist für SharePoint zuständig.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Modul in PowerShell installieren&lt;/li&gt;
&lt;li&gt;Object ID aus Azure Automation unter Identity hinzufügen&lt;/li&gt;
&lt;li&gt;Berechtigungen auf die Managed Identity in PowerShell mit der Object ID freischalten&lt;/li&gt;
&lt;li&gt;PnP Modul in Azure Automation hinzufügen&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;PowerShell Terminal:&lt;/strong&gt;&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;powershell&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;Install-Module PnP.PowerShell
$ObjectID=&amp;quot;&amp;lt;Object ID&amp;gt;&amp;quot;
Add-PnPAzureADServicePrincipalAppRole -Principal $ObjectID -AppRole &amp;quot;Group.Read.All&amp;quot; -BuiltInType MicrosoftGraph
Add-PnPAzureADServicePrincipalAppRole -Principal $ObjectID -AppRole &amp;quot;Sites.FullControl.All&amp;quot; -BuiltInType SharePointOnline&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Azure Automation Runbook:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Mit folgendem Befehl verbindest du PnP im Azure Automation Runbook mit SharePoint:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Connect-PnPOnline -ManagedIdentity&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Danach werden die Befehle ausgeführt, die zur Erstellung von Dokumenten-Bibliotheken, Ordnern, Listen usw. verwendet werden. Dazu im weiteren Verlauf mehr.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weitere Informationen zu PnP:&lt;/strong&gt; &lt;br/&gt;
&lt;a href=&quot;https://pnp.github.io/powershell/cmdlets/index.html&quot; target=&quot;_blank&quot;&gt;PnP Cmdlets&lt;/a&gt;&lt;br/&gt;
&lt;a href=&quot;https://pnp.github.io/powershell/articles/azureautomationrunbook.html&quot; target=&quot;_blank&quot;&gt;PnP with Azure Automation Runbooks&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ExchangeOnlineManagement:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;ExchangeOnlineManagement ist unter anderem für die Erstellung von Berechtigungen, Berechtigungsstrukturen und Sicherheitsgruppen zuständig. Exchange Online ist generell für die cloudbasierte Messaging-Plattform von Microsoft verantwortlich (E-Mail und die damit einher gehenden Berechtigungsstrukturen).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Modul in PowerShell installieren&lt;/li&gt;
&lt;li&gt;Object ID aus Azure Automation unter Identity hinzufügen&lt;/li&gt;
&lt;li&gt;Mit MSGraph verbinden (mit Global Admin Rechten)&lt;/li&gt;
&lt;li&gt;Parameter einfügen&lt;/li&gt;
&lt;li&gt;Neue Berechtigungen für Exchange erstellen&lt;/li&gt;
&lt;li&gt;Managed ID entsprechende Azure AD Rolle zuweisen (Exchange Admin)&lt;/li&gt;
&lt;li&gt;Danach in Entra ID prüfen, ob die Rolle hinzugefügt wurde und dann in Azure Automation das Modul für Exchange hinzufügen&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;PowerShell Terminal:&lt;/strong&gt;&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;powershell&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;Install-Module Microsoft.Graph
#Object ID aus Azure Automation unter Identity
$ObjectID=&amp;quot;&amp;lt;Object ID&amp;gt;&amp;quot;
#Connect to MSGraph mit Global Admin Rechten
Connect-MgGraph -Scopes AppRoleAssignment.ReadWrite.All,Application.Read.All
#Parameter
$AppRoleID=&amp;quot;dc50a0fb-09a3-484d-be87-e023b12c6440&amp;quot; #GUID überall gleich
$ResourceID=(Get-MgServicePrincipal -Filter &amp;quot;AppId eq &amp;#39;00000002-0000-0ff1-ce00-000000000000&amp;#39;&amp;quot;).Id
#Neue Berechtigungen für Exchange
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $ObjectID -PrincipalId $ObjectID -AppRoleId $AppRoleID -ResourceId $ResourceID
#Managed ID entsprechende Azure AD Rolle zuweisen (Exchange Admin)
Connect-MgGraph -Scopes RoleManagement.ReadWrite.Directory
$ERoleID=&amp;quot;29232cdf-9323-42fd-ade2-1d097af3e4de&amp;quot; #GUID für Exchange Admin
New-MgRoleManagementDirectoryRoleAssignment -PrincipalId $ObjectID -RoleDefinitionId $ERoleID -DirectoryScopeId &amp;quot;/&amp;quot;
#Danach im Entra ID im Azure Portal prüfen, ob Role hinzugefügt wurde und dann in Azure Automation das Modul für Exchange hinzufügen&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Azure Automation Runbook:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Um ExchangeOnline im Azure Automation Runbook mit dem Tenant zu verbinden, verwendest du folgenden Befehl:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Connect-ExchangeOnline -ManagedIdentity -Organization tenant.onmicrosoft.com&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Webhooks:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Damit das Azure Automation Runbook automatisch getriggert wird, muss ein Webhook erstellt werden.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/Webhook1.png&quot; alt=&quot;Ablauf automatische Aktivierung Webhook&quot; title=&quot;Ablauf automatische Aktivierung Webhook&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Ein Webhook erstellst du im Azure Portal. Wähle dort das entsprechende Runbook und dann &quot;Add Webhook&quot;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/Webhook2.png&quot; alt=&quot;Webhook im Azure Portal hinzufügen&quot; title=&quot;Webhook im Azure Portal hinzufügen&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/Webhook3.png&quot; alt=&quot;Create new Webhook&quot; title=&quot;Create new Webhook&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/Webhook4.png&quot; alt=&quot;Webhook URL&quot; title=&quot;Webhook URL&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Der WebHook wird wie folgt im Azure Automation Runbook hinterlegt, damit das Runbook dadurch getriggert wird:&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;powershell&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;param
(
    [Parameter(Mandatory=$false)]
    [object] $WebhookData
)&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;br/&gt;
&lt;p&gt;Die URL wird verwendet, um von externen Applikationen aus getriggert zu werden.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;beispiel&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Anwendungsbeispiele Power Automate Flow&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Automatisches Anlegen eines Ordners bei Hinzufügen einer Person:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Hierfür erstellen wir einen Flow, der ausgeführt wird, sobald eine neue Person einem bestimmten Team in Microsoft Teams hinzugefügt wird. Dieser Flow triggert den Webhook, der das Azure Automation Runbook ausführt.
In dem Runbook befindet sich der Code, der zum Beispiel auf einer SharePoint-Seite (PnP.PowerShell) einen Ordner anlegt, auf den nur diese neu hinzugefügte Person Zugriff hat (ExchangeOnline).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/WebhookPowerAutomate.png&quot; alt=&quot;Webhook durch Ereignis triggern&quot; title=&quot;Webhook durch Ereignis triggern&quot;&gt;&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;Weitere Informationen zu Webhooks und wie diese in Azure Automation gestartet werden:
&lt;a href=&quot;https://learn.microsoft.com/de-de/azure/automation/automation-webhooks?tabs=portal&quot; target=&quot;_blank&quot;&gt;Starten eines Azure Automation-Runbooks über einen Webhook | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automatisches Anlegen einer Dokumenten-Bibliothek in SharePoint:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In diesem Fall wird eine E-Mail versendet, in der automatisch eine Dokumenten-Bibliothek auf einer Kommunikationsseite im SharePoint einer Organisation angelegt werden soll. Dort sollen nur bestimmte Nutzer Zugriff erhalten.
Durch die E-Mail wird ein Flow ausgeführt, der die Webhook (URL) triggert und somit das Azure Automation Runbook ausführt.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/automatische-erstellung-sharepoint-strukturen/WebhookPowerAutomateBody.png&quot; alt=&quot;Anlegen einer Dokumenten-Bibliothek durch E-Mail-Trigger&quot; title=&quot;Anlegen einer Dokumenten-Bibliothek durch E-Mail-Trigger&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Azure Automation Runbook:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;(Mit &quot; #&quot; gekennzeichnete Zeilen sind auskommentiert und dienen nur dazu das Skript besser zu verstehen.)&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;powershell&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;#Webhook Verbindung
param
(
    [Parameter(Mandatory=$false)]
    [object] $WebhookData
)

#Output des Webhooks noch einmal wiedergeben
write-output $WebhookData.WebhookName
write-output $WebhookData.RequestBody
write-output $WebhookData.RequestHeader

$BibliothekName=$WebhookData.RequestBody

#Umlaute und Leerzeichen ersetzen, da SharePoint diese in der URL nicht erkennt.
$SecName=$BibliothekName.replace(&amp;quot; &amp;quot;, &amp;quot;_&amp;quot;).replace(&amp;quot;Ä&amp;quot;, &amp;quot;Ae&amp;quot;).replace(&amp;quot;ä&amp;quot;, &amp;quot;ae&amp;quot;).replace(&amp;quot;Ü&amp;quot;, &amp;quot;Ue&amp;quot;).replace(&amp;quot;ü&amp;quot;, &amp;quot;ue&amp;quot;).replace(&amp;quot;Ö&amp;quot;, &amp;quot;Oe&amp;quot;).replace(&amp;quot;ö&amp;quot;, &amp;quot;oe&amp;quot;)

####Exchange
#Mit Exchange Online verbinden
Connect-ExchangeOnline -ManagedIdentity -Organization tenant.onmicrosoft.com
#Erstellen einer Sicherheitsgruppe. Die auf diesen Ordner zugreifen dürfen.
New-DistributionGroup -Name &amp;quot;Sicherheitsgruppe1&amp;quot; -Type &amp;quot;Security&amp;quot; -Confirm:$false
#Sicherheitsgruppe Mitglieder hinzufügen
Add-DistributionGroupMember -Identity &amp;quot;Sicherheitsgruppe1&amp;quot; -Member &amp;quot;TestUser@tenant.com&amp;quot; -BypassSecurityGroupManagerCheck -Confirm:$false
#Verbindung zu Exchange trennen
Disconnect-ExchangeOnline -Confirm:$false

####PnP.Online
#Mit der SharePoint Site verbinden, auf dem die Dokumenten-Bibliothek angelegt werden soll
Connect-PnPOnline tenant.sharepoint.com -ManagedIdentity
#Neue Dokumenten Bibliothek erstellen mit Namen, der in der E-Mail stand
New-PnPList -Title $BibliothekName -Url $SecName -Template DocumentLibrary
#Wartezeit, damit alle wichtigen Änderungen erkannt werden (Kann auch weniger Zeit sein)
Start-Sleep -Seconds 180
#Inheritance der Ordner Brechen und Permissions setzen
Get-PnPFolder -Url &amp;quot;$SecName&amp;quot; | Set-PnPFolderPermission -List $BibliothekName -InheritPermissions
Set-PnPFolderPermission -List $BibliothekNa,e -Identity &amp;quot;$SecName&amp;quot;  -User &amp;quot;Sicherheitsgruppe1 -AddRole &amp;quot;Mitwirken&amp;quot; -ClearExisting&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;Weitere Einsatzmöglichkeiten in der SharePoint-Administration (Approvals, Site Creation mit Genehmigung):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input/Informationen, die durch eine Canvas-App eingegeben werden&lt;/li&gt;
&lt;li&gt;Self-Service für Mitarbeitende und Kontrolle mittels Freigaben/Genehmigungen&lt;/li&gt;
&lt;li&gt;Erstellen von SharePoint-Seiten, -Ordnern, -Dokumenten-Bibliotheken und -Listen&lt;/li&gt;
&lt;li&gt;Erstellen von Sicherheitsgruppen&lt;/li&gt;
&lt;li&gt;Verwalten von Berechtigungen&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Fazit&lt;/h2&gt;
&lt;p&gt;Mit Azure Automation lassen sich insbesondere administrative wiederkehrende Aufgaben mit überschaubarem Programmieraufwand automatisieren und beschleunigen. Dies bedeutet eine Minimierung manueller Aufwände und möglicher Fehlerquellen, effizientere Ressourcennutzung und schnellere Reaktionszeiten. In Verbindung mit SharePoint bietet Azure Automation eine effiziente und vielseitige Lösung zur kompletten oder teilweisen Automatisierung von IT-Prozessen in der Dokumentenverwaltung.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Automatically Open an Entity in a New Tab]]></title><description><![CDATA[Opening a record or an entity in a new tab seems pretty straightforward: Ctrl and click – and we are done, right? So why the need to write a…]]></description><link>https://www.pragmatic-development.io/blog/automatically-open-entity-in-new-tab/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/automatically-open-entity-in-new-tab/</guid><pubDate>Thu, 09 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Opening a record or an entity in a new tab seems pretty straightforward: Ctrl and click – and we are done, right? So why the need to write a whole blog post about it? Let me explain.&lt;/p&gt;
&lt;p&gt;A user could solve this task perfectly fine – but what if you needed to achieve the desired outcome automatically? Imagine the following scenario: your Power App or Model-driven application uses logic
to copy a record. Some attributes can directly be applied to the new entity, while others need to be manually inserted by the user. You would need the new entity to pop up for
the user to complete the missing data without blocking the original record nor obscuring the navigation. &lt;/p&gt;
&lt;div class=&quot;row justify-content-around&quot;&gt;
  &lt;div class=&quot;col-12 col-md-8&quot;&gt;
    &lt;img src=&quot;/images/blog/open-in-new-tab.gif&quot; alt=&quot;Open a record in a new tab&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;br/&gt;
&lt;p&gt;&lt;strong&gt;So how do we achieve this?&lt;/strong&gt; The short answer would be: Use openURL. But let&apos;s dive deeper.&lt;/p&gt;
&lt;p&gt;The challenge is to get the URL right. Lucky for us, the structure of Dynamics’ URLs is quite clear:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;baseUrl&gt;?appid=&amp;#x3C;appid&gt;&amp;#x26;pagetype=entityrecord&amp;#x26;etn=&amp;#x3C;logicalName&gt;&amp;#x26;id=&amp;#x3C;entityId&gt;[&amp;#x26;formid=&amp;#x3C;formId&gt;]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Looks complicated? Don’t worry. Our task is to simply determine each single part and attach it to form the URL. Let&apos;s start.&lt;/p&gt;
&lt;p&gt;First, we need the base URL of our current environment. This can be retrieved from the global context.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const baseUrl = Xrm.Utility.getGlobalContext().getClientUrl();&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;E.g.: &lt;a href=&quot;&quot;&gt;https://mybusiness.crm16.dynamics.com/main.aspx&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You might consider getting the name of the environment too. With it you can customize the URL depending on the current stage. E.g. mybusiness-dev or mybusiness-test&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const environmentName = baseUrl.split(&quot;//&quot;)?.[1].split(&quot;.&quot;)?.[0];&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You should be familiar with the app Id. But if you like to enhance this feature and open the entity in another app, then you might want to retrieve it from a settings file.&lt;/p&gt;
&lt;p&gt;The entity logical name and id depend on your use case and which entity you like to open. But it shouldn’t be a problem for you to get them.&lt;/p&gt;
&lt;p&gt;As a bonus, you could also specify the form which should be used. But this is not mandatory. In case you would like to specify it, just add  &lt;code&gt;&amp;#x26;formid=&amp;#x3C;formId&gt;&lt;/code&gt;  to the end of the URL.
Now that we have all parts, let’s put them together.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;const myUrl = `${baseUrl}/main.aspx?appid=${appId}&amp;#x26;pagetype=entityrecord&amp;#x26;etn=${entityLogicalName}&amp;#x26;id=${entityId}&amp;#x26;formid=${formId}`;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Hooray. We have a nice URL. The last remaining step is to call it:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Xrm.Navigation.openUrl(myUrl);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In the end it comes down to this little function.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;typescript&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;static openInNewTab(entityId: string, entityLogicalName: string) {
    const baseUrl = Xrm.Utility.getGlobalContext().getClientUrl();
    const caseServiceUrl = `${baseUrl}/main.aspx?appid=&amp;lt;your App Id&amp;gt;&amp;amp;pagetype=entityrecord&amp;amp;etn=${entityLogicalName}&amp;amp;id=${entityId}`;
    Xrm.Navigation.openUrl(caseServiceUrl);
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;I hope you find this post useful and will use this functionality in your next project or anytime you would like to open an entity in a new tab or even in another app.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[File storage solutions in Model-Driven App scenarios]]></title><description><![CDATA[Whenever we design software systems, it is crucial to create and manage documents in a way that makes them accessible through the UI in a…]]></description><link>https://www.pragmatic-development.io/blog/power-apps-file-storage-solutions/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/power-apps-file-storage-solutions/</guid><pubDate>Sun, 10 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Whenever we design software systems, it is crucial to create and manage documents in a way that makes them accessible through the UI in a streamlined fashion - ideally without leaving the system boundaries. So, let&apos;s take a look at different file storage solutions and how they can be applied in your model-driven app scenario.&lt;/p&gt;
&lt;h2&gt;SharePoint Online Integration&lt;/h2&gt;
&lt;p&gt;Since the early Dynamics Customer Engagement times, you could store files through a model-driven app by using the built-in SharePoint integration.
Meaning you can access files through the documents grid in the model-driven app in the context of a record:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/power-apps-file-strorage-solutions/SPO-Files.png&quot; alt=&quot;Show files stored in SharePoint in a model-driven app.&quot;&gt;
... or by opening the menu Related ➡️Documents:
&lt;img src=&quot;/images/blog/power-apps-file-strorage-solutions/access-files-related-menu.png&quot; alt=&quot;Show files backed in SharePoint document library.&quot;&gt;
... as well as via SharePoint Document Library:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/power-apps-file-strorage-solutions/SPO.png&quot; alt=&quot;Show files backed in SharePoint document library.&quot;&gt;
The SharePoint integration can bei configured for standard and custom tables and is only limited by the restrictions of the underlying SharePoint.&lt;/p&gt;
&lt;h3&gt;Integrate documents subgrid on the main form&lt;/h3&gt;
&lt;p&gt;Your users might find it too difficult or inefficient to navigate to the files tab everytime they need to view a document. In this case, try this more user-friendly approach and directly embed the documents subgrid on a main form&apos;s section as subgrid. This is not possible through the app designer but needs to be done directly by manipulating the form xml as described in the docs: &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/maker/model-driven-apps/add-documents-tab-entity-main-form&quot; target=&quot;_blank&quot; title=&quot;Add or remove the SharePoint documents tab to the main form for any table&quot;&gt;Add the Documents tab to the main form for a table&lt;/a&gt;.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;xml&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;  &amp;lt;control id=&amp;quot;DocumentSubGrid&amp;quot; classid=&amp;quot;{E7A81278-8635-4d9e-8D4D-59480B391C5B}&amp;quot; indicationOfSubgrid=&amp;quot;true&amp;quot; uniqueid=&amp;quot;{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}&amp;quot;&amp;gt; 
    &amp;lt;parameters&amp;gt; 
      &amp;lt;ViewId&amp;gt;{0016F9F3-41CC-4276-9D11-04308D15858D}&amp;lt;/ViewId&amp;gt; 
      &amp;lt;IsUserView&amp;gt;false&amp;lt;/IsUserView&amp;gt;         
      &amp;lt;RelationshipName&amp;gt;Account_SharepointDocument&amp;lt;/RelationshipName&amp;gt;
      &amp;lt;TargetEntityType&amp;gt;sharepointdocument&amp;lt;/TargetEntityType&amp;gt; 
      &amp;lt;AutoExpand&amp;gt;Fixed&amp;lt;/AutoExpand&amp;gt; 
      &amp;lt;EnableQuickFind&amp;gt;false&amp;lt;/EnableQuickFind&amp;gt; 
      &amp;lt;EnableViewPicker&amp;gt;true&amp;lt;/EnableViewPicker&amp;gt; 
      &amp;lt;ViewIds /&amp;gt; 
      &amp;lt;EnableJumpBar&amp;gt;false&amp;lt;/EnableJumpBar&amp;gt; 
      &amp;lt;ChartGridMode&amp;gt;Grid&amp;lt;/ChartGridMode&amp;gt; 
      &amp;lt;VisualizationId /&amp;gt; 
      &amp;lt;IsUserChart&amp;gt;false&amp;lt;/IsUserChart&amp;gt; 
      &amp;lt;EnableChartPicker&amp;gt;false&amp;lt;/EnableChartPicker&amp;gt; 
      &amp;lt;RecordsPerPage&amp;gt;10&amp;lt;/RecordsPerPage&amp;gt; 
      &amp;lt;HeaderColorCode&amp;gt;#F3F3F3&amp;lt;/HeaderColorCode&amp;gt; 
    &amp;lt;/parameters&amp;gt; 
  &amp;lt;/control&amp;gt;&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;All files will be grouped under the context of a specific Dataverse record and displayed in a list. While this tackles many document management challenges, it can definitely make things difficult if you want to be able to associate a certain document type (e.g., a contract, final quote document, etc.) on a record and want to make this information accessible through business logic. For example, allow proceeding of Business Process Flow only if a certain document is provided. Let&apos;s take a look at how to handle that situation in the next section.&lt;/p&gt;
&lt;h2&gt;Dataverse File Type Column&lt;/h2&gt;
&lt;p&gt;Dataverse has a dedicated &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/files-images-overview?tabs=sdk&quot; target=&quot;_blank&quot; title=&quot;Files and Images Overview on Microsoft Learn&quot;&gt;File Datatype&lt;/a&gt; column that allows storage of a single file. This file is rendered as a regular control and can be placed directly on the form. As you can see from the screenshot, you could even set business required field levels as well as file size upper boundaries.
&lt;img src=&quot;/images/blog/power-apps-file-strorage-solutions/FileTypeColumn.png&quot; alt=&quot;Show files backed in SharePoint document library.&quot;&gt;
File upload via the webclient is limited to 128 MB, though via the API it is possible to store up to 10 GB. You can globally restrict the permitted filetype through the system &lt;a href=&quot;https://learn.microsoft.com/en-us/power-platform/admin/system-settings-dialog-box-general-tab&quot; target=&quot;_blank&quot; title=&quot;System Settings general tab on Microsoft Learn&quot;&gt;settings general tab&lt;/a&gt;.
&lt;img src=&quot;/images/blog/power-apps-file-strorage-solutions/FileTypeForm.png&quot; alt=&quot;Show files backed in SharePoint document library.&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Interaction server side&lt;/h3&gt;
&lt;p&gt;You can access the uploaded binary data by using a Power Automate flow with the &lt;a href=&quot;https://learn.microsoft.com/en-us/power-automate/dataverse/upload-download-file&quot; target=&quot;_blank&quot; title=&quot;Upload or download image and file content&quot;&gt;Download a file or an image&lt;/a&gt; action. Alternatively, WebAPI Actions will allow you to download or upload files. Large uploads can be split into chunks to work more reliably.&lt;/p&gt;
&lt;p&gt;Sample code for upload and download: &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/file-column-data?tabs=sdk#use-dataverse-messages-to-upload-a-file&quot; target=&quot;_blank&quot; title=&quot;Use dataverse messages to upload a file&quot;&gt;Use file column data (Microsoft Dataverse)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The following messages are available for interacting with file type column data:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Upload&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/initializefileblocksupload?view=dataverse-latest&quot; target=&quot;_blank&quot; title=&quot;Initialize File Blocks Upload&quot;&gt;InitializeFileBlocksUpload&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/uploadblock?view=dataverse-latest&quot; target=&quot;_blank&quot; title=&quot;Upload Block&quot;&gt;UploadBlock&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/commitfileblocksupload?view=dataverse-latest&quot; target=&quot;_blank&quot; title=&quot;Commit File Blocks Upload&quot;&gt;CommitFileBlocksUpload&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Download&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/initializefileblocksdownload?view=dataverse-latest&quot; target=&quot;_blank&quot; title=&quot;Initialize File Blocks Download&quot;&gt;InitializeFileBlocksDownload&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/downloadblock?view=dataverse-latest&quot; target=&quot;_blank&quot; title=&quot;Download Block&quot;&gt;DownloadBlock&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For additonal information regarding the datamodel behind the file type, you can find details in the &lt;a href=&quot;https://learn.microsoft.com/en-us/power-apps/developer/data-platform/file-column-data&quot; target=&quot;_blank&quot; title=&quot;File column data&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I would like to highlight that the stored files do not consume database capacity as they are using the file capacity of the Dataverse storage system.&lt;/p&gt;
&lt;h3&gt;Interaction client side&lt;/h3&gt;
&lt;p&gt;The attribute on the form can be queried just like a regular column. Executing &lt;code&gt;formContext.getAttribute(&apos;new_filetypecolumn&apos;).getValue();&lt;/code&gt; will present the following information about the uploaded file:&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;json&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;{
    &amp;quot;attributeName&amp;quot;: &amp;quot;new_filetypecolumn&amp;quot;,
    &amp;quot;fileName&amp;quot;: &amp;quot;Lieferschein Nr. 2022122901.pdf&amp;quot;,
    &amp;quot;fileSize&amp;quot;: 213212,
    &amp;quot;fileUrl&amp;quot;: &amp;quot;https://orgname.crm4.dynamics.com/api/data/v9.0/accounts(8f5b9adb-79a8-ed11-aad1-0022489c4f48)/new_filetypecolumn/$value&amp;quot;,
    &amp;quot;mimeType&amp;quot;: &amp;quot;application/pdf&amp;quot;
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;Accessing the url in fileUrl property allows downloading the file directly. &lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;When designing the file storage solution for your app, consider whether information about the existence of specific documents is required, as they might be associated with events in your business logic. If so, you should prefer using a file type column since querying and accessibility are far easier.&lt;/p&gt;
&lt;p&gt;On the other hand, if you need to store multiple files for a record or need even more advanced capabilities like SharePoint’s full text search or collaborative functionality on Microsoft Office file types, then SharePoint Document Integration is definitely the way to go.&lt;/p&gt;
&lt;p&gt;P.S. If you found this article helpful, be sure to also learn how to &lt;a href=&quot;https://www.pragmatic-development.io/blog/automatically-create-subfolders-in-sharepoint-after-record-is-saved-in-a-model-driven-app/&quot;&gt;Automatically create subfolders in SharePoint after record is saved in a model-driven App&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Migrate Steps and Custom Apis to new assembly with a fully automated process]]></title><description><![CDATA[If a plugin-type needs to be renamed or removed completely in a Dataverse environment it is necessary to take some additional steps to make…]]></description><link>https://www.pragmatic-development.io/blog/migrate-steps-and-custom-apis-to-new-assembly-with-a-fully-automated-process/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/migrate-steps-and-custom-apis-to-new-assembly-with-a-fully-automated-process/</guid><pubDate>Tue, 18 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If a plugin-type needs to be renamed or removed completely in a Dataverse environment it is necessary to take some additional steps to make sure that managed solution deployments that incorporate the removal/rename of a plugin-type are still successful. As manually updating all steps and APIs to the new assembly can be tedious and error-prone we&apos;ll come up with a fully automated process.&lt;/p&gt;
&lt;h3&gt;Simply removing or renaming a plugin-type causes error&lt;/h3&gt;
&lt;p&gt;Imagine you have a plugin assembly with a plugin-type called &lt;code&gt;Plugins.SamplePlugin1&lt;/code&gt; and &lt;code&gt;Plugins.SamplePlugin2&lt;/code&gt;. If you decide to remove/rename plugin-type &lt;code&gt;Plugins.SamplePlugin2&lt;/code&gt; from the assembly and try to update the assembly content, you are presented with the error below:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Plug-in assembly does not contain the required types or assembly content cannot be updated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The probably tempting approach is to just delete the plugin-type with Plugin Registration Tool. While this would work in an unmanaged scenario it will definitly fail once you move to the next stage in a managed solution and try the very same thing upfront in a pre-deployment task. Only the error message differs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The evaluation of the current component(name=SdkMessageProcessingStep, id=XXX) in the current operation (Delete) failed during managed property evaluation of condition: {Managed Property Name: ismanaged}&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Solution: Steps and custom-api need to reference the new assembly&lt;/h3&gt;
&lt;p&gt;So what is the solution? To correctly remove the plugin-type it is required to bump the assembly version on the first two digits (&lt;strong&gt;major.minor&lt;/strong&gt;.build.revision), while third and fourth do not change assembly handling from a Dataverse standpoint.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/migrate-steps-and-custom-apis-to-new-assembly/bump-assembly-version.png&quot; alt=&quot;VisualStudio Dialog to change the assembly version.&quot;&gt;&lt;/p&gt;
&lt;p&gt;After that you cannot do an in-place update but have to re-register that assembly as new and fresh assembly.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/migrate-steps-and-custom-apis-to-new-assembly/registered-assembly-with-two-versions.png&quot; alt=&quot;Plugin Registration Tool showing assembly in two different versions&quot;&gt;&lt;/p&gt;
&lt;p&gt;You end up with a new registered assembly without any steps associated. Furthermore, in case you have custom APIs defined, those will also reference the plugin-types in the original plugin assembly.&lt;/p&gt;
&lt;h3&gt;An automated solution for removing or renaming plugin-types&lt;/h3&gt;
&lt;p&gt;Before you now think about updating the plugin steps and custom apis by hand we will provide you with a programmatic approach to updating the steps and custom-apis to the plugin-types in the new assembly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Line 4: Change the assembly name&lt;/li&gt;
&lt;li&gt;Line 5: Change the original version number&lt;/li&gt;
&lt;li&gt;Line 6: Change the new version number&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you run the method &lt;code&gt;MigrateStepsAndApisToNewAssemblyVersion&lt;/code&gt; it will look for the assemblies in the given version number and update the associated plugin-types and apis to use the new assembly.&lt;/p&gt;
&lt;p&gt;For diagnostic purposes new plugin-types as well as plugin-types that are not present in the new assembly are documented in lines 81/85.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;static async Task Main(string[] args)
{
    using var serviceClient = new ServiceClient(&amp;quot;ADD-YOUR-CONNECTIONSTRING&amp;quot;);
    const string assemblyName = &amp;quot;Pragdev.Sample.Plugins&amp;quot;;
    var firstVersion = &amp;quot;2.0.0.0&amp;quot;;
    var secondVersion = &amp;quot;3.0.0.0&amp;quot;;

    await MigrateStepsAndApisToNewAssemblyVersion(serviceClient, assemblyName, firstVersion, secondVersion);
}

private static async Task MigrateStepsAndApisToNewAssemblyVersion(ServiceClient serviceClient, string assemblyName, string firstVersion, string secondVersion)
{
    // Get assembly by name
    var assemblies = await serviceClient.RetrieveMultipleAsync(new QueryExpression(&amp;quot;pluginassembly&amp;quot;)
    {
        ColumnSet = new ColumnSet(&amp;quot;version&amp;quot;),
        Criteria =
        {
            Conditions =
            {
                new ConditionExpression(&amp;quot;name&amp;quot;, ConditionOperator.Equal, assemblyName),
            }
        }
    });

    if (assemblies.Entities.Count == 0)
    {
        Console.WriteLine(&amp;quot;Assembly {0} not found.&amp;quot;, assemblyName);
    }

    var firstVersionAssembly = assemblies.Entities
        .FirstOrDefault(a =&amp;gt; string.Equals(a.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;version&amp;quot;), firstVersion));

    if (firstVersionAssembly == null)
    {
        Console.WriteLine(&amp;quot;Assembly {0} not found in version {1}.&amp;quot;, assemblyName, firstVersion);
    }

    var secondVersionAssembly = assemblies.Entities
        .FirstOrDefault(a =&amp;gt; string.Equals(a.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;version&amp;quot;), secondVersion));

    if (secondVersionAssembly == null)
    {
        Console.WriteLine(&amp;quot;Assembly {0} not found in version {1}.&amp;quot;, assemblyName, secondVersion);
    }

    Console.WriteLine(&amp;quot;Searching for types in version {0}.&amp;quot;, firstVersion);
    var typesFirst = await serviceClient.RetrieveMultipleAsync(new QueryExpression(&amp;quot;plugintype&amp;quot;)
    {
        ColumnSet = new ColumnSet(&amp;quot;typename&amp;quot;),
        Criteria =
        {
            Conditions =
            {
                new ConditionExpression(&amp;quot;pluginassemblyid&amp;quot;, ConditionOperator.Equal, firstVersionAssembly.Id),
            }
        }
    });

    Console.WriteLine(&amp;quot;Found {0} types in first assembly.&amp;quot;, typesFirst.Entities.Count);

    Console.WriteLine(&amp;quot;Searching for types in version {0}.&amp;quot;, secondVersion);
    var typesSecond = await serviceClient.RetrieveMultipleAsync(new QueryExpression(&amp;quot;plugintype&amp;quot;)
    {
        ColumnSet = new ColumnSet(&amp;quot;typename&amp;quot;),
        Criteria =
        {
            Conditions =
            {
                new ConditionExpression(&amp;quot;pluginassemblyid&amp;quot;, ConditionOperator.Equal, secondVersionAssembly.Id),
            }
        }
    });
    Console.WriteLine(&amp;quot;Found {0} types in second assembly.&amp;quot;, typesSecond.Entities.Count);


    var typeNamesInFirst = typesFirst.Entities.Select(e =&amp;gt; e.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;typename&amp;quot;)).OrderBy(x =&amp;gt; x).ToList();
    var typeNamesInSecond = typesSecond.Entities.Select(e =&amp;gt; e.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;typename&amp;quot;)).OrderBy(x =&amp;gt; x).ToList();

    var typesMissingInNewAssembly = typeNamesInFirst.Except(typeNamesInSecond, StringComparer.InvariantCultureIgnoreCase).ToList();
    Console.WriteLine(&amp;quot;Types missing in new Assembly: {0}&amp;quot;, typesMissingInNewAssembly.Any() ? string.Join(&amp;quot;, &amp;quot;, typesMissingInNewAssembly) : &amp;quot;none&amp;quot;);


    var typesNewlyCreatedInNewAssembly = typeNamesInSecond.Except(typeNamesInFirst, StringComparer.InvariantCultureIgnoreCase).ToList();
    Console.WriteLine(&amp;quot;Types newly created in new Assembly: {0}&amp;quot;, typesNewlyCreatedInNewAssembly.Any() ? string.Join(&amp;quot;, &amp;quot;, typesNewlyCreatedInNewAssembly) : &amp;quot;none&amp;quot;);

    foreach (var pluginType in typesSecond.Entities)
    {
        var typeName = pluginType.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;typename&amp;quot;);

        if(!typeName.Contains(&amp;quot;preserve&amp;quot;, StringComparison.OrdinalIgnoreCase))
        {
            continue;
        }

        Console.WriteLine(&amp;quot;Analyzing plugintype: &amp;quot; + typeName);

        var typeInBothAssemblies = typesFirst.Entities
            .FirstOrDefault(pt =&amp;gt;
                string.Equals(pt.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;typename&amp;quot;), typeName, StringComparison.OrdinalIgnoreCase));


        if (typeInBothAssemblies != null)
        {
            var stepsFirst = await serviceClient.RetrieveMultipleAsync(
                new QueryExpression(&amp;quot;sdkmessageprocessingstep&amp;quot;)
                {
                    ColumnSet = new ColumnSet(),
                    Criteria =
                    {
                        Conditions =
                        {
                            new ConditionExpression(&amp;quot;plugintypeid&amp;quot;, ConditionOperator.Equal, typeInBothAssemblies.Id),
                            new ConditionExpression(&amp;quot;category&amp;quot;, ConditionOperator.NotEqual, &amp;quot;CustomAPI&amp;quot;),
                        }
                    }
                });

            Console.WriteLine(&amp;quot;Found {0} steps for {1}.&amp;quot;, stepsFirst.Entities.Count, typeName);

            foreach (var step in stepsFirst.Entities)
            {
                step[&amp;quot;plugintypeid&amp;quot;] = pluginType.ToEntityReference();
                try
                {
                    await serviceClient.UpdateAsync(step);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(&amp;quot;Error updating step {0} for type {1}&amp;quot;, step.Id, typeName);
                    Console.WriteLine(ex.ToString());
                }
            }

            var apiFirst = await serviceClient.RetrieveMultipleAsync(
                new QueryExpression(&amp;quot;customapi&amp;quot;)
                {
                    ColumnSet = new ColumnSet(),
                    Criteria =
                    {
                        Conditions =
                        {
                            new ConditionExpression(&amp;quot;plugintypeid&amp;quot;, ConditionOperator.Equal, typeInBothAssemblies.Id),
                        }
                    }
                });

            Console.WriteLine(&amp;quot;Found {0} apis for {1}.&amp;quot;, apiFirst.Entities.Count, typeName);

            foreach (var api in apiFirst.Entities)
            {
                api[&amp;quot;plugintypeid&amp;quot;] = pluginType.ToEntityReference();
                try
                {
                    await serviceClient.UpdateAsync(api);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(&amp;quot;Error updating api {0} for type {1}&amp;quot;, api.Id, typeName);
                    Console.WriteLine(ex.ToString());
                }
            }
        }
        else
        {
            Console.WriteLine(&amp;quot;Plugintype: &amp;quot; + typeName + &amp;quot; is not contained in both assemblies.&amp;quot;);
        }
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;In this article we have walked through the process of renaming/removing a plugin-type in a Dataverse environment and correctly deploying your changes through managed solutions to the target environment. Furthermore, we automated the recurring tasks to prevent errors and make sure that steps and custom-apis reference the new assembly. I hope this approach will help you next time you need to update a plugin-assembly in your managed solution.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Responsive Design in Canvas Apps & Custom Pages]]></title><description><![CDATA[With Custom Pages and Canvas Apps, there is a way to design custom Low Code applications from a blank page for the Power Platform. These…]]></description><link>https://www.pragmatic-development.io/blog/responsive-design-in-canvas-apps-and-custom-pages/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/responsive-design-in-canvas-apps-and-custom-pages/</guid><pubDate>Sun, 10 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;With Custom Pages and Canvas Apps, there is a way to design custom Low Code applications from a blank page for the Power Platform. These kinds of apps require some thought to be put into achieving a responsive layout. By default, the designer puts an absolute number to most of the heights and widths. In this article, I share my key takeaways to designing responsive Canvas Apps. These of course also apply for Custom Pages that bring the possibilities of Canvas Apps into Model-driven apps.&lt;/p&gt;
&lt;h2&gt;Mobile vs bigger screens (or tablets) - and start from there&lt;/h2&gt;
&lt;p&gt;At first, it is important to understand how your app will be (mainly) used: on mobile or bigger screens (like tablets or desktops). Although the apps can have a responsive layout, it is always easier to start with the screen dimensions that most users have. So ask yourself: how will your app be used? Especially for Canvas Apps you need to decide on the basic layout when creating the app. Everything else can be configured later in the display settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/canvas-app-format-settings.png&quot; alt=&quot;Canvas App Format Settings&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Set the correct display settings&lt;/h2&gt;
&lt;p&gt;In the designer, you have a couple of display settings you can specify for your app. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/general-display-settings.png&quot; alt=&quot;General Display Settings&quot;&gt;&lt;/p&gt;
&lt;p&gt;Changing these settings can have an impact on your specified dimensions on elements or containers.&lt;/p&gt;
&lt;p&gt;Check out the docs for more details &lt;a href=&quot;https://docs.microsoft.com/en-us/power-apps/maker/canvas-apps/set-aspect-ratio-portrait-landscape&quot;&gt;here&lt;/a&gt;!&lt;/p&gt;
&lt;h2&gt;Use containers (a lot)&lt;/h2&gt;
&lt;p&gt;Using containers is key for every responsive layout. Nearly everything needs to be wrapped inside a container. There are three main types of containers that can be nested depending on your requirements.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/container-types.png&quot; alt=&quot;Container Types&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vertical Container - Stack elements or containers vertically&lt;/li&gt;
&lt;li&gt;Horizontal Container - Stack elements or containers vertically&lt;/li&gt;
&lt;li&gt;Container - Wrap certain elements&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Use absolute heights for absolute heights&lt;/h2&gt;
&lt;p&gt;Whenever you want to have a fixed height at some elements or containers an absolute height needs to be specified. Otherwise, a relative layout could crash the dimensions of these containers. In the examples, the header container has a fixed height of 100 Pixels and the height of the content container is calculated.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/container-with-absolute-height.png&quot; alt=&quot;Container with Absolute Height&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Use flexible heights for flexible heights&lt;/h2&gt;
&lt;p&gt;For containers, you can also specify a flexible height including the portions of the available space they are taking up. In this example, we have a fixed header (100 px) and the content (90 %) and footer (10 %) container are sharing the remaining space based on their portions.&lt;/p&gt;
&lt;p&gt;Remember to also set a minimum height so that your layout does not screw up on smaller screens. The height of the label should also be based on the &lt;code&gt;Parent.Height&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/container-with-flexible-height.png&quot; alt=&quot;Container with Flexible Height&quot;&gt;&lt;/p&gt;
&lt;p&gt;This also will be applied if you have specified an absolute height for a container which can be confusing. Usually I therefore also reference the &lt;code&gt;Parent.Height&lt;/code&gt; so that I know that a relative height has already been taken care of.&lt;/p&gt;
&lt;h2&gt;Use alignments for (child) elements&lt;/h2&gt;
&lt;p&gt;Whenever you create responsive containers make sure to specify the alignment so that the available space is filled up properly.&lt;/p&gt;
&lt;p&gt;In the following example, the container is configured to stretch vertically and horizontally.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/container-alignments.png&quot; alt=&quot;Container Alignments&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Use relative heights and widths everywhere&lt;/h2&gt;
&lt;p&gt;We have already looked at the heights in detail. The same applies to the widths of course. Specify absolute sizes only if necessary and use relative sizing / responsive container alignments everywhere else. A single absolute value could potentially screw up the responsive layout.&lt;/p&gt;
&lt;h2&gt;Testing your layouts&lt;/h2&gt;
&lt;p&gt;You can use the Developer Tools of all modern browsers for testing your layouts. Open the Device Emulation inside the Dev Tools. There are already some of the dimensions pre-configured but you could also specify your own. Also, you can change the screen orientation through the toolbar on the top of the emulation window.&lt;/p&gt;
&lt;p&gt;The following screenshot shows a Canvas App with Dev tools opened. But the same applies to (embedded) Custom Pages. There you can also hide the purple navigation bar of the Maker portal by appending &lt;code&gt;&amp;#x26;hideNavbar=true&lt;/code&gt; onto the URL.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/developer-tools-for-layout-testing.png&quot; alt=&quot;Developer Tools for Layout Testing&quot;&gt;&lt;/p&gt;
&lt;h2&gt;More to consider&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Calculating the X and Y values of containers or elements (e.g. for applying left and right paddings of 10 px) and a relative width&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/padding-calculations-inside-containers.png&quot; alt=&quot;Padding Calculations inside Containers&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use the configuration of wrapping containers and their heights for relative font sizes (but maybe set a minimum width as well)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/responsive-design-in-canvas-apps-and-custom-pages/relative-font-sizes.png&quot; alt=&quot;Relative Font Sizes&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Use flyout menus on small screens&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The editor experience provides everything you need for building responsive Power Apps. Start by choosing the general display settings and make sure that you have explicitly updated every height and every width of every container or element.&lt;/p&gt;
&lt;p&gt;Need help with your responsive Power Apps? Feel free to send me an email!&lt;/p&gt;</content:encoded></item><item><title><![CDATA[Automatically create subfolders in SharePoint after record is saved in a model-driven App]]></title><description><![CDATA[If you need to automatically set up a SharePoint folder structure for your users in a model-driven app in real-time, it is easy to…]]></description><link>https://www.pragmatic-development.io/blog/automatically-create-subfolders-in-sharepoint-after-record-is-saved-in-a-model-driven-app/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/automatically-create-subfolders-in-sharepoint-after-record-is-saved-in-a-model-driven-app/</guid><pubDate>Mon, 20 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you need to automatically set up a SharePoint folder structure for your users in a model-driven app in real-time, it is easy to accomplish that with a few lines of code.
Power Apps have a &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/user/collaborate-using-sharepoint&quot;&gt;build-in integration&lt;/a&gt; for file storage inside of a SharePoint document library, if you enable document management on that table.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/CreateFoldersAutomatically.gif&quot; alt=&quot;Showcase folder creation&quot; title=&quot;Showcase folder creation&quot;&gt;&lt;/p&gt;
&lt;p&gt;When you initially access the documents subgrid a new folder is automatically created – accompanied by a record in &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/reference/entities/sharepointdocumentlocation&quot;&gt;SharePointDocumentLocation&lt;/a&gt; table. This record is the glue that ties the SharePoint folder to a specific record in Dataverse. The referenced record is saved in the &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/reference/entities/sharepointdocumentlocation#BKMK_RegardingObjectId&quot;&gt;RegardingObjectId&lt;/a&gt;. &lt;/p&gt;
&lt;h2&gt;The objective&lt;/h2&gt;
&lt;p&gt;If we want to automatically create a certain folder structure for our users to initially work with, we must set up this as a prerequisite before we can create any folders, since we do not know when users will access the subgrid for the first time. The upcoming solution uses the same request pattern that the platform is using for the OOB functionality of the document’s subgrid.&lt;/p&gt;
&lt;h2&gt;A Solution&lt;/h2&gt;
&lt;p&gt;Use the JavaScript function below in an onload event to create three folders once the record is initially saved. It will only trigger the folder creation once as we determine inside the function if folder creation is necessary.&lt;/p&gt;
&lt;h3&gt;Things to know&lt;/h3&gt;
&lt;p&gt;This is client-side code. It runs in the browser so there won&apos;t be any folder creation if records get created from workflows or via Web API. If that is an objective you need to revert to Server side logic in Power Automate or try to postpone folder creation until the point when the user is accessing the documents subgrid of the record.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;typescript&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;namespace BlogSample {
    let isCreate = false;

    /**
     * Creates folders in default location on entity creation. Upon first load isCreate will be
     * initialized to true and after save with the subsequent onload the folders will be created.
     */
    export async function createFolders(context: Xrm.Events.EventContext) {
        const fCtx = context.getFormContext();
        if (fCtx.ui.getFormType() === XrmEnum.FormType.Create) {
            isCreate = true;
            console.log(&amp;quot;first load.&amp;quot;)
            return;
        }

        if (isCreate) {
            console.log(&amp;quot;Need to create folders.&amp;quot;);

            isCreate = false;
            const entityId = fCtx.data.entity.getId();
            const entityLogicalName = fCtx.data.entity.getEntityName();
            await retrieveSPDocuments(entityId, entityLogicalName);

            const foldersToCreate = [&amp;quot;FolderOne&amp;quot;, &amp;quot;FolderTwo&amp;quot;, &amp;quot;FolderThree&amp;quot;]

            for (const folder of foldersToCreate) {

                await createFolder(entityId, entityLogicalName, folder);
            }
            return;
        }

        console.log(&amp;quot;nothing to do.&amp;quot;)
    }

    /**
     * Retrieves elements in default sharepoint location or its subdirectory.
     * @param regardingObjectid The id of the record we want query documents for.
     * @param regardingObjectName The logical name of the record we want query documents for.
     * @param relativeLocation Queries for elements in a subdirectory.
     * @returns 
     */
    async function retrieveSPDocuments(regardingObjectid: string, regardingObjectName: string, relativeLocation: string = &amp;quot;&amp;quot;) {
        let relLoc = `&amp;lt;filter type=&amp;quot;and&amp;quot;&amp;gt;
                            &amp;lt;condition attribute=&amp;quot;relativelocation&amp;quot; operator=&amp;quot;eq&amp;quot; value=&amp;quot;${relativeLocation}&amp;quot;/&amp;gt;
                          &amp;lt;/filter&amp;gt;`;
        let query = `&amp;lt;fetch distinct=&amp;quot;false&amp;quot; mapping=&amp;quot;logical&amp;quot; returntotalrecordcount=&amp;quot;true&amp;quot; no-lock=&amp;quot;false&amp;quot;&amp;gt;
                            &amp;lt;entity name=&amp;quot;sharepointdocument&amp;quot;&amp;gt;
                                &amp;lt;attribute name=&amp;quot;documentid&amp;quot;/&amp;gt;
                                &amp;lt;attribute name=&amp;quot;fullname&amp;quot;/&amp;gt;
                                &amp;lt;attribute name=&amp;quot;relativelocation&amp;quot;/&amp;gt;
                                &amp;lt;attribute name=&amp;quot;filetype&amp;quot;/&amp;gt;
                                &amp;lt;attribute name=&amp;quot;absoluteurl&amp;quot;/&amp;gt;
                                &amp;lt;attribute name=&amp;quot;title&amp;quot;/&amp;gt;
                                &amp;lt;attribute name=&amp;quot;sharepointdocumentid&amp;quot;/&amp;gt;
                                &amp;lt;order attribute=&amp;quot;relativelocation&amp;quot; descending=&amp;quot;false&amp;quot;/&amp;gt;
                                &amp;lt;filter&amp;gt;
                                    &amp;lt;condition attribute=&amp;quot;isrecursivefetch&amp;quot; operator=&amp;quot;eq&amp;quot; value=&amp;quot;0&amp;quot;/&amp;gt;
                                &amp;lt;/filter&amp;gt;
                                ${(relativeLocation !== &amp;quot;&amp;quot;) ? relLoc : &amp;quot;&amp;quot;}
                                &amp;lt;link-entity name=&amp;quot;${regardingObjectName}&amp;quot; from=&amp;quot;${regardingObjectName}id&amp;quot; to=&amp;quot;regardingobjectid&amp;quot; alias=&amp;quot;bb&amp;quot;&amp;gt;
                                    &amp;lt;filter type=&amp;quot;and&amp;quot;&amp;gt;
                                        &amp;lt;condition attribute=&amp;quot;${regardingObjectName}id&amp;quot; operator=&amp;quot;eq&amp;quot; value=&amp;quot;${regardingObjectid}&amp;quot;/&amp;gt;
                                    &amp;lt;/filter&amp;gt;
                                &amp;lt;/link-entity&amp;gt;
                            &amp;lt;/entity&amp;gt;
                        &amp;lt;/fetch&amp;gt;`;

        let spDocuments = await Xrm.WebApi.retrieveMultipleRecords(&amp;quot;sharepointdocument&amp;quot;, `?fetchXml=${encodeURIComponent(query)}`);

        return spDocuments;
    }

    /**
     * Create a folder for given entity record in default sharepoint location.
     * @param regardingObjectid The id of the record we want query documents for.
     * @param regardingObjectName The logical name of the record we want query documents for.
     * @param folderName The name of the new folder.
     * @param folderPath Create in subdirectory
     * @returns 
     */
    async function createFolder(regardingObjectid: string, regardingObjectName: string, folderName: string, folderPath: string = &amp;quot;&amp;quot;) {
        console.log(`Create SP folder ${folderName} for ${regardingObjectName}: ${regardingObjectid}`);

        let spDocuments = await retrieveSPDocuments(regardingObjectid, regardingObjectName);

        const folderExists = spDocuments.entities.some((x: any) =&amp;gt; x.fullname === folderName);

        if (folderExists) {
            console.log(`Folder &amp;#39;${folderName}&amp;#39; already exists!`);
            return spDocuments;
        }

        console.log(`Creating folder: ${folderName} in path: ${folderPath}`);

        let parentEntityRef: any = {};
        parentEntityRef[&amp;quot;@odata.type&amp;quot;] = `Microsoft.Dynamics.CRM.${regardingObjectName}`;
        parentEntityRef[`${regardingObjectName}id`] = `${regardingObjectid}`;

        let folderPayload = {
            &amp;quot;FileName&amp;quot;: folderName,
            &amp;quot;LocationId&amp;quot;: &amp;quot;00000000-0000-0000-0000-000000000000&amp;quot;,
            &amp;quot;ParentEntityReference&amp;quot;: parentEntityRef,
            &amp;quot;IsFolder&amp;quot;: true,
            &amp;quot;FolderPath&amp;quot;: folderPath
        };

        await fetch(`${Xrm.Utility.getGlobalContext().getClientUrl()}/api/data/v${Xrm.Utility.getGlobalContext().getVersion().substring(0, 3)}/NewDocument`, {
            &amp;quot;headers&amp;quot;: {
                &amp;quot;accept&amp;quot;: &amp;quot;application/json&amp;quot;,
                &amp;quot;content-type&amp;quot;: &amp;quot;application/json&amp;quot;,
                &amp;quot;odata-maxversion&amp;quot;: &amp;quot;4.0&amp;quot;,
                &amp;quot;odata-version&amp;quot;: &amp;quot;4.0&amp;quot;,
                &amp;quot;prefer&amp;quot;: &amp;quot;odata.include-annotations=\&amp;quot;*\&amp;quot;&amp;quot;,
                &amp;quot;sec-fetch-dest&amp;quot;: &amp;quot;empty&amp;quot;,
                &amp;quot;sec-fetch-mode&amp;quot;: &amp;quot;cors&amp;quot;,
                &amp;quot;sec-fetch-site&amp;quot;: &amp;quot;same-origin&amp;quot;
            },
            &amp;quot;body&amp;quot;: JSON.stringify(folderPayload),
            &amp;quot;method&amp;quot;: &amp;#39;POST&amp;#39;,
            &amp;quot;mode&amp;quot;: &amp;quot;cors&amp;quot;,
            &amp;quot;credentials&amp;quot;: &amp;quot;include&amp;quot;
        });
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;</content:encoded></item><item><title><![CDATA[Creating Word or PDF Documents in Power Platform with Power Automate Word Online connector]]></title><description><![CDATA[Today's post shows a way to create a document with data from arbitrary tables in Dataverse. The data is queried and transformed in a web…]]></description><link>https://www.pragmatic-development.io/blog/creating-word-or-pdf-documents-in-power-platform-with-word-online-connector/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/creating-word-or-pdf-documents-in-power-platform-with-word-online-connector/</guid><pubDate>Sat, 07 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Today&apos;s post shows a way to create a document with data from arbitrary tables in Dataverse. The data is queried and transformed in a web resource and the document - based on a word template - is created with Power Automate&apos;s &lt;a href=&quot;https://emea.flow.microsoft.com/de-de/connectors/shared_wordonlinebusiness/word-online-business/&quot;&gt;Connector for Word Online&lt;/a&gt; to render the final document as PDF.
This allows the creation of sophisticated documents without the limitations (single table+referencing tables, poor formatting) of OOB document templates.&lt;/p&gt;
&lt;p&gt;Generating a document in Power Platform with OOB features usually involves &lt;a href=&quot;https://docs.microsoft.com/en-us/power-platform/admin/using-word-templates-dynamics-365&quot;&gt;Document Templates&lt;/a&gt; or &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/maker/model-driven-apps/add-reporting-to-app&quot;&gt;SSRS reports&lt;/a&gt; based on FetchXML queries. While the first one has several limitations regarding data retrieval - the latter is frankly just cumbersome to work with although it has received some updates to the &lt;a href=&quot;https://www.microsoft.com/en-us/download/details.aspx?id=56973&quot;&gt;report authoring extensions&lt;/a&gt; lately.&lt;/p&gt;
&lt;p&gt;With Power Automate&apos;s &lt;a href=&quot;https://emea.flow.microsoft.com/de-de/connectors/shared_wordonlinebusiness/word-online-business/&quot;&gt;connector for Word Online&lt;/a&gt; we can easily author the template in Word and fill placeholders including repeating content controls like tables or bulleted lists to make sophisticated documents without additional costs for a specific document generation engine.&lt;/p&gt;
&lt;p&gt;The main concern one has with the OOB Word Templates in Power Apps is the fact that it is always scoped to a single entity along with its related entities. This makes it not feasible if the document has to span arbitrary tables.&lt;/p&gt;
&lt;p&gt;Therefore we are going to fetch all data upfront in the client and pass a tailored dataset for easy consumption to Power Automate. This gives us the flexibility to do the data shaping in javascript and leave it to the Word connector to create the final document.&lt;/p&gt;
&lt;p&gt;In our sample, we will print out teams with their roles and members as well as users with there roles and memberships.
&lt;img src=&quot;/images/blog/DocumentsCombined.png&quot; alt=&quot;Word and PDF Document&quot; title=&quot;Word and PDF Document&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Set Up the Word Document&lt;/h2&gt;
&lt;p&gt;As the process of template creation is thoroughly described by &lt;a href=&quot;https://www.flowjoe.io/2021/03/10/a-complete-guide-to-creating-and-populating-word-templates-with-power-automate/&quot;&gt;FlowJoe&lt;/a&gt;, I  would advise going there for further information on how to get going with the document placeholders. The template I have set up can be downloaded &lt;a href=&quot;/images/blog/UsersAndTeamsTemplate.docx&quot;&gt;here&lt;/a&gt;. The template uses two repeating section content controls for the &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;teams&lt;/code&gt; tables.
&lt;img src=&quot;/images/blog/WordTemplateScreenshot.png&quot; alt=&quot;Word Template&quot; title=&quot;Word Template&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The flow&lt;/h2&gt;
&lt;p&gt;When we create the flow we use a HTTP trigger that receives all data already in its desired shape. To set up the HTTP trigger use the sample JSON below &lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;json&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;{
    &amp;quot;users&amp;quot;: [
        {
            &amp;quot;fullname&amp;quot;: &amp;quot;Test User1&amp;quot;,
            &amp;quot;caltype&amp;quot;: &amp;quot;Enterprise&amp;quot;,
            &amp;quot;title&amp;quot;: &amp;quot;Software Architect&amp;quot;,
            &amp;quot;teams&amp;quot;: &amp;quot;• Team1&amp;quot;,
            &amp;quot;roles&amp;quot;: &amp;quot;• System Administrator\n• Basic User\n• Sales, Enterprise app access&amp;quot;
        }
    ],
    &amp;quot;teams&amp;quot;: [
        {
            &amp;quot;name&amp;quot;: &amp;quot;Team1&amp;quot;,
            &amp;quot;description&amp;quot;: &amp;quot;Default team for the parent business unit. The name and membership for default team are inherited from their parent business unit.&amp;quot;,
            &amp;quot;admin&amp;quot;: &amp;quot;Test User1&amp;quot;,
            &amp;quot;isdefault&amp;quot;: &amp;quot;Yes&amp;quot;,
            &amp;quot;teamtype&amp;quot;: &amp;quot;Owner&amp;quot;,
            &amp;quot;members&amp;quot;: &amp;quot;• Test User2\n• Test User1&amp;quot;,
            &amp;quot;roles&amp;quot;: &amp;quot;&amp;quot;
        }
    ]
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;ul&gt;
&lt;li&gt;The flow uses an HTTP trigger (1) so that we can call it from a web resource.&lt;/li&gt;
&lt;li&gt;Use the sample JSON above to generate the schema for the requests body (2).&lt;/li&gt;
&lt;li&gt;Place the template in OneDrive and wire the documents placeholders with the array from the request body (3)/(4).&lt;/li&gt;
&lt;li&gt;Create a temporary file in OneDrive (5) and convert it to PDF (6).&lt;/li&gt;
&lt;li&gt;Finally, return the file PDF file (7). &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/FlowDocumentCreationNumbered.png&quot; alt=&quot;Flow with Word Online Connector&quot; title=&quot;Flow with Word Online Connector&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Get the data&lt;/h2&gt;
&lt;p&gt;As explained before we will retrieve the data and pass it in good shape to the flow that is triggered via HTTP. This approach makes it easy to get the data via WebAPI and make the necessary transformations on the data. For example it would be easy to format dates, numbers and currencies to a desired appearance.&lt;/p&gt;
&lt;p&gt;If you create a command bar button with the handler &lt;code&gt;PowerAutomateDocCreation.CreateDocument&lt;/code&gt;, the users und teams are queried via WebAPI and the document is fetched for the Power Automate HTTP endpoint. Finally the file is download and  &lt;a href=&quot;https://stackoverflow.com/a/65415631&quot;&gt;offered to be downloaded&lt;/a&gt;.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;javascript&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;var PowerAutomateDocCreation;
(function (PowerAutomateDocCreation) {
  PowerAutomateDocCreation.CreateDocument = async function () {
    const userDto = await getUsers();
    const teamDto = await getTeams();

    const blob = await fetchDocument(userDto, teamDto);

    offerDownload(blob);
  };

  async function getUsers() {
    const users = await Xrm.WebApi.online.retrieveMultipleRecords(
      &amp;quot;systemuser&amp;quot;,
      &amp;quot;?$select=fullname,domainname,caltype,userlicensetype,isemailaddressapprovedbyo365admin,islicensed,title,employeeid,_businessunitid_value&amp;amp;$filter=accessmode eq 0&amp;amp;$expand=teammembership_association($select=name,teamid,systemmanaged,isdefault,teamtype),systemuserroles_association($select=name,ismanaged,roleid)&amp;quot;
    );

    const userDto = users.entities.map((x) =&amp;gt; {
      return {
        fullname: x.fullname,
        caltype: x[&amp;quot;caltype@OData.Community.Display.V1.FormattedValue&amp;quot;],
        title: x.title ?? &amp;quot;&amp;quot;,
        isdefault: x[&amp;quot;isdefault@OData.Community.Display.V1.FormattedValue&amp;quot;],
        teamtype: x[&amp;quot;teamtype@OData.Community.Display.V1.FormattedValue&amp;quot;],
        teams: x.teammembership_association
          .map((y) =&amp;gt; &amp;quot;• &amp;quot; + y.name)
          .join(&amp;quot;\n&amp;quot;),
        roles: x.systemuserroles_association
          .map((y) =&amp;gt; &amp;quot;• &amp;quot; + y.name)
          .join(&amp;quot;\n&amp;quot;),
      };
    });

    return userDto;
  }

  async function getTeams() {
    const teams = await Xrm.WebApi.online.retrieveMultipleRecords(
      &amp;quot;team&amp;quot;,
      &amp;quot;?$select=name,teamid,description,_businessunitid_value,_administratorid_value,systemmanaged,isdefault,teamtype,teamid&amp;amp;$expand=teammembership_association($select=fullname,domainname;$filter=accessmode eq 0),teamroles_association($select=name,ismanaged,roleid)&amp;quot;
    );

    const teamDto = teams.entities.map((x) =&amp;gt; {
      return {
        name: x.name,
        description: x.description ?? &amp;quot;&amp;quot;,
        admin:
          x[&amp;quot;_administratorid_value@OData.Community.Display.V1.FormattedValue&amp;quot;],
        isdefault: x[&amp;quot;isdefault@OData.Community.Display.V1.FormattedValue&amp;quot;],
        teamtype: x[&amp;quot;teamtype@OData.Community.Display.V1.FormattedValue&amp;quot;],
        members: x.teammembership_association
          .map((y) =&amp;gt; &amp;quot;• &amp;quot; + y.fullname)
          .join(&amp;quot;\n&amp;quot;),
        roles: x.teamroles_association.map((y) =&amp;gt; &amp;quot;• &amp;quot; + y.name).join(&amp;quot;\n&amp;quot;),
      };
    });

    return teamDto;
  }

  async function fetchDocument(userDto, teamDto) {
    const payload = {
      users: userDto,
      teams: teamDto,
    };

    response = await fetch(
      &amp;quot;https://&amp;lt;URL TO YOUR HTTP TRIGGER&amp;gt;&amp;quot;,
      {
        method: &amp;quot;POST&amp;quot;,
        headers: {
          &amp;quot;Content-Type&amp;quot;: &amp;quot;application/json&amp;quot;,
        },
        body: JSON.stringify(payload),
      }
    );

    const blob = await response.blob();
    return blob;
  }

  function offerDownload(blob) {
    const url = window.URL.createObjectURL(blob);
    const a = document.createElement(&amp;quot;a&amp;quot;);
    a.href = url;
    a.download = &amp;quot;UserAndTeamsReport.pdf&amp;quot;;
    document.body.appendChild(a); // we need to append the element to the dom -&amp;gt; otherwise it will not work in firefox
    a.click();
    a.remove();
  }
})(PowerAutomateDocCreation || (PowerAutomateDocCreation = {}));&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;</content:encoded></item><item><title><![CDATA[Power Platform Application Insights Instrumentation]]></title><description><![CDATA[Model-driven apps and Microsoft Dataverse diagnostics and performance data can be monitored in an Azure Application Insights resource. This…]]></description><link>https://www.pragmatic-development.io/blog/power-platform-application-insights-instrumentation/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/power-platform-application-insights-instrumentation/</guid><pubDate>Fri, 02 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Model-driven apps and Microsoft Dataverse diagnostics and performance data can be monitored in an Azure Application Insights resource. This enhancement allows developers and admins to gain a deep understanding of what&apos;s going on inside the platform. Out of the box, there are form-loads, plug-ins, and platform APIs logging   error and performance data.&lt;/p&gt;
&lt;p&gt;On a high level, you can report user activity with specific forms and it will allow product owners to understand adoption blockers and drivers.&lt;/p&gt;
&lt;h2&gt;Setup Application Insights&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Login to &lt;a href=&quot;https://portal.azure.com/&quot;&gt;Azure Portal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Create an Application Insights resource&lt;/li&gt;
&lt;li&gt;Activate Data Export to App Insights from &lt;a href=&quot;https://admin.powerplatform.microsoft.com/analytics/dataexport&quot;&gt;Power Platform admin center&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/activate-application-insights.png&quot; alt=&quot;Activate Application Insights Data Export in PAC&quot; title=&quot;Activate Application Insights Data Export in PAC&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Monitoring a Model-driven app&lt;/h2&gt;
&lt;p&gt;Once you have enabled data export you can immediately start to see monitoring events in your Application Insights resource.
&lt;img src=&quot;/images/blog/application-insights-monitoring.png&quot; alt=&quot;Activate Application Insights Monitoring&quot; title=&quot;Activate Application Insights Monitoring&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Add instrumentation code to your custom plugins&lt;/h2&gt;
&lt;p&gt;Knowing about timings in platform-level functionality is handy, but this feature really shines if you can extend it to your own plugins. While there has always been a way of utilizing the ITracingService for Plug-in Trace Logs there is now an additional &lt;a href=&quot;https://docs.microsoft.com/en-us/dotnet/api/microsoft.xrm.sdk.plugintelemetry.ilogger&quot;&gt;ILogger&lt;/a&gt; service that can be obtained from the service provider.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;var logger = (ILogger)serviceProvider.GetService(typeof(ILogger));
logger.LogInformation(&amp;quot;log something useful&amp;quot;);&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;The sample plugin below retrieves the share price from &lt;a href=&quot;https://marketstack.com/&quot;&gt;marketstack&lt;/a&gt; once a &lt;code&gt;tickersymbol&lt;/code&gt; is set on an account and updates the &lt;code&gt;stackexchange&lt;/code&gt; accordingly.&lt;/p&gt;
&lt;p&gt;If we create an update on the account we can capture the Request Id from the response headers collection inside F12 developer tools network tab.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/application-insights-request-id.png&quot; alt=&quot;Application Insights Find RequestId&quot; title=&quot;Application Insights Find RequestId&quot;&gt;&lt;/p&gt;
&lt;p&gt;This Request Id (&lt;code&gt;req_id&lt;/code&gt;) can be used to trace down the request along with the dependency graph of sub-components e.g. the HTTP Call to the marketstack API by searching for it under transaction search.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/application-insights-request-search.png&quot; alt=&quot;Transaction Search&quot; title=&quot;Transaction Search&quot;&gt;&lt;/p&gt;
&lt;p&gt;When we identified the transaction we see all operations that happened inside that transaction including other plugins as well as connections to external dependencies like in this case the financial data service.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/application-insights-end-to-end-transaction-tracking.png&quot; alt=&quot;Transaction Search&quot; title=&quot;Transaction Search&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Capture telemetry over time for analysis and debugging.&lt;/li&gt;
&lt;li&gt;Easily query events with Kusto Query Language (KQL).&lt;/li&gt;
&lt;li&gt;Analyze if issue was caused by custom code or platform code. &lt;/li&gt;
&lt;li&gt;Alerts based off performance thresholds for form-loads, APIs, and plug-ins&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Sample Plugin to receive stock market prices for an account&lt;/h2&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;public class GetStockExchangePlugin : IPlugin
{
    // Get an API Key from https://marketstack.com/
    private const string ApiKey = &amp;quot;&amp;lt;ENTER YOUR OWN API KEY&amp;gt;&amp;quot;;

    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var logger = (ILogger)serviceProvider.GetService(typeof(ILogger));

        Entity target;

        if (!context.InputParameters.TryGetValue(&amp;quot;Target&amp;quot;, out var targetParameter))
        {
            throw new InvalidPluginExecutionException(&amp;quot;Target not present.&amp;quot;);
        }
        else
        {
            target = (Entity)targetParameter;
        }
        var tickersymbol = target.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;tickersymbol&amp;quot;);

        if (!string.IsNullOrWhiteSpace(tickersymbol))
        {
            var stock = GetStockData(logger, tickersymbol);
            var symbol = stock?.data?.FirstOrDefault();
            if (symbol != null)
            {
                target[&amp;quot;stockexchange&amp;quot;] = $&amp;quot;{symbol.close:c} @{symbol.date:d}&amp;quot;;
            }
        }
    }

    private static Rootobject GetStockData(ILogger logger, string symbol)
    {
        using (logger.BeginScope(&amp;quot;FetchStockData&amp;quot;))
        {
            logger.LogInformation(&amp;quot;Outbound call to marketstack started.&amp;quot;);

            using (HttpClient client = new HttpClient())
            {
                client.Timeout = TimeSpan.FromMilliseconds(15000);
                client.DefaultRequestHeaders.ConnectionClose = true;

                logger.LogInformation(&amp;quot;Getting Stock data for: {0}&amp;quot;, symbol);

                var response = client
                .GetAsync($&amp;quot;http://api.marketstack.com/v1/eod?access_key={ApiKey}&amp;amp;symbols={symbol}&amp;amp;date_to={DateTime.UtcNow:yyyy-MM-dd)}&amp;amp;limit=1&amp;quot;)
                .ConfigureAwait(false)
                .GetAwaiter()
                .GetResult();

                if ((int)response.StatusCode == 422)
                {
                    logger.LogError(&amp;quot;&amp;#39;{0}&amp;#39; not found.&amp;quot;, symbol);
                    return null;
                }

                response.EnsureSuccessStatusCode();

                var responseStream = response.Content
                    .ReadAsStreamAsync()
                    .ConfigureAwait(false)
                    .GetAwaiter()
                    .GetResult();
                return ParseResponse(logger, responseStream);
            }
        }
    }

    private static Rootobject ParseResponse(ILogger logger, Stream responseStream)
    {
        using (responseStream)
        using (logger.BeginScope(&amp;quot;Parsing Response&amp;quot;))
        {
            var rootobject = (Rootobject)new DataContractJsonSerializer(typeof(Rootobject), new DataContractJsonSerializerSettings
            {
                DateTimeFormat = new DateTimeFormat(&amp;quot;yyyy-MM-ddTHH:mm:ss+0000&amp;quot;)
            }).ReadObject(responseStream);
            return rootobject;
        }
    }

    [DataContract]
    public class Rootobject
    {
        [DataMember]
        public StockData[] data { get; set; }
    }

    [DataContract]
    public class StockData
    {
        [DataMember]
        public decimal close { get; set; }

        [DataMember]
        public DateTime date { get; set; }
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;</content:encoded></item><item><title><![CDATA[Calling Dataverse Custom API from JavaScript]]></title><description><![CDATA[If you followed along our last blog post on how to implement business logic with Dataverse Custom API, you got a solid understanding how to…]]></description><link>https://www.pragmatic-development.io/blog/calling-dataverse-custom-api-from-javascript/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/calling-dataverse-custom-api-from-javascript/</guid><pubDate>Tue, 25 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you followed along our last blog post on how to &lt;a href=&quot;/blog/implement-business-logic-with-dataverse-custom-api&quot;&gt;implement business logic with Dataverse Custom API&lt;/a&gt;, you got a solid understanding how to set up a Custom API. Each API can be configured to have multiple &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/customapirequestparameter-table-columns&quot;&gt;request parameters&lt;/a&gt; of different datatypes. Passing a request object to &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/execute#execute-an-action&quot;&gt;Xrm.WebApi.online.execute&lt;/a&gt; along with a payload of request parameters triggers the action.&lt;/p&gt;
&lt;p&gt;Lets setup a bound action that features all of those parameters and post back to the caller to see if all of them have made it to the server side.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/custom-api-parameter-types.png&quot; alt=&quot;Custom API featuring all input parameter types available&quot; title=&quot;Custom API featuring all input parameter types available&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Calling bound API from JavaScript&lt;/h2&gt;
&lt;p&gt;The function below uses &lt;code&gt;Xrm.WebApi.online.execute&lt;/code&gt; to trigger the API. It is important to describe the payload inside the &lt;code&gt;getMetadata&lt;/code&gt; function.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Boolean ⇾ Edm.Boolean&lt;/li&gt;
&lt;li&gt;Decimal ⇾ Edm.Decimal&lt;/li&gt;
&lt;li&gt;Entity ⇾ mscrm.account&lt;/li&gt;
&lt;li&gt;EntityCollection ⇾ Collection(mscrm.crmbaseentity)&lt;/li&gt;
&lt;li&gt;EntityReference ⇾ mscrm.account&lt;/li&gt;
&lt;li&gt;Float ⇾ Edm.Float&lt;/li&gt;
&lt;li&gt;Integer ⇾ Edm.Int32&lt;/li&gt;
&lt;li&gt;Money ⇾ Edm.Money&lt;/li&gt;
&lt;li&gt;Picklist ⇾ Edm.Int32&lt;/li&gt;
&lt;li&gt;String ⇾ Edm.String&lt;/li&gt;
&lt;li&gt;StringArray ⇾ Collection(Edm.String)&lt;/li&gt;
&lt;/ul&gt;
&lt;deckgo-highlight-code language=&quot;javascript&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;async function callAction() {
  let account = {
    entityType: &amp;quot;account&amp;quot;,
    id: &amp;quot;2a932859-0ebb-eb11-bacc-000d3a2d9f66&amp;quot;,
  };

  let DecimalParam = 42.2;
  let BooleanParameter = &amp;quot;true&amp;quot;;
  let FloatParam = 42.4;
  let MoneyParameter = 42;
  let IntegerParam = 42;
  let StringParam = &amp;quot;Lorem Ipsum&amp;quot;;
  let EntityReferenceParam = account;
  let EntityParam = account;
  let EntityCollectionParam = [account, account];
  let PicklistParam = 42;
  let StringArrayParam = [&amp;quot;Lorem&amp;quot;, &amp;quot;Ipsum&amp;quot;];

  function new_TestApiParameters(
    entity,
    BooleanParameter,
    DecimalParam,
    FloatParam,
    MoneyParameter,
    IntegerParam,
    StringParam,
    EntityReferenceParam,
    EntityParam,
    EntityCollectionParam,
    PicklistParam,
    StringArrayParam
  ) {
    this.entity = entity;
    this.BooleanParameter = BooleanParameter;
    this.DecimalParam = DecimalParam;
    this.FloatParam = FloatParam;
    this.MoneyParameter = MoneyParameter;
    this.IntegerParam = IntegerParam;
    this.StringParam = StringParam;
    this.EntityReferenceParam = EntityReferenceParam;
    this.EntityParam = EntityParam;
    this.EntityCollectionParam = EntityCollectionParam;
    this.PicklistParam = PicklistParam;
    this.StringArrayParam = StringArrayParam;

    this.getMetadata = function () {
      return {
        operationName: &amp;quot;new_TestApiParameters&amp;quot;,
        boundParameter: &amp;quot;entity&amp;quot;,
        parameterTypes: {
          entity: {
            typeName: &amp;quot;mscrm.account&amp;quot;,
            structuralProperty: 5,
          },
          BooleanParameter: {
            typeName: &amp;quot;Edm.Boolean&amp;quot;,
            structuralProperty: 1,
          },
          DecimalParam: {
            typeName: &amp;quot;Edm.Decimal&amp;quot;,
            structuralProperty: 1,
          },
          FloatParam: {
            typeName: &amp;quot;Edm.Float&amp;quot;,
            structuralProperty: 1,
          },
          MoneyParameter: {
            typeName: &amp;quot;Edm.Money&amp;quot;,
            structuralProperty: 1,
          },
          IntegerParam: {
            typeName: &amp;quot;Edm.Int32&amp;quot;,
            structuralProperty: 1,
          },
          StringParam: {
            typeName: &amp;quot;Edm.String&amp;quot;,
            structuralProperty: 1,
          },
          EntityReferenceParam: {
            typeName: &amp;quot;mscrm.account&amp;quot;,
            structuralProperty: 5,
          },
          EntityParam: {
            typeName: &amp;quot;mscrm.account&amp;quot;,
            structuralProperty: 5,
          },
          EntityCollectionParam: {
            typeName: &amp;quot;Collection(mscrm.crmbaseentity)&amp;quot;,
            structuralProperty: 4,
          },
          PicklistParam: {
            typeName: &amp;quot;Edm.Int32&amp;quot;,
            structuralProperty: 1,
          },
          StringArrayParam: {
            typeName: &amp;quot;Collection(Edm.String)&amp;quot;,
            structuralProperty: 4,
          },
        },
        operationType: 0,
      };
    };
  }

  var request = new new_TestApiParameters(
    account,
    BooleanParameter,
    DecimalParam,
    FloatParam,
    MoneyParameter,
    IntegerParam,
    StringParam,
    EntityReferenceParam,
    EntityParam,
    EntityCollectionParam,
    PicklistParam,
    StringArrayParam
  );

  let res = await Xrm.WebApi.online.execute(request);
  console.log((await res.json()).Response);
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;h3&gt;Console Output:&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;BooleanParameter: True DecimalParam: 42.2 FloatParam: 42.4 MoneyParameter: 42 IntegerParam: 42 StringParam: &apos;Lorem Ipsum&apos; EntityReferenceParam: account EntityParam: &apos;account&apos; EntityCollectionParam&apos;s Count: &apos;2&apos; PicklistParam: &apos;42&apos; StringArrayParam: &apos;2&apos;&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Implementing a plugin&lt;/h2&gt;
&lt;p&gt;This plugin returns all parameters within a single string back to the caller. If it is a collection based parameter the count of elements is returned instead.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;public class TestApiParametersPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
        var response = &amp;quot;&amp;quot;;
        if (context.InputParameters.TryGetValue(&amp;quot;BooleanParameter&amp;quot;, out var BooleanParameter))
        {
            response += $&amp;quot;{nameof(BooleanParameter)}: {BooleanParameter}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;DecimalParam&amp;quot;, out var DecimalParam))
        {
            response += $&amp;quot;{nameof(DecimalParam)}: {DecimalParam}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;FloatParam&amp;quot;, out var FloatParam))
        {
            response += $&amp;quot;{nameof(FloatParam)}: {FloatParam}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;MoneyParameter&amp;quot;, out var MoneyParameter))
        {
            response += $&amp;quot;{nameof(MoneyParameter)}: {((Money)MoneyParameter)?.Value}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;IntegerParam&amp;quot;, out var IntegerParam))
        {
            response += $&amp;quot;{nameof(IntegerParam)}: {IntegerParam}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;StringParam&amp;quot;, out var StringParam))
        {
            response += $&amp;quot;{nameof(StringParam)}: &amp;#39;{StringParam}&amp;#39;\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;EntityReferenceParam&amp;quot;, out var EntityReferenceParam))
        {
            response += $&amp;quot;{nameof(EntityReferenceParam)}: {((EntityReference)EntityReferenceParam).LogicalName}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;EntityParam&amp;quot;, out var EntityParam))
        {
            response += $&amp;quot;{nameof(EntityParam)}: &amp;#39;{((Entity)EntityParam).LogicalName}&amp;#39;\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;EntityCollectionParam&amp;quot;, out var EntityCollectionParam))
        {
            response += $&amp;quot;{nameof(EntityCollectionParam)}&amp;#39;s Count: &amp;#39;{((EntityCollection)EntityCollectionParam).Entities.Count}&amp;#39;\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;PicklistParam&amp;quot;, out var PicklistParam))
        {
            response += $&amp;quot;{nameof(PicklistParam)}: {((OptionSetValue)PicklistParam)?.Value}\n&amp;quot;;
        }
        if (context.InputParameters.TryGetValue(&amp;quot;StringArrayParam&amp;quot;, out var StringArrayParam))
        {
            response += $&amp;quot;{nameof(StringArrayParam)}: &amp;#39;{((string[])StringArrayParam).Length}&amp;#39;\n&amp;quot;;
        }


        context.OutputParameters.Add(&amp;quot;Response&amp;quot;, response);
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;</content:encoded></item><item><title><![CDATA[Implement business logic with Dataverse Custom API]]></title><description><![CDATA[In march 2021 Microsoft announced that the Dataverse Custom API reaches General Availability and therefore it's about time to take a look at…]]></description><link>https://www.pragmatic-development.io/blog/implement-business-logic-with-dataverse-custom-api/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/implement-business-logic-with-dataverse-custom-api/</guid><pubDate>Thu, 13 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In march 2021 Microsoft &lt;a href=&quot;https://powerapps.microsoft.com/en-us/blog/microsoft-dataverse-custom-api-general-availability/&quot;&gt;announced&lt;/a&gt; that the Dataverse Custom API reaches General Availability and therefore it&apos;s about time to take a look at those new extension points for the Power Platform.
The concept that allows encapsulating multiple steps of business-logic under a custom operational contract is available since &lt;a href=&quot;https://docs.microsoft.com/en-us/previous-versions/dynamicscrm-2016/developers-guide/dn481600(v=crm.8)&quot;&gt;Dynamics 2013&lt;/a&gt; with custom process actions and significantly improved reusability across the application as consumption of those actions was available from scripts in the frontend, plugins/codeactivity on the serverside, as well as workflow steps of the workflow designer. So let&apos;s see how custom APIs can make the business-logic shine.&lt;/p&gt;
&lt;h2&gt;What are custom APIs?&lt;/h2&gt;
&lt;p&gt;The definition of &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/custom-api&quot;&gt;Custom APIs&lt;/a&gt; consists of metadata about the API itself, its optional request parameters as well as response properties. This information is stored in dataverse tables and is solution aware allowing it to be easily managed in ALM scenarios.&lt;/p&gt;
&lt;h3&gt;Affected Tables&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/customapi-table-columns&quot;&gt;CustomAPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/customapirequestparameter-table-columns&quot;&gt;CustomAPIRequestParameter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/customapiresponseproperty-table-columns&quot;&gt;CustomAPIResponseProperty&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Getting started with an example&lt;/h2&gt;
&lt;p&gt;Lets dive into a sample to see how this all works out. Imagine we have have setup a table for invoices with a total amount and each of those invoices can have line items with an individual amount.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/custom-api-sample-model.png&quot; alt=&quot;Table relationship of invoice and its line items&quot; title=&quot;Table relationship of invoice and its line items&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The new API should be bound to the invoice table and accept an input collection of line items that should be added to that invoice.&lt;/li&gt;
&lt;li&gt;Once the update of given line items succeeded, the total amount has to be updated on the invoice record and must be returned to the caller.&lt;/li&gt;
&lt;li&gt;The Action may only be called by users having create privilege on invoices.&lt;/li&gt;
&lt;li&gt;In case of failures the whole operation needs to be rolled back, to prevent an inconsistent state of data (e.g. only some of the line item updates succeeded).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Configuration&lt;/h3&gt;
&lt;p&gt;Lets start with the metadata describing the API&apos;s appearance. The easiest way is use the XrmToolBox plugin &lt;a href=&quot;https://github.com/drivardxrm/XTB.CustomApiManager&quot;&gt;CustomApiManager&lt;/a&gt; created by &lt;a href=&quot;https://itmustbecode.com/&quot;&gt;David Rivard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;From the screenshot below you can see the configuration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API Name: new_AddInvoiceLineItems&lt;/li&gt;
&lt;li&gt;Plugin Type: BlogDemo.AddInvoiceLineItemsPlugin&lt;/li&gt;
&lt;li&gt;Execute Privilege Name: prvCreatenew_Invoice&lt;/li&gt;
&lt;li&gt;Request Parameter: InvoiceLineItems&lt;/li&gt;
&lt;li&gt;Response Property: TotalAmount&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/custom-api-metadata.png&quot; alt=&quot;Custom API Metadata&quot; title=&quot;Custom API Metadata&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Implementation&lt;/h3&gt;
&lt;p&gt;Once we setup the API its time to write the plugin code so that we can wire it up later on in the metadata.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
using static Microsoft.Xrm.Sdk.Query.ConditionOperator;

namespace BlogDemo
{
    public class AddInvoiceLineItemsPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            var context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            var factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            var service = factory.CreateOrganizationService(context.UserId);

            if (!(context.InputParameters.TryGetValue(&amp;quot;Target&amp;quot;, out var target)
                &amp;amp;&amp;amp; context.InputParameters.TryGetValue(&amp;quot;InvoiceLineItems&amp;quot;, out var lineItems)))
            {
                throw new InvalidPluginExecutionException(&amp;quot;Invoice or Line Items are not present.&amp;quot;);
            }

            var invoice = target as EntityReference;
            var invoiceLineItems = lineItems as EntityCollection;

            // Retrieve all invoice item that were passed along with request parameter
            var lineitems = service.RetrieveMultiple(new QueryExpression(&amp;quot;new_invoicelineitem&amp;quot;)
            {
                Criteria = new FilterExpression
                {
                    Conditions =
                    {
                        new ConditionExpression(&amp;quot;new_invoicelineitemid&amp;quot;, In,
                            invoiceLineItems.Entities.Select(x =&amp;gt; x.Id).ToArray())
                    }
                }
            }).Entities;

            // Set the lookup of each line item to the invoice reference
            lineitems.Select(x =&amp;gt; new Entity(&amp;quot;new_invoicelineitem&amp;quot;, x.Id)
            {
                Attributes =
                {
                    [&amp;quot;new_invoiceid&amp;quot;] = invoice
                }
            }).ToList()
            .ForEach(service.Update);

            // Calculate the total amount of all line items including those that might have been previously added.
            var lineItemAmount = new Money(service.RetrieveMultiple(new QueryExpression(&amp;quot;new_invoicelineitem&amp;quot;)
            {
                ColumnSet = new ColumnSet(&amp;quot;new_amount&amp;quot;),
                Criteria = new FilterExpression
                {
                    Conditions = {
                        new ConditionExpression(&amp;quot;new_invoiceid&amp;quot;, Equal, invoice.Id),
                        new ConditionExpression(&amp;quot;new_amount&amp;quot;, NotNull),
                    }
                }
            }).Entities
            .Sum(x =&amp;gt; x.GetAttributeValue&amp;lt;Money&amp;gt;(&amp;quot;new_amount&amp;quot;).Value));

            // Uncomment this line if you want to prove that previous service calls are rolled back.
            //throw new InvalidPluginExecutionException(&amp;quot;Are we correctly rolling back&amp;quot;);

            // Update total amount on invoice
            service.Update(new Entity(invoice.LogicalName, invoice.Id)
            {
                [&amp;quot;new_totalamount&amp;quot;] = lineItemAmount
            });

            context.OutputParameters.Add(&amp;quot;TotalAmount&amp;quot;, lineItemAmount);
        }
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;When the plugin dll is registered its time to update the custom api&apos;s plugin type. Remember we do not register a step for the plugin type as it is configuration option in the custom api record (see red box on screenshot above).&lt;/p&gt;
&lt;h3&gt;Testing&lt;/h3&gt;
&lt;p&gt;To test our code we use the F12 tool&apos;s console window in the browser to kickstart the action with the help of &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi&quot;&gt;Xrm.WebApi&lt;/a&gt; client object model.&lt;/p&gt;
&lt;p&gt;First we create an invoice and two line items that are not yet associated.
The action is described with the constructor function &lt;code&gt;AddInvoiceLineItems&lt;/code&gt;. You can find find more about this admittedly complex parameter configuration in the official &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/execute&quot;&gt;docs&lt;/a&gt;.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;javascript&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;let invoiceIndex = 1;
let invoiceItemIndex = 1;
var invoiceId = await Xrm.WebApi.createRecord(&amp;quot;new_invoice&amp;quot;, {
  new_name: &amp;quot;invoice &amp;quot; + invoiceIndex++,
});
let invoiceItemId1 = await Xrm.WebApi.createRecord(&amp;quot;new_invoicelineitem&amp;quot;, {
  new_name: &amp;quot;invoice&amp;quot; + invoiceIndex + &amp;quot; lineitem &amp;quot; + invoiceItemIndex++,
  new_amount: 100
});
let invoiceItemId2 = await Xrm.WebApi.createRecord(&amp;quot;new_invoicelineitem&amp;quot;, {
  new_name: &amp;quot;invoice&amp;quot; + invoiceIndex + &amp;quot; lineitem &amp;quot; + invoiceItemIndex++,
  new_amount: 100
});

function AddInvoiceLineItems(invoiceId, invoiceLineItems) {
  return {
    entity: {
      entityType: &amp;quot;new_invoice&amp;quot;,
      id: invoiceId,
    },
    InvoiceLineItems: invoiceLineItems,

    getMetadata() {
      return {
        operationName: &amp;quot;new_AddInvoiceLineItems&amp;quot;,
        boundParameter: &amp;quot;entity&amp;quot;,
        parameterTypes: {
          entity: {
            typeName: &amp;quot;mscrm.new_invoice&amp;quot;,
            structuralProperty: 5,
          },
          InvoiceLineItems: {
            typeName: &amp;quot;Collection(mscrm.crmbaseentity)&amp;quot;,
            structuralProperty: 4,
          },
        },
        operationType: 0,
      };
    },
  };
};

let response = await Xrm.WebApi.online.execute(
  new AddInvoiceLineItems(invoiceId.id, [invoiceItemId1, invoiceItemId2])
);

let actionReponse = await response.json();

console.log(&amp;quot;Total Invoice Amount is: &amp;quot; + actionReponse.TotalAmount);&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;&lt;strong&gt;OUTPUT: Total Invoice Amount is: 200&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Xrm.WebApi.online.execute function executes the action and sends a POST request to the Dataverse Web API.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;json&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;POST: https://&amp;lt;environnmenturl&amp;gt;/api/data/v9.0/new_invoices(b0ddfe3d-7db4-eb11-8236-0022487eee10)/Microsoft.Dynamics.CRM.new_AddInvoiceLineItems
{
  &amp;quot;entity&amp;quot;: {
    &amp;quot;@odata.type&amp;quot;: &amp;quot;Microsoft.Dynamics.CRM.new_invoice&amp;quot;,
    &amp;quot;new_invoiceid&amp;quot;: &amp;quot;b0ddfe3d-7db4-eb11-8236-0022487eee10&amp;quot;
  },
  &amp;quot;InvoiceLineItems&amp;quot;: [
    {
      &amp;quot;@odata.type&amp;quot;: &amp;quot;Microsoft.Dynamics.CRM.new_invoicelineitem&amp;quot;,
      &amp;quot;new_invoicelineitemid&amp;quot;: &amp;quot;b1ddfe3d-7db4-eb11-8236-0022487eee10&amp;quot;
    },
    {
      &amp;quot;@odata.type&amp;quot;: &amp;quot;Microsoft.Dynamics.CRM.new_invoicelineitem&amp;quot;,
      &amp;quot;new_invoicelineitemid&amp;quot;: &amp;quot;b2ddfe3d-7db4-eb11-8236-0022487eee10&amp;quot;
    }
  ]
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;As you can see it was very straight forward to implement the logic inside a plugin and call it from client-side scripts. You can find an unmanaged solution of the demo &lt;a href=&quot;/images/blog/CustomApiDemo_1_0_0_0.zip&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Wrapping up&lt;/h2&gt;
&lt;p&gt;The new approach of Custom APIs has significant advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Can be unbound (global) or bound to a table/entity&lt;/li&gt;
&lt;li&gt;Custom API definitions are solution aware components included in a solution through a set of folders and XML documents.&lt;/li&gt;
&lt;li&gt;Can designate that a user must have a specific privilege to call the message.&lt;/li&gt;
&lt;li&gt;Can be marked private if they are not intended to be used by anyone else.&lt;/li&gt;
&lt;li&gt;With Custom API the message creator simply associates their plug-in type with the Custom API to provide the main operation logic.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Access Dataverse Web API from .NET Core with Microsoft.PowerPlatform.Dataverse.Client]]></title><description><![CDATA[For applications targeting the full .NET framework the process of connecting to the Dataverse Web API is usually handled by the XrmTooling…]]></description><link>https://www.pragmatic-development.io/blog/access-dataverse-web-api-from-.net-core-with-microsoft.powerplatform.dataverse.client/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/access-dataverse-web-api-from-.net-core-with-microsoft.powerplatform.dataverse.client/</guid><pubDate>Tue, 11 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For applications targeting the full .NET framework the process of connecting to the Dataverse Web API is usually handled by the &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.CrmSdk.XrmTooling.CoreAssembly/&quot;&gt;XrmTooling SDK&lt;/a&gt; and the use of &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/xrm-tooling/use-connection-strings-xrm-tooling-connect&quot;&gt;connection strings&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For .NET Core applications their is finally (in public preview) an official SDK available providing the same feature set known from Xrm Tooling. This package is intended to work with .NET full framework 4.6.2, 4.7.2 and 4.8, .NET core 3.0, 3.1 and 5.0. Library&apos;s source code is available at &lt;a href=&quot;https://github.com/microsoft/PowerPlatform-DataverseServiceClient&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the upcoming sample we will build an application with authentication via client credentials to access the Dataverse Web API.&lt;/p&gt;
&lt;h2&gt;Application registration in Azure Directory&lt;/h2&gt;
&lt;p&gt;We assume that you did create an app registration upfront and configured Dynamics impersonation permissions with clientId and client secret. The process is fully documented in &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/use-single-tenant-server-server-authentication&quot;&gt;docs.microsoft.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Create .NET Core Console Application (with Visual Studio Code)&lt;/h2&gt;
&lt;p&gt;Next step is to create a .NET Core Console App with the required dependency on &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.PowerPlatform.Dataverse.Client/&quot;&gt;Microsoft.PowerPlatform.Dataverse.Client&lt;/a&gt;.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;powershell&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;mkdir &amp;quot;connect-to-dataverse-webapi-with-dataverse-client&amp;quot;
cd &amp;quot;connect-to-dataverse-webapi-with-dataverse-client&amp;quot;
dotnet new console
dotnet add package Microsoft.PowerPlatform.Dataverse.Client
code .&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;Replace the code in Program.cs with the code below.&lt;/p&gt;
&lt;h3&gt;Things to note:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Connection is configured with established pattern of &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/xrm-tooling/use-connection-strings-xrm-tooling-connect&quot;&gt;connection strings&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The library has async/await support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Edit lines 13-15 by setting clientId and clientSecret.&lt;/p&gt;
&lt;p&gt;In line 21 the Dataverse Web API is queried for 10 accounts along with their name.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  highlight-lines=&quot;13,15&quot;&gt;
          &lt;code slot=&quot;code&quot;&gt;using Microsoft.PowerPlatform.Dataverse.Client;
using Microsoft.Xrm.Sdk.Query;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace connect_to_dataverse_webapi_with_dataverse_client
{
    class Program
    {
        static async Task Main(string[] args)
        {
            const string clientId = &amp;quot;&amp;lt;clientId&amp;gt;&amp;quot;,
                         clientSecret = &amp;quot;&amp;lt;clientSecret&amp;gt;&amp;quot;,
                         environment = &amp;quot;&amp;lt;yourEnvironment&amp;gt;.crm4&amp;quot;;

            var connectionString = @$&amp;quot;Url=https://{environment}.dynamics.com;AuthType=ClientSecret;ClientId={clientId};ClientSecret={clientSecret};RequireNewInstance=true&amp;quot;;

            using var serviceClient = new ServiceClient(connectionString);

            var accountsCollection = await serviceClient.RetrieveMultipleAsync(new QueryExpression(&amp;quot;account&amp;quot;)
            {
                ColumnSet = new ColumnSet(&amp;quot;name&amp;quot;),
                TopCount = 10
            });

            Console.WriteLine(string.Join(&amp;quot;\n&amp;quot;,
                accountsCollection.Entities
                    .Select(x =&amp;gt; $&amp;quot;{x.GetAttributeValue&amp;lt;string&amp;gt;(&amp;quot;name&amp;quot;)}, {x.Id}&amp;quot;)));
        }
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;h2&gt;Room for improvement&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Move the auth constants to &lt;code&gt;app.config&lt;/code&gt; (console) or &lt;code&gt;local.settings.json&lt;/code&gt; (Azure Function) file and load the values as environment variables&lt;/li&gt;
&lt;li&gt;Better error handling&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Access Dataverse Web API from .NET Core or Azure Functions with client credentials]]></title><description><![CDATA[For applications targeting the full .NET framework the process of connecting to the Dataverse Web API is usually handled by the XrmTooling…]]></description><link>https://www.pragmatic-development.io/blog/access-dataverse-web-api-from-.net-core-or-azure-functions-with-client-credentials/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/access-dataverse-web-api-from-.net-core-or-azure-functions-with-client-credentials/</guid><pubDate>Sun, 09 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For applications targeting the full .NET framework the process of connecting to the Dataverse Web API is usually handled by the &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.CrmSdk.XrmTooling.CoreAssembly/&quot;&gt;XrmTooling SDK&lt;/a&gt; and the use of &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/xrm-tooling/use-connection-strings-xrm-tooling-connect&quot;&gt;connection strings&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For .NET Core applications the security handshake needs to be implemented with the help of the &lt;a href=&quot;https://github.com/AzureAD/microsoft-authentication-library-for-dotnet&quot;&gt;Microsoft Authentication Library (MSAL)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Update:
There is finally an official SDK library targeting .NET Core. You can find a walkthrough of the setup in &lt;a href=&quot;/blog/access-dataverse-web-api-from-.net-core-with-microsoft.powerplatform.dataverse.client&quot;&gt;Accessing Dataverse Web API from .NET Core with Microsoft.PowerPlatform.Dataverse.Client&lt;/a&gt;.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In the upcoming sample we will build an application with authentication via client credentials to access the Dataverse Web API.&lt;/p&gt;
&lt;h2&gt;Application registration in Azure Directory&lt;/h2&gt;
&lt;p&gt;We assume that you did create an app registration upfront and configured Dynamics impersonation permissions with clientId and client secret. The process is fully documented in &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/use-single-tenant-server-server-authentication&quot;&gt;docs.microsoft.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Create .NET Core Console Application (with Visual Studio Code)&lt;/h2&gt;
&lt;p&gt;Next step is to create a .NET Core Console App with the required dependency on &lt;a href=&quot;https://www.nuget.org/packages/Microsoft.Identity.Client&quot;&gt;Microsoft Authentication Library (MSAL)&lt;/a&gt;.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;powershell&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  &gt;
          &lt;code slot=&quot;code&quot;&gt;mkdir &amp;quot;connect-to-dataverse-webapi&amp;quot;
cd &amp;quot;connect-to-dataverse-webapi&amp;quot;
dotnet new console
dotnet add package Microsoft.Identity.Client --version 4.30.1
code .&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;p&gt;Replace the code in Program.cs with the code below.&lt;/p&gt;
&lt;p&gt;Edit lines 15-18 by setting clientId, clientSecret and tenantId.&lt;/p&gt;
&lt;p&gt;In line 43 the Dataverse Web API is queried for 10 accounts along with their name.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;csharp&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  highlight-lines=&quot;15,18&quot;&gt;
          &lt;code slot=&quot;code&quot;&gt;using Microsoft.Identity.Client;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;

namespace connect_to_dataverse_webapi
{
    class Program
    {
        static async Task Main(string[] args)
        {
            const string clientId = &amp;quot;&amp;lt;clientId&amp;gt;&amp;quot;,
                         clientSecret = &amp;quot;&amp;lt;clientSecret&amp;gt;&amp;quot;,
                         tenantId = &amp;quot;&amp;lt;tenantId&amp;gt;&amp;quot;,
                         environment = &amp;quot;&amp;lt;yourEnvironment&amp;gt;.crm4&amp;quot;,
                         apiVersion = &amp;quot;9.2&amp;quot;;

            var scope = new[] { $&amp;quot;https://{environment}.dynamics.com/.default&amp;quot; };
            var webAPI = $&amp;quot;https://{environment}.dynamics.com/api/data/v{apiVersion}/&amp;quot;;
            var authority = $&amp;quot;https://login.microsoftonline.com/{tenantId}&amp;quot;;

            var clientApp = ConfidentialClientApplicationBuilder.Create(clientId)
                 .WithClientSecret(clientSecret)
                 .WithAuthority(new Uri(authority))
                 .Build();

            var authResult = await clientApp.AcquireTokenForClient(scope).ExecuteAsync();

            using var httpClient = new HttpClient
            {
                BaseAddress = new Uri(webAPI),
                Timeout = new TimeSpan(0, 0, 10)
            };

            httpClient.DefaultRequestHeaders.Add(&amp;quot;OData-MaxVersion&amp;quot;, &amp;quot;4.0&amp;quot;);
            httpClient.DefaultRequestHeaders.Add(&amp;quot;OData-Version&amp;quot;, &amp;quot;4.0&amp;quot;);
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(&amp;quot;application/json&amp;quot;));
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(&amp;quot;Bearer&amp;quot;, authResult.AccessToken);

            var response = await httpClient.GetAsync(&amp;quot;accounts?$top=10&amp;amp;$select=name&amp;quot;);

            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };

            if (response.IsSuccessStatusCode)
            {
                var accountsResponse = await response.Content.ReadAsStringAsync();

                var accounts = JsonSerializer.Deserialize&amp;lt;Response&amp;lt;Account&amp;gt;&amp;gt;(accountsResponse, options).Value;

                Console.WriteLine(string.Join(&amp;quot;\n&amp;quot;, accounts.Select(x =&amp;gt; $&amp;quot;{x.Name}, {x.AccountId}&amp;quot;)));
            }
            else
            {
                var errorResponse = await response.Content.ReadAsStringAsync();

                var error = JsonSerializer.Deserialize&amp;lt;ErrorResponse&amp;gt;(errorResponse, options);

                Console.WriteLine(&amp;quot;HTTP: {0} ReasonPhrase: {1}\nError: {2}&amp;quot;, (int)response.StatusCode, response.ReasonPhrase, error.Error.Message);
            }
        }
    }

    class Response&amp;lt;T&amp;gt;
    {
        public T[] Value { get; set; }
    }

    class Account
    {
        public string Name { get; set; }
        public Guid AccountId { get; set; }
    }

    class ErrorResponse
    {
        public ErrorMessage Error { get; set; }

        public class ErrorMessage
        {
            public string Message { get; set; }
        }
    }
}&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;h2&gt;Room for improvement&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Reuse of bearer token and automatic renewal before its expiration. See &lt;code&gt;authResult.ExpiresOn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Move the auth constants to &lt;code&gt;app.config&lt;/code&gt; (console) or &lt;code&gt;local.settings.json&lt;/code&gt; (Azure Function) file and load the values as environment variables&lt;/li&gt;
&lt;li&gt;Better error handling&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[Prevent New... button on lookup controls in model-driven apps]]></title><description><![CDATA[As soon as a user has at least user create privilege on a Dataverse table a 'New ...' button is shown upon opening the lookup search dialog…]]></description><link>https://www.pragmatic-development.io/blog/prevent-new-button-on-lookup-controls/</link><guid isPermaLink="false">https://www.pragmatic-development.io/blog/prevent-new-button-on-lookup-controls/</guid><pubDate>Fri, 07 May 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As soon as a user has at least &lt;code&gt;user create&lt;/code&gt; privilege on a Dataverse table a &apos;New ...&apos; button is shown upon opening the lookup search dialog. This sometimes-handy feature might be a source of inconsistency for the user experience and might lead to wrong data entry as records might be created at times when only existing ones should be used, depending on your business process.&lt;/p&gt;
&lt;h2&gt;Issue&lt;/h2&gt;
&lt;p&gt;Sadly, there is no way of hiding the out of the box button from the user interface via the &lt;strong&gt;form editor&lt;/strong&gt; and to remove the &lt;code&gt;create privilege&lt;/code&gt; might not be a feasible option.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/prevent-new-button-on-lookup-controls_01.png&quot; alt=&quot;Lookup dialog showing a &amp;#x27;New ...&amp;#x27; Button&quot; title=&quot;Lookup dialog showing a &amp;#x27;New ...&amp;#x27; Button&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Luckily there is an option within the form XML schema that allows exactly the desired behavior. Adding the tag &lt;code&gt;&amp;#x3C;IsInlineNewEnabled&gt;false&amp;#x3C;/IsInlineNewEnabled&gt;&lt;/code&gt; (usually this tag is not present in formXml), will remove the button for all users no matter what their privileges allow. The provided solution is supported and works for Dynamics Customer Engagement on-premises as well as current Power Platform model-driven apps.&lt;/p&gt;
&lt;h2&gt;Procedure&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Create an unmanaged solution containing only the form&lt;/li&gt;
&lt;li&gt;Export the solution zip file and extract it to disk&lt;/li&gt;
&lt;li&gt;Locate the lookup control based on its schema name in &lt;code&gt;customizations.xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;IsInlineNewEnabled&lt;/code&gt; tag to control&apos;s parameters collection&lt;/li&gt;
&lt;li&gt;Zip the contents of the extracted folder including your modified &lt;code&gt;customizations.xml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Import the newly created solution zip file and publish all changes&lt;/li&gt;
&lt;li&gt;Verify that the button is gone now&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The parameters of your lookup instance might not match the ones shown here. Or in case you did not set any additional parameters via form editor might not even be present at all. In that case just add a parameters tag including the tag &lt;code&gt;IsInlineNewEnabled&lt;/code&gt;.&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;xml&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  highlight-lines=&quot;8&quot;&gt;
          &lt;code slot=&quot;code&quot;&gt;&amp;lt;control id=&amp;quot;new_contractid&amp;quot; classid=&amp;quot;{270BD3DB-D9AF-4782-9025-509E298DEC0A}&amp;quot; datafieldname=&amp;quot;new_contractid&amp;quot; uniqueid=&amp;quot;{8dcea322-b6ee-5a9b-a429-b47dc98d75a7}&amp;quot;&amp;gt;
    &amp;lt;parameters&amp;gt;
        &amp;lt;AutoResolve&amp;gt;true&amp;lt;/AutoResolve&amp;gt;
        &amp;lt;DisableMru&amp;gt;false&amp;lt;/DisableMru&amp;gt;
        &amp;lt;DisableQuickFind&amp;gt;false&amp;lt;/DisableQuickFind&amp;gt;
        &amp;lt;DisableViewPicker&amp;gt;false&amp;lt;/DisableViewPicker&amp;gt;
        &amp;lt;AllowFilterOff&amp;gt;false&amp;lt;/AllowFilterOff&amp;gt;
        &amp;lt;IsInlineNewEnabled&amp;gt;false&amp;lt;/IsInlineNewEnabled&amp;gt;
    &amp;lt;/parameters&amp;gt;
&amp;lt;/control&amp;gt;&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;As a result, the &apos;New...&apos; button is gone.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/blog/prevent-new-button-on-lookup-controls_02.png&quot; alt=&quot;Lookup dialog showing no &amp;#x27;New ...&amp;#x27; Button&quot; title=&quot;Lookup dialog showing no &amp;#x27;New ...&amp;#x27; Button&quot;&gt;&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;The lookup control schema verifies that this is a valid modification of the formXML and the customization guidelines state that &lt;a href=&quot;https://docs.microsoft.com/en-us/powerapps/developer/data-platform/supported-customizations#customizations-applied-using-code&quot;&gt;Modifying the customization.xml&lt;/a&gt; is a valid act.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/dynamics365/customerengagement/on-premises/developer/customize-dev/form-xml-schema&quot;&gt;Schema in docs.microsoft.com&lt;/a&gt;&lt;/p&gt;
&lt;deckgo-highlight-code language=&quot;xml&quot; terminal=&quot;carbon&quot; theme=&quot;vscode&quot; line-numbers=&quot;true&quot;  highlight-lines=&quot;60,63&quot;&gt;
          &lt;code slot=&quot;code&quot;&gt;&amp;lt;!-- Parameters for the lookup control --&amp;gt;
&amp;lt;xs:choice minOccurs=&amp;quot;1&amp;quot;
           maxOccurs=&amp;quot;unbounded&amp;quot;&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DefaultViewId&amp;quot;
                type=&amp;quot;FormGuidType&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;FilterRelationshipName&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DependentAttributeName&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DependentAttributeType&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;AutoResolve&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;ResolveEmailAddress&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DefaultViewReadOnly&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;ViewPickerReadOnly&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;AllowFilterOff&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DisableMru&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DisableQuickFind&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;DisableViewPicker&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;AvailableViewIds&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;EntityLogicalName&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;IsInlineNewEnabled&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;InlineViewIds&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;UnboundLookupTypes&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;UnboundLookupBrowse&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;UnboundLookupControlType&amp;quot;
                type=&amp;quot;xs:string&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
    &amp;lt;xs:element name=&amp;quot;ShowAsBreadcrumbControl&amp;quot;
                type=&amp;quot;xs:boolean&amp;quot;
                minOccurs=&amp;quot;0&amp;quot;
                maxOccurs=&amp;quot;1&amp;quot; /&amp;gt;
&amp;lt;/xs:choice&amp;gt;&lt;/code&gt;
        &lt;/deckgo-highlight-code&gt;</content:encoded></item></channel></rss>