<?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://derekwilson.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://derekwilson.github.io//" rel="alternate" type="text/html" /><updated>2026-04-09T09:38:32+00:00</updated><id>https://derekwilson.github.io//feed.xml</id><title type="html">derek wilson</title><subtitle>personal space for personal views
</subtitle><entry><title type="html">wrist-list for Amazfit/ZeppOS released</title><link href="https://derekwilson.github.io//blog/2026/04/07/wrist-list-for-zeppos" rel="alternate" type="text/html" title="wrist-list for Amazfit/ZeppOS released" /><published>2026-04-07T12:00:00+00:00</published><updated>2026-04-07T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2026/04/07/wrist-list-for-zeppos</id><content type="html" xml:base="https://derekwilson.github.io//blog/2026/04/07/wrist-list-for-zeppos"><![CDATA[<h2 id="wrist-list">wrist-list</h2>

<p>A couple of years ago I bought an <a href="/blog/2024/10/01/amazfit-bip5">Amazfit Bip5 Unity</a> and I <a href="/blog/2025/03/31/timestyle-zeppos">ported my watchface</a> to the Amazfit/ZeppOS device. I have now ported wrist-list for <a href="https://www.amazfit.com/pages/watch-classify">Amazfit/ZeppOS devices</a>.</p>

<p>The primary use case for the app is to be able to check off shopping items as you go around the supermarket without having to keep on getting your phone out. Hopefully it can also be used for many other todo list use cases.</p>

<p>wrist-list is not designed to edit and maintain lists, rather it will display lists from you chosen mobile todo list app conveniently on your wrist.</p>

<p>The main workflow is that you use your todo list application to produce your list, for example <a href="https://play.google.com/store/apps/details?id=com.google.android.keep&amp;hl=en">Google Keep</a>, then get the list onto the clipboard and paste it into the wrist-list app settings page on your phone and then send it to your device. Once the list is on your device you can check and un-check items. If you exit the app on your device the list and all the check marks are saved until you either reset the list or send a new list from your phone.</p>

<h2 id="settings">Settings</h2>

<p>The todo list is controlled from the settings page of the wrist-list app within the Zepp app on your phone.</p>

<ol>
  <li>Open Zepp app on your phone</li>
  <li>Select the Device tab, the rightmost tab at the bottom of the screen</li>
  <li>Then select More in the Device App Settings panel</li>
  <li>Then select wrist-list</li>
</ol>

<p><a href="/images/jekyll/2026-04-01/1.png">
    <img title="Settings" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Settings" src="/images/jekyll/2026-04-01/1.png" width="300" height="600" />
</a>
<a href="/images/jekyll/2026-04-01/2.png">
    <img title="Settings" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Settings" src="/images/jekyll/2026-04-01/2.png" width="300" height="600" />
</a></p>

<p>You should see a page like this</p>

<p><a href="/images/jekyll/2026-04-01/3.png">
    <img title="Settings" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Settings" src="/images/jekyll/2026-04-01/3.png" width="300" height="600" />
</a></p>

<p>The main items on this page are</p>

<h3 id="import-options">Import Options</h3>

<h4 id="separator">Separator</h4>

<p>Separators are used to split the text into list items as it is pasted into wrist-list.</p>

<ol>
  <li>Google Keep, is used when importing lists from Google Keep, see below for details of importing</li>
  <li>CSV, uses comma and semicolon <code class="language-plaintext highlighter-rouge">,;</code> as a separator</li>
  <li>Markdown, uses asterisk plus and minus <code class="language-plaintext highlighter-rouge">*+-</code> as a separator</li>
</ol>

<p>In addition spaces are trimmed from each items and empty items are removed</p>

<h4 id="sort-items">Sort Items</h4>

<p>If selected then the items are sorted alphabetically as they are imported. Otherwise the items are left in the order they were in the source text.</p>

<h4 id="delete-checked-items">Delete Checked Items</h4>

<p>If selected then items that have been checked in the source text are removed as they are imported. Otherwise all items are imported, the checked items are marked as checked. The identification of checked items depends on the source separator format. For Google Keep checked items start with <code class="language-plaintext highlighter-rouge">[x]</code>, for markdown checked items start <code class="language-plaintext highlighter-rouge">+</code></p>

<h3 id="other-options">Other options</h3>

<h4 id="group-checked-items">Group Checked Items</h4>

<p>If selected then checked items are placed together at the bottom of the list, otherwise they appear throughout the list where they were originally imported. This will apply when items are imported and also as they are checked on the watch.</p>

<h3 id="import">Import</h3>

<p>When you select import then a panel is displayed with a text entry field. You should either type items that you want to import into the list here, or perhaps more easily paste them into the text field. The typed or pasted text needs to include your selected separator style.</p>

<p>For example getting your list from Google Keep</p>

<ul>
  <li>Open Keep on your phone, goto the main screen that displays your lists</li>
  <li>Long press on a list</li>
  <li>From the overflow menu select “Send”</li>
</ul>

<p><a href="/images/jekyll/2026-04-01/keep1.png">
    <img title="Keep" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Keep" src="/images/jekyll/2026-04-01/keep1.png" width="300" height="400" />
</a></p>

<ul>
  <li>On the share menu, either copy it to the clipboard or send it to an app where you can copy the list to the clipboard. Different versions of Android will present the share options differently</li>
</ul>

<p><a href="/images/jekyll/2026-04-01/keep2a.png">
    <img title="Share" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Share" src="/images/jekyll/2026-04-01/keep2a.png" width="300" height="500" />
</a>
<a href="/images/jekyll/2026-04-01/keep2b.png">
    <img title="Share" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Share" src="/images/jekyll/2026-04-01/keep2b.png" width="300" height="500" />
</a>
<a href="/images/jekyll/2026-04-01/keep2c.png">
    <img title="Share" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Share" src="/images/jekyll/2026-04-01/keep2c.png" width="220" height="500" />
</a>
<a href="/images/jekyll/2026-04-01/keep2d.png">
    <img title="Share" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Share" src="/images/jekyll/2026-04-01/keep2d.png" width="220" height="500" />
</a></p>

<ul>
  <li>Goto the settings page of the wrist-list app within the Zepp app on your phone as detailed above</li>
  <li>Select Reset if you want to start a new list, or do not if you just want to add to a list</li>
  <li>Select Import</li>
  <li>Clear any existing text you do not want</li>
  <li>Long press on the text entry field and select paste</li>
</ul>

<p>You do not need to have wrist-list open on your watch to transfer the list, the next time it is opened on you watch then the list will be updated.</p>

<p><a href="/images/jekyll/2026-04-01/device1.png">
    <img title="List" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="List" src="/images/jekyll/2026-04-01/device1.png" width="200" height="238" />
</a></p>

<h3 id="reset">Reset</h3>

<p>This will remove all items from the current list. When you import items they are added to the list so you may want to reset before starting a new list.</p>

<h3 id="the-todo-list">The todo list</h3>

<p>This is the current list of items. Items can be deleted from wrist-list however it will not delete the items from the source app. This list is kept in sync with the one on your watch, it also shows the number of items and the number checked.</p>

<h2 id="import-list-format">Import list format</h2>

<p>wrist-list imports text from the clipboard and then breaks the text up into multiple items and puts them into the list. The format that wrist-list expects on the clipboard depends on separator selected on the settings page.</p>

<p>As long as the clipboard contains plain text with the correct separators then it can be imported into wrist-list. In these examples the text is shown with each item on a new line, this is just for clarity, the line breaks are not essential, it would work if the text was just on one line.</p>

<p>The item itself cannot contain a separator character</p>

<h3 id="google-keep">Google Keep</h3>

<p>The expected format is</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ ] bread 
[x] cheese 
[ ] milk
</code></pre></div></div>

<h3 id="csv">CSV</h3>

<p>The expected format is</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bread, 
milk; 
cheese,
</code></pre></div></div>

<h3 id="markdown">Markdown</h3>

<p>The expected format is</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- milk
+ cheese
- bread
</code></pre></div></div>

<h2 id="installing-wrist-list">Installing wrist-list</h2>

<p>You can install it on any Amazfit watch that uses ZeppOS v3 or better by using the Zepp App on your phone and going to device tab and selecting the App Store. You can search for wrist-list or find it in the Utilities section</p>

<p><a href="/images/jekyll/2026-04-01/store1.jpg">
    <img title="wrist-list" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-list" src="/images/jekyll/2026-04-01/store1.jpg" width="150" height="300" />
</a>
<a href="/images/jekyll/2026-04-01/store2.jpg">
    <img title="wrist-list" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-list" src="/images/jekyll/2026-04-01/store2.jpg" width="150" height="300" />
</a>
<a href="/images/jekyll/2026-04-01/store3.png">
    <img title="wrist-list" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-list" src="/images/jekyll/2026-04-01/store3.png" width="150" height="300" />
</a></p>

<p>The <a href="https://bitbucket.org/derekwilson/wrist-list/src/master/">source code</a> for wrist-list is publicly available and open source.</p>

<h2 id="success-criteria">Success criteria</h2>

<p>I try and think what I would want as success criteria when launching an app. My previous ZeppOS app <a href="/blog/2025/03/31/timestyle-zeppos">Timestyle+ achieved 1,000 downloads</a> in the first two months. I thought that anything better than that would be good.</p>

<p><a href="/images/jekyll/2026-04-01/downloads1.png">
    <img title="Total Downloads" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Total Downloads" src="/images/jekyll/2026-04-01/downloads1.png" width="500" height="300" />
</a></p>

<p>There have been over 2,500 downloads of wrist-list in the first month.</p>]]></content><author><name></name></author><category term="Gadgets" /><category term="General" /><category term="JavaScript" /><category term="ZeppOS" /><category term="Wrist-list" /><category term="Gadgets" /><category term="General" /><category term="JavaScript" /><category term="ZeppOS" /><category term="Wrist-list" /><summary type="html"><![CDATA[wrist-list]]></summary></entry><entry><title type="html">Timestyle for Amazfit/ZeppOS</title><link href="https://derekwilson.github.io//blog/2025/03/31/timestyle-zeppos" rel="alternate" type="text/html" title="Timestyle for Amazfit/ZeppOS" /><published>2025-03-31T12:00:00+00:00</published><updated>2025-03-31T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2025/03/31/timestyle-zeppos</id><content type="html" xml:base="https://derekwilson.github.io//blog/2025/03/31/timestyle-zeppos"><![CDATA[<p>Ten years ago I bought a <a href="https://en.wikipedia.org/wiki/Pebble_Time">Pebble Time</a>, and I loved <a href="https://www.dantilden.com/projects/timestyle/">Dan Tilden’s wonderful iconic Timestyle</a> watch face. 5 years later I bought a <a href="/blog/2019/08/27/fitbit-versa">Fitbit Vera</a> and I could not find a watch face as good as Timestyle so I ported Dan’s design and wrote the code again from scratch and released <a href="https://gallery.fitbit.com/details/dfe5fccd-01e5-4979-a5ad-070673df12dd">Timestyle for Fitbit</a> on the app atore.</p>

<p>Last year I bought an <a href="/blog/2024/10/01/amazfit-bip5">Amazfit Bip5 Unity</a> and once again I missed the clear simplicity of Dan’s design and I decided to port the design to ZeppOS. The environment was completely different so I ended up rewriting the code again from scratch. I am very happy with the result.</p>

<p><a href="/images/jekyll/2025-03-01/1.png">
    <img title="Mono" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Mono" src="/images/jekyll/2025-03-01/1.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/2.png">
    <img title="Blue" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Blue" src="/images/jekyll/2025-03-01/2.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/3.png">
    <img title="Khaki" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Khaki" src="/images/jekyll/2025-03-01/3.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/4.png">
    <img title="Pink" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Pink" src="/images/jekyll/2025-03-01/4.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/6.png">
    <img title="Electric" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Electric" src="/images/jekyll/2025-03-01/6.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/7.png">
    <img title="Christmas" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Christmas" src="/images/jekyll/2025-03-01/7.png" width="200" height="238" />
</a></p>

<h2 id="installing-timestyle">Installing Timestyle</h2>

<p>I have gone to the effort of getting the watch face published in the official app store - you can install it on any Bip 5 watch by using the Zepp App and going to device tab and selecting watch faces. If it is not on the featured list then you can find it in the Simplicity section</p>

<p><a href="/images/jekyll/2025-03-01/zepp_app1.jpg">
    <img title="Devices Tab" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Devices Tab" src="/images/jekyll/2025-03-01/zepp_app1.jpg" width="150" height="300" />
</a>
<a href="/images/jekyll/2025-03-01/zepp_app2.jpg">
    <img title="Watch face description" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Watch face description" src="/images/jekyll/2025-03-01/zepp_app2.jpg" width="150" height="300" />
</a></p>

<h2 id="main-design-ideas">Main design ideas</h2>

<p>Unlike most ZeppOS watch faces Timestyle uses pure text to render the display, this means that it is very power friendly and also can offer the ability to have multiple colour and font renders in one watch face.</p>

<ul>
  <li>8 built-in colour schemes</li>
  <li>7 fonts</li>
  <li>12 or 24 hour display</li>
  <li>Watch battery level</li>
  <li>Optionally display any two of steps, PAI, stand, fat burn, or heart rate</li>
  <li>Day/Date</li>
  <li>Supports English, French, German and Spanish</li>
</ul>

<p>There have been some changes since the last port. ZeppOS provides no easy mechanism for configuring the watch face from the Zepp App and the configuration options on the device are limited to being able to select between builtin colour schemes and fonts and selecting which sensor data to display on the sidebar. So its not really possible to support the ability to customise the colour schemes, however I have provided that ability in a separate app called Timestyle+.</p>

<h2 id="timestyle">Timestyle+</h2>

<p>The structure of watch faces in ZeppOS means that some more complex UI designs are not really possible. So to add extra functionality to the Timestyle watch face I have created and published the Timestyle+ app on the official app store.</p>

<p>You do not need to install Timestyle+, if you are happy with the built-in options then you can just use the Timestyle watch face as is.</p>

<p><a href="/images/jekyll/2025-03-01/plus1.png">
    <img title="Main Menu" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Main Menu" src="/images/jekyll/2025-03-01/plus1.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/plus2.png">
    <img title="Scheme List" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Scheme List" src="/images/jekyll/2025-03-01/plus2.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/plus3.png">
    <img title="Customise scheme" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Customise scheme" src="/images/jekyll/2025-03-01/plus3.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2025-03-01/plus4.png">
    <img title="Colour picker" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Colour picker" src="/images/jekyll/2025-03-01/plus4.png" width="200" height="238" />
</a></p>

<p>After installing Timestyle+ you will see extra colour schemes in the Timestyle watch face configuration screen. These schemes are called Custom1, Custom2 etc. You can change the colours for these custom colour schemes in this app and the changes will be reflected in the Timestyle watch face.</p>

<h2 id="installing-timestyle-1">Installing Timestyle+</h2>

<p>You can install it on any Bip 5 watch by using the Zepp App and going to device tab and selecting app store. You can search for Timestyle+ or find it in the Utilities section</p>

<p><a href="/images/jekyll/2025-03-01/zepp_app3.jpg">
    <img title="Timestyle+ description" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Timestyle+ description" src="/images/jekyll/2025-03-01/zepp_app3.jpg" width="150" height="300" />
</a></p>

<h2 id="other-amazfitzeppos-devices">Other Amazfit/ZeppOS devices</h2>

<p>As Timestyle and Timestyle+ both target ZeppOS v1 in theory they should run on almost any Amazfit device however I have noticed that testing in the simulator is not the same as a real device, for instance the way large font characters are handled. So with this im mind I will add support for other devices when I have the real devices to test on.</p>

<h2 id="success-criteria">Success criteria</h2>

<p>When I published Timestyle for Fitbit I tried to <a href="/blog/2020/01/23/fitbit-timestyle-stats">measure the performance against my success criteria</a>. Timestyle for Fitbit continues to be successful with over 10,000 downloads and over 1,000 reviews averaging 4.5 stars.</p>

<p>I guess when I published the watch face for ZeppOS I wanted to match that performance. Well it turns out that Timestyle is easilly my most successful project. The Fitbit version got about 1,000 downloads in the first three months, the ZeppOS version has had almost 20,000 downloads in the first three months and Timestyle+ has had 700 downloads in the first month.</p>

<p><a href="/images/jekyll/2025-03-01/downloads1.png">
    <img title="Total Downloads" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Total Downloads" src="/images/jekyll/2025-03-01/downloads1.png" width="500" height="300" />
</a></p>

<p><a href="/images/jekyll/2025-03-01/downloads2.png">
    <img title="Total Downloads" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Total Downloads" src="/images/jekyll/2025-03-01/downloads2.png" width="500" height="300" />
</a></p>]]></content><author><name></name></author><category term="Gadgets" /><category term="General" /><category term="JavaScript" /><category term="ZeppOS" /><category term="Timestyle" /><category term="Gadgets" /><category term="General" /><category term="JavaScript" /><category term="ZeppOS" /><category term="Timestyle" /><summary type="html"><![CDATA[Ten years ago I bought a Pebble Time, and I loved Dan Tilden’s wonderful iconic Timestyle watch face. 5 years later I bought a Fitbit Vera and I could not find a watch face as good as Timestyle so I ported Dan’s design and wrote the code again from scratch and released Timestyle for Fitbit on the app atore.]]></summary></entry><entry><title type="html">Another Gadget: Amazfit Bip5</title><link href="https://derekwilson.github.io//blog/2024/10/01/amazfit-bip5" rel="alternate" type="text/html" title="Another Gadget: Amazfit Bip5" /><published>2024-10-01T12:00:00+00:00</published><updated>2024-10-01T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2024/10/01/amazfit-bip5</id><content type="html" xml:base="https://derekwilson.github.io//blog/2024/10/01/amazfit-bip5"><![CDATA[<p>Almost exactly 5 years after I bought my previous everyday wearable <a href="/blog/2019/08/27/fitbit-versa">a Fitbit Vera</a> I decided to buy a new one.</p>

<h2 id="my-requirements">My requirements</h2>

<p>Having previously used Pebble and Fitbit devices I boiled down my requirements to</p>

<ul>
  <li>Good battery life, I think a week is pretty much the minimum I would like to have to live with</li>
  <li>SDK available, I want to be able to write my own apps and download others</li>
  <li>Affordable, I am not sure I understand watches costing hundreds (or thousands) of dollard and only lasting a couple of years</li>
  <li>Notifications on my wrist, I dont use my phone lock screen</li>
</ul>

<p>I dont think this is a demanding list and it was certainly covered by both Fitbit and Pebble but today its quite tricky. Most fitness trackers just come with the prebuilt software and thats not really what I want, I want a large clear time display that I can easily see and also I have got used to having the cricket scores on my wrist. Watches that do have an SDK pretty much need charging every day and tend to be quite expensive.</p>

<h2 id="amazfit-bip-5">Amazfit Bip 5</h2>

<p>I settled on an <a href="https://www.amazfit.com/pages/amazfit-bip-5-unity">Amazfit Bip 5</a>, and as it turned out I am very happy with it. The only minor drawbacks are</p>

<ul>
  <li>Screen does not automatically dim and brighten</li>
  <li>Screen isn’t really visible in bright sunlight</li>
  <li>It took a couple of weeks for the battery life to settle down</li>
</ul>

<p>Also there are some unexpected bonuses</p>

<ul>
  <li>The wrist movement detection to turn the screen on is much better than Fitbit</li>
  <li>The movement detection can be turned off at night</li>
  <li>It seems very well made</li>
  <li>The developer community support is fantastic</li>
</ul>

<h2 id="wrist-spin">wrist-spin</h2>

<p>I have ported wrist-spin my cricket score tracker to Pebble, Fitbit and <a href="/blog/2024/06/01/what-has-been-happening">Android</a> devices so it made sense for me to port it to the Bip 5. I have added support for ZeppOS to the <a href="https://bitbucket.org/derekwilson/wrist-spin">source repo</a>. Its largely a port of the Fitbit javascript code. I hope that it will run on other ZeppOS devices but its my first app so lets see how that works out.</p>

<p><a href="/images/jekyll/2024-10-01/screen1.png">
    <img title="Match Display" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Match Display" src="/images/jekyll/2024-10-01/screen1.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2024-10-01/screen2.png">
    <img title="Match Details Display" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Match Details Display" src="/images/jekyll/2024-10-01/screen2.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2024-10-01/screen3.png">
    <img title="Main Menu" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Main Menu" src="/images/jekyll/2024-10-01/screen3.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2024-10-01/screen4.png">
    <img title="Select Match" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Select Match" src="/images/jekyll/2024-10-01/screen4.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2024-10-01/screen5.png">
    <img title="Options" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Options" src="/images/jekyll/2024-10-01/screen5.png" width="200" height="238" />
</a>
<a href="/images/jekyll/2024-10-01/screen6.png">
    <img title="Data Source" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Data Source" src="/images/jekyll/2024-10-01/screen6.png" width="200" height="238" />
</a></p>]]></content><author><name></name></author><category term="Gadgets" /><category term="General" /><category term="JavaScript" /><category term="ZeppOS" /><category term="Wrist-spin" /><category term="Gadgets" /><category term="General" /><category term="JavaScript" /><category term="ZeppOS" /><category term="Wrist-spin" /><summary type="html"><![CDATA[Almost exactly 5 years after I bought my previous everyday wearable a Fitbit Vera I decided to buy a new one.]]></summary></entry><entry><title type="html">What has been happening?</title><link href="https://derekwilson.github.io//blog/2024/06/01/what-has-been-happening" rel="alternate" type="text/html" title="What has been happening?" /><published>2024-06-01T12:00:00+00:00</published><updated>2024-06-01T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2024/06/01/what-has-been-happening</id><content type="html" xml:base="https://derekwilson.github.io//blog/2024/06/01/what-has-been-happening"><![CDATA[<p><a href="/blog/2023/05/04/end-of-an-era">I retired</a> just over a year ago. I did say I would not be posting as often and it turns out I was right. I have still been doing some work on my projects so I though I would post a quick status update</p>

<h2 id="podcastutilities-for-android">PodcastUtilities for Android</h2>

<p><a href="https://www.amazon.com/dp/B0BG7SZJTL/">PodcastUtilities for Android</a> has developed into a pretty fully functional app now with the addition of the ability to configure the app rather than having to edit an XML file.</p>

<ul>
  <li>Reworked the UI for adding a new feed to lookup the URL from the clipboard and the title from the feed</li>
  <li>Added ability to share a podcast RSS feed URL</li>
  <li>Added ability to modify the configuration from within the app</li>
  <li>Added ability to share the current control file off the device</li>
  <li>Added support for dark mode</li>
  <li>Accessibility fixes: Text scaling on toolbar and colour contrast</li>
  <li>Added keyboard support</li>
  <li>Made available for Windows Subsystem for Android</li>
</ul>

<p><a href="/images/jekyll/2024-06-01/pu1.png">
    <img title="PodcastUtilities1" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="PodcastUtilities1" src="/images/jekyll/2024-06-01/pu1.png" width="150" height="300" />
</a>
<a href="/images/jekyll/2024-06-01/pu2.png">
    <img title="PodcastUtilities2" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="PodcastUtilities2" src="/images/jekyll/2024-06-01/pu2.png" width="150" height="300" />
</a>
<a href="/images/jekyll/2024-06-01/pu3.png">
    <img title="PodcastUtilities3" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="PodcastUtilities3" src="/images/jekyll/2024-06-01/pu3.png" width="150" height="300" />
</a></p>

<h2 id="podcastutilities-for-windowslinux-and-mac">PodcastUtilities for Windows/Linux and Mac</h2>

<p>For the desktop version of PodcastUtilities I’ve updated the chocolatey packages for the <a href="https://community.chocolatey.org/packages/podcastutilities">Windows only</a> version and the <a href="https://community.chocolatey.org/packages/podcastutilities-core">cross platform version</a>. The main change was to upgrade the target from .NET Core 2.1 to .NET Core 3.1. This is because there were security vulnerability warnings with .NET Core 2.1 and as its out of support I needed to update.</p>

<ul>
  <li>updated .NET Core CLI tools to target .NET Core 3.1 rather than 2.1 (2.1 has security warnings that are not going to be patched)</li>
  <li>Improved processing of episodes with ‘.’ in the title when used as a filename</li>
  <li>Expose and log the inner exception when returning errors from IEpisodeFinder and ICopier</li>
</ul>

<h2 id="trailblazer">Trailblazer</h2>

<p>I did a bit of a road trip with <a href="https://play.google.com/store/apps/details?id=com.andrewandderek.trailblazer">Trailblazer</a> and found a few minor glitches that were impacting how it can be used so we have smoothed over those rough edges.</p>

<ul>
  <li>Added ability to combine tracks, by multiselecting them</li>
  <li>Added the display of segment information to the statistics page</li>
  <li>Added support for dark mode</li>
  <li>Track list is now searchable</li>
  <li>Track recording will continue even if the app is restarted by the OS</li>
  <li>Importing tracks without timestamp data (eg. MyMaps) is now supported</li>
  <li>Fixed an out of memory issue when comparing large tracks</li>
  <li>Fixed issue with importing KML tracks from Google Earth</li>
</ul>

<h2 id="rameater">RamEater</h2>

<p>I dont tend to do much work on <a href="https://play.google.com/store/apps/details?id=derekwilson.net.rameater">RamEater</a> as its pretty complete but I did fix a minor issue with notifications permissions on Android 13. I am not looking forward to the additional restrictions imposed by Google with the forced update to Android 14, I am glad the app is also available on the <a href="https://www.amazon.com/Derek-Wilson-RamEater/dp/B0B1LBJYY1/">Amazon App Store</a></p>

<h2 id="wrist-spin-for-android">wrist-spin for Android</h2>

<p>Almost a decade ago I produced wrist-spin a <a href="https://apps.rebble.io/en_US/application/56904b60e74aedc6b600000b?query=crick&amp;section=watchapps">cricket scoring app for Pebble watches</a>. When that platform <a href="/blog/2018/07/27/rebble-alliance">came to and end</a> I ported the app to Fitbit watches. Because of issues with <a href="/blog/2019/08/27/fitbit-versa">the way the watches talk to the internet</a> I needed to use an Azure function proxy, which meant it ran through my Azure account so I was unwilling to put the app on the Fitbit app store. Now that Fitbit (or Google) have decided that <a href="https://community.fitbit.com/t5/Versa-4/Versa-4-can-t-install-any-apps/td-p/5293802">developers will not be allowed to develop apps for the newer Fitbit watches</a> it looks as though that platform is also coming to an end.</p>

<p>I decided to port the app again, from javascript to Android Kotlin/Java with a view to running the app on android phones and maybe <a href="https://wearos.google.com">WearOS</a> watches. It was fun and now I have released <a href="https://www.amazon.com/dp/B0D2ZJFS3G/">wrist-spin for Android phones</a>. I originally produced wrist-spin for wearables as I wanted a low friction method of keeping up to date with the score and the main drive behind the port was to target <a href="https://wearos.google.com">WearOS</a>. However having produced the phone app I find that having the score on my lock screen or spoken using Text To Speech to work surprisingly well.</p>

<p><a href="/images/jekyll/2024-06-01/wrist-spin1.png">
    <img title="wrist-spin1" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-spin1" src="/images/jekyll/2024-06-01/wrist-spin1.png" width="150" height="300" />
</a>
<a href="/images/jekyll/2024-06-01/wrist-spin2.png">
    <img title="wrist-spin2" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-spin2" src="/images/jekyll/2024-06-01/wrist-spin2.png" width="150" height="300" />
</a>
<a href="/images/jekyll/2024-06-01/wrist-spin3.png">
    <img title="wrist-spin3" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-spin3" src="/images/jekyll/2024-06-01/wrist-spin3.png" width="150" height="300" />
</a>
<a href="/images/jekyll/2024-06-01/wrist-spin4.png">
    <img title="wrist-spin4" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="wrist-spin4" src="/images/jekyll/2024-06-01/wrist-spin4.png" width="150" height="300" />
</a></p>]]></content><author><name></name></author><category term="Xamarin" /><category term="Development" /><category term="Android" /><category term=".Net" /><category term="PodcastUtilities" /><category term="RamEater" /><category term="Trailblazer" /><category term="Wrist-spin" /><category term="Mobile" /><category term="Xamarin" /><category term="Development" /><category term="Android" /><category term=".Net" /><category term="PodcastUtilities" /><category term="RamEater" /><category term="Trailblazer" /><category term="Wrist-spin" /><category term="Mobile" /><summary type="html"><![CDATA[I retired just over a year ago. I did say I would not be posting as often and it turns out I was right. I have still been doing some work on my projects so I though I would post a quick status update]]></summary></entry><entry><title type="html">Worldolio updated</title><link href="https://derekwilson.github.io//blog/2023/10/09/worldolio-new-version" rel="alternate" type="text/html" title="Worldolio updated" /><published>2023-10-09T12:00:00+00:00</published><updated>2023-10-09T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2023/10/09/worldolio-new-version</id><content type="html" xml:base="https://derekwilson.github.io//blog/2023/10/09/worldolio-new-version"><![CDATA[<p>In my <a href="/blog/2023/05/04/end-of-an-era">previous-post</a>, in May, I did say that I would no longer feel compelled to post once a month, and indeed it turns out that’s exactly what has happened. I did also say that I would continue to learn and work on personal projects and with that in mind I have made some progress.</p>

<p><a href="https://worldolio.azurewebsites.net/default.aspx?ctrl=tab">Worldolio</a> is an application that enables you to keep track of various geographical information for cities around the world. It was a collaborative project that was started 20 years ago but we have not really updated it since 2008. A while ago I did produce a <a href="https://community.chocolatey.org/packages/worldolio">package available on Chocolatey</a> to enable the 2008 build to be easily installed.</p>

<p>I have been away for a number of months but on my return I managed to find some time to look into producing an update for the time zone data. The whole build mechanism was last run on Windows XP so it was a bit of challenge working out how to produce the update and documenting how it can be done in future.</p>

<p>In the 15 years since the last release there have been quite a number of changes to the time zone for the cities in Worldolio. The changes include the Russian Federation dropping DST and Turkey changing time zone.</p>

<p>Added new timezones:</p>
<ul>
  <li>(UTC+03:00) Istanbul</li>
  <li>(UTC+06:00) Dhaka</li>
  <li>(UTC-05:00) Havana</li>
  <li>(UTC-05:00) Haiti</li>
  <li>(UTC+02:00) Tripoli</li>
  <li>(UTC+11:00) Norfolk Island</li>
  <li>(UTC+02:00) Khartoum</li>
  <li>(UTC+02:00) Damascus</li>
  <li>(UTC+03:00) Minsk</li>
  <li>(UTC-05:00) Turks and Caicos</li>
  <li>(UTC+03:00) Volgograd</li>
  <li>(UTC-07:00) Yukon</li>
</ul>

<p>Updated timezones:</p>
<ul>
  <li>(GMT-04:00) La Paz</li>
  <li>(UTC-01:00) Cabo Verde Is.</li>
  <li>(GMT+03:00) Moscow; St. Petersburg; Volgograd</li>
  <li>(GMT+05:00) Ekaterinburg</li>
  <li>(GMT+06:00) Almaty; Novosibirsk</li>
  <li>(GMT+07:00) Krasnoyarsk</li>
  <li>(GMT+08:00) Irkutsk; Ulaan Bataar</li>
  <li>(GMT+09:00) Yakutsk</li>
  <li>(GMT+10:00) Vladivostok</li>
</ul>

<p>Removed timezones:</p>
<ul>
  <li>(GMT+04:00) Yerevan</li>
  <li>(GMT-07:00) Chihuahua; La Paz; Mazatlan - Old</li>
  <li>(GMT-06:00) Guadalajara; Mexico City; Monterrey - Old</li>
</ul>

<p>Corrected timezones for: Almaty, Ankara, Bishkek, Damascus, Dhaka, Havana, Istanbul, Khartoum, Minsk, Port-au-Prince, Tripoli, Volgograd, Whitehorse, Yerevan, Kingston, Grand Turk</p>

<p>I have updated <a href="https://worldolio.azurewebsites.net/Pages/WOWin/default.aspx?ctrl=tab">Windows desktop application</a> and its <a href="https://community.chocolatey.org/packages/worldolio">chocolatey package</a> as well as the <a href="https://worldolio.azurewebsites.net/Pages/WOWebApp/addmap.aspx">web site version</a>.</p>

<p>Upgrading Worldolio from chocolatey is done by running <code class="language-plaintext highlighter-rouge">choco upgrade worldolio</code> in Powershell like this (you need to run the upgrade as an administrator)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Windows PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.

PS C:\Data&gt; choco list --local-only
Chocolatey v0.10.15
chocolatey 0.10.15
podcastutilities 3.1.0.0
podcastutilities-core 3.1.0.0
worldolio 2.0.0.0
4 packages installed.
PS C:\Data&gt; choco upgrade worldolio
Chocolatey v0.10.15
Upgrading the following packages:
worldolio
By upgrading you accept licenses for the packages.

You have worldolio v2.0.0.0 installed. Version 2.0.1.0 is available based on your source(s).
Progress: Downloading worldolio 2.0.1.0... 100%

worldolio v2.0.1.0 [Approved]
worldolio package files upgrade completed. Performing other installation steps.
The package worldolio wants to run 'chocolateyinstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[A]ll - yes to all/[N]o/[P]rint): y

Extracting C:\ProgramData\chocolatey\lib\worldolio\tools\\Worldolio.zip to C:\ProgramData\worldolio...
C:\ProgramData\worldolio
Added C:\ProgramData\chocolatey\bin\worldolio.exe shim pointed to 'c:\programdata\worldolio\worldolio.exe'.
 The upgrade of worldolio was successful.
  Software installed to 'C:\ProgramData\worldolio'

Chocolatey upgraded 1/1 packages.
 See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).
PS C:\Data&gt;
PS C:\Data&gt; choco list --local-only
Chocolatey v0.10.15
chocolatey 0.10.15
podcastutilities 3.1.0.0
podcastutilities-core 3.1.0.0
worldolio 2.0.1.0
4 packages installed.
PS C:\Data&gt;
</code></pre></div></div>]]></content><author><name></name></author><category term=".Net" /><category term="Worldolio" /><category term="Development" /><category term=".Net" /><category term="Worldolio" /><category term="Development" /><summary type="html"><![CDATA[In my previous-post, in May, I did say that I would no longer feel compelled to post once a month, and indeed it turns out that’s exactly what has happened. I did also say that I would continue to learn and work on personal projects and with that in mind I have made some progress.]]></summary></entry><entry><title type="html">End of an Era</title><link href="https://derekwilson.github.io//blog/2023/05/04/end-of-an-era" rel="alternate" type="text/html" title="End of an Era" /><published>2023-05-04T12:00:00+00:00</published><updated>2023-05-04T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2023/05/04/end-of-an-era</id><content type="html" xml:base="https://derekwilson.github.io//blog/2023/05/04/end-of-an-era"><![CDATA[<p>The end of an era, that is how a colleague described it when I decided to retire. I’m not sure if that is good or bad but I think he meant it in a good way.</p>

<p>Well it certainly wil be a change. I stared work in 1987, I <a href="/blog/2008/09/23/hello-world!">started this blog in 2008</a>. The initial idea was to have a place where I could keep notes and information that I would otherwise forget. It also was potentially a place to show off my abilities to potential employers as well as announce developments in my personal software projects for pretty much the same reason. I remember when I started the blog reading other developer’s experiences in creating content, one of the pieces of advice that stuck in my mind was that I should decide on the minimum amount of content I wanted to write, and stick to it.</p>

<p>In my head I decided that I would <a href="/blog/archive">write once a month</a>. This kept the blog alive but it also meant that I had to achieve things to write in the blog, learning things or producing new releases of my software projects. So a win - win activity.</p>

<p>I <a href="/blog/2018/09/27/ten-years-after">took stock after ten years</a> and felt that the blog was achieving what I intended. Now I am retired I think I need to reevaluate the objectives of this blog. I am still keen to carry on learning and plan more personal project work so I anticipate still producing content. However, one of the things I am learning about being retied is the joy of being able to set my own deadlines and not needing to be constrained by my employment. With that in mind I will still produce content but I no longer feel that it needs to be every month.</p>]]></content><author><name></name></author><category term="General" /><category term="General" /><summary type="html"><![CDATA[The end of an era, that is how a colleague described it when I decided to retire. I’m not sure if that is good or bad but I think he meant it in a good way.]]></summary></entry><entry><title type="html">Multiplatform test projects on .NET Core and .NET Framework</title><link href="https://derekwilson.github.io//blog/2023/03/27/dotnet-multiplatform-tests" rel="alternate" type="text/html" title="Multiplatform test projects on .NET Core and .NET Framework" /><published>2023-03-27T12:00:00+00:00</published><updated>2023-03-27T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2023/03/27/dotnet-multiplatform-tests</id><content type="html" xml:base="https://derekwilson.github.io//blog/2023/03/27/dotnet-multiplatform-tests"><![CDATA[<p>Previously I wrote about <a href="/blog/2019/04/26/dotnet-multiplatform">porting PodcastUtilities to .NETCore</a>, that is, producing a DLL project that can build multiple platform targets, one for the cross platform <a href="https://msdn.microsoft.com/en-us/magazine/mt842506.aspx">.NET Standard</a> and one for the Windows only .NET Framework. At the time we understood that we had incurred some tech debit, in that the tests still only ran on .NET Framework due to their use of <a href="https://github.com/ayende/rhino-mocks">RhinoMocks</a>. RhinoMocks is <a href="https://blog.ladeak.net/posts/rhinomocks-updater">no longer maintained and does not support .NETCore</a>.</p>

<p>When I wrote the previous post I was using VS2017 which would not run any of the tests that targetted .NETFramework 3.5, I had to use the stand-alone NUnit runner. Now with VS2022 the picture is better as VS will run the old .NETFramework tests. However we would like to be able to run the tests for <code class="language-plaintext highlighter-rouge">PodcastUtilities.Common.DLL</code> on .NETFramework as well as .NETCore, after all the assembly can target both platforms.</p>

<p>So, over the last month we have taken the existing old RhinoMock tests in <code class="language-plaintext highlighter-rouge">PodcastUtilities.Common.Tests</code> and produced a new <code class="language-plaintext highlighter-rouge">PodcastUtilities.Common.Multiplatform.Tests</code> which has all the original unit tests that can be run on .NETCore and .NETFramework.</p>

<h2 id="the-mocking-framework">The Mocking Framework</h2>

<p>Obviously I did what most people do and selected the <a href="https://www.danclarke.com/comparing-dotnet-mocking-libraries">best known mocking framework</a>. Also it must be said I have used <a href="https://github.com/moq/moq4">Moq</a> in the past. Moq supports both .NETFramework (oldest version 4.6.2) as well as .NETCore (the oldest version I could get to run in VS2022 was 3.1)</p>

<h2 id="porting-rhinomocks-to-moq">Porting RhinoMocks to Moq</h2>

<p>There were approximately 500 tests to migrate. It was pretty straightforward, some tests made little use of mocks and most of the rest were ported like this</p>

<h3 id="rhinomock-test">RhinoMock Test</h3>

<p>As an example this is a test written using RhinoMocks</p>

<figure class="highlight"><pre><code class="language-c#" data-lang="c#"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
</pre></td><td class="code"><pre><span class="p">[</span><span class="n">TestFixture</span><span class="p">]</span>
<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">WhenTestingBehaviour</span>
<span class="p">{</span>
  <span class="c1">/// &lt;summary&gt;</span>
  <span class="c1">/// Seal the method so it can not be overriden. We want all _context to be</span>
  <span class="c1">/// set in the &lt;see cref="GivenThat" /&gt; method.</span>
  <span class="c1">/// &lt;/summary&gt;</span>
  <span class="p">[</span><span class="n">SetUp</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">SetUp</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="nf">GivenThat</span><span class="p">();</span>

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

  <span class="c1">/// &lt;summary&gt;</span>
  <span class="c1">/// Set up the _context of the test.</span>
  <span class="c1">/// &lt;/summary&gt;</span>
  <span class="k">protected</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">GivenThat</span><span class="p">()</span>
  <span class="p">{</span>
  <span class="p">}</span>

  <span class="c1">/// &lt;summary&gt;</span>
  <span class="c1">/// Invoke the action being tested.</span>
  <span class="c1">/// &lt;/summary&gt;</span>
  <span class="k">protected</span> <span class="k">abstract</span> <span class="k">void</span> <span class="nf">When</span><span class="p">();</span>

  <span class="k">protected</span> <span class="n">TM</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">TM</span><span class="p">&gt;()</span>
    <span class="k">where</span> <span class="n">TM</span> <span class="p">:</span> <span class="k">class</span>
  <span class="err">{</span>
    <span class="nc">return</span> <span class="n">MockRepository</span><span class="p">.</span><span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">TM</span><span class="p">&gt;();</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">WhenTestingTheDownloader</span> <span class="p">:</span> <span class="n">WhenTestingBehaviour</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="n">Downloader</span> <span class="n">FeedDownloader</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">protected</span> <span class="n">IWebClient</span> <span class="n">WebClient</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">protected</span> <span class="n">IPodcastFeedFactory</span> <span class="n">FeedFactory</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">protected</span> <span class="n">Uri</span> <span class="n">Address</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">protected</span> <span class="n">IPodcastFeed</span> <span class="n">Feed</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">protected</span> <span class="n">Stream</span> <span class="n">StreamData</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">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">GivenThat</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">base</span><span class="p">.</span><span class="nf">GivenThat</span><span class="p">();</span>

    <span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"http://localhost/fred"</span><span class="p">);</span>
    <span class="n">WebClient</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IWebClient</span><span class="p">&gt;();</span>
    <span class="n">FeedFactory</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IPodcastFeedFactory</span><span class="p">&gt;();</span>
    <span class="n">FeedDownloader</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Downloader</span><span class="p">(</span><span class="n">WebClient</span><span class="p">,</span><span class="n">FeedFactory</span><span class="p">);</span>

    <span class="n">StreamData</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MemoryStream</span><span class="p">();</span>

    <span class="n">WebClient</span><span class="p">.</span><span class="nf">Stub</span><span class="p">(</span><span class="n">client</span> <span class="p">=&gt;</span> <span class="n">client</span><span class="p">.</span><span class="nf">OpenRead</span><span class="p">(</span><span class="n">Address</span><span class="p">)).</span><span class="nf">Return</span><span class="p">(</span><span class="n">StreamData</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">WhenTestingTheDownloaderInRss</span> <span class="p">:</span> <span class="n">WhenTestingTheDownloader</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">When</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">Feed</span> <span class="p">=</span> <span class="n">FeedDownloader</span><span class="p">.</span><span class="nf">DownloadFeed</span><span class="p">(</span><span class="n">PodcastFeedFormat</span><span class="p">.</span><span class="n">RSS</span><span class="p">,</span><span class="n">Address</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="n">Test</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">ItShouldDownloadTheFeed</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">WebClient</span><span class="p">.</span><span class="nf">AssertWasCalled</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="nf">OpenRead</span><span class="p">(</span><span class="n">Address</span><span class="p">));</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="n">Test</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">ItShouldReturnAFeed</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">FeedFactory</span><span class="p">.</span><span class="nf">AssertWasCalled</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="nf">CreatePodcastFeed</span><span class="p">(</span><span class="n">PodcastFeedFormat</span><span class="p">.</span><span class="n">RSS</span><span class="p">,</span> <span class="n">StreamData</span><span class="p">,</span> <span class="k">null</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="moq-test">Moq Test</h3>

<p>As you can see converting it to Moq is really just about translating syntax the meaning of the test is the same.</p>

<figure class="highlight"><pre><code class="language-c#" data-lang="c#"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
</pre></td><td class="code"><pre><span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">WhenTestingBehaviour</span>
<span class="p">{</span>
  <span class="c1">/// &lt;summary&gt;</span>
  <span class="c1">/// Seal the method so it can not be overriden. We want all context to be</span>
  <span class="c1">/// set in the &lt;see cref="GivenThat" /&gt; method.</span>
  <span class="c1">/// &lt;/summary&gt;</span>
  <span class="p">[</span><span class="n">SetUp</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Setup</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="nf">GivenThat</span><span class="p">();</span>

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

  <span class="c1">/// &lt;summary&gt;</span>
  <span class="c1">/// Set up the context of the test.</span>
  <span class="c1">/// &lt;/summary&gt;</span>
  <span class="k">protected</span> <span class="k">virtual</span> <span class="k">void</span> <span class="nf">GivenThat</span><span class="p">()</span>
  <span class="p">{</span>
  <span class="p">}</span>

  <span class="c1">/// &lt;summary&gt;</span>
  <span class="c1">/// Invoke the action being tested.</span>
  <span class="c1">/// &lt;/summary&gt;</span>
  <span class="k">protected</span> <span class="k">abstract</span> <span class="k">void</span> <span class="nf">When</span><span class="p">();</span>

  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">MOCKTYPE</span><span class="p">&gt;</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">MOCKTYPE</span><span class="p">&gt;()</span>
      <span class="k">where</span> <span class="n">MOCKTYPE</span> <span class="p">:</span> <span class="k">class</span>
  <span class="err">{</span>
    <span class="nc">return</span> <span class="k">new</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">MOCKTYPE</span><span class="p">&gt;(</span><span class="n">MockBehavior</span><span class="p">.</span><span class="n">Loose</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">MOCKTYPE</span><span class="p">&gt;</span> <span class="n">GenerateStrictMock</span><span class="p">&lt;</span><span class="n">MOCKTYPE</span><span class="p">&gt;()</span>
      <span class="k">where</span> <span class="n">MOCKTYPE</span> <span class="p">:</span> <span class="k">class</span>
  <span class="err">{</span>
    <span class="nc">return</span> <span class="k">new</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">MOCKTYPE</span><span class="p">&gt;(</span><span class="n">MockBehavior</span><span class="p">.</span><span class="n">Strict</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">abstract</span> <span class="k">class</span> <span class="nc">WhenTestingTheDownloader</span> <span class="p">:</span> <span class="n">WhenTestingBehaviour</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="n">Downloader</span> <span class="n">FeedDownloader</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">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IWebClient</span><span class="p">&gt;</span> <span class="n">WebClient</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">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IPodcastFeedFactory</span><span class="p">&gt;</span> <span class="n">FeedFactory</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">protected</span> <span class="n">Uri</span> <span class="n">Address</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">protected</span> <span class="n">IPodcastFeed</span> <span class="n">Feed</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">protected</span> <span class="n">Stream</span> <span class="n">StreamData</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">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">GivenThat</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">base</span><span class="p">.</span><span class="nf">GivenThat</span><span class="p">();</span>

    <span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"http://localhost/fred"</span><span class="p">);</span>
    <span class="n">WebClient</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IWebClient</span><span class="p">&gt;();</span>
    <span class="n">FeedFactory</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IPodcastFeedFactory</span><span class="p">&gt;();</span>
    <span class="n">FeedDownloader</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Downloader</span><span class="p">(</span><span class="n">WebClient</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span> <span class="n">FeedFactory</span><span class="p">.</span><span class="n">Object</span><span class="p">);</span>

    <span class="n">StreamData</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MemoryStream</span><span class="p">();</span>

    <span class="n">WebClient</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">client</span> <span class="p">=&gt;</span> <span class="n">client</span><span class="p">.</span><span class="nf">OpenRead</span><span class="p">(</span><span class="n">Address</span><span class="p">)).</span><span class="nf">Returns</span><span class="p">(</span><span class="n">StreamData</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">WhenTestingTheDownloaderInRss</span> <span class="p">:</span> <span class="n">WhenTestingTheDownloader</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">When</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">Feed</span> <span class="p">=</span> <span class="n">FeedDownloader</span><span class="p">.</span><span class="nf">DownloadFeed</span><span class="p">(</span><span class="n">PodcastFeedFormat</span><span class="p">.</span><span class="n">RSS</span><span class="p">,</span> <span class="n">Address</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="n">Test</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">ItShouldDownloadTheFeed</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">WebClient</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="nf">OpenRead</span><span class="p">(</span><span class="n">Address</span><span class="p">));</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="n">Test</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">ItShouldReturnAFeed</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">FeedFactory</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="nf">CreatePodcastFeed</span><span class="p">(</span><span class="n">PodcastFeedFormat</span><span class="p">.</span><span class="n">RSS</span><span class="p">,</span> <span class="n">StreamData</span><span class="p">,</span> <span class="k">null</span><span class="p">));</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="a-more-complex-rhinomock-test">A more complex RhinoMock Test</h3>

<p>There were a few complex tests, for example this one needed to ensure that the calls happened in a specific order</p>

<figure class="highlight"><pre><code class="language-c#" data-lang="c#"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
</pre></td><td class="code"><pre><span class="k">public</span> <span class="k">class</span> <span class="nc">WhenThereAreSomePodcastsContainingFilesNeedingSorting</span> <span class="p">:</span> <span class="n">WhenTestingThePlaylistGenerator</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="n">MockRepository</span> <span class="n">mocks</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MockRepository</span><span class="p">();</span>

  <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">GivenThat</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">Playlist</span> <span class="p">=</span> <span class="n">mocks</span><span class="p">.</span><span class="n">DynamicMock</span><span class="p">&lt;</span><span class="n">IPlaylist</span><span class="p">&gt;();</span>

    <span class="k">base</span><span class="p">.</span><span class="nf">GivenThat</span><span class="p">();</span>

    <span class="n">Podcasts</span><span class="p">.</span><span class="nf">Clear</span><span class="p">();</span>
    <span class="n">Podcasts</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">PodcastInfo</span><span class="p">(</span><span class="n">ControlFile</span><span class="p">)</span> <span class="p">{</span> <span class="n">Folder</span> <span class="p">=</span> <span class="s">"Hanselminutes"</span> <span class="p">});</span>
    <span class="n">Podcasts</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">PodcastInfo</span><span class="p">(</span><span class="n">ControlFile</span><span class="p">)</span> <span class="p">{</span> <span class="n">Folder</span> <span class="p">=</span> <span class="s">"This Developers Life"</span> <span class="p">});</span>
    <span class="n">Podcasts</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="n">Pattern</span><span class="p">.</span><span class="n">Value</span> <span class="p">=</span> <span class="s">"*.mp3"</span><span class="p">;</span>
    <span class="n">Podcasts</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="n">Pattern</span><span class="p">.</span><span class="n">Value</span> <span class="p">=</span> <span class="s">"*.wma"</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">podcastFiles1</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="p">{</span><span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;(),</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;()};</span>
    <span class="n">podcastFiles1</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Return</span><span class="p">(</span><span class="s">@"c:\destination\Hanselminutes\001.mp3"</span><span class="p">);</span>
    <span class="n">podcastFiles1</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Return</span><span class="p">(</span><span class="s">@"c:\destination\Hanselminutes\002.mp3"</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">podcastFiles2</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="p">{</span><span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;(),</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;(),</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;()};</span>
    <span class="c1">// add them so they need sorting</span>
    <span class="n">podcastFiles2</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Return</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life\997.wma"</span><span class="p">);</span>
    <span class="n">podcastFiles2</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Return</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life\999.wma"</span><span class="p">);</span>
    <span class="n">podcastFiles2</span><span class="p">[</span><span class="m">2</span><span class="p">].</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Return</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life\998.wma"</span><span class="p">);</span>

    <span class="n">Finder</span><span class="p">.</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="s">@"c:\destination\Hanselminutes"</span><span class="p">,</span> <span class="s">"*.mp3"</span><span class="p">))</span>
      <span class="p">.</span><span class="nf">Return</span><span class="p">(</span><span class="n">podcastFiles1</span><span class="p">);</span>

    <span class="n">Finder</span><span class="p">.</span><span class="nf">Stub</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life"</span><span class="p">,</span> <span class="s">"*.wma"</span><span class="p">))</span>
      <span class="p">.</span><span class="nf">Return</span><span class="p">(</span><span class="n">podcastFiles2</span><span class="p">);</span>

    <span class="k">using</span> <span class="p">(</span><span class="n">mocks</span><span class="p">.</span><span class="nf">Ordered</span><span class="p">())</span>
    <span class="p">{</span>
      <span class="n">Playlist</span><span class="p">.</span><span class="nf">Expect</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||Hanselminutes||001.mp3"</span><span class="p">)).</span><span class="nf">Return</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
      <span class="n">Playlist</span><span class="p">.</span><span class="nf">Expect</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||Hanselminutes||002.mp3"</span><span class="p">)).</span><span class="nf">Return</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
      <span class="n">Playlist</span><span class="p">.</span><span class="nf">Expect</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||997.wma"</span><span class="p">)).</span><span class="nf">Return</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
      <span class="n">Playlist</span><span class="p">.</span><span class="nf">Expect</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||998.wma"</span><span class="p">)).</span><span class="nf">Return</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
      <span class="n">Playlist</span><span class="p">.</span><span class="nf">Expect</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||999.wma"</span><span class="p">)).</span><span class="nf">Return</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">Playlist</span><span class="p">.</span><span class="nf">Replay</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">When</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">PlaylistGenerator</span><span class="p">.</span><span class="nf">GeneratePlaylist</span><span class="p">(</span><span class="n">ControlFile</span><span class="p">,</span><span class="k">false</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="n">Test</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">ItShouldAddAllTheTracksForEachPodcastInTheCorrectOrder</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">Playlist</span><span class="p">.</span><span class="nf">VerifyAllExpectations</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="a-more-complex-moq-test">A more complex Moq Test</h3>

<p>The syntax changes for this change were more divergent but the meaning is still recognisable</p>

<figure class="highlight"><pre><code class="language-c#" data-lang="c#"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
</pre></td><td class="code"><pre><span class="k">public</span> <span class="k">class</span> <span class="nc">WhenThereAreSomePodcastsContainingFilesNeedingSorting</span> <span class="p">:</span> <span class="n">WhenTestingThePlaylistGenerator</span>
<span class="p">{</span>
  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IPlaylist</span><span class="p">&gt;</span> <span class="n">StrictPlaylist</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">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="n">_file1</span><span class="p">;</span>
  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="n">_file2</span><span class="p">;</span>
  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="n">_file3</span><span class="p">;</span>
  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="n">_file4</span><span class="p">;</span>
  <span class="k">protected</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="n">_file5</span><span class="p">;</span>

  <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">GivenThat</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="k">base</span><span class="p">.</span><span class="nf">GivenThat</span><span class="p">();</span>

    <span class="n">StrictPlaylist</span> <span class="p">=</span> <span class="n">GenerateStrictMock</span><span class="p">&lt;</span><span class="n">IPlaylist</span><span class="p">&gt;();</span>
    <span class="n">Factory</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">factory</span> <span class="p">=&gt;</span> <span class="n">factory</span><span class="p">.</span><span class="nf">CreatePlaylist</span><span class="p">(</span><span class="n">It</span><span class="p">.</span><span class="n">IsAny</span><span class="p">&lt;</span><span class="n">PlaylistFormat</span><span class="p">&gt;(),</span> <span class="n">It</span><span class="p">.</span><span class="n">IsAny</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;()))</span>
        <span class="p">.</span><span class="nf">Returns</span><span class="p">(</span><span class="n">StrictPlaylist</span><span class="p">.</span><span class="n">Object</span><span class="p">);</span>

    <span class="n">Podcasts</span><span class="p">.</span><span class="nf">Clear</span><span class="p">();</span>
    <span class="n">Podcasts</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">PodcastInfo</span><span class="p">(</span><span class="n">ControlFile</span><span class="p">.</span><span class="n">Object</span><span class="p">)</span> <span class="p">{</span> <span class="n">Folder</span> <span class="p">=</span> <span class="s">"Hanselminutes"</span> <span class="p">});</span>
    <span class="n">Podcasts</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">PodcastInfo</span><span class="p">(</span><span class="n">ControlFile</span><span class="p">.</span><span class="n">Object</span><span class="p">)</span> <span class="p">{</span> <span class="n">Folder</span> <span class="p">=</span> <span class="s">"This Developers Life"</span> <span class="p">});</span>
    <span class="n">Podcasts</span><span class="p">[</span><span class="m">0</span><span class="p">].</span><span class="n">Pattern</span><span class="p">.</span><span class="n">Value</span> <span class="p">=</span> <span class="s">"*.mp3"</span><span class="p">;</span>
    <span class="n">Podcasts</span><span class="p">[</span><span class="m">1</span><span class="p">].</span><span class="n">Pattern</span><span class="p">.</span><span class="n">Value</span> <span class="p">=</span> <span class="s">"*.wma"</span><span class="p">;</span>

    <span class="n">_file1</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;();</span>
    <span class="n">_file2</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;();</span>
    <span class="n">_file3</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;();</span>
    <span class="n">_file4</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;();</span>
    <span class="n">_file5</span> <span class="p">=</span> <span class="n">GenerateMock</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;();</span>

    <span class="kt">var</span> <span class="n">podcastFiles1</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="p">{</span> <span class="n">_file1</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span> <span class="n">_file2</span><span class="p">.</span><span class="n">Object</span> <span class="p">};</span>
    <span class="n">_file1</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="s">@"c:\destination\Hanselminutes\001.mp3"</span><span class="p">);</span>
    <span class="n">_file2</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="s">@"c:\destination\Hanselminutes\002.mp3"</span><span class="p">);</span>

    <span class="c1">// add them so they need sorting</span>
    <span class="kt">var</span> <span class="n">podcastFiles2</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">IFileInfo</span><span class="p">&gt;</span> <span class="p">{</span> <span class="n">_file3</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span> <span class="n">_file4</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span> <span class="n">_file5</span><span class="p">.</span><span class="n">Object</span> <span class="p">};</span>
    <span class="n">_file3</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life\997.wma"</span><span class="p">);</span>
    <span class="n">_file4</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life\999.wma"</span><span class="p">);</span>
    <span class="n">_file5</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="n">FullName</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life\998.wma"</span><span class="p">);</span>

    <span class="n">Finder</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="s">@"c:\destination\Hanselminutes"</span><span class="p">,</span> <span class="s">"*.mp3"</span><span class="p">))</span>
      <span class="p">.</span><span class="nf">Returns</span><span class="p">(</span><span class="n">podcastFiles1</span><span class="p">);</span>

    <span class="n">Finder</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">f</span> <span class="p">=&gt;</span> <span class="n">f</span><span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="s">@"c:\destination\This Developers Life"</span><span class="p">,</span> <span class="s">"*.wma"</span><span class="p">))</span>
      <span class="p">.</span><span class="nf">Returns</span><span class="p">(</span><span class="n">podcastFiles2</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">sequence</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MockSequence</span><span class="p">();</span>
    <span class="c1">// Create the expectations, notice that the Setup is called via InSequence</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">InSequence</span><span class="p">(</span><span class="n">sequence</span><span class="p">).</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||Hanselminutes||001.mp3"</span><span class="p">)).</span><span class="nf">Returns</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">InSequence</span><span class="p">(</span><span class="n">sequence</span><span class="p">).</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||Hanselminutes||002.mp3"</span><span class="p">)).</span><span class="nf">Returns</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">InSequence</span><span class="p">(</span><span class="n">sequence</span><span class="p">).</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||997.wma"</span><span class="p">)).</span><span class="nf">Returns</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">InSequence</span><span class="p">(</span><span class="n">sequence</span><span class="p">).</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||998.wma"</span><span class="p">)).</span><span class="nf">Returns</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">InSequence</span><span class="p">(</span><span class="n">sequence</span><span class="p">).</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||999.wma"</span><span class="p">)).</span><span class="nf">Returns</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>

    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">SetupGet</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">NumberOfTracks</span><span class="p">).</span><span class="nf">Returns</span><span class="p">(</span><span class="m">5</span><span class="p">);</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">SaveFile</span><span class="p">(</span><span class="s">@"c:\file.tmp"</span><span class="p">));</span>
  <span class="p">}</span>

  <span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">When</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="n">PlaylistGenerator</span><span class="p">.</span><span class="nf">GeneratePlaylist</span><span class="p">(</span><span class="n">ControlFile</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="p">[</span><span class="n">Test</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">ItShouldAddAllTheTracksForEachPodcastInTheCorrectOrder</span><span class="p">()</span>
  <span class="p">{</span>
    <span class="c1">// the verification order does not matter - its the setup order that counts</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||Hanselminutes||001.mp3"</span><span class="p">),</span> <span class="n">Times</span><span class="p">.</span><span class="nf">Once</span><span class="p">());</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||Hanselminutes||002.mp3"</span><span class="p">),</span> <span class="n">Times</span><span class="p">.</span><span class="nf">Once</span><span class="p">());</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||997.wma"</span><span class="p">),</span> <span class="n">Times</span><span class="p">.</span><span class="nf">Once</span><span class="p">());</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||998.wma"</span><span class="p">),</span> <span class="n">Times</span><span class="p">.</span><span class="nf">Once</span><span class="p">());</span>
    <span class="n">StrictPlaylist</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">AddTrack</span><span class="p">(</span><span class="s">@".||This Developers Life||999.wma"</span><span class="p">),</span> <span class="n">Times</span><span class="p">.</span><span class="nf">Once</span><span class="p">());</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h2 id="multiple-targets-in-the-test-assembly-project">Multiple targets in the test assembly project</h2>

<p><code class="language-plaintext highlighter-rouge">PodcastUtilities.Common.DLL</code>, the assembly under test, is slightly different when built for .NETFramework. .NETFramework supports MTP via P/Invoke whereas the .NETCore build does not. This means that there are between 20 and 30 extra tests for the .NETFramework version. We would like those tests to be run when the tests are run on .NETFramework.</p>

<p>Initially I created a test assembly in VS2022 that targetted .NETCore. To convert it to target both .NETCore and .NETFramework the <code class="language-plaintext highlighter-rouge">.csproj</code> file was changed like this</p>

<p>Change</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;TargetFramework&gt;netcoreapp3.1&lt;/TargetFramework&gt;
</code></pre></div></div>

<p>to be</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;TargetFrameworks&gt;net462;netcoreapp3.1&lt;/TargetFrameworks&gt;
</code></pre></div></div>

<p>There may be a way of doing this in UI but I just edited the <code class="language-plaintext highlighter-rouge">.csproj</code> file.</p>

<p>We also added the following section into the <code class="language-plaintext highlighter-rouge">.csproj</code> file.</p>

<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre>  <span class="c">&lt;!-- .NET Core 3.1 references, compilation flags and build options --&gt;</span>
  <span class="nt">&lt;PropertyGroup</span> <span class="na">Condition=</span><span class="s">" '$(TargetFramework)' == 'netcoreapp3.1'"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;DefineConstants&gt;</span>NETCORE;NETCORE3_1<span class="nt">&lt;/DefineConstants&gt;</span>
  <span class="nt">&lt;/PropertyGroup&gt;</span>

  <span class="nt">&lt;ItemGroup</span> <span class="na">Condition=</span><span class="s">" '$(TargetFramework)' == 'netcoreapp3.1'"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;Compile</span> <span class="na">Remove=</span><span class="s">".\Platform\FileSystemAwareFileUtilitiesTests\**\*.cs"</span> <span class="na">Label=</span><span class="s">"NO_MTP"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;Compile</span> <span class="na">Remove=</span><span class="s">".\Platform\MtpTests\**\*.cs"</span> <span class="na">Label=</span><span class="s">"NO_MTP"</span> <span class="nt">/&gt;</span>
  <span class="nt">&lt;/ItemGroup&gt;</span>

  <span class="c">&lt;!-- .NET references, compilation flags and build options --&gt;</span>
  <span class="nt">&lt;PropertyGroup</span> <span class="na">Condition=</span><span class="s">" '$(TargetFramework)' == 'net462'"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;DefineConstants&gt;</span>NET462;NETFULL<span class="nt">&lt;/DefineConstants&gt;</span>
  <span class="nt">&lt;/PropertyGroup&gt;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>In the .NETCore target we remove all the tests from the <code class="language-plaintext highlighter-rouge">FileSystemAwareFileUtilitiesTests</code> and the <code class="language-plaintext highlighter-rouge">MtpTests</code> folders, as those tests cannot be run (or even compiled) on that platform as they are MTP tests.</p>

<p>By adding the <code class="language-plaintext highlighter-rouge">DefineConstants</code> we can write code for a specific platform in C# like this</p>

<figure class="highlight"><pre><code class="language-c#" data-lang="c#"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="code"><pre><span class="cp">#if NETFULL
</span>  <span class="nf">CodeThatCanOnlyRunOnWindowsDotNetFramework</span><span class="p">()</span>
<span class="err">#</span><span class="n">endif</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>You can see the complete code for the project including the tests <a href="https://github.com/derekwilson/PodcastUtilities">in Github</a></p>]]></content><author><name></name></author><category term="Development" /><category term="General" /><category term="PodcastUtilities" /><category term=".Net" /><category term="Development" /><category term="General" /><category term="PodcastUtilities" /><category term=".Net" /><summary type="html"><![CDATA[Previously I wrote about porting PodcastUtilities to .NETCore, that is, producing a DLL project that can build multiple platform targets, one for the cross platform .NET Standard and one for the Windows only .NET Framework. At the time we understood that we had incurred some tech debit, in that the tests still only ran on .NET Framework due to their use of RhinoMocks. RhinoMocks is no longer maintained and does not support .NETCore.]]></summary></entry><entry><title type="html">Recommender Updated</title><link href="https://derekwilson.github.io//blog/2023/02/28/recommender-updated" rel="alternate" type="text/html" title="Recommender Updated" /><published>2023-02-28T12:00:00+00:00</published><updated>2023-02-28T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2023/02/28/recommender-updated</id><content type="html" xml:base="https://derekwilson.github.io//blog/2023/02/28/recommender-updated"><![CDATA[<p>Its been six years since I updated Recommender. Not really because I have abandoned it but rather it was pretty complete. However unlike other platforms Android does seem to be in a constant state of churn which has meant that there were some issues starting to surface</p>

<ol>
  <li>It targetted SDK 23 and this is <a href="https://support.google.com/googleplay/android-developer/answer/11926878?hl=en">no longer allowed and not surfaced by the play store</a></li>
  <li>The legacy Android support library didnt work properly with newer devices, it did not use the whole screen</li>
  <li>The <a href="https://developer.android.com/about/versions/11/privacy/storage">Android file system permissions</a> have breaking changes in them</li>
  <li>The share mechanism also has <a href="https://developer.android.com/reference/android/os/FileUriExposedException">breaking changes</a></li>
</ol>

<p>To address these issues I have</p>

<ol>
  <li>Targeted SDK 31</li>
  <li>Use the latest Jetpack libraries</li>
  <li>Move exporting, importing, sharing and logging to use the app public folder <code class="language-plaintext highlighter-rouge">/sdcard/Android/data/net.derekwilson.recommender</code></li>
  <li>Enable the user to select a file using <a href="https://developer.android.com/guide/topics/providers/document-provider">SAF</a>, handily enough now Google has decided to not allow opening from the app public folder</li>
  <li>Only share using content providers</li>
</ol>

<p>I have also made a few changes to try and isolate the app from Googles endless churn</p>

<ol>
  <li>Removed all references to Google play services, migrated to using AppCenter for crashes and analytics</li>
  <li>Target and deploy using <a href="https://play.google.com/store/apps/details?id=net.derekwilson.recommender">Google Play Store</a> as well as <a href="https://www.amazon.com/gp/product/B0BVPH5YMY">Amazon App Store</a>, Amazon do not have the same restrictions on target SDKs</li>
</ol>

<p>I also made a few minor changes</p>

<ol>
  <li>Re-tweaked the share decoder from pages on the Amazon web site</li>
  <li>Added a decoder for goodreads.com</li>
  <li>Tweaked the backup/restore process to make it more flexible</li>
</ol>

<p>The <a href="https://bitbucket.org/derekwilson/recommender-android">source code</a> is in bitbucket.</p>]]></content><author><name></name></author><category term="Android" /><category term="Recommender" /><category term="Development" /><category term="Mobile" /><category term="Android" /><category term="Recommender" /><category term="Development" /><category term="Mobile" /><summary type="html"><![CDATA[Its been six years since I updated Recommender. Not really because I have abandoned it but rather it was pretty complete. However unlike other platforms Android does seem to be in a constant state of churn which has meant that there were some issues starting to surface]]></summary></entry><entry><title type="html">Xamarin Android Part 8</title><link href="https://derekwilson.github.io//blog/2023/01/22/xamarin-android-part8" rel="alternate" type="text/html" title="Xamarin Android Part 8" /><published>2023-01-22T12:00:00+00:00</published><updated>2023-01-22T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2023/01/22/xamarin-android-part8</id><content type="html" xml:base="https://derekwilson.github.io//blog/2023/01/22/xamarin-android-part8"><![CDATA[<h1 id="custom-views-in-xamarin-android-applications">Custom Views in Xamarin Android Applications</h1>

<p>This post is part of a <a href="/blog/2021/12/28/xamarin-android-part1">series of posts</a> exploring writing apps for Android using <a href="https://docs.microsoft.com/en-us/xamarin/android/">Xamarin Android</a>.</p>

<p>I have found that the extra complexity of using fragments, with their similar but subtly different lifecycle from activities, to not really be worth the effort. So I tend to compose activities from custom views instead. One view I have used in multiple apps is a custom view to show the progress of a task.</p>

<p>As we have seen in Xamarin Android, the view layouts use the same XML as writing apps using Kotlin of Java and this custom view is no excpetion. The layout for the control looks like this</p>

<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
</pre></td><td class="code"><pre><span class="nt">&lt;LinearLayout</span>
	<span class="na">android:orientation=</span><span class="s">"vertical"</span>
	<span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
	<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
	<span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span><span class="nt">&gt;</span>
	<span class="nt">&lt;TextView</span>
		<span class="na">android:id=</span><span class="s">"@+id/progress_bar_message"</span>
		<span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
		<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
		<span class="na">android:paddingTop=</span><span class="s">"5dp"</span>
		<span class="na">android:textAlignment=</span><span class="s">"center"</span>
		<span class="na">android:background=</span><span class="s">"@color/primary"</span>
		<span class="na">android:alpha=</span><span class="s">"0.8"</span>
		<span class="na">android:textColor=</span><span class="s">"@color/background"</span>
		<span class="na">android:text=</span><span class="s">"@string/placeholder"</span>
		<span class="na">android:gravity=</span><span class="s">"center_horizontal"</span> <span class="nt">/&gt;</span>
	<span class="nt">&lt;ProgressBar</span>
		<span class="na">android:id=</span><span class="s">"@+id/indeterminateBar"</span>
		<span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
		<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
		<span class="na">android:paddingTop=</span><span class="s">"5dp"</span>
		<span class="na">android:paddingBottom=</span><span class="s">"5dp"</span>
		<span class="na">android:background=</span><span class="s">"@color/primary"</span>
		<span class="na">android:alpha=</span><span class="s">"0.5"</span>
		<span class="na">android:indeterminate=</span><span class="s">"true"</span>
		<span class="nt">/&gt;</span>
	<span class="nt">&lt;ProgressBar</span>
		<span class="na">android:id=</span><span class="s">"@+id/steppedBar"</span>
		<span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
		<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
		<span class="na">android:background=</span><span class="s">"@color/primary"</span>
		<span class="na">android:alpha=</span><span class="s">"0.8"</span>
		<span class="na">android:max=</span><span class="s">"10"</span>
		<span class="na">android:progress=</span><span class="s">"5"</span>
		<span class="na">style=</span><span class="s">"?android:attr/progressBarStyleHorizontal"</span>
		<span class="na">android:indeterminate=</span><span class="s">"false"</span>
		<span class="nt">/&gt;</span>
<span class="nt">&lt;/LinearLayout&gt;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>I used the same layout when implementing the view in Kotlin and C#</p>

<h3 id="a-progress-custom-view-in-kotlin">A progress custom view in Kotlin</h3>

<p>The implementation in Kotlin is pretty standard android development</p>

<figure class="highlight"><pre><code class="language-kotlin" data-lang="kotlin"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
</pre></td><td class="code"><pre><span class="kd">class</span> <span class="nc">ProgressSpinnerView</span> <span class="p">:</span> <span class="nc">LinearLayout</span> <span class="p">{</span>

    <span class="nd">@BindView</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">progress_bar_message</span><span class="p">)</span>
    <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">messageView</span><span class="p">:</span> <span class="nc">TextView</span>

    <span class="nd">@BindView</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">indeterminateBar</span><span class="p">)</span>
    <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">indeterminateBar</span><span class="p">:</span> <span class="nc">ProgressBar</span>

    <span class="nd">@BindView</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">id</span><span class="p">.</span><span class="n">steppedBar</span><span class="p">)</span>
    <span class="k">lateinit</span> <span class="kd">var</span> <span class="py">steppedBar</span><span class="p">:</span> <span class="nc">ProgressBar</span>

    <span class="k">constructor</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">)</span> <span class="p">:</span> <span class="k">super</span><span class="p">(</span><span class="n">context</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">init</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">constructor</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">:</span> <span class="nc">AttributeSet</span><span class="p">)</span> <span class="p">:</span> <span class="k">super</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">init</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">constructor</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">:</span> <span class="nc">AttributeSet</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">:</span> <span class="k">super</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">init</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">var</span> <span class="py">message</span><span class="p">:</span> <span class="nc">String</span><span class="p">?</span> <span class="p">=</span> <span class="k">null</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">field</span> <span class="p">=</span> <span class="n">value</span>
            <span class="n">messageView</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">value</span>
        <span class="p">}</span>

    <span class="kd">var</span> <span class="py">max</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">1</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">field</span> <span class="p">=</span> <span class="n">value</span>
            <span class="n">steppedBar</span><span class="p">.</span><span class="n">max</span> <span class="p">=</span> <span class="n">value</span>
        <span class="p">}</span>

    <span class="kd">var</span> <span class="py">progress</span><span class="p">:</span> <span class="nc">Int</span> <span class="p">=</span> <span class="mi">0</span>
        <span class="k">set</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">field</span> <span class="p">=</span> <span class="n">value</span>
            <span class="n">steppedBar</span><span class="p">.</span><span class="n">progress</span> <span class="p">=</span> <span class="n">value</span>
        <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">init</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">:</span> <span class="nc">AttributeSet</span><span class="p">?,</span> <span class="n">defStyle</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">view</span> <span class="p">=</span> <span class="nf">inflateView</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
        <span class="nc">ButterKnife</span><span class="p">.</span><span class="nf">bind</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">view</span><span class="p">)</span>

        <span class="nf">loadAttributes</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">loadAttributes</span><span class="p">(</span><span class="n">attrs</span><span class="p">:</span> <span class="nc">AttributeSet</span><span class="p">?,</span> <span class="n">defStyle</span><span class="p">:</span> <span class="nc">Int</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">a</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">obtainStyledAttributes</span><span class="p">(</span>
                <span class="n">attrs</span><span class="p">,</span> <span class="nc">R</span><span class="p">.</span><span class="n">styleable</span><span class="p">.</span><span class="nc">ProgressSpinnerView</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>

        <span class="n">message</span> <span class="p">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">getString</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">styleable</span><span class="p">.</span><span class="nc">ProgressSpinnerView_message</span><span class="p">)</span>

        <span class="n">a</span><span class="p">.</span><span class="nf">recycle</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">fun</span> <span class="nf">inflateView</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="nc">Context</span><span class="p">):</span> <span class="nc">View</span> <span class="p">{</span>
        <span class="kd">val</span> <span class="py">inflater</span><span class="p">:</span> <span class="nc">LayoutInflater</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">getSystemService</span><span class="p">(</span><span class="nc">Context</span><span class="p">.</span><span class="nc">LAYOUT_INFLATER_SERVICE</span><span class="p">)</span> <span class="k">as</span> <span class="nc">LayoutInflater</span>
        <span class="k">return</span> <span class="n">inflater</span><span class="p">.</span><span class="nf">inflate</span><span class="p">(</span><span class="nc">R</span><span class="p">.</span><span class="n">layout</span><span class="p">.</span><span class="n">view_progress_spinner</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="k">true</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">slideDown</span><span class="p">(</span><span class="n">indeterminateProgress</span><span class="p">:</span> <span class="nc">Boolean</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">indeterminateBar</span><span class="p">.</span><span class="n">visibility</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">indeterminateProgress</span><span class="p">)</span> <span class="nc">View</span><span class="p">.</span><span class="nc">VISIBLE</span> <span class="k">else</span> <span class="nc">View</span><span class="p">.</span><span class="nc">GONE</span>
        <span class="n">steppedBar</span><span class="p">.</span><span class="n">visibility</span> <span class="p">=</span> <span class="k">if</span> <span class="p">(</span><span class="n">indeterminateProgress</span><span class="p">)</span> <span class="nc">View</span><span class="p">.</span><span class="nc">GONE</span> <span class="k">else</span> <span class="nc">View</span><span class="p">.</span><span class="nc">VISIBLE</span>
        <span class="n">visibility</span> <span class="p">=</span> <span class="nc">View</span><span class="p">.</span><span class="nc">VISIBLE</span>
        <span class="kd">val</span> <span class="py">animate</span> <span class="p">=</span> <span class="nc">TranslateAnimation</span><span class="p">(</span>
                <span class="mf">0f</span><span class="p">,</span>                  <span class="c1">// fromXDelta</span>
                <span class="mf">0f</span><span class="p">,</span>                  <span class="c1">// toXDelta</span>
                <span class="p">-</span><span class="n">height</span><span class="p">.</span><span class="nf">toFloat</span><span class="p">(),</span>   <span class="c1">// fromYDelta</span>
                <span class="mf">0f</span><span class="p">)</span>                  <span class="c1">// toYDelta</span>
        <span class="n">animate</span><span class="p">.</span><span class="n">duration</span> <span class="p">=</span> <span class="mi">500</span>
        <span class="nf">clearAnimation</span><span class="p">()</span>
        <span class="nf">startAnimation</span><span class="p">(</span><span class="n">animate</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">fun</span> <span class="nf">slideUp</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">visibility</span> <span class="p">=</span> <span class="nc">View</span><span class="p">.</span><span class="nc">GONE</span>
        <span class="kd">val</span> <span class="py">animate</span> <span class="p">=</span> <span class="nc">TranslateAnimation</span><span class="p">(</span>
                <span class="mf">0f</span><span class="p">,</span>                  <span class="c1">// fromXDelta</span>
                <span class="mf">0f</span><span class="p">,</span>                  <span class="c1">// toXDelta</span>
                <span class="mf">0f</span><span class="p">,</span>                  <span class="c1">// fromYDelta</span>
                <span class="p">-</span><span class="n">height</span><span class="p">.</span><span class="nf">toFloat</span><span class="p">())</span>   <span class="c1">// toYDelta</span>
        <span class="n">animate</span><span class="p">.</span><span class="n">duration</span> <span class="p">=</span> <span class="mi">500</span>
        <span class="nf">clearAnimation</span><span class="p">()</span>
        <span class="nf">startAnimation</span><span class="p">(</span><span class="n">animate</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="a-progress-custom-view-in-c">A progress custom view in C#</h3>

<p>In C# the code is pretty much the same, with the slight differences in the syntax.</p>

<figure class="highlight"><pre><code class="language-c#" data-lang="c#"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
</pre></td><td class="code"><pre><span class="k">namespace</span> <span class="nn">PodcastUtilities.AndroidLogic.CustomViews</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ProgressSpinnerView</span> <span class="p">:</span> <span class="n">LinearLayout</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="n">TextView</span> <span class="n">messageView</span><span class="p">;</span>
    <span class="k">private</span> <span class="n">ProgressBar</span> <span class="n">indeterminateBar</span><span class="p">;</span>
    <span class="k">private</span> <span class="n">ProgressBar</span> <span class="n">steppedBar</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">ProgressSpinnerView</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span><span class="n">context</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">Init</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="nf">ProgressSpinnerView</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">,</span> <span class="n">IAttributeSet</span> <span class="n">attrs</span><span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">Init</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="nf">ProgressSpinnerView</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">,</span> <span class="n">IAttributeSet</span> <span class="n">attrs</span><span class="p">,</span> <span class="kt">int</span> <span class="n">defStyleAttr</span><span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="n">defStyleAttr</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">Init</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">attrs</span><span class="p">,</span> <span class="n">defStyleAttr</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">Message</span>
    <span class="p">{</span>
        <span class="k">set</span>
        <span class="p">{</span>
            <span class="n">messageView</span><span class="p">.</span><span class="n">Text</span> <span class="p">=</span> <span class="k">value</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kt">int</span> <span class="n">Max</span>
    <span class="p">{</span>
        <span class="k">set</span>
        <span class="p">{</span>
            <span class="n">steppedBar</span><span class="p">.</span><span class="n">Max</span> <span class="p">=</span> <span class="k">value</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kt">int</span> <span class="n">Progress</span>
    <span class="p">{</span>
        <span class="k">set</span>
        <span class="p">{</span>
            <span class="n">steppedBar</span><span class="p">.</span><span class="n">Progress</span> <span class="p">=</span> <span class="k">value</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">void</span> <span class="nf">Init</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">,</span> <span class="n">IAttributeSet</span> <span class="n">attrs</span><span class="p">,</span> <span class="kt">int</span> <span class="n">defStyle</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">view</span> <span class="p">=</span> <span class="nf">InflateView</span><span class="p">(</span><span class="n">context</span><span class="p">);</span>
        <span class="n">messageView</span> <span class="p">=</span> <span class="n">FindViewById</span><span class="p">&lt;</span><span class="n">TextView</span><span class="p">&gt;(</span><span class="n">Resource</span><span class="p">.</span><span class="n">Id</span><span class="p">.</span><span class="n">progress_bar_message</span><span class="p">);</span>
        <span class="n">indeterminateBar</span> <span class="p">=</span> <span class="n">FindViewById</span><span class="p">&lt;</span><span class="n">ProgressBar</span><span class="p">&gt;(</span><span class="n">Resource</span><span class="p">.</span><span class="n">Id</span><span class="p">.</span><span class="n">indeterminateBar</span><span class="p">);</span>
        <span class="n">steppedBar</span> <span class="p">=</span> <span class="n">FindViewById</span><span class="p">&lt;</span><span class="n">ProgressBar</span><span class="p">&gt;(</span><span class="n">Resource</span><span class="p">.</span><span class="n">Id</span><span class="p">.</span><span class="n">steppedBar</span><span class="p">);</span>

        <span class="nf">LoadAttributes</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">void</span> <span class="nf">LoadAttributes</span><span class="p">(</span><span class="n">IAttributeSet</span> <span class="n">attrs</span><span class="p">,</span> <span class="kt">int</span> <span class="n">defStyle</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">a</span> <span class="p">=</span> <span class="n">Context</span><span class="p">.</span><span class="nf">ObtainStyledAttributes</span><span class="p">(</span><span class="n">attrs</span><span class="p">,</span> <span class="n">Resource</span><span class="p">.</span><span class="n">Styleable</span><span class="p">.</span><span class="n">ProgressSpinnerView</span><span class="p">,</span> <span class="n">defStyle</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span>

        <span class="n">Message</span> <span class="p">=</span> <span class="n">a</span><span class="p">.</span><span class="nf">GetString</span><span class="p">(</span><span class="n">Resource</span><span class="p">.</span><span class="n">Styleable</span><span class="p">.</span><span class="n">ProgressSpinnerView_message</span><span class="p">);</span>

        <span class="n">a</span><span class="p">.</span><span class="nf">Recycle</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="n">View</span> <span class="nf">InflateView</span><span class="p">(</span><span class="n">Context</span> <span class="n">context</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">LayoutInflater</span> <span class="n">inflater</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">GetSystemService</span><span class="p">(</span><span class="n">Context</span><span class="p">.</span><span class="n">LayoutInflaterService</span><span class="p">)</span> <span class="k">as</span> <span class="n">LayoutInflater</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">inflater</span><span class="p">.</span><span class="nf">Inflate</span><span class="p">(</span><span class="n">Resource</span><span class="p">.</span><span class="n">Layout</span><span class="p">.</span><span class="n">view_progress_spinner</span><span class="p">,</span> <span class="k">this</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">SlideDown</span><span class="p">(</span><span class="kt">bool</span> <span class="n">indeterminateProgress</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">indeterminateBar</span><span class="p">.</span><span class="n">Visibility</span> <span class="p">=</span> <span class="n">indeterminateProgress</span> <span class="p">?</span> <span class="n">ViewStates</span><span class="p">.</span><span class="n">Visible</span> <span class="p">:</span> <span class="n">ViewStates</span><span class="p">.</span><span class="n">Gone</span><span class="p">;</span>
        <span class="n">steppedBar</span><span class="p">.</span><span class="n">Visibility</span> <span class="p">=</span> <span class="n">indeterminateProgress</span> <span class="p">?</span> <span class="n">ViewStates</span><span class="p">.</span><span class="n">Gone</span> <span class="p">:</span> <span class="n">ViewStates</span><span class="p">.</span><span class="n">Visible</span><span class="p">;</span>
        <span class="k">this</span><span class="p">.</span><span class="n">Visibility</span> <span class="p">=</span> <span class="n">ViewStates</span><span class="p">.</span><span class="n">Visible</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">animate</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">TranslateAnimation</span><span class="p">(</span>
            <span class="m">0f</span><span class="p">,</span>             <span class="c1">// fromXDelta</span>
            <span class="m">0f</span><span class="p">,</span>             <span class="c1">// toXDelta</span>
            <span class="p">-</span><span class="k">this</span><span class="p">.</span><span class="n">Height</span><span class="p">,</span>   <span class="c1">// fromYDelta</span>
            <span class="m">0f</span><span class="p">);</span>            <span class="c1">// toYDelta</span>
        <span class="n">animate</span><span class="p">.</span><span class="n">Duration</span> <span class="p">=</span> <span class="m">500</span><span class="p">;</span>
        <span class="nf">ClearAnimation</span><span class="p">();</span>
        <span class="nf">StartAnimation</span><span class="p">(</span><span class="n">animate</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">SlideUp</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="n">Visibility</span> <span class="p">=</span> <span class="n">ViewStates</span><span class="p">.</span><span class="n">Gone</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">animate</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">TranslateAnimation</span><span class="p">(</span>
            <span class="m">0f</span><span class="p">,</span>             <span class="c1">// fromXDelta</span>
            <span class="m">0f</span><span class="p">,</span>             <span class="c1">// toXDelta</span>
            <span class="m">0f</span><span class="p">,</span>             <span class="c1">// fromYDelta</span>
            <span class="p">-</span><span class="k">this</span><span class="p">.</span><span class="n">Height</span><span class="p">);</span>  <span class="c1">// toYDelta</span>
        <span class="n">animate</span><span class="p">.</span><span class="n">Duration</span> <span class="p">=</span> <span class="m">500</span><span class="p">;</span>
        <span class="nf">ClearAnimation</span><span class="p">();</span>
        <span class="nf">StartAnimation</span><span class="p">(</span><span class="n">animate</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></figure>

<h3 id="using-the-custom-view">Using the custom view</h3>

<p>We need to mark the <code class="language-plaintext highlighter-rouge">message</code> property as stylable so it can be set in XML. I do this by creating a file called <code class="language-plaintext highlighter-rouge">attrs_progress_spinner_view.xml</code> in the <code class="language-plaintext highlighter-rouge">values</code> folder of the resources.</p>

<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="code"><pre><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="nt">&lt;resources&gt;</span>
 <span class="nt">&lt;declare-styleable</span> <span class="na">name=</span><span class="s">"ProgressSpinnerView"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;attr</span> <span class="na">name=</span><span class="s">"message"</span> <span class="na">format=</span><span class="s">"string"</span><span class="nt">/&gt;</span>
 <span class="nt">&lt;/declare-styleable&gt;</span>
<span class="nt">&lt;/resources&gt;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>Then to use the custom view we can simply include it in a layout like this</p>

<figure class="highlight"><pre><code class="language-xml" data-lang="xml"><table class="rouge-table"><tbody><tr><td class="gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="code"><pre><span class="nt">&lt;PodcastUtilities.AndroidLogic.CustomViews.ProgressSpinnerView</span>
    <span class="na">android:id=</span><span class="s">"@+id/progressBar"</span>
    <span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
    <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
    <span class="na">app:layout_constraintTop_toTopOf=</span><span class="s">"parent"</span>
    <span class="na">android:elevation=</span><span class="s">"2dp"</span>
    <span class="na">android:visibility=</span><span class="s">"gone"</span>
    <span class="na">app:message=</span><span class="s">"@string/finding_podcasts_progress"</span>
    <span class="nt">&gt;</span>
<span class="nt">&lt;/PodcastUtilities.AndroidLogic.CustomViews.ProgressSpinnerView&gt;</span>
</pre></td></tr></tbody></table></code></pre></figure>

<p>And it looks like this</p>

<p><a href="/images/jekyll/2023-01-01/screen1.png">
    <img title="Custom View" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Custom View" src="/images/jekyll/2023-01-01/screen1.png" width="600" height="200" />
</a></p>

<p>There is a slight gotcha when using Xamarin Android, <a href="https://stackoverflow.com/questions/17445550/how-to-construct-custom-views-in-xamarin">according to stackoverflow</a> the XML element name should be the name of the class including the full namespace. Sometimes this needs to be in lower case and sometimes the case needs to match what was specified in  the C# class file. I have not worked out the pattern, if you get an inflation error then try playing with the namespace in the XML element.</p>

<p>Also using custom views tends to blow up the designer in Visual Studio.</p>]]></content><author><name></name></author><category term="Xamarin" /><category term="Development" /><category term="Android" /><category term=".Net" /><category term="PodcastUtilities" /><category term="Mobile" /><category term="Xamarin" /><category term="Development" /><category term="Android" /><category term=".Net" /><category term="PodcastUtilities" /><category term="Mobile" /><summary type="html"><![CDATA[Custom Views in Xamarin Android Applications]]></summary></entry><entry><title type="html">PassTheParcel released</title><link href="https://derekwilson.github.io//blog/2022/12/29/passtheparcel-released" rel="alternate" type="text/html" title="PassTheParcel released" /><published>2022-12-29T12:00:00+00:00</published><updated>2022-12-29T12:00:00+00:00</updated><id>https://derekwilson.github.io//blog/2022/12/29/passtheparcel-released</id><content type="html" xml:base="https://derekwilson.github.io//blog/2022/12/29/passtheparcel-released"><![CDATA[<p>This month I have released <a href="https://bitbucket.org/derekwilson/passtheparcel/src/master/">PassTheParcel</a>. PassTheParcel is a simple, quick and easy to use app to play music for “Pass The Parcel” or “Musical Chair” type games.</p>

<p><a href="/images/jekyll/2022-12-01/screen1.png">
    <img title="Screen" style="border-top: 0px; border-right: 0px; background-image: none; border-bottom: 0px; padding-top: 0px; padding-left: 0px; border-left: 0px; display: inline; padding-right: 0px" border="0" alt="Screen" src="/images/jekyll/2022-12-01/screen1.png" width="150" height="300" />
</a></p>

<p>Its designed to do a simple task</p>

<ul>
  <li>Select a music media file from your device’s storage</li>
  <li>Optionally select the minimum and maximum length of time to play the music each time the Start button is pressed.</li>
  <li>Start the music - it will automatically stop after a random number of seconds between the limits</li>
  <li>After the music is stopped press start again to play the next section</li>
</ul>

<p>Benefits</p>

<ul>
  <li>You can select any music media stored on your device</li>
  <li>As it randomly stops the person using the app can join in the game</li>
  <li>You can take as long as you want to unwrap the parcel as the music will not start again until the start button is pressed</li>
  <li>There are no adverts</li>
</ul>

<p>PassTheParcel is available on <a href="https://play.google.com/store/apps/details?id=net.derekwilson.passtheparcel">Gooogle Play</a>, it can be <a href="https://bitbucket.org/derekwilson/passtheparcel/src/master/Support/Releases/">side-loaded from the Bitbucket repo</a>, as well as <a href="https://www.amazon.com/Derek-Wilson-PassTheParcel/dp/B0BQQ9GJNV/">installed from the Amazon Appstore</a>.</p>

<p>The <a href="https://bitbucket.org/derekwilson/passtheparcel/src/master/">source code</a> is in Bitbucket.</p>]]></content><author><name></name></author><category term="Android" /><category term="Development" /><category term="PassTheParcel" /><category term="Kotlin" /><category term="Android" /><category term="Development" /><category term="PassTheParcel" /><category term="Kotlin" /><summary type="html"><![CDATA[This month I have released PassTheParcel. PassTheParcel is a simple, quick and easy to use app to play music for “Pass The Parcel” or “Musical Chair” type games.]]></summary></entry></feed>