Jekyll2020-12-30T19:25:58+00:00https://jasonclair.co.uk/feed.xmlJason Clair - Developer BlogTechnical blog focusing on Microsoft technologies, specifically Dynamics CRM & SharePointJason Clairblog@jasonclair.co.ukLookups – How to Disable ‘Recents’2020-07-08T00:00:00+00:002020-07-08T18:00:00+00:00https://jasonclair.co.uk/dynamics/disable-recents-on-looksup<p>In my previous <a href="/dynamics/using-addCustomView-with-lookup-controls">post</a>, I showed how to use Custom Views to use linked entities to filter items in a lookup. As mentioned in the notes, one of the issues with this is that the ‘Recents’ still show and are not filtered in the same way, therefore, whenever using this functionality I disable the ‘Recents’ from showing.</p>
<p>To do this, go to the Form Designer & open the field properties and tick the option for ‘Disable most recently used items for this field’ (at the time of writing I could only do this using the Classic Experience)</p>
<figure class="">
<img src="/assets/images/posts/disable-recents/DisablingRecents.png" alt="Disable most recently used items for this field is checked" /><figcaption>
Field property in Classic Experience
</figcaption></figure>Jason Clairblog@jasonclair.co.ukIn my previous post, I showed how to use Custom Views to use linked entities to filter items in a lookup. As mentioned in the notes, one of the issues with this is that the ‘Recents’ still show and are not filtered in the same way, therefore, whenever using this functionality I disable the ‘Recents’ from showing.Using addCustomView with Lookups2020-06-20T00:00:00+00:002020-06-20T18:00:00+00:00https://jasonclair.co.uk/dynamics/using-addCustomView-with-lookup-controls<p>I recently wanted to filter a Lookup within Dynamics 365 to show Contacts where the linked Accounts’ Parent Account was a specific Account. The idea being that the user can select any Contact from the Account Hierarchy but shouldn’t be able to select any Contacts from outside of that structure.</p>
<p>Normally, I would use <a href="https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/controls/addcustomfilter" target="_blank">addCustomFilter</a> & <a href="https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/controls/addpresearch" target="_blank">addPreSearch</a> to perform this action but I was receiving an error regarding the linked entity not existing. A bit of research suggests that the preFilter cannot have a linked entity included.</p>
<p>Whilst researching this functionality, I found the <a href="https://docs.microsoft.com/en-us/powerapps/developer/model-driven-apps/clientapi/reference/controls/addcustomview" target="_blank">addCustomView</a> function which allows a custom view to be created using fetchXml and layoutXml (a tip on getting these is <a href="/dynamics/getting-fetchxml-and-layoutxml-from-advanced-find">here</a>). This can then be assigned to the lookup and used as a filter to only show specific records.</p>
<h2 id="account-structure">Account Structure</h2>
<p>In this example, I am using the Accounts & Contacts in the image below. I will be viewing the Parent Account or Child Account records and will be expecting only users assigned to either of these show in the lookup.</p>
<figure class="">
<img src="/assets/images/posts/addCustomView-with-lookups/1_Contacts.png" alt="View showing the contacts with Dynamics 365" /><figcaption>
View showing the contacts with Dynamics 365
</figcaption></figure>
<h2 id="before">Before</h2>
<p>As would be expected, without including any custom filters all of the contacts are shown when trying to use the lookup.</p>
<figure class="">
<img src="/assets/images/posts/addCustomView-with-lookups/2_UnfilteredData.png" alt="All contacts showing as no filter in place" /><figcaption>
All contacts showing as no filter in place
</figcaption></figure>
<h2 id="javascript">JavaScript</h2>
<p>Adding the below JavaScript and running the function on Page Load will apply the custom view. In this example, I have hardcoded the GUIDs the ID of the Parent Account.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kd">function</span> <span class="nx">filterContacts</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// the view ID can be anything unique</span>
<span class="kd">var</span> <span class="nx">viewId</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">{00000000-0000-0000-0000-000000000003}</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">entityName</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">contact</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">viewDisplayName</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Contacts In Account Family</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">fetchXml</span> <span class="o">=</span><span class="dl">"</span><span class="s2"><fetch version=</span><span class="se">\"</span><span class="s2">1.0</span><span class="se">\"</span><span class="s2"> output-format=</span><span class="se">\"</span><span class="s2">xml-platform</span><span class="se">\"</span><span class="s2"> mapping=</span><span class="se">\"</span><span class="s2">logical</span><span class="se">\"</span><span class="s2"> distinct=</span><span class="se">\"</span><span class="s2">false</span><span class="se">\"</span><span class="s2">><entity name=</span><span class="se">\"</span><span class="s2">contact</span><span class="se">\"</span><span class="s2">><attribute name=</span><span class="se">\"</span><span class="s2">fullname</span><span class="se">\"</span><span class="s2">/><attribute name=</span><span class="se">\"</span><span class="s2">contactid</span><span class="se">\"</span><span class="s2">/><attribute name=</span><span class="se">\"</span><span class="s2">parentcustomerid</span><span class="se">\"</span><span class="s2">/><order attribute=</span><span class="se">\"</span><span class="s2">fullname</span><span class="se">\"</span><span class="s2"> descending=</span><span class="se">\"</span><span class="s2">false</span><span class="se">\"</span><span class="s2">/><link-entity name=</span><span class="se">\"</span><span class="s2">account</span><span class="se">\"</span><span class="s2"> from=</span><span class="se">\"</span><span class="s2">accountid</span><span class="se">\"</span><span class="s2"> to=</span><span class="se">\"</span><span class="s2">parentcustomerid</span><span class="se">\"</span><span class="s2"> link-type=</span><span class="se">\"</span><span class="s2">inner</span><span class="se">\"</span><span class="s2"> alias=</span><span class="se">\"</span><span class="s2">ac</span><span class="se">\"</span><span class="s2">><filter type=</span><span class="se">\"</span><span class="s2">and</span><span class="se">\"</span><span class="s2">><filter type=</span><span class="se">\"</span><span class="s2">or</span><span class="se">\"</span><span class="s2">><condition attribute=</span><span class="se">\"</span><span class="s2">accountid</span><span class="se">\"</span><span class="s2"> operator=</span><span class="se">\"</span><span class="s2">eq</span><span class="se">\"</span><span class="s2"> uiname=</span><span class="se">\"</span><span class="s2">Parent Account</span><span class="se">\"</span><span class="s2"> uitype=</span><span class="se">\"</span><span class="s2">account</span><span class="se">\"</span><span class="s2"> value=</span><span class="se">\"</span><span class="s2">{6AA4C0E1-679B-EA11-A812-000D3A0BA151}</span><span class="se">\"</span><span class="s2">/><condition attribute=</span><span class="se">\"</span><span class="s2">parentaccountid</span><span class="se">\"</span><span class="s2"> operator=</span><span class="se">\"</span><span class="s2">eq</span><span class="se">\"</span><span class="s2"> uiname=</span><span class="se">\"</span><span class="s2">Parent Account</span><span class="se">\"</span><span class="s2"> uitype=</span><span class="se">\"</span><span class="s2">account</span><span class="se">\"</span><span class="s2"> value=</span><span class="se">\"</span><span class="s2">{6AA4C0E1-679B-EA11-A812-000D3A0BA151}</span><span class="se">\"</span><span class="s2">/></filter></filter></link-entity></entity></fetch></span><span class="dl">"</span><span class="p">;</span>
<span class="kd">var</span> <span class="nx">layoutXml</span> <span class="o">=</span> <span class="dl">"</span><span class="s2"><grid name=</span><span class="se">\"</span><span class="s2">resultset</span><span class="se">\"</span><span class="s2"> object=</span><span class="se">\"</span><span class="s2">2</span><span class="se">\"</span><span class="s2"> jump=</span><span class="se">\"</span><span class="s2">lastname</span><span class="se">\"</span><span class="s2"> select=</span><span class="se">\"</span><span class="s2">1</span><span class="se">\"</span><span class="s2"> icon=</span><span class="se">\"</span><span class="s2">1</span><span class="se">\"</span><span class="s2"> preview=</span><span class="se">\"</span><span class="s2">1</span><span class="se">\"</span><span class="s2">><row name=</span><span class="se">\"</span><span class="s2">result</span><span class="se">\"</span><span class="s2"> id=</span><span class="se">\"</span><span class="s2">contactid</span><span class="se">\"</span><span class="s2">><cell name=</span><span class="se">\"</span><span class="s2">fullname</span><span class="se">\"</span><span class="s2"> width=</span><span class="se">\"</span><span class="s2">300</span><span class="se">\"</span><span class="s2">/><cell name=</span><span class="se">\"</span><span class="s2">parentcustomerid</span><span class="se">\"</span><span class="s2"> width=</span><span class="se">\"</span><span class="s2">100</span><span class="se">\"</span><span class="s2">/></row></grid></span><span class="dl">"</span><span class="p">;</span>
<span class="nx">Xrm</span><span class="p">.</span><span class="nx">Page</span><span class="p">.</span><span class="nx">getControl</span><span class="p">(</span><span class="dl">"</span><span class="s2">jason_contactid</span><span class="dl">"</span><span class="p">).</span><span class="nx">addCustomView</span><span class="p">(</span><span class="nx">viewId</span><span class="p">,</span> <span class="nx">entityName</span><span class="p">,</span> <span class="nx">viewDisplayName</span><span class="p">,</span> <span class="nx">fetchXml</span><span class="p">,</span> <span class="nx">layoutXml</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
<span class="nx">Xrm</span><span class="p">.</span><span class="nx">Page</span><span class="p">.</span><span class="nx">getControl</span><span class="p">(</span><span class="dl">"</span><span class="s2">jason_contactid</span><span class="dl">"</span><span class="p">).</span><span class="nx">setDefaultView</span><span class="p">(</span><span class="nx">viewId</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="after">After</h2>
<p>After publishing the changes & reloading the page, you can now see that the Contact lookup is filtered to only show the the Contacts linked to the Parent Account.</p>
<figure class="">
<img src="/assets/images/posts/addCustomView-with-lookups/3_FilteredLookup.png" alt="Lookup showing only contacts with the parent child account structure" /><figcaption>
Lookup showing only contacts with the parent child account structure
</figcaption></figure>
<h2 id="notes">Notes</h2>
<p>This was an interesting way of getting around the preFilter not accepting a linked entity and was easy to add into the solution. One thing to note is that this will not prevent items showing in the ‘Recents’ view for the lookup, so I would recommend disabling that too, information about how to do that is in this <a href="/dynamics/disable-recents-on-lookup">post</a>.</p>Jason Clairblog@jasonclair.co.ukI recently wanted to filter a Lookup within Dynamics 365 to show Contacts where the linked Accounts’ Parent Account was a specific Account. The idea being that the user can select any Contact from the Account Hierarchy but shouldn’t be able to select any Contacts from outside of that structure.Getting FetchXml and LayoutXml from Advanced Find2020-06-02T00:00:00+00:002020-06-02T18:00:00+00:00https://jasonclair.co.uk/dynamics/getting-fetchxml-and-layoutxml-from-advanced-find<p>When working with custom views inside Dynamics 365 you may need to create fetchXml and layoutXml strings for new temporary custom views.</p>
<p>The easiest way to do this, in my opinion, is to create a new Advanced Find with the information & filters you require.</p>
<figure class="">
<img src="/assets/images/posts/getting-fetch-xml/1_CreatingAdvancedFind.png" alt="New Advanced Find" /><figcaption>
New Advanced Find
</figcaption></figure>
<p>Then execute the Advanced Find and get the results.</p>
<figure class="">
<img src="/assets/images/posts/getting-fetch-xml/2_RunningAdvancedFind.png" alt="Showing the results in Advanced Find" /><figcaption>
Showing the results in Advanced Find
</figcaption></figure>
<p>Once you’ve got the results page, open the Developer Tools (F12 in Chrome & Edge) and use the search functionality to look for fetchXml or layoutXml. You should find a div containing your strings.</p>
<figure class="">
<img src="/assets/images/posts/getting-fetch-xml/3_GettingLayoutXml.png" alt="layoutXml and fetchXml showing in the DOM" /><figcaption>
layoutXml and fetchXml showing in the DOM
</figcaption></figure>Jason Clairblog@jasonclair.co.ukWhen working with custom views inside Dynamics 365 you may need to create fetchXml and layoutXml strings for new temporary custom views.Dynamics 365 Solution won’t uninstall2020-05-27T00:00:00+00:002020-05-27T18:00:00+00:00https://jasonclair.co.uk/dynamics/Did-Not-Have-Valid-QuickFind-Query<p>Whilst uninstalling a managed solution I received an error message stating that one of my entities “did not have a valid QuickFind query”. I checked the entity and there definitely was a valid QuickFind view.</p>
<p>It turns out that the solution could not be uninstalled as the entity was being used in the Categorized Search functionality. After removing the entity from the Categorized Search I was able to uninstall the solution correctly.</p>
<p>To remove the entities from the Categorized Search, go to Settings -> Administration -> System Settings and then under the General tab scroll down until the you see the Select button for “Select entities for Categorized Search”</p>
<figure class="">
<img src="/assets/images/posts/quick-find-error/Settings.png" alt="Setting within System Settings" /><figcaption>
Setting within System Settings
</figcaption></figure>Jason Clairblog@jasonclair.co.ukWhilst uninstalling a managed solution I received an error message stating that one of my entities “did not have a valid QuickFind query”. I checked the entity and there definitely was a valid QuickFind view.Dynamics 365 – Automated HTML Reports with Microsoft Flow2019-08-26T00:00:00+00:002019-09-26T18:00:00+00:00https://jasonclair.co.uk/dynamics/Microsoft-Flow-Automated-HTML-Reports-From-Dynamics-365-Data<p>A request that I recently got from a customer was to send a weekly report showing all of the Opportunities that had gone 180 days past the estimated close date and that were still open. The report should be sent as an e-mail and sent to a specific person who can then chase up the owners of the Opportunities.</p>
<h2 id="microsoft-flow">Microsoft Flow</h2>
<p>In the past, this would have required quite a bit of work to get the scheduling to work and to create the HTML for the e-mail. Now, thanks to <a href="https://flow.microsoft.com/en-us/" target="_blank">Microsoft Flow</a> this is a nice and easy task.</p>
<p>Flow comes with a set of connectors out of the box which can connect directly to Dynamics 365. The only real potentially hard part is the OData filters, however, it’s easy to create the ODate query using FetchXML Builder inside the <a href="https://www.xrmtoolbox.com/" target="_blank">XrmToolbox</a></p>
<h2 id="starting-the-flow">Starting the Flow</h2>
<p>Flow needs something to trigger it to run. As this is going to be a weekly e-mail, I have selected the scheduler trigger which allows the time frame to be entered. Other triggers do exist and can include record creation/updates in Dynamics.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/1_setting_frequency.png" alt="Setting the schedule to repeat weekly" /><figcaption>
Setting the schedule to repeat weekly
</figcaption></figure>
<h2 id="querying-for-old-opportunities">Querying for Old Opportunities</h2>
<p>Once the Flow has started, the first step is to query Dynamics for all open Opportunities that are 180 days past their estimated close date. This can be done using the ‘Get Records’ action for Dynamics 365 and passing in the OData filter. As mentioned before, if you are not comfortable with OData then the XrmToolbox allows you to create a FetchXML query and then view the OData for that query.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/2_FetchXmlBuilder.png" alt="Using FetchXML Builder to create the OData string" /><figcaption>
Using FetchXML Builder to create the OData string
</figcaption></figure>
<p>Using the FetchXML Builder gives the full WebAPI for the query. Inside the Get Records connector we just need to take the bit are Filter= and enter this into the Filter Query box.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/3_GettingData.png" alt="Setting the Get Records action to get old & open opportunities" /><figcaption>
Setting the Get Records action to get old & open opportunities
</figcaption></figure>
<p>At this point, we can also pass in other parameters such as the Expand query. This would allow information from related entities to come through. I will be using the Owner full name field, however, the Expand functionality doesn’t seem to work on this field – I believe this is because the Owner could be either a Team or a User so the Expand doesn’t know which entity to expand. There may be other workarounds regarding parsing the JSON, but for this example, I will perform a lookup later on in the Flow to get the Owners name.</p>
<h2 id="starting-the-table">Starting the Table</h2>
<p>The output of this Flow is going to be a simple HTML Table which contains a row for each Opportunity found. Starting the table is as simple as initializing a new variable of type String and setting the value to be the beginning fo an HTML table & the headers.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/4_HTMLTable.png" alt="Starting the HTML Table" /><figcaption>
Starting the HTML Table
</figcaption></figure>
<h2 id="building-the-rows">Building the rows</h2>
<p>Now that the table has been defined, we next need to loop through the items. This is done by using the ‘Apply to Each’ action. In this example, as we’re querying the Owner field which can be a User or a Team, we need to do a conditional step to find out which entity to query.</p>
<p>If the Owner Type is System User then the next steps will query the Users records, otherwise, the Teams records will be queried. After finding the name the table string variable created before will be updated to include a new row.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/5_UserCheck.png" alt="Querying the OwnerType, getting the User record and then creating HTML Row" /><figcaption>
Querying the OwnerType, getting the User record and then creating HTML Row
</figcaption></figure>
<h2 id="closing-the-table-and-sending-the-email">Closing the Table and Sending the Email</h2>
<p>Once the loop has finished the final tasks are to close the table, which is done by just appending the closing tag to the string variable and then sending the e-mail.</p>
<p>In this example, I am using the Outlook Send E-mail action. Adding the table string variable to the body will put the HTML into the e-mail message.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/6_CloseTable.png" alt="Closing the Table tag outside of the loop & then creating & sending the email" /><figcaption>
Closing the Table tag outside of the loop & then creating & sending the email
</figcaption></figure>
<p>The e-mail that is then sent looks like the below. With a bit more effort on the styling, this could be turned into a nice looking weekly report.</p>
<figure class="">
<img src="/assets/images/posts/flow-html-report/7_SentEmail.png" alt="Email that has been sent" /><figcaption>
Email that has been sent
</figcaption></figure>Jason Clairblog@jasonclair.co.ukA request that I recently got from a customer was to send a weekly report showing all of the Opportunities that had gone 180 days past the estimated close date and that were still open. The report should be sent as an e-mail and sent to a specific person who can then chase up the owners of the Opportunities.Unified Interface – Where are the On-Demand Workflows?2019-06-30T00:00:00+00:002019-06-30T18:00:00+00:00https://jasonclair.co.uk/dynamics/Unified-Interface-Where-Are-The-Workflows<p>My customers have got a lot of on-demand workflows that have been created over the years. They are used to being able to go “Run Workflows” to start a new on-demand workflow.</p>
<p>Some of these customers had decided that they wanted to do more training on Microsoft Flow before releasing it to the users, therefore, they asked me to remove the Flow options from Dynamics. This was done by going to Administration -> System Settings -> Customization and disabling ‘Enable Microsoft Flow’ and by removing the ‘Run Flows’ permissions from the customization tab of security roles.</p>
<p>Whilst looking at upgrading some of the environments to Unified Interface one of the first things that got asked was “How do I start an on-demand workflow?”. The answer? Under the “Flow” tab that had been hidden by the settings previously mentioned! Enabling Microsoft Flow & re-adding the ‘Run Flows’ permissions allows the Flow option to appear and under there the on-demand workflows appear too.</p>
<figure class="">
<img src="/assets/images/posts/unified-interface-workflows/FlowMenu.png" alt="Run Workflow now appears under the Flow menu" /><figcaption>
Run Workflow now appears under the Flow menu
</figcaption></figure>Jason Clairblog@jasonclair.co.ukMy customers have got a lot of on-demand workflows that have been created over the years. They are used to being able to go “Run Workflows” to start a new on-demand workflow.Dynamics 365 & Azure DevOps – Unit Testing2019-03-28T00:00:00+00:002019-03-28T18:00:00+00:00https://jasonclair.co.uk/dynamics/Dynamics-365-and-Azure-DevOps-Unit-Testing<p>In a previous <a href="/dynamics/Dynamics-365-Visual-Studio-Team-Services-Build-And-Release-Automated-Solution-Deployment">post</a> I have shown how to use Azure DevOps (was Visual Studio Team Services) builds with <a href="https://marketplace.visualstudio.com/items?itemName=WaelHamze.xrm-ci-framework-build-tasks" target="_blank">Dynamics 365 Build Tools</a> to export an un-managed solution as a managed solution and then install into into a different instance.</p>
<p>I have also then provided some guidance on how to use <a href="https://dynamicsvalue.com/home" target="_blank">FakeXrmEasy</a> to create unit tests for plugins & coded activities in another <a href="/dynamics/Dynamics-365-Unit-Testing-Plugins">post</a>.</p>
<p>The next step is to prevent solutions being installed if the unit tests fail. This a simple but powerful addition to the build profile as it helps to further reduce the amount of bugs deployed to production. It can also be used to identify issues before development code is moved to test instances.</p>
<p>I will assume that the previous <a href="/dynamics/Dynamics-365-Visual-Studio-Team-Services-Build-And-Release-Automated-Solution-Deployment">post</a> about using Azure DevOps has been read and understood.</p>
<p>The first step is to ensure that the unit test assembly (.dll) files are in the source control. Without these, the test runner will be unable to run the tests.</p>
<p>Adding the unit test step is as easy as editing the build & adding the <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/test/vstest?view=azure-devops" target="_blank">Visual Studio Test</a> step at the beginning of the agent and then configuring the Test files field to include the search pattern that would find the test assembly.</p>
<figure class="">
<img src="/assets/images/posts/dynamics-and-azure-devops/VisualStudioTestSettings.png" alt="Example configuration fo the Visual Studio Test step - in my example the unit test project compiles to UnitTests.dll" /><figcaption>
Example configuration fo the Visual Studio Test step - in my example the unit test project compiles to UnitTests.dll
</figcaption></figure>
<p>After the step has been added, any future runs of the build will first check to make sure the unit tests pass. If they do not, then the build fails and subsequently the release is not triggered, so the solutions are not installed on the target instance.</p>
<p>I hope you will agree that this is a quick & easy addition to the build profile which will allow even more confidence that plugins & coded activities inside solutions are bug free.</p>Jason Clairblog@jasonclair.co.ukIn a previous post I have shown how to use Azure DevOps (was Visual Studio Team Services) builds with Dynamics 365 Build Tools to export an un-managed solution as a managed solution and then install into into a different instance.Dynamics 365 - Unit Testing Plugins2019-03-23T00:00:00+00:002019-03-23T18:00:00+00:00https://jasonclair.co.uk/dynamics/Dynamics-365-Unit-Testing-Plugins<p>I am an advocate of using unit testing wherever possible. This is because it allows repeatable tests to run, usually in a fraction of a second. Using unit tests allows me to be confident that later updates will not affect the current functionality.</p>
<p>One of the issues with unit testing I had with unit testing for Dynamics plugins & coded activities is that I did not want the unit test to run directly against the Dynamics instance. The two main reasons for this are:</p>
<ol>
<li>I did not want to be performing actions against an instance because part of the reason the test could fail is if the record the test is expecting to be there has been deleted</li>
<li>I did not want to test the connection to the platform. The unit tests should just test the code, not the overall platform.</li>
</ol>
<h2 id="mocks">Mocks</h2>
<p>To prevent connecting to an actual instance, we can utilise Mocks.</p>
<blockquote>
<p>In a unit test, mock objects can simulate the behavior of complex, real objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test.</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Mock_object" target="_blank">https://en.wikipedia.org/wiki/Mock_object</a></li>
</ul>
</blockquote>
<p>In this example, I will be using a Mocking Framework called FakeXrmEasy. This is a fantastic framework which allows most (if not all) of Dynamics functionality to be replicated. The website provides good examples of how to use it – but I’ll go through another example in this post.</p>
<h2 id="test-scenario">Test Scenario</h2>
<p>The plugin that I will be using for this example is simple. It trigger on PreOperation when an Account is created. It will take the name & address1_city fields and concatenate them into another field called <em>new_nameandcity</em>.</p>
<p>The tests that I will run on this plugin are:</p>
<ol>
<li>t should work when name and address1_city are provided</li>
<li>It should throw an exception when address1_city is not provided</li>
</ol>
<h2 id="installation">Installation</h2>
<p>In my example, I have created a new Unit Test project (I am using MS Test, but this framework does work with others, like NUnit). Inside the Unit Test project, open <a href="https://www.nuget.org/" target="_blank">NuGet</a> and search for FakeXrmEasy, then install the version that matches your target instance of Dynamics.</p>
<p>Alternatively, you can use the NuGet console to install directly from the command line – the names follow the pattern ‘FakeXrmEasy.version’, for exampleFakeXrmEasy.365.</p>
<h2 id="plugin-modification">Plugin Modification</h2>
<p>If you are using the Dynamics 365 Toolkit to generate the plugin structure, then you should see that the plugin constructor accepts secure & unsecure configuration as parameters.</p>
<p>For the unit test framework to be able to access the plugin, another constructor needs to be added which accepts no parameters. I thought that a parameterless constructor had to be added, but <a href="https://www.linkedin.com/in/betimbeja/" target="_blank">Betim Beja</a> has pointed out that he has updated the framework to allow the plugin instance to be created & then passed it to ExecutePlugin.</p>
<p>In this example, I have also added a property which I use to determine if the plugin is being executed by the unit test runner or by the Dynamics system. I have done this to highlight that sometimes your code may need to be modified to run correctly in both environments. In my example, I am running on Create in PreOperation. If the code is executed within Dynamics, then the Update command cannot be called at the end otherwise an exception is shown regarding record not existing. However, if Update is not ran in the test, then the record is not Updated and the test fails.</p>
<h2 id="code">Code</h2>
<p>The two unit tests are included below. A sample project has also been uploaded to <a href="https://github.com/JasonClair/blog_examples/tree/master/plugin_unit_test_examples" target="_blank">GitHub</a>.</p>
<h3 id="account-created-unit-test--successful">Account Created Unit Test – Successful</h3>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">/// <summary></span>
<span class="c1">/// Test that when a new account is created the new_nameandcity field is updated with the values</span>
<span class="c1">/// from the name & address1_city fields</span>
<span class="c1">/// </summary></span>
<span class="p">[</span><span class="n">TestMethod</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Field_Updated_When_Account_Created</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">// Create the fake context. This allows us to setup a mocked version of Dynamics for use later in the test</span>
<span class="n">XrmFakedContext</span> <span class="n">context</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">XrmFakedContext</span><span class="p">();</span>
<span class="c1">// Generate a GUID which will be assigned to the fake record. It is done here to allow the record to be retrieved</span>
<span class="c1">// later during the test</span>
<span class="n">Guid</span> <span class="n">accountGuid</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">();</span>
<span class="c1">// Create the entity record which will be the target. In this case I have set the name & address1_city</span>
<span class="c1">// field to have a value, and assigned the new_nameandcity field to have no value</span>
<span class="n">Entity</span> <span class="n">account</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Entity</span><span class="p">(</span><span class="s">"account"</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">Id</span> <span class="p">=</span> <span class="n">accountGuid</span><span class="p">,</span>
<span class="n">Attributes</span> <span class="p">=</span>
<span class="p">{</span>
<span class="p">[</span><span class="s">"name"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"Acme"</span><span class="p">,</span>
<span class="p">[</span><span class="s">"address1_city"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"America"</span><span class="p">,</span>
<span class="p">[</span><span class="s">"new_nameandcity"</span><span class="p">]</span> <span class="p">=</span> <span class="s">""</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="c1">// This will setup the context with the entities created. This is the set of entities we will be able to access</span>
<span class="c1">// during the test. </span>
<span class="n">context</span><span class="p">.</span><span class="nf">Initialize</span><span class="p">(</span><span class="k">new</span> <span class="n">List</span><span class="p"><</span><span class="n">Entity</span><span class="p">></span> <span class="p">{</span> <span class="n">account</span> <span class="p">});</span>
<span class="c1">// Execute the plugin. I have added the MessageName & stage as this plugin executes PreOperation</span>
<span class="n">context</span><span class="p">.</span><span class="n">ExecutePluginWithTarget</span><span class="p"><</span><span class="n">PreOperationaccountCreate</span><span class="p">>(</span><span class="n">account</span><span class="p">,</span> <span class="s">"Create"</span><span class="p">,</span> <span class="m">20</span><span class="p">);</span>
<span class="c1">// Retrieve the update account from the fake context</span>
<span class="n">Entity</span> <span class="n">updatedAccount</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">GetOrganizationService</span><span class="p">()</span>
<span class="p">.</span><span class="nf">Retrieve</span><span class="p">(</span><span class="s">"account"</span><span class="p">,</span> <span class="n">accountGuid</span><span class="p">,</span> <span class="k">new</span> <span class="nf">ColumnSet</span><span class="p">(</span><span class="k">new</span> <span class="kt">string</span><span class="p">[]</span> <span class="p">{</span> <span class="s">"new_nameandcity"</span> <span class="p">}));</span>
<span class="c1">// Assert that the value meets our expected value</span>
<span class="n">Assert</span><span class="p">.</span><span class="nf">AreEqual</span><span class="p">(</span><span class="s">"Acme - America"</span><span class="p">,</span> <span class="n">updatedAccount</span><span class="p">.</span><span class="n">GetAttributeValue</span><span class="p"><</span><span class="kt">string</span><span class="p">>(</span><span class="s">"new_nameandcity"</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="account-created-unit-test--exception-thrown">Account Created Unit Test – Exception Thrown</h3>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">/// <summary></span>
<span class="c1">/// The plugin should throw an exception if the city or name fields are not filled in</span>
<span class="c1">/// </summary></span>
<span class="p">[</span><span class="n">TestMethod</span><span class="p">]</span>
<span class="p">[</span><span class="nf">ExpectedException</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">Exception</span><span class="p">))]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Error_Thrown_If_City_Is_Null</span><span class="p">()</span>
<span class="p">{</span>
<span class="c1">// Create the fake context. This allows us to setup a mocked version of Dynamics for use later in the test</span>
<span class="n">XrmFakedContext</span> <span class="n">context</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">XrmFakedContext</span><span class="p">();</span>
<span class="c1">// Generate a GUID which will be assigned to the fake record. It is done here to allow the record to be retrieved</span>
<span class="c1">// later during the test</span>
<span class="n">Guid</span> <span class="n">accountGuid</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">();</span>
<span class="c1">// Create an account record which has no address1_city filled in. This will be used during the plugin execution</span>
<span class="n">Entity</span> <span class="n">account</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Entity</span><span class="p">(</span><span class="s">"account"</span><span class="p">)</span>
<span class="p">{</span>
<span class="n">Id</span> <span class="p">=</span> <span class="n">accountGuid</span><span class="p">,</span>
<span class="n">Attributes</span> <span class="p">=</span>
<span class="p">{</span>
<span class="p">[</span><span class="s">"name"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"Acme"</span><span class="p">,</span>
<span class="p">[</span><span class="s">"address1_city"</span><span class="p">]</span> <span class="p">=</span> <span class="s">""</span><span class="p">,</span>
<span class="p">[</span><span class="s">"new_nameandcity"</span><span class="p">]</span> <span class="p">=</span> <span class="s">""</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="c1">// This will setup the context with the entities created. This is the set of entities we will be able to access</span>
<span class="c1">// during the test. </span>
<span class="n">context</span><span class="p">.</span><span class="nf">Initialize</span><span class="p">(</span><span class="k">new</span> <span class="n">List</span><span class="p"><</span><span class="n">Entity</span><span class="p">></span> <span class="p">{</span> <span class="n">account</span> <span class="p">});</span>
<span class="c1">// Execute the plugin. I have added the MessageName & stage as this plugin executes PreOperation</span>
<span class="n">context</span><span class="p">.</span><span class="n">ExecutePluginWithTarget</span><span class="p"><</span><span class="n">PreOperationaccountCreate</span><span class="p">>(</span><span class="n">account</span><span class="p">,</span> <span class="s">"Create"</span><span class="p">,</span> <span class="m">20</span><span class="p">);</span>
<span class="c1">// I am not asserting anything here as the plugin only needs to throw an exception. If no exception is thrown</span>
<span class="c1">// then the unit test would fail</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="summary">Summary</h2>
<p>Hopefully you can see how easy unit testing can be when using the FakeXrmEasy framework. Although unit testing can cause a bit more work upfront, it easily pays for itself through reduced bugs being introduced at later dates.</p>
<p>Unit testing will also allow more confidence when refactoring or adding new functionality as you can re-run all of the tests in seconds to make sure none of the previous functionality has changed.</p>
<p>It is important to remember that unit tests need to be maintained overtime. If the functionality is meant to change, then the unit test needs to be updated to include this. You may also forget to test certain scenarios, when this is noticed, the unit tests should be added again. The aim is to have every execution path covered.</p>
<h2 id="further-functionality">Further Functionality</h2>
<p>In a previous <a href="/dynamics/Dynamics-365-Visual-Studio-Team-Services-Build-And-Release-Automated-Solution-Deployment">post</a>, I demonstrated using Azure DevOps (was previously named Visual Studio Team Services) pipelines to automated deployment of managed solutions using <a href="https://marketplace.visualstudio.com/items?itemName=WaelHamze.xrm-ci-framework-build-tasks" target="_blank">Dynamics 365 Build Tools</a>. It is possible to integrate unit tests into the pipeline, to prevent deployment of solutions if the unit tests fail. I will cover how to do this in a later blog.</p>Jason Clairblog@jasonclair.co.ukI am an advocate of using unit testing wherever possible. This is because it allows repeatable tests to run, usually in a fraction of a second. Using unit tests allows me to be confident that later updates will not affect the current functionality.Dynamics 365 – Show SharePoint Documents2019-02-17T00:00:00+00:002019-02-17T18:00:00+00:00https://jasonclair.co.uk/dynamics/Dynamics-365-Show-SharePoint-Documents-On-Form<p>In a previous <a href="/dynamics/Dynamics-365-Enable-SharePoint-Document-Management">post</a>, I showed how to enable Dynamics 365 SharePoint integration. This allows you to store attachments in SharePoint instead of Dynamics – which is a lot cheaper and allows for better document management.</p>
<p>One of the issues with using SharePoint to store the attachments is that they cannot be shown on Dynamics forms out of the box. This means that the user has to use the navigation menu to see them. In my experience this can cause frustration for the users.</p>
<p>This is an easy fix by using JavaScript and an IFrame on the form. The content of the IFrame is set by JavaScript. In my case, I have also used a separate tab to have the IFrame in. I do this so that I can hide the entire section if there is no associated Document Location.</p>
<p>This idea was originally found on <a href="https://jlattimer.blogspot.com/2017/01/show-sharepoint-documents-on-main-form.html" target="_blank">Jason Lattimer’s blog</a>, but I have modified it for my own needs by adding the tab functionality. I have also updated the code to use formContext instead of Xrm.Page to future proof it.</p>
<h2 id="form-setup">Form Setup</h2>
<p>I use the components below on my form. I recommend using the settings in the IFrame to give the cleanest look, but these changeable based on your requirements.</p>
<ul>
<li>Tab</li>
<li>IFrame
<ul>
<li>Restrict cross-frame scripting has to be disabled</li>
<li>Number of Rows set to 34</li>
<li>Scrolling set to Never</li>
<li>Display Border disabled</li>
</ul>
</li>
</ul>
<h2 id="script-setup">Script Setup</h2>
<p>The following script then needs to be added to the on-load event of the Form with the formContext passed as the initial variable & then the tab name and IFrame name following it.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// Show Documents sub-grid on the main form</span>
<span class="kd">function</span> <span class="nx">setDocumentsIFrame</span><span class="p">(</span><span class="nx">executionContext</span><span class="p">,</span> <span class="nx">tabName</span><span class="p">,</span> <span class="nx">iframeName</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">formContext</span> <span class="o">=</span> <span class="nx">executionContext</span><span class="p">.</span><span class="nx">getFormContext</span><span class="p">();</span>
<span class="c1">// Hide the tab until we're sure there's something to show</span>
<span class="kd">var</span> <span class="nx">tab</span> <span class="o">=</span> <span class="nx">formContext</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">tabName</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">tab</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Could not find tab: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">tabName</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">tab</span><span class="p">.</span><span class="nx">setVisible</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="c1">// Only want to run the code if the record is just being created, otherwise</span>
<span class="c1">// there will no documents to show anyway</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">formContext</span><span class="p">.</span><span class="nx">ui</span><span class="p">.</span><span class="nx">getFormType</span><span class="p">()</span> <span class="o">!==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">iframe</span> <span class="o">=</span> <span class="nx">formContext</span><span class="p">.</span><span class="nx">getControl</span><span class="p">(</span><span class="nx">iframeName</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">iframe</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Could not find iframe: </span><span class="dl">"</span> <span class="o">+</span> <span class="nx">iframeName</span><span class="p">);</span>
<span class="k">return</span><span class="p">;</span>
<span class="p">}</span>
<span class="kd">var</span> <span class="nx">id</span> <span class="o">=</span> <span class="nx">formContext</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">entity</span><span class="p">.</span><span class="nx">getId</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="dl">"</span><span class="s2">{</span><span class="dl">"</span><span class="p">,</span> <span class="dl">""</span><span class="p">).</span><span class="nx">replace</span><span class="p">(</span><span class="dl">"</span><span class="s2">}</span><span class="dl">"</span><span class="p">,</span> <span class="dl">""</span><span class="p">);</span>
<span class="c1">// Query SharePoint Documents Locations to find any records which are related to the </span>
<span class="c1">// record we are currently viewing</span>
<span class="kd">var</span> <span class="nx">req</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">GET</span><span class="dl">"</span><span class="p">,</span>
<span class="nx">formContext</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">getClientUrl</span><span class="p">()</span> <span class="o">+</span>
<span class="dl">"</span><span class="s2">/api/data/v8.0/sharepointdocumentlocations?$select=_regardingobjectid_value&$filter=_regardingobjectid_value eq </span><span class="dl">"</span> <span class="o">+</span>
<span class="nx">id</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">OData-MaxVersion</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">4.0</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">OData-Version</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">4.0</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Accept</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/json; charset=utf-8</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Prefer</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">odata.include-annotations=</span><span class="se">\"</span><span class="s2">OData.Community.Display.V1.FormattedValue</span><span class="se">\"</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">results</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">response</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">results</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">length</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// If we have any related items then show the tab and set the IFrame URL accordingly</span>
<span class="nx">tab</span><span class="p">.</span><span class="nx">setVisible</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">url</span> <span class="o">=</span> <span class="nx">formContext</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">getClientUrl</span><span class="p">()</span> <span class="o">+</span>
<span class="dl">"</span><span class="s2">/userdefined/areas.aspx?formid=ab44efca-df12-432e-a74a-83de61c3f3e9&inlineEdit=1&navItemName=Documents&oId=%7b</span><span class="dl">"</span> <span class="o">+</span>
<span class="nx">formContext</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">entity</span><span class="p">.</span><span class="nx">getId</span><span class="p">().</span><span class="nx">replace</span><span class="p">(</span><span class="dl">"</span><span class="s2">{</span><span class="dl">"</span><span class="p">,</span> <span class="dl">""</span><span class="p">).</span><span class="nx">replace</span><span class="p">(</span><span class="dl">"</span><span class="s2">}</span><span class="dl">"</span><span class="p">,</span> <span class="dl">""</span><span class="p">)</span> <span class="o">+</span>
<span class="dl">"</span><span class="s2">%7d&oType=</span><span class="dl">"</span> <span class="o">+</span>
<span class="nx">formContext</span><span class="p">.</span><span class="nx">context</span><span class="p">.</span><span class="nx">getQueryStringParameters</span><span class="p">().</span><span class="nx">etc</span> <span class="o">+</span>
<span class="dl">"</span><span class="s2">&pagemode=iframe&rof=true&security=852023&tabSet=areaSPDocuments&theme=Outlook15White</span><span class="dl">"</span><span class="p">;</span>
<span class="nx">iframe</span><span class="p">.</span><span class="nx">setSrc</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">tab</span><span class="p">.</span><span class="nx">setVisible</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">statusText</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="nx">req</span><span class="p">.</span><span class="nx">send</span><span class="p">();</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="finished-product">Finished Product</h2>
<p>After the form & script have been updated, the documents sub- grid should appear on the form as below.</p>
<figure class="">
<img src="/assets/images/posts/show-sharepoint-documents/DocumentsShown.png" alt="Documents showing in sub-grid on the Accounts Main Form" /><figcaption>
Documents showing in sub-grid on the Accounts Main Form
</figcaption></figure>Jason Clairblog@jasonclair.co.ukIn a previous post, I showed how to enable Dynamics 365 SharePoint integration. This allows you to store attachments in SharePoint instead of Dynamics – which is a lot cheaper and allows for better document management.Dynamics 365 – Enabling SharePoint Document Management2019-02-14T00:00:00+00:002019-02-14T18:00:00+00:00https://jasonclair.co.uk/dynamics/Dynamics-365-Enable-SharePoint-Document-Management<p>Dynamics 365 is able to store attachments on records. This is obviously already useful but was made even more useful when Microsoft added the ability to search within documents (<a href="https://docs.microsoft.com/en-us/dynamics365/customer-engagement/basics/relevance-search-results" target="_blank">more info</a>).</p>
<p>Unfortunately, the amount of free storage you get within Dynamics is quite low, and the cost per GB is expensive. This means that storing documents such as quotes & invoices quickly becomes expensive.</p>
<p>One of the methods that can be used to reduce the cost is to store the documents in Microsoft SharePoint. SharePoint is a collaborative platform and has a vastly reduced cost per GB than Dynamics.</p>
<p>A downside is that the search functionality for Dynamics cannot check within documents stored on SharePoint. Due to this, it may be worth having some documents stored in SharePoint & some in Dynamics.</p>
<h2 id="security">Security</h2>
<p>An important thing to keep in mind is that security roles do not get copied automatically. There are some 3rd party tools that can do it, and it may be possible to create your own solution, but out of the box, you will need to manage this manually.</p>
<p>Due to the structure of the SharePoint folders, it will be easier for people to navigate around the folders. This means that someone may be able to easily see documents for an opportunity that they do not have permission to see in Dynamics.</p>
<p>This means that you will want to take care when enabling the integration and make sure that you’re happy with the permissions. I would recommend testing the permissions from a range of user permissions.</p>
<h2 id="setup">Setup</h2>
<p>The environment during this demo is a Dynamics 365 Free 30 day trial. The trial also comes with a trial version of SharePoint.</p>
<p>The first step to enable Dynamics 365 SharePoint Integration is to go to <strong>Settings -> Document Management</strong> and from within there select <strong>Document Management Settings</strong>.</p>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/1_DocumentManagementSettings.png" alt="Document Management Settings" /><figcaption>
Document Management Settings
</figcaption></figure>
<p>A new window will open. In this window, you will be able to select the entities to enable Document Management on. You will also be able to enter the SharePoint URL (in my example I have made a site specifically for CRM documents).</p>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/2_chooseentities.png" alt="First Window showing the entities that can be enabled & the site url" /><figcaption>
First Window showing the entities that can be enabled & the site url
</figcaption></figure>
<p>The second page will then allow you to confirm the URL entered & select the folder structure. In my case I have selected to base it on the account entity. This means all the files will be stored under the account folder – each entity will get its own subfolder.</p>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/3_FolderStructure.png" alt="Selecting the folder structure" /><figcaption>
Selecting the folder structure
</figcaption></figure>
<p>Selecting next will allow the system to create the top-level folders in the SharePoint site.</p>
<p>Interestingly, even though I have selected to have a folder structure based on an entity, each entity will still get an individual top-level folder. The folder structure will still be honored though, so any opportunity documents are added under <em>/account/AccountName/opportunity/</em></p>
<p><img src="/assets/images/posts/enable-sharepoint-document-management/4_CreateFolders.png" alt="Folders are created on SharePoint site" /></p>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/4_CreateFolders.png" alt="Folders are created on SharePoint site" /><figcaption>
Folders are created on SharePoint site
</figcaption></figure>
<h2 id="usage">Usage</h2>
<p>The documents are then accessible from within a record using the navigation menu at the top for related entities using the Documents link.</p>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/5_NavigateToDocuments.png" alt="Navigation Menu shows Documents under Related Items" /><figcaption>
Navigation Menu shows Documents under Related Items
</figcaption></figure>
<p>It is not possible to add a sub-grid for SharePoint Documents to the page out of the box. I have created another <a href="/dynamics/dynamics-365-show-sharepoint-documents-on-form">post</a> which shows how you can use an IFrame to achieve this.</p>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/6_DocumentsShown.png" alt="The view from within Dynamics" /><figcaption>
The view from within Dynamics
</figcaption></figure>
<figure class="">
<img src="/assets/images/posts/enable-sharepoint-document-management/7_DocumentShownInSharePoint.png" alt="The view from within SharePoint" /><figcaption>
The view from within SharePoint
</figcaption></figure>
<h2 id="summary">Summary</h2>
<p>The Dynamics 365 SharePoint Integration is easy to setup & can help reduce costs. You will need to consider how you implement the security, but this should be manageable.</p>
<p>You will also receive the added benefit of version control & SharePoint workflows, which can help with document management.</p>
<p>The ability to search within documents in Dynamics will be lost, but it is possible to search with SharePoint. This is something that you will have to decide if it is worth it or not. It is possible to have some documents in Dynamics & some in SharePoint.</p>Jason Clairblog@jasonclair.co.ukDynamics 365 is able to store attachments on records. This is obviously already useful but was made even more useful when Microsoft added the ability to search within documents (more info).