<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://onthefencedevelopment.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://onthefencedevelopment.com/" rel="alternate" type="text/html" /><updated>2026-01-07T22:26:53+00:00</updated><id>https://onthefencedevelopment.com/feed.xml</id><title type="html">On The Fence Development</title><subtitle>Ramblings of a .NET &amp; Xamarin (soon to be MAUI) Developer from the UK</subtitle><entry><title type="html">A New Start in 2026</title><link href="https://onthefencedevelopment.com/2026/01/07/a-new-start.html" rel="alternate" type="text/html" title="A New Start in 2026" /><published>2026-01-07T00:00:00+00:00</published><updated>2026-01-07T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2026/01/07/a-new-start</id><content type="html" xml:base="https://onthefencedevelopment.com/2026/01/07/a-new-start.html"><![CDATA[<p><img src="/assets/images/fresh-start-2026.png" alt="ai generated image showing a road heading off into the distance towards 2026 which has a light shining through it. on the road there is a text marking saying start with an arrow pointing to the horizon. the sky is blue with clouds separating and confetti falling." width="220px" style="float:left; margin-right: 10px;" />
2026 is a new start for me and while I’m looking forward to the next twelve months, this optimism has come at a cost.</p>

<p>The cost has been the closure of my Limited Company that I setup in 2023 and marks my departure from the world of contracting and my return to regular employment.</p>

<p>Now I’m fortunate in that I’m returning to work for not only a previous client, but also a previous employer who reached out to me with this opportunity. I clearly tick all their boxes, and they know I can do the job they need me to do.</p>

<p>So although there will be challenges ahead, my new role will see me helping drive their product forward; I do need something outside of work to keep my mind working.</p>

<!--more-->

<p>This blog was created many years ago with the theme being that I wouldn’t fixate on a single technology, that I would mix things up a bit. I think this has stood me in good stead for being a contractor as it kept my skills sharp and varied.</p>

<p>With that in mind, it should come as no surprise that I have a number of personal projects including the <a class="page-link" href="/smite-scoreboard-for-android-ios.html">Smite Scoreboard</a> app and <a class="page-link" href="/motorhome-stopover-home.html">Motorhome Stopover</a> website with its companion mobile app.</p>

<p>While these projects are always quite needy in that they require regular updates, especially the mobile apps as the respective App Stores require apps to be built with the more recent versions of their SDKs. I fully understand the reasons for this, i.e. security updates and the like, but it is a regular pain in the backside to have to reimplement a feature because the API methods I was using have been deprecated.</p>

<p>But with at least one of these apps being functionally complete, I felt like I needed something new to get my teeth into. So I’ve decided to revisit a project that was initially just a proof of concept. A project that makes a statement in todays political climate in terms of privacy.</p>

<p>The project is called ShhhSMS and allows secure text messages to be sent over good old-fashioned SMS. No WhatsApp, no Signal, no Telegram … none of that.</p>

<p>The source code and (I think) a decent README file explaining the rationale of the project and it’s usage can be found <a href="https://github.com/OnTheFenceDevelopment/ShhhSMS" target="_blank">on Github</a></p>

<p>But it doesn’t stop there, no, why make things easy on myself.</p>

<p>With the current source code written using the now obsolete Xamarin framework and only targeting Android devices, I’m thinking that I should see if I can rebuild this app using MAUI and (because I don’t need to target iOS) do it entirely on a system running Linux and using the JetBrains Rider IDE.</p>

<p>Now I know I am going to run into some obstacles along the way. Some of them will be small and others not so much but hopefully I won’t run into any showstoppers that stop my in my tracks.</p>

<p>I’ll be sure to report back in a series of posts but even if I’m taking on an unachieveable goal here …. I’m sure I’ll have some fun along the way (yep, I’m that kinda developer).</p>

<blockquote>
  <p>If you made it here then well done! I know that this post is a little scrappy, but I was keen to get it out of my head and blogged. 
Thanks for sticking with it :)</p>
</blockquote>]]></content><author><name></name></author><category term="career" /><category term="regular-employment" /><category term="linux" /><category term="ubuntu" /><category term="jetbrains-rider" /><category term="privacy" /><summary type="html"><![CDATA[2026 is a new start for me and while I’m looking forward to the next twelve months, this optimism has come at a cost. The cost has been the closure of my Limited Company that I setup in 2023 and marks my departure from the world of contracting and my return to regular employment. Now I’m fortunate in that I’m returning to work for not only a previous client, but also a previous employer who reached out to me with this opportunity. I clearly tick all their boxes, and they know I can do the job they need me to do. So although there will be challenges ahead, my new role will see me helping drive their product forward; I do need something outside of work to keep my mind working.]]></summary></entry><entry><title type="html">Contracting is Dead in the UK</title><link href="https://onthefencedevelopment.com/2025/11/09/contracting-is-dead.html" rel="alternate" type="text/html" title="Contracting is Dead in the UK" /><published>2025-11-09T00:00:00+00:00</published><updated>2025-11-09T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2025/11/09/contracting-is-dead</id><content type="html" xml:base="https://onthefencedevelopment.com/2025/11/09/contracting-is-dead.html"><![CDATA[<p><img src="/assets/images/thats_all_folks.png" alt="looney tunes logo" width="220px" style="float:left; margin-right: 10px;" /></p>

<p>When I started contracting back in 2011 the market was buoyant and it was not unusual to be fielding multiple calls a week (or day) from recruiters asking whether you were available or not. This meant that when you were coming to the end of one contract you normally had another secured to follow on. Life was good.</p>

<p>Then Covid happened and the whole landscape changed with the contract market essentially imploding overnight. Unfortunately, when the world started to get back to normal the contract market didn’t follow suit, it remained pretty much stagnant.</p>

<p>The primary cause of this can be leveled at IR35 and the Off-Payroll changes rolled out to the private sector in April 2021. Now, I’m not going to try to explain the abomination that is IR35 because that’s not going to achieve anything, it will make this a very long and boring post and frankly it isn’t going away, so there is no point banging on about it.</p>

<p>The long and the short if it is …. I’m out … I’m done with contracting and that’s that!</p>

<p>But where does that leave me, because the permanent job market isn’t a great deal better.</p>

<!--more-->

<p>In recent months I’ve been trying to sit on the fence a little by searching for a permanent role while also keeping an eye out for a contract gig to hopefully keep cash flowing in the right direction. The thought process was that I could hopefully secure a contract for a couple of months while hopefully progressing through the interview process for a permanent role or maybe even two.</p>

<p>But there’s a flaw in this approach which can be summed up in an old saying;</p>

<blockquote>
  <p>A man who chases two rabbits will catch neither</p>
</blockquote>

<p>The process of finding a job needs something I was not providing …. focus.</p>

<p>That changes today!</p>

<p>But while I can focus my efforts on securing a permanent role I can do little about the concerns that some hiring managers have about bringing a contractor in as a permanent employee.</p>

<h2 id="the-should-i-hire-a-contractor-as-an-employee-dilemma">The ‘Should I Hire a Contractor as an Employee’ Dilemma</h2>
<p>There are many people on LinkedIn saying that they would never hire a contractor because;</p>

<blockquote>
  <p>If a nice juicy contract came along they’d be off like a shot.</p>
</blockquote>

<p>Now while I can understand the thought process here it is pretty shortsighted to say the least. The market is flat on its back, and even if a ‘nice juicy contract’ did come along, it would be a foolhardy contractor that would cut and run because what would happen when that contract came to an end? There is no certainty that another contract could be secured to follow-on, and they would be in limbo once more.</p>

<p>Also, should I be able to secure a permanent role I’d be free of IR35/OffPayroll and all that goes with it. Believe me … if I can free myself of the spectre that horribly flawed abomination I’d not be looking to stray into its path again.</p>

<p>I could argue that a contractor would be a better option because they will have gained a wealth of experience across multiple projects, tech stacks, project management processes and sectors. They will have been motivated to keep their skills up to date abd build a reputation for getting the job done (and done to a high standard).</p>

<h2 id="next-steps">Next Steps</h2>
<p>Now that the decision has been made to focus on a single rabbit here is my plan;</p>

<ul>
  <li>Update Job Boards to remove ‘Contract’ as a desired job types and setup corresponding job searches and notifications</li>
  <li>Reach out to previous Clients that I would work for again (not all fall into this category)</li>
  <li>Send an availability update to recruiters who I’ve dealt with before or who have contacted me recently with permanent roles</li>
  <li>Revisit and push my personal projects forward as part of my portfolio</li>
  <li>Put a training plan together - what’s new in C#, .NET and MAUI</li>
  <li>Blog more … and that means here</li>
  <li>Consider a YouTube channel … scary stuff and outside my comfort zone, but I need to consider whatever it takes to raise my profile &amp; get myself noticed.</li>
</ul>]]></content><author><name></name></author><category term="career" /><category term="contracting" /><category term="ir35" /><summary type="html"><![CDATA[When I started contracting back in 2011 the market was buoyant and it was not unusual to be fielding multiple calls a week (or day) from recruiters asking whether you were available or not. This meant that when you were coming to the end of one contract you normally had another secured to follow on. Life was good. Then Covid happened and the whole landscape changed with the contract market essentially imploding overnight. Unfortunately, when the world started to get back to normal the contract market didn’t follow suit, it remained pretty much stagnant. The primary cause of this can be leveled at IR35 and the Off-Payroll changes rolled out to the private sector in April 2021. Now, I’m not going to try to explain the abomination that is IR35 because that’s not going to achieve anything, it will make this a very long and boring post and frankly it isn’t going away, so there is no point banging on about it. The long and the short if it is …. I’m out … I’m done with contracting and that’s that! But where does that leave me, because the permanent job market isn’t a great deal better.]]></summary></entry><entry><title type="html">The Day I Cancelled Pluralsight</title><link href="https://onthefencedevelopment.com/2025/09/09/the-day-i-cancelled-pluralsight.html" rel="alternate" type="text/html" title="The Day I Cancelled Pluralsight" /><published>2025-09-09T00:00:00+00:00</published><updated>2025-09-09T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2025/09/09/the-day-i-cancelled-pluralsight</id><content type="html" xml:base="https://onthefencedevelopment.com/2025/09/09/the-day-i-cancelled-pluralsight.html"><![CDATA[<p><img src="/assets/images/pluralsight-subscription-cancelled.png" alt="crossed out pluralsight logo" width="220px" style="float:left; margin-right: 10px;" />
Back in early 2012 I watched an online course on Pluralsight - <strong>Data Layer Validation with Entity Framework 4.1+</strong> by Julie Lerman.</p>

<p>It was the first of a great many courses I’ve watched since then and I’ve learned a lot.</p>

<p>But in recent years I’ve found that I’ve spent more time watching other high quality courses elsewhere.</p>

<p>Previously there wasn’t anything like Pluralsight - sure, there was YouTube but the quality just wasn’t there. You could find some useful content but it fell short of the quality of the courses on Pluralsight.
<!--more--></p>

<p>In the year so far I’ve accessed around a dozen courses on Pluralsight which may sound like a lot, but just because I’ve accessed them doesn’t mean I’ve watched them. While I have viewed a handful from start to finish, most have only been watched for a few minutes, maybe just a single section within an entire course.</p>

<p>Add to this the fact that Pluralsight content for certain topics is limited, e.g. Umbraco &amp; MAUI, so when the renewal price came in at over $120 I decided it was time to pull the plug. Now, $120 may not seem like a lot of money ($10/month) but sometimes you need to do some housekeeping and Pluralsight isn’t the only service to be axed.</p>

<p>I’ll be a little sorry to see it go but with so much high quality accessible for free. Some of it from software technology ‘rockstars’ like James Montemagno, Nick Cosentino, Gerald Versluis, Milan Jovanović. AI content is becoming more plentiful but I’ve found Liam Ottley and Nate Herk the top of the pile for my needs.</p>

<p>This is also from an ever-increasing number of content creators who really know their stuff and have the ability to present it.</p>

<p>Below is a list of YouTube sites that I tend to keep an eye on or have found high-quality content on in the past. As I’m a .NET/C# developer the channels are slanted in that direction but some are a bit more general.</p>

<h3 id="resources">Resources:</h3>
<ul>
  <li>James Montemagno: <a href="https://www.youtube.com/@JamesMontemagno" target="_blank">https://www.youtube.com/@JamesMontemagno</a></li>
  <li>Nick Cosentino: <a href="https://www.youtube.com/@DevLeader" target="_blank">https://www.youtube.com/@DevLeader</a></li>
  <li>Gerald Versluis: <a href="https://www.youtube.com/@jfversluis" target="_blank">https://www.youtube.com/@jfversluis</a></li>
  <li>Milan Jovanović: <a href="https://www.youtube.com/@MilanJovanovicTech" target="_blank">https://www.youtube.com/@MilanJovanovicTech</a></li>
  <li>JadeCodes: <a href="https://www.youtube.com/@Jade-Codes" target="_blank">https://www.youtube.com/@Jade-Codes</a></li>
  <li>Liam Ottley: <a href="https://www.youtube.com/@LiamOttley" target="_blank">https://www.youtube.com/@LiamOttley</a></li>
  <li>Nate Herk: <a href="https://www.youtube.com/@nateherk" target="_blank">https://www.youtube.com/@nateherk</a></li>
  <li>Coding Tutorials: <a href="https://www.youtube.com/@CodingTutorialsAreGo" target="_blank">https://www.youtube.com/@CodingTutorialsAreGo</a></li>
  <li>Code Wrinkles: <a href="https://www.youtube.com/@Codewrinkles" target="_blank">https://www.youtube.com/@Codewrinkles</a></li>
  <li>JonDJones: <a href="https://www.youtube.com/@jondjones" target="_blank">https://www.youtube.com/@jondjones</a></li>
  <li>Web Dev Simplified: <a href="https://www.youtube.com/@WebDevSimplified" target="_blank">https://www.youtube.com/@WebDevSimplified</a></li>
  <li>Android Developers: <a href="https://www.youtube.com/@AndroidDevelopers" target="_blank">https://www.youtube.com/@AndroidDevelopers</a></li>
</ul>]]></content><author><name></name></author><category term="pluralsight" /><category term="training" /><category term="short-post" /><summary type="html"><![CDATA[Back in early 2012 I watched an online course on Pluralsight - Data Layer Validation with Entity Framework 4.1+ by Julie Lerman. It was the first of a great many courses I’ve watched since then and I’ve learned a lot. But in recent years I’ve found that I’ve spent more time watching other high quality courses elsewhere. Previously there wasn’t anything like Pluralsight - sure, there was YouTube but the quality just wasn’t there. You could find some useful content but it fell short of the quality of the courses on Pluralsight.]]></summary></entry><entry><title type="html">Adding Clustering to MAUI Maps on Android</title><link href="https://onthefencedevelopment.com/2025/07/22/adding-clustering-to-maui-maps-on-android.html" rel="alternate" type="text/html" title="Adding Clustering to MAUI Maps on Android" /><published>2025-07-22T00:00:00+00:00</published><updated>2025-07-22T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2025/07/22/adding-clustering-to-maui-maps-on-android</id><content type="html" xml:base="https://onthefencedevelopment.com/2025/07/22/adding-clustering-to-maui-maps-on-android.html"><![CDATA[<p>Over the years I’ve developed a number of mobile applications that require the ability to display a number or locations on a map.</p>

<p>The challenge comes when there are a large number of locations to display because the map very quickly becomes overwhelmed with marker pins - but only really on Android because Apple Maps provides a degree of clustering out of the box.</p>

<p><a href="/assets/images/maui-default-maps-android-ios.png" target="_blank"><img src="/assets/images/maui-default-maps-android-ios.png" alt="image of android emulator and ios simulator running the sample application which displays a map of the UK with 500 randomly dropped pins. The landmass of the UK is almost totally obscured by the pins while ios is only displaying about 50 pins which massively reduces the on screen clutter." /></a></p>

<p>The above images show the difference between the standard Android and iOS maps with 500 markers randomly dropped onto them and as you can see, Android isn’t really cutting it as the UK landmass is pretty much totally obscured by the marker pins. While it could be argued that the iOS version is the lesser of the two implementations because there is clearly data missing, zooming in will cause more marker pins to be revealed.</p>

<p>In this post I’ll show you how to extend the functionality of the regular Android mapping implementation provided by the <code class="language-plaintext highlighter-rouge">Microsoft.Maui.Controls.Maps</code> package to include Marker Pin Clustering which, in my opinion, will improve the UX no end.
<!--more--></p>

<p>Now, before we start, I really hate reading blog posts which relate to a subject which is totally visual but don’t provide any indiction of the end goal. I follow the steps and then find that whatever the post is achieving doesn’t relate to what I want to achieve. So in an effort to save you from that, here is what we are going to be working towards.</p>

<p><a href="/assets/images/maui-clustering-map-android.png" target="_blank"><img src="/assets/images/maui-clustering-map-android.png" alt="image of any android emulator running the sample application which displays a map of the UK with 500 randomly dropped pins. Only five actual marker pins are displayed while the others are replaced with blue circular markers each with a number in the middle of them indicating how many marker pins they represent." width="220px&quot;; style=&quot;float:left; margin-right: 10px;" /></a>
<a href="/assets/images/maui-clustering-map-android-zoomed-out.png" target="_blank"><img src="/assets/images/maui-clustering-map-android-zoomed-out.png" alt="image of the same instance of the application from the previous image but this one has been zoomed out so only three clusters are displayed, one indicating that is contains over 200 markers pins while the others indicate that they contain over 100 each" width="220px&quot;; style=&quot;float:left; margin-right: 10px;" /></a></p>

<p>As you can see in the first screenshot, the map is much less cluttered with only five actual marker pins being displayed. The rest are replaced with circular markers, each containing a number that indicates how many marker pins they contain. The marker pins will drop out of the clusters as the user zooms in to the map and will be reabsorbed into the clusters as they zoom out as in the second screenshot.</p>

<p>What we need to do is extend the functionality of the Google Map being displayed on Android which would have been achieved using a Custom Renderer in the days of Xamarin. This is still possible in MAUI but the implementation has changed and they are now called Handlers.</p>

<p>Now this would be a pretty long post if I explained every line of code, so I’ve pushed the working solution to Github and there is a link at the bottom of the post so you can head over there and take a look yourself.</p>

<blockquote>
  <p>Note: You will need your own Google Maps API key to enable the Maps to load (see resources below)</p>
</blockquote>

<h2 id="creating-the-starting-point">Creating the Starting Point</h2>
<p>I created the sample application by following the steps in the ‘MAUI Maps’ article on Microsoft Learn (see resources at the bottom of this post) and edited the <code class="language-plaintext highlighter-rouge">MainPage.xaml</code> file so that it would display the map;</p>

<pre><code class="language-xaml">&lt;?xml version="1.0" encoding="utf-8" ?&gt;
&lt;ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="http://schemas.microsoft.com/dotnet/2021/maui/maps"
             x:Class="MauiMaps.MainPage"&gt;
    &lt;maps:Map x:Name="MyMap" /&gt;
&lt;/ContentPage&gt;
</code></pre>

<p>In the code behind file for the MainPage, <code class="language-plaintext highlighter-rouge">MainPage.xaml.cs</code>, I added a simple method to add 500 pins in random locations across the UK.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnAppearing</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">base</span><span class="p">.</span><span class="nf">OnAppearing</span><span class="p">();</span>
    
    <span class="n">MyMap</span><span class="p">.</span><span class="nf">MoveToRegion</span><span class="p">(</span><span class="n">MapSpan</span><span class="p">.</span><span class="nf">FromCenterAndRadius</span><span class="p">(</span><span class="k">new</span> <span class="nf">Location</span><span class="p">(</span><span class="m">54.5</span><span class="p">,</span> <span class="p">-</span><span class="m">3.0</span><span class="p">),</span> <span class="n">Distance</span><span class="p">.</span><span class="nf">FromKilometers</span><span class="p">(</span><span class="m">500</span><span class="p">)));</span>

    <span class="nf">AddRandomPinsToMap</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">void</span> <span class="nf">AddRandomPinsToMap</span><span class="p">()</span>
<span class="p">{</span>
    <span class="n">Random</span> <span class="n">rand</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Random</span><span class="p">();</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">var</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="m">500</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">latitude</span> <span class="p">=</span> <span class="n">rand</span><span class="p">.</span><span class="nf">NextDouble</span><span class="p">()</span> <span class="p">*</span> <span class="p">(</span><span class="m">58.7</span> <span class="p">-</span> <span class="m">49.9</span><span class="p">)</span> <span class="p">+</span> <span class="m">49.9</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">longitude</span> <span class="p">=</span> <span class="n">rand</span><span class="p">.</span><span class="nf">NextDouble</span><span class="p">()</span> <span class="p">*</span> <span class="p">(</span><span class="m">1.8</span> <span class="p">+</span> <span class="m">8.6</span><span class="p">)</span> <span class="p">-</span> <span class="m">8.6</span><span class="p">;</span>

        <span class="kt">var</span> <span class="n">pin</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Pin</span>
        <span class="p">{</span>
            <span class="n">Label</span> <span class="p">=</span> <span class="s">$"Location </span><span class="p">{</span><span class="n">i</span> <span class="p">+</span> <span class="m">1</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">Location</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Location</span><span class="p">(</span><span class="n">latitude</span><span class="p">,</span> <span class="n">longitude</span><span class="p">),</span>
            <span class="n">Type</span> <span class="p">=</span> <span class="n">PinType</span><span class="p">.</span><span class="n">Place</span>
        <span class="p">};</span>

        <span class="n">MyMap</span><span class="p">.</span><span class="n">Pins</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">pin</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I also updated the <code class="language-plaintext highlighter-rouge">.csproj</code> file so that only the Android application would be built,</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;TargetFrameworks&gt;</span>net9.0-android;<span class="nt">&lt;/TargetFrameworks&gt;</span>
</code></pre></div></div>

<p>The result of this is the first Android screenshot at the top of the post.</p>

<h2 id="creating-the-maui-handler">Creating the MAUI Handler</h2>
<p>The next step was to add the Handler, which is made up of a simple class that inherits from <code class="language-plaintext highlighter-rouge">Microsoft.Maui.Controls.Maps.Map</code> and another, partial, class that inherits from <code class="language-plaintext highlighter-rouge">Microsoft.Maui.Handlers.ViewHandler</code> - I’ve added these to the Controls and Handlers folders that I’ve created in the project.</p>

<p>I then needed to register the Handler within the <code class="language-plaintext highlighter-rouge">CreateMauiApp()</code> method of <code class="language-plaintext highlighter-rouge">MauiProgram.cs</code> as below;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="nf">ConfigureMauiHandlers</span><span class="p">(</span><span class="n">handlers</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">handlers</span><span class="p">.</span><span class="n">AddHandler</span><span class="p">&lt;</span><span class="n">ClusteringMap</span><span class="p">,</span> <span class="n">ClusteringMapHandler</span><span class="p">&gt;();</span>
<span class="p">});</span>
</code></pre></div></div>

<p>With the Handler classes registered I just needed to implement them, so I added a new file called <code class="language-plaintext highlighter-rouge">ClusteringMapHandler.Android.cs</code> within the <code class="language-plaintext highlighter-rouge">Platforms/Android</code> folder and rename the class itself to match the Handler class created above and made it <code class="language-plaintext highlighter-rouge">partial</code>.</p>

<blockquote>
  <p>So, to be clear - the file is called <code class="language-plaintext highlighter-rouge">ClusteringMapHandler.Android.cs</code> but the partial class it contains is just <code class="language-plaintext highlighter-rouge">ClusteringMapHandler</code> - matching the class in the Handlers folder.</p>
</blockquote>

<p>With all that in place I then updated the <code class="language-plaintext highlighter-rouge">MainPage.xaml</code> to use the new <code class="language-plaintext highlighter-rouge">ClustingMap</code></p>

<pre><code class="language-xaml">&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiMaps.Controls"
             x:Class="MauiMaps.MainPage"&gt;
    &lt;local:ClusteringMap x:Name="TheMap" /&gt;
&lt;/ContentPage&gt;
</code></pre>

<p>Running the application now displayed the map but didn’t display any of the pins or allow me to change the map type from the regular <code class="language-plaintext highlighter-rouge">Street</code> appearance to <code class="language-plaintext highlighter-rouge">Satellite</code>. After a bit of digging around I found that although I didn’t need to change any of the existing code, by introducing a Handler I now needed to map any of the properties I wanted to access from the standard Map to my new <code class="language-plaintext highlighter-rouge">ClusteringMap</code> - and that included the <code class="language-plaintext highlighter-rouge">Pins</code> and <code class="language-plaintext highlighter-rouge">MapType</code> properties. I also noticed that the map wasn’t centering over the middle of the UK anymore so I also had to deal with the <code class="language-plaintext highlighter-rouge">MoveToRegion</code> method.</p>

<p>This is where the shared <code class="language-plaintext highlighter-rouge">ClusteringMapHandler</code> comes in, i.e. now the one in the <code class="language-plaintext highlighter-rouge">Platforms</code> folder. In here we need to add a <code class="language-plaintext highlighter-rouge">PropertyMapper</code> which will be passed to the inherited <code class="language-plaintext highlighter-rouge">ViewHandler</code> and as many properties and/or methods as we want to interact with.</p>

<p>I’ve updated the file as below;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">ClusteringMapHandler</span><span class="p">()</span> <span class="p">:</span> <span class="n">ViewHandler</span><span class="p">&lt;</span><span class="n">ClusteringMap</span><span class="p">,</span> <span class="n">PlatformView</span><span class="p">&gt;(</span><span class="n">Mapper</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">IPropertyMapper</span><span class="p">&lt;</span><span class="n">ClusteringMap</span><span class="p">,</span> <span class="n">ClusteringMapHandler</span><span class="p">&gt;</span> <span class="n">Mapper</span> <span class="p">=</span>
        <span class="k">new</span> <span class="n">PropertyMapper</span><span class="p">&lt;</span><span class="n">ClusteringMap</span><span class="p">,</span> <span class="n">ClusteringMapHandler</span><span class="p">&gt;(</span><span class="n">ViewHandler</span><span class="p">.</span><span class="n">ViewMapper</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="c1">// Add property mappings here if needed</span>
            <span class="p">[</span><span class="k">nameof</span><span class="p">(</span><span class="n">ClusteringMap</span><span class="p">.</span><span class="n">Pins</span><span class="p">)]</span> <span class="p">=</span> <span class="n">MapPins</span><span class="p">,</span>
            <span class="p">[</span><span class="k">nameof</span><span class="p">(</span><span class="n">ClusteringMap</span><span class="p">.</span><span class="n">MapType</span><span class="p">)]</span> <span class="p">=</span> <span class="n">MapMapType</span><span class="p">,</span>
        <span class="p">};</span>

    <span class="k">private</span> <span class="k">static</span> <span class="k">partial</span> <span class="k">void</span> <span class="nf">MapPins</span><span class="p">(</span><span class="n">ClusteringMapHandler</span> <span class="n">handler</span><span class="p">,</span> <span class="n">ClusteringMap</span> <span class="n">customMap</span><span class="p">);</span>

    <span class="k">private</span> <span class="k">static</span> <span class="k">partial</span> <span class="k">void</span> <span class="nf">MapMapType</span><span class="p">(</span><span class="n">ClusteringMapHandler</span> <span class="n">handler</span><span class="p">,</span> <span class="n">ClusteringMap</span> <span class="n">customMap</span><span class="p">);</span>

    <span class="k">private</span> <span class="k">partial</span> <span class="k">void</span> <span class="nf">MapRegion</span><span class="p">(</span><span class="n">ClusteringMapHandler</span> <span class="n">handler</span><span class="p">,</span> <span class="n">ClusteringMap</span> <span class="n">map</span><span class="p">,</span> <span class="n">MapSpan</span> <span class="n">region</span><span class="p">);</span>
    
    <span class="k">public</span> <span class="k">void</span> <span class="nf">MoveToRegion</span><span class="p">(</span><span class="n">MapSpan</span> <span class="n">region</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">MapRegion</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">VirtualView</span><span class="p">,</span> <span class="n">region</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Next we need to head over to the Android specific partial class, i.e. <code class="language-plaintext highlighter-rouge">ClusteringMapHandler.Android.cs</code>, and code it out so that we can map the two sides of the Handler.</p>

<p>Now there’s quite a lot going on in this file, so I won’t go into detail of each line but the highlights are;</p>

<ul>
  <li>We need to override two methods from the <code class="language-plaintext highlighter-rouge">ViewHandler</code> that the shared handler class inherits, namely <code class="language-plaintext highlighter-rouge">CreatePlatformView</code> and <code class="language-plaintext highlighter-rouge">ConnectHandler</code> (there is a <code class="language-plaintext highlighter-rouge">DisconnectHandler</code> but we don’t need that right now).</li>
  <li>We also need to provide the implementation for the property mappings defined in the shared Handler.</li>
  <li>Finally, there is an internal class that implements <code class="language-plaintext highlighter-rouge">IOnMapReadyCallback</code> and overrides the <code class="language-plaintext highlighter-rouge">MapReadyCallback</code> method.</li>
</ul>

<p>When the <code class="language-plaintext highlighter-rouge">ClusteringMapHandler</code> is instantiated the overridden methods are called to create the <code class="language-plaintext highlighter-rouge">MapView</code>. In doing so the <code class="language-plaintext highlighter-rouge">OnMapReady</code> method on the <code class="language-plaintext highlighter-rouge">MapReadyCallback</code> is invoked to hook up the mappings to the properties we set up and handle any calls to <code class="language-plaintext highlighter-rouge">MoveToRegion</code> that may have been called before the Map was fully instantiated.</p>

<p>The property mappings are fairly straightforward and involve calling the same methods with the same parameters as you would with the regular Map object.</p>

<p>With all this in place we are now practically back where we started, with a map that will display 500 randomly dropped pins that will almost obscure the map - but, with the MAUI abstraction out of the way we now have the ability to access the underlying map.</p>

<h2 id="finally-implementing-marker-pin-clustering">(Finally) Implementing Marker Pin Clustering</h2>
<p>With the Handler in place and running we can now implement clustering and the good news is that now we have access to the Google Map that sits on the other side of the MAUI Mapping abstraction we can install a nuget package to do all the heaving lifting for us.</p>

<p>The nuget package we are looking for is <code class="language-plaintext highlighter-rouge">GoogleMapsUtils.Android.Maui</code> which at the time of writing is at version 1.0.2 and works fine for MAUI 9.0 as far as I can tell. The package details states that it supports .NET 7.0 and Android API v33 but if you look at the package on Github (see resources below) and look at the <code class="language-plaintext highlighter-rouge">.csproj</code> of the Android project you’ll see that it is supporting Android 21 so backward compatibility shouldn’t be a problem …. buy you’ll be double-checking that right.</p>

<p>If you are planning on an iOS version of your app you will need to tweak the <code class="language-plaintext highlighter-rouge">.csproj</code> file a little so that the Android specific package isn’t included in the iOS build. To ensure the package is only built for Android targets just moved the <code class="language-plaintext highlighter-rouge">PackageReference</code> into a new <code class="language-plaintext highlighter-rouge">ItemGroup</code> and apply a <code class="language-plaintext highlighter-rouge">Condition</code> as below;</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup</span> <span class="na">Condition=</span><span class="s">"$(TargetFramework.StartsWith('net9.0-android'))"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"GoogleMapsUtils.Android.Maui"</span> <span class="na">Version=</span><span class="s">"1.0.2"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<p>Next we need to create a simple DTO class that will be used by the Cluster Manager that is provided by the Google Map Utils package we just installed. This needs to be created somewhere within the <code class="language-plaintext highlighter-rouge">Platforms/Android</code> folder, I’ve just added it to the Handlers folder so it sits alongside the <code class="language-plaintext highlighter-rouge">ClusteringMapHandler.Android.cs</code> file.</p>

<p>The file is pretty simple, although you could include additional properties should you need them but we’ll just implement the <code class="language-plaintext highlighter-rouge">IClusterItem</code> interface for now;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Android.Gms.Maps.Model</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Com.Google.Maps.Android.Clustering</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">MauiMaps.Handlers</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ClusterMarker</span> <span class="p">:</span> <span class="n">Java</span><span class="p">.</span><span class="n">Lang</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span> <span class="n">IClusterItem</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">LatLng</span> <span class="n">Position</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">Snippet</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">Title</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now all we need to do is update the <code class="language-plaintext highlighter-rouge">ClusteringMapHandler.Android.cs</code> file to implement clustering and the good news is that we’re only talking about a handful of lines of code.</p>

<p>I’ve added a local variable to hold the ClusterManager to the top of the file;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="n">ClusterManager</span><span class="p">?</span> <span class="n">_clusterManager</span><span class="p">;</span>
</code></pre></div></div>

<p>Within the <code class="language-plaintext highlighter-rouge">MapPins</code> method I’ve replaced the <code class="language-plaintext highlighter-rouge">MarkerOptions</code> with our new <code class="language-plaintext highlighter-rouge">ClusterMarker</code> above. I’m also storing the markers in a temporary list which is then passed into the ClusterManager. The updated method looks like this;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">static</span> <span class="k">partial</span> <span class="k">void</span> <span class="nf">MapPins</span><span class="p">(</span><span class="n">ClusteringMapHandler</span> <span class="n">handler</span><span class="p">,</span> <span class="n">ClusteringMap</span> <span class="n">clusteringMap</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">googleMap</span> <span class="p">=</span> <span class="n">handler</span><span class="p">.</span><span class="n">_googleMap</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">googleMap</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    
    <span class="n">googleMap</span><span class="p">.</span><span class="nf">Clear</span><span class="p">();</span>
    
    <span class="kt">var</span> <span class="n">markers</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">ClusterMarker</span><span class="p">&gt;();</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">pin</span> <span class="k">in</span> <span class="n">clusteringMap</span><span class="p">.</span><span class="n">Pins</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">marker</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ClusterMarker</span>
        <span class="p">{</span>
            <span class="n">Position</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">LatLng</span><span class="p">(</span><span class="n">pin</span><span class="p">.</span><span class="n">Location</span><span class="p">.</span><span class="n">Latitude</span><span class="p">,</span> <span class="n">pin</span><span class="p">.</span><span class="n">Location</span><span class="p">.</span><span class="n">Longitude</span><span class="p">),</span>
            <span class="n">Title</span> <span class="p">=</span> <span class="n">pin</span><span class="p">.</span><span class="n">Label</span><span class="p">,</span>
            <span class="n">Snippet</span> <span class="p">=</span> <span class="n">pin</span><span class="p">.</span><span class="n">Address</span>
        <span class="p">};</span>
        
        <span class="n">markers</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">marker</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">handler</span><span class="p">.</span><span class="n">_clusterManager</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">handler</span><span class="p">.</span><span class="n">_clusterManager</span><span class="p">.</span><span class="nf">ClearItems</span><span class="p">();</span>
        <span class="n">handler</span><span class="p">.</span><span class="n">_clusterManager</span><span class="p">.</span><span class="nf">AddItems</span><span class="p">(</span><span class="n">markers</span><span class="p">);</span>
        <span class="n">handler</span><span class="p">.</span><span class="n">_clusterManager</span><span class="p">.</span><span class="nf">Cluster</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Finally, we just need to update the <code class="language-plaintext highlighter-rouge">MapReadyCallback</code> so that the <code class="language-plaintext highlighter-rouge">ClusterManager</code> is instantiated correctly and that the Map uses it when the map is panned and zoomed.</p>

<p>Just add the following lines in the <code class="language-plaintext highlighter-rouge">OnMapReady</code> method after the <code class="language-plaintext highlighter-rouge">handler._googleMap</code> property is set;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">handler</span><span class="p">.</span><span class="n">_clusterManager</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ClusterManager</span><span class="p">(</span><span class="n">handler</span><span class="p">.</span><span class="n">Context</span><span class="p">,</span> <span class="n">handler</span><span class="p">.</span><span class="n">_googleMap</span><span class="p">);</span>
<span class="n">handler</span><span class="p">.</span><span class="n">_googleMap</span><span class="p">.</span><span class="nf">SetOnCameraIdleListener</span><span class="p">(</span><span class="n">handler</span><span class="p">.</span><span class="n">_clusterManager</span><span class="p">);</span>
</code></pre></div></div>

<p>That’s it - you have now implemented Clustering on the Android Maps using MAUI.</p>

<p>Simple huh 😁</p>

<p>I do have some follow up posts planned with iOS Maps in my sights - watch this space.</p>

<h3 id="resources">Resources:</h3>
<ul>
  <li>Sample Application on Github: <a href="https://github.com/OnTheFenceDevelopment/MauiMaps.git" target="_blank">https://github.com/OnTheFenceDevelopment/MauiMaps.git</a></li>
  <li>MAUI Maps on Microsoft Learn: <a href="https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/map?view=net-maui-9.0" target="_blank">https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/map?view=net-maui-9.0</a></li>
  <li>Creating Google Maps API Keys: <a href="https://developers.google.com/maps/documentation/android-sdk/get-api-key" target="_blank">https://developers.google.com/maps/documentation/android-sdk/get-api-key</a></li>
  <li>GoogleMapsUtils.Android.Maui on Github:  <a href="https://github.com/tranhoangnam16705/GoogleMapsUtils" target="_blank">https://github.com/tranhoangnam16705/GoogleMapsUtils</a></li>
</ul>]]></content><author><name></name></author><category term="maui" /><category term="android" /><category term="mobile-development" /><category term="mapping" /><summary type="html"><![CDATA[Over the years I’ve developed a number of mobile applications that require the ability to display a number or locations on a map. The challenge comes when there are a large number of locations to display because the map very quickly becomes overwhelmed with marker pins - but only really on Android because Apple Maps provides a degree of clustering out of the box. The above images show the difference between the standard Android and iOS maps with 500 markers randomly dropped onto them and as you can see, Android isn’t really cutting it as the UK landmass is pretty much totally obscured by the marker pins. While it could be argued that the iOS version is the lesser of the two implementations because there is clearly data missing, zooming in will cause more marker pins to be revealed. In this post I’ll show you how to extend the functionality of the regular Android mapping implementation provided by the Microsoft.Maui.Controls.Maps package to include Marker Pin Clustering which, in my opinion, will improve the UX no end.]]></summary></entry><entry><title type="html">Developing a Jekyll Website with Docker</title><link href="https://onthefencedevelopment.com/2025/06/17/developing-a-jekyll-site-with-docker.html" rel="alternate" type="text/html" title="Developing a Jekyll Website with Docker" /><published>2025-06-17T00:00:00+00:00</published><updated>2025-06-17T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2025/06/17/developing-a-jekyll-site-with-docker</id><content type="html" xml:base="https://onthefencedevelopment.com/2025/06/17/developing-a-jekyll-site-with-docker.html"><![CDATA[<p><img src="/assets/images/docker_logo.png" alt="docker logo" width="220px" style="float:left; margin-right: 10px;" /></p>

<p>Yesterday I spun up Jetbrains Rider, loaded up the Jekyll folder for this very website and prepared to get a blog post out of my head and online … but this is not that blog post!</p>

<p>No, that post had to go on hold while I battled with error after error as I tried to get the site built and the Jekyll service up and running. Before I knew it, the evening was drawing to a close and I still didn’t have the site running locally.</p>

<p>The problem was clear though - dependency issues with Ruby and its Bundler that are core to Jekyll’s operation. Something had updated on my MacBook Pro and it had really upset the apple cart.</p>

<p><a href="/assets/images/jekyll-bundler-dependency-error-rider.png" target="_blank"><img src="/assets/images/jekyll-bundler-dependency-error-rider.png" alt="error in Rider terminal when trying to start jekyll server showing what appears to be a dependency error" /></a></p>

<p>So how was I going to get around that?
<!--more--></p>

<p>Well, the clue is in the title really - Docker to the rescue (<a href="/2023/02/25/rationalising-my-sql-server-installs-with-docker.html">again</a>).</p>

<p>If the issue was down to an update being installed on the Mac then I was clearly not in control of the development environment so even if I managed to get it working this time …. how long would it be before it happened again and burnt another evening?</p>

<p>What I needed was a stable environment, one that I control (as far as possible) and have some confidence that I could replicate it should I need to fallback to a Windows system.</p>

<p>That’s what Docker provides - the ability to create systems that run only the required versions of the required components and nothing else. Don’t need a database? Then don’t install one. Don’t need a webserver? Then don’t install one … you get the idea.</p>

<p>For my Jekyll solution all I needed to do was create two simple text files with a handful of commands and I was off to the races!</p>

<p>Now, this isn’t a post about the ins and outs of Docker, there are plenty of posts online written by people with far more knowledge than I have on the subject.</p>

<p>That said, at a high level there are two primary components to a Docker based solution; an Image and a Container based on that Image. The two files I mention above relate to each of those components.</p>

<h2 id="the-dockerfile">The Dockerfile</h2>
<p>The Dockerfile is where we define what we need included in the Image and as Jekyll is built on top of Ruby that seems to be a good foundation for our image, so create a new text file called Dockerfile in the root Jekyll folder, i.e. the same folder as the <code class="language-plaintext highlighter-rouge">_config.yml</code> file, and add the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FROM ruby:3.1

RUN apt-get update <span class="o">&amp;&amp;</span> apt-get <span class="nb">install</span> <span class="nt">-y</span> build-essential nodejs npm

WORKDIR /srv/jekyll

RUN gem <span class="nb">install </span>bundler jekyll

EXPOSE 4000

</code></pre></div></div>

<p>From top to bottom the file specifies that the image will be based on version 3.1 of the Ruby image. We then install some required packages, <code class="language-plaintext highlighter-rouge">build-essential</code>, <code class="language-plaintext highlighter-rouge">nodejs</code> and <code class="language-plaintext highlighter-rouge">npm</code>.
Next we set the Working Directory, run the <code class="language-plaintext highlighter-rouge">gem</code> comment to install <code class="language-plaintext highlighter-rouge">jekyll</code> and the <code class="language-plaintext highlighter-rouge">bundler</code> and finally specify the port that <code class="language-plaintext highlighter-rouge">jekyll</code> will be served on.</p>

<h1 id="the-docker-composeyml-file">The docker-compose.yml file</h1>
<p>This is where we configure the container and by placing it in the same root folder as the Dockerfile we can simplify things a little bit, e.g. we can now use a simple <code class="language-plaintext highlighter-rouge">.</code> for the build property which will tell Docker to use the Dockerfile in this current directory (the one we created above).
We can use a similar shortcut to define the same folder as the one to bind the volume to.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>services:
  jekyll:
    container_name: jekyll-dev
    build: <span class="nb">.</span>
    volumes:
      - .:/srv/jekyll
    ports:
      - <span class="s2">"4000:4000"</span>
    working_dir: /srv/jekyll
    <span class="nb">command</span>: sh <span class="nt">-c</span> <span class="s2">"bundle install &amp;&amp; bundle exec jekyll serve --host 0.0.0.0 --watch"</span>
    
</code></pre></div></div>

<p>The host to container name and port mapping is also defined along with the working directory and that’s about it - well, all we need anyway. Finally we define the command to run on the container and this will build the website, start the Jekyll server and watch for changes within the working directory (triggering a rebuild as and when they are detected).</p>

<blockquote>
  <p>I haven’t managed to get the <code class="language-plaintext highlighter-rouge">livereload</code> option to work yet but will investigate this further as it’s a useful thing to have. For now the <code class="language-plaintext highlighter-rouge">watch</code> option will suffice for me.</p>
</blockquote>

<p>Now all we need to do is start everything up and all we need to do for that is open a terminal (or command window), navigate to the folder containing these two files and enter the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up <span class="nt">-d</span>
</code></pre></div></div>

<p>This will load and execute the commands in the document-compose.yml file which will in turn load the Dockerfile and build the Docker image if required. The <code class="language-plaintext highlighter-rouge">-d</code> will detach the terminal from the process we just started in order to run the container. This just means that you could close the terminal or continue using it without affecting the running process.</p>

<p>It might take a few seconds, or even minutes depending on the size of the site being built, but we will be able to open a browser and navigate to http://localhost:4000 which should bring up the website.</p>

<p><a href="/assets/images/jekyll-docker-website.png" target="_blank"><img src="/assets/images/jekyll-docker-website.png" alt="the home page of this website running on docker and showing this post at the top of the list" /></a></p>

<p>As I’m on a Mac I’m using Jetbrains Rider and it auto-saves as I go so even as I’m writing this post my new Docker container will be periodically rebuilding the site. When I’m done all I need to do is refresh my browser (and maybe wait for the last rebuild and refresh again) to see the post in all it’s glory.</p>

<p>When I’ve finished what I’m doing I just need to run the following command in the terminal and the Docker container will be spun down and the Image deleted - we don’t need it anymore.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose down
</code></pre></div></div>

<p>The good thing about this is that with these two text files committed to source control I can pull everything down to a different computer, and as long as it’s running Docker I can run the Docker Compose command to build the image and spin up a container without having to worry about any dependency issues.</p>

<p>Well, that’s the hope anyway.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="docker" /><summary type="html"><![CDATA[Yesterday I spun up Jetbrains Rider, loaded up the Jekyll folder for this very website and prepared to get a blog post out of my head and online … but this is not that blog post! No, that post had to go on hold while I battled with error after error as I tried to get the site built and the Jekyll service up and running. Before I knew it, the evening was drawing to a close and I still didn’t have the site running locally. The problem was clear though - dependency issues with Ruby and its Bundler that are core to Jekyll’s operation. Something had updated on my MacBook Pro and it had really upset the apple cart. So how was I going to get around that?]]></summary></entry><entry><title type="html">Save Blushes with Git Hooks</title><link href="https://onthefencedevelopment.com/2025/04/24/save-blushes-with-git-hooks.html" rel="alternate" type="text/html" title="Save Blushes with Git Hooks" /><published>2025-04-24T00:00:00+00:00</published><updated>2025-04-24T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2025/04/24/save-blushes-with-git-hooks</id><content type="html" xml:base="https://onthefencedevelopment.com/2025/04/24/save-blushes-with-git-hooks.html"><![CDATA[<p>We’ve all done it - we’ve committed a file, pushed it to the repository and it’s broken the build …. I know, shock, horror huh!?</p>

<p>Now I’m not suggesting that experienced developers are committing code that doesn’t even build - I mean, we all check that much right ….. right? But do we always remember to run the unit tests?</p>

<p>No, we don’t, and what happens next - we have to run the tests locally, find the issue, fix it and then go through the cycle again. While you make be able to squash these ‘fixing build’ commits out of the history but that build failure will always be there (unless you’re the DevOps admin of course).</p>

<p>But what if there was a way that you could avoid having to remember to do these simple, but easily forgotten, tasks. Well, that’s where git hooks come in.
<!--more--></p>

<p>The git hooks I’m going to describe in this post are just simple scripts that can be executed before or after certain git operations, e.g. pre/post commit, pre/post push. Other hooks exist but lets keep the scope a little tight for now.</p>

<p>I’ve only really used the <code class="language-plaintext highlighter-rouge">pre</code> hooks before because I’m trying to catch issues before it’s too late - I’ve not needed to do anything after a commit or push has been successfully completed.</p>

<h3 id="pre-commit">Pre-Commit</h3>
<p>Before I commit I want to make sure that te code still builds so I’d like to perform a Clean and Build - afterall, what’s the point of committing something that doesn’t work 🤷‍♂️?</p>

<p>The first thing to do is to create a file called <code class="language-plaintext highlighter-rouge">pre-commit</code> (with no file extension) within the hidden <code class="language-plaintext highlighter-rouge">.git</code> folder inside your solution folder and within the <code class="language-plaintext highlighter-rouge">hooks</code> sub-folder, .i.e <code class="language-plaintext highlighter-rouge">.git\hooks</code>.</p>

<p>The file should have the following content:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
dotnet clean<span class="p">;</span> dotnet build

<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-ne</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"Build failed - Please fix errors and commit again"</span>
    <span class="nb">exit</span> <span class="nv">$?</span>
<span class="k">fi

</span><span class="nb">echo</span> <span class="s2">"Build Succeeded"</span>
<span class="nb">exit </span>0
</code></pre></div></div>

<blockquote>
  <p>If you are working on a non-Windows system you will need to ensure the file is executable. This can be achieved via the terminal using <code class="language-plaintext highlighter-rouge">chmod +x pre-commit</code> but other methods exist.</p>
</blockquote>

<p>If I make a change to a file that results in the build failing, e.g. removing a semi-colon from line 5 below, and then attempt to commit the change I will get an error.</p>

<p><a href="/assets/images/pre-commit-failing-build.png" target="_blank"><img src="/assets/images/pre-commit-failing-build.png" alt="Rider IDE with a Program.cs file containing an error and console error messages confirming that the commit has failed" /></a></p>

<p>Now you may be thinking “his code may well build but he’s still forgetting to run his unit tests” - well, no I haven’t - I just don’t want to do it every time I commit my changes. I think it’s more useful to run my unit tests when I’m pushing my changes to the remote repository and that’s what we’ll look at next.</p>

<h3 id="pre-push">Pre-Push</h3>
<p>It shouldn’t surprise you that adding a script to run before a push pretty much follow the same process as above, just within a file called <code class="language-plaintext highlighter-rouge">pre-push</code>, saved in the same location as <code class="language-plaintext highlighter-rouge">pre-commit</code> (flagged as being executable if required).</p>

<p>My <code class="language-plaintext highlighter-rouge">pre-push</code> file looks like this;</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
dotnet <span class="nb">test</span><span class="p">;</span>

<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-ne</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">echo</span> <span class="s2">"One or more tests have failed - Please fix errors and commit again"</span>
    <span class="nb">exit</span> <span class="nv">$?</span>
<span class="k">fi

</span><span class="nb">echo</span> <span class="s2">"Tests Succeeded"</span>
<span class="nb">exit </span>0
</code></pre></div></div>
<p>In the screenshot below you’ll see a standard <code class="language-plaintext highlighter-rouge">UnitTest1.cs</code> file that Rider created for me along with a default test - although I’ve configured this to fail using <code class="language-plaintext highlighter-rouge">Assert.Fail()</code>.</p>

<p>I’ve committed this file without any problems as the code still builds just fine and now I just need to push it to the <code class="language-plaintext highlighter-rouge">origin</code> - which will trigger the unit tests to be executed.</p>

<p><a href="/assets/images/pre-push-tests-failing.png" target="_blank"><img src="/assets/images/pre-push-tests-failing.png" alt="Rider IDE with a Unit Test file configured to fail" /></a></p>

<p>BOOM! The push has failed due to the failing test.</p>

<p><a href="/assets/images/pre-push-tests-failing-console.png" target="_blank"><img src="/assets/images/pre-push-tests-failing-console.png" alt="Rider IDE console showing the output of the test run confirming that one has failed" /></a></p>

<p>All I need to do is change the <code class="language-plaintext highlighter-rouge">Assert.Fail()</code> to <code class="language-plaintext highlighter-rouge">Assert.Pass()</code>, commit the change is push it.</p>

<p><a href="/assets/images/pre-push-tests-fixed-console.png" target="_blank"><img src="/assets/images/pre-push-tests-fixed-console.png" alt="Rider IDE git window showing commit history with a toast message confirming the successful push" /></a></p>

<h3 id="conclusion">Conclusion</h3>
<p>With all the buzz these days being around AI assistants I think we sometimes forget the tools that have been with us all along. Git Hooks are a fundamental feature of git and easily overlooked. 
With just a little configuration we have enabled a mechanism to ensure we cannot commit code that doesn’t build or push tests that do not pass - saving our blushes by minimising the possibility of breaking the build.</p>]]></content><author><name></name></author><category term="git" /><category term="source-control" /><category term="productivity" /><summary type="html"><![CDATA[We’ve all done it - we’ve committed a file, pushed it to the repository and it’s broken the build …. I know, shock, horror huh!? Now I’m not suggesting that experienced developers are committing code that doesn’t even build - I mean, we all check that much right ….. right? But do we always remember to run the unit tests? No, we don’t, and what happens next - we have to run the tests locally, find the issue, fix it and then go through the cycle again. While you make be able to squash these ‘fixing build’ commits out of the history but that build failure will always be there (unless you’re the DevOps admin of course). But what if there was a way that you could avoid having to remember to do these simple, but easily forgotten, tasks. Well, that’s where git hooks come in.]]></summary></entry><entry><title type="html">Spurious Sockets Exception reported by Sentry</title><link href="https://onthefencedevelopment.com/2025/03/11/spurious-sockets-exception-sentry.html" rel="alternate" type="text/html" title="Spurious Sockets Exception reported by Sentry" /><published>2025-03-11T00:00:00+00:00</published><updated>2025-03-11T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2025/03/11/spurious-sockets-exception-sentry</id><content type="html" xml:base="https://onthefencedevelopment.com/2025/03/11/spurious-sockets-exception-sentry.html"><![CDATA[<p>While finishing off the migration of my <a href="https://onthefencedevelopment.com/smite-scoreboard-for-android-ios.html">Smite Scoreboard</a> app to MAUI and having completed the bulk of the work I turned my attention to replacing <a href="https://onthefencedevelopment.com/smite-scoreboard-for-android-ios.html" target="_blank">the soon-to-be retired</a> <a href="https://onthefencedevelopment.com/smite-scoreboard-for-android-ios.html" target="_blank">App Center</a> for a suitable alternative capable of capturing exceptions and providing enough information to investigate them effectively.</p>

<p>After reviewing a number of alternatives I opted for <a href="https://www.sentry.io" target="_blank">Sentry</a>, mainly because of the presence of a decent free tier that offered the basic features I require with decent data retention policy.</p>

<p>After configuring a project on my shiny new Sentry account, adding the required initialisation code to the MAUI project and replacing all the AppCenter references it was time to try it out …. and that’s where the trouble started.
<!--more--></p>

<p>Temporary code was added to temporarily trigger exceptions to verify everything was wired up correctly and was impressed by the details that Sentry had collected - even without any debug symbol files being uploaded.</p>

<p>However, something quite unexpected was also being reported - an exception I’ve not seen before and while it didn’t prevent the app from starting and running normally it wasn’t something I could push to production until I’d fully investigated and resolved the issue.</p>

<p><img src="/assets/images/sentry-sockets-exception.png" alt="Sentry Issue Report from Smite Scoreboard for System.Net.Sockets.SocketException" /></p>

<p>Looking at the stack trace this is an <code class="language-plaintext highlighter-rouge">AggregateException</code> which is normally associated with asynchronous code and this was confirmed by the exception message;</p>

<blockquote>
  <p>A Task’s exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread.</p>
</blockquote>

<p>Unfortunately there wasn’t enough information in the issue details for me to identify the offending code so it was not possible to simply add a breakpoint and drop into the regular debugging loop that we all know so well.</p>

<p>This meant that I needed to adjust my approach, dropping breakpoints on general Exception Handlers Sentry had replaced App Center and into the Global Exception Handler I’d implemented to capture any unhandled exceptions that may occur but none of these were hit.</p>

<p>During this process I determined that the exception was being thrown during the app startup, before the UI came to rest on the app landing page - this would at least limit the code I needed to review.</p>

<p>It took a little while to work out when the exception was being thrown but I’m still not 100% sure why it is happening, but appears to happen when the <code class="language-plaintext highlighter-rouge">App.MainPage</code> is being set to a new instance of <code class="language-plaintext highlighter-rouge">AppShell</code> within the <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> class.</p>

<h3 id="a-breakthrough---of-sorts">A breakthrough - of sorts!</h3>
<p>This led me to a moment of clarity …..</p>

<blockquote>
  <p>This isn’t my code causing this issue - it’s something else!</p>
</blockquote>

<p>While this was encouraging it wasn’t enough for me to draw a line under the issue and move on - no, that’s not how I roll, I need to be sure. But how was I going to get the certainty?</p>

<p>Well it turned out to be pretty easy really - I just created a brand new, vanilla solution using <code class="language-plaintext highlighter-rouge">File &gt; New Project &gt; MAUI</code>, imported the Sentry package via Nuget and wired everything up.</p>

<p>I started up the new app and boom …. there it was, the same exception as before with only the most minimal of code changes.</p>

<p><img src="/assets/images/sentry-sockets-exception-new-solution.png" alt="Sentry Issue Report from new Project for System.Net.Sockets.SocketException" /></p>

<p>Now I was certain that the issue was not of my making (not directly anyway) as I could replicate it using the built-in solution templates ….. but what was causing it?</p>

<p>Well I’m sorry to say …. I don’t know. I have burnt quite a few hours getting to this point when I could have been getting the app pushed to the respective stores instead of chasing this issue around.</p>

<p>This is what I can tell you though:</p>
<ul>
  <li>I use a Mac - not a Windows based system</li>
  <li>I develop using JetBrains Rider - not Visual Studio Code</li>
  <li>The issue only affects physical Android and iOS devices and NOT emulators/simulators</li>
  <li>While the exception type was always the same the message did differ slightly between Android and iOS (see below)</li>
  <li>The issue only manifests when I start the app from Rider in Debug mode, i.e. starting the app from Rider is Release mode or by tapping on the launch icon on the device itself does not seem to trigger the exception</li>
</ul>

<h3 id="solution---of-sorts">Solution - of sorts!</h3>
<p>I’m not normally one for suppressing errors but I really don’t think this will be an issue for this particular app which is pretty simple, having no local database and requiring no internet access - so that’s what I’m going to do and fortunately the Sentry implementation provides a degree of extensibility that will facilitate this.</p>

<p>First I create a new class called <code class="language-plaintext highlighter-rouge">SentryExceptionTypeFilter</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Sentry.Extensibility</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">SmiteScoreboard</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">SentryExceptionTypeFilter</span><span class="p">&lt;</span><span class="n">TException</span><span class="p">&gt;</span> <span class="p">:</span> <span class="n">IExceptionFilter</span> <span class="k">where</span> <span class="n">TException</span> <span class="p">:</span> <span class="n">Exception</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">Type</span> <span class="n">_filteredType</span> <span class="p">=</span> <span class="k">typeof</span><span class="p">(</span><span class="n">TException</span><span class="p">);</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="nf">Filter</span><span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">_filteredType</span><span class="p">.</span><span class="nf">IsInstanceOfType</span><span class="p">(</span><span class="n">ex</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This was then used as part of the <code class="language-plaintext highlighter-rouge">UseSentry</code> initialisation within the <code class="language-plaintext highlighter-rouge">CreateMauiApp</code> in the <code class="language-plaintext highlighter-rouge">MauiProgram</code> class</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.Extensions.Logging</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">SmiteTesting</span><span class="p">;</span>

<span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">MauiProgram</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">MauiApp</span> <span class="nf">CreateMauiApp</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">MauiApp</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">();</span>
        <span class="n">builder</span>
            <span class="p">.</span><span class="n">UseMauiApp</span><span class="p">&lt;</span><span class="n">App</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">UseSentry</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span>
            <span class="p">{</span>
                <span class="n">options</span><span class="p">.</span><span class="n">Dsn</span> <span class="p">=</span> <span class="s">"&lt;DATA SOURCE NAME FROM SENTRY PORTAL&gt;"</span><span class="p">;</span>
<span class="cp">#if DEBUG
</span>                <span class="n">options</span><span class="p">.</span><span class="nf">AddExceptionFilter</span><span class="p">(</span><span class="k">new</span> <span class="n">SentryExceptionTypeFilter</span><span class="p">&lt;</span><span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Sockets</span><span class="p">.</span><span class="n">SocketException</span><span class="p">&gt;());</span>
<span class="cp">#endif
</span>            <span class="p">})</span>
            <span class="p">.</span><span class="nf">ConfigureFonts</span><span class="p">(</span><span class="n">fonts</span> <span class="p">=&gt;</span>
            <span class="p">{</span>
                <span class="n">fonts</span><span class="p">.</span><span class="nf">AddFont</span><span class="p">(</span><span class="s">"OpenSans-Regular.ttf"</span><span class="p">,</span> <span class="s">"OpenSansRegular"</span><span class="p">);</span>
                <span class="n">fonts</span><span class="p">.</span><span class="nf">AddFont</span><span class="p">(</span><span class="s">"OpenSans-Semibold.ttf"</span><span class="p">,</span> <span class="s">"OpenSansSemibold"</span><span class="p">);</span>
            <span class="p">});</span>

        <span class="k">return</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With these in place I was able to run the application in Debug mode with no exception being reported to Sentry. The preprocessor directive will also ensure that this only affects Debug builds so Release builds sent to Production would not have filtering applied and that seems to be the best I can do right now.</p>

<h3 id="conclusion">Conclusion</h3>
<p>All I can conclude from this is that the exception is due to some incompatibility between MAUI and Rider running on a Mac when building in Debug mode and targeting physical Android and iOS devices.</p>

<p>Not having a suitable Windows system to hand right now I can’t really check it out there but as/when/if I do I’ll be sure to continue with my investigations. For now if you are seeing any of the following exceptions turning up in your Sentry issues while debugging using Rider on a Mac, don’t despair - it’s quite probably not your fault.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">System.Net.Sockets.SocketException</code> - with message of <code class="language-plaintext highlighter-rouge">Operation Canceled</code> on Android (not a typo btw, just US spelling)</li>
  <li><code class="language-plaintext highlighter-rouge">System.Net.Sockets.SocketException</code> - with message of <code class="language-plaintext highlighter-rouge">Connection Refused</code> on iOS</li>
  <li><code class="language-plaintext highlighter-rouge">System.Net.Sockets.SocketException</code> - with message of <code class="language-plaintext highlighter-rouge">No route to host</code> on iOS</li>
</ul>

<p>With all that out of the way - v3 of Smite Scoreboard will hit the stores in a few days, once I’ve finalised testing and sorted my Apple certificates &amp; provisioning profiles.</p>]]></content><author><name></name></author><category term="development" /><category term="maui" /><category term="mobile-development" /><category term="sentry" /><summary type="html"><![CDATA[While finishing off the migration of my Smite Scoreboard app to MAUI and having completed the bulk of the work I turned my attention to replacing the soon-to-be retired App Center for a suitable alternative capable of capturing exceptions and providing enough information to investigate them effectively. After reviewing a number of alternatives I opted for Sentry, mainly because of the presence of a decent free tier that offered the basic features I require with decent data retention policy. After configuring a project on my shiny new Sentry account, adding the required initialisation code to the MAUI project and replacing all the AppCenter references it was time to try it out …. and that’s where the trouble started.]]></summary></entry><entry><title type="html">Deploying and Debugging MAUI Apps to Older iOS Devices with Latest Xcode</title><link href="https://onthefencedevelopment.com/2024/11/22/deploying-maui-apps-to-older-ios-devices-with-latest-xcode.html" rel="alternate" type="text/html" title="Deploying and Debugging MAUI Apps to Older iOS Devices with Latest Xcode" /><published>2024-11-22T00:00:00+00:00</published><updated>2024-11-22T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2024/11/22/deploying-maui-apps-to-older-ios-devices-with-latest-xcode</id><content type="html" xml:base="https://onthefencedevelopment.com/2024/11/22/deploying-maui-apps-to-older-ios-devices-with-latest-xcode.html"><![CDATA[<p>While migrating a clients application from Xamarin.Forms to MAUI there was a requirement to test it on older iOS devices such as an iPhone 6s. Now that’s not really a big issue, I happened to have one in my ‘test phone’ collection that hadn’t been recycled or handed out to a family member who had dropped their own phone in the toilet.</p>

<p>Getting the phone charged and booted was the easy part - getting it connected to my MacBook Pro so that I could deploy and debug the application to it required a little bit more effort, but not too much as it turned out.
<!--more--></p>

<p>If you’ve read any of my recent posts you will know that I’m an avid user of the Jetbrains Rider IDE and while it has it’s oddities it serves me well. So when I connected the iPhone 6s to the Mac I was surprised to find that it didn’t appear in the list of available devices. I’ve had no issues with newer iPhone/iPads so was pretty sure this was not a Rider issue.</p>

<p><img src="/assets/images/rider-missing-ios-device.png" alt="Jetbrains Rider iOS Device List" /></p>

<p>I tried rebooting the device, rebooting the Mac, checking for Rider updates and even different cables but nothing worked. With everything else checked, my attention turned towards Xcode which I’d recently updated to v16.1 so that I could build a previous MAUI migration using the very latest iOS SDK to comply with Apple Store requirements.</p>

<p>Taking this into account, i.e. the need to update Xcode to support newer versions of iOS, it made sense that in addition to dropping support for older versions, Apple would also be dropping support for using older physical devices within Xcode.</p>

<p>This was pretty much confirmed when I opened Xcode and navigated to ‘Window &gt; Devices and Simulators’ with my device connected.</p>

<p><img src="/assets/images/xcode-device-missing-support-files.png" alt="Xcode Devices and Simulator Page displaying a DeviceSupport Error" /></p>

<p>While Xcode is able to determine the phones model, iOS version, memory capacity and even the serial number, it looks like there are some missing ‘Device Support’ files which will prevent it from going much further than that.</p>

<p>The suggestion is that I might need to install the latest version of Xcode I’m pretty sure that the opposite is required - I need an older version.</p>

<p>But which version do I need? Which one supports physical devices running iOS 14.6 like my iPhone 6s?</p>

<p>Yeah, there are probably sites out there, and I am using the Xcodes utility which allows me to have multiple Xcode versions installed side by side (only one being active at a time mind you) but is there another way?</p>

<p>Well as you’ve probably guessed by the fact this post isn’t finished yet there is an arguably cleaner way to get the latest version of Xcode to support older physical devices - and it only takes a few minutes and consists of just two steps.</p>

<h4 id="step-1-download-the-support-files">Step 1: Download the Support Files</h4>
<p>While it is possible to extract the files from a suitable version of Xcode, why do that when someone else has done all the hard work for you?</p>

<ul>
  <li>Just head over to <a href="https://github.com/iGhibli/iOS-DeviceSupport/" target="_blank">https://github.com/iGhibli/iOS-DeviceSupport/</a> and download the Zip file to your Mac.</li>
  <li>With the file downloaded, double-click on it to extract it and open the resulting folder to reveal a <code class="language-plaintext highlighter-rouge">DeviceSupport</code> folder with zip files for each iOS version from 8.0 to 16.4 (at the time of writing).</li>
</ul>

<p><img src="/assets/images/extracted-DeviceSupport-files.png" alt="Downloaded and Extracted Device Support files" /></p>

<h4 id="step-2-copy-them-to-the-required-location">Step 2: Copy them to the required location</h4>
<p>There are a lot of files in here but fortunately for me only one labelled 14.6 which was, according to the filename, pulled from Xcode 12.4.</p>

<p>I copied this file to my Desktop and extracted it to find that it contained two files;</p>

<p><img src="/assets/images/extracted-DeviceSupport-4.6-files.png" alt="Extracted Device Support files for iOS 14.6" /></p>

<p>Now for the slightly non-obvious bit - getting that folder to where it needs to be.</p>
<ul>
  <li>Open Finder and navigate to <code class="language-plaintext highlighter-rouge">Applications</code></li>
  <li>Right-Click on Xcode (yeah, sorry - I’m using a two button mouse, you do whatever works to open the context menu)</li>
  <li>Select <code class="language-plaintext highlighter-rouge">Show Package Contents</code></li>
</ul>

<p><img src="/assets/images/xcode-application-show-package-contents.png" alt="Finder showing Xcode Application Context Menu " /></p>

<ul>
  <li>Expand the resulting folder to the following path <code class="language-plaintext highlighter-rouge">Contents &gt; Developer &gt; Platforms &gt; iPhoneOS.platform &gt; DeviceSupport</code></li>
  <li>Copy the extracted folder into this folder</li>
</ul>

<p><img src="/assets/images/applications-xcode-content.png" alt="Xcode application Content folder structure" /></p>

<ul>
  <li>Restart Xcode and open <code class="language-plaintext highlighter-rouge">Window &gt; Devices and Simulators</code></li>
</ul>

<p>If you’ve pulled in the correct Support files you should now see that the error message has disappeared</p>

<p><img src="/assets/images/xcode-device-with-support-files.png" alt="Xcode Devices and Simulator Page With No DeviceSupport Error" /></p>

<p>Restart Rider and expand the Devices list and you should also see the device listed for selection here as well - result.</p>

<p><img src="/assets/images/rider-full-ios-device-list.png" alt="Jetbrains Rider iOS Device List with iPhone 6s now displayed" /></p>

<p>So, that’s that - how to get the latest version of Xcode to deploy and debug applications on an older, technically unsupported physical device.</p>

<h2 id="resources">Resources:</h2>
<p>Jon Brown Blog Post about the Xcodes Utility: <a href="https://jonbrown.org/blog/xcodes-walkthrough-and-review/" target="_blank">https://jonbrown.org/blog/xcodes-walkthrough-and-review/</a></p>]]></content><author><name></name></author><category term="maui" /><category term="mobile-phone" /><category term="mobile-development" /><category term="ios" /><category term="xcode" /><summary type="html"><![CDATA[While migrating a clients application from Xamarin.Forms to MAUI there was a requirement to test it on older iOS devices such as an iPhone 6s. Now that’s not really a big issue, I happened to have one in my ‘test phone’ collection that hadn’t been recycled or handed out to a family member who had dropped their own phone in the toilet. Getting the phone charged and booted was the easy part - getting it connected to my MacBook Pro so that I could deploy and debug the application to it required a little bit more effort, but not too much as it turned out.]]></summary></entry><entry><title type="html">Dynamic AppShell Items in MAUI</title><link href="https://onthefencedevelopment.com/2024/08/25/dynamic-appshell-items-in-maui.html" rel="alternate" type="text/html" title="Dynamic AppShell Items in MAUI" /><published>2024-08-25T00:00:00+00:00</published><updated>2024-08-25T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2024/08/25/dynamic-appshell-items-in-maui</id><content type="html" xml:base="https://onthefencedevelopment.com/2024/08/25/dynamic-appshell-items-in-maui.html"><![CDATA[<p>I’m currently working on a handful of Xamarin to MAUI migration projects with most of them using the AppShell navigation framework built-in to Xamarin.Forms - something I previously found a little clunky.</p>

<p>I’m sure most phone users are familiar with the concept of the AppShell even if they don’t know it, but basically it provides a slide out menu along with the good old ‘hamburger’ button and swipe gestures to open and close it.  With the menu open a number of menu items are presented for the user to select to navigate around the app and/or invoke operations such as logging in or out.</p>

<p>And therein lies the rub for both Xamarin and MAUI as out of the box the AppShell doesn’t appear to provide an obvious, clean mechanism to dynamically show or hide items based on state within the app, e.g. whether a user is logged in or not so that the appropriate menu options can be displayed.</p>

<p>The existing applications I’m working with had gotten around this shortcoming in slightly different ways which involved some form of ‘Global State’ class or methods being added to the <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> class which required various ugly casts dotted around the app to call them. Surely there had to be a better way than this.</p>

<!--more-->

<p>I decided that I wanted to implement something a little cleaner and more consistent for these apps and going forward. After a good amount of internet searching there didn’t seem to be anything obvious I was missing within MAUI but there were some throwaway comments such as ‘just use multiple AppShells’ or ‘just use MVVM’ which wasn’t overly helpful but got me thinking.</p>

<p>Having multiple AppShells and swapping them out as required didn’t smell right to me but as I’d already opted to use the MAUI Community Toolkit MVVM library to streamline the development for these apps I was already in a good starting point to investigate this option.</p>

<p>Before we start lets take a quick review of the startup process of a MAUI app by looking at a vanilla configuration from a File &gt; New Project operation.</p>

<p><img src="/assets/images/maui-startup-classes.png" alt="screenshot from the rider IDE showing the first three classes invoked during startup with MauiProgram.cs at the top, App.xaml.cs in the middle and AppShell.xaml.cs at the bottom" width="100%;" /></p>

<p>The entry point of a MAUI app, is the <code class="language-plaintext highlighter-rouge">MauiApplication.cs</code> class (the top one in the above image) which makes a reference to <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> (the middle one) which in turn calls out to <code class="language-plaintext highlighter-rouge">AppShell.xaml.cs</code> (obviously, the bottom one).</p>

<p>Ideally we want to be able to bind items in the <code class="language-plaintext highlighter-rouge">AppShell.xaml</code> view so that we can control their visibility, or whatever other properties we want/need to. This can be achieved throughout the rest of the app using View Models registered with the Dependency Service but the AppShell is being instantiated directly in the <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> via a parameterless constructor so how do we get a View Model in there?</p>

<p>Well, the answer is to inject it into <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> instead and then pass it into <code class="language-plaintext highlighter-rouge">AppShell.xaml.cs</code> by adding a suitable parameter to the existing constructor. If we also register the View Model as a Singleton then it can be injected throughout the app carrying its state around with it and allowing interaction with the bound elements of the AppShell.</p>

<p>I’ve created a <a href="/assets/downloads/dynamic_appshell_maui.zip">working project</a> to demonstrate this, which can be downloaded from the Resources section at the bottom of this post, but here is an overview of what’s required.</p>

<h3 id="project-setup">Project Setup</h3>
<p>The project is a standard File &gt; New Project &gt; MAUI Application with some additional Pages and View Models. It also has the <code class="language-plaintext highlighter-rouge">CommunityToolkit.Mvvm</code> and <code class="language-plaintext highlighter-rouge">CommunityToolkit.Maui</code> NuGet packages installed. These are not strictly speaking required but the solution relies on MVVM and while other options are available, including manual implementation of <code class="language-plaintext highlighter-rouge">INotifyPropertyChanged</code> etc this is the router I’ve chosen.</p>

<p>Once the packages are installed the first thing to do is enable the AppShell menu as the vanilla code has disabled it by default.</p>

<p>The first thing we need to di is open the <code class="language-plaintext highlighter-rouge">AppShell.xaml</code> file and locate the line that sets the <code class="language-plaintext highlighter-rouge">FlyoutBehaviour</code> which will probably have the value <code class="language-plaintext highlighter-rouge">Disabled</code>. Change this setting to <code class="language-plaintext highlighter-rouge">Flyout</code>.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Shell</span>
    
    
    <span class="na">Shell.FlyoutBehavior=</span><span class="s">"Flyout"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/Shell&gt;</span>
</code></pre></div></div>

<h3 id="view-model-setup">View Model Setup</h3>

<p>First, create a new class called <code class="language-plaintext highlighter-rouge">AppShellViewModel.cs</code> wherever your other View Models will reside.</p>

<p>Next we need to register this with the Dependency Injection Container so open <code class="language-plaintext highlighter-rouge">MauiProgram.cs</code> and register the View Model as a <strong>Singleton</strong> as below;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">public</span> <span class="k">static</span> <span class="n">MauiApp</span> <span class="nf">CreateMauiApp</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">MauiApp</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">();</span>
        <span class="n">builder</span>
            <span class="p">.</span><span class="n">UseMauiApp</span><span class="p">&lt;</span><span class="n">App</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">UseMauiCommunityToolkit</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">ConfigureFonts</span><span class="p">(</span><span class="n">fonts</span> <span class="p">=&gt;</span>
            <span class="p">{</span>
                <span class="n">fonts</span><span class="p">.</span><span class="nf">AddFont</span><span class="p">(</span><span class="s">"OpenSans-Regular.ttf"</span><span class="p">,</span> <span class="s">"OpenSansRegular"</span><span class="p">);</span>
                <span class="n">fonts</span><span class="p">.</span><span class="nf">AddFont</span><span class="p">(</span><span class="s">"OpenSans-Semibold.ttf"</span><span class="p">,</span> <span class="s">"OpenSansSemibold"</span><span class="p">);</span>
            <span class="p">});</span>

        <span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">AppShellViewModel</span><span class="p">&gt;();</span>
                
        <span class="k">return</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>Registering the View Model as a Singleton means that there will only ever be a single instance of it while the app is running. This means that if the <code class="language-plaintext highlighter-rouge">AppShellViewModel</code> is injected into another View Model which acts on a property bound to the AppShell view, that change will be reflected there.</p>

<p>Now we need to update <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> by adding a parameter for the new View Model which will now be injected at startup. We also need to update the instantiation of the <code class="language-plaintext highlighter-rouge">AppShell</code></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">public</span> <span class="nf">App</span><span class="p">(</span><span class="n">AppShellViewModel</span> <span class="n">appShellViewModel</span><span class="p">)</span>   <span class="c1">// New Parameter to received injected ViewModel</span>
    <span class="p">{</span>
        <span class="nf">InitializeComponent</span><span class="p">();</span>
        <span class="n">MainPage</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AppShell</span><span class="p">(</span><span class="n">appShellViewModel</span><span class="p">);</span>  <span class="c1">// Instantiate AppShell with injected ViewModel instance</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>So we now have a Singleton instance of the <code class="language-plaintext highlighter-rouge">AppShellViewModel</code> successfully ‘injected’ into the AppShell that we can bind to UI elements on the AppShell view itself - once we’ve added some properties to bind to of course, so let’s do that next.</p>

<h3 id="appshell-view-and-bindings">AppShell View and Bindings</h3>
<p>The <code class="language-plaintext highlighter-rouge">AppShellViewModel</code> is going to be doing all the heavy lifting here but with the Community Toolkit for MVVM installed the code is quite minimal as you’ll see from the sample app. Below is an example of a boolean property which can be bound to a menu item and control its visibility.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">AppShellViewModel</span> <span class="p">:</span> <span class="n">ObservableObject</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">ObservableProperty</span><span class="p">]</span> 
    <span class="k">private</span> <span class="kt">bool</span> <span class="n">_isLoggedIn</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With that property in place we can add some UI elements to the AppShell and bind them to it. In the sample app I have added a Login and a Logout button which obviously shouldn’t be displayed at the same time. These have been bound to the <code class="language-plaintext highlighter-rouge">IsLoggedIn</code> property generated by the Community Toolkit along with a Property Converter to flip the boolean value so that only one is ever displayed.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;Shell</span>
    <span class="na">x:Class=</span><span class="s">"Maui.DynamicShell.AppShell"</span>
    <span class="na">xmlns=</span><span class="s">"http://schemas.microsoft.com/dotnet/2021/maui"</span>
    <span class="na">xmlns:x=</span><span class="s">"http://schemas.microsoft.com/winfx/2009/xaml"</span>
    <span class="na">xmlns:local=</span><span class="s">"clr-namespace:Maui.DynamicShell"</span>
    <span class="na">xmlns:toolkit=</span><span class="s">"http://schemas.microsoft.com/dotnet/2022/maui/toolkit"</span>
    <span class="na">Shell.FlyoutBehavior=</span><span class="s">"Flyout"</span>
    <span class="na">x:DataType=</span><span class="s">"local:AppShellViewModel"</span>
    <span class="na">Title=</span><span class="s">"Dynamic Shell Demo"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;Shell.Resources&gt;</span>
        <span class="nt">&lt;ResourceDictionary&gt;</span>
            <span class="nt">&lt;toolkit:InvertedBoolConverter</span> <span class="na">x:Key=</span><span class="s">"InvertedBoolConverter"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;/ResourceDictionary&gt;</span>
    <span class="nt">&lt;/Shell.Resources&gt;</span>
    
    <span class="nt">&lt;ShellContent</span>
        <span class="na">Title=</span><span class="s">"Home"</span>
        <span class="na">ContentTemplate=</span><span class="s">"{DataTemplate local:MainPage}"</span>
        <span class="na">Route=</span><span class="s">"MainPage"</span> <span class="nt">/&gt;</span>
    
    <span class="nt">&lt;Shell.FlyoutContent&gt;</span>
        <span class="nt">&lt;StackLayout</span> <span class="na">Margin=</span><span class="s">"20,20,20,0"</span> <span class="na">Grid.Row=</span><span class="s">"1"</span> <span class="na">Spacing=</span><span class="s">"15"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;Button</span> <span class="na">IsVisible=</span><span class="s">"{Binding IsLoggedIn, Converter={StaticResource InvertedBoolConverter}}"</span> <span class="na">Text=</span><span class="s">"Login"</span> <span class="na">Command=</span><span class="s">"{Binding LoginCommand}"</span> <span class="nt">/&gt;</span>
            <span class="nt">&lt;Button</span> <span class="na">IsVisible=</span><span class="s">"{Binding IsLoggedIn}"</span> <span class="na">Text=</span><span class="s">"Logout"</span> <span class="na">Command=</span><span class="s">"{Binding LogoutCommand}"</span> <span class="nt">/&gt;</span>
        <span class="nt">&lt;/StackLayout&gt;</span>
    <span class="nt">&lt;/Shell.FlyoutContent&gt;</span>
<span class="nt">&lt;/Shell&gt;</span>
</code></pre></div></div>

<h3 id="making-it-work">Making it Work</h3>
<p>So lets review what we’ve done here</p>

<ul>
  <li>Created a View Model and registered it as a <strong>Singleton</strong> with Dependency Service (this Singleton bit is important)</li>
  <li>Updated <code class="language-plaintext highlighter-rouge">App.xaml.cs</code> to allow the Singleton instance of the View Model to be injected and passed to <code class="language-plaintext highlighter-rouge">AppShell.xaml.cs</code></li>
  <li>Updated <code class="language-plaintext highlighter-rouge">AppShell.xaml.cs</code> to allow the Singleton instance of the View Model to be passed in via a regular parameter</li>
  <li>Added an Observable Property to the View Model and bound it to a couple of buttons on the <code class="language-plaintext highlighter-rouge">AppShell.xaml</code> view</li>
</ul>

<p>Now consider a Login page which has a LoginViewModel with a LoginCommand where we want to hide the Login button when the Login was successful and for the Logout button to be displayed instead.</p>

<p>All we need to do is to update the LoginViewModel to allow the AppShellViewModel to be injected as below;</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">private</span> <span class="k">readonly</span> <span class="n">AppShellViewModel</span> <span class="n">_appShellViewModel</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">LoginViewModel</span><span class="p">(</span><span class="n">AppShellViewModel</span> <span class="n">appShellViewModel</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_appShellViewModel</span> <span class="p">=</span> <span class="n">appShellViewModel</span><span class="p">;</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>We can now update the LoginCommand to update the <code class="language-plaintext highlighter-rouge">IsLoggedIn</code> property of the <code class="language-plaintext highlighter-rouge">AppShellViewModel</code>, setting it to true to cause the Login button to be hidden and the Logout button to be displayed</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="p">[</span><span class="n">RelayCommand</span><span class="p">]</span>
    <span class="k">private</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">Login</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="p">.</span>
        <span class="p">.</span>
        <span class="p">.</span>
        <span class="k">if</span><span class="p">(</span><span class="n">loginSuccessful</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">_appShellViewModel</span><span class="p">.</span><span class="n">IsLoggedIn</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
            <span class="k">await</span> <span class="n">Shell</span><span class="p">.</span><span class="n">Current</span><span class="p">.</span><span class="nf">GoToAsync</span><span class="p">(</span><span class="s">$"//</span><span class="p">{</span><span class="k">nameof</span><span class="p">(</span><span class="n">MainPage</span><span class="p">)}</span><span class="s">"</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>In the example application I’ve added a <code class="language-plaintext highlighter-rouge">LogoutCommand</code> to the <code class="language-plaintext highlighter-rouge">AppShellViewModel</code> which sets <code class="language-plaintext highlighter-rouge">IsLoggedIn</code> to false, hiding the Logout button and displaying the Login button.</p>

<p>I’ve also added a handful of other Views and ViewModels to provide a bit more of a rounded example, but they are obviously not required for the example to work.</p>

<p>If you run the sample app in Android you will be able to open the menu and select either one of the ‘regular pages’ and return to the main page by clicking the ‘Back’ button on the device or the back arrow to the top left of the view.
Clicking the ‘Login’ option will open a page with a single ‘Login’ button - before clicking it, notice that the back arrow has been replaced with the hamburger menu so you can open the menu and verify that the ‘Login’ option is still displayed. Clicking the ‘Login’ button will take you back to the main page of the app and if you open the menu you will see that the ‘Login’ option is now hidden and that the ‘Logout’ option is displayed in its place.
Clicking the ‘Logout’ option won’t appear to do anything but opening the menu will show that the ‘Login’ option is once again displayed.</p>

<p>So there it is, a simple and clean solution to enable you to interact with the AppShell menu from anywhere in your app.</p>

<p>I hope you find it useful.</p>

<h3 id="resources">Resources</h3>
<p>Sample Application: <a href="/assets/downloads/dynamic_appshell_maui.zip">Download</a></p>]]></content><author><name></name></author><category term="mobile-development" /><category term="maui" /><category term="dotnet" /><summary type="html"><![CDATA[I’m currently working on a handful of Xamarin to MAUI migration projects with most of them using the AppShell navigation framework built-in to Xamarin.Forms - something I previously found a little clunky. I’m sure most phone users are familiar with the concept of the AppShell even if they don’t know it, but basically it provides a slide out menu along with the good old ‘hamburger’ button and swipe gestures to open and close it. With the menu open a number of menu items are presented for the user to select to navigate around the app and/or invoke operations such as logging in or out. And therein lies the rub for both Xamarin and MAUI as out of the box the AppShell doesn’t appear to provide an obvious, clean mechanism to dynamically show or hide items based on state within the app, e.g. whether a user is logged in or not so that the appropriate menu options can be displayed. The existing applications I’m working with had gotten around this shortcoming in slightly different ways which involved some form of ‘Global State’ class or methods being added to the App.xaml.cs class which required various ugly casts dotted around the app to call them. Surely there had to be a better way than this.]]></summary></entry><entry><title type="html">Configure Default Build Action for all items in a Folder</title><link href="https://onthefencedevelopment.com/2024/05/16/configure-build-action-for-folder.html" rel="alternate" type="text/html" title="Configure Default Build Action for all items in a Folder" /><published>2024-05-16T00:00:00+00:00</published><updated>2024-05-16T00:00:00+00:00</updated><id>https://onthefencedevelopment.com/2024/05/16/configure-build-action-for-folder</id><content type="html" xml:base="https://onthefencedevelopment.com/2024/05/16/configure-build-action-for-folder.html"><![CDATA[<p>How many times have you added an ‘non-content’ file like a SQL script to a folder and forget to set the required build action?</p>

<p>Well today I resolved this issue once and for all in two projects I’m currently working on with a simple update to the respective <code class="language-plaintext highlighter-rouge">csproj</code> files.</p>

<!--more-->

<p>In one of the projects I am using <a href="https://dbup.readthedocs.io/en/latest/" target="_blank">DbUp</a>, a package which allows me to create and apply database migrations easily. All I need to do is create a suitably named SQL script containing the required updates and <code class="language-plaintext highlighter-rouge">DbUp</code> will bundle it up and when the resulting executable is invoked it will check whether the script has already been run and if not it will do so.</p>

<p>Well … it will do that if I remember to set the <code class="language-plaintext highlighter-rouge">Build Action</code> to <code class="language-plaintext highlighter-rouge">EmbeddedResource</code> that is … something I frequently forget to do and only remember when I test the migration and wonder why it wasn’t applied.</p>

<p>I’d previously worked for a client where adding a migration script (they were using Entity Framework Migrations) that required a specific build action wasn’t an issue because it appeared to be handled automatically. At the time I tried to find the setting in Visual Studio but to no avail. Speaking to the tech lead he mentioned that it was a simple configuration made directly in the <code class="language-plaintext highlighter-rouge">csproj</code> file but he couldn’t bring it to mind and I didn’t investigate further before I left them.</p>

<p>Today, after bumping into this issue twice I decided enough was enough and had a dig around to find what turned out to be a simple solution.</p>

<p>The <code class="language-plaintext highlighter-rouge">csproj</code> file within one of the solution had a single <code class="language-plaintext highlighter-rouge">&lt;ItemGroup&gt;</code> containing all the SQL scripts - looking something like this;</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
    <span class="nt">&lt;EmbeddedResource</span> <span class="na">Include=</span><span class="s">"Scripts\00001-CreateTables.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;EmbeddedResource</span> <span class="na">Include=</span><span class="s">"Scripts\00002-AddSeedData.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;EmbeddedResource</span> <span class="na">Include=</span><span class="s">"Scripts\00003-UpdateUserTable.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;EmbeddedResource</span> <span class="na">Include=</span><span class="s">"Scripts\00004-AddAuditTable.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;EmbeddedResource</span> <span class="na">Include=</span><span class="s">"Scripts\00005-FixUsersTable.sql"</span> <span class="nt">/&gt;</span>
    .
    .
    .
    .
    .
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<p>But it also had an <code class="language-plaintext highlighter-rouge">&lt;ItemGroup&gt;</code> above this that looked like this:</p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
    <span class="nt">&lt;None</span> <span class="na">Remove=</span><span class="s">"Scripts\00001-CreateTables.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;None</span> <span class="na">Remove=</span><span class="s">"Scripts\00002-AddSeedData.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;None</span> <span class="na">Remove=</span><span class="s">"Scripts\00003-UpdateUserTable.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;None</span> <span class="na">Remove=</span><span class="s">"Scripts\00004-AddAuditTable.sql"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;None</span> <span class="na">Remove=</span><span class="s">"Scripts\00005-FixUsersTable.sql"</span> <span class="nt">/&gt;</span>
    .
    .
    .
    .
    .
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<p>So, to ensure the my SQL scripts were configured correctly without any input from me I removed these two blocks and replaced them with the following</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
    <span class="nt">&lt;EmbeddedResource</span> <span class="na">Include=</span><span class="s">"Scripts\*.sql"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;CopyToOutputDirectory&gt;</span>Never<span class="nt">&lt;/CopyToOutputDirectory&gt;</span>
    <span class="nt">&lt;/EmbeddedResource&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<p>Job done - when I create a new .sql file in that folder it will automatically be configured correctly. Right-clicking on a file and selecting <code class="language-plaintext highlighter-rouge">Properties</code> for the newly added file confirms this;</p>

<p><img src="/assets/images/sql-file-build-action.png" alt="sql file properties dialog in rider showing correct configuration" /></p>]]></content><author><name></name></author><category term="development" /><category term="dotnet" /><category term="csharp" /><category term="configuration" /><summary type="html"><![CDATA[How many times have you added an ‘non-content’ file like a SQL script to a folder and forget to set the required build action? Well today I resolved this issue once and for all in two projects I’m currently working on with a simple update to the respective csproj files.]]></summary></entry></feed>